Skip to content

Commit 7ad909f

Browse files
committed
quickfix: add check for turning if-else chains into tagged switches
Closes dominikhgh-6
1 parent d073bad commit 7ad909f

File tree

6 files changed

+238
-0
lines changed

6 files changed

+238
-0
lines changed

quickfix/analysis.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,4 +20,8 @@ var Analyzers = lint.InitializeAnalyzers(Docs, map[string]*analysis.Analyzer{
2020
Run: CheckTaglessSwitch,
2121
Requires: []*analysis.Analyzer{inspect.Analyzer},
2222
},
23+
"QF1003": {
24+
Run: CheckIfElseToSwitch,
25+
Requires: []*analysis.Analyzer{inspect.Analyzer},
26+
},
2327
})

quickfix/doc.go

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,4 +39,30 @@ After:
3939
`,
4040
Since: "Unreleased",
4141
},
42+
"QF1003": {
43+
Title: "Convert if/else-if chain to tagged switch",
44+
Text: ` A series of if/else-if checks comparing the same variable against values can be replaced with a tagged switch.
45+
46+
Before:
47+
48+
if x == 1 || x == 2 {
49+
...
50+
} else if x == 3 {
51+
...
52+
} else {
53+
...
54+
}
55+
56+
After:
57+
58+
switch x {
59+
case 1, 2:
60+
...
61+
case 3:
62+
...
63+
default:
64+
...
65+
}`,
66+
Since: "Unreleased",
67+
},
4268
}

quickfix/lint.go

Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -404,3 +404,115 @@ func CheckTaglessSwitch(pass *analysis.Pass) (interface{}, error) {
404404
code.Preorder(pass, fn, (*ast.SwitchStmt)(nil))
405405
return nil, nil
406406
}
407+
408+
func CheckIfElseToSwitch(pass *analysis.Pass) (interface{}, error) {
409+
fn := func(node ast.Node, stack []ast.Node) {
410+
if _, ok := stack[len(stack)-2].(*ast.IfStmt); ok {
411+
// this if statement is part of an if-else chain
412+
return
413+
}
414+
ifstmt := node.(*ast.IfStmt)
415+
416+
m := map[ast.Expr][]*ast.BinaryExpr{}
417+
for item := ifstmt; item != nil; {
418+
if item.Init != nil {
419+
return
420+
}
421+
if item.Body == nil {
422+
return
423+
}
424+
425+
skip := false
426+
ast.Inspect(item.Body, func(node ast.Node) bool {
427+
if branch, ok := node.(*ast.BranchStmt); ok && branch.Tok != token.GOTO {
428+
skip = true
429+
return false
430+
}
431+
return true
432+
})
433+
if skip {
434+
return
435+
}
436+
437+
var pairs []*ast.BinaryExpr
438+
if !findSwitchPairs(pass, item.Cond, &pairs) {
439+
return
440+
}
441+
m[item.Cond] = pairs
442+
switch els := item.Else.(type) {
443+
case *ast.IfStmt:
444+
item = els
445+
case *ast.BlockStmt, nil:
446+
item = nil
447+
default:
448+
panic(fmt.Sprintf("unreachable: %T", els))
449+
}
450+
}
451+
452+
var x ast.Expr
453+
for _, pair := range m {
454+
if len(pair) == 0 {
455+
continue
456+
}
457+
if x == nil {
458+
x = pair[0].X
459+
} else {
460+
if !astutil.Equal(x, pair[0].X) {
461+
return
462+
}
463+
}
464+
}
465+
if x == nil {
466+
// shouldn't happen
467+
return
468+
}
469+
470+
// We require at least two 'if' to make this suggestion, to
471+
// avoid clutter in the editor.
472+
if len(m) < 2 {
473+
return
474+
}
475+
476+
var edits []analysis.TextEdit
477+
for item := ifstmt; item != nil; {
478+
var end token.Pos
479+
if item.Else != nil {
480+
end = item.Else.Pos()
481+
} else {
482+
// delete up to but not including the closing brace.
483+
end = item.Body.Rbrace
484+
}
485+
486+
var conds []string
487+
for _, cond := range m[item.Cond] {
488+
y := cond.Y
489+
if p, ok := y.(*ast.ParenExpr); ok {
490+
y = p.X
491+
}
492+
conds = append(conds, report.Render(pass, y))
493+
}
494+
sconds := strings.Join(conds, ", ")
495+
edits = append(edits,
496+
edit.ReplaceWithString(pass.Fset, edit.Range{item.If, item.Body.Lbrace + 1}, "case "+sconds+":"),
497+
edit.Delete(edit.Range{item.Body.Rbrace, end}))
498+
499+
switch els := item.Else.(type) {
500+
case *ast.IfStmt:
501+
item = els
502+
case *ast.BlockStmt:
503+
edits = append(edits, edit.ReplaceWithString(pass.Fset, edit.Range{els.Lbrace, els.Lbrace + 1}, "default:"))
504+
item = nil
505+
case nil:
506+
item = nil
507+
default:
508+
panic(fmt.Sprintf("unreachable: %T", els))
509+
}
510+
}
511+
// FIXME this forces the first case to begin in column 0. try to fix the indentation
512+
edits = append(edits, edit.ReplaceWithString(pass.Fset, edit.Range{ifstmt.If, ifstmt.If}, fmt.Sprintf("switch %s {\n", report.Render(pass, x))))
513+
report.Report(pass, ifstmt, fmt.Sprintf("could use tagged switch on %s", report.Render(pass, x)),
514+
report.Fixes(edit.Fix("Replace with tagged switch", edits...)))
515+
}
516+
code.PreorderStack(pass, fn, (*ast.IfStmt)(nil))
517+
return nil, nil
518+
}

quickfix/lint_test.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ func TestAll(t *testing.T) {
1111
"QF1000": {{Dir: "CheckStringsIndexByte"}},
1212
"QF1001": {{Dir: "CheckDeMorgan"}},
1313
"QF1002": {{Dir: "CheckTaglessSwitch"}},
14+
"QF1003": {{Dir: "CheckIfElseToSwitch"}},
1415
}
1516

1617
testutil.Run(t, Analyzers, checks)
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
package pkg
2+
3+
func fn() {
4+
var x, y int
5+
var z []int
6+
var a bool
7+
8+
if x == 1 || x == 2 { // want `could use tagged switch on x`
9+
} else if x == 3 {
10+
}
11+
12+
if x == 1 || x == 2 { // want `could use tagged switch on x`
13+
} else if x == 3 {
14+
} else {
15+
}
16+
17+
if x == 1 || x == 2 {
18+
} else if y == 3 {
19+
} else {
20+
}
21+
22+
if a == (x == y) { // want `could use tagged switch on a`
23+
} else if a == (x != y) {
24+
}
25+
26+
if z[0] == 1 || z[0] == 2 { // want `could use tagged switch on z\[0\]`
27+
} else if z[0] == 3 {
28+
}
29+
30+
for {
31+
if x == 1 || x == 2 { // want `could use tagged switch on x`
32+
} else if x == 3 {
33+
}
34+
}
35+
36+
for {
37+
if x == 1 || x == 2 {
38+
} else if x == 3 {
39+
break
40+
}
41+
}
42+
43+
if x == 1 || x == 2 {
44+
}
45+
}
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
package pkg
2+
3+
func fn() {
4+
var x, y int
5+
var z []int
6+
var a bool
7+
8+
switch x {
9+
case 1, 2: // want `could use tagged switch on x`
10+
case 3:
11+
}
12+
13+
switch x {
14+
case 1, 2: // want `could use tagged switch on x`
15+
case 3:
16+
default:
17+
}
18+
19+
if x == 1 || x == 2 {
20+
} else if y == 3 {
21+
} else {
22+
}
23+
24+
switch a {
25+
case x == y: // want `could use tagged switch on a`
26+
case x != y:
27+
}
28+
29+
switch z[0] {
30+
case 1, 2: // want `could use tagged switch on z\[0\]`
31+
case 3:
32+
}
33+
34+
for {
35+
switch x {
36+
case 1, 2: // want `could use tagged switch on x`
37+
case 3:
38+
}
39+
}
40+
41+
for {
42+
if x == 1 || x == 2 {
43+
} else if x == 3 {
44+
break
45+
}
46+
}
47+
48+
if x == 1 || x == 2 {
49+
}
50+
}

0 commit comments

Comments
 (0)