src

Go monorepo.
git clone git://code.dwrz.net/src
Log | Files | Refs

qf1003.go (5312B)


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