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