Repository: mgechev/revive
Pull request #821 (2023-05-08 00:10:46)

refactor: extract shared code for linting if-else chains
The rules `early-return`, `indent-error-flow` and `superfluous-else` have a similar structure. This moves the common logic for classifying if-else chains to a common package.

A few side benefits:
- "early-return" now handles os.Exit/log.Panicf/etc
- "superfluous-else" now handles (builtin) panic
- "superfluous-else" and "indent-error-flow" now handle if/else chains with 2+ "if" branches

Motivation: preliminary refactor to make [#816](https://github.com/mgechev/revive/issues/816) easier to implement. Tidying up code I added to `early-return` rule previously.

I added tests wherever I noticed a change in behaviour (noted above).

I added the new package at `internal/ifelse`. If there's a better home, let me know.


Patch:
diff --git a/RULES_DESCRIPTIONS.md b/RULES_DESCRIPTIONS.md
index e37640b..c52719b 100644
--- a/RULES_DESCRIPTIONS.md
+++ b/RULES_DESCRIPTIONS.md
@@ -92,7 +92,7 @@ Example:
 
 ```toml
 [rule.add-constant]
-  arguments = [{maxLitCount = "3",allowStrs ="\"\"",allowInts="0,1,2",allowFloats="0.0,0.,1.0,1.,2.0,2.","ignoreFuncs": "os\\.*,fmt\\.Println,make"}]
+  arguments = [{ maxLitCount = "3", allowStrs = "\"\"", allowInts = "0,1,2", allowFloats = "0.0,0.,1.0,1.,2.0,2.", ignoreFuncs = "os\\.*,fmt\\.Println,make" }]
 ```
 
 ## argument-limit
diff --git a/config/config.go b/config/config.go
index 4fbab0e..04cd214 100644
--- a/config/config.go
+++ b/config/config.go
@@ -3,7 +3,7 @@ package config
 import (
 	"errors"
 	"fmt"
-	"io/ioutil"
+	"os"
 
 	"github.com/mgechev/revive/formatter"
 
@@ -140,7 +140,7 @@ func GetLintingRules(config *lint.Config, extraRules []lint.Rule) ([]lint.Rule,
 }
 
 func parseConfig(path string, config *lint.Config) error {
-	file, err := ioutil.ReadFile(path)
+	file, err := os.ReadFile(path)
 	if err != nil {
 		return errors.New("cannot read the config file")
 	}
diff --git a/go.mod b/go.mod
index cb1d7d9..a0b349d 100644
--- a/go.mod
+++ b/go.mod
@@ -11,12 +11,12 @@ require (
 	github.com/mitchellh/go-homedir v1.1.0
 	github.com/olekukonko/tablewriter v0.0.5
 	github.com/pkg/errors v0.9.1
-	golang.org/x/tools v0.7.0
+	golang.org/x/tools v0.9.1
 )
 
 require (
 	github.com/mattn/go-colorable v0.1.13 // indirect
 	github.com/mattn/go-isatty v0.0.17 // indirect
 	github.com/mattn/go-runewidth v0.0.9 // indirect
-	golang.org/x/sys v0.6.0 // indirect
+	golang.org/x/sys v0.8.0 // indirect
 )
diff --git a/go.sum b/go.sum
index 122e938..72eaa9b 100644
--- a/go.sum
+++ b/go.sum
@@ -67,6 +67,8 @@ golang.org/x/sys v0.5.0 h1:MUK/U/4lj1t1oPg0HfuXDN/Z1wv31ZJ/YcPiGccS4DU=
 golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.6.0 h1:MVltZSvRTcU2ljQOhs94SXPftV6DCNnZViHeQps87pQ=
 golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.8.0 h1:EBmGv8NaZBZTWvrbjNoL6HVt+IVy3QDQpJs7VRIw3tU=
+golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/tools v0.1.12 h1:VveCTK38A2rkS8ZqFY25HIDFscX5X9OoEhJd3quQmXU=
 golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
 golang.org/x/tools v0.2.0 h1:G6AHpWxTMGY1KyEYoAQ5WTtIekUUvDNjan3ugu60JvE=
@@ -81,6 +83,8 @@ golang.org/x/tools v0.6.0 h1:BOw41kyTf3PuCW1pVQf8+Cyg8pMlkYB1oo9iJ6D/lKM=
 golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
 golang.org/x/tools v0.7.0 h1:W4OVu8VVOaIO0yzWMNdepAulS7YfoS3Zabrm8DOXXU4=
 golang.org/x/tools v0.7.0/go.mod h1:4pg6aUX35JBAogB10C9AtvVL+qowtN4pT3CGSQex14s=
+golang.org/x/tools v0.9.1 h1:8WMNJAz3zrtPmnYC7ISf5dEn3MT0gY7jBJfw27yrrLo=
+golang.org/x/tools v0.9.1/go.mod h1:owI94Op576fPu3cIGQeHs3joujW/2Oc6MtlxbF5dfNc=
 gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
 gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
 gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
diff --git a/internal/ifelse/branch.go b/internal/ifelse/branch.go
new file mode 100644
index 0000000..0d51a0a
--- /dev/null
+++ b/internal/ifelse/branch.go
@@ -0,0 +1,68 @@
+package ifelse
+
+import (
+	"fmt"
+	"go/ast"
+	"go/token"
+)
+
+// Branch contains information about a branch within an if-else chain.
+type Branch struct {
+	BranchKind
+	Call // The function called at the end for kind Panic or Exit.
+}
+
+// BlockBranch gets the Branch of an ast.BlockStmt.
+func BlockBranch(block *ast.BlockStmt) Branch {
+	blockLen := len(block.List)
+	if blockLen == 0 {
+		return Branch{BranchKind: Empty}
+	}
+
+	switch stmt := block.List[blockLen-1].(type) {
+	case *ast.ReturnStmt:
+		return Branch{BranchKind: Return}
+	case *ast.BlockStmt:
+		return BlockBranch(stmt)
+	case *ast.BranchStmt:
+		switch stmt.Tok {
+		case token.BREAK:
+			return Branch{BranchKind: Break}
+		case token.CONTINUE:
+			return Branch{BranchKind: Continue}
+		case token.GOTO:
+			return Branch{BranchKind: Goto}
+		}
+	case *ast.ExprStmt:
+		fn, ok := ExprCall(stmt)
+		if !ok {
+			break
+		}
+		kind, ok := DeviatingFuncs[fn]
+		if ok {
+			return Branch{BranchKind: kind, Call: fn}
+		}
+	}
+
+	return Branch{BranchKind: Regular}
+}
+
+// String returns a brief string representation
+func (b Branch) String() string {
+	switch b.BranchKind {
+	case Panic, Exit:
+		return fmt.Sprintf("... %v()", b.Call)
+	default:
+		return b.BranchKind.String()
+	}
+}
+
+// LongString returns a longer form string representation
+func (b Branch) LongString() string {
+	switch b.BranchKind {
+	case Panic, Exit:
+		return fmt.Sprintf("call to %v function", b.Call)
+	default:
+		return b.BranchKind.LongString()
+	}
+}
diff --git a/internal/ifelse/branch_kind.go b/internal/ifelse/branch_kind.go
new file mode 100644
index 0000000..7df276f
--- /dev/null
+++ b/internal/ifelse/branch_kind.go
@@ -0,0 +1,98 @@
+package ifelse
+
+// BranchKind is a classifier for if-else branches. It says whether the branch is empty,
+// and whether the branch ends with a statement that deviates control flow.
+type BranchKind int
+
+const (
+	// Empty branches do nothing
+	Empty BranchKind = iota
+
+	// Return branches return from the current function
+	Return
+
+	// Continue branches continue a surrounding "for" loop
+	Continue
+
+	// Break branches break a surrounding "for" loop
+	Break
+
+	// Goto branches conclude with a "goto" statement
+	Goto
+
+	// Panic branches panic the current function
+	Panic
+
+	// Exit branches end the program
+	Exit
+
+	// Regular branches do not fit any category above
+	Regular
+)
+
+// IsEmpty tests if the branch is empty
+func (k BranchKind) IsEmpty() bool { return k == Empty }
+
+// Returns tests if the branch returns from the current function
+func (k BranchKind) Returns() bool { return k == Return }
+
+// Deviates tests if the control does not flow to the first
+// statement following the if-else chain.
+func (k BranchKind) Deviates() bool {
+	switch k {
+	case Empty, Regular:
+		return false
+	case Return, Continue, Break, Goto, Panic, Exit:
+		return true
+	default:
+		panic("invalid kind")
+	}
+}
+
+// String returns a brief string representation
+func (k BranchKind) String() string {
+	switch k {
+	case Empty:
+		return ""
+	case Regular:
+		return "..."
+	case Return:
+		return "... return"
+	case Continue:
+		return "... continue"
+	case Break:
+		return "... break"
+	case Goto:
+		return "... goto"
+	case Panic:
+		return "... panic()"
+	case Exit:
+		return "... os.Exit()"
+	default:
+		panic("invalid kind")
+	}
+}
+
+// LongString returns a longer form string representation
+func (k BranchKind) LongString() string {
+	switch k {
+	case Empty:
+		return "an empty block"
+	case Regular:
+		return "a regular statement"
+	case Return:
+		return "a return statement"
+	case Continue:
+		return "a continue statement"
+	case Break:
+		return "a break statement"
+	case Goto:
+		return "a goto statement"
+	case Panic:
+		return "a function call that panics"
+	case Exit:
+		return "a function call that exits the program"
+	default:
+		panic("invalid kind")
+	}
+}
diff --git a/internal/ifelse/chain.go b/internal/ifelse/chain.go
new file mode 100644
index 0000000..d6afebf
--- /dev/null
+++ b/internal/ifelse/chain.go
@@ -0,0 +1,9 @@
+package ifelse
+
+// Chain contains information about an if-else chain.
+type Chain struct {
+	If                   Branch // what happens at the end of the "if" block
+	Else                 Branch // what happens at the end of the "else" block
+	HasInitializer       bool   // is there an "if"-initializer somewhere in the chain?
+	HasPriorNonDeviating bool   // is there a prior "if" block that does NOT deviate control flow?
+}
diff --git a/internal/ifelse/doc.go b/internal/ifelse/doc.go
new file mode 100644
index 0000000..0aa2c98
--- /dev/null
+++ b/internal/ifelse/doc.go
@@ -0,0 +1,6 @@
+// Package ifelse provides helpers for analysing the control flow in if-else chains,
+// presently used by the following rules:
+// - early-return
+// - indent-error-flow
+// - superfluous-else
+package ifelse
diff --git a/internal/ifelse/func.go b/internal/ifelse/func.go
new file mode 100644
index 0000000..7ba3519
--- /dev/null
+++ b/internal/ifelse/func.go
@@ -0,0 +1,51 @@
+package ifelse
+
+import (
+	"fmt"
+	"go/ast"
+)
+
+// Call contains the name of a function that deviates control flow.
+type Call struct {
+	Pkg  string // The package qualifier of the function, if not built-in.
+	Name string // The function name.
+}
+
+// DeviatingFuncs lists known control flow deviating function calls.
+var DeviatingFuncs = map[Call]BranchKind{
+	{"os", "Exit"}:     Exit,
+	{"log", "Fatal"}:   Exit,
+	{"log", "Fatalf"}:  Exit,
+	{"log", "Fatalln"}: Exit,
+	{"", "panic"}:      Panic,
+	{"log", "Panic"}:   Panic,
+	{"log", "Panicf"}:  Panic,
+	{"log", "Panicln"}: Panic,
+}
+
+// ExprCall gets the Call of an ExprStmt, if any.
+func ExprCall(expr *ast.ExprStmt) (Call, bool) {
+	call, ok := expr.X.(*ast.CallExpr)
+	if !ok {
+		return Call{}, false
+	}
+	switch v := call.Fun.(type) {
+	case *ast.Ident:
+		return Call{Name: v.Name}, true
+	case *ast.SelectorExpr:
+		if ident, ok := v.X.(*ast.Ident); ok {
+			return Call{Name: v.Sel.Name, Pkg: ident.Name}, true
+		}
+	}
+	return Call{}, false
+}
+
+// String returns the function name with package qualifier (if any)
+func (f Call) String() string {
+	switch {
+	case f.Pkg != "":
+		return fmt.Sprintf("%s.%s", f.Pkg, f.Name)
+	default:
+		return f.Name
+	}
+}
diff --git a/internal/ifelse/rule.go b/internal/ifelse/rule.go
new file mode 100644
index 0000000..ec0db01
--- /dev/null
+++ b/internal/ifelse/rule.go
@@ -0,0 +1,92 @@
+package ifelse
+
+import (
+	"go/ast"
+	"go/token"
+
+	"github.com/mgechev/revive/lint"
+)
+
+// Rule is an interface for linters operating on if-else chains
+type Rule interface {
+	CheckIfElse(chain Chain) (failMsg string)
+}
+
+// Apply evaluates the given Rule on if-else chains found within the given AST,
+// and returns the failures.
+//
+// Note that in if-else chain with multiple "if" blocks, only the *last* one is checked,
+// that is to say, given:
+//
+//	if foo {
+//	    ...
+//	} else if bar {
+//		...
+//	} else {
+//		...
+//	}
+//
+// Only the block following "bar" is linted. This is because the rules that use this function
+// do not presently have anything to say about earlier blocks in the chain.
+func Apply(rule Rule, node ast.Node, target Target) []lint.Failure {
+	v := &visitor{rule: rule, target: target}
+	ast.Walk(v, node)
+	return v.failures
+}
+
+type visitor struct {
+	failures []lint.Failure
+	target   Target
+	rule     Rule
+}
+
+func (v *visitor) Visit(node ast.Node) ast.Visitor {
+	ifStmt, ok := node.(*ast.IfStmt)
+	if !ok {
+		return v
+	}
+
+	v.visitChain(ifStmt, Chain{})
+	return nil
+}
+
+func (v *visitor) visitChain(ifStmt *ast.IfStmt, chain Chain) {
+	// look for other if-else chains nested inside this if { } block
+	ast.Walk(v, ifStmt.Body)
+
+	if ifStmt.Else == nil {
+		// no else branch
+		return
+	}
+
+	if as, ok := ifStmt.Init.(*ast.AssignStmt); ok && as.Tok == token.DEFINE {
+		chain.HasInitializer = true
+	}
+	chain.If = BlockBranch(ifStmt.Body)
+
+	switch elseBlock := ifStmt.Else.(type) {
+	case *ast.IfStmt:
+		if !chain.If.Deviates() {
+			chain.HasPriorNonDeviating = true
+		}
+		v.visitChain(elseBlock, chain)
+	case *ast.BlockStmt:
+		// look for other if-else chains nested inside this else { } block
+		ast.Walk(v, elseBlock)
+		chain.Else = BlockBranch(elseBlock)
+		if failMsg := v.rule.CheckIfElse(chain); failMsg != "" {
+			if chain.HasInitializer {
+				// if statement has a := initializer, so we might need to move the assignment
+				// onto its own line in case the body references it
+				failMsg += " (move short variable declaration to its own line if necessary)"
+			}
+			v.failures = append(v.failures, lint.Failure{
+				Confidence: 1,
+				Node:       v.target.node(ifStmt),
+				Failure:    failMsg,
+			})
+		}
+	default:
+		panic("invalid node type for else")
+	}
+}
diff --git a/internal/ifelse/target.go b/internal/ifelse/target.go
new file mode 100644
index 0000000..81ff1c3
--- /dev/null
+++ b/internal/ifelse/target.go
@@ -0,0 +1,25 @@
+package ifelse
+
+import "go/ast"
+
+// Target decides what line/column should be indicated by the rule in question.
+type Target int
+
+const (
+	// TargetIf means the text refers to the "if"
+	TargetIf Target = iota
+
+	// TargetElse means the text refers to the "else"
+	TargetElse
+)
+
+func (t Target) node(ifStmt *ast.IfStmt) ast.Node {
+	switch t {
+	case TargetIf:
+		return ifStmt
+	case TargetElse:
+		return ifStmt.Else
+	default:
+		panic("bad target")
+	}
+}
diff --git a/logging/logger.go b/logging/logger.go
index c911b2e..9d603a4 100644
--- a/logging/logger.go
+++ b/logging/logger.go
@@ -2,7 +2,6 @@ package logging
 
 import (
 	"io"
-	"io/ioutil"
 	"log"
 	"os"
 )
@@ -27,7 +26,7 @@ func GetLogger() (*log.Logger, error) {
 		}
 	} else {
 		// Suppress all logging output if debug mode is disabled
-		writer = ioutil.Discard
+		writer = io.Discard
 	}
 
 	logger = log.New(writer, "", log.LstdFlags)
diff --git a/revivelib/core.go b/revivelib/core.go
index d3df579..a275e9e 100644
--- a/revivelib/core.go
+++ b/revivelib/core.go
@@ -1,8 +1,8 @@
 package revivelib
 
 import (
-	"io/ioutil"
 	"log"
+	"os"
 	"strings"
 
 	"github.com/mgechev/dots"
@@ -88,7 +88,7 @@ func (r *Revive) Lint(patterns ...*LintPattern) (<-chan lint.Failure, error) {
 	}
 
 	revive := lint.New(func(file string) ([]byte, error) {
-		contents, err := ioutil.ReadFile(file)
+		contents, err := os.ReadFile(file)
 
 		if err != nil {
 			return nil, errors.Wrap(err, "reading file "+file)
diff --git a/rule/comment-spacings.go b/rule/comment-spacings.go
index e0898c3..0d75c55 100644
--- a/rule/comment-spacings.go
+++ b/rule/comment-spacings.go
@@ -23,6 +23,7 @@ func (r *CommentSpacingsRule) configure(arguments lint.Arguments) {
 		r.allowList = []string{
 			"//go:",
 			"//revive:",
+			"//nolint:",
 		}
 
 		for _, arg := range arguments {
diff --git a/rule/early-return.go b/rule/early-return.go
index ed0fcfa..f5f8ff5 100644
--- a/rule/early-return.go
+++ b/rule/early-return.go
@@ -2,9 +2,8 @@ package rule
 
 import (
 	"fmt"
-	"go/ast"
-	"go/token"
 
+	"github.com/mgechev/revive/internal/ifelse"
 	"github.com/mgechev/revive/lint"
 )
 
@@ -13,16 +12,8 @@ import (
 type EarlyReturnRule struct{}
 
 // Apply applies the rule to given file.
-func (*EarlyReturnRule) Apply(file *lint.File, _ lint.Arguments) []lint.Failure {
-	var failures []lint.Failure
-
-	onFailure := func(failure lint.Failure) {
-		failures = append(failures, failure)
-	}
-
-	w := lintEarlyReturnRule{onFailure: onFailure}
-	ast.Walk(w, file.AST)
-	return failures
+func (e *EarlyReturnRule) Apply(file *lint.File, _ lint.Arguments) []lint.Failure {
+	return ifelse.Apply(e, file.AST, ifelse.TargetIf)
 }
 
 // Name returns the rule name.
@@ -30,147 +21,26 @@ func (*EarlyReturnRule) Name() string {
 	return "early-return"
 }
 
-type lintEarlyReturnRule struct {
-	onFailure func(lint.Failure)
-}
-
-func (w lintEarlyReturnRule) Visit(node ast.Node) ast.Visitor {
-	ifStmt, ok := node.(*ast.IfStmt)
-	if !ok {
-		return w
-	}
-
-	w.visitIf(ifStmt, false, false)
-	return nil
-}
-
-func (w lintEarlyReturnRule) visitIf(ifStmt *ast.IfStmt, hasNonReturnBranch, hasIfInitializer bool) {
-	// look for other if-else chains nested inside this if { } block
-	ast.Walk(w, ifStmt.Body)
-
-	if ifStmt.Else == nil {
-		// no else branch
+// CheckIfElse evaluates the rule against an ifelse.Chain.
+func (e *EarlyReturnRule) CheckIfElse(chain ifelse.Chain) (failMsg string) {
+	if !chain.Else.Deviates() {
+		// this rule only applies if the else-block deviates control flow
 		return
 	}
 
-	if as, ok := ifStmt.Init.(*ast.AssignStmt); ok && as.Tok == token.DEFINE {
-		hasIfInitializer = true
-	}
-	bodyFlow := w.branchFlow(ifStmt.Body)
-
-	switch elseBlock := ifStmt.Else.(type) {
-	case *ast.IfStmt:
-		if bodyFlow.canFlowIntoNext() {
-			hasNonReturnBranch = true
-		}
-		w.visitIf(elseBlock, hasNonReturnBranch, hasIfInitializer)
-
-	case *ast.BlockStmt:
-		// look for other if-else chains nested inside this else { } block
-		ast.Walk(w, elseBlock)
-
-		if hasNonReturnBranch && bodyFlow != branchFlowEmpty {
-			// if we de-indent this block then a previous branch
-			// might flow into it, affecting program behaviour
-			return
-		}
-
-		if !bodyFlow.canFlowIntoNext() {
-			// avoid overlapping with superfluous-else
-			return
-		}
-
-		elseFlow := w.branchFlow(elseBlock)
-		if !elseFlow.canFlowIntoNext() {
-			failMsg := fmt.Sprintf("if c {%[1]s } else {%[2]s } can be simplified to if !c {%[2]s }%[1]s",
-				bodyFlow, elseFlow)
-
-			if hasIfInitializer {
-				// if statement has a := initializer, so we might need to move the assignment
-				// onto its own line in case the body references it
-				failMsg += " (move short variable declaration to its own line if necessary)"
-			}
-
-			w.onFailure(lint.Failure{
-				Confidence: 1,
-				Node:       ifStmt,
-				Failure:    failMsg,
-			})
-		}
-
-	default:
-		panic("invalid node type for else")
-	}
-}
-
-type branchFlowKind int
-
-const (
-	branchFlowEmpty branchFlowKind = iota
-	branchFlowReturn
-	branchFlowPanic
-	branchFlowContinue
-	branchFlowBreak
-	branchFlowGoto
-	branchFlowRegular
-)
-
-func (w lintEarlyReturnRule) branchFlow(block *ast.BlockStmt) branchFlowKind {
-	blockLen := len(block.List)
-	if blockLen == 0 {
-		return branchFlowEmpty
-	}
-
-	switch stmt := block.List[blockLen-1].(type) {
-	case *ast.ReturnStmt:
-		return branchFlowReturn
-	case *ast.BlockStmt:
-		return w.branchFlow(stmt)
-	case *ast.BranchStmt:
-		switch stmt.Tok {
-		case token.BREAK:
-			return branchFlowBreak
-		case token.CONTINUE:
-			return branchFlowContinue
-		case token.GOTO:
-			return branchFlowGoto
-		}
-	case *ast.ExprStmt:
-		if call, ok := stmt.X.(*ast.CallExpr); ok && isIdent(call.Fun, "panic") {
-			return branchFlowPanic
-		}
+	if chain.HasPriorNonDeviating && !chain.If.IsEmpty() {
+		// if we de-indent this block then a previous branch
+		// might flow into it, affecting program behaviour
+		return
 	}
 
-	return branchFlowRegular
-}
-
-// Whether this branch's control can flow into the next statement following the if-else chain
-func (k branchFlowKind) canFlowIntoNext() bool {
-	switch k {
-	case branchFlowReturn, branchFlowPanic, branchFlowContinue, branchFlowBreak, branchFlowGoto:
-		return false
-	default:
-		return true
+	if chain.If.Deviates() {
+		// avoid overlapping with superfluous-else
+		return
 	}
-}
 
-func (k branchFlowKind) String() string {
-	switch k {
-	case branchFlowEmpty:
-		return ""
-	case branchFlowReturn:
-		return " ... return"
-	case branchFlowPanic:
-		return " ... panic()"
-	case branchFlowContinue:
-		return " ... continue"
-	case branchFlowBreak:
-		return " ... break"
-	case branchFlowGoto:
-		return " ... goto"
-	case branchFlowRegular:
-		return " ..."
-	default:
-		panic("invalid kind")
+	if chain.If.IsEmpty() {
+		return fmt.Sprintf("if c { } else { %[1]v } can be simplified to if !c { %[1]v }", chain.Else)
 	}
+	return fmt.Sprintf("if c { ... } else { %[1]v } can be simplified to if !c { %[1]v } ...", chain.Else)
 }
diff --git a/rule/indent-error-flow.go b/rule/indent-error-flow.go
index e455801..4d62404 100644
--- a/rule/indent-error-flow.go
+++ b/rule/indent-error-flow.go
@@ -1,9 +1,7 @@
 package rule
 
 import (
-	"go/ast"
-	"go/token"
-
+	"github.com/mgechev/revive/internal/ifelse"
 	"github.com/mgechev/revive/lint"
 )
 
@@ -11,16 +9,8 @@ import (
 type IndentErrorFlowRule struct{}
 
 // Apply applies the rule to given file.
-func (*IndentErrorFlowRule) Apply(file *lint.File, _ lint.Arguments) []lint.Failure {
-	var failures []lint.Failure
-
-	onFailure := func(failure lint.Failure) {
-		failures = append(failures, failure)
-	}
-
-	w := lintElse{make(map[*ast.IfStmt]bool), onFailure}
-	ast.Walk(w, file.AST)
-	return failures
+func (e *IndentErrorFlowRule) Apply(file *lint.File, _ lint.Arguments) []lint.Failure {
+	return ifelse.Apply(e, file.AST, ifelse.TargetElse)
 }
 
 // Name returns the rule name.
@@ -28,51 +18,23 @@ func (*IndentErrorFlowRule) Name() string {
 	return "indent-error-flow"
 }
 
-type lintElse struct {
-	ignore    map[*ast.IfStmt]bool
-	onFailure func(lint.Failure)
-}
-
-func (w lintElse) Visit(node ast.Node) ast.Visitor {
-	ifStmt, ok := node.(*ast.IfStmt)
-	if !ok || ifStmt.Else == nil {
-		return w
-	}
-	if w.ignore[ifStmt] {
-		if elseif, ok := ifStmt.Else.(*ast.IfStmt); ok {
-			w.ignore[elseif] = true
-		}
-		return w
+// CheckIfElse evaluates the rule against an ifelse.Chain.
+func (e *IndentErrorFlowRule) CheckIfElse(chain ifelse.Chain) (failMsg string) {
+	if !chain.If.Deviates() {
+		// this rule only applies if the if-block deviates control flow
+		return
 	}
-	if elseif, ok := ifStmt.Else.(*ast.IfStmt); ok {
-		w.ignore[elseif] = true
-		return w
-	}
-	if _, ok := ifStmt.Else.(*ast.BlockStmt); !ok {
-		// only care about elses without conditions
-		return w
-	}
-	if len(ifStmt.Body.List) == 0 {
-		return w
-	}
-	shortDecl := false // does the if statement have a ":=" initialization statement?
-	if ifStmt.Init != nil {
-		if as, ok := ifStmt.Init.(*ast.AssignStmt); ok && as.Tok == token.DEFINE {
-			shortDecl = true
-		}
+
+	if chain.HasPriorNonDeviating {
+		// if we de-indent the "else" block then a previous branch
+		// might flow into it, affecting program behaviour
+		return
 	}
-	lastStmt := ifStmt.Body.List[len(ifStmt.Body.List)-1]
-	if _, ok := lastStmt.(*ast.ReturnStmt); ok {
-		extra := ""
-		if shortDecl {
-			extra = " (move short variable declaration to its own line if necessary)"
-		}
-		w.onFailure(lint.Failure{
-			Confidence: 1,
-			Node:       ifStmt.Else,
-			Category:   "indent",
-			Failure:    "if block ends with a return statement, so drop this else and outdent its block" + extra,
-		})
+
+	if !chain.If.Returns() {
+		// avoid overlapping with superfluous-else
+		return
 	}
-	return w
+
+	return "if block ends with a return statement, so drop this else and outdent its block"
 }
diff --git a/rule/nested-structs.go b/rule/nested-structs.go
index fd12269..147bd48 100644
--- a/rule/nested-structs.go
+++ b/rule/nested-structs.go
@@ -14,7 +14,6 @@ func (*NestedStructs) Apply(file *lint.File, _ lint.Arguments) []lint.Failure {
 	var failures []lint.Failure
 
 	walker := &lintNestedStructs{
-		fileAST: file.AST,
 		onFailure: func(failure lint.Failure) {
 			failures = append(failures, failure)
 		},
@@ -31,47 +30,46 @@ func (*NestedStructs) Name() string {
 }
 
 type lintNestedStructs struct {
-	fileAST   *ast.File
 	onFailure func(lint.Failure)
 }
 
 func (l *lintNestedStructs) Visit(n ast.Node) ast.Visitor {
-	switch v := n.(type) {
-	case *ast.TypeSpec:
-		_, isInterface := v.Type.(*ast.InterfaceType)
-		if isInterface {
-			return nil // do not analyze interface declarations
-		}
-	case *ast.FuncDecl:
-		if v.Body != nil {
-			ast.Walk(l, v.Body)
-		}
-		return nil
-	case *ast.Field:
-		_, isChannelField := v.Type.(*ast.ChanType)
-		if isChannelField {
-			return nil
-		}
+	if v, ok := n.(*ast.StructType); ok {
+		ls := &lintStruct{l.onFailure}
+		ast.Walk(ls, v.Fields)
+	}
 
-		filter := func(n ast.Node) bool {
-			switch n.(type) {
-			case *ast.StructType:
-				return true
-			default:
-				return false
-			}
-		}
-		structs := pick(v, filter, nil)
-		for _, s := range structs {
-			l.onFailure(lint.Failure{
-				Failure:    "no nested structs are allowed",
-				Category:   "style",
-				Node:       s,
-				Confidence: 1,
-			})
+	return l
+}
+
+type lintStruct struct {
+	onFailure func(lint.Failure)
+}
+
+func (l *lintStruct) Visit(n ast.Node) ast.Visitor {
+	switch s := n.(type) {
+	case *ast.StructType:
+		l.fail(s)
+		return nil
+	case *ast.ArrayType:
+		if _, ok := s.Elt.(*ast.StructType); ok {
+			l.fail(s)
 		}
-		return nil // no need to visit (again) the field
+		return nil
+	case *ast.ChanType:
+		return nil
+	case *ast.MapType:
+		return nil
+	default:
+		return l
 	}
+}
 
-	return l
+func (l *lintStruct) fail(n ast.Node) {
+	l.onFailure(lint.Failure{
+		Failure:    "no nested structs are allowed",
+		Category:   "style",
+		Node:       n,
+		Confidence: 1,
+	})
 }
diff --git a/rule/struct-tag.go b/rule/struct-tag.go
index 45308d3..d1c8056 100644
--- a/rule/struct-tag.go
+++ b/rule/struct-tag.go
@@ -137,7 +137,7 @@ func (lintStructTagRule) getTagName(tag *structtag.Tag) string {
 	case keyProtobuf:
 		for _, option := range tag.Options {
 			if strings.HasPrefix(option, "name=") {
-				return strings.TrimLeft(option, "name=")
+				return strings.TrimPrefix(option, "name=")
 			}
 		}
 		return "" //protobuf tag lacks 'name' option
diff --git a/rule/superfluous-else.go b/rule/superfluous-else.go
index a9e4380..9dce59a 100644
--- a/rule/superfluous-else.go
+++ b/rule/superfluous-else.go
@@ -2,9 +2,7 @@ package rule
 
 import (
 	"fmt"
-	"go/ast"
-	"go/token"
-
+	"github.com/mgechev/revive/internal/ifelse"
 	"github.com/mgechev/revive/lint"
 )
 
@@ -12,27 +10,8 @@ import (
 type SuperfluousElseRule struct{}
 
 // Apply applies the rule to given file.
-func (*SuperfluousElseRule) Apply(file *lint.File, _ lint.Arguments) []lint.Failure {
-	var failures []lint.Failure
-	onFailure := func(failure lint.Failure) {
-		failures = append(failures, failure)
-	}
-
-	branchingFunctions := map[string]map[string]bool{
-		"os": {"Exit": true},
-		"log": {
-			"Fatal":   true,
-			"Fatalf":  true,
-			"Fatalln": true,
-			"Panic":   true,
-			"Panicf":  true,
-			"Panicln": true,
-		},
-	}
-
-	w := lintSuperfluousElse{make(map[*ast.IfStmt]bool), onFailure, branchingFunctions}
-	ast.Walk(w, file.AST)
-	return failures
+func (e *SuperfluousElseRule) Apply(file *lint.File, _ lint.Arguments) []lint.Failure {
+	return ifelse.Apply(e, file.AST, ifelse.TargetElse)
 }
 
 // Name returns the rule name.
@@ -40,75 +19,23 @@ func (*SuperfluousElseRule) Name() string {
 	return "superfluous-else"
 }
 
-type lintSuperfluousElse struct {
-	ignore             map[*ast.IfStmt]bool
-	onFailure          func(lint.Failure)
-	branchingFunctions map[string]map[string]bool
-}
-
-func (w lintSuperfluousElse) Visit(node ast.Node) ast.Visitor {
-	ifStmt, ok := node.(*ast.IfStmt)
-	if !ok || ifStmt.Else == nil {
-		return w
-	}
-	if w.ignore[ifStmt] {
-		if elseif, ok := ifStmt.Else.(*ast.IfStmt); ok {
-			w.ignore[elseif] = true
-		}
-		return w
-	}
-	if elseif, ok := ifStmt.Else.(*ast.IfStmt); ok {
-		w.ignore[elseif] = true
-		return w
-	}
-	if _, ok := ifStmt.Else.(*ast.BlockStmt); !ok {
-		// only care about elses without conditions
-		return w
-	}
-	if len(ifStmt.Body.List) == 0 {
-		return w
-	}
-	shortDecl := false // does the if statement have a ":=" initialization statement?
-	if ifStmt.Init != nil {
-		if as, ok := ifStmt.Init.(*ast.AssignStmt); ok && as.Tok == token.DEFINE {
-			shortDecl = true
-		}
-	}
-	extra := ""
-	if shortDecl {
-		extra = " (move short variable declaration to its own line if necessary)"
+// CheckIfElse evaluates the rule against an ifelse.Chain.
+func (e *SuperfluousElseRule) CheckIfElse(chain ifelse.Chain) (failMsg string) {
+	if !chain.If.Deviates() {
+		// this rule only applies if the if-block deviates control flow
+		return
 	}
 
-	lastStmt := ifStmt.Body.List[len(ifStmt.Body.List)-1]
-	switch stmt := lastStmt.(type) {
-	case *ast.BranchStmt:
-		tok := stmt.Tok.String()
-		if tok != "fallthrough" {
-			w.onFailure(newFailure(ifStmt.Else, "if block ends with a "+tok+" statement, so drop this else and outdent its block"+extra))
-		}
-	case *ast.ExprStmt:
-		if ce, ok := stmt.X.(*ast.CallExpr); ok { // it's a function call
-			if fc, ok := ce.Fun.(*ast.SelectorExpr); ok {
-				if id, ok := fc.X.(*ast.Ident); ok {
-					fn := fc.Sel.Name
-					pkg := id.Name
-					if w.branchingFunctions[pkg][fn] { // it's a call to a branching function
-						w.onFailure(
-							newFailure(ifStmt.Else, fmt.Sprintf("if block ends with call to %s.%s function, so drop this else and outdent its block%s", pkg, fn, extra)))
-					}
-				}
-			}
-		}
+	if chain.HasPriorNonDeviating {
+		// if we de-indent the "else" block then a previous branch
+		// might flow into it, affecting program behaviour
+		return
 	}
 
-	return w
-}
-
-func newFailure(node ast.Node, msg string) lint.Failure {
-	return lint.Failure{
-		Confidence: 1,
-		Node:       node,
-		Category:   "indent",
-		Failure:    msg,
+	if chain.If.Returns() {
+		// avoid overlapping with indent-error-flow
+		return
 	}
+
+	return fmt.Sprintf("if block ends with %v, so drop this else and outdent its block", chain.If.LongString())
 }


Test patch:
diff --git a/test/golint_test.go b/test/golint_test.go
index f443688..7d12625 100644
--- a/test/golint_test.go
+++ b/test/golint_test.go
@@ -2,7 +2,7 @@ package test
 
 import (
 	"flag"
-	"io/ioutil"
+	"os"
 	"path"
 	"regexp"
 	"testing"
@@ -41,9 +41,9 @@ func TestAll(t *testing.T) {
 		t.Fatalf("Bad -lint.match value %q: %v", *lintMatch, err)
 	}
 
-	fis, err := ioutil.ReadDir(baseDir)
+	fis, err := os.ReadDir(baseDir)
 	if err != nil {
-		t.Fatalf("ioutil.ReadDir: %v", err)
+		t.Fatalf("os.ReadDir: %v", err)
 	}
 	if len(fis) == 0 {
 		t.Fatalf("no files in %v", baseDir)
@@ -53,12 +53,18 @@ func TestAll(t *testing.T) {
 			continue
 		}
 		t.Run(fi.Name(), func(t *testing.T) {
-			src, err := ioutil.ReadFile(path.Join(baseDir, fi.Name()))
+			filePath := path.Join(baseDir, fi.Name())
+			src, err := os.ReadFile(filePath)
 			if err != nil {
 				t.Fatalf("Failed reading %s: %v", fi.Name(), err)
 			}
 
-			if err := assertFailures(t, baseDir, fi, src, rules, map[string]lint.RuleConfig{}); err != nil {
+			fileInfo, err := os.Stat(filePath)
+			if err != nil {
+				t.Fatalf("Failed reading %s: %v", fi.Name(), err)
+			}
+
+			if err := assertFailures(t, baseDir, fileInfo, src, rules, map[string]lint.RuleConfig{}); err != nil {
 				t.Errorf("Linting %s: %v", fi.Name(), err)
 			}
 		})
diff --git a/test/utils.go b/test/utils.go
index 3de6761..6ebb205 100644
--- a/test/utils.go
+++ b/test/utils.go
@@ -8,7 +8,6 @@ import (
 	"go/parser"
 	"go/token"
 	"go/types"
-	"io/ioutil"
 	"os"
 	"strconv"
 	"strings"
@@ -21,7 +20,7 @@ import (
 func testRule(t *testing.T, filename string, rule lint.Rule, config ...*lint.RuleConfig) {
 	baseDir := "../testdata/"
 	filename = filename + ".go"
-	src, err := ioutil.ReadFile(baseDir + filename)
+	src, err := os.ReadFile(baseDir + filename)
 	if err != nil {
 		t.Fatalf("Bad filename path in test for %s: %v", rule.Name(), err)
 	}
@@ -42,7 +41,7 @@ func testRule(t *testing.T, filename string, rule lint.Rule, config ...*lint.Rul
 
 func assertSuccess(t *testing.T, baseDir string, fi os.FileInfo, rules []lint.Rule, config map[string]lint.RuleConfig) error {
 	l := lint.New(func(file string) ([]byte, error) {
-		return ioutil.ReadFile(baseDir + file)
+		return os.ReadFile(baseDir + file)
 	}, 0)
 
 	ps, err := l.Lint([][]string{{fi.Name()}}, rules, lint.Config{
@@ -64,7 +63,7 @@ func assertSuccess(t *testing.T, baseDir string, fi os.FileInfo, rules []lint.Ru
 
 func assertFailures(t *testing.T, baseDir string, fi os.FileInfo, src []byte, rules []lint.Rule, config map[string]lint.RuleConfig) error {
 	l := lint.New(func(file string) ([]byte, error) {
-		return ioutil.ReadFile(baseDir + file)
+		return os.ReadFile(baseDir + file)
 	}, 0)
 
 	ins := parseInstructions(t, fi.Name(), src)
diff --git a/testdata/comment-spacings.go b/testdata/comment-spacings.go
index 5b1d281..6d94137 100644
--- a/testdata/comment-spacings.go
+++ b/testdata/comment-spacings.go
@@ -39,3 +39,6 @@ Should be valid
 
 /* valid
  */
+
+//nolint:staticcheck // nolint should be in the default list of acceptable comments.
+var b string
diff --git a/testdata/early-return.go b/testdata/early-return.go
index 212b6c9..15475b0 100644
--- a/testdata/early-return.go
+++ b/testdata/early-return.go
@@ -2,6 +2,8 @@
 
 package fixtures
 
+import "os"
+
 func earlyRet() bool {
 	if cond { //   MATCH /if c { ... } else { ... return } can be simplified to if !c { ... return } .../
 		println()
@@ -124,4 +126,10 @@ func earlyRet() bool {
 	} else {
 		return false
 	}
+
+	if cond { //MATCH /if c { ... } else { ... os.Exit() } can be simplified to if !c { ... os.Exit() } .../
+		println()
+	} else {
+		os.Exit(0)
+	}
 }
diff --git a/testdata/golint/indent-error-flow.go b/testdata/golint/indent-error-flow.go
index 5d2fa19..64ea06e 100644
--- a/testdata/golint/indent-error-flow.go
+++ b/testdata/golint/indent-error-flow.go
@@ -38,3 +38,12 @@ func h(f func() bool, x int) string {
 	}
 }
 
+func i() string {
+	if err == author.ErrCourseNotFound {
+		return "not found"
+	} else if err == author.AnotherError {
+		return "something else"
+	} else { // MATCH /if block ends with a return statement, so drop this else and outdent its block/
+		return "okay"
+	}
+}
diff --git a/testdata/nested-structs.go b/testdata/nested-structs.go
index b1e1102..18f5e2d 100644
--- a/testdata/nested-structs.go
+++ b/testdata/nested-structs.go
@@ -18,6 +18,17 @@ type Quux struct {
 type Quuz struct {
 }
 
+type Quiz struct {
+	s struct{} // MATCH /no nested structs are allowed/
+}
+
+type nestedStructInChan struct {
+	c chan struct {
+		a int
+		b struct{ c int } // MATCH /no nested structs are allowed/
+	}
+}
+
 func waldo() (s struct{ b bool }) { return s }
 
 func fred() interface{} {
@@ -45,3 +56,9 @@ type issue744 struct {
 type mySetInterface interface {
 	GetSet() map[string]struct{}
 }
+
+// issue 824
+type test struct {
+	foo []chan struct{}     // Must not match
+	bar map[string]struct{} // Must not match
+}
diff --git a/testdata/struct-tag.go b/testdata/struct-tag.go
index 21e73aa..93cbc14 100644
--- a/testdata/struct-tag.go
+++ b/testdata/struct-tag.go
@@ -116,6 +116,8 @@ type Simple struct {
 	ODouble              *float64 `protobuf:"fixed64,014,opt,name=o_double,json=oDouble"`      // MATCH /duplicated tag number 14/
 	ODoubleStr           *float64 `protobuf:"fixed6,17,opt,name=o_double_str,json=oDoubleStr"` // MATCH /invalid protobuf tag name 'fixed6'/
 	OString              *string  `protobuf:"bytes,18,opt,name=o_string,json=oString"`
+	OString2             *string  `protobuf:"bytes,name=ameno"`
+	OString3             *string  `protobuf:"bytes,name=ameno"` // MATCH /duplicate tag name: 'ameno'/
 	XXX_NoUnkeyedLiteral struct{} `json:"-"`
 	XXX_unrecognized     []byte   `json:"-"`
 	XXX_sizecache        int32    `json:"-"`
diff --git a/testdata/superfluous-else.go b/testdata/superfluous-else.go
index bca11e5..aaade1b 100644
--- a/testdata/superfluous-else.go
+++ b/testdata/superfluous-else.go
@@ -4,9 +4,9 @@
 package pkg
 
 import (
-	"os"
 	"fmt"
 	"log"
+	"os"
 )
 
 func h(f func() bool) string {
@@ -74,72 +74,82 @@ func j(f func() bool) string {
 }
 
 func fatal1() string {
-		if f() {
-			a := 1
-			log.Fatal("x")
-		} else { // MATCH /if block ends with call to log.Fatal function, so drop this else and outdent its block/
-			log.Printf("non-positive")
-		}
+	if f() {
+		a := 1
+		log.Fatal("x")
+	} else { // MATCH /if block ends with call to log.Fatal function, so drop this else and outdent its block/
+		log.Printf("non-positive")
+	}
 	return "ok"
 }
 
 func fatal2() string {
-		if f() {
-			a := 1
-			log.Fatalf("x")
-		} else { // MATCH /if block ends with call to log.Fatalf function, so drop this else and outdent its block/
-			log.Printf("non-positive")
-		}
+	if f() {
+		a := 1
+		log.Fatalf("x")
+	} else { // MATCH /if block ends with call to log.Fatalf function, so drop this else and outdent its block/
+		log.Printf("non-positive")
+	}
 	return "ok"
 }
 
 func fatal3() string {
-		if f() {
-			a := 1
-			log.Fatalln("x")
-		} else { // MATCH /if block ends with call to log.Fatalln function, so drop this else and outdent its block/
-			log.Printf("non-positive")
-		}
+	if f() {
+		a := 1
+		log.Fatalln("x")
+	} else { // MATCH /if block ends with call to log.Fatalln function, so drop this else and outdent its block/
+		log.Printf("non-positive")
+	}
 	return "ok"
 }
 
 func exit1() string {
-		if f() {
-			a := 1
-			os.Exit(2)
-		} else { // MATCH /if block ends with call to os.Exit function, so drop this else and outdent its block/
-			log.Printf("non-positive")
-		}
+	if f() {
+		a := 1
+		os.Exit(2)
+	} else { // MATCH /if block ends with call to os.Exit function, so drop this else and outdent its block/
+		log.Printf("non-positive")
+	}
 	return "ok"
 }
 
 func Panic1() string {
-		if f() {
-			a := 1
-			log.Panic(2)
-		} else { // MATCH /if block ends with call to log.Panic function, so drop this else and outdent its block/
-			log.Printf("non-positive")
-		}
+	if f() {
+		a := 1
+		log.Panic(2)
+	} else { // MATCH /if block ends with call to log.Panic function, so drop this else and outdent its block/
+		log.Printf("non-positive")
+	}
 	return "ok"
 }
 
 func Panic2() string {
-		if f() {
-			a := 1
-			log.Panicf(2)
-		} else { // MATCH /if block ends with call to log.Panicf function, so drop this else and outdent its block/
-			log.Printf("non-positive")
-		}
+	if f() {
+		a := 1
+		log.Panicf(2)
+	} else { // MATCH /if block ends with call to log.Panicf function, so drop this else and outdent its block/
+		log.Printf("non-positive")
+	}
 	return "ok"
 }
 
 func Panic3() string {
-		if f() {
-			a := 1
-			log.Panicln(2)
-		} else { // MATCH /if block ends with call to log.Panicln function, so drop this else and outdent its block/
-			log.Printf("non-positive")
-		}
+	if f() {
+		a := 1
+		log.Panicln(2)
+	} else { // MATCH /if block ends with call to log.Panicln function, so drop this else and outdent its block/
+		log.Printf("non-positive")
+	}
+	return "ok"
+}
+
+func Panic4() string {
+	if f() {
+		a := 1
+		panic(2)
+	} else { // MATCH /if block ends with call to panic function, so drop this else and outdent its block/
+		log.Printf("non-positive")
+	}
 	return "ok"
 }
 
@@ -154,4 +164,14 @@ func noreg_19(f func() bool, x int) string {
 	} else {
 		// side effect
 	}
-}
\ No newline at end of file
+}
+
+func MultiBranch() string {
+	if _, ok := f(); ok {
+		continue
+	} else if _, err := get(); err == nil {
+		continue
+	} else { // MATCH /if block ends with a continue statement, so drop this else and outdent its block (move short variable declaration to its own line if necessary)/
+		delete(m, x)
+	}
+}
diff --git a/testdata/unused-param.go b/testdata/unused-param.go
index bd7acc3..a7ae1e8 100644
--- a/testdata/unused-param.go
+++ b/testdata/unused-param.go
@@ -3,7 +3,6 @@ package fixtures
 import (
 	"fmt"
 	"go/ast"
-	"io/ioutil"
 	"os"
 	"runtime"
 	"testing"
@@ -103,7 +102,7 @@ func getCompareFailCause(n *node, which int, prevValue string, prevIndex uint64)
 
 func assertSuccess(t *testing.T, baseDir string, fi os.FileInfo, src []byte, rules []lint.Rule, config map[string]lint.RuleConfig) error { // MATCH /parameter 'src' seems to be unused, consider removing or renaming it as _/
 	l := lint.New(func(file string) ([]byte, error) {
-		return ioutil.ReadFile(baseDir + file)
+		return os.ReadFile(baseDir + file)
 	})
 
 	ps, err := l.Lint([][]string{[]string{fi.Name()}}, rules, lint.Config{