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

     1  package qf1003
     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/edit"
    11  	"github.com/amarpal/go-tools/analysis/lint"
    12  	"github.com/amarpal/go-tools/analysis/report"
    13  	"github.com/amarpal/go-tools/go/ast/astutil"
    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:     "QF1003",
    22  		Run:      run,
    23  		Requires: []*analysis.Analyzer{inspect.Analyzer},
    24  	},
    25  	Doc: &lint.Documentation{
    26  		Title: "Convert if/else-if chain to tagged switch",
    27  		Text: `
    28  A series of if/else-if checks comparing the same variable against
    29  values can be replaced with a tagged switch.`,
    30  		Before: `
    31  if x == 1 || x == 2 {
    32      ...
    33  } else if x == 3 {
    34      ...
    35  } else {
    36      ...
    37  }`,
    38  
    39  		After: `
    40  switch x {
    41  case 1, 2:
    42      ...
    43  case 3:
    44      ...
    45  default:
    46      ...
    47  }`,
    48  		Since:    "2021.1",
    49  		Severity: lint.SeverityInfo,
    50  	},
    51  })
    52  
    53  var Analyzer = SCAnalyzer.Analyzer
    54  
    55  func run(pass *analysis.Pass) (interface{}, error) {
    56  	fn := func(node ast.Node, stack []ast.Node) {
    57  		if _, ok := stack[len(stack)-2].(*ast.IfStmt); ok {
    58  			// this if statement is part of an if-else chain
    59  			return
    60  		}
    61  		ifstmt := node.(*ast.IfStmt)
    62  
    63  		m := map[ast.Expr][]*ast.BinaryExpr{}
    64  		for item := ifstmt; item != nil; {
    65  			if item.Init != nil {
    66  				return
    67  			}
    68  			if item.Body == nil {
    69  				return
    70  			}
    71  
    72  			skip := false
    73  			ast.Inspect(item.Body, func(node ast.Node) bool {
    74  				if branch, ok := node.(*ast.BranchStmt); ok && branch.Tok != token.GOTO {
    75  					skip = true
    76  					return false
    77  				}
    78  				return true
    79  			})
    80  			if skip {
    81  				return
    82  			}
    83  
    84  			var pairs []*ast.BinaryExpr
    85  			if !findSwitchPairs(pass, item.Cond, &pairs) {
    86  				return
    87  			}
    88  			m[item.Cond] = pairs
    89  			switch els := item.Else.(type) {
    90  			case *ast.IfStmt:
    91  				item = els
    92  			case *ast.BlockStmt, nil:
    93  				item = nil
    94  			default:
    95  				panic(fmt.Sprintf("unreachable: %T", els))
    96  			}
    97  		}
    98  
    99  		var x ast.Expr
   100  		for _, pair := range m {
   101  			if len(pair) == 0 {
   102  				continue
   103  			}
   104  			if x == nil {
   105  				x = pair[0].X
   106  			} else {
   107  				if !astutil.Equal(x, pair[0].X) {
   108  					return
   109  				}
   110  			}
   111  		}
   112  		if x == nil {
   113  			// shouldn't happen
   114  			return
   115  		}
   116  
   117  		// We require at least two 'if' to make this suggestion, to
   118  		// avoid clutter in the editor.
   119  		if len(m) < 2 {
   120  			return
   121  		}
   122  
   123  		// Note that we insert the switch statement as the first text edit instead of the last one so that gopls has an
   124  		// easier time converting it to an LSP-conforming edit.
   125  		//
   126  		// Specifically:
   127  		// > Text edits ranges must never overlap, that means no part of the original
   128  		// > document must be manipulated by more than one edit. However, it is
   129  		// > possible that multiple edits have the same start position: multiple
   130  		// > inserts, or any number of inserts followed by a single remove or replace
   131  		// > edit. If multiple inserts have the same position, the order in the array
   132  		// > defines the order in which the inserted strings appear in the resulting
   133  		// > text.
   134  		//
   135  		// See https://go.dev/issue/63930
   136  		//
   137  		// FIXME this edit forces the first case to begin in column 0 because we ignore indentation. try to fix that.
   138  		edits := []analysis.TextEdit{edit.ReplaceWithString(edit.Range{ifstmt.If, ifstmt.If}, fmt.Sprintf("switch %s {\n", report.Render(pass, x)))}
   139  		for item := ifstmt; item != nil; {
   140  			var end token.Pos
   141  			if item.Else != nil {
   142  				end = item.Else.Pos()
   143  			} else {
   144  				// delete up to but not including the closing brace.
   145  				end = item.Body.Rbrace
   146  			}
   147  
   148  			var conds []string
   149  			for _, cond := range m[item.Cond] {
   150  				y := cond.Y
   151  				if p, ok := y.(*ast.ParenExpr); ok {
   152  					y = p.X
   153  				}
   154  				conds = append(conds, report.Render(pass, y))
   155  			}
   156  			sconds := strings.Join(conds, ", ")
   157  			edits = append(edits,
   158  				edit.ReplaceWithString(edit.Range{item.If, item.Body.Lbrace + 1}, "case "+sconds+":"),
   159  				edit.Delete(edit.Range{item.Body.Rbrace, end}))
   160  
   161  			switch els := item.Else.(type) {
   162  			case *ast.IfStmt:
   163  				item = els
   164  			case *ast.BlockStmt:
   165  				edits = append(edits, edit.ReplaceWithString(edit.Range{els.Lbrace, els.Lbrace + 1}, "default:"))
   166  				item = nil
   167  			case nil:
   168  				item = nil
   169  			default:
   170  				panic(fmt.Sprintf("unreachable: %T", els))
   171  			}
   172  		}
   173  		report.Report(pass, ifstmt, fmt.Sprintf("could use tagged switch on %s", report.Render(pass, x)),
   174  			report.Fixes(edit.Fix("Replace with tagged switch", edits...)),
   175  			report.ShortRange())
   176  	}
   177  	code.PreorderStack(pass, fn, (*ast.IfStmt)(nil))
   178  	return nil, nil
   179  }
   180  
   181  func findSwitchPairs(pass *analysis.Pass, expr ast.Expr, pairs *[]*ast.BinaryExpr) bool {
   182  	binexpr, ok := astutil.Unparen(expr).(*ast.BinaryExpr)
   183  	if !ok {
   184  		return false
   185  	}
   186  	switch binexpr.Op {
   187  	case token.EQL:
   188  		if code.MayHaveSideEffects(pass, binexpr.X, nil) || code.MayHaveSideEffects(pass, binexpr.Y, nil) {
   189  			return false
   190  		}
   191  		// syntactic identity should suffice. we do not allow side
   192  		// effects in the case clauses, so there should be no way for
   193  		// values to change.
   194  		if len(*pairs) > 0 && !astutil.Equal(binexpr.X, (*pairs)[0].X) {
   195  			return false
   196  		}
   197  		*pairs = append(*pairs, binexpr)
   198  		return true
   199  	case token.LOR:
   200  		return findSwitchPairs(pass, binexpr.X, pairs) && findSwitchPairs(pass, binexpr.Y, pairs)
   201  	default:
   202  		return false
   203  	}
   204  }