initial commit: ifinit and boolnames
This commit is contained in:
200
ifinit/analyzer.go
Normal file
200
ifinit/analyzer.go
Normal file
@@ -0,0 +1,200 @@
|
||||
// Package ifinit reports if-statement init clauses that span more than one line.
|
||||
package ifinit
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"go/ast"
|
||||
"go/token"
|
||||
"strings"
|
||||
|
||||
"golang.org/x/tools/go/analysis"
|
||||
"golang.org/x/tools/go/analysis/passes/inspect"
|
||||
"golang.org/x/tools/go/ast/inspector"
|
||||
)
|
||||
|
||||
const doc = `checks that if-statement init clauses occupy a single line
|
||||
|
||||
A multi-line init clause pushes the condition being tested far away from the
|
||||
"if" keyword, so the reader has to scan past the setup to find out what is
|
||||
actually being decided. The fix hoists the init clause into its own statement
|
||||
above the if.`
|
||||
|
||||
var Analyzer = &analysis.Analyzer{
|
||||
Name: "ifinit",
|
||||
Doc: doc,
|
||||
Requires: []*analysis.Analyzer{inspect.Analyzer},
|
||||
Run: run,
|
||||
}
|
||||
|
||||
func run(pass *analysis.Pass) (any, error) {
|
||||
result, ok := pass.ResultOf[inspect.Analyzer]
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("inspect analyzer result is unavailable")
|
||||
}
|
||||
|
||||
insp, ok := result.(*inspector.Inspector)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("inspect analyzer returned %T, want *inspector.Inspector", result)
|
||||
}
|
||||
|
||||
insp.WithStack([]ast.Node{(*ast.IfStmt)(nil)}, func(n ast.Node, isPush bool, stack []ast.Node) bool {
|
||||
if !isPush {
|
||||
return false
|
||||
}
|
||||
|
||||
stmt, ok := n.(*ast.IfStmt)
|
||||
if !ok {
|
||||
return true
|
||||
}
|
||||
if stmt.Init == nil {
|
||||
return true
|
||||
}
|
||||
|
||||
start := pass.Fset.Position(stmt.Init.Pos())
|
||||
end := pass.Fset.Position(stmt.Init.End())
|
||||
if start.Line == end.Line {
|
||||
return true
|
||||
}
|
||||
|
||||
diag := analysis.Diagnostic{
|
||||
Pos: stmt.Init.Pos(),
|
||||
End: stmt.Init.End(),
|
||||
Message: fmt.Sprintf("if statement init clause spans %d lines; it must be on a single line", end.Line-start.Line+1),
|
||||
}
|
||||
|
||||
if fix, ok := hoist(pass, stmt, stack); ok {
|
||||
diag.SuggestedFixes = []analysis.SuggestedFix{fix}
|
||||
}
|
||||
|
||||
pass.Report(diag)
|
||||
|
||||
return true
|
||||
})
|
||||
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// hoist rewrites "if <init>; <cond> {" into "<init>" on its own line followed by
|
||||
// "if <cond> {". The init clause's own text is left untouched, so comments and
|
||||
// internal line breaks survive.
|
||||
func hoist(pass *analysis.Pass, stmt *ast.IfStmt, stack []ast.Node) (analysis.SuggestedFix, bool) {
|
||||
if !isHoistable(stmt, stack) {
|
||||
return analysis.SuggestedFix{}, false
|
||||
}
|
||||
|
||||
if !isShadowSafe(pass, stmt) {
|
||||
return analysis.SuggestedFix{}, false
|
||||
}
|
||||
|
||||
indent, ok := lineIndent(pass, stmt.Pos())
|
||||
if !ok {
|
||||
return analysis.SuggestedFix{}, false
|
||||
}
|
||||
|
||||
return analysis.SuggestedFix{
|
||||
Message: "Hoist the init clause above the if statement",
|
||||
TextEdits: []analysis.TextEdit{
|
||||
// Drop the leading "if ".
|
||||
{Pos: stmt.Pos(), End: stmt.Init.Pos()},
|
||||
// Turn the "; " separator into a line break and a fresh "if ".
|
||||
{Pos: stmt.Init.End(), End: stmt.Cond.Pos(), NewText: []byte("\n" + indent + "if ")},
|
||||
},
|
||||
}, true
|
||||
}
|
||||
|
||||
func isHoistable(stmt *ast.IfStmt, stack []ast.Node) bool {
|
||||
if len(stack) < 2 {
|
||||
return false
|
||||
}
|
||||
|
||||
switch parent := stack[len(stack)-2].(type) {
|
||||
case *ast.IfStmt:
|
||||
// An "else if": the init clause cannot move above the chain without
|
||||
// restructuring the whole thing into a nested else block.
|
||||
return parent.Else != ast.Stmt(stmt)
|
||||
case *ast.LabeledStmt:
|
||||
// Hoisting would leave the label pointing at the init clause instead.
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
// shadowSafe reports whether hoisting stmt's init clause out of the if's scope
|
||||
// leaves every name it binds meaning the same thing.
|
||||
//
|
||||
// Only ":=" declares anything, and only in the if statement's own scope. Moving
|
||||
// that declaration up one level is safe exactly when none of the names it binds
|
||||
// already resolve to something in the enclosing scope: otherwise the hoisted
|
||||
// ":=" either assigns to that existing object instead of declaring a fresh one,
|
||||
// or collides with a later declaration in the same block.
|
||||
//
|
||||
// The check is deliberately conservative. It refuses whenever a name resolves at
|
||||
// all -- including to a package-level or universe name -- rather than trying to
|
||||
// prove that nothing reads the shadowed object afterwards.
|
||||
func isShadowSafe(pass *analysis.Pass, stmt *ast.IfStmt) bool {
|
||||
assign, ok := stmt.Init.(*ast.AssignStmt)
|
||||
if !ok || assign.Tok != token.DEFINE {
|
||||
return true
|
||||
}
|
||||
|
||||
scope := pass.TypesInfo.Scopes[stmt]
|
||||
if scope == nil {
|
||||
return false
|
||||
}
|
||||
|
||||
enclosing := scope.Parent()
|
||||
if enclosing == nil {
|
||||
return false
|
||||
}
|
||||
|
||||
for _, lhs := range assign.Lhs {
|
||||
ident, ok := lhs.(*ast.Ident)
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
|
||||
if ident.Name == "_" {
|
||||
continue
|
||||
}
|
||||
|
||||
// token.NoPos makes the lookup ignore declaration order, so a name
|
||||
// declared later in the same block counts as a conflict too.
|
||||
if _, obj := enclosing.LookupParent(ident.Name, token.NoPos); obj != nil {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
// lineIndent returns the whitespace preceding pos on its line, and reports
|
||||
// whether pos is the first non-whitespace token on that line.
|
||||
func lineIndent(pass *analysis.Pass, pos token.Pos) (string, bool) {
|
||||
if pass.ReadFile == nil {
|
||||
return "", false
|
||||
}
|
||||
|
||||
file := pass.Fset.File(pos)
|
||||
if file == nil {
|
||||
return "", false
|
||||
}
|
||||
|
||||
src, err := pass.ReadFile(file.Name())
|
||||
if err != nil {
|
||||
return "", false
|
||||
}
|
||||
|
||||
lineStart := file.Offset(file.LineStart(file.Line(pos)))
|
||||
offset := file.Offset(pos)
|
||||
if offset > len(src) {
|
||||
return "", false
|
||||
}
|
||||
|
||||
prefix := string(src[lineStart:offset])
|
||||
if strings.TrimLeft(prefix, " \t") != "" {
|
||||
return "", false
|
||||
}
|
||||
|
||||
return prefix, true
|
||||
}
|
||||
13
ifinit/analyzer_test.go
Normal file
13
ifinit/analyzer_test.go
Normal file
@@ -0,0 +1,13 @@
|
||||
package ifinit_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"golang.org/x/tools/go/analysis/analysistest"
|
||||
|
||||
"git.offline-twitter.com/offline-labs/go-lint/ifinit"
|
||||
)
|
||||
|
||||
func TestAnalyzer(t *testing.T) {
|
||||
analysistest.RunWithSuggestedFixes(t, analysistest.TestData(), ifinit.Analyzer, "cases")
|
||||
}
|
||||
24
ifinit/plugin.go
Normal file
24
ifinit/plugin.go
Normal file
@@ -0,0 +1,24 @@
|
||||
package ifinit
|
||||
|
||||
import (
|
||||
"github.com/golangci/plugin-module-register/register"
|
||||
"golang.org/x/tools/go/analysis"
|
||||
)
|
||||
|
||||
func init() {
|
||||
register.Plugin("ifinit", New)
|
||||
}
|
||||
|
||||
type plugin struct{}
|
||||
|
||||
func New(any) (register.LinterPlugin, error) {
|
||||
return &plugin{}, nil
|
||||
}
|
||||
|
||||
func (p *plugin) BuildAnalyzers() ([]*analysis.Analyzer, error) {
|
||||
return []*analysis.Analyzer{Analyzer}, nil
|
||||
}
|
||||
|
||||
func (p *plugin) GetLoadMode() string {
|
||||
return register.LoadModeTypesInfo
|
||||
}
|
||||
88
ifinit/testdata/src/cases/scopes.go
vendored
Normal file
88
ifinit/testdata/src/cases/scopes.go
vendored
Normal file
@@ -0,0 +1,88 @@
|
||||
package cases
|
||||
|
||||
func do(args ...int) error { return nil }
|
||||
|
||||
var errState error
|
||||
|
||||
// Safe: both names are fresh in the block that would receive the hoisted statement.
|
||||
func freshNames() {
|
||||
if v, err := multi( // want `init clause spans 3 lines`
|
||||
1,
|
||||
); err != nil {
|
||||
_ = v
|
||||
}
|
||||
}
|
||||
|
||||
// Safe: "_" declares nothing and err is fresh.
|
||||
func blankAndFresh() {
|
||||
if _, err := multi( // want `init clause spans 3 lines`
|
||||
1,
|
||||
); err != nil {
|
||||
_ = err
|
||||
}
|
||||
}
|
||||
|
||||
// Safe: "=" assigns to an existing variable; it declares nothing at all.
|
||||
func plainAssign() error {
|
||||
var err error
|
||||
|
||||
if err = do( // want `init clause spans 3 lines`
|
||||
1,
|
||||
); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Unsafe: err already exists in the enclosing block. Hoisting the ":=" would
|
||||
// assign to that err rather than declaring a new one, so the returned error
|
||||
// would silently change.
|
||||
func shadowsExistingLocal() error {
|
||||
v, err := multi(1)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_ = v
|
||||
|
||||
if v2, err := multi( // want `init clause spans 3 lines`
|
||||
2,
|
||||
); err != nil {
|
||||
_ = v2
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
// Unsafe: the same names are redeclared later in this block, so hoisting turns
|
||||
// the later ":=" into "no new variables on left side of :=".
|
||||
func declaredLaterInBlock() {
|
||||
if v, err := multi( // want `init clause spans 3 lines`
|
||||
1,
|
||||
); err != nil {
|
||||
_ = v
|
||||
}
|
||||
|
||||
v, err := multi(2)
|
||||
_, _ = v, err
|
||||
}
|
||||
|
||||
// Unsafe: err is a parameter, so hoisting would rebind it for the rest of the body.
|
||||
func shadowsParam(err error) {
|
||||
if v, err := multi( // want `init clause spans 3 lines`
|
||||
1,
|
||||
); err != nil {
|
||||
_ = v
|
||||
}
|
||||
}
|
||||
|
||||
// Conservatively refused: shadowing a package-level name. Nothing reads errState
|
||||
// after the if here, so this one would in fact be safe -- the check does not try
|
||||
// to prove that.
|
||||
func shadowsPackageLevel() {
|
||||
if v, errState := multi( // want `init clause spans 3 lines`
|
||||
1,
|
||||
); errState != nil {
|
||||
_ = v
|
||||
}
|
||||
}
|
||||
91
ifinit/testdata/src/cases/scopes.go.golden
vendored
Normal file
91
ifinit/testdata/src/cases/scopes.go.golden
vendored
Normal file
@@ -0,0 +1,91 @@
|
||||
package cases
|
||||
|
||||
func do(args ...int) error { return nil }
|
||||
|
||||
var errState error
|
||||
|
||||
// Safe: both names are fresh in the block that would receive the hoisted statement.
|
||||
func freshNames() {
|
||||
v, err := multi( // want `init clause spans 3 lines`
|
||||
1,
|
||||
)
|
||||
if err != nil {
|
||||
_ = v
|
||||
}
|
||||
}
|
||||
|
||||
// Safe: "_" declares nothing and err is fresh.
|
||||
func blankAndFresh() {
|
||||
_, err := multi( // want `init clause spans 3 lines`
|
||||
1,
|
||||
)
|
||||
if err != nil {
|
||||
_ = err
|
||||
}
|
||||
}
|
||||
|
||||
// Safe: "=" assigns to an existing variable; it declares nothing at all.
|
||||
func plainAssign() error {
|
||||
var err error
|
||||
|
||||
err = do( // want `init clause spans 3 lines`
|
||||
1,
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Unsafe: err already exists in the enclosing block. Hoisting the ":=" would
|
||||
// assign to that err rather than declaring a new one, so the returned error
|
||||
// would silently change.
|
||||
func shadowsExistingLocal() error {
|
||||
v, err := multi(1)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_ = v
|
||||
|
||||
if v2, err := multi( // want `init clause spans 3 lines`
|
||||
2,
|
||||
); err != nil {
|
||||
_ = v2
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
// Unsafe: the same names are redeclared later in this block, so hoisting turns
|
||||
// the later ":=" into "no new variables on left side of :=".
|
||||
func declaredLaterInBlock() {
|
||||
if v, err := multi( // want `init clause spans 3 lines`
|
||||
1,
|
||||
); err != nil {
|
||||
_ = v
|
||||
}
|
||||
|
||||
v, err := multi(2)
|
||||
_, _ = v, err
|
||||
}
|
||||
|
||||
// Unsafe: err is a parameter, so hoisting would rebind it for the rest of the body.
|
||||
func shadowsParam(err error) {
|
||||
if v, err := multi( // want `init clause spans 3 lines`
|
||||
1,
|
||||
); err != nil {
|
||||
_ = v
|
||||
}
|
||||
}
|
||||
|
||||
// Conservatively refused: shadowing a package-level name. Nothing reads errState
|
||||
// after the if here, so this one would in fact be safe -- the check does not try
|
||||
// to prove that.
|
||||
func shadowsPackageLevel() {
|
||||
if v, errState := multi( // want `init clause spans 3 lines`
|
||||
1,
|
||||
); errState != nil {
|
||||
_ = v
|
||||
}
|
||||
}
|
||||
113
ifinit/testdata/src/cases/structural.go
vendored
Normal file
113
ifinit/testdata/src/cases/structural.go
vendored
Normal file
@@ -0,0 +1,113 @@
|
||||
package cases
|
||||
|
||||
func multi(args ...int) (int, error) { return 0, nil }
|
||||
|
||||
func single() (int, error) { return 0, nil }
|
||||
|
||||
func count(args ...int) int { return 0 }
|
||||
|
||||
func clean() {
|
||||
if v, err := single(); err != nil {
|
||||
_ = v
|
||||
}
|
||||
|
||||
x := count(
|
||||
1,
|
||||
2,
|
||||
)
|
||||
if x > 0 {
|
||||
_ = x
|
||||
}
|
||||
}
|
||||
|
||||
func multiLineInit() {
|
||||
if v, err := multi( // want `init clause spans 4 lines`
|
||||
1,
|
||||
2,
|
||||
); err != nil {
|
||||
_ = v
|
||||
}
|
||||
}
|
||||
|
||||
func elseIfChain() {
|
||||
if x := count(1); x > 0 {
|
||||
_ = x
|
||||
} else if v, err := multi( // want `init clause spans 3 lines`
|
||||
1,
|
||||
); err != nil {
|
||||
_ = v
|
||||
}
|
||||
}
|
||||
|
||||
func nestedInsideBody() {
|
||||
if true {
|
||||
if v, err := multi( // want `init clause spans 3 lines`
|
||||
1,
|
||||
); err != nil {
|
||||
_ = v
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Only "if" init clauses are covered; for and switch are deliberately untouched.
|
||||
func otherInitClauses() {
|
||||
for i := count(
|
||||
1,
|
||||
); i < 10; i++ {
|
||||
_ = i
|
||||
}
|
||||
|
||||
switch v := count(
|
||||
1,
|
||||
); v {
|
||||
case 1:
|
||||
}
|
||||
}
|
||||
|
||||
func doSomething(args ...int) error { return nil }
|
||||
|
||||
func setup(args ...int) {}
|
||||
|
||||
// The line break falls after ":=" rather than inside the call's arguments.
|
||||
func brokenAfterAssignOp() {
|
||||
if result, err := // want `init clause spans 2 lines`
|
||||
multi(1, 2); err != nil {
|
||||
_ = result
|
||||
}
|
||||
}
|
||||
|
||||
// Assignment to an existing variable rather than a definition.
|
||||
func assignNotDefine() error {
|
||||
var err error
|
||||
if err = doSomething( // want `init clause spans 4 lines`
|
||||
1,
|
||||
2,
|
||||
); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// The init clause is a bare call expression, not an assignment at all.
|
||||
func exprStmtInit(condition bool) {
|
||||
if setup( // want `init clause spans 4 lines`
|
||||
1,
|
||||
2,
|
||||
); condition {
|
||||
_ = condition
|
||||
}
|
||||
}
|
||||
|
||||
// gofmt joins "else" and "if", but the parser accepts them on separate lines --
|
||||
// and then the "if" really is the first token on its line. Hoisting here would
|
||||
// emit "} v, err := multi(...)", which does not compile.
|
||||
func elseOnItsOwnLine() {
|
||||
if true {
|
||||
} else
|
||||
if v, err := multi( // want `init clause spans 3 lines`
|
||||
1,
|
||||
); err != nil {
|
||||
_ = v
|
||||
}
|
||||
}
|
||||
118
ifinit/testdata/src/cases/structural.go.golden
vendored
Normal file
118
ifinit/testdata/src/cases/structural.go.golden
vendored
Normal file
@@ -0,0 +1,118 @@
|
||||
package cases
|
||||
|
||||
func multi(args ...int) (int, error) { return 0, nil }
|
||||
|
||||
func single() (int, error) { return 0, nil }
|
||||
|
||||
func count(args ...int) int { return 0 }
|
||||
|
||||
func clean() {
|
||||
if v, err := single(); err != nil {
|
||||
_ = v
|
||||
}
|
||||
|
||||
x := count(
|
||||
1,
|
||||
2,
|
||||
)
|
||||
if x > 0 {
|
||||
_ = x
|
||||
}
|
||||
}
|
||||
|
||||
func multiLineInit() {
|
||||
v, err := multi( // want `init clause spans 4 lines`
|
||||
1,
|
||||
2,
|
||||
)
|
||||
if err != nil {
|
||||
_ = v
|
||||
}
|
||||
}
|
||||
|
||||
func elseIfChain() {
|
||||
if x := count(1); x > 0 {
|
||||
_ = x
|
||||
} else if v, err := multi( // want `init clause spans 3 lines`
|
||||
1,
|
||||
); err != nil {
|
||||
_ = v
|
||||
}
|
||||
}
|
||||
|
||||
func nestedInsideBody() {
|
||||
if true {
|
||||
v, err := multi( // want `init clause spans 3 lines`
|
||||
1,
|
||||
)
|
||||
if err != nil {
|
||||
_ = v
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Only "if" init clauses are covered; for and switch are deliberately untouched.
|
||||
func otherInitClauses() {
|
||||
for i := count(
|
||||
1,
|
||||
); i < 10; i++ {
|
||||
_ = i
|
||||
}
|
||||
|
||||
switch v := count(
|
||||
1,
|
||||
); v {
|
||||
case 1:
|
||||
}
|
||||
}
|
||||
|
||||
func doSomething(args ...int) error { return nil }
|
||||
|
||||
func setup(args ...int) {}
|
||||
|
||||
// The line break falls after ":=" rather than inside the call's arguments.
|
||||
func brokenAfterAssignOp() {
|
||||
result, err := // want `init clause spans 2 lines`
|
||||
multi(1, 2)
|
||||
if err != nil {
|
||||
_ = result
|
||||
}
|
||||
}
|
||||
|
||||
// Assignment to an existing variable rather than a definition.
|
||||
func assignNotDefine() error {
|
||||
var err error
|
||||
err = doSomething( // want `init clause spans 4 lines`
|
||||
1,
|
||||
2,
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// The init clause is a bare call expression, not an assignment at all.
|
||||
func exprStmtInit(condition bool) {
|
||||
setup( // want `init clause spans 4 lines`
|
||||
1,
|
||||
2,
|
||||
)
|
||||
if condition {
|
||||
_ = condition
|
||||
}
|
||||
}
|
||||
|
||||
// gofmt joins "else" and "if", but the parser accepts them on separate lines --
|
||||
// and then the "if" really is the first token on its line. Hoisting here would
|
||||
// emit "} v, err := multi(...)", which does not compile.
|
||||
func elseOnItsOwnLine() {
|
||||
if true {
|
||||
} else
|
||||
if v, err := multi( // want `init clause spans 3 lines`
|
||||
1,
|
||||
); err != nil {
|
||||
_ = v
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user