github.com/blend/go-sdk@v1.20220411.3/profanity/go_calls.go (about) 1 /* 2 3 Copyright (c) 2022 - Present. Blend Labs, Inc. All rights reserved 4 Use of this source code is governed by a MIT license that can be found in the LICENSE file. 5 6 */ 7 8 package profanity 9 10 import ( 11 "fmt" 12 "go/ast" 13 "go/parser" 14 "go/token" 15 "path/filepath" 16 "strings" 17 18 "github.com/blend/go-sdk/stringutil" 19 "github.com/blend/go-sdk/validate" 20 ) 21 22 var ( 23 _ Rule = (*GoCalls)(nil) 24 ) 25 26 // GoCalls returns a profanity error if a given function is called in a package. 27 type GoCalls []GoCall 28 29 // Validate implements validation for the rule. 30 func (gc GoCalls) Validate() error { 31 for _, c := range gc { 32 if c.Package == "" && c.Func == "" { 33 return validate.Error(validate.ErrStringRequired, nil) 34 } 35 } 36 return nil 37 } 38 39 // Check implements Rule. 40 func (gc GoCalls) Check(filename string, contents []byte) RuleResult { 41 if filepath.Ext(filename) != ".go" { 42 return RuleResult{OK: true} 43 } 44 45 fset := token.NewFileSet() 46 fileAst, err := parser.ParseFile(fset, filename, contents, parser.AllErrors|parser.ParseComments) 47 if err != nil { 48 return RuleResult{Err: err} 49 } 50 51 var results []RuleResult 52 ast.Inspect(fileAst, func(n ast.Node) bool { 53 if n == nil { 54 return false 55 } 56 switch nt := n.(type) { 57 case *ast.CallExpr: 58 switch ft := nt.Fun.(type) { 59 case *ast.SelectorExpr: 60 for _, fn := range gc { 61 if isIdent(ft.X, fn.Package) && isIdent(ft.Sel, fn.Func) { 62 var message string 63 if fn.Package != "" { 64 message = fmt.Sprintf("go file includes function call: \"%s.%s\"", fn.Package, fn.Func) 65 } else { 66 message = fmt.Sprintf("go file includes function call: %q", fn.Func) 67 } 68 results = append(results, RuleResult{ 69 File: filename, 70 Line: fset.Position(ft.Pos()).Line, 71 Message: message, 72 }) 73 return false 74 } 75 } 76 return false 77 case *ast.Ident: // check package local functions and built-ins 78 for _, fn := range gc { 79 if fn.Package == "" { 80 if isIdent(ft, fn.Func) { 81 results = append(results, RuleResult{ 82 File: filename, 83 Line: fset.Position(ft.Pos()).Line, 84 Message: fmt.Sprintf("go file includes function call: %q", fn.Func), 85 }) 86 return false 87 } 88 } 89 } 90 return false 91 } 92 } 93 return true 94 }) 95 if len(results) > 0 { 96 return results[0] 97 } 98 return RuleResult{OK: true} 99 } 100 101 // Strings implements fmt.Stringer. 102 func (gc GoCalls) String() string { 103 var tokens []string 104 for _, call := range gc { 105 tokens = append(tokens, call.String()) 106 } 107 return fmt.Sprintf("go calls: %s", strings.Join(tokens, " ")) 108 } 109 110 // GoCall is a package and function name pair. 111 // 112 // `Package` is the package selector, typically the last path 113 // segment of the import (ex. "github.com/foo/bar" would be "bar") 114 // 115 // `Func` is the function name. 116 // 117 // If package is empty string, it is assumed that the function 118 // is local to the calling package or a builtin. 119 type GoCall struct { 120 Package string `yaml:"package"` 121 Func string `yaml:"func"` 122 } 123 124 // String implements fmt.Stringer 125 func (gc GoCall) String() string { 126 if gc.Package != "" { 127 return gc.Package + "." + gc.Func 128 } 129 return gc.Func 130 } 131 132 func isIdent(expr ast.Expr, ident string) bool { 133 if ident == "" { 134 return true 135 } 136 id, ok := expr.(*ast.Ident) 137 return ok && stringutil.Glob(id.Name, ident) 138 }