github.com/songshiyun/revive@v1.1.5-0.20220323112655-f8433a19b3c5/rule/string-format.go (about)

     1  package rule
     2  
     3  import (
     4  	"fmt"
     5  	"go/ast"
     6  	"go/token"
     7  	"regexp"
     8  	"strconv"
     9  
    10  	"github.com/songshiyun/revive/lint"
    11  )
    12  
    13  // #region Revive API
    14  
    15  // StringFormatRule lints strings and/or comments according to a set of regular expressions given as Arguments
    16  type StringFormatRule struct{}
    17  
    18  // Apply applies the rule to the given file.
    19  func (r *StringFormatRule) Apply(file *lint.File, arguments lint.Arguments) []lint.Failure {
    20  	var failures []lint.Failure
    21  
    22  	onFailure := func(failure lint.Failure) {
    23  		failures = append(failures, failure)
    24  	}
    25  
    26  	w := lintStringFormatRule{onFailure: onFailure}
    27  	w.parseArguments(arguments)
    28  	ast.Walk(w, file.AST)
    29  
    30  	return failures
    31  }
    32  
    33  // Name returns the rule name.
    34  func (r *StringFormatRule) Name() string {
    35  	return "string-format"
    36  }
    37  
    38  // ParseArgumentsTest is a public wrapper around w.parseArguments used for testing. Returns the error message provided to panic, or nil if no error was encountered
    39  func (r *StringFormatRule) ParseArgumentsTest(arguments lint.Arguments) *string {
    40  	w := lintStringFormatRule{}
    41  	c := make(chan interface{})
    42  	// Parse the arguments in a goroutine, defer a recover() call, return the error encountered (or nil if there was no error)
    43  	go func() {
    44  		defer func() {
    45  			err := recover()
    46  			c <- err
    47  		}()
    48  		w.parseArguments(arguments)
    49  	}()
    50  	err := <-c
    51  	if err != nil {
    52  		e := fmt.Sprintf("%s", err)
    53  		return &e
    54  	}
    55  	return nil
    56  }
    57  
    58  // #endregion
    59  
    60  // #region Internal structure
    61  
    62  type lintStringFormatRule struct {
    63  	onFailure func(lint.Failure)
    64  
    65  	rules              []stringFormatSubrule
    66  	stringDeclarations map[string]string
    67  }
    68  
    69  type stringFormatSubrule struct {
    70  	parent       *lintStringFormatRule
    71  	scope        stringFormatSubruleScope
    72  	regexp       *regexp.Regexp
    73  	errorMessage string
    74  }
    75  
    76  type stringFormatSubruleScope struct {
    77  	funcName string // Function name the rule is scoped to
    78  	argument int    // (optional) Which argument in calls to the function is checked against the rule (the first argument is checked by default)
    79  	field    string // (optional) If the argument to be checked is a struct, which member of the struct is checked against the rule (top level members only)
    80  }
    81  
    82  // Regex inserted to match valid function/struct field identifiers
    83  const identRegex = "[_A-Za-z][_A-Za-z0-9]*"
    84  
    85  var parseStringFormatScope = regexp.MustCompile(
    86  	fmt.Sprintf("^(%s(?:\\.%s)?)(?:\\[([0-9]+)\\](?:\\.(%s))?)?$", identRegex, identRegex, identRegex))
    87  
    88  // #endregion
    89  
    90  // #region Argument parsing
    91  
    92  func (w *lintStringFormatRule) parseArguments(arguments lint.Arguments) {
    93  	for i, argument := range arguments {
    94  		scope, regex, errorMessage := w.parseArgument(argument, i)
    95  		w.rules = append(w.rules, stringFormatSubrule{
    96  			parent:       w,
    97  			scope:        scope,
    98  			regexp:       regex,
    99  			errorMessage: errorMessage,
   100  		})
   101  	}
   102  }
   103  
   104  func (w lintStringFormatRule) parseArgument(argument interface{}, ruleNum int) (scope stringFormatSubruleScope, regex *regexp.Regexp, errorMessage string) {
   105  	g, ok := argument.([]interface{}) // Cast to generic slice first
   106  	if !ok {
   107  		w.configError("argument is not a slice", ruleNum, 0)
   108  	}
   109  	if len(g) < 2 {
   110  		w.configError("less than two slices found in argument, scope and regex are required", ruleNum, len(g)-1)
   111  	}
   112  	rule := make([]string, len(g))
   113  	for i, obj := range g {
   114  		val, ok := obj.(string)
   115  		if !ok {
   116  			w.configError("unexpected value, string was expected", ruleNum, i)
   117  		}
   118  		rule[i] = val
   119  	}
   120  
   121  	// Validate scope and regex length
   122  	if rule[0] == "" {
   123  		w.configError("empty scope provided", ruleNum, 0)
   124  	} else if len(rule[1]) < 2 {
   125  		w.configError("regex is too small (regexes should begin and end with '/')", ruleNum, 1)
   126  	}
   127  
   128  	// Parse rule scope
   129  	scope = stringFormatSubruleScope{}
   130  	matches := parseStringFormatScope.FindStringSubmatch(rule[0])
   131  	if matches == nil {
   132  		// The rule's scope didn't match the parsing regex at all, probably a configuration error
   133  		w.parseError("unable to parse rule scope", ruleNum, 0)
   134  	} else if len(matches) != 4 {
   135  		// The rule's scope matched the parsing regex, but an unexpected number of submatches was returned, probably a bug
   136  		w.parseError(fmt.Sprintf("unexpected number of submatches when parsing scope: %d, expected 4", len(matches)), ruleNum, 0)
   137  	}
   138  	scope.funcName = matches[1]
   139  	if len(matches[2]) > 0 {
   140  		var err error
   141  		scope.argument, err = strconv.Atoi(matches[2])
   142  		if err != nil {
   143  			w.parseError("unable to parse argument number in rule scope", ruleNum, 0)
   144  		}
   145  	}
   146  	if len(matches[3]) > 0 {
   147  		scope.field = matches[3]
   148  	}
   149  
   150  	// Strip / characters from the beginning and end of rule[1] before compiling
   151  	regex, err := regexp.Compile(rule[1][1 : len(rule[1])-1])
   152  	if err != nil {
   153  		w.parseError(fmt.Sprintf("unable to compile %s as regexp", rule[1]), ruleNum, 1)
   154  	}
   155  
   156  	// Use custom error message if provided
   157  	if len(rule) == 3 {
   158  		errorMessage = rule[2]
   159  	}
   160  	return scope, regex, errorMessage
   161  }
   162  
   163  // Report an invalid config, this is specifically the user's fault
   164  func (w lintStringFormatRule) configError(msg string, ruleNum, option int) {
   165  	panic(fmt.Sprintf("invalid configuration for string-format: %s [argument %d, option %d]", msg, ruleNum, option))
   166  }
   167  
   168  // Report a general config parsing failure, this may be the user's fault, but it isn't known for certain
   169  func (w lintStringFormatRule) parseError(msg string, ruleNum, option int) {
   170  	panic(fmt.Sprintf("failed to parse configuration for string-format: %s [argument %d, option %d]", msg, ruleNum, option))
   171  }
   172  
   173  // #endregion
   174  
   175  // #region Node traversal
   176  
   177  func (w lintStringFormatRule) Visit(node ast.Node) ast.Visitor {
   178  	// First, check if node is a call expression
   179  	call, ok := node.(*ast.CallExpr)
   180  	if !ok {
   181  		return w
   182  	}
   183  
   184  	// Get the name of the call expression to check against rule scope
   185  	callName, ok := w.getCallName(call)
   186  	if !ok {
   187  		return w
   188  	}
   189  
   190  	for _, rule := range w.rules {
   191  		if rule.scope.funcName == callName {
   192  			rule.Apply(call)
   193  		}
   194  	}
   195  
   196  	return w
   197  }
   198  
   199  // Return the name of a call expression in the form of package.Func or Func
   200  func (w lintStringFormatRule) getCallName(call *ast.CallExpr) (callName string, ok bool) {
   201  	if ident, ok := call.Fun.(*ast.Ident); ok {
   202  		// Local function call
   203  		return ident.Name, true
   204  	}
   205  
   206  	if selector, ok := call.Fun.(*ast.SelectorExpr); ok {
   207  		// Scoped function call
   208  		scope, ok := selector.X.(*ast.Ident)
   209  		if !ok {
   210  			return "", false
   211  		}
   212  		return scope.Name + "." + selector.Sel.Name, true
   213  	}
   214  
   215  	return "", false
   216  }
   217  
   218  // #endregion
   219  
   220  // #region Linting logic
   221  
   222  // Apply a single format rule to a call expression (should be done after verifying the that the call expression matches the rule's scope)
   223  func (rule stringFormatSubrule) Apply(call *ast.CallExpr) {
   224  	if len(call.Args) <= rule.scope.argument {
   225  		return
   226  	}
   227  
   228  	arg := call.Args[rule.scope.argument]
   229  	var lit *ast.BasicLit
   230  	if len(rule.scope.field) > 0 {
   231  		// Try finding the scope's Field, treating arg as a composite literal
   232  		composite, ok := arg.(*ast.CompositeLit)
   233  		if !ok {
   234  			return
   235  		}
   236  		for _, el := range composite.Elts {
   237  			kv, ok := el.(*ast.KeyValueExpr)
   238  			if !ok {
   239  				continue
   240  			}
   241  			key, ok := kv.Key.(*ast.Ident)
   242  			if !ok || key.Name != rule.scope.field {
   243  				continue
   244  			}
   245  
   246  			// We're now dealing with the exact field in the rule's scope, so if anything fails, we can safely return instead of continuing the loop
   247  			lit, ok = kv.Value.(*ast.BasicLit)
   248  			if !ok || lit.Kind != token.STRING {
   249  				return
   250  			}
   251  		}
   252  	} else {
   253  		var ok bool
   254  		// Treat arg as a string literal
   255  		lit, ok = arg.(*ast.BasicLit)
   256  		if !ok || lit.Kind != token.STRING {
   257  			return
   258  		}
   259  	}
   260  	// Unquote the string literal before linting
   261  	unquoted := lit.Value[1 : len(lit.Value)-1]
   262  	rule.lintMessage(unquoted, lit)
   263  }
   264  
   265  func (rule stringFormatSubrule) lintMessage(s string, node ast.Node) {
   266  	// Fail if the string doesn't match the user's regex
   267  	if rule.regexp.MatchString(s) {
   268  		return
   269  	}
   270  	var failure string
   271  	if len(rule.errorMessage) > 0 {
   272  		failure = rule.errorMessage
   273  	} else {
   274  		failure = fmt.Sprintf("string literal doesn't match user defined regex /%s/", rule.regexp.String())
   275  	}
   276  	rule.parent.onFailure(lint.Failure{
   277  		Confidence: 1,
   278  		Failure:    failure,
   279  		Node:       node,
   280  	})
   281  }
   282  
   283  // #endregion