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 }