Source file src/cmd/vendor/golang.org/x/tools/go/analysis/passes/modernize/stringscutprefix.go

     1  // Copyright 2025 The Go Authors. All rights reserved.
     2  // Use of this source code is governed by a BSD-style
     3  // license that can be found in the LICENSE file.
     4  
     5  package modernize
     6  
     7  import (
     8  	"fmt"
     9  	"go/ast"
    10  	"go/token"
    11  	"strings"
    12  
    13  	"golang.org/x/tools/go/analysis"
    14  	"golang.org/x/tools/go/analysis/passes/inspect"
    15  	"golang.org/x/tools/go/types/typeutil"
    16  	"golang.org/x/tools/internal/analysis/analyzerutil"
    17  	typeindexanalyzer "golang.org/x/tools/internal/analysis/typeindex"
    18  	"golang.org/x/tools/internal/astutil"
    19  	"golang.org/x/tools/internal/refactor"
    20  	"golang.org/x/tools/internal/typesinternal"
    21  	"golang.org/x/tools/internal/typesinternal/typeindex"
    22  	"golang.org/x/tools/internal/versions"
    23  )
    24  
    25  var StringsCutPrefixAnalyzer = &analysis.Analyzer{
    26  	Name: "stringscutprefix",
    27  	Doc:  analyzerutil.MustExtractDoc(doc, "stringscutprefix"),
    28  	Requires: []*analysis.Analyzer{
    29  		inspect.Analyzer,
    30  		typeindexanalyzer.Analyzer,
    31  	},
    32  	Run: stringscutprefix,
    33  	URL: "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#stringscutprefix",
    34  }
    35  
    36  // stringscutprefix offers a fix to replace an if statement which
    37  // calls to the 2 patterns below with strings.CutPrefix or strings.CutSuffix.
    38  //
    39  // Patterns:
    40  //
    41  //  1. if strings.HasPrefix(s, pre) { use(strings.TrimPrefix(s, pre) }
    42  //     =>
    43  //     if after, ok := strings.CutPrefix(s, pre); ok { use(after) }
    44  //
    45  //  2. if after := strings.TrimPrefix(s, pre); after != s { use(after) }
    46  //     =>
    47  //     if after, ok := strings.CutPrefix(s, pre); ok { use(after) }
    48  //
    49  // Similar patterns apply for CutSuffix.
    50  //
    51  // The use must occur within the first statement of the block, and the offered fix
    52  // only replaces the first occurrence of strings.TrimPrefix/TrimSuffix.
    53  //
    54  // Variants:
    55  // - bytes.HasPrefix/HasSuffix usage as pattern 1.
    56  func stringscutprefix(pass *analysis.Pass) (any, error) {
    57  	var (
    58  		index = pass.ResultOf[typeindexanalyzer.Analyzer].(*typeindex.Index)
    59  		info  = pass.TypesInfo
    60  
    61  		stringsTrimPrefix = index.Object("strings", "TrimPrefix")
    62  		bytesTrimPrefix   = index.Object("bytes", "TrimPrefix")
    63  		stringsTrimSuffix = index.Object("strings", "TrimSuffix")
    64  		bytesTrimSuffix   = index.Object("bytes", "TrimSuffix")
    65  	)
    66  	if !index.Used(stringsTrimPrefix, bytesTrimPrefix, stringsTrimSuffix, bytesTrimSuffix) {
    67  		return nil, nil
    68  	}
    69  
    70  	for curFile := range filesUsingGoVersion(pass, versions.Go1_20) {
    71  		for curIfStmt := range curFile.Preorder((*ast.IfStmt)(nil)) {
    72  			ifStmt := curIfStmt.Node().(*ast.IfStmt)
    73  
    74  			// pattern1
    75  			if call, ok := ifStmt.Cond.(*ast.CallExpr); ok && ifStmt.Init == nil && len(ifStmt.Body.List) > 0 {
    76  
    77  				obj := typeutil.Callee(info, call)
    78  				if !typesinternal.IsFunctionNamed(obj, "strings", "HasPrefix", "HasSuffix") &&
    79  					!typesinternal.IsFunctionNamed(obj, "bytes", "HasPrefix", "HasSuffix") {
    80  					continue
    81  				}
    82  				isPrefix := strings.HasSuffix(obj.Name(), "Prefix")
    83  
    84  				// Replace the first occurrence of strings.TrimPrefix(s, pre) in the first statement only,
    85  				// but not later statements in case s or pre are modified by intervening logic (ditto Suffix).
    86  				firstStmt := curIfStmt.Child(ifStmt.Body).Child(ifStmt.Body.List[0])
    87  				for curCall := range firstStmt.Preorder((*ast.CallExpr)(nil)) {
    88  					call1 := curCall.Node().(*ast.CallExpr)
    89  					obj1 := typeutil.Callee(info, call1)
    90  					// bytesTrimPrefix or stringsTrimPrefix might be nil if the file doesn't import it,
    91  					// so we need to ensure the obj1 is not nil otherwise the call1 is not TrimPrefix and cause a panic (ditto Suffix).
    92  					if obj1 == nil ||
    93  						obj1 != stringsTrimPrefix && obj1 != bytesTrimPrefix &&
    94  							obj1 != stringsTrimSuffix && obj1 != bytesTrimSuffix {
    95  						continue
    96  					}
    97  
    98  					isPrefix1 := strings.HasSuffix(obj1.Name(), "Prefix")
    99  					var cutFuncName, varName, message, fixMessage string
   100  					if isPrefix && isPrefix1 {
   101  						cutFuncName = "CutPrefix"
   102  						varName = "after"
   103  						message = "HasPrefix + TrimPrefix can be simplified to CutPrefix"
   104  						fixMessage = "Replace HasPrefix/TrimPrefix with CutPrefix"
   105  					} else if !isPrefix && !isPrefix1 {
   106  						cutFuncName = "CutSuffix"
   107  						varName = "before"
   108  						message = "HasSuffix + TrimSuffix can be simplified to CutSuffix"
   109  						fixMessage = "Replace HasSuffix/TrimSuffix with CutSuffix"
   110  					} else {
   111  						continue
   112  					}
   113  
   114  					// Have: if strings.HasPrefix(s0, pre0) { ...strings.TrimPrefix(s, pre)... } (ditto Suffix)
   115  					var (
   116  						s0   = call.Args[0]
   117  						pre0 = call.Args[1]
   118  						s    = call1.Args[0]
   119  						pre  = call1.Args[1]
   120  					)
   121  
   122  					// check whether the obj1 uses the exact the same argument with strings.HasPrefix
   123  					// shadow variables won't be valid because we only access the first statement (ditto Suffix).
   124  					if astutil.EqualSyntax(s0, s) && astutil.EqualSyntax(pre0, pre) {
   125  						after := refactor.FreshName(info.Scopes[ifStmt], ifStmt.Pos(), varName)
   126  						prefix, importEdits := refactor.AddImport(
   127  							info,
   128  							curFile.Node().(*ast.File),
   129  							obj1.Pkg().Name(),
   130  							obj1.Pkg().Path(),
   131  							cutFuncName,
   132  							call.Pos(),
   133  						)
   134  						okVarName := refactor.FreshName(info.Scopes[ifStmt], ifStmt.Pos(), "ok")
   135  						pass.Report(analysis.Diagnostic{
   136  							// highlight at HasPrefix call (ditto Suffix).
   137  							Pos:     call.Pos(),
   138  							End:     call.End(),
   139  							Message: message,
   140  							SuggestedFixes: []analysis.SuggestedFix{{
   141  								Message: fixMessage,
   142  								// if              strings.HasPrefix(s, pre)     { use(strings.TrimPrefix(s, pre)) }
   143  								//    ------------ -----------------        -----      --------------------------
   144  								// if after, ok := strings.CutPrefix(s, pre); ok { use(after)                      }
   145  								// (ditto Suffix)
   146  								TextEdits: append(importEdits, []analysis.TextEdit{
   147  									{
   148  										Pos:     call.Fun.Pos(),
   149  										End:     call.Fun.Pos(),
   150  										NewText: fmt.Appendf(nil, "%s, %s :=", after, okVarName),
   151  									},
   152  									{
   153  										Pos:     call.Fun.Pos(),
   154  										End:     call.Fun.End(),
   155  										NewText: fmt.Appendf(nil, "%s%s", prefix, cutFuncName),
   156  									},
   157  									{
   158  										Pos:     call.End(),
   159  										End:     call.End(),
   160  										NewText: fmt.Appendf(nil, "; %s ", okVarName),
   161  									},
   162  									{
   163  										Pos:     call1.Pos(),
   164  										End:     call1.End(),
   165  										NewText: []byte(after),
   166  									},
   167  								}...),
   168  							}}},
   169  						)
   170  						break
   171  					}
   172  				}
   173  			}
   174  
   175  			// pattern2
   176  			if bin, ok := ifStmt.Cond.(*ast.BinaryExpr); ok &&
   177  				bin.Op == token.NEQ &&
   178  				ifStmt.Init != nil &&
   179  				isSimpleAssign(ifStmt.Init) {
   180  				assign := ifStmt.Init.(*ast.AssignStmt)
   181  				if call, ok := assign.Rhs[0].(*ast.CallExpr); ok && assign.Tok == token.DEFINE {
   182  					lhs := assign.Lhs[0]
   183  					obj := typeutil.Callee(info, call)
   184  
   185  					if obj == nil ||
   186  						obj != stringsTrimPrefix && obj != bytesTrimPrefix && obj != stringsTrimSuffix && obj != bytesTrimSuffix {
   187  						continue
   188  					}
   189  
   190  					isPrefix1 := strings.HasSuffix(obj.Name(), "Prefix")
   191  					var cutFuncName, message, fixMessage string
   192  					if isPrefix1 {
   193  						cutFuncName = "CutPrefix"
   194  						message = "TrimPrefix can be simplified to CutPrefix"
   195  						fixMessage = "Replace TrimPrefix with CutPrefix"
   196  					} else {
   197  						cutFuncName = "CutSuffix"
   198  						message = "TrimSuffix can be simplified to CutSuffix"
   199  						fixMessage = "Replace TrimSuffix with CutSuffix"
   200  					}
   201  
   202  					if astutil.EqualSyntax(lhs, bin.X) && astutil.EqualSyntax(call.Args[0], bin.Y) ||
   203  						(astutil.EqualSyntax(lhs, bin.Y) && astutil.EqualSyntax(call.Args[0], bin.X)) {
   204  						// TODO(adonovan): avoid FreshName when not needed; see errorsastype.
   205  						okVarName := refactor.FreshName(info.Scopes[ifStmt], ifStmt.Pos(), "ok")
   206  						// Have one of:
   207  						//   if rest := TrimPrefix(s, prefix); rest != s { (ditto Suffix)
   208  						//   if rest := TrimPrefix(s, prefix); s != rest { (ditto Suffix)
   209  
   210  						// We use AddImport not to add an import (since it exists already)
   211  						// but to compute the correct prefix in the dot-import case.
   212  						prefix, importEdits := refactor.AddImport(
   213  							info,
   214  							curFile.Node().(*ast.File),
   215  							obj.Pkg().Name(),
   216  							obj.Pkg().Path(),
   217  							cutFuncName,
   218  							call.Pos(),
   219  						)
   220  
   221  						pass.Report(analysis.Diagnostic{
   222  							// highlight from the init and the condition end.
   223  							Pos:     ifStmt.Init.Pos(),
   224  							End:     ifStmt.Cond.End(),
   225  							Message: message,
   226  							SuggestedFixes: []analysis.SuggestedFix{{
   227  								Message: fixMessage,
   228  								// if x     := strings.TrimPrefix(s, pre); x != s ...
   229  								//     ----            ----------          ------
   230  								// if x, ok := strings.CutPrefix (s, pre); ok     ...
   231  								// (ditto Suffix)
   232  								TextEdits: append(importEdits, []analysis.TextEdit{
   233  									{
   234  										Pos:     assign.Lhs[0].End(),
   235  										End:     assign.Lhs[0].End(),
   236  										NewText: fmt.Appendf(nil, ", %s", okVarName),
   237  									},
   238  									{
   239  										Pos:     call.Fun.Pos(),
   240  										End:     call.Fun.End(),
   241  										NewText: fmt.Appendf(nil, "%s%s", prefix, cutFuncName),
   242  									},
   243  									{
   244  										Pos:     ifStmt.Cond.Pos(),
   245  										End:     ifStmt.Cond.End(),
   246  										NewText: []byte(okVarName),
   247  									},
   248  								}...),
   249  							}},
   250  						})
   251  					}
   252  				}
   253  			}
   254  		}
   255  	}
   256  	return nil, nil
   257  }
   258  

View as plain text