github.com/mlmmr/revel-cmd@v0.21.2-0.20191112133115-68d8795776dd/parser/reflect.go (about)

     1  // Copyright (c) 2012-2016 The Revel Framework Authors, All rights reserved.
     2  // Revel Framework source code and usage is governed by a MIT style
     3  // license that can be found in the LICENSE file.
     4  
     5  package parser
     6  
     7  // This file handles the app code introspection.
     8  // It catalogs the controllers, their methods, and their arguments.
     9  
    10  import (
    11  	"go/ast"
    12  	"go/parser"
    13  	"go/scanner"
    14  	"go/token"
    15  	"os"
    16  	"path/filepath"
    17  	"strings"
    18  
    19  	"github.com/mlmmr/revel-cmd/model"
    20  	"github.com/mlmmr/revel-cmd/utils"
    21  )
    22  
    23  // A container used to support the reflection package
    24  type processContainer struct {
    25  	root, rootImportPath string                // The paths
    26  	paths                *model.RevelContainer // The Revel paths
    27  	srcInfo              *model.SourceInfo     // The source information container
    28  }
    29  
    30  // Maps a controller simple name (e.g. "Login") to the methods for which it is a
    31  // receiver.
    32  type methodMap map[string][]*model.MethodSpec
    33  
    34  // ProcessSource parses the app controllers directory and
    35  // returns a list of the controller types found.
    36  // Otherwise CompileError if the parsing fails.
    37  func ProcessSource(paths *model.RevelContainer) (_ *model.SourceInfo, compileError error) {
    38  	pc := &processContainer{paths: paths}
    39  	for _, root := range paths.CodePaths {
    40  		rootImportPath := importPathFromPath(root, paths.BasePath)
    41  		if rootImportPath == "" {
    42  			utils.Logger.Info("Skipping empty code path", "path", root)
    43  			continue
    44  		}
    45  		pc.root, pc.rootImportPath = root, rootImportPath
    46  
    47  		// Start walking the directory tree.
    48  		compileError = utils.Walk(root, pc.processPath)
    49  		if compileError != nil {
    50  			return
    51  		}
    52  	}
    53  
    54  	return pc.srcInfo, compileError
    55  }
    56  
    57  // Called during the "walk process"
    58  func (pc *processContainer) processPath(path string, info os.FileInfo, err error) error {
    59  	if err != nil {
    60  		utils.Logger.Error("Error scanning app source:", "error", err)
    61  		return nil
    62  	}
    63  
    64  	if !info.IsDir() || info.Name() == "tmp" {
    65  		return nil
    66  	}
    67  
    68  	// Get the import path of the package.
    69  	pkgImportPath := pc.rootImportPath
    70  	if pc.root != path {
    71  		pkgImportPath = pc.rootImportPath + "/" + filepath.ToSlash(path[len(pc.root)+1:])
    72  	}
    73  
    74  	// Parse files within the path.
    75  	var pkgs map[string]*ast.Package
    76  	fset := token.NewFileSet()
    77  	pkgs, err = parser.ParseDir(
    78  		fset,
    79  		path,
    80  		func(f os.FileInfo) bool {
    81  			return !f.IsDir() && !strings.HasPrefix(f.Name(), ".") && strings.HasSuffix(f.Name(), ".go")
    82  		},
    83  		0)
    84  
    85  	if err != nil {
    86  		if errList, ok := err.(scanner.ErrorList); ok {
    87  			var pos = errList[0].Pos
    88  			newError := &utils.Error{
    89  				SourceType:  ".go source",
    90  				Title:       "Go Compilation Error",
    91  				Path:        pos.Filename,
    92  				Description: errList[0].Msg,
    93  				Line:        pos.Line,
    94  				Column:      pos.Column,
    95  				SourceLines: utils.MustReadLines(pos.Filename),
    96  			}
    97  
    98  			errorLink := pc.paths.Config.StringDefault("error.link", "")
    99  			if errorLink != "" {
   100  				newError.SetLink(errorLink)
   101  			}
   102  			return newError
   103  		}
   104  
   105  		// This is exception, err already checked above. Here just a print
   106  		ast.Print(nil, err)
   107  		utils.Logger.Fatal("Failed to parse dir", "error", err)
   108  	}
   109  
   110  	// Skip "main" packages.
   111  	delete(pkgs, "main")
   112  
   113  	// Ignore packages that end with _test
   114  	// These cannot be included in source code that is not generated specifically as a test
   115  	for i := range pkgs {
   116  		if len(i) > 6 {
   117  			if string(i[len(i)-5:]) == "_test" {
   118  				delete(pkgs, i)
   119  			}
   120  		}
   121  	}
   122  
   123  	// If there is no code in this directory, skip it.
   124  	if len(pkgs) == 0 {
   125  		return nil
   126  	}
   127  
   128  	// There should be only one package in this directory.
   129  	if len(pkgs) > 1 {
   130  		for i := range pkgs {
   131  			println("Found package ", i)
   132  		}
   133  		utils.Logger.Fatal("Most unexpected! Multiple packages in a single directory:", "packages", pkgs)
   134  	}
   135  
   136  	var pkg *ast.Package
   137  	for _, v := range pkgs {
   138  		pkg = v
   139  	}
   140  
   141  	if pkg != nil {
   142  		pc.srcInfo = appendSourceInfo(pc.srcInfo, processPackage(fset, pkgImportPath, path, pkg))
   143  	} else {
   144  		utils.Logger.Info("Ignoring package, because it contained no packages", "path", path)
   145  	}
   146  	return nil
   147  }
   148  
   149  // Process a single package within a file
   150  func processPackage(fset *token.FileSet, pkgImportPath, pkgPath string, pkg *ast.Package) *model.SourceInfo {
   151  	var (
   152  		structSpecs     []*model.TypeInfo
   153  		initImportPaths []string
   154  
   155  		methodSpecs     = make(methodMap)
   156  		validationKeys  = make(map[string]map[int]string)
   157  		scanControllers = strings.HasSuffix(pkgImportPath, "/controllers") ||
   158  			strings.Contains(pkgImportPath, "/controllers/")
   159  		scanTests = strings.HasSuffix(pkgImportPath, "/tests") ||
   160  			strings.Contains(pkgImportPath, "/tests/")
   161  	)
   162  
   163  	// For each source file in the package...
   164  	utils.Logger.Info("Exaimining files in path", "package", pkgPath)
   165  	for fname, file := range pkg.Files {
   166  		// Imports maps the package key to the full import path.
   167  		// e.g. import "sample/app/models" => "models": "sample/app/models"
   168  		imports := map[string]string{}
   169  
   170  		// For each declaration in the source file...
   171  		for _, decl := range file.Decls {
   172  			addImports(imports, decl, pkgPath)
   173  
   174  			if scanControllers {
   175  				// Match and add both structs and methods
   176  				structSpecs = appendStruct(fname, structSpecs, pkgImportPath, pkg, decl, imports, fset)
   177  				appendAction(fset, methodSpecs, decl, pkgImportPath, pkg.Name, imports)
   178  			} else if scanTests {
   179  				structSpecs = appendStruct(fname, structSpecs, pkgImportPath, pkg, decl, imports, fset)
   180  			}
   181  
   182  			// If this is a func... (ignore nil for external (non-Go) function)
   183  			if funcDecl, ok := decl.(*ast.FuncDecl); ok && funcDecl.Body != nil {
   184  				// Scan it for validation calls
   185  				lineKeys := GetValidationKeys(fname, fset, funcDecl, imports)
   186  				if len(lineKeys) > 0 {
   187  					validationKeys[pkgImportPath+"."+getFuncName(funcDecl)] = lineKeys
   188  				}
   189  
   190  				// Check if it's an init function.
   191  				if funcDecl.Name.Name == "init" {
   192  					initImportPaths = []string{pkgImportPath}
   193  				}
   194  			}
   195  		}
   196  	}
   197  
   198  	// Add the method specs to the struct specs.
   199  	for _, spec := range structSpecs {
   200  		spec.MethodSpecs = methodSpecs[spec.StructName]
   201  	}
   202  
   203  	return &model.SourceInfo{
   204  		StructSpecs:     structSpecs,
   205  		ValidationKeys:  validationKeys,
   206  		InitImportPaths: initImportPaths,
   207  	}
   208  }
   209  
   210  // getFuncName returns a name for this func or method declaration.
   211  // e.g. "(*Application).SayHello" for a method, "SayHello" for a func.
   212  func getFuncName(funcDecl *ast.FuncDecl) string {
   213  	prefix := ""
   214  	if funcDecl.Recv != nil {
   215  		recvType := funcDecl.Recv.List[0].Type
   216  		if recvStarType, ok := recvType.(*ast.StarExpr); ok {
   217  			prefix = "(*" + recvStarType.X.(*ast.Ident).Name + ")"
   218  		} else {
   219  			prefix = recvType.(*ast.Ident).Name
   220  		}
   221  		prefix += "."
   222  	}
   223  	return prefix + funcDecl.Name.Name
   224  }
   225  
   226  // getStructTypeDecl checks if the given decl is a type declaration for a
   227  // struct.  If so, the TypeSpec is returned.
   228  func getStructTypeDecl(decl ast.Decl, fset *token.FileSet) (spec *ast.TypeSpec, found bool) {
   229  	genDecl, ok := decl.(*ast.GenDecl)
   230  	if !ok {
   231  		return
   232  	}
   233  
   234  	if genDecl.Tok != token.TYPE {
   235  		return
   236  	}
   237  
   238  	if len(genDecl.Specs) == 0 {
   239  		utils.Logger.Warn("Warn: Surprising: %s:%d Decl contains no specifications", fset.Position(decl.Pos()).Filename, fset.Position(decl.Pos()).Line)
   240  		return
   241  	}
   242  
   243  	spec = genDecl.Specs[0].(*ast.TypeSpec)
   244  	_, found = spec.Type.(*ast.StructType)
   245  
   246  	return
   247  }