github.com/amarpal/go-tools@v0.0.0-20240422043104-40142f59f616/simple/s1007/s1007.go (about)

     1  package s1007
     2  
     3  import (
     4  	"fmt"
     5  	"go/ast"
     6  	"go/token"
     7  	"strings"
     8  
     9  	"github.com/amarpal/go-tools/analysis/code"
    10  	"github.com/amarpal/go-tools/analysis/facts/generated"
    11  	"github.com/amarpal/go-tools/analysis/lint"
    12  	"github.com/amarpal/go-tools/analysis/report"
    13  	"github.com/amarpal/go-tools/knowledge"
    14  
    15  	"golang.org/x/tools/go/analysis"
    16  	"golang.org/x/tools/go/analysis/passes/inspect"
    17  )
    18  
    19  var SCAnalyzer = lint.InitializeAnalyzer(&lint.Analyzer{
    20  	Analyzer: &analysis.Analyzer{
    21  		Name:     "S1007",
    22  		Run:      run,
    23  		Requires: []*analysis.Analyzer{inspect.Analyzer, generated.Analyzer},
    24  	},
    25  	Doc: &lint.Documentation{
    26  		Title: `Simplify regular expression by using raw string literal`,
    27  		Text: `Raw string literals use backticks instead of quotation marks and do not support
    28  any escape sequences. This means that the backslash can be used
    29  freely, without the need of escaping.
    30  
    31  Since regular expressions have their own escape sequences, raw strings
    32  can improve their readability.`,
    33  		Before:  `regexp.Compile("\\A(\\w+) profile: total \\d+\\n\\z")`,
    34  		After:   "regexp.Compile(`\\A(\\w+) profile: total \\d+\\n\\z`)",
    35  		Since:   "2017.1",
    36  		MergeIf: lint.MergeIfAny,
    37  	},
    38  })
    39  
    40  var Analyzer = SCAnalyzer.Analyzer
    41  
    42  func run(pass *analysis.Pass) (interface{}, error) {
    43  	fn := func(node ast.Node) {
    44  		call := node.(*ast.CallExpr)
    45  		if !code.IsCallToAny(pass, call, "regexp.MustCompile", "regexp.Compile") {
    46  			return
    47  		}
    48  		sel, ok := call.Fun.(*ast.SelectorExpr)
    49  		if !ok {
    50  			return
    51  		}
    52  		lit, ok := call.Args[knowledge.Arg("regexp.Compile.expr")].(*ast.BasicLit)
    53  		if !ok {
    54  			// TODO(dominikh): support string concat, maybe support constants
    55  			return
    56  		}
    57  		if lit.Kind != token.STRING {
    58  			// invalid function call
    59  			return
    60  		}
    61  		if lit.Value[0] != '"' {
    62  			// already a raw string
    63  			return
    64  		}
    65  		val := lit.Value
    66  		if !strings.Contains(val, `\\`) {
    67  			return
    68  		}
    69  		if strings.Contains(val, "`") {
    70  			return
    71  		}
    72  
    73  		bs := false
    74  		for _, c := range val {
    75  			if !bs && c == '\\' {
    76  				bs = true
    77  				continue
    78  			}
    79  			if bs && c == '\\' {
    80  				bs = false
    81  				continue
    82  			}
    83  			if bs {
    84  				// backslash followed by non-backslash -> escape sequence
    85  				return
    86  			}
    87  		}
    88  
    89  		report.Report(pass, call, fmt.Sprintf("should use raw string (`...`) with regexp.%s to avoid having to escape twice", sel.Sel.Name), report.FilterGenerated())
    90  	}
    91  	code.Preorder(pass, fn, (*ast.CallExpr)(nil))
    92  	return nil, nil
    93  }