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  }