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  }