mirror of
https://github.com/kevin-DL/revel-cmd.git
synced 2026-01-11 18:54:31 +00:00
Reformat of code Allow user to use a mix of command line arguments and flags Enhance the import tool to detect missing packages in the modules side Added test cases for all commands
245 lines
6.9 KiB
Go
245 lines
6.9 KiB
Go
// Copyright (c) 2012-2016 The Revel Framework Authors, All rights reserved.
|
|
// Revel Framework source code and usage is governed by a MIT style
|
|
// license that can be found in the LICENSE file.
|
|
|
|
package parser
|
|
|
|
// This file handles the app code introspection.
|
|
// It catalogs the controllers, their methods, and their arguments.
|
|
|
|
import (
|
|
"go/ast"
|
|
"go/parser"
|
|
"go/scanner"
|
|
"go/token"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
|
|
"github.com/revel/cmd/model"
|
|
"github.com/revel/cmd/utils"
|
|
)
|
|
|
|
// A container used to support the reflection package
|
|
type processContainer struct {
|
|
root, rootImportPath string // The paths
|
|
paths *model.RevelContainer // The Revel paths
|
|
srcInfo *model.SourceInfo // The source information container
|
|
}
|
|
|
|
// Maps a controller simple name (e.g. "Login") to the methods for which it is a
|
|
// receiver.
|
|
type methodMap map[string][]*model.MethodSpec
|
|
|
|
// ProcessSource parses the app controllers directory and
|
|
// returns a list of the controller types found.
|
|
// Otherwise CompileError if the parsing fails.
|
|
func ProcessSource(paths *model.RevelContainer) (_ *model.SourceInfo, compileError error) {
|
|
pc := &processContainer{paths: paths}
|
|
for _, root := range paths.CodePaths {
|
|
rootImportPath := importPathFromPath(root)
|
|
if rootImportPath == "" {
|
|
utils.Logger.Info("Skipping empty code path", "path", root)
|
|
continue
|
|
}
|
|
pc.root, pc.rootImportPath = root, rootImportPath
|
|
|
|
// Start walking the directory tree.
|
|
compileError = utils.Walk(root, pc.processPath)
|
|
}
|
|
|
|
return pc.srcInfo, compileError
|
|
}
|
|
|
|
// Called during the "walk process"
|
|
func (pc *processContainer) processPath(path string, info os.FileInfo, err error) error {
|
|
if err != nil {
|
|
utils.Logger.Error("Error scanning app source:", "error", err)
|
|
return nil
|
|
}
|
|
|
|
if !info.IsDir() || info.Name() == "tmp" {
|
|
return nil
|
|
}
|
|
|
|
// Get the import path of the package.
|
|
pkgImportPath := pc.rootImportPath
|
|
if pc.root != path {
|
|
pkgImportPath = pc.rootImportPath + "/" + filepath.ToSlash(path[len(pc.root)+1:])
|
|
}
|
|
|
|
// Parse files within the path.
|
|
var pkgs map[string]*ast.Package
|
|
fset := token.NewFileSet()
|
|
pkgs, err = parser.ParseDir(
|
|
fset,
|
|
path,
|
|
func(f os.FileInfo) bool {
|
|
return !f.IsDir() && !strings.HasPrefix(f.Name(), ".") && strings.HasSuffix(f.Name(), ".go")
|
|
},
|
|
0)
|
|
|
|
if err != nil {
|
|
if errList, ok := err.(scanner.ErrorList); ok {
|
|
var pos = errList[0].Pos
|
|
newError := &utils.Error{
|
|
SourceType: ".go source",
|
|
Title: "Go Compilation Error",
|
|
Path: pos.Filename,
|
|
Description: errList[0].Msg,
|
|
Line: pos.Line,
|
|
Column: pos.Column,
|
|
SourceLines: utils.MustReadLines(pos.Filename),
|
|
}
|
|
|
|
errorLink := pc.paths.Config.StringDefault("error.link", "")
|
|
if errorLink != "" {
|
|
newError.SetLink(errorLink)
|
|
}
|
|
return newError
|
|
}
|
|
|
|
// This is exception, err already checked above. Here just a print
|
|
ast.Print(nil, err)
|
|
utils.Logger.Fatal("Failed to parse dir", "error", err)
|
|
}
|
|
|
|
// Skip "main" packages.
|
|
delete(pkgs, "main")
|
|
|
|
// Ignore packages that end with _test
|
|
// These cannot be included in source code that is not generated specifically as a test
|
|
for i := range pkgs {
|
|
if len(i) > 6 {
|
|
if string(i[len(i)-5:]) == "_test" {
|
|
delete(pkgs, i)
|
|
}
|
|
}
|
|
}
|
|
|
|
// If there is no code in this directory, skip it.
|
|
if len(pkgs) == 0 {
|
|
return nil
|
|
}
|
|
|
|
// There should be only one package in this directory.
|
|
if len(pkgs) > 1 {
|
|
for i := range pkgs {
|
|
println("Found package ", i)
|
|
}
|
|
utils.Logger.Fatal("Most unexpected! Multiple packages in a single directory:", "packages", pkgs)
|
|
}
|
|
|
|
var pkg *ast.Package
|
|
for _, v := range pkgs {
|
|
pkg = v
|
|
}
|
|
|
|
if pkg != nil {
|
|
pc.srcInfo = appendSourceInfo(pc.srcInfo, processPackage(fset, pkgImportPath, path, pkg))
|
|
} else {
|
|
utils.Logger.Info("Ignoring package, because it contained no packages", "path", path)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// Process a single package within a file
|
|
func processPackage(fset *token.FileSet, pkgImportPath, pkgPath string, pkg *ast.Package) *model.SourceInfo {
|
|
var (
|
|
structSpecs []*model.TypeInfo
|
|
initImportPaths []string
|
|
|
|
methodSpecs = make(methodMap)
|
|
validationKeys = make(map[string]map[int]string)
|
|
scanControllers = strings.HasSuffix(pkgImportPath, "/controllers") ||
|
|
strings.Contains(pkgImportPath, "/controllers/")
|
|
scanTests = strings.HasSuffix(pkgImportPath, "/tests") ||
|
|
strings.Contains(pkgImportPath, "/tests/")
|
|
)
|
|
|
|
// For each source file in the package...
|
|
utils.Logger.Info("Exaimining files in path", "package", pkgPath)
|
|
for fname, file := range pkg.Files {
|
|
// Imports maps the package key to the full import path.
|
|
// e.g. import "sample/app/models" => "models": "sample/app/models"
|
|
imports := map[string]string{}
|
|
|
|
// For each declaration in the source file...
|
|
for _, decl := range file.Decls {
|
|
addImports(imports, decl, pkgPath)
|
|
|
|
if scanControllers {
|
|
// Match and add both structs and methods
|
|
structSpecs = appendStruct(fname, structSpecs, pkgImportPath, pkg, decl, imports, fset)
|
|
appendAction(fset, methodSpecs, decl, pkgImportPath, pkg.Name, imports)
|
|
} else if scanTests {
|
|
structSpecs = appendStruct(fname, structSpecs, pkgImportPath, pkg, decl, imports, fset)
|
|
}
|
|
|
|
// If this is a func... (ignore nil for external (non-Go) function)
|
|
if funcDecl, ok := decl.(*ast.FuncDecl); ok && funcDecl.Body != nil {
|
|
// Scan it for validation calls
|
|
lineKeys := GetValidationKeys(fname, fset, funcDecl, imports)
|
|
if len(lineKeys) > 0 {
|
|
validationKeys[pkgImportPath+"."+getFuncName(funcDecl)] = lineKeys
|
|
}
|
|
|
|
// Check if it's an init function.
|
|
if funcDecl.Name.Name == "init" {
|
|
initImportPaths = []string{pkgImportPath}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Add the method specs to the struct specs.
|
|
for _, spec := range structSpecs {
|
|
spec.MethodSpecs = methodSpecs[spec.StructName]
|
|
}
|
|
|
|
return &model.SourceInfo{
|
|
StructSpecs: structSpecs,
|
|
ValidationKeys: validationKeys,
|
|
InitImportPaths: initImportPaths,
|
|
}
|
|
}
|
|
|
|
// getFuncName returns a name for this func or method declaration.
|
|
// e.g. "(*Application).SayHello" for a method, "SayHello" for a func.
|
|
func getFuncName(funcDecl *ast.FuncDecl) string {
|
|
prefix := ""
|
|
if funcDecl.Recv != nil {
|
|
recvType := funcDecl.Recv.List[0].Type
|
|
if recvStarType, ok := recvType.(*ast.StarExpr); ok {
|
|
prefix = "(*" + recvStarType.X.(*ast.Ident).Name + ")"
|
|
} else {
|
|
prefix = recvType.(*ast.Ident).Name
|
|
}
|
|
prefix += "."
|
|
}
|
|
return prefix + funcDecl.Name.Name
|
|
}
|
|
|
|
// getStructTypeDecl checks if the given decl is a type declaration for a
|
|
// struct. If so, the TypeSpec is returned.
|
|
func getStructTypeDecl(decl ast.Decl, fset *token.FileSet) (spec *ast.TypeSpec, found bool) {
|
|
genDecl, ok := decl.(*ast.GenDecl)
|
|
if !ok {
|
|
return
|
|
}
|
|
|
|
if genDecl.Tok != token.TYPE {
|
|
return
|
|
}
|
|
|
|
if len(genDecl.Specs) == 0 {
|
|
utils.Logger.Warn("Warn: Surprising: %s:%d Decl contains no specifications", fset.Position(decl.Pos()).Filename, fset.Position(decl.Pos()).Line)
|
|
return
|
|
}
|
|
|
|
spec = genDecl.Specs[0].(*ast.TypeSpec)
|
|
_, found = spec.Type.(*ast.StructType)
|
|
|
|
return
|
|
}
|