Repository: go-critic/go-critic
Pull request #1290 (2022-11-08 23:17:29)

checkers/wrapperFunc: suggest `strings.Contains*` instead of `strings.Index* >= 0`
This change utilize the fact that the `strings.Contains*` functions are just wrappers for `strings.Index* >= 0`. The same goes for the `bytes` package.


Patch:
diff --git a/.github/workflows/go.yml b/.github/workflows/go.yml
index a0f87e8..e48dbb3 100644
--- a/.github/workflows/go.yml
+++ b/.github/workflows/go.yml
@@ -1,5 +1,7 @@
 name: Go
-on: [push, pull_request]
+
+on: [pull_request]
+
 jobs:
 
   build:
@@ -7,7 +9,7 @@ jobs:
     runs-on: ubuntu-latest
     strategy:
       matrix:
-        go: ['1.17', '1.18']
+        go: ['1.19.x', '1.20.x']
     steps:
 
     - name: Set up Go 1.x
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index 546f89f..192b3f4 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -8,7 +8,7 @@ jobs:
     runs-on: ubuntu-latest
     steps:
       # Drafts your next Release notes as Pull Requests are merged into "master"
-      - uses: toolmantim/release-drafter@v5.20.0
+      - uses: toolmantim/release-drafter@v5.23.0
         env:
           GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
 
diff --git a/.golangci.yml b/.golangci.yml
index 906f0bc..3dd8599 100644
--- a/.golangci.yml
+++ b/.golangci.yml
@@ -14,15 +14,14 @@
       "gofmt",
       "goimports",
       "golint",
-      "govet",
       "gosimple",
+      "govet",
       "ineffassign",
       "megacheck",
       "misspell",
       "nakedret",
       "staticcheck",
       "structcheck",
-      "typecheck",
       "unconvert",
       "unparam",
       "unused",
@@ -37,6 +36,7 @@
     "lll",
     "maligned",
     "prealloc",
+    "typecheck",
   ],
   "linters-settings": {
     "gocritic": {
diff --git a/Makefile b/Makefile
index cb72d63..8e30655 100644
--- a/Makefile
+++ b/Makefile
@@ -1,4 +1,4 @@
-.PHONY: test test-checker test-goroot docs ci ci-tidy ci-tests ci-linter cover install
+.PHONY: test test-checker test-goroot docs ci ci-tidy ci-tests ci-linter cover install gen-checker
 
 GOPATH_DIR=`go env GOPATH`
 
@@ -33,17 +33,17 @@ ci-tidy:
 	git diff --exit-code --quiet || (echo "Please run 'go mod tidy' to clean up the 'go.mod' and 'go.sum' files."; false)
 
 ci-tests:
-	GOCRITIC_EXTERNAL_TESTS=1 go test -v -race -count=1 -coverprofile=coverage.out ./...
+	go test -v -race -count=1 -coverprofile=coverage.out ./...
 
 ci-generate:
 	@go generate ./...
 	@git diff --exit-code --quiet || (echo "Please run 'go generate ./...' to update precompiled rules."; false)
 
 ci-linter:
-	@curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -b $(GOPATH_DIR)/bin v1.46.2
+	@curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -b $(GOPATH_DIR)/bin v1.51.1
 	@$(GOPATH_DIR)/bin/golangci-lint run -v
-	cd tools && go install github.com/quasilyte/go-consistent
-	@$(GOPATH_DIR)/bin/go-consistent ./...
+	# cd tools && go install github.com/quasilyte/go-consistent
+	# @$(GOPATH_DIR)/bin/go-consistent ./...
 	go build -o gocritic ./cmd/gocritic
 	./gocritic check -enableAll ./...
 
@@ -56,3 +56,53 @@ gocritic:
 
 install:
 	go install ./cmd/gocritic
+
+TEST_DIR=checkers/testdata/$(CHECKER_NAME)
+ifeq ($(CHECKER_TYPE),)
+CHECKER_TYPE_NAME=Expr
+CHECKER_TYPE=expr ast.Expr
+endif
+ifeq ($(CHECKER_CATEGORY),)
+CHECKER_CATEGORY=diagnostic
+endif
+
+define CHECKER_BODY
+package checkers
+
+import (
+	"go/ast"
+
+	"github.com/go-critic/go-critic/checkers/internal/astwalk"
+	"github.com/go-critic/go-critic/framework/linter"
+)
+
+func init() {
+	var info linter.CheckerInfo
+	info.Name = "$(CHECKER_NAME)"
+	info.Tags = []string{"$(CHECKER_CATEGORY)", "experimental"}
+	info.Summary = "Write your summary here."
+	info.Before = ``
+	info.After = ``
+
+	collection.AddChecker(&info, func(ctx *linter.CheckerContext) linter.FileWalker {
+		return astwalk.WalkerFor$(CHECKER_TYPE_NAME)(&$(CHECKER_NAME)Checker{ctx: ctx})
+	})
+}
+
+type $(CHECKER_NAME)Checker struct {
+	astwalk.WalkHandler
+	ctx *linter.CheckerContext
+}
+
+func (c *$(CHECKER_NAME)Checker) Visit$(CHECKER_TYPE_NAME)($(CHECKER_TYPE)) {
+	// your code goes here
+}
+endef
+
+export CHECKER_BODY
+
+gen-checker:
+	mkdir $(TEST_DIR)
+	echo "package checker_test" > $(TEST_DIR)/negative_tests.go
+	echo "package checker_test" > $(TEST_DIR)/positive_tests.go
+	echo "$$CHECKER_BODY" > checkers/$(CHECKER_NAME)_checker.go
diff --git a/checkers/analyzer/analyzer.go b/checkers/analyzer/analyzer.go
index f0f3600..2c92a93 100644
--- a/checkers/analyzer/analyzer.go
+++ b/checkers/analyzer/analyzer.go
@@ -2,7 +2,8 @@
 package analyzer
 
 import (
-	"github.com/go-critic/go-critic/framework/linter"
+	"github.com/go-critic/go-critic/linter"
+
 	"golang.org/x/tools/go/analysis"
 )
 
diff --git a/checkers/analyzer/run.go b/checkers/analyzer/run.go
index be23336..bb76d68 100644
--- a/checkers/analyzer/run.go
+++ b/checkers/analyzer/run.go
@@ -7,10 +7,10 @@ import (
 	"strings"
 	"sync"
 
-	"golang.org/x/tools/go/analysis"
-
 	_ "github.com/go-critic/go-critic/checkers" // Register go-critic checkers
-	"github.com/go-critic/go-critic/framework/linter"
+	"github.com/go-critic/go-critic/linter"
+
+	"golang.org/x/tools/go/analysis"
 )
 
 type gocritic struct {
diff --git a/checkers/appendAssign_checker.go b/checkers/appendAssign_checker.go
index a9324dd..a0727f4 100644
--- a/checkers/appendAssign_checker.go
+++ b/checkers/appendAssign_checker.go
@@ -6,7 +6,8 @@ import (
 	"go/types"
 
 	"github.com/go-critic/go-critic/checkers/internal/astwalk"
-	"github.com/go-critic/go-critic/framework/linter"
+	"github.com/go-critic/go-critic/linter"
+
 	"github.com/go-toolsmith/astequal"
 	"github.com/go-toolsmith/astp"
 	"golang.org/x/tools/go/ast/astutil"
diff --git a/checkers/appendCombine_checker.go b/checkers/appendCombine_checker.go
index 3c81449..cce1a8f 100644
--- a/checkers/appendCombine_checker.go
+++ b/checkers/appendCombine_checker.go
@@ -5,7 +5,8 @@ import (
 	"go/token"
 
 	"github.com/go-critic/go-critic/checkers/internal/astwalk"
-	"github.com/go-critic/go-critic/framework/linter"
+	"github.com/go-critic/go-critic/linter"
+
 	"github.com/go-toolsmith/astcast"
 	"github.com/go-toolsmith/astequal"
 )
diff --git a/checkers/badCond_checker.go b/checkers/badCond_checker.go
index 149f0ac..b0834ff 100644
--- a/checkers/badCond_checker.go
+++ b/checkers/badCond_checker.go
@@ -7,7 +7,8 @@ import (
 
 	"github.com/go-critic/go-critic/checkers/internal/astwalk"
 	"github.com/go-critic/go-critic/checkers/internal/lintutil"
-	"github.com/go-critic/go-critic/framework/linter"
+	"github.com/go-critic/go-critic/linter"
+
 	"github.com/go-toolsmith/astcast"
 	"github.com/go-toolsmith/astcopy"
 	"github.com/go-toolsmith/astequal"
@@ -114,30 +115,43 @@ func (c *badCondChecker) checkForStmt(stmt *ast.ForStmt) {
 
 	iter := astcast.ToIdent(init.Lhs[0])
 	cond := astcast.ToBinaryExpr(stmt.Cond)
-	if cond.Op != token.GTR || !astequal.Expr(iter, cond.X) {
+
+	var i, n ast.Expr
+	var op token.Token
+	switch {
+	case cond.Op == token.GTR && astequal.Expr(iter, cond.X):
+		i = cond.X
+		n = cond.Y
+		op = token.LSS
+	case cond.Op == token.LSS && astequal.Expr(iter, cond.Y):
+		i = cond.Y
+		n = cond.X
+		op = token.GTR
+	default:
 		return
 	}
-	if !typep.SideEffectFree(c.ctx.TypesInfo, cond.Y) {
+
+	if !typep.SideEffectFree(c.ctx.TypesInfo, n) {
 		return
 	}
 
 	post := astcast.ToIncDecStmt(stmt.Post)
-	if post.Tok != token.INC || !astequal.Expr(iter, post.X) {
+	if post.Tok != token.INC || !astequal.Expr(iter, i) {
 		return
 	}
 
-	mutated := lintutil.CouldBeMutated(c.ctx.TypesInfo, stmt.Body, cond.Y) ||
+	mutated := lintutil.CouldBeMutated(c.ctx.TypesInfo, stmt.Body, n) ||
 		lintutil.CouldBeMutated(c.ctx.TypesInfo, stmt.Body, iter)
 	if mutated {
 		return
 	}
 
-	c.warnForStmt(stmt, cond)
+	c.warnForStmt(stmt, op, cond)
 }
 
-func (c *badCondChecker) warnForStmt(cause ast.Node, cond *ast.BinaryExpr) {
+func (c *badCondChecker) warnForStmt(cause ast.Node, op token.Token, cond *ast.BinaryExpr) {
 	suggest := astcopy.BinaryExpr(cond)
-	suggest.Op = token.LSS
+	suggest.Op = op
 	c.ctx.Warn(cause, "`%s` in loop; probably meant `%s`?",
 		cond, suggest)
 }
diff --git a/checkers/badRegexp_checker.go b/checkers/badRegexp_checker.go
index 8a35900..4b957f0 100644
--- a/checkers/badRegexp_checker.go
+++ b/checkers/badRegexp_checker.go
@@ -9,7 +9,8 @@ import (
 	"unicode/utf8"
 
 	"github.com/go-critic/go-critic/checkers/internal/astwalk"
-	"github.com/go-critic/go-critic/framework/linter"
+	"github.com/go-critic/go-critic/linter"
+
 	"github.com/quasilyte/regex/syntax"
 )
 
diff --git a/checkers/boolExprSimplify_checker.go b/checkers/boolExprSimplify_checker.go
index b4000a8..9c7897f 100644
--- a/checkers/boolExprSimplify_checker.go
+++ b/checkers/boolExprSimplify_checker.go
@@ -5,16 +5,16 @@ import (
 	"go/token"
 	"strconv"
 
+	"github.com/go-critic/go-critic/checkers/internal/astwalk"
+	"github.com/go-critic/go-critic/checkers/internal/lintutil"
+	"github.com/go-critic/go-critic/linter"
+
 	"github.com/go-toolsmith/astcast"
 	"github.com/go-toolsmith/astcopy"
 	"github.com/go-toolsmith/astequal"
 	"github.com/go-toolsmith/astp"
 	"github.com/go-toolsmith/typep"
 	"golang.org/x/tools/go/ast/astutil"
-
-	"github.com/go-critic/go-critic/checkers/internal/astwalk"
-	"github.com/go-critic/go-critic/checkers/internal/lintutil"
-	"github.com/go-critic/go-critic/framework/linter"
 )
 
 func init() {
diff --git a/checkers/builtinShadowDecl_checker.go b/checkers/builtinShadowDecl_checker.go
index 94d51a9..0eaf9b8 100644
--- a/checkers/builtinShadowDecl_checker.go
+++ b/checkers/builtinShadowDecl_checker.go
@@ -4,7 +4,7 @@ import (
 	"go/ast"
 
 	"github.com/go-critic/go-critic/checkers/internal/astwalk"
-	"github.com/go-critic/go-critic/framework/linter"
+	"github.com/go-critic/go-critic/linter"
 )
 
 func init() {
diff --git a/checkers/builtinShadow_checker.go b/checkers/builtinShadow_checker.go
index 1e1661d..b910462 100644
--- a/checkers/builtinShadow_checker.go
+++ b/checkers/builtinShadow_checker.go
@@ -4,7 +4,7 @@ import (
 	"go/ast"
 
 	"github.com/go-critic/go-critic/checkers/internal/astwalk"
-	"github.com/go-critic/go-critic/framework/linter"
+	"github.com/go-critic/go-critic/linter"
 )
 
 func init() {
diff --git a/checkers/captLocal_checker.go b/checkers/captLocal_checker.go
index d9b4b7e..dd3a415 100644
--- a/checkers/captLocal_checker.go
+++ b/checkers/captLocal_checker.go
@@ -4,7 +4,7 @@ import (
 	"go/ast"
 
 	"github.com/go-critic/go-critic/checkers/internal/astwalk"
-	"github.com/go-critic/go-critic/framework/linter"
+	"github.com/go-critic/go-critic/linter"
 )
 
 func init() {
diff --git a/checkers/caseOrder_checker.go b/checkers/caseOrder_checker.go
index 047ea4f..e403476 100644
--- a/checkers/caseOrder_checker.go
+++ b/checkers/caseOrder_checker.go
@@ -5,7 +5,7 @@ import (
 	"go/types"
 
 	"github.com/go-critic/go-critic/checkers/internal/astwalk"
-	"github.com/go-critic/go-critic/framework/linter"
+	"github.com/go-critic/go-critic/linter"
 )
 
 func init() {
diff --git a/checkers/checkers.go b/checkers/checkers.go
index 0c2ebc0..5797daf 100644
--- a/checkers/checkers.go
+++ b/checkers/checkers.go
@@ -4,7 +4,7 @@ package checkers
 import (
 	"os"
 
-	"github.com/go-critic/go-critic/framework/linter"
+	"github.com/go-critic/go-critic/linter"
 )
 
 var collection = &linter.CheckerCollection{
diff --git a/checkers/codegenComment_checker.go b/checkers/codegenComment_checker.go
index 52a72d2..eebe55b 100644
--- a/checkers/codegenComment_checker.go
+++ b/checkers/codegenComment_checker.go
@@ -6,7 +6,7 @@ import (
 	"strings"
 
 	"github.com/go-critic/go-critic/checkers/internal/astwalk"
-	"github.com/go-critic/go-critic/framework/linter"
+	"github.com/go-critic/go-critic/linter"
 )
 
 func init() {
diff --git a/checkers/commentFormatting_checker.go b/checkers/commentFormatting_checker.go
index f330b72..40c8296 100644
--- a/checkers/commentFormatting_checker.go
+++ b/checkers/commentFormatting_checker.go
@@ -8,7 +8,7 @@ import (
 	"unicode/utf8"
 
 	"github.com/go-critic/go-critic/checkers/internal/astwalk"
-	"github.com/go-critic/go-critic/framework/linter"
+	"github.com/go-critic/go-critic/linter"
 )
 
 func init() {
diff --git a/checkers/commentedOutCode_checker.go b/checkers/commentedOutCode_checker.go
index 554e062..dcad296 100644
--- a/checkers/commentedOutCode_checker.go
+++ b/checkers/commentedOutCode_checker.go
@@ -8,7 +8,8 @@ import (
 	"strings"
 
 	"github.com/go-critic/go-critic/checkers/internal/astwalk"
-	"github.com/go-critic/go-critic/framework/linter"
+	"github.com/go-critic/go-critic/linter"
+
 	"github.com/go-toolsmith/strparse"
 )
 
diff --git a/checkers/commentedOutImport_checker.go b/checkers/commentedOutImport_checker.go
index 3c08656..2f33973 100644
--- a/checkers/commentedOutImport_checker.go
+++ b/checkers/commentedOutImport_checker.go
@@ -6,7 +6,7 @@ import (
 	"regexp"
 
 	"github.com/go-critic/go-critic/checkers/internal/astwalk"
-	"github.com/go-critic/go-critic/framework/linter"
+	"github.com/go-critic/go-critic/linter"
 )
 
 func init() {
diff --git a/checkers/defaultCaseOrder_checker.go b/checkers/defaultCaseOrder_checker.go
index e06944d..ef3a4c1 100644
--- a/checkers/defaultCaseOrder_checker.go
+++ b/checkers/defaultCaseOrder_checker.go
@@ -4,7 +4,7 @@ import (
 	"go/ast"
 
 	"github.com/go-critic/go-critic/checkers/internal/astwalk"
-	"github.com/go-critic/go-critic/framework/linter"
+	"github.com/go-critic/go-critic/linter"
 )
 
 func init() {
diff --git a/checkers/deferInLoop_checker.go b/checkers/deferInLoop_checker.go
index da90fe6..5b9266e 100644
--- a/checkers/deferInLoop_checker.go
+++ b/checkers/deferInLoop_checker.go
@@ -4,7 +4,7 @@ import (
 	"go/ast"
 
 	"github.com/go-critic/go-critic/checkers/internal/astwalk"
-	"github.com/go-critic/go-critic/framework/linter"
+	"github.com/go-critic/go-critic/linter"
 )
 
 func init() {
diff --git a/checkers/deprecatedComment_checker.go b/checkers/deprecatedComment_checker.go
index 0eb5072..dc4df5d 100644
--- a/checkers/deprecatedComment_checker.go
+++ b/checkers/deprecatedComment_checker.go
@@ -5,7 +5,7 @@ import (
 	"strings"
 
 	"github.com/go-critic/go-critic/checkers/internal/astwalk"
-	"github.com/go-critic/go-critic/framework/linter"
+	"github.com/go-critic/go-critic/linter"
 )
 
 func init() {
diff --git a/checkers/docStub_checker.go b/checkers/docStub_checker.go
index d8aaaf7..738544f 100644
--- a/checkers/docStub_checker.go
+++ b/checkers/docStub_checker.go
@@ -7,7 +7,7 @@ import (
 	"strings"
 
 	"github.com/go-critic/go-critic/checkers/internal/astwalk"
-	"github.com/go-critic/go-critic/framework/linter"
+	"github.com/go-critic/go-critic/linter"
 )
 
 func init() {
diff --git a/checkers/dupBranchBody_checker.go b/checkers/dupBranchBody_checker.go
index ad16e3b..734e894 100644
--- a/checkers/dupBranchBody_checker.go
+++ b/checkers/dupBranchBody_checker.go
@@ -4,7 +4,8 @@ import (
 	"go/ast"
 
 	"github.com/go-critic/go-critic/checkers/internal/astwalk"
-	"github.com/go-critic/go-critic/framework/linter"
+	"github.com/go-critic/go-critic/linter"
+
 	"github.com/go-toolsmith/astequal"
 )
 
diff --git a/checkers/dupCase_checker.go b/checkers/dupCase_checker.go
index a565007..9e36d99 100644
--- a/checkers/dupCase_checker.go
+++ b/checkers/dupCase_checker.go
@@ -5,7 +5,7 @@ import (
 
 	"github.com/go-critic/go-critic/checkers/internal/astwalk"
 	"github.com/go-critic/go-critic/checkers/internal/lintutil"
-	"github.com/go-critic/go-critic/framework/linter"
+	"github.com/go-critic/go-critic/linter"
 )
 
 func init() {
diff --git a/checkers/dupImports_checker.go b/checkers/dupImports_checker.go
index 54658eb..8f55d0b 100644
--- a/checkers/dupImports_checker.go
+++ b/checkers/dupImports_checker.go
@@ -4,7 +4,7 @@ import (
 	"fmt"
 	"go/ast"
 
-	"github.com/go-critic/go-critic/framework/linter"
+	"github.com/go-critic/go-critic/linter"
 )
 
 func init() {
diff --git a/checkers/dupSubExpr_checker.go b/checkers/dupSubExpr_checker.go
index 00f8fd0..3516091 100644
--- a/checkers/dupSubExpr_checker.go
+++ b/checkers/dupSubExpr_checker.go
@@ -6,7 +6,8 @@ import (
 	"go/types"
 
 	"github.com/go-critic/go-critic/checkers/internal/astwalk"
-	"github.com/go-critic/go-critic/framework/linter"
+	"github.com/go-critic/go-critic/linter"
+
 	"github.com/go-toolsmith/astequal"
 	"github.com/go-toolsmith/typep"
 )
diff --git a/checkers/elseif_checker.go b/checkers/elseif_checker.go
index dcc9648..5cd9a3e 100644
--- a/checkers/elseif_checker.go
+++ b/checkers/elseif_checker.go
@@ -4,7 +4,8 @@ import (
 	"go/ast"
 
 	"github.com/go-critic/go-critic/checkers/internal/astwalk"
-	"github.com/go-critic/go-critic/framework/linter"
+	"github.com/go-critic/go-critic/linter"
+
 	"github.com/go-toolsmith/astp"
 )
 
diff --git a/checkers/embedded_rules.go b/checkers/embedded_rules.go
index b17178e..7be115f 100644
--- a/checkers/embedded_rules.go
+++ b/checkers/embedded_rules.go
@@ -7,10 +7,10 @@ import (
 	"go/token"
 	"os"
 
-	"github.com/quasilyte/go-ruleguard/ruleguard"
-
 	"github.com/go-critic/go-critic/checkers/rulesdata"
-	"github.com/go-critic/go-critic/framework/linter"
+	"github.com/go-critic/go-critic/linter"
+
+	"github.com/quasilyte/go-ruleguard/ruleguard"
 )
 
 //go:generate go run ./rules/precompile.go -rules ./rules/rules.go -o ./rulesdata/rulesdata.go
diff --git a/checkers/emptyFallthrough_checker.go b/checkers/emptyFallthrough_checker.go
index ebb8dad..1b2f58c 100644
--- a/checkers/emptyFallthrough_checker.go
+++ b/checkers/emptyFallthrough_checker.go
@@ -5,7 +5,7 @@ import (
 	"go/token"
 
 	"github.com/go-critic/go-critic/checkers/internal/astwalk"
-	"github.com/go-critic/go-critic/framework/linter"
+	"github.com/go-critic/go-critic/linter"
 )
 
 func init() {
diff --git a/checkers/evalOrder_checker.go b/checkers/evalOrder_checker.go
index 6ba07fe..7365970 100644
--- a/checkers/evalOrder_checker.go
+++ b/checkers/evalOrder_checker.go
@@ -7,7 +7,8 @@ import (
 
 	"github.com/go-critic/go-critic/checkers/internal/astwalk"
 	"github.com/go-critic/go-critic/checkers/internal/lintutil"
-	"github.com/go-critic/go-critic/framework/linter"
+	"github.com/go-critic/go-critic/linter"
+
 	"github.com/go-toolsmith/astcast"
 	"github.com/go-toolsmith/astequal"
 	"github.com/go-toolsmith/typep"
diff --git a/checkers/exitAfterDefer_checker.go b/checkers/exitAfterDefer_checker.go
index 63e0049..2781dc2 100644
--- a/checkers/exitAfterDefer_checker.go
+++ b/checkers/exitAfterDefer_checker.go
@@ -4,7 +4,8 @@ import (
 	"go/ast"
 
 	"github.com/go-critic/go-critic/checkers/internal/astwalk"
-	"github.com/go-critic/go-critic/framework/linter"
+	"github.com/go-critic/go-critic/linter"
+
 	"github.com/go-toolsmith/astfmt"
 	"github.com/go-toolsmith/astp"
 	"golang.org/x/tools/go/ast/astutil"
diff --git a/checkers/filepathJoin_checker.go b/checkers/filepathJoin_checker.go
index 698f536..6d62ef2 100644
--- a/checkers/filepathJoin_checker.go
+++ b/checkers/filepathJoin_checker.go
@@ -5,7 +5,8 @@ import (
 	"strings"
 
 	"github.com/go-critic/go-critic/checkers/internal/astwalk"
-	"github.com/go-critic/go-critic/framework/linter"
+	"github.com/go-critic/go-critic/linter"
+
 	"github.com/go-toolsmith/astcast"
 )
 
diff --git a/checkers/flagName_checker.go b/checkers/flagName_checker.go
index 7f6ce3c..e704010 100644
--- a/checkers/flagName_checker.go
+++ b/checkers/flagName_checker.go
@@ -7,7 +7,8 @@ import (
 	"strings"
 
 	"github.com/go-critic/go-critic/checkers/internal/astwalk"
-	"github.com/go-critic/go-critic/framework/linter"
+	"github.com/go-critic/go-critic/linter"
+
 	"github.com/go-toolsmith/astcast"
 )
 
diff --git a/checkers/hexLiteral_checker.go b/checkers/hexLiteral_checker.go
index ae61a11..fead716 100644
--- a/checkers/hexLiteral_checker.go
+++ b/checkers/hexLiteral_checker.go
@@ -6,7 +6,8 @@ import (
 	"strings"
 
 	"github.com/go-critic/go-critic/checkers/internal/astwalk"
-	"github.com/go-critic/go-critic/framework/linter"
+	"github.com/go-critic/go-critic/linter"
+
 	"github.com/go-toolsmith/astcast"
 )
 
diff --git a/checkers/hugeParam_checker.go b/checkers/hugeParam_checker.go
index 910be18..07a1a22 100644
--- a/checkers/hugeParam_checker.go
+++ b/checkers/hugeParam_checker.go
@@ -4,8 +4,7 @@ import (
 	"go/ast"
 
 	"github.com/go-critic/go-critic/checkers/internal/astwalk"
-	"github.com/go-critic/go-critic/framework/linter"
-	"golang.org/x/exp/typeparams"
+	"github.com/go-critic/go-critic/linter"
 )
 
 func init() {
@@ -50,9 +49,6 @@ func (c *hugeParamChecker) checkParams(params []*ast.Field) {
 	for _, p := range params {
 		for _, id := range p.Names {
 			typ := c.ctx.TypeOf(id)
-			if _, ok := typ.(*typeparams.TypeParam); ok {
-				continue
-			}
 			size, ok := c.ctx.SizeOf(typ)
 			if ok && size >= c.sizeThreshold {
 				c.warn(id, size)
diff --git a/checkers/ifElseChain_checker.go b/checkers/ifElseChain_checker.go
index c3d127c..b36f1f1 100644
--- a/checkers/ifElseChain_checker.go
+++ b/checkers/ifElseChain_checker.go
@@ -4,7 +4,7 @@ import (
 	"go/ast"
 
 	"github.com/go-critic/go-critic/checkers/internal/astwalk"
-	"github.com/go-critic/go-critic/framework/linter"
+	"github.com/go-critic/go-critic/linter"
 )
 
 func init() {
diff --git a/checkers/importShadow_checker.go b/checkers/importShadow_checker.go
index 5ac711f..76a11e3 100644
--- a/checkers/importShadow_checker.go
+++ b/checkers/importShadow_checker.go
@@ -5,7 +5,7 @@ import (
 	"go/types"
 
 	"github.com/go-critic/go-critic/checkers/internal/astwalk"
-	"github.com/go-critic/go-critic/framework/linter"
+	"github.com/go-critic/go-critic/linter"
 )
 
 func init() {
diff --git a/checkers/initClause_checker.go b/checkers/initClause_checker.go
index a1b6b2a..8a3182f 100644
--- a/checkers/initClause_checker.go
+++ b/checkers/initClause_checker.go
@@ -4,7 +4,8 @@ import (
 	"go/ast"
 
 	"github.com/go-critic/go-critic/checkers/internal/astwalk"
-	"github.com/go-critic/go-critic/framework/linter"
+	"github.com/go-critic/go-critic/linter"
+
 	"github.com/go-toolsmith/astp"
 )
 
diff --git a/checkers/internal/astwalk/local_def_visitor.go b/checkers/internal/astwalk/local_def_visitor.go
index 47de589..0c9c149 100644
--- a/checkers/internal/astwalk/local_def_visitor.go
+++ b/checkers/internal/astwalk/local_def_visitor.go
@@ -7,9 +7,9 @@ import (
 // LocalDefVisitor visits every name definitions inside a function.
 //
 // Next elements are considered as name definitions:
-//	- Function parameters (input, output, receiver)
-//	- Every LHS of ":=" assignment that defines a new name
-//	- Every local var/const declaration.
+//   - Function parameters (input, output, receiver)
+//   - Every LHS of ":=" assignment that defines a new name
+//   - Every local var/const declaration.
 //
 // NOTE: this visitor is experimental.
 // This is also why it lives in a separate file.
@@ -18,20 +18,18 @@ type LocalDefVisitor interface {
 	VisitLocalDef(Name, ast.Expr)
 }
 
-type (
-	// NameKind describes what kind of name Name object holds.
-	NameKind int
+// NameKind describes what kind of name Name object holds.
+type NameKind int
 
-	// Name holds ver/const/param definition symbol info.
-	Name struct {
-		ID   *ast.Ident
-		Kind NameKind
+// Name holds ver/const/param definition symbol info.
+type Name struct {
+	ID   *ast.Ident
+	Kind NameKind
 
-		// Index is NameVar-specific field that is used to
-		// specify nth tuple element being assigned to the name.
-		Index int
-	}
-)
+	// Index is NameVar-specific field that is used to
+	// specify nth tuple element being assigned to the name.
+	Index int
+}
 
 // NOTE: set of name kinds is not stable and may change over time.
 //
diff --git a/checkers/internal/astwalk/visitor.go b/checkers/internal/astwalk/visitor.go
index e5031a9..3486a8e 100644
--- a/checkers/internal/astwalk/visitor.go
+++ b/checkers/internal/astwalk/visitor.go
@@ -4,67 +4,64 @@ import (
 	"go/ast"
 )
 
-// Visitor interfaces.
-type (
-	// DocCommentVisitor visits every doc-comment.
-	// Does not visit doc-comments for function-local definitions (types, etc).
-	// Also does not visit package doc-comment (file-level doc-comments).
-	DocCommentVisitor interface {
-		VisitDocComment(*ast.CommentGroup)
-	}
+// DocCommentVisitor visits every doc-comment.
+// Does not visit doc-comments for function-local definitions (types, etc).
+// Also does not visit package doc-comment (file-level doc-comments).
+type DocCommentVisitor interface {
+	VisitDocComment(*ast.CommentGroup)
+}
 
-	// FuncDeclVisitor visits every top-level function declaration.
-	FuncDeclVisitor interface {
-		walkerEvents
-		VisitFuncDecl(*ast.FuncDecl)
-	}
+// FuncDeclVisitor visits every top-level function declaration.
+type FuncDeclVisitor interface {
+	walkerEvents
+	VisitFuncDecl(*ast.FuncDecl)
+}
 
-	// ExprVisitor visits every expression inside AST file.
-	ExprVisitor interface {
-		walkerEvents
-		VisitExpr(ast.Expr)
-	}
+// ExprVisitor visits every expression inside AST file.
+type ExprVisitor interface {
+	walkerEvents
+	VisitExpr(ast.Expr)
+}
 
-	// LocalExprVisitor visits every expression inside function body.
-	LocalExprVisitor interface {
-		walkerEvents
-		VisitLocalExpr(ast.Expr)
-	}
+// LocalExprVisitor visits every expression inside function body.
+type LocalExprVisitor interface {
+	walkerEvents
+	VisitLocalExpr(ast.Expr)
+}
 
-	// StmtListVisitor visits every statement list inside function body.
-	// This includes block statement bodies as well as implicit blocks
-	// introduced by case clauses and alike.
-	StmtListVisitor interface {
-		walkerEvents
-		VisitStmtList(ast.Node, []ast.Stmt)
-	}
+// StmtListVisitor visits every statement list inside function body.
+// This includes block statement bodies as well as implicit blocks
+// introduced by case clauses and alike.
+type StmtListVisitor interface {
+	walkerEvents
+	VisitStmtList(ast.Node, []ast.Stmt)
+}
 
-	// StmtVisitor visits every statement inside function body.
-	StmtVisitor interface {
-		walkerEvents
-		VisitStmt(ast.Stmt)
-	}
+// StmtVisitor visits every statement inside function body.
+type StmtVisitor interface {
+	walkerEvents
+	VisitStmt(ast.Stmt)
+}
 
-	// TypeExprVisitor visits every type describing expression.
-	// It also traverses struct types and interface types to run
-	// checker over their fields/method signatures.
-	TypeExprVisitor interface {
-		walkerEvents
-		VisitTypeExpr(ast.Expr)
-	}
+// TypeExprVisitor visits every type describing expression.
+// It also traverses struct types and interface types to run
+// checker over their fields/method signatures.
+type TypeExprVisitor interface {
+	walkerEvents
+	VisitTypeExpr(ast.Expr)
+}
 
-	// LocalCommentVisitor visits every comment inside function body.
-	LocalCommentVisitor interface {
-		walkerEvents
-		VisitLocalComment(*ast.CommentGroup)
-	}
+// LocalCommentVisitor visits every comment inside function body.
+type LocalCommentVisitor interface {
+	walkerEvents
+	VisitLocalComment(*ast.CommentGroup)
+}
 
-	// CommentVisitor visits every comment.
-	CommentVisitor interface {
-		walkerEvents
-		VisitComment(*ast.CommentGroup)
-	}
-)
+// CommentVisitor visits every comment.
+type CommentVisitor interface {
+	walkerEvents
+	VisitComment(*ast.CommentGroup)
+}
 
 // walkerEvents describes common hooks available for most visitor types.
 type walkerEvents interface {
diff --git a/checkers/internal/astwalk/walker.go b/checkers/internal/astwalk/walker.go
index cd5e1c9..f838a64 100644
--- a/checkers/internal/astwalk/walker.go
+++ b/checkers/internal/astwalk/walker.go
@@ -3,7 +3,7 @@ package astwalk
 import (
 	"go/types"
 
-	"github.com/go-critic/go-critic/framework/linter"
+	"github.com/go-critic/go-critic/linter"
 )
 
 // WalkerForFuncDecl returns file walker implementation for FuncDeclVisitor.
diff --git a/framework/linttest/testdata/sanity/tests.go b/checkers/internal/linttest/testdata/sanity/tests.go
similarity index 100%
rename from framework/linttest/testdata/sanity/tests.go
rename to checkers/internal/linttest/testdata/sanity/tests.go
diff --git a/checkers/mapKey_checker.go b/checkers/mapKey_checker.go
index 64c2821..596ff87 100644
--- a/checkers/mapKey_checker.go
+++ b/checkers/mapKey_checker.go
@@ -7,7 +7,8 @@ import (
 
 	"github.com/go-critic/go-critic/checkers/internal/astwalk"
 	"github.com/go-critic/go-critic/checkers/internal/lintutil"
-	"github.com/go-critic/go-critic/framework/linter"
+	"github.com/go-critic/go-critic/linter"
+
 	"github.com/go-toolsmith/astcast"
 	"github.com/go-toolsmith/astp"
 	"github.com/go-toolsmith/typep"
diff --git a/checkers/methodExprCall_checker.go b/checkers/methodExprCall_checker.go
index 2553def..4206d4e 100644
--- a/checkers/methodExprCall_checker.go
+++ b/checkers/methodExprCall_checker.go
@@ -5,7 +5,8 @@ import (
 	"go/token"
 
 	"github.com/go-critic/go-critic/checkers/internal/astwalk"
-	"github.com/go-critic/go-critic/framework/linter"
+	"github.com/go-critic/go-critic/linter"
+
 	"github.com/go-toolsmith/astcast"
 	"github.com/go-toolsmith/astcopy"
 	"github.com/go-toolsmith/typep"
diff --git a/checkers/nestingReduce_checker.go b/checkers/nestingReduce_checker.go
index a68acec..86b8696 100644
--- a/checkers/nestingReduce_checker.go
+++ b/checkers/nestingReduce_checker.go
@@ -4,7 +4,7 @@ import (
 	"go/ast"
 
 	"github.com/go-critic/go-critic/checkers/internal/astwalk"
-	"github.com/go-critic/go-critic/framework/linter"
+	"github.com/go-critic/go-critic/linter"
 )
 
 func init() {
diff --git a/checkers/newDeref_checker.go b/checkers/newDeref_checker.go
index 04a3474..fb50283 100644
--- a/checkers/newDeref_checker.go
+++ b/checkers/newDeref_checker.go
@@ -2,12 +2,13 @@ package checkers
 
 import (
 	"go/ast"
+	"go/types"
 
 	"github.com/go-critic/go-critic/checkers/internal/astwalk"
 	"github.com/go-critic/go-critic/checkers/internal/lintutil"
-	"github.com/go-critic/go-critic/framework/linter"
+	"github.com/go-critic/go-critic/linter"
+
 	"github.com/go-toolsmith/astcast"
-	"golang.org/x/exp/typeparams"
 	"golang.org/x/tools/go/ast/astutil"
 )
 
@@ -35,7 +36,7 @@ func (c *newDerefChecker) VisitExpr(expr ast.Expr) {
 	if astcast.ToIdent(call.Fun).Name == "new" {
 		typ := c.ctx.TypeOf(call.Args[0])
 		// allow *new(T) if T is a type parameter, see #1272 for details
-		if typeparams.IsTypeParam(typ) {
+		if _, ok := typ.(*types.TypeParam); ok {
 			return
 		}
 		zv := lintutil.ZeroValueOf(astutil.Unparen(call.Args[0]), typ)
diff --git a/checkers/nilValReturn_checker.go b/checkers/nilValReturn_checker.go
index 0a8e793..c3539cb 100644
--- a/checkers/nilValReturn_checker.go
+++ b/checkers/nilValReturn_checker.go
@@ -5,7 +5,8 @@ import (
 	"go/token"
 
 	"github.com/go-critic/go-critic/checkers/internal/astwalk"
-	"github.com/go-critic/go-critic/framework/linter"
+	"github.com/go-critic/go-critic/linter"
+
 	"github.com/go-toolsmith/astequal"
 	"github.com/go-toolsmith/typep"
 )
diff --git a/checkers/octalLiteral_checker.go b/checkers/octalLiteral_checker.go
index bed227a..b2c2519 100644
--- a/checkers/octalLiteral_checker.go
+++ b/checkers/octalLiteral_checker.go
@@ -7,7 +7,8 @@ import (
 	"unicode"
 
 	"github.com/go-critic/go-critic/checkers/internal/astwalk"
-	"github.com/go-critic/go-critic/framework/linter"
+	"github.com/go-critic/go-critic/linter"
+
 	"github.com/go-toolsmith/astcast"
 )
 
diff --git a/checkers/paramTypeCombine_checker.go b/checkers/paramTypeCombine_checker.go
index c80e6f8..1f76ab6 100644
--- a/checkers/paramTypeCombine_checker.go
+++ b/checkers/paramTypeCombine_checker.go
@@ -4,7 +4,8 @@ import (
 	"go/ast"
 
 	"github.com/go-critic/go-critic/checkers/internal/astwalk"
-	"github.com/go-critic/go-critic/framework/linter"
+	"github.com/go-critic/go-critic/linter"
+
 	"github.com/go-toolsmith/astcopy"
 	"github.com/go-toolsmith/astequal"
 )
diff --git a/checkers/ptrToRefParam_checker.go b/checkers/ptrToRefParam_checker.go
index 88c8f4c..6b86933 100644
--- a/checkers/ptrToRefParam_checker.go
+++ b/checkers/ptrToRefParam_checker.go
@@ -5,7 +5,7 @@ import (
 	"go/types"
 
 	"github.com/go-critic/go-critic/checkers/internal/astwalk"
-	"github.com/go-critic/go-critic/framework/linter"
+	"github.com/go-critic/go-critic/linter"
 )
 
 func init() {
diff --git a/checkers/rangeExprCopy_checker.go b/checkers/rangeExprCopy_checker.go
index 813fff3..8adc16b 100644
--- a/checkers/rangeExprCopy_checker.go
+++ b/checkers/rangeExprCopy_checker.go
@@ -5,7 +5,7 @@ import (
 	"go/types"
 
 	"github.com/go-critic/go-critic/checkers/internal/astwalk"
-	"github.com/go-critic/go-critic/framework/linter"
+	"github.com/go-critic/go-critic/linter"
 )
 
 func init() {
diff --git a/checkers/rangeValCopy_checker.go b/checkers/rangeValCopy_checker.go
index eafc549..e1f76fd 100644
--- a/checkers/rangeValCopy_checker.go
+++ b/checkers/rangeValCopy_checker.go
@@ -4,8 +4,7 @@ import (
 	"go/ast"
 
 	"github.com/go-critic/go-critic/checkers/internal/astwalk"
-	"github.com/go-critic/go-critic/framework/linter"
-	"golang.org/x/exp/typeparams"
+	"github.com/go-critic/go-critic/linter"
 )
 
 func init() {
@@ -66,10 +65,8 @@ func (c *rangeValCopyChecker) VisitStmt(stmt ast.Stmt) {
 	if typ == nil {
 		return
 	}
-	if _, ok := typ.(*typeparams.TypeParam); ok {
-		return
-	}
-	if size, ok := c.ctx.SizeOf(typ); ok && size >= c.sizeThreshold {
+	size, ok := c.ctx.SizeOf(typ)
+	if ok && size >= c.sizeThreshold {
 		c.warn(rng, size)
 	}
 }
diff --git a/checkers/regexpPattern_checker.go b/checkers/regexpPattern_checker.go
index 31dc4aa..5d57c84 100644
--- a/checkers/regexpPattern_checker.go
+++ b/checkers/regexpPattern_checker.go
@@ -7,7 +7,7 @@ import (
 	"strings"
 
 	"github.com/go-critic/go-critic/checkers/internal/astwalk"
-	"github.com/go-critic/go-critic/framework/linter"
+	"github.com/go-critic/go-critic/linter"
 )
 
 func init() {
diff --git a/checkers/regexpSimplify_checker.go b/checkers/regexpSimplify_checker.go
index 5b15e05..5c5e54a 100644
--- a/checkers/regexpSimplify_checker.go
+++ b/checkers/regexpSimplify_checker.go
@@ -8,10 +8,10 @@ import (
 	"strings"
 	"unicode/utf8"
 
-	"github.com/quasilyte/regex/syntax"
-
 	"github.com/go-critic/go-critic/checkers/internal/astwalk"
-	"github.com/go-critic/go-critic/framework/linter"
+	"github.com/go-critic/go-critic/linter"
+
+	"github.com/quasilyte/regex/syntax"
 )
 
 func init() {
diff --git a/checkers/ruleguard_checker.go b/checkers/ruleguard_checker.go
index 000007a..769ebad 100644
--- a/checkers/ruleguard_checker.go
+++ b/checkers/ruleguard_checker.go
@@ -12,9 +12,9 @@ import (
 	"sort"
 	"strings"
 
-	"github.com/quasilyte/go-ruleguard/ruleguard"
+	"github.com/go-critic/go-critic/linter"
 
-	"github.com/go-critic/go-critic/framework/linter"
+	"github.com/quasilyte/go-ruleguard/ruleguard"
 )
 
 func init() {
diff --git a/checkers/rules/rules.go b/checkers/rules/rules.go
index 1137585..d269443 100644
--- a/checkers/rules/rules.go
+++ b/checkers/rules/rules.go
@@ -258,12 +258,18 @@ func wrapperFunc(m dsl.Matcher) {
 	m.Match(`strings.SplitN($_, $_, -1)`).Report("use strings.Split method in `$$`")
 	m.Match(`strings.Replace($_, $_, $_, -1)`).Report("use strings.ReplaceAll method in `$$`")
 	m.Match(`strings.Map(unicode.ToTitle, $_)`).Report("use strings.ToTitle method in `$$`")
+	m.Match(`strings.Index($s1, $s2) >= 0`, `strings.Index($s1, $s2) != -1`).Suggest(`strings.Contains($s1, $s2)`)
+	m.Match(`strings.IndexAny($s1, $s2) >= 0`, `strings.IndexAny($s1, $s2) != -1`).Suggest(`strings.ContainsAny($s1, $s2)`)
+	m.Match(`strings.IndexRune($s1, $s2) >= 0`, `strings.IndexRune($s1, $s2) != -1`).Suggest(`strings.ContainsRune($s1, $s2)`)
 
 	m.Match(`bytes.SplitN(b, []byte("."), -1)`).Report("use bytes.Split method in `$$`")
 	m.Match(`bytes.Replace($_, $_, $_, -1)`).Report("use bytes.ReplaceAll method in `$$`")
 	m.Match(`bytes.Map(unicode.ToUpper, $_)`).Report("use bytes.ToUpper method in `$$`")
 	m.Match(`bytes.Map(unicode.ToLower, $_)`).Report("use bytes.ToLower method in `$$`")
 	m.Match(`bytes.Map(unicode.ToTitle, $_)`).Report("use bytes.ToTitle method in `$$`")
+	m.Match(`bytes.Index($b1, $b2) >= 0`, `bytes.Index($b1, $b2) != -1`).Suggest(`bytes.Contains($b1, $b2)`)
+	m.Match(`bytes.IndexAny($b1, $b2) >= 0`, `bytes.IndexAny($b1, $b2) != -1`).Suggest(`bytes.ContainsAny($b1, $b2)`)
+	m.Match(`bytes.IndexRune($b1, $b2) >= 0`, `bytes.IndexRune($b1, $b2) != -1`).Suggest(`bytes.ContainsRune($b1, $b2)`)
 
 	m.Match(`draw.DrawMask($_, $_, $_, $_, nil, image.Point{}, $_)`).
 		Report("use draw.Draw method in `$$`")
diff --git a/checkers/rulesdata/rulesdata.go b/checkers/rulesdata/rulesdata.go
index f0b147a..7240695 100644
--- a/checkers/rulesdata/rulesdata.go
+++ b/checkers/rulesdata/rulesdata.go
@@ -853,39 +853,93 @@ var PrecompiledRules = &ir.File{
 					ReportTemplate: "use strings.ToTitle method in `$$`",
 				},
 				{
-					Line:           262,
-					SyntaxPatterns: []ir.PatternString{{Line: 262, Value: "bytes.SplitN(b, []byte(\".\"), -1)"}},
+					Line: 261,
+					SyntaxPatterns: []ir.PatternString{
+						{Line: 261, Value: "strings.Index($s1, $s2) >= 0"},
+						{Line: 261, Value: "strings.Index($s1, $s2) != -1"},
+					},
+					ReportTemplate:  "suggestion: strings.Contains($s1, $s2)",
+					SuggestTemplate: "strings.Contains($s1, $s2)",
+				},
+				{
+					Line: 262,
+					SyntaxPatterns: []ir.PatternString{
+						{Line: 262, Value: "strings.IndexAny($s1, $s2) >= 0"},
+						{Line: 262, Value: "strings.IndexAny($s1, $s2) != -1"},
+					},
+					ReportTemplate:  "suggestion: strings.ContainsAny($s1, $s2)",
+					SuggestTemplate: "strings.ContainsAny($s1, $s2)",
+				},
+				{
+					Line: 263,
+					SyntaxPatterns: []ir.PatternString{
+						{Line: 263, Value: "strings.IndexRune($s1, $s2) >= 0"},
+						{Line: 263, Value: "strings.IndexRune($s1, $s2) != -1"},
+					},
+					ReportTemplate:  "suggestion: strings.ContainsRune($s1, $s2)",
+					SuggestTemplate: "strings.ContainsRune($s1, $s2)",
+				},
+				{
+					Line:           265,
+					SyntaxPatterns: []ir.PatternString{{Line: 265, Value: "bytes.SplitN(b, []byte(\".\"), -1)"}},
 					ReportTemplate: "use bytes.Split method in `$$`",
 				},
 				{
-					Line:           263,
-					SyntaxPatterns: []ir.PatternString{{Line: 263, Value: "bytes.Replace($_, $_, $_, -1)"}},
+					Line:           266,
+					SyntaxPatterns: []ir.PatternString{{Line: 266, Value: "bytes.Replace($_, $_, $_, -1)"}},
 					ReportTemplate: "use bytes.ReplaceAll method in `$$`",
 				},
 				{
-					Line:           264,
-					SyntaxPatterns: []ir.PatternString{{Line: 264, Value: "bytes.Map(unicode.ToUpper, $_)"}},
+					Line:           267,
+					SyntaxPatterns: []ir.PatternString{{Line: 267, Value: "bytes.Map(unicode.ToUpper, $_)"}},
 					ReportTemplate: "use bytes.ToUpper method in `$$`",
 				},
 				{
-					Line:           265,
-					SyntaxPatterns: []ir.PatternString{{Line: 265, Value: "bytes.Map(unicode.ToLower, $_)"}},
+					Line:           268,
+					SyntaxPatterns: []ir.PatternString{{Line: 268, Value: "bytes.Map(unicode.ToLower, $_)"}},
 					ReportTemplate: "use bytes.ToLower method in `$$`",
 				},
 				{
-					Line:           266,
-					SyntaxPatterns: []ir.PatternString{{Line: 266, Value: "bytes.Map(unicode.ToTitle, $_)"}},
+					Line:           269,
+					SyntaxPatterns: []ir.PatternString{{Line: 269, Value: "bytes.Map(unicode.ToTitle, $_)"}},
 					ReportTemplate: "use bytes.ToTitle method in `$$`",
 				},
 				{
-					Line:           268,
-					SyntaxPatterns: []ir.PatternString{{Line: 268, Value: "draw.DrawMask($_, $_, $_, $_, nil, image.Point{}, $_)"}},
+					Line: 270,
+					SyntaxPatterns: []ir.PatternString{
+						{Line: 270, Value: "bytes.Index($b1, $b2) >= 0"},
+						{Line: 270, Value: "bytes.Index($b1, $b2) != -1"},
+					},
+					ReportTemplate:  "suggestion: bytes.Contains($b1, $b2)",
+					SuggestTemplate: "bytes.Contains($b1, $b2)",
+				},
+				{
+					Line: 271,
+					SyntaxPatterns: []ir.PatternString{
+						{Line: 271, Value: "bytes.IndexAny($b1, $b2) >= 0"},
+						{Line: 271, Value: "bytes.IndexAny($b1, $b2) != -1"},
+					},
+					ReportTemplate:  "suggestion: bytes.ContainsAny($b1, $b2)",
+					SuggestTemplate: "bytes.ContainsAny($b1, $b2)",
+				},
+				{
+					Line: 272,
+					SyntaxPatterns: []ir.PatternString{
+						{Line: 272, Value: "bytes.IndexRune($b1, $b2) >= 0"},
+						{Line: 272, Value: "bytes.IndexRune($b1, $b2) != -1"},
+					},
+					ReportTemplate:  "suggestion: bytes.ContainsRune($b1, $b2)",
+					SuggestTemplate: "bytes.ContainsRune($b1, $b2)",
+				},
+				{
+					Line:           274,
+					SyntaxPatterns: []ir.PatternString{{Line: 274, Value: "draw.DrawMask($_, $_, $_, $_, nil, image.Point{}, $_)"}},
 					ReportTemplate: "use draw.Draw method in `$$`",
 				},
 			},
 		},
 		{
-			Line:        276,
+			Line:        282,
 			Name:        "regexpMust",
 			MatcherName: "m",
 			DocTags:     []string{"style"},
@@ -894,22 +948,22 @@ var PrecompiledRules = &ir.File{
 			DocAfter:    "re := regexp.MustCompile(\"const pattern\")",
 			Rules: []ir.Rule{
 				{
-					Line:           277,
-					SyntaxPatterns: []ir.PatternString{{Line: 277, Value: "regexp.Compile($pat)"}},
+					Line:           283,
+					SyntaxPatterns: []ir.PatternString{{Line: 283, Value: "regexp.Compile($pat)"}},
 					ReportTemplate: "for const patterns like $pat, use regexp.MustCompile",
 					WhereExpr: ir.FilterExpr{
-						Line:  278,
+						Line:  284,
 						Op:    ir.FilterVarConstOp,
 						Src:   "m[\"pat\"].Const",
 						Value: "pat",
 					},
 				},
 				{
-					Line:           281,
-					SyntaxPatterns: []ir.PatternString{{Line: 281, Value: "regexp.CompilePOSIX($pat)"}},
+					Line:           287,
+					SyntaxPatterns: []ir.PatternString{{Line: 287, Value: "regexp.CompilePOSIX($pat)"}},
 					ReportTemplate: "for const patterns like $pat, use regexp.MustCompilePOSIX",
 					WhereExpr: ir.FilterExpr{
-						Line:  282,
+						Line:  288,
 						Op:    ir.FilterVarConstOp,
 						Src:   "m[\"pat\"].Const",
 						Value: "pat",
@@ -918,7 +972,7 @@ var PrecompiledRules = &ir.File{
 			},
 		},
 		{
-			Line:        290,
+			Line:        296,
 			Name:        "badCall",
 			MatcherName: "m",
 			DocTags:     []string{"diagnostic"},
@@ -927,22 +981,22 @@ var PrecompiledRules = &ir.File{
 			DocAfter:    "strings.Replace(s, from, to, -1)",
 			Rules: []ir.Rule{
 				{
-					Line:           291,
-					SyntaxPatterns: []ir.PatternString{{Line: 291, Value: "strings.Replace($_, $_, $_, $zero)"}},
+					Line:           297,
+					SyntaxPatterns: []ir.PatternString{{Line: 297, Value: "strings.Replace($_, $_, $_, $zero)"}},
 					ReportTemplate: "suspicious arg 0, probably meant -1",
 					WhereExpr: ir.FilterExpr{
-						Line: 292,
+						Line: 298,
 						Op:   ir.FilterEqOp,
 						Src:  "m[\"zero\"].Value.Int() == 0",
 						Args: []ir.FilterExpr{
 							{
-								Line:  292,
+								Line:  298,
 								Op:    ir.FilterVarValueIntOp,
 								Src:   "m[\"zero\"].Value.Int()",
 								Value: "zero",
 							},
 							{
-								Line:  292,
+								Line:  298,
 								Op:    ir.FilterIntOp,
 								Src:   "0",
 								Value: int64(0),
@@ -952,22 +1006,22 @@ var PrecompiledRules = &ir.File{
 					LocationVar: "zero",
 				},
 				{
-					Line:           294,
-					SyntaxPatterns: []ir.PatternString{{Line: 294, Value: "bytes.Replace($_, $_, $_, $zero)"}},
+					Line:           300,
+					SyntaxPatterns: []ir.PatternString{{Line: 300, Value: "bytes.Replace($_, $_, $_, $zero)"}},
 					ReportTemplate: "suspicious arg 0, probably meant -1",
 					WhereExpr: ir.FilterExpr{
-						Line: 295,
+						Line: 301,
 						Op:   ir.FilterEqOp,
 						Src:  "m[\"zero\"].Value.Int() == 0",
 						Args: []ir.FilterExpr{
 							{
-								Line:  295,
+								Line:  301,
 								Op:    ir.FilterVarValueIntOp,
 								Src:   "m[\"zero\"].Value.Int()",
 								Value: "zero",
 							},
 							{
-								Line:  295,
+								Line:  301,
 								Op:    ir.FilterIntOp,
 								Src:   "0",
 								Value: int64(0),
@@ -977,22 +1031,22 @@ var PrecompiledRules = &ir.File{
 					LocationVar: "zero",
 				},
 				{
-					Line:           298,
-					SyntaxPatterns: []ir.PatternString{{Line: 298, Value: "strings.SplitN($_, $_, $zero)"}},
+					Line:           304,
+					SyntaxPatterns: []ir.PatternString{{Line: 304, Value: "strings.SplitN($_, $_, $zero)"}},
 					ReportTemplate: "suspicious arg 0, probably meant -1",
 					WhereExpr: ir.FilterExpr{
-						Line: 299,
+						Line: 305,
 						Op:   ir.FilterEqOp,
 						Src:  "m[\"zero\"].Value.Int() == 0",
 						Args: []ir.FilterExpr{
 							{
-								Line:  299,
+								Line:  305,
 								Op:    ir.FilterVarValueIntOp,
 								Src:   "m[\"zero\"].Value.Int()",
 								Value: "zero",
 							},
 							{
-								Line:  299,
+								Line:  305,
 								Op:    ir.FilterIntOp,
 								Src:   "0",
 								Value: int64(0),
@@ -1002,22 +1056,22 @@ var PrecompiledRules = &ir.File{
 					LocationVar: "zero",
 				},
 				{
-					Line:           301,
-					SyntaxPatterns: []ir.PatternString{{Line: 301, Value: "bytes.SplitN($_, $_, $zero)"}},
+					Line:           307,
+					SyntaxPatterns: []ir.PatternString{{Line: 307, Value: "bytes.SplitN($_, $_, $zero)"}},
 					ReportTemplate: "suspicious arg 0, probably meant -1",
 					WhereExpr: ir.FilterExpr{
-						Line: 302,
+						Line: 308,
 						Op:   ir.FilterEqOp,
 						Src:  "m[\"zero\"].Value.Int() == 0",
 						Args: []ir.FilterExpr{
 							{
-								Line:  302,
+								Line:  308,
 								Op:    ir.FilterVarValueIntOp,
 								Src:   "m[\"zero\"].Value.Int()",
 								Value: "zero",
 							},
 							{
-								Line:  302,
+								Line:  308,
 								Op:    ir.FilterIntOp,
 								Src:   "0",
 								Value: int64(0),
@@ -1027,19 +1081,19 @@ var PrecompiledRules = &ir.File{
 					LocationVar: "zero",
 				},
 				{
-					Line:           305,
-					SyntaxPatterns: []ir.PatternString{{Line: 305, Value: "append($_)"}},
+					Line:           311,
+					SyntaxPatterns: []ir.PatternString{{Line: 311, Value: "append($_)"}},
 					ReportTemplate: "no-op append call, probably missing arguments",
 				},
 				{
-					Line:           307,
-					SyntaxPatterns: []ir.PatternString{{Line: 307, Value: "filepath.Join($_)"}},
+					Line:           313,
+					SyntaxPatterns: []ir.PatternString{{Line: 313, Value: "filepath.Join($_)"}},
 					ReportTemplate: "suspicious Join on 1 argument",
 				},
 			},
 		},
 		{
-			Line:        314,
+			Line:        320,
 			Name:        "assignOp",
 			MatcherName: "m",
 			DocTags:     []string{"style"},
@@ -1048,87 +1102,87 @@ var PrecompiledRules = &ir.File{
 			DocAfter:    "x *= 2",
 			Rules: []ir.Rule{
 				{
-					Line:           315,
-					SyntaxPatterns: []ir.PatternString{{Line: 315, Value: "$x = $x + 1"}},
+					Line:           321,
+					SyntaxPatterns: []ir.PatternString{{Line: 321, Value: "$x = $x + 1"}},
 					ReportTemplate: "replace `$$` with `$x++`",
-					WhereExpr:      ir.FilterExpr{Line: 315, Op: ir.FilterVarPureOp, Src: "m[\"x\"].Pure", Value: "x"},
+					WhereExpr:      ir.FilterExpr{Line: 321, Op: ir.FilterVarPureOp, Src: "m[\"x\"].Pure", Value: "x"},
 				},
 				{
-					Line:           316,
-					SyntaxPatterns: []ir.PatternString{{Line: 316, Value: "$x = $x - 1"}},
+					Line:           322,
+					SyntaxPatterns: []ir.PatternString{{Line: 322, Value: "$x = $x - 1"}},
 					ReportTemplate: "replace `$$` with `$x--`",
-					WhereExpr:      ir.FilterExpr{Line: 316, Op: ir.FilterVarPureOp, Src: "m[\"x\"].Pure", Value: "x"},
+					WhereExpr:      ir.FilterExpr{Line: 322, Op: ir.FilterVarPureOp, Src: "m[\"x\"].Pure", Value: "x"},
 				},
 				{
-					Line:           318,
-					SyntaxPatterns: []ir.PatternString{{Line: 318, Value: "$x = $x + $y"}},
+					Line:           324,
+					SyntaxPatterns: []ir.PatternString{{Line: 324, Value: "$x = $x + $y"}},
 					ReportTemplate: "replace `$$` with `$x += $y`",
-					WhereExpr:      ir.FilterExpr{Line: 318, Op: ir.FilterVarPureOp, Src: "m[\"x\"].Pure", Value: "x"},
+					WhereExpr:      ir.FilterExpr{Line: 324, Op: ir.FilterVarPureOp, Src: "m[\"x\"].Pure", Value: "x"},
 				},
 				{
-					Line:           319,
-					SyntaxPatterns: []ir.PatternString{{Line: 319, Value: "$x = $x - $y"}},
+					Line:           325,
+					SyntaxPatterns: []ir.PatternString{{Line: 325, Value: "$x = $x - $y"}},
 					ReportTemplate: "replace `$$` with `$x -= $y`",
-					WhereExpr:      ir.FilterExpr{Line: 319, Op: ir.FilterVarPureOp, Src: "m[\"x\"].Pure", Value: "x"},
+					WhereExpr:      ir.FilterExpr{Line: 325, Op: ir.FilterVarPureOp, Src: "m[\"x\"].Pure", Value: "x"},
 				},
 				{
-					Line:           321,
-					SyntaxPatterns: []ir.PatternString{{Line: 321, Value: "$x = $x * $y"}},
+					Line:           327,
+					SyntaxPatterns: []ir.PatternString{{Line: 327, Value: "$x = $x * $y"}},
 					ReportTemplate: "replace `$$` with `$x *= $y`",
-					WhereExpr:      ir.FilterExpr{Line: 321, Op: ir.FilterVarPureOp, Src: "m[\"x\"].Pure", Value: "x"},
+					WhereExpr:      ir.FilterExpr{Line: 327, Op: ir.FilterVarPureOp, Src: "m[\"x\"].Pure", Value: "x"},
 				},
 				{
-					Line:           322,
-					SyntaxPatterns: []ir.PatternString{{Line: 322, Value: "$x = $x / $y"}},
+					Line:           328,
+					SyntaxPatterns: []ir.PatternString{{Line: 328, Value: "$x = $x / $y"}},
 					ReportTemplate: "replace `$$` with `$x /= $y`",
-					WhereExpr:      ir.FilterExpr{Line: 322, Op: ir.FilterVarPureOp, Src: "m[\"x\"].Pure", Value: "x"},
+					WhereExpr:      ir.FilterExpr{Line: 328, Op: ir.FilterVarPureOp, Src: "m[\"x\"].Pure", Value: "x"},
 				},
 				{
-					Line:           323,
-					SyntaxPatterns: []ir.PatternString{{Line: 323, Value: "$x = $x % $y"}},
+					Line:           329,
+					SyntaxPatterns: []ir.PatternString{{Line: 329, Value: "$x = $x % $y"}},
 					ReportTemplate: "replace `$$` with `$x %= $y`",
-					WhereExpr:      ir.FilterExpr{Line: 323, Op: ir.FilterVarPureOp, Src: "m[\"x\"].Pure", Value: "x"},
+					WhereExpr:      ir.FilterExpr{Line: 329, Op: ir.FilterVarPureOp, Src: "m[\"x\"].Pure", Value: "x"},
 				},
 				{
-					Line:           324,
-					SyntaxPatterns: []ir.PatternString{{Line: 324, Value: "$x = $x & $y"}},
+					Line:           330,
+					SyntaxPatterns: []ir.PatternString{{Line: 330, Value: "$x = $x & $y"}},
 					ReportTemplate: "replace `$$` with `$x &= $y`",
-					WhereExpr:      ir.FilterExpr{Line: 324, Op: ir.FilterVarPureOp, Src: "m[\"x\"].Pure", Value: "x"},
+					WhereExpr:      ir.FilterExpr{Line: 330, Op: ir.FilterVarPureOp, Src: "m[\"x\"].Pure", Value: "x"},
 				},
 				{
-					Line:           325,
-					SyntaxPatterns: []ir.PatternString{{Line: 325, Value: "$x = $x | $y"}},
+					Line:           331,
+					SyntaxPatterns: []ir.PatternString{{Line: 331, Value: "$x = $x | $y"}},
 					ReportTemplate: "replace `$$` with `$x |= $y`",
-					WhereExpr:      ir.FilterExpr{Line: 325, Op: ir.FilterVarPureOp, Src: "m[\"x\"].Pure", Value: "x"},
+					WhereExpr:      ir.FilterExpr{Line: 331, Op: ir.FilterVarPureOp, Src: "m[\"x\"].Pure", Value: "x"},
 				},
 				{
-					Line:           326,
-					SyntaxPatterns: []ir.PatternString{{Line: 326, Value: "$x = $x ^ $y"}},
+					Line:           332,
+					SyntaxPatterns: []ir.PatternString{{Line: 332, Value: "$x = $x ^ $y"}},
 					ReportTemplate: "replace `$$` with `$x ^= $y`",
-					WhereExpr:      ir.FilterExpr{Line: 326, Op: ir.FilterVarPureOp, Src: "m[\"x\"].Pure", Value: "x"},
+					WhereExpr:      ir.FilterExpr{Line: 332, Op: ir.FilterVarPureOp, Src: "m[\"x\"].Pure", Value: "x"},
 				},
 				{
-					Line:           327,
-					SyntaxPatterns: []ir.PatternString{{Line: 327, Value: "$x = $x << $y"}},
+					Line:           333,
+					SyntaxPatterns: []ir.PatternString{{Line: 333, Value: "$x = $x << $y"}},
 					ReportTemplate: "replace `$$` with `$x <<= $y`",
-					WhereExpr:      ir.FilterExpr{Line: 327, Op: ir.FilterVarPureOp, Src: "m[\"x\"].Pure", Value: "x"},
+					WhereExpr:      ir.FilterExpr{Line: 333, Op: ir.FilterVarPureOp, Src: "m[\"x\"].Pure", Value: "x"},
 				},
 				{
-					Line:           328,
-					SyntaxPatterns: []ir.PatternString{{Line: 328, Value: "$x = $x >> $y"}},
+					Line:           334,
+					SyntaxPatterns: []ir.PatternString{{Line: 334, Value: "$x = $x >> $y"}},
 					ReportTemplate: "replace `$$` with `$x >>= $y`",
-					WhereExpr:      ir.FilterExpr{Line: 328, Op: ir.FilterVarPureOp, Src: "m[\"x\"].Pure", Value: "x"},
+					WhereExpr:      ir.FilterExpr{Line: 334, Op: ir.FilterVarPureOp, Src: "m[\"x\"].Pure", Value: "x"},
 				},
 				{
-					Line:           329,
-					SyntaxPatterns: []ir.PatternString{{Line: 329, Value: "$x = $x &^ $y"}},
+					Line:           335,
+					SyntaxPatterns: []ir.PatternString{{Line: 335, Value: "$x = $x &^ $y"}},
 					ReportTemplate: "replace `$$` with `$x &^= $y`",
-					WhereExpr:      ir.FilterExpr{Line: 329, Op: ir.FilterVarPureOp, Src: "m[\"x\"].Pure", Value: "x"},
+					WhereExpr:      ir.FilterExpr{Line: 335, Op: ir.FilterVarPureOp, Src: "m[\"x\"].Pure", Value: "x"},
 				},
 			},
 		},
 		{
-			Line:        336,
+			Line:        342,
 			Name:        "preferWriteByte",
 			MatcherName: "m",
 			DocTags:     []string{"performance", "experimental", "opinionated"},
@@ -1136,45 +1190,45 @@ var PrecompiledRules = &ir.File{
 			DocBefore:   "w.WriteRune('\\n')",
 			DocAfter:    "w.WriteByte('\\n')",
 			Rules: []ir.Rule{{
-				Line:           340,
-				SyntaxPatterns: []ir.PatternString{{Line: 340, Value: "$w.WriteRune($c)"}},
+				Line:           346,
+				SyntaxPatterns: []ir.PatternString{{Line: 346, Value: "$w.WriteRune($c)"}},
 				ReportTemplate: "consider writing single byte rune $c with $w.WriteByte($c)",
 				WhereExpr: ir.FilterExpr{
-					Line: 341,
+					Line: 347,
 					Op:   ir.FilterAndOp,
 					Src:  "m[\"w\"].Type.Implements(\"io.ByteWriter\") && (m[\"c\"].Const && m[\"c\"].Value.Int() < runeSelf)",
 					Args: []ir.FilterExpr{
 						{
-							Line:  341,
+							Line:  347,
 							Op:    ir.FilterVarTypeImplementsOp,
 							Src:   "m[\"w\"].Type.Implements(\"io.ByteWriter\")",
 							Value: "w",
-							Args:  []ir.FilterExpr{{Line: 341, Op: ir.FilterStringOp, Src: "\"io.ByteWriter\"", Value: "io.ByteWriter"}},
+							Args:  []ir.FilterExpr{{Line: 347, Op: ir.FilterStringOp, Src: "\"io.ByteWriter\"", Value: "io.ByteWriter"}},
 						},
 						{
-							Line: 341,
+							Line: 347,
 							Op:   ir.FilterAndOp,
 							Src:  "(m[\"c\"].Const && m[\"c\"].Value.Int() < runeSelf)",
 							Args: []ir.FilterExpr{
 								{
-									Line:  341,
+									Line:  347,
 									Op:    ir.FilterVarConstOp,
 									Src:   "m[\"c\"].Const",
 									Value: "c",
 								},
 								{
-									Line: 341,
+									Line: 347,
 									Op:   ir.FilterLtOp,
 									Src:  "m[\"c\"].Value.Int() < runeSelf",
 									Args: []ir.FilterExpr{
 										{
-											Line:  341,
+											Line:  347,
 											Op:    ir.FilterVarValueIntOp,
 											Src:   "m[\"c\"].Value.Int()",
 											Value: "c",
 										},
 										{
-											Line:  341,
+											Line:  347,
 											Op:    ir.FilterIntOp,
 											Src:   "runeSelf",
 											Value: int64(128),
@@ -1188,7 +1242,7 @@ var PrecompiledRules = &ir.File{
 			}},
 		},
 		{
-			Line:        349,
+			Line:        355,
 			Name:        "preferFprint",
 			MatcherName: "m",
 			DocTags:     []string{"performance", "experimental"},
@@ -1197,66 +1251,66 @@ var PrecompiledRules = &ir.File{
 			DocAfter:    "fmt.Fprintf(w, \"%x\", 10)",
 			Rules: []ir.Rule{
 				{
-					Line:            350,
-					SyntaxPatterns:  []ir.PatternString{{Line: 350, Value: "$w.Write([]byte(fmt.Sprint($*args)))"}},
+					Line:            356,
+					SyntaxPatterns:  []ir.PatternString{{Line: 356, Value: "$w.Write([]byte(fmt.Sprint($*args)))"}},
 					ReportTemplate:  "fmt.Fprint($w, $args) should be preferred to the $$",
 					SuggestTemplate: "fmt.Fprint($w, $args)",
 					WhereExpr: ir.FilterExpr{
-						Line:  351,
+						Line:  357,
 						Op:    ir.FilterVarTypeImplementsOp,
 						Src:   "m[\"w\"].Type.Implements(\"io.Writer\")",
 						Value: "w",
-						Args:  []ir.FilterExpr{{Line: 351, Op: ir.FilterStringOp, Src: "\"io.Writer\"", Value: "io.Writer"}},
+						Args:  []ir.FilterExpr{{Line: 357, Op: ir.FilterStringOp, Src: "\"io.Writer\"", Value: "io.Writer"}},
 					},
 				},
 				{
-					Line:            355,
-					SyntaxPatterns:  []ir.PatternString{{Line: 355, Value: "$w.Write([]byte(fmt.Sprintf($*args)))"}},
+					Line:            361,
+					SyntaxPatterns:  []ir.PatternString{{Line: 361, Value: "$w.Write([]byte(fmt.Sprintf($*args)))"}},
 					ReportTemplate:  "fmt.Fprintf($w, $args) should be preferred to the $$",
 					SuggestTemplate: "fmt.Fprintf($w, $args)",
 					WhereExpr: ir.FilterExpr{
-						Line:  356,
+						Line:  362,
 						Op:    ir.FilterVarTypeImplementsOp,
 						Src:   "m[\"w\"].Type.Implements(\"io.Writer\")",
 						Value: "w",
-						Args:  []ir.FilterExpr{{Line: 356, Op: ir.FilterStringOp, Src: "\"io.Writer\"", Value: "io.Writer"}},
+						Args:  []ir.FilterExpr{{Line: 362, Op: ir.FilterStringOp, Src: "\"io.Writer\"", Value: "io.Writer"}},
 					},
 				},
 				{
-					Line:            360,
-					SyntaxPatterns:  []ir.PatternString{{Line: 360, Value: "$w.Write([]byte(fmt.Sprintln($*args)))"}},
+					Line:            366,
+					SyntaxPatterns:  []ir.PatternString{{Line: 366, Value: "$w.Write([]byte(fmt.Sprintln($*args)))"}},
 					ReportTemplate:  "fmt.Fprintln($w, $args) should be preferred to the $$",
 					SuggestTemplate: "fmt.Fprintln($w, $args)",
 					WhereExpr: ir.FilterExpr{
-						Line:  361,
+						Line:  367,
 						Op:    ir.FilterVarTypeImplementsOp,
 						Src:   "m[\"w\"].Type.Implements(\"io.Writer\")",
 						Value: "w",
-						Args:  []ir.FilterExpr{{Line: 361, Op: ir.FilterStringOp, Src: "\"io.Writer\"", Value: "io.Writer"}},
+						Args:  []ir.FilterExpr{{Line: 367, Op: ir.FilterStringOp, Src: "\"io.Writer\"", Value: "io.Writer"}},
 					},
 				},
 				{
-					Line:            365,
-					SyntaxPatterns:  []ir.PatternString{{Line: 365, Value: "io.WriteString($w, fmt.Sprint($*args))"}},
+					Line:            371,
+					SyntaxPatterns:  []ir.PatternString{{Line: 371, Value: "io.WriteString($w, fmt.Sprint($*args))"}},
 					ReportTemplate:  "suggestion: fmt.Fprint($w, $args)",
 					SuggestTemplate: "fmt.Fprint($w, $args)",
 				},
 				{
-					Line:            366,
-					SyntaxPatterns:  []ir.PatternString{{Line: 366, Value: "io.WriteString($w, fmt.Sprintf($*args))"}},
+					Line:            372,
+					SyntaxPatterns:  []ir.PatternString{{Line: 372, Value: "io.WriteString($w, fmt.Sprintf($*args))"}},
 					ReportTemplate:  "suggestion: fmt.Fprintf($w, $args)",
 					SuggestTemplate: "fmt.Fprintf($w, $args)",
 				},
 				{
-					Line:            367,
-					SyntaxPatterns:  []ir.PatternString{{Line: 367, Value: "io.WriteString($w, fmt.Sprintln($*args))"}},
+					Line:            373,
+					SyntaxPatterns:  []ir.PatternString{{Line: 373, Value: "io.WriteString($w, fmt.Sprintln($*args))"}},
 					ReportTemplate:  "suggestion: fmt.Fprintln($w, $args)",
 					SuggestTemplate: "fmt.Fprintln($w, $args)",
 				},
 			},
 		},
 		{
-			Line:        374,
+			Line:        380,
 			Name:        "dupArg",
 			MatcherName: "m",
 			DocTags:     []string{"diagnostic"},
@@ -1265,62 +1319,62 @@ var PrecompiledRules = &ir.File{
 			DocAfter:    "copy(dst, src)",
 			Rules: []ir.Rule{
 				{
-					Line: 375,
+					Line: 381,
 					SyntaxPatterns: []ir.PatternString{
-						{Line: 375, Value: "$x.Equal($x)"},
-						{Line: 375, Value: "$x.Equals($x)"},
-						{Line: 375, Value: "$x.Compare($x)"},
-						{Line: 375, Value: "$x.Cmp($x)"},
+						{Line: 381, Value: "$x.Equal($x)"},
+						{Line: 381, Value: "$x.Equals($x)"},
+						{Line: 381, Value: "$x.Compare($x)"},
+						{Line: 381, Value: "$x.Cmp($x)"},
 					},
 					ReportTemplate: "suspicious method call with the same argument and receiver",
-					WhereExpr:      ir.FilterExpr{Line: 376, Op: ir.FilterVarPureOp, Src: "m[\"x\"].Pure", Value: "x"},
+					WhereExpr:      ir.FilterExpr{Line: 382, Op: ir.FilterVarPureOp, Src: "m[\"x\"].Pure", Value: "x"},
 				},
 				{
-					Line: 379,
+					Line: 385,
 					SyntaxPatterns: []ir.PatternString{
-						{Line: 379, Value: "copy($x, $x)"},
-						{Line: 380, Value: "math.Max($x, $x)"},
-						{Line: 381, Value: "math.Min($x, $x)"},
-						{Line: 382, Value: "reflect.Copy($x, $x)"},
-						{Line: 383, Value: "reflect.DeepEqual($x, $x)"},
-						{Line: 384, Value: "strings.Contains($x, $x)"},
-						{Line: 385, Value: "strings.Compare($x, $x)"},
-						{Line: 386, Value: "strings.EqualFold($x, $x)"},
-						{Line: 387, Value: "strings.HasPrefix($x, $x)"},
-						{Line: 388, Value: "strings.HasSuffix($x, $x)"},
-						{Line: 389, Value: "strings.Index($x, $x)"},
-						{Line: 390, Value: "strings.LastIndex($x, $x)"},
-						{Line: 391, Value: "strings.Split($x, $x)"},
-						{Line: 392, Value: "strings.SplitAfter($x, $x)"},
-						{Line: 393, Value: "strings.SplitAfterN($x, $x, $_)"},
-						{Line: 394, Value: "strings.SplitN($x, $x, $_)"},
-						{Line: 395, Value: "strings.Replace($_, $x, $x, $_)"},
-						{Line: 396, Value: "strings.ReplaceAll($_, $x, $x)"},
-						{Line: 397, Value: "bytes.Contains($x, $x)"},
-						{Line: 398, Value: "bytes.Compare($x, $x)"},
-						{Line: 399, Value: "bytes.Equal($x, $x)"},
-						{Line: 400, Value: "bytes.EqualFold($x, $x)"},
-						{Line: 401, Value: "bytes.HasPrefix($x, $x)"},
-						{Line: 402, Value: "bytes.HasSuffix($x, $x)"},
-						{Line: 403, Value: "bytes.Index($x, $x)"},
-						{Line: 404, Value: "bytes.LastIndex($x, $x)"},
-						{Line: 405, Value: "bytes.Split($x, $x)"},
-						{Line: 406, Value: "bytes.SplitAfter($x, $x)"},
-						{Line: 407, Value: "bytes.SplitAfterN($x, $x, $_)"},
-						{Line: 408, Value: "bytes.SplitN($x, $x, $_)"},
-						{Line: 409, Value: "bytes.Replace($_, $x, $x, $_)"},
-						{Line: 410, Value: "bytes.ReplaceAll($_, $x, $x)"},
-						{Line: 411, Value: "types.Identical($x, $x)"},
-						{Line: 412, Value: "types.IdenticalIgnoreTags($x, $x)"},
-						{Line: 413, Value: "draw.Draw($x, $_, $x, $_, $_)"},
+						{Line: 385, Value: "copy($x, $x)"},
+						{Line: 386, Value: "math.Max($x, $x)"},
+						{Line: 387, Value: "math.Min($x, $x)"},
+						{Line: 388, Value: "reflect.Copy($x, $x)"},
+						{Line: 389, Value: "reflect.DeepEqual($x, $x)"},
+						{Line: 390, Value: "strings.Contains($x, $x)"},
+						{Line: 391, Value: "strings.Compare($x, $x)"},
+						{Line: 392, Value: "strings.EqualFold($x, $x)"},
+						{Line: 393, Value: "strings.HasPrefix($x, $x)"},
+						{Line: 394, Value: "strings.HasSuffix($x, $x)"},
+						{Line: 395, Value: "strings.Index($x, $x)"},
+						{Line: 396, Value: "strings.LastIndex($x, $x)"},
+						{Line: 397, Value: "strings.Split($x, $x)"},
+						{Line: 398, Value: "strings.SplitAfter($x, $x)"},
+						{Line: 399, Value: "strings.SplitAfterN($x, $x, $_)"},
+						{Line: 400, Value: "strings.SplitN($x, $x, $_)"},
+						{Line: 401, Value: "strings.Replace($_, $x, $x, $_)"},
+						{Line: 402, Value: "strings.ReplaceAll($_, $x, $x)"},
+						{Line: 403, Value: "bytes.Contains($x, $x)"},
+						{Line: 404, Value: "bytes.Compare($x, $x)"},
+						{Line: 405, Value: "bytes.Equal($x, $x)"},
+						{Line: 406, Value: "bytes.EqualFold($x, $x)"},
+						{Line: 407, Value: "bytes.HasPrefix($x, $x)"},
+						{Line: 408, Value: "bytes.HasSuffix($x, $x)"},
+						{Line: 409, Value: "bytes.Index($x, $x)"},
+						{Line: 410, Value: "bytes.LastIndex($x, $x)"},
+						{Line: 411, Value: "bytes.Split($x, $x)"},
+						{Line: 412, Value: "bytes.SplitAfter($x, $x)"},
+						{Line: 413, Value: "bytes.SplitAfterN($x, $x, $_)"},
+						{Line: 414, Value: "bytes.SplitN($x, $x, $_)"},
+						{Line: 415, Value: "bytes.Replace($_, $x, $x, $_)"},
+						{Line: 416, Value: "bytes.ReplaceAll($_, $x, $x)"},
+						{Line: 417, Value: "types.Identical($x, $x)"},
+						{Line: 418, Value: "types.IdenticalIgnoreTags($x, $x)"},
+						{Line: 419, Value: "draw.Draw($x, $_, $x, $_, $_)"},
 					},
 					ReportTemplate: "suspicious duplicated args in $$",
-					WhereExpr:      ir.FilterExpr{Line: 414, Op: ir.FilterVarPureOp, Src: "m[\"x\"].Pure", Value: "x"},
+					WhereExpr:      ir.FilterExpr{Line: 420, Op: ir.FilterVarPureOp, Src: "m[\"x\"].Pure", Value: "x"},
 				},
 			},
 		},
 		{
-			Line:        422,
+			Line:        428,
 			Name:        "returnAfterHttpError",
 			MatcherName: "m",
 			DocTags:     []string{"diagnostic", "experimental"},
@@ -1328,14 +1382,14 @@ var PrecompiledRules = &ir.File{
 			DocBefore:   "if err != nil { http.Error(...); }",
 			DocAfter:    "if err != nil { http.Error(...); return; }",
 			Rules: []ir.Rule{{
-				Line:           423,
-				SyntaxPatterns: []ir.PatternString{{Line: 423, Value: "if $_ { $*_; http.Error($w, $err, $code) }"}},
+				Line:           429,
+				SyntaxPatterns: []ir.PatternString{{Line: 429, Value: "if $_ { $*_; http.Error($w, $err, $code) }"}},
 				ReportTemplate: "Possibly return is missed after the http.Error call",
 				LocationVar:    "w",
 			}},
 		},
 		{
-			Line:        432,
+			Line:        438,
 			Name:        "preferFilepathJoin",
 			MatcherName: "m",
 			DocTags:     []string{"style", "experimental"},
@@ -1343,35 +1397,35 @@ var PrecompiledRules = &ir.File{
 			DocBefore:   "x + string(os.PathSeparator) + y",
 			DocAfter:    "filepath.Join(x, y)",
 			Rules: []ir.Rule{{
-				Line:            433,
-				SyntaxPatterns:  []ir.PatternString{{Line: 433, Value: "$x + string(os.PathSeparator) + $y"}},
+				Line:            439,
+				SyntaxPatterns:  []ir.PatternString{{Line: 439, Value: "$x + string(os.PathSeparator) + $y"}},
 				ReportTemplate:  "filepath.Join($x, $y) should be preferred to the $$",
 				SuggestTemplate: "filepath.Join($x, $y)",
 				WhereExpr: ir.FilterExpr{
-					Line: 434,
+					Line: 440,
 					Op:   ir.FilterAndOp,
 					Src:  "m[\"x\"].Type.Is(`string`) && m[\"y\"].Type.Is(`string`)",
 					Args: []ir.FilterExpr{
 						{
-							Line:  434,
+							Line:  440,
 							Op:    ir.FilterVarTypeIsOp,
 							Src:   "m[\"x\"].Type.Is(`string`)",
 							Value: "x",
-							Args:  []ir.FilterExpr{{Line: 434, Op: ir.FilterStringOp, Src: "`string`", Value: "string"}},
+							Args:  []ir.FilterExpr{{Line: 440, Op: ir.FilterStringOp, Src: "`string`", Value: "string"}},
 						},
 						{
-							Line:  434,
+							Line:  440,
 							Op:    ir.FilterVarTypeIsOp,
 							Src:   "m[\"y\"].Type.Is(`string`)",
 							Value: "y",
-							Args:  []ir.FilterExpr{{Line: 434, Op: ir.FilterStringOp, Src: "`string`", Value: "string"}},
+							Args:  []ir.FilterExpr{{Line: 440, Op: ir.FilterStringOp, Src: "`string`", Value: "string"}},
 						},
 					},
 				},
 			}},
 		},
 		{
-			Line:        443,
+			Line:        449,
 			Name:        "preferStringWriter",
 			MatcherName: "m",
 			DocTags:     []string{"performance", "experimental"},
@@ -1380,35 +1434,35 @@ var PrecompiledRules = &ir.File{
 			DocAfter:    "w.WriteString(\"foo\")",
 			Rules: []ir.Rule{
 				{
-					Line:            444,
-					SyntaxPatterns:  []ir.PatternString{{Line: 444, Value: "$w.Write([]byte($s))"}},
+					Line:            450,
+					SyntaxPatterns:  []ir.PatternString{{Line: 450, Value: "$w.Write([]byte($s))"}},
 					ReportTemplate:  "$w.WriteString($s) should be preferred to the $$",
 					SuggestTemplate: "$w.WriteString($s)",
 					WhereExpr: ir.FilterExpr{
-						Line:  445,
+						Line:  451,
 						Op:    ir.FilterVarTypeImplementsOp,
 						Src:   "m[\"w\"].Type.Implements(\"io.StringWriter\")",
 						Value: "w",
-						Args:  []ir.FilterExpr{{Line: 445, Op: ir.FilterStringOp, Src: "\"io.StringWriter\"", Value: "io.StringWriter"}},
+						Args:  []ir.FilterExpr{{Line: 451, Op: ir.FilterStringOp, Src: "\"io.StringWriter\"", Value: "io.StringWriter"}},
 					},
 				},
 				{
-					Line:            449,
-					SyntaxPatterns:  []ir.PatternString{{Line: 449, Value: "io.WriteString($w, $s)"}},
+					Line:            455,
+					SyntaxPatterns:  []ir.PatternString{{Line: 455, Value: "io.WriteString($w, $s)"}},
 					ReportTemplate:  "$w.WriteString($s) should be preferred to the $$",
 					SuggestTemplate: "$w.WriteString($s)",
 					WhereExpr: ir.FilterExpr{
-						Line:  450,
+						Line:  456,
 						Op:    ir.FilterVarTypeImplementsOp,
 						Src:   "m[\"w\"].Type.Implements(\"io.StringWriter\")",
 						Value: "w",
-						Args:  []ir.FilterExpr{{Line: 450, Op: ir.FilterStringOp, Src: "\"io.StringWriter\"", Value: "io.StringWriter"}},
+						Args:  []ir.FilterExpr{{Line: 456, Op: ir.FilterStringOp, Src: "\"io.StringWriter\"", Value: "io.StringWriter"}},
 					},
 				},
 			},
 		},
 		{
-			Line:        459,
+			Line:        465,
 			Name:        "sliceClear",
 			MatcherName: "m",
 			DocTags:     []string{"performance", "experimental"},
@@ -1416,22 +1470,22 @@ var PrecompiledRules = &ir.File{
 			DocBefore:   "for i := 0; i < len(buf); i++ { buf[i] = 0 }",
 			DocAfter:    "for i := range buf { buf[i] = 0 }",
 			Rules: []ir.Rule{{
-				Line:           460,
-				SyntaxPatterns: []ir.PatternString{{Line: 460, Value: "for $i := 0; $i < len($xs); $i++ { $xs[$i] = $zero }"}},
+				Line:           466,
+				SyntaxPatterns: []ir.PatternString{{Line: 466, Value: "for $i := 0; $i < len($xs); $i++ { $xs[$i] = $zero }"}},
 				ReportTemplate: "rewrite as for-range so compiler can recognize this pattern",
 				WhereExpr: ir.FilterExpr{
-					Line: 461,
+					Line: 467,
 					Op:   ir.FilterEqOp,
 					Src:  "m[\"zero\"].Value.Int() == 0",
 					Args: []ir.FilterExpr{
 						{
-							Line:  461,
+							Line:  467,
 							Op:    ir.FilterVarValueIntOp,
 							Src:   "m[\"zero\"].Value.Int()",
 							Value: "zero",
 						},
 						{
-							Line:  461,
+							Line:  467,
 							Op:    ir.FilterIntOp,
 							Src:   "0",
 							Value: int64(0),
@@ -1441,7 +1495,7 @@ var PrecompiledRules = &ir.File{
 			}},
 		},
 		{
-			Line:        469,
+			Line:        475,
 			Name:        "syncMapLoadAndDelete",
 			MatcherName: "m",
 			DocTags:     []string{"diagnostic", "experimental"},
@@ -1449,33 +1503,33 @@ var PrecompiledRules = &ir.File{
 			DocBefore:   "v, ok := m.Load(k); if ok { m.Delete($k); f(v); }",
 			DocAfter:    "v, deleted := m.LoadAndDelete(k); if deleted { f(v) }",
 			Rules: []ir.Rule{{
-				Line:           470,
-				SyntaxPatterns: []ir.PatternString{{Line: 470, Value: "$_, $ok := $m.Load($k); if $ok { $m.Delete($k); $*_ }"}},
+				Line:           476,
+				SyntaxPatterns: []ir.PatternString{{Line: 476, Value: "$_, $ok := $m.Load($k); if $ok { $m.Delete($k); $*_ }"}},
 				ReportTemplate: "use $m.LoadAndDelete to perform load+delete operations atomically",
 				WhereExpr: ir.FilterExpr{
-					Line: 471,
+					Line: 477,
 					Op:   ir.FilterAndOp,
 					Src:  "m.GoVersion().GreaterEqThan(\"1.15\") &&\n\tm[\"m\"].Type.Is(`*sync.Map`)",
 					Args: []ir.FilterExpr{
 						{
-							Line:  471,
+							Line:  477,
 							Op:    ir.FilterGoVersionGreaterEqThanOp,
 							Src:   "m.GoVersion().GreaterEqThan(\"1.15\")",
 							Value: "1.15",
 						},
 						{
-							Line:  472,
+							Line:  478,
 							Op:    ir.FilterVarTypeIsOp,
 							Src:   "m[\"m\"].Type.Is(`*sync.Map`)",
 							Value: "m",
-							Args:  []ir.FilterExpr{{Line: 472, Op: ir.FilterStringOp, Src: "`*sync.Map`", Value: "*sync.Map"}},
+							Args:  []ir.FilterExpr{{Line: 478, Op: ir.FilterStringOp, Src: "`*sync.Map`", Value: "*sync.Map"}},
 						},
 					},
 				},
 			}},
 		},
 		{
-			Line:        480,
+			Line:        486,
 			Name:        "sprintfQuotedString",
 			MatcherName: "m",
 			DocTags:     []string{"diagnostic", "experimental"},
@@ -1483,34 +1537,34 @@ var PrecompiledRules = &ir.File{
 			DocBefore:   "fmt.Sprintf(`\"%s\"`, s)",
 			DocAfter:    "fmt.Sprintf(`%q`, s)",
 			Rules: []ir.Rule{{
-				Line:           481,
-				SyntaxPatterns: []ir.PatternString{{Line: 481, Value: "fmt.Sprintf($s, $*_)"}},
+				Line:           487,
+				SyntaxPatterns: []ir.PatternString{{Line: 487, Value: "fmt.Sprintf($s, $*_)"}},
 				ReportTemplate: "use %q instead of \"%s\" for quoted strings",
 				WhereExpr: ir.FilterExpr{
-					Line: 482,
+					Line: 488,
 					Op:   ir.FilterOrOp,
 					Src:  "m[\"s\"].Text.Matches(\"^`.*\\\"%s\\\".*`$\") ||\n\tm[\"s\"].Text.Matches(`^\".*\\\\\"%s\\\\\".*\"$`)",
 					Args: []ir.FilterExpr{
 						{
-							Line:  482,
+							Line:  488,
 							Op:    ir.FilterVarTextMatchesOp,
 							Src:   "m[\"s\"].Text.Matches(\"^`.*\\\"%s\\\".*`$\")",
 							Value: "s",
-							Args:  []ir.FilterExpr{{Line: 482, Op: ir.FilterStringOp, Src: "\"^`.*\\\"%s\\\".*`$\"", Value: "^`.*\"%s\".*`$"}},
+							Args:  []ir.FilterExpr{{Line: 488, Op: ir.FilterStringOp, Src: "\"^`.*\\\"%s\\\".*`$\"", Value: "^`.*\"%s\".*`$"}},
 						},
 						{
-							Line:  483,
+							Line:  489,
 							Op:    ir.FilterVarTextMatchesOp,
 							Src:   "m[\"s\"].Text.Matches(`^\".*\\\\\"%s\\\\\".*\"$`)",
 							Value: "s",
-							Args:  []ir.FilterExpr{{Line: 483, Op: ir.FilterStringOp, Src: "`^\".*\\\\\"%s\\\\\".*\"$`", Value: "^\".*\\\\\"%s\\\\\".*\"$"}},
+							Args:  []ir.FilterExpr{{Line: 489, Op: ir.FilterStringOp, Src: "`^\".*\\\\\"%s\\\\\".*\"$`", Value: "^\".*\\\\\"%s\\\\\".*\"$"}},
 						},
 					},
 				},
 			}},
 		},
 		{
-			Line:        491,
+			Line:        497,
 			Name:        "offBy1",
 			MatcherName: "m",
 			DocTags:     []string{"diagnostic"},
@@ -1519,80 +1573,80 @@ var PrecompiledRules = &ir.File{
 			DocAfter:    "xs[len(xs)-1]",
 			Rules: []ir.Rule{
 				{
-					Line:            492,
-					SyntaxPatterns:  []ir.PatternString{{Line: 492, Value: "$x[len($x)]"}},
+					Line:            498,
+					SyntaxPatterns:  []ir.PatternString{{Line: 498, Value: "$x[len($x)]"}},
 					ReportTemplate:  "index expr always panics; maybe you wanted $x[len($x)-1]?",
 					SuggestTemplate: "$x[len($x)-1]",
 					WhereExpr: ir.FilterExpr{
-						Line: 493,
+						Line: 499,
 						Op:   ir.FilterAndOp,
 						Src:  "m[\"x\"].Pure && m[\"x\"].Type.Is(`[]$_`)",
 						Args: []ir.FilterExpr{
-							{Line: 493, Op: ir.FilterVarPureOp, Src: "m[\"x\"].Pure", Value: "x"},
+							{Line: 499, Op: ir.FilterVarPureOp, Src: "m[\"x\"].Pure", Value: "x"},
 							{
-								Line:  493,
+								Line:  499,
 								Op:    ir.FilterVarTypeIsOp,
 								Src:   "m[\"x\"].Type.Is(`[]$_`)",
 								Value: "x",
-								Args:  []ir.FilterExpr{{Line: 493, Op: ir.FilterStringOp, Src: "`[]$_`", Value: "[]$_"}},
+								Args:  []ir.FilterExpr{{Line: 499, Op: ir.FilterStringOp, Src: "`[]$_`", Value: "[]$_"}},
 							},
 						},
 					},
 				},
 				{
-					Line: 500,
+					Line: 506,
 					SyntaxPatterns: []ir.PatternString{
-						{Line: 501, Value: "$i := strings.Index($s, $_); $_ := $slicing[$i:]"},
-						{Line: 502, Value: "$i := strings.Index($s, $_); $_ = $slicing[$i:]"},
-						{Line: 503, Value: "$i := bytes.Index($s, $_); $_ := $slicing[$i:]"},
-						{Line: 504, Value: "$i := bytes.Index($s, $_); $_ = $slicing[$i:]"},
+						{Line: 507, Value: "$i := strings.Index($s, $_); $_ := $slicing[$i:]"},
+						{Line: 508, Value: "$i := strings.Index($s, $_); $_ = $slicing[$i:]"},
+						{Line: 509, Value: "$i := bytes.Index($s, $_); $_ := $slicing[$i:]"},
+						{Line: 510, Value: "$i := bytes.Index($s, $_); $_ = $slicing[$i:]"},
 					},
 					ReportTemplate: "Index() can return -1; maybe you wanted to do $s[$i+1:]",
 					WhereExpr: ir.FilterExpr{
-						Line: 505,
+						Line: 511,
 						Op:   ir.FilterEqOp,
 						Src:  "m[\"s\"].Text == m[\"slicing\"].Text",
 						Args: []ir.FilterExpr{
-							{Line: 505, Op: ir.FilterVarTextOp, Src: "m[\"s\"].Text", Value: "s"},
-							{Line: 505, Op: ir.FilterVarTextOp, Src: "m[\"slicing\"].Text", Value: "slicing"},
+							{Line: 511, Op: ir.FilterVarTextOp, Src: "m[\"s\"].Text", Value: "s"},
+							{Line: 511, Op: ir.FilterVarTextOp, Src: "m[\"slicing\"].Text", Value: "slicing"},
 						},
 					},
 					LocationVar: "slicing",
 				},
 				{
-					Line: 509,
+					Line: 515,
 					SyntaxPatterns: []ir.PatternString{
-						{Line: 510, Value: "$i := strings.Index($s, $_); $_ := $slicing[:$i]"},
-						{Line: 511, Value: "$i := strings.Index($s, $_); $_ = $slicing[:$i]"},
-						{Line: 512, Value: "$i := bytes.Index($s, $_); $_ := $slicing[:$i]"},
-						{Line: 513, Value: "$i := bytes.Index($s, $_); $_ = $slicing[:$i]"},
+						{Line: 516, Value: "$i := strings.Index($s, $_); $_ := $slicing[:$i]"},
+						{Line: 517, Value: "$i := strings.Index($s, $_); $_ = $slicing[:$i]"},
+						{Line: 518, Value: "$i := bytes.Index($s, $_); $_ := $slicing[:$i]"},
+						{Line: 519, Value: "$i := bytes.Index($s, $_); $_ = $slicing[:$i]"},
 					},
 					ReportTemplate: "Index() can return -1; maybe you wanted to do $s[:$i+1]",
 					WhereExpr: ir.FilterExpr{
-						Line: 514,
+						Line: 520,
 						Op:   ir.FilterEqOp,
 						Src:  "m[\"s\"].Text == m[\"slicing\"].Text",
 						Args: []ir.FilterExpr{
-							{Line: 514, Op: ir.FilterVarTextOp, Src: "m[\"s\"].Text", Value: "s"},
-							{Line: 514, Op: ir.FilterVarTextOp, Src: "m[\"slicing\"].Text", Value: "slicing"},
+							{Line: 520, Op: ir.FilterVarTextOp, Src: "m[\"s\"].Text", Value: "s"},
+							{Line: 520, Op: ir.FilterVarTextOp, Src: "m[\"slicing\"].Text", Value: "slicing"},
 						},
 					},
 					LocationVar: "slicing",
 				},
 				{
-					Line: 518,
+					Line: 524,
 					SyntaxPatterns: []ir.PatternString{
-						{Line: 519, Value: "$s[strings.Index($s, $_):]"},
-						{Line: 520, Value: "$s[:strings.Index($s, $_)]"},
-						{Line: 521, Value: "$s[bytes.Index($s, $_):]"},
-						{Line: 522, Value: "$s[:bytes.Index($s, $_)]"},
+						{Line: 525, Value: "$s[strings.Index($s, $_):]"},
+						{Line: 526, Value: "$s[:strings.Index($s, $_)]"},
+						{Line: 527, Value: "$s[bytes.Index($s, $_):]"},
+						{Line: 528, Value: "$s[:bytes.Index($s, $_)]"},
 					},
 					ReportTemplate: "Index() can return -1; maybe you wanted to do Index()+1",
 				},
 			},
 		},
 		{
-			Line:        530,
+			Line:        536,
 			Name:        "unslice",
 			MatcherName: "m",
 			DocTags:     []string{"style"},
@@ -1600,35 +1654,35 @@ var PrecompiledRules = &ir.File{
 			DocBefore:   "copy(b[:], values...)",
 			DocAfter:    "copy(b, values...)",
 			Rules: []ir.Rule{{
-				Line:            531,
-				SyntaxPatterns:  []ir.PatternString{{Line: 531, Value: "$s[:]"}},
+				Line:            537,
+				SyntaxPatterns:  []ir.PatternString{{Line: 537, Value: "$s[:]"}},
 				ReportTemplate:  "could simplify $$ to $s",
 				SuggestTemplate: "$s",
 				WhereExpr: ir.FilterExpr{
-					Line: 532,
+					Line: 538,
 					Op:   ir.FilterOrOp,
 					Src:  "m[\"s\"].Type.Is(`string`) || m[\"s\"].Type.Is(`[]$_`)",
 					Args: []ir.FilterExpr{
 						{
-							Line:  532,
+							Line:  538,
 							Op:    ir.FilterVarTypeIsOp,
 							Src:   "m[\"s\"].Type.Is(`string`)",
 							Value: "s",
-							Args:  []ir.FilterExpr{{Line: 532, Op: ir.FilterStringOp, Src: "`string`", Value: "string"}},
+							Args:  []ir.FilterExpr{{Line: 538, Op: ir.FilterStringOp, Src: "`string`", Value: "string"}},
 						},
 						{
-							Line:  532,
+							Line:  538,
 							Op:    ir.FilterVarTypeIsOp,
 							Src:   "m[\"s\"].Type.Is(`[]$_`)",
 							Value: "s",
-							Args:  []ir.FilterExpr{{Line: 532, Op: ir.FilterStringOp, Src: "`[]$_`", Value: "[]$_"}},
+							Args:  []ir.FilterExpr{{Line: 538, Op: ir.FilterStringOp, Src: "`[]$_`", Value: "[]$_"}},
 						},
 					},
 				},
 			}},
 		},
 		{
-			Line:        541,
+			Line:        547,
 			Name:        "yodaStyleExpr",
 			MatcherName: "m",
 			DocTags:     []string{"style", "experimental"},
@@ -1637,105 +1691,105 @@ var PrecompiledRules = &ir.File{
 			DocAfter:    "return ptr != nil",
 			Rules: []ir.Rule{
 				{
-					Line:           542,
-					SyntaxPatterns: []ir.PatternString{{Line: 542, Value: "$constval != $x"}},
+					Line:           548,
+					SyntaxPatterns: []ir.PatternString{{Line: 548, Value: "$constval != $x"}},
 					ReportTemplate: "consider to change order in expression to $x != $constval",
 					WhereExpr: ir.FilterExpr{
-						Line: 542,
+						Line: 548,
 						Op:   ir.FilterAndOp,
 						Src:  "m[\"constval\"].Node.Is(`BasicLit`) && !m[\"x\"].Node.Is(`BasicLit`)",
 						Args: []ir.FilterExpr{
 							{
-								Line:  542,
+								Line:  548,
 								Op:    ir.FilterVarNodeIsOp,
 								Src:   "m[\"constval\"].Node.Is(`BasicLit`)",
 								Value: "constval",
-								Args:  []ir.FilterExpr{{Line: 542, Op: ir.FilterStringOp, Src: "`BasicLit`", Value: "BasicLit"}},
+								Args:  []ir.FilterExpr{{Line: 548, Op: ir.FilterStringOp, Src: "`BasicLit`", Value: "BasicLit"}},
 							},
 							{
-								Line: 542,
+								Line: 548,
 								Op:   ir.FilterNotOp,
 								Src:  "!m[\"x\"].Node.Is(`BasicLit`)",
 								Args: []ir.FilterExpr{{
-									Line:  542,
+									Line:  548,
 									Op:    ir.FilterVarNodeIsOp,
 									Src:   "m[\"x\"].Node.Is(`BasicLit`)",
 									Value: "x",
-									Args:  []ir.FilterExpr{{Line: 542, Op: ir.FilterStringOp, Src: "`BasicLit`", Value: "BasicLit"}},
+									Args:  []ir.FilterExpr{{Line: 548, Op: ir.FilterStringOp, Src: "`BasicLit`", Value: "BasicLit"}},
 								}},
 							},
 						},
 					},
 				},
 				{
-					Line:           544,
-					SyntaxPatterns: []ir.PatternString{{Line: 544, Value: "$constval == $x"}},
+					Line:           550,
+					SyntaxPatterns: []ir.PatternString{{Line: 550, Value: "$constval == $x"}},
 					ReportTemplate: "consider to change order in expression to $x == $constval",
 					WhereExpr: ir.FilterExpr{
-						Line: 544,
+						Line: 550,
 						Op:   ir.FilterAndOp,
 						Src:  "m[\"constval\"].Node.Is(`BasicLit`) && !m[\"x\"].Node.Is(`BasicLit`)",
 						Args: []ir.FilterExpr{
 							{
-								Line:  544,
+								Line:  550,
 								Op:    ir.FilterVarNodeIsOp,
 								Src:   "m[\"constval\"].Node.Is(`BasicLit`)",
 								Value: "constval",
-								Args:  []ir.FilterExpr{{Line: 544, Op: ir.FilterStringOp, Src: "`BasicLit`", Value: "BasicLit"}},
+								Args:  []ir.FilterExpr{{Line: 550, Op: ir.FilterStringOp, Src: "`BasicLit`", Value: "BasicLit"}},
 							},
 							{
-								Line: 544,
+								Line: 550,
 								Op:   ir.FilterNotOp,
 								Src:  "!m[\"x\"].Node.Is(`BasicLit`)",
 								Args: []ir.FilterExpr{{
-									Line:  544,
+									Line:  550,
 									Op:    ir.FilterVarNodeIsOp,
 									Src:   "m[\"x\"].Node.Is(`BasicLit`)",
 									Value: "x",
-									Args:  []ir.FilterExpr{{Line: 544, Op: ir.FilterStringOp, Src: "`BasicLit`", Value: "BasicLit"}},
+									Args:  []ir.FilterExpr{{Line: 550, Op: ir.FilterStringOp, Src: "`BasicLit`", Value: "BasicLit"}},
 								}},
 							},
 						},
 					},
 				},
 				{
-					Line:           547,
-					SyntaxPatterns: []ir.PatternString{{Line: 547, Value: "nil != $x"}},
+					Line:           553,
+					SyntaxPatterns: []ir.PatternString{{Line: 553, Value: "nil != $x"}},
 					ReportTemplate: "consider to change order in expression to $x != nil",
 					WhereExpr: ir.FilterExpr{
-						Line: 547,
+						Line: 553,
 						Op:   ir.FilterNotOp,
 						Src:  "!m[\"x\"].Node.Is(`BasicLit`)",
 						Args: []ir.FilterExpr{{
-							Line:  547,
+							Line:  553,
 							Op:    ir.FilterVarNodeIsOp,
 							Src:   "m[\"x\"].Node.Is(`BasicLit`)",
 							Value: "x",
-							Args:  []ir.FilterExpr{{Line: 547, Op: ir.FilterStringOp, Src: "`BasicLit`", Value: "BasicLit"}},
+							Args:  []ir.FilterExpr{{Line: 553, Op: ir.FilterStringOp, Src: "`BasicLit`", Value: "BasicLit"}},
 						}},
 					},
 				},
 				{
-					Line:           549,
-					SyntaxPatterns: []ir.PatternString{{Line: 549, Value: "nil == $x"}},
+					Line:           555,
+					SyntaxPatterns: []ir.PatternString{{Line: 555, Value: "nil == $x"}},
 					ReportTemplate: "consider to change order in expression to $x == nil",
 					WhereExpr: ir.FilterExpr{
-						Line: 549,
+						Line: 555,
 						Op:   ir.FilterNotOp,
 						Src:  "!m[\"x\"].Node.Is(`BasicLit`)",
 						Args: []ir.FilterExpr{{
-							Line:  549,
+							Line:  555,
 							Op:    ir.FilterVarNodeIsOp,
 							Src:   "m[\"x\"].Node.Is(`BasicLit`)",
 							Value: "x",
-							Args:  []ir.FilterExpr{{Line: 549, Op: ir.FilterStringOp, Src: "`BasicLit`", Value: "BasicLit"}},
+							Args:  []ir.FilterExpr{{Line: 555, Op: ir.FilterStringOp, Src: "`BasicLit`", Value: "BasicLit"}},
 						}},
 					},
 				},
 			},
 		},
 		{
-			Line:        557,
+			Line:        563,
 			Name:        "equalFold",
 			MatcherName: "m",
 			DocTags:     []string{"performance", "experimental"},
@@ -1744,114 +1798,114 @@ var PrecompiledRules = &ir.File{
 			DocAfter:    "strings.EqualFold(x, y)",
 			Rules: []ir.Rule{
 				{
-					Line: 566,
+					Line: 572,
 					SyntaxPatterns: []ir.PatternString{
-						{Line: 567, Value: "strings.ToLower($x) == $y"},
-						{Line: 568, Value: "strings.ToLower($x) == strings.ToLower($y)"},
-						{Line: 569, Value: "$x == strings.ToLower($y)"},
-						{Line: 570, Value: "strings.ToUpper($x) == $y"},
-						{Line: 571, Value: "strings.ToUpper($x) == strings.ToUpper($y)"},
-						{Line: 572, Value: "$x == strings.ToUpper($y)"},
+						{Line: 573, Value: "strings.ToLower($x) == $y"},
+						{Line: 574, Value: "strings.ToLower($x) == strings.ToLower($y)"},
+						{Line: 575, Value: "$x == strings.ToLower($y)"},
+						{Line: 576, Value: "strings.ToUpper($x) == $y"},
+						{Line: 577, Value: "strings.ToUpper($x) == strings.ToUpper($y)"},
+						{Line: 578, Value: "$x == strings.ToUpper($y)"},
 					},
 					ReportTemplate:  "consider replacing with strings.EqualFold($x, $y)",
 					SuggestTemplate: "strings.EqualFold($x, $y)",
 					WhereExpr: ir.FilterExpr{
-						Line: 573,
+						Line: 579,
 						Op:   ir.FilterAndOp,
 						Src:  "m[\"x\"].Pure && m[\"y\"].Pure && m[\"x\"].Text != m[\"y\"].Text",
 						Args: []ir.FilterExpr{
 							{
-								Line: 573,
+								Line: 579,
 								Op:   ir.FilterAndOp,
 								Src:  "m[\"x\"].Pure && m[\"y\"].Pure",
 								Args: []ir.FilterExpr{
-									{Line: 573, Op: ir.FilterVarPureOp, Src: "m[\"x\"].Pure", Value: "x"},
-									{Line: 573, Op: ir.FilterVarPureOp, Src: "m[\"y\"].Pure", Value: "y"},
+									{Line: 579, Op: ir.FilterVarPureOp, Src: "m[\"x\"].Pure", Value: "x"},
+									{Line: 579, Op: ir.FilterVarPureOp, Src: "m[\"y\"].Pure", Value: "y"},
 								},
 							},
 							{
-								Line: 573,
+								Line: 579,
 								Op:   ir.FilterNeqOp,
 								Src:  "m[\"x\"].Text != m[\"y\"].Text",
 								Args: []ir.FilterExpr{
-									{Line: 573, Op: ir.FilterVarTextOp, Src: "m[\"x\"].Text", Value: "x"},
-									{Line: 573, Op: ir.FilterVarTextOp, Src: "m[\"y\"].Text", Value: "y"},
+									{Line: 579, Op: ir.FilterVarTextOp, Src: "m[\"x\"].Text", Value: "x"},
+									{Line: 579, Op: ir.FilterVarTextOp, Src: "m[\"y\"].Text", Value: "y"},
 								},
 							},
 						},
 					},
 				},
 				{
-					Line: 578,
+					Line: 584,
 					SyntaxPatterns: []ir.PatternString{
-						{Line: 579, Value: "strings.ToLower($x) != $y"},
-						{Line: 580, Value: "strings.ToLower($x) != strings.ToLower($y)"},
-						{Line: 581, Value: "$x != strings.ToLower($y)"},
-						{Line: 582, Value: "strings.ToUpper($x) != $y"},
-						{Line: 583, Value: "strings.ToUpper($x) != strings.ToUpper($y)"},
-						{Line: 584, Value: "$x != strings.ToUpper($y)"},
+						{Line: 585, Value: "strings.ToLower($x) != $y"},
+						{Line: 586, Value: "strings.ToLower($x) != strings.ToLower($y)"},
+						{Line: 587, Value: "$x != strings.ToLower($y)"},
+						{Line: 588, Value: "strings.ToUpper($x) != $y"},
+						{Line: 589, Value: "strings.ToUpper($x) != strings.ToUpper($y)"},
+						{Line: 590, Value: "$x != strings.ToUpper($y)"},
 					},
 					ReportTemplate:  "consider replacing with !strings.EqualFold($x, $y)",
 					SuggestTemplate: "!strings.EqualFold($x, $y)",
 					WhereExpr: ir.FilterExpr{
-						Line: 585,
+						Line: 591,
 						Op:   ir.FilterAndOp,
 						Src:  "m[\"x\"].Pure && m[\"y\"].Pure && m[\"x\"].Text != m[\"y\"].Text",
 						Args: []ir.FilterExpr{
 							{
-								Line: 585,
+								Line: 591,
 								Op:   ir.FilterAndOp,
 								Src:  "m[\"x\"].Pure && m[\"y\"].Pure",
 								Args: []ir.FilterExpr{
-									{Line: 585, Op: ir.FilterVarPureOp, Src: "m[\"x\"].Pure", Value: "x"},
-									{Line: 585, Op: ir.FilterVarPureOp, Src: "m[\"y\"].Pure", Value: "y"},
+									{Line: 591, Op: ir.FilterVarPureOp, Src: "m[\"x\"].Pure", Value: "x"},
+									{Line: 591, Op: ir.FilterVarPureOp, Src: "m[\"y\"].Pure", Value: "y"},
 								},
 							},
 							{
-								Line: 585,
+								Line: 591,
 								Op:   ir.FilterNeqOp,
 								Src:  "m[\"x\"].Text != m[\"y\"].Text",
 								Args: []ir.FilterExpr{
-									{Line: 585, Op: ir.FilterVarTextOp, Src: "m[\"x\"].Text", Value: "x"},
-									{Line: 585, Op: ir.FilterVarTextOp, Src: "m[\"y\"].Text", Value: "y"},
+									{Line: 591, Op: ir.FilterVarTextOp, Src: "m[\"x\"].Text", Value: "x"},
+									{Line: 591, Op: ir.FilterVarTextOp, Src: "m[\"y\"].Text", Value: "y"},
 								},
 							},
 						},
 					},
 				},
 				{
-					Line: 590,
+					Line: 596,
 					SyntaxPatterns: []ir.PatternString{
-						{Line: 591, Value: "bytes.Equal(bytes.ToLower($x), $y)"},
-						{Line: 592, Value: "bytes.Equal(bytes.ToLower($x), bytes.ToLower($y))"},
-						{Line: 593, Value: "bytes.Equal($x, bytes.ToLower($y))"},
-						{Line: 594, Value: "bytes.Equal(bytes.ToUpper($x), $y)"},
-						{Line: 595, Value: "bytes.Equal(bytes.ToUpper($x), bytes.ToUpper($y))"},
-						{Line: 596, Value: "bytes.Equal($x, bytes.ToUpper($y))"},
+						{Line: 597, Value: "bytes.Equal(bytes.ToLower($x), $y)"},
+						{Line: 598, Value: "bytes.Equal(bytes.ToLower($x), bytes.ToLower($y))"},
+						{Line: 599, Value: "bytes.Equal($x, bytes.ToLower($y))"},
+						{Line: 600, Value: "bytes.Equal(bytes.ToUpper($x), $y)"},
+						{Line: 601, Value: "bytes.Equal(bytes.ToUpper($x), bytes.ToUpper($y))"},
+						{Line: 602, Value: "bytes.Equal($x, bytes.ToUpper($y))"},
 					},
 					ReportTemplate:  "consider replacing with bytes.EqualFold($x, $y)",
 					SuggestTemplate: "bytes.EqualFold($x, $y)",
 					WhereExpr: ir.FilterExpr{
-						Line: 597,
+						Line: 603,
 						Op:   ir.FilterAndOp,
 						Src:  "m[\"x\"].Pure && m[\"y\"].Pure && m[\"x\"].Text != m[\"y\"].Text",
 						Args: []ir.FilterExpr{
 							{
-								Line: 597,
+								Line: 603,
 								Op:   ir.FilterAndOp,
 								Src:  "m[\"x\"].Pure && m[\"y\"].Pure",
 								Args: []ir.FilterExpr{
-									{Line: 597, Op: ir.FilterVarPureOp, Src: "m[\"x\"].Pure", Value: "x"},
-									{Line: 597, Op: ir.FilterVarPureOp, Src: "m[\"y\"].Pure", Value: "y"},
+									{Line: 603, Op: ir.FilterVarPureOp, Src: "m[\"x\"].Pure", Value: "x"},
+									{Line: 603, Op: ir.FilterVarPureOp, Src: "m[\"y\"].Pure", Value: "y"},
 								},
 							},
 							{
-								Line: 597,
+								Line: 603,
 								Op:   ir.FilterNeqOp,
 								Src:  "m[\"x\"].Text != m[\"y\"].Text",
 								Args: []ir.FilterExpr{
-									{Line: 597, Op: ir.FilterVarTextOp, Src: "m[\"x\"].Text", Value: "x"},
-									{Line: 597, Op: ir.FilterVarTextOp, Src: "m[\"y\"].Text", Value: "y"},
+									{Line: 603, Op: ir.FilterVarTextOp, Src: "m[\"x\"].Text", Value: "x"},
+									{Line: 603, Op: ir.FilterVarTextOp, Src: "m[\"y\"].Text", Value: "y"},
 								},
 							},
 						},
@@ -1860,7 +1914,7 @@ var PrecompiledRules = &ir.File{
 			},
 		},
 		{
-			Line:        606,
+			Line:        612,
 			Name:        "argOrder",
 			MatcherName: "m",
 			DocTags:     []string{"diagnostic"},
@@ -1868,45 +1922,45 @@ var PrecompiledRules = &ir.File{
 			DocBefore:   "strings.HasPrefix(\"#\", userpass)",
 			DocAfter:    "strings.HasPrefix(userpass, \"#\")",
 			Rules: []ir.Rule{{
-				Line: 607,
+				Line: 613,
 				SyntaxPatterns: []ir.PatternString{
-					{Line: 608, Value: "strings.HasPrefix($lit, $s)"},
-					{Line: 609, Value: "bytes.HasPrefix($lit, $s)"},
-					{Line: 610, Value: "strings.HasSuffix($lit, $s)"},
-					{Line: 611, Value: "bytes.HasSuffix($lit, $s)"},
-					{Line: 612, Value: "strings.Contains($lit, $s)"},
-					{Line: 613, Value: "bytes.Contains($lit, $s)"},
-					{Line: 614, Value: "strings.TrimPrefix($lit, $s)"},
-					{Line: 615, Value: "bytes.TrimPrefix($lit, $s)"},
-					{Line: 616, Value: "strings.TrimSuffix($lit, $s)"},
-					{Line: 617, Value: "bytes.TrimSuffix($lit, $s)"},
-					{Line: 618, Value: "strings.Split($lit, $s)"},
-					{Line: 619, Value: "bytes.Split($lit, $s)"},
+					{Line: 614, Value: "strings.HasPrefix($lit, $s)"},
+					{Line: 615, Value: "bytes.HasPrefix($lit, $s)"},
+					{Line: 616, Value: "strings.HasSuffix($lit, $s)"},
+					{Line: 617, Value: "bytes.HasSuffix($lit, $s)"},
+					{Line: 618, Value: "strings.Contains($lit, $s)"},
+					{Line: 619, Value: "bytes.Contains($lit, $s)"},
+					{Line: 620, Value: "strings.TrimPrefix($lit, $s)"},
+					{Line: 621, Value: "bytes.TrimPrefix($lit, $s)"},
+					{Line: 622, Value: "strings.TrimSuffix($lit, $s)"},
+					{Line: 623, Value: "bytes.TrimSuffix($lit, $s)"},
+					{Line: 624, Value: "strings.Split($lit, $s)"},
+					{Line: 625, Value: "bytes.Split($lit, $s)"},
 				},
 				ReportTemplate: "$lit and $s arguments order looks reversed",
 				WhereExpr: ir.FilterExpr{
-					Line: 620,
+					Line: 626,
 					Op:   ir.FilterAndOp,
 					Src:  "(m[\"lit\"].Const || m[\"lit\"].ConstSlice) &&\n\t!(m[\"s\"].Const || m[\"s\"].ConstSlice) &&\n\t!m[\"lit\"].Node.Is(`Ident`)",
 					Args: []ir.FilterExpr{
 						{
-							Line: 620,
+							Line: 626,
 							Op:   ir.FilterAndOp,
 							Src:  "(m[\"lit\"].Const || m[\"lit\"].ConstSlice) &&\n\t!(m[\"s\"].Const || m[\"s\"].ConstSlice)",
 							Args: []ir.FilterExpr{
 								{
-									Line: 620,
+									Line: 626,
 									Op:   ir.FilterOrOp,
 									Src:  "(m[\"lit\"].Const || m[\"lit\"].ConstSlice)",
 									Args: []ir.FilterExpr{
 										{
-											Line:  620,
+											Line:  626,
 											Op:    ir.FilterVarConstOp,
 											Src:   "m[\"lit\"].Const",
 											Value: "lit",
 										},
 										{
-											Line:  620,
+											Line:  626,
 											Op:    ir.FilterVarConstSliceOp,
 											Src:   "m[\"lit\"].ConstSlice",
 											Value: "lit",
@@ -1914,22 +1968,22 @@ var PrecompiledRules = &ir.File{
 									},
 								},
 								{
-									Line: 621,
+									Line: 627,
 									Op:   ir.FilterNotOp,
 									Src:  "!(m[\"s\"].Const || m[\"s\"].ConstSlice)",
 									Args: []ir.FilterExpr{{
-										Line: 621,
+										Line: 627,
 										Op:   ir.FilterOrOp,
 										Src:  "(m[\"s\"].Const || m[\"s\"].ConstSlice)",
 										Args: []ir.FilterExpr{
 											{
-												Line:  621,
+												Line:  627,
 												Op:    ir.FilterVarConstOp,
 												Src:   "m[\"s\"].Const",
 												Value: "s",
 											},
 											{
-												Line:  621,
+												Line:  627,
 												Op:    ir.FilterVarConstSliceOp,
 												Src:   "m[\"s\"].ConstSlice",
 												Value: "s",
@@ -1940,15 +1994,15 @@ var PrecompiledRules = &ir.File{
 							},
 						},
 						{
-							Line: 622,
+							Line: 628,
 							Op:   ir.FilterNotOp,
 							Src:  "!m[\"lit\"].Node.Is(`Ident`)",
 							Args: []ir.FilterExpr{{
-								Line:  622,
+								Line:  628,
 								Op:    ir.FilterVarNodeIsOp,
 								Src:   "m[\"lit\"].Node.Is(`Ident`)",
 								Value: "lit",
-								Args:  []ir.FilterExpr{{Line: 622, Op: ir.FilterStringOp, Src: "`Ident`", Value: "Ident"}},
+								Args:  []ir.FilterExpr{{Line: 628, Op: ir.FilterStringOp, Src: "`Ident`", Value: "Ident"}},
 							}},
 						},
 					},
@@ -1956,7 +2010,7 @@ var PrecompiledRules = &ir.File{
 			}},
 		},
 		{
-			Line:        630,
+			Line:        636,
 			Name:        "stringConcatSimplify",
 			MatcherName: "m",
 			DocTags:     []string{"style", "experimental"},
@@ -1965,27 +2019,27 @@ var PrecompiledRules = &ir.File{
 			DocAfter:    "x + \"_\" + y",
 			Rules: []ir.Rule{
 				{
-					Line:            631,
-					SyntaxPatterns:  []ir.PatternString{{Line: 631, Value: "strings.Join([]string{$x, $y}, \"\")"}},
+					Line:            637,
+					SyntaxPatterns:  []ir.PatternString{{Line: 637, Value: "strings.Join([]string{$x, $y}, \"\")"}},
 					ReportTemplate:  "suggestion: $x + $y",
 					SuggestTemplate: "$x + $y",
 				},
 				{
-					Line:            632,
-					SyntaxPatterns:  []ir.PatternString{{Line: 632, Value: "strings.Join([]string{$x, $y, $z}, \"\")"}},
+					Line:            638,
+					SyntaxPatterns:  []ir.PatternString{{Line: 638, Value: "strings.Join([]string{$x, $y, $z}, \"\")"}},
 					ReportTemplate:  "suggestion: $x + $y + $z",
 					SuggestTemplate: "$x + $y + $z",
 				},
 				{
-					Line:            633,
-					SyntaxPatterns:  []ir.PatternString{{Line: 633, Value: "strings.Join([]string{$x, $y}, $glue)"}},
+					Line:            639,
+					SyntaxPatterns:  []ir.PatternString{{Line: 639, Value: "strings.Join([]string{$x, $y}, $glue)"}},
 					ReportTemplate:  "suggestion: $x + $glue + $y",
 					SuggestTemplate: "$x + $glue + $y",
 				},
 			},
 		},
 		{
-			Line:        640,
+			Line:        646,
 			Name:        "timeExprSimplify",
 			MatcherName: "m",
 			DocTags:     []string{"style", "experimental"},
@@ -1994,39 +2048,39 @@ var PrecompiledRules = &ir.File{
 			DocAfter:    "t.UnixMilli()",
 			Rules: []ir.Rule{
 				{
-					Line:            645,
-					SyntaxPatterns:  []ir.PatternString{{Line: 645, Value: "$t.Unix() / 1000"}},
+					Line:            651,
+					SyntaxPatterns:  []ir.PatternString{{Line: 651, Value: "$t.Unix() / 1000"}},
 					ReportTemplate:  "use $t.UnixMilli() instead of $$",
 					SuggestTemplate: "$t.UnixMilli()",
 					WhereExpr: ir.FilterExpr{
-						Line: 646,
+						Line: 652,
 						Op:   ir.FilterAndOp,
 						Src:  "m.GoVersion().GreaterEqThan(\"1.17\") && isTime(m[\"t\"])",
 						Args: []ir.FilterExpr{
 							{
-								Line:  646,
+								Line:  652,
 								Op:    ir.FilterGoVersionGreaterEqThanOp,
 								Src:   "m.GoVersion().GreaterEqThan(\"1.17\")",
 								Value: "1.17",
 							},
 							{
-								Line: 646,
+								Line: 652,
 								Op:   ir.FilterOrOp,
 								Src:  "isTime(m[\"t\"])",
 								Args: []ir.FilterExpr{
 									{
-										Line:  646,
+										Line:  652,
 										Op:    ir.FilterVarTypeIsOp,
 										Src:   "m[\"t\"].Type.Is(`time.Time`)",
 										Value: "t",
-										Args:  []ir.FilterExpr{{Line: 642, Op: ir.FilterStringOp, Src: "`time.Time`", Value: "time.Time"}},
+										Args:  []ir.FilterExpr{{Line: 648, Op: ir.FilterStringOp, Src: "`time.Time`", Value: "time.Time"}},
 									},
 									{
-										Line:  646,
+										Line:  652,
 										Op:    ir.FilterVarTypeIsOp,
 										Src:   "m[\"t\"].Type.Is(`*time.Time`)",
 										Value: "t",
-										Args:  []ir.FilterExpr{{Line: 642, Op: ir.FilterStringOp, Src: "`*time.Time`", Value: "*time.Time"}},
+										Args:  []ir.FilterExpr{{Line: 648, Op: ir.FilterStringOp, Src: "`*time.Time`", Value: "*time.Time"}},
 									},
 								},
 							},
@@ -2034,39 +2088,39 @@ var PrecompiledRules = &ir.File{
 					},
 				},
 				{
-					Line:            650,
-					SyntaxPatterns:  []ir.PatternString{{Line: 650, Value: "$t.UnixNano() * 1000"}},
+					Line:            656,
+					SyntaxPatterns:  []ir.PatternString{{Line: 656, Value: "$t.UnixNano() * 1000"}},
 					ReportTemplate:  "use $t.UnixMicro() instead of $$",
 					SuggestTemplate: "$t.UnixMicro()",
 					WhereExpr: ir.FilterExpr{
-						Line: 651,
+						Line: 657,
 						Op:   ir.FilterAndOp,
 						Src:  "m.GoVersion().GreaterEqThan(\"1.17\") && isTime(m[\"t\"])",
 						Args: []ir.FilterExpr{
 							{
-								Line:  651,
+								Line:  657,
 								Op:    ir.FilterGoVersionGreaterEqThanOp,
 								Src:   "m.GoVersion().GreaterEqThan(\"1.17\")",
 								Value: "1.17",
 							},
 							{
-								Line: 651,
+								Line: 657,
 								Op:   ir.FilterOrOp,
 								Src:  "isTime(m[\"t\"])",
 								Args: []ir.FilterExpr{
 									{
-										Line:  651,
+										Line:  657,
 										Op:    ir.FilterVarTypeIsOp,
 										Src:   "m[\"t\"].Type.Is(`time.Time`)",
 										Value: "t",
-										Args:  []ir.FilterExpr{{Line: 642, Op: ir.FilterStringOp, Src: "`time.Time`", Value: "time.Time"}},
+										Args:  []ir.FilterExpr{{Line: 648, Op: ir.FilterStringOp, Src: "`time.Time`", Value: "time.Time"}},
 									},
 									{
-										Line:  651,
+										Line:  657,
 										Op:    ir.FilterVarTypeIsOp,
 										Src:   "m[\"t\"].Type.Is(`*time.Time`)",
 										Value: "t",
-										Args:  []ir.FilterExpr{{Line: 642, Op: ir.FilterStringOp, Src: "`*time.Time`", Value: "*time.Time"}},
+										Args:  []ir.FilterExpr{{Line: 648, Op: ir.FilterStringOp, Src: "`*time.Time`", Value: "*time.Time"}},
 									},
 								},
 							},
@@ -2076,7 +2130,7 @@ var PrecompiledRules = &ir.File{
 			},
 		},
 		{
-			Line:        660,
+			Line:        666,
 			Name:        "timeCmpSimplify",
 			MatcherName: "m",
 			DocTags:     []string{"style", "experimental"},
@@ -2085,55 +2139,55 @@ var PrecompiledRules = &ir.File{
 			DocAfter:    "t.After(tt)",
 			Rules: []ir.Rule{
 				{
-					Line:            665,
-					SyntaxPatterns:  []ir.PatternString{{Line: 665, Value: "!$t.Before($tt)"}},
+					Line:            671,
+					SyntaxPatterns:  []ir.PatternString{{Line: 671, Value: "!$t.Before($tt)"}},
 					ReportTemplate:  "suggestion: $t.After($tt)",
 					SuggestTemplate: "$t.After($tt)",
 					WhereExpr: ir.FilterExpr{
-						Line: 666,
+						Line: 672,
 						Op:   ir.FilterOrOp,
 						Src:  "isTime(m[\"t\"])",
 						Args: []ir.FilterExpr{
 							{
-								Line:  666,
+								Line:  672,
 								Op:    ir.FilterVarTypeIsOp,
 								Src:   "m[\"t\"].Type.Is(`time.Time`)",
 								Value: "t",
-								Args:  []ir.FilterExpr{{Line: 662, Op: ir.FilterStringOp, Src: "`time.Time`", Value: "time.Time"}},
+								Args:  []ir.FilterExpr{{Line: 668, Op: ir.FilterStringOp, Src: "`time.Time`", Value: "time.Time"}},
 							},
 							{
-								Line:  666,
+								Line:  672,
 								Op:    ir.FilterVarTypeIsOp,
 								Src:   "m[\"t\"].Type.Is(`*time.Time`)",
 								Value: "t",
-								Args:  []ir.FilterExpr{{Line: 662, Op: ir.FilterStringOp, Src: "`*time.Time`", Value: "*time.Time"}},
+								Args:  []ir.FilterExpr{{Line: 668, Op: ir.FilterStringOp, Src: "`*time.Time`", Value: "*time.Time"}},
 							},
 						},
 					},
 				},
 				{
-					Line:            669,
-					SyntaxPatterns:  []ir.PatternString{{Line: 669, Value: "!$t.After($tt)"}},
+					Line:            675,
+					SyntaxPatterns:  []ir.PatternString{{Line: 675, Value: "!$t.After($tt)"}},
 					ReportTemplate:  "suggestion: $t.Before($tt)",
 					SuggestTemplate: "$t.Before($tt)",
 					WhereExpr: ir.FilterExpr{
-						Line: 670,
+						Line: 676,
 						Op:   ir.FilterOrOp,
 						Src:  "isTime(m[\"t\"])",
 						Args: []ir.FilterExpr{
 							{
-								Line:  670,
+								Line:  676,
 								Op:    ir.FilterVarTypeIsOp,
 								Src:   "m[\"t\"].Type.Is(`time.Time`)",
 								Value: "t",
-								Args:  []ir.FilterExpr{{Line: 662, Op: ir.FilterStringOp, Src: "`time.Time`", Value: "time.Time"}},
+								Args:  []ir.FilterExpr{{Line: 668, Op: ir.FilterStringOp, Src: "`time.Time`", Value: "time.Time"}},
 							},
 							{
-								Line:  670,
+								Line:  676,
 								Op:    ir.FilterVarTypeIsOp,
 								Src:   "m[\"t\"].Type.Is(`*time.Time`)",
 								Value: "t",
-								Args:  []ir.FilterExpr{{Line: 662, Op: ir.FilterStringOp, Src: "`*time.Time`", Value: "*time.Time"}},
+								Args:  []ir.FilterExpr{{Line: 668, Op: ir.FilterStringOp, Src: "`*time.Time`", Value: "*time.Time"}},
 							},
 						},
 					},
@@ -2141,7 +2195,7 @@ var PrecompiledRules = &ir.File{
 			},
 		},
 		{
-			Line:        678,
+			Line:        684,
 			Name:        "exposedSyncMutex",
 			MatcherName: "m",
 			DocTags:     []string{"style", "experimental"},
@@ -2150,57 +2204,57 @@ var PrecompiledRules = &ir.File{
 			DocAfter:    "type Foo struct{ ...; mu sync.Mutex; ... }",
 			Rules: []ir.Rule{
 				{
-					Line:           683,
-					SyntaxPatterns: []ir.PatternString{{Line: 683, Value: "type $x struct { $*_; sync.Mutex; $*_ }"}},
+					Line:           689,
+					SyntaxPatterns: []ir.PatternString{{Line: 689, Value: "type $x struct { $*_; sync.Mutex; $*_ }"}},
 					ReportTemplate: "don't embed sync.Mutex",
 					WhereExpr: ir.FilterExpr{
-						Line:  684,
+						Line:  690,
 						Op:    ir.FilterVarTextMatchesOp,
 						Src:   "isExported(m[\"x\"])",
 						Value: "x",
-						Args:  []ir.FilterExpr{{Line: 680, Op: ir.FilterStringOp, Src: "`^\\p{Lu}`", Value: "^\\p{Lu}"}},
+						Args:  []ir.FilterExpr{{Line: 686, Op: ir.FilterStringOp, Src: "`^\\p{Lu}`", Value: "^\\p{Lu}"}},
 					},
 				},
 				{
-					Line:           687,
-					SyntaxPatterns: []ir.PatternString{{Line: 687, Value: "type $x struct { $*_; *sync.Mutex; $*_ }"}},
+					Line:           693,
+					SyntaxPatterns: []ir.PatternString{{Line: 693, Value: "type $x struct { $*_; *sync.Mutex; $*_ }"}},
 					ReportTemplate: "don't embed *sync.Mutex",
 					WhereExpr: ir.FilterExpr{
-						Line:  688,
+						Line:  694,
 						Op:    ir.FilterVarTextMatchesOp,
 						Src:   "isExported(m[\"x\"])",
 						Value: "x",
-						Args:  []ir.FilterExpr{{Line: 680, Op: ir.FilterStringOp, Src: "`^\\p{Lu}`", Value: "^\\p{Lu}"}},
+						Args:  []ir.FilterExpr{{Line: 686, Op: ir.FilterStringOp, Src: "`^\\p{Lu}`", Value: "^\\p{Lu}"}},
 					},
 				},
 				{
-					Line:           691,
-					SyntaxPatterns: []ir.PatternString{{Line: 691, Value: "type $x struct { $*_; sync.RWMutex; $*_ }"}},
+					Line:           697,
+					SyntaxPatterns: []ir.PatternString{{Line: 697, Value: "type $x struct { $*_; sync.RWMutex; $*_ }"}},
 					ReportTemplate: "don't embed sync.RWMutex",
 					WhereExpr: ir.FilterExpr{
-						Line:  692,
+						Line:  698,
 						Op:    ir.FilterVarTextMatchesOp,
 						Src:   "isExported(m[\"x\"])",
 						Value: "x",
-						Args:  []ir.FilterExpr{{Line: 680, Op: ir.FilterStringOp, Src: "`^\\p{Lu}`", Value: "^\\p{Lu}"}},
+						Args:  []ir.FilterExpr{{Line: 686, Op: ir.FilterStringOp, Src: "`^\\p{Lu}`", Value: "^\\p{Lu}"}},
 					},
 				},
 				{
-					Line:           695,
-					SyntaxPatterns: []ir.PatternString{{Line: 695, Value: "type $x struct { $*_; *sync.RWMutex; $*_ }"}},
+					Line:           701,
+					SyntaxPatterns: []ir.PatternString{{Line: 701, Value: "type $x struct { $*_; *sync.RWMutex; $*_ }"}},
 					ReportTemplate: "don't embed *sync.RWMutex",
 					WhereExpr: ir.FilterExpr{
-						Line:  696,
+						Line:  702,
 						Op:    ir.FilterVarTextMatchesOp,
 						Src:   "isExported(m[\"x\"])",
 						Value: "x",
-						Args:  []ir.FilterExpr{{Line: 680, Op: ir.FilterStringOp, Src: "`^\\p{Lu}`", Value: "^\\p{Lu}"}},
+						Args:  []ir.FilterExpr{{Line: 686, Op: ir.FilterStringOp, Src: "`^\\p{Lu}`", Value: "^\\p{Lu}"}},
 					},
 				},
 			},
 		},
 		{
-			Line:        704,
+			Line:        710,
 			Name:        "badSorting",
 			MatcherName: "m",
 			DocTags:     []string{"diagnostic", "experimental"},
@@ -2209,48 +2263,48 @@ var PrecompiledRules = &ir.File{
 			DocAfter:    "sort.Strings(xs)",
 			Rules: []ir.Rule{
 				{
-					Line:            705,
-					SyntaxPatterns:  []ir.PatternString{{Line: 705, Value: "$x = sort.IntSlice($x)"}},
+					Line:            711,
+					SyntaxPatterns:  []ir.PatternString{{Line: 711, Value: "$x = sort.IntSlice($x)"}},
 					ReportTemplate:  "suspicious sort.IntSlice usage, maybe sort.Ints was intended?",
 					SuggestTemplate: "sort.Ints($x)",
 					WhereExpr: ir.FilterExpr{
-						Line:  706,
+						Line:  712,
 						Op:    ir.FilterVarTypeIsOp,
 						Src:   "m[\"x\"].Type.Is(`[]int`)",
 						Value: "x",
-						Args:  []ir.FilterExpr{{Line: 706, Op: ir.FilterStringOp, Src: "`[]int`", Value: "[]int"}},
+						Args:  []ir.FilterExpr{{Line: 712, Op: ir.FilterStringOp, Src: "`[]int`", Value: "[]int"}},
 					},
 				},
 				{
-					Line:            710,
-					SyntaxPatterns:  []ir.PatternString{{Line: 710, Value: "$x = sort.Float64Slice($x)"}},
+					Line:            716,
+					SyntaxPatterns:  []ir.PatternString{{Line: 716, Value: "$x = sort.Float64Slice($x)"}},
 					ReportTemplate:  "suspicious sort.Float64s usage, maybe sort.Float64s was intended?",
 					SuggestTemplate: "sort.Float64s($x)",
 					WhereExpr: ir.FilterExpr{
-						Line:  711,
+						Line:  717,
 						Op:    ir.FilterVarTypeIsOp,
 						Src:   "m[\"x\"].Type.Is(`[]float64`)",
 						Value: "x",
-						Args:  []ir.FilterExpr{{Line: 711, Op: ir.FilterStringOp, Src: "`[]float64`", Value: "[]float64"}},
+						Args:  []ir.FilterExpr{{Line: 717, Op: ir.FilterStringOp, Src: "`[]float64`", Value: "[]float64"}},
 					},
 				},
 				{
-					Line:            715,
-					SyntaxPatterns:  []ir.PatternString{{Line: 715, Value: "$x = sort.StringSlice($x)"}},
+					Line:            721,
+					SyntaxPatterns:  []ir.PatternString{{Line: 721, Value: "$x = sort.StringSlice($x)"}},
 					ReportTemplate:  "suspicious sort.StringSlice usage, maybe sort.Strings was intended?",
 					SuggestTemplate: "sort.Strings($x)",
 					WhereExpr: ir.FilterExpr{
-						Line:  716,
+						Line:  722,
 						Op:    ir.FilterVarTypeIsOp,
 						Src:   "m[\"x\"].Type.Is(`[]string`)",
 						Value: "x",
-						Args:  []ir.FilterExpr{{Line: 716, Op: ir.FilterStringOp, Src: "`[]string`", Value: "[]string"}},
+						Args:  []ir.FilterExpr{{Line: 722, Op: ir.FilterStringOp, Src: "`[]string`", Value: "[]string"}},
 					},
 				},
 			},
 		},
 		{
-			Line:        725,
+			Line:        731,
 			Name:        "externalErrorReassign",
 			MatcherName: "m",
 			DocTags:     []string{"diagnostic", "experimental"},
@@ -2258,34 +2312,34 @@ var PrecompiledRules = &ir.File{
 			DocBefore:   "io.EOF = nil",
 			DocAfter:    "/* don't do it */",
 			Rules: []ir.Rule{{
-				Line:           726,
-				SyntaxPatterns: []ir.PatternString{{Line: 726, Value: "$pkg.$err = $x"}},
+				Line:           732,
+				SyntaxPatterns: []ir.PatternString{{Line: 732, Value: "$pkg.$err = $x"}},
 				ReportTemplate: "suspicious reassigment of error from another package",
 				WhereExpr: ir.FilterExpr{
-					Line: 727,
+					Line: 733,
 					Op:   ir.FilterAndOp,
 					Src:  "m[\"err\"].Type.Is(`error`) && m[\"pkg\"].Object.Is(`PkgName`)",
 					Args: []ir.FilterExpr{
 						{
-							Line:  727,
+							Line:  733,
 							Op:    ir.FilterVarTypeIsOp,
 							Src:   "m[\"err\"].Type.Is(`error`)",
 							Value: "err",
-							Args:  []ir.FilterExpr{{Line: 727, Op: ir.FilterStringOp, Src: "`error`", Value: "error"}},
+							Args:  []ir.FilterExpr{{Line: 733, Op: ir.FilterStringOp, Src: "`error`", Value: "error"}},
 						},
 						{
-							Line:  727,
+							Line:  733,
 							Op:    ir.FilterVarObjectIsOp,
 							Src:   "m[\"pkg\"].Object.Is(`PkgName`)",
 							Value: "pkg",
-							Args:  []ir.FilterExpr{{Line: 727, Op: ir.FilterStringOp, Src: "`PkgName`", Value: "PkgName"}},
+							Args:  []ir.FilterExpr{{Line: 733, Op: ir.FilterStringOp, Src: "`PkgName`", Value: "PkgName"}},
 						},
 					},
 				},
 			}},
 		},
 		{
-			Line:        735,
+			Line:        741,
 			Name:        "emptyDecl",
 			MatcherName: "m",
 			DocTags:     []string{"diagnostic", "experimental"},
@@ -2294,24 +2348,24 @@ var PrecompiledRules = &ir.File{
 			DocAfter:    "/* nothing */",
 			Rules: []ir.Rule{
 				{
-					Line:           736,
-					SyntaxPatterns: []ir.PatternString{{Line: 736, Value: "var()"}},
+					Line:           742,
+					SyntaxPatterns: []ir.PatternString{{Line: 742, Value: "var()"}},
 					ReportTemplate: "empty var() block",
 				},
 				{
-					Line:           737,
-					SyntaxPatterns: []ir.PatternString{{Line: 737, Value: "const()"}},
+					Line:           743,
+					SyntaxPatterns: []ir.PatternString{{Line: 743, Value: "const()"}},
 					ReportTemplate: "empty const() block",
 				},
 				{
-					Line:           738,
-					SyntaxPatterns: []ir.PatternString{{Line: 738, Value: "type()"}},
+					Line:           744,
+					SyntaxPatterns: []ir.PatternString{{Line: 744, Value: "type()"}},
 					ReportTemplate: "empty type() block",
 				},
 			},
 		},
 		{
-			Line:        745,
+			Line:        751,
 			Name:        "dynamicFmtString",
 			MatcherName: "m",
 			DocTags:     []string{"diagnostic", "experimental"},
@@ -2320,16 +2374,16 @@ var PrecompiledRules = &ir.File{
 			DocAfter:    "fmt.Errorf(\"%s\", msg)",
 			Rules: []ir.Rule{
 				{
-					Line:            746,
-					SyntaxPatterns:  []ir.PatternString{{Line: 746, Value: "fmt.Errorf($f)"}},
+					Line:            752,
+					SyntaxPatterns:  []ir.PatternString{{Line: 752, Value: "fmt.Errorf($f)"}},
 					ReportTemplate:  "use errors.New($f) or fmt.Errorf(\"%s\", $f) instead",
 					SuggestTemplate: "errors.New($f)",
 					WhereExpr: ir.FilterExpr{
-						Line: 747,
+						Line: 753,
 						Op:   ir.FilterNotOp,
 						Src:  "!m[\"f\"].Const",
 						Args: []ir.FilterExpr{{
-							Line:  747,
+							Line:  753,
 							Op:    ir.FilterVarConstOp,
 							Src:   "m[\"f\"].Const",
 							Value: "f",
@@ -2337,15 +2391,15 @@ var PrecompiledRules = &ir.File{
 					},
 				},
 				{
-					Line:            751,
-					SyntaxPatterns:  []ir.PatternString{{Line: 751, Value: "fmt.Errorf($f($*args))"}},
+					Line:            757,
+					SyntaxPatterns:  []ir.PatternString{{Line: 757, Value: "fmt.Errorf($f($*args))"}},
 					ReportTemplate:  "use errors.New($f($*args)) or fmt.Errorf(\"%s\", $f($*args)) instead",
 					SuggestTemplate: "errors.New($f($*args))",
 				},
 			},
 		},
 		{
-			Line:        760,
+			Line:        766,
 			Name:        "stringsCompare",
 			MatcherName: "m",
 			DocTags:     []string{"style", "experimental"},
@@ -2354,25 +2408,25 @@ var PrecompiledRules = &ir.File{
 			DocAfter:    "x < y",
 			Rules: []ir.Rule{
 				{
-					Line:            761,
-					SyntaxPatterns:  []ir.PatternString{{Line: 761, Value: "strings.Compare($s1, $s2) == 0"}},
+					Line:            767,
+					SyntaxPatterns:  []ir.PatternString{{Line: 767, Value: "strings.Compare($s1, $s2) == 0"}},
 					ReportTemplate:  "suggestion: $s1 == $s2",
 					SuggestTemplate: "$s1 == $s2",
 				},
 				{
-					Line: 764,
+					Line: 770,
 					SyntaxPatterns: []ir.PatternString{
-						{Line: 764, Value: "strings.Compare($s1, $s2) == -1"},
-						{Line: 765, Value: "strings.Compare($s1, $s2) < 0"},
+						{Line: 770, Value: "strings.Compare($s1, $s2) == -1"},
+						{Line: 771, Value: "strings.Compare($s1, $s2) < 0"},
 					},
 					ReportTemplate:  "suggestion: $s1 < $s2",
 					SuggestTemplate: "$s1 < $s2",
 				},
 				{
-					Line: 768,
+					Line: 774,
 					SyntaxPatterns: []ir.PatternString{
-						{Line: 768, Value: "strings.Compare($s1, $s2) == 1"},
-						{Line: 769, Value: "strings.Compare($s1, $s2) > 0"},
+						{Line: 774, Value: "strings.Compare($s1, $s2) == 1"},
+						{Line: 775, Value: "strings.Compare($s1, $s2) > 0"},
 					},
 					ReportTemplate:  "suggestion: $s1 > $s2",
 					SuggestTemplate: "$s1 > $s2",
@@ -2380,7 +2434,7 @@ var PrecompiledRules = &ir.File{
 			},
 		},
 		{
-			Line:        777,
+			Line:        783,
 			Name:        "uncheckedInlineErr",
 			MatcherName: "m",
 			DocTags:     []string{"diagnostic", "experimental"},
@@ -2388,47 +2442,47 @@ var PrecompiledRules = &ir.File{
 			DocBefore:   "if err := expr(); err2 != nil { /*...*/ }",
 			DocAfter:    "if err := expr(); err != nil { /*...*/ }",
 			Rules: []ir.Rule{{
-				Line: 778,
+				Line: 784,
 				SyntaxPatterns: []ir.PatternString{
-					{Line: 779, Value: "if $err := $_($*_); $err2 != nil { $*_ }"},
-					{Line: 780, Value: "if $err = $_($*_); $err2 != nil { $*_ }"},
-					{Line: 781, Value: "if $*_, $err := $_($*_); $err2 != nil { $*_ }"},
-					{Line: 782, Value: "if $*_, $err = $_($*_); $err2 != nil { $*_ }"},
+					{Line: 785, Value: "if $err := $_($*_); $err2 != nil { $*_ }"},
+					{Line: 786, Value: "if $err = $_($*_); $err2 != nil { $*_ }"},
+					{Line: 787, Value: "if $*_, $err := $_($*_); $err2 != nil { $*_ }"},
+					{Line: 788, Value: "if $*_, $err = $_($*_); $err2 != nil { $*_ }"},
 				},
 				ReportTemplate: "$err error is unchecked, maybe intended to check it instead of $err2",
 				WhereExpr: ir.FilterExpr{
-					Line: 783,
+					Line: 789,
 					Op:   ir.FilterAndOp,
 					Src:  "m[\"err\"].Type.Implements(\"error\") && m[\"err2\"].Type.Implements(\"error\") &&\n\tm[\"err\"].Text != m[\"err2\"].Text",
 					Args: []ir.FilterExpr{
 						{
-							Line: 783,
+							Line: 789,
 							Op:   ir.FilterAndOp,
 							Src:  "m[\"err\"].Type.Implements(\"error\") && m[\"err2\"].Type.Implements(\"error\")",
 							Args: []ir.FilterExpr{
 								{
-									Line:  783,
+									Line:  789,
 									Op:    ir.FilterVarTypeImplementsOp,
 									Src:   "m[\"err\"].Type.Implements(\"error\")",
 									Value: "err",
-									Args:  []ir.FilterExpr{{Line: 783, Op: ir.FilterStringOp, Src: "\"error\"", Value: "error"}},
+									Args:  []ir.FilterExpr{{Line: 789, Op: ir.FilterStringOp, Src: "\"error\"", Value: "error"}},
 								},
 								{
-									Line:  783,
+									Line:  789,
 									Op:    ir.FilterVarTypeImplementsOp,
 									Src:   "m[\"err2\"].Type.Implements(\"error\")",
 									Value: "err2",
-									Args:  []ir.FilterExpr{{Line: 783, Op: ir.FilterStringOp, Src: "\"error\"", Value: "error"}},
+									Args:  []ir.FilterExpr{{Line: 789, Op: ir.FilterStringOp, Src: "\"error\"", Value: "error"}},
 								},
 							},
 						},
 						{
-							Line: 784,
+							Line: 790,
 							Op:   ir.FilterNeqOp,
 							Src:  "m[\"err\"].Text != m[\"err2\"].Text",
 							Args: []ir.FilterExpr{
-								{Line: 784, Op: ir.FilterVarTextOp, Src: "m[\"err\"].Text", Value: "err"},
-								{Line: 784, Op: ir.FilterVarTextOp, Src: "m[\"err2\"].Text", Value: "err2"},
+								{Line: 790, Op: ir.FilterVarTextOp, Src: "m[\"err\"].Text", Value: "err"},
+								{Line: 790, Op: ir.FilterVarTextOp, Src: "m[\"err2\"].Text", Value: "err2"},
 							},
 						},
 					},
diff --git a/checkers/singleCaseSwitch_checker.go b/checkers/singleCaseSwitch_checker.go
index b369a43..4de5d19 100644
--- a/checkers/singleCaseSwitch_checker.go
+++ b/checkers/singleCaseSwitch_checker.go
@@ -5,7 +5,8 @@ import (
 	"go/token"
 
 	"github.com/go-critic/go-critic/checkers/internal/astwalk"
-	"github.com/go-critic/go-critic/framework/linter"
+	"github.com/go-critic/go-critic/linter"
+
 	"golang.org/x/tools/go/ast/astutil"
 )
 
diff --git a/checkers/sloppyReassign_checker.go b/checkers/sloppyReassign_checker.go
index 2f9ac62..c80a171 100644
--- a/checkers/sloppyReassign_checker.go
+++ b/checkers/sloppyReassign_checker.go
@@ -5,7 +5,8 @@ import (
 	"go/token"
 
 	"github.com/go-critic/go-critic/checkers/internal/astwalk"
-	"github.com/go-critic/go-critic/framework/linter"
+	"github.com/go-critic/go-critic/linter"
+
 	"github.com/go-toolsmith/astcast"
 	"github.com/go-toolsmith/astcopy"
 	"github.com/go-toolsmith/astequal"
diff --git a/checkers/sloppyTypeAssert_checker.go b/checkers/sloppyTypeAssert_checker.go
index 5541977..c3bc159 100644
--- a/checkers/sloppyTypeAssert_checker.go
+++ b/checkers/sloppyTypeAssert_checker.go
@@ -5,7 +5,8 @@ import (
 	"go/types"
 
 	"github.com/go-critic/go-critic/checkers/internal/astwalk"
-	"github.com/go-critic/go-critic/framework/linter"
+	"github.com/go-critic/go-critic/linter"
+
 	"github.com/go-toolsmith/astcast"
 )
 
diff --git a/checkers/sortSlice_checker.go b/checkers/sortSlice_checker.go
index 29550da..4394f20 100644
--- a/checkers/sortSlice_checker.go
+++ b/checkers/sortSlice_checker.go
@@ -6,7 +6,8 @@ import (
 
 	"github.com/go-critic/go-critic/checkers/internal/astwalk"
 	"github.com/go-critic/go-critic/checkers/internal/lintutil"
-	"github.com/go-critic/go-critic/framework/linter"
+	"github.com/go-critic/go-critic/linter"
+
 	"github.com/go-toolsmith/astcast"
 	"github.com/go-toolsmith/astequal"
 	"github.com/go-toolsmith/typep"
diff --git a/checkers/sqlQuery_checker.go b/checkers/sqlQuery_checker.go
index eb3b49d..c1cf6bb 100644
--- a/checkers/sqlQuery_checker.go
+++ b/checkers/sqlQuery_checker.go
@@ -5,7 +5,8 @@ import (
 	"go/types"
 
 	"github.com/go-critic/go-critic/checkers/internal/astwalk"
-	"github.com/go-critic/go-critic/framework/linter"
+	"github.com/go-critic/go-critic/linter"
+
 	"github.com/go-toolsmith/astcast"
 )
 
diff --git a/checkers/todoCommentWithoutDetail_checker.go b/checkers/todoCommentWithoutDetail_checker.go
index 5ec2881..ca2b6d2 100644
--- a/checkers/todoCommentWithoutDetail_checker.go
+++ b/checkers/todoCommentWithoutDetail_checker.go
@@ -5,7 +5,7 @@ import (
 	"regexp"
 
 	"github.com/go-critic/go-critic/checkers/internal/astwalk"
-	"github.com/go-critic/go-critic/framework/linter"
+	"github.com/go-critic/go-critic/linter"
 )
 
 func init() {
diff --git a/checkers/tooManyResults_checker.go b/checkers/tooManyResults_checker.go
index 4d4dcc2..9527c57 100644
--- a/checkers/tooManyResults_checker.go
+++ b/checkers/tooManyResults_checker.go
@@ -5,7 +5,7 @@ import (
 	"go/types"
 
 	"github.com/go-critic/go-critic/checkers/internal/astwalk"
-	"github.com/go-critic/go-critic/framework/linter"
+	"github.com/go-critic/go-critic/linter"
 )
 
 func init() {
diff --git a/checkers/truncateCmp_checker.go b/checkers/truncateCmp_checker.go
index 9d40c2b..34946d1 100644
--- a/checkers/truncateCmp_checker.go
+++ b/checkers/truncateCmp_checker.go
@@ -6,7 +6,8 @@ import (
 	"go/types"
 
 	"github.com/go-critic/go-critic/checkers/internal/astwalk"
-	"github.com/go-critic/go-critic/framework/linter"
+	"github.com/go-critic/go-critic/linter"
+
 	"github.com/go-toolsmith/astcast"
 	"github.com/go-toolsmith/astp"
 )
diff --git a/checkers/typeAssertChain_checker.go b/checkers/typeAssertChain_checker.go
index d87657c..48757bc 100644
--- a/checkers/typeAssertChain_checker.go
+++ b/checkers/typeAssertChain_checker.go
@@ -6,7 +6,8 @@ import (
 
 	"github.com/go-critic/go-critic/checkers/internal/astwalk"
 	"github.com/go-critic/go-critic/checkers/internal/lintutil"
-	"github.com/go-critic/go-critic/framework/linter"
+	"github.com/go-critic/go-critic/linter"
+
 	"github.com/go-toolsmith/astcast"
 	"github.com/go-toolsmith/astequal"
 	"github.com/go-toolsmith/astp"
diff --git a/checkers/typeDefFirst_checker.go b/checkers/typeDefFirst_checker.go
index bc59eef..2177001 100644
--- a/checkers/typeDefFirst_checker.go
+++ b/checkers/typeDefFirst_checker.go
@@ -5,8 +5,7 @@ import (
 	"go/token"
 
 	"github.com/go-critic/go-critic/checkers/internal/astwalk"
-	"github.com/go-critic/go-critic/framework/linter"
-	"golang.org/x/exp/typeparams"
+	"github.com/go-critic/go-critic/linter"
 )
 
 func init() {
@@ -81,7 +80,7 @@ func (c *typeDefFirstChecker) receiverType(e ast.Expr) string {
 		return e.Name
 	case *ast.IndexExpr:
 		return c.receiverType(e.X)
-	case *typeparams.IndexListExpr:
+	case *ast.IndexListExpr:
 		return c.receiverType(e.X)
 	default:
 		panic("unreachable")
diff --git a/checkers/typeSwitchVar_checker.go b/checkers/typeSwitchVar_checker.go
index 1e11e49..b4339de 100644
--- a/checkers/typeSwitchVar_checker.go
+++ b/checkers/typeSwitchVar_checker.go
@@ -5,7 +5,8 @@ import (
 
 	"github.com/go-critic/go-critic/checkers/internal/astwalk"
 	"github.com/go-critic/go-critic/checkers/internal/lintutil"
-	"github.com/go-critic/go-critic/framework/linter"
+	"github.com/go-critic/go-critic/linter"
+
 	"github.com/go-toolsmith/astequal"
 	"github.com/go-toolsmith/astp"
 )
diff --git a/checkers/typeUnparen_checker.go b/checkers/typeUnparen_checker.go
index cd8e043..8c6491c 100644
--- a/checkers/typeUnparen_checker.go
+++ b/checkers/typeUnparen_checker.go
@@ -4,7 +4,8 @@ import (
 	"go/ast"
 
 	"github.com/go-critic/go-critic/checkers/internal/astwalk"
-	"github.com/go-critic/go-critic/framework/linter"
+	"github.com/go-critic/go-critic/linter"
+
 	"github.com/go-toolsmith/astcopy"
 	"github.com/go-toolsmith/astequal"
 )
diff --git a/checkers/underef_checker.go b/checkers/underef_checker.go
index d0426a9..e9d7cc6 100644
--- a/checkers/underef_checker.go
+++ b/checkers/underef_checker.go
@@ -5,7 +5,8 @@ import (
 	"go/types"
 
 	"github.com/go-critic/go-critic/checkers/internal/astwalk"
-	"github.com/go-critic/go-critic/framework/linter"
+	"github.com/go-critic/go-critic/linter"
+
 	"github.com/go-toolsmith/astcast"
 	"github.com/go-toolsmith/astp"
 )
diff --git a/checkers/unlabelStmt_checker.go b/checkers/unlabelStmt_checker.go
index bcca24d..38e689c 100644
--- a/checkers/unlabelStmt_checker.go
+++ b/checkers/unlabelStmt_checker.go
@@ -6,7 +6,7 @@ import (
 
 	"github.com/go-critic/go-critic/checkers/internal/astwalk"
 	"github.com/go-critic/go-critic/checkers/internal/lintutil"
-	"github.com/go-critic/go-critic/framework/linter"
+	"github.com/go-critic/go-critic/linter"
 )
 
 func init() {
diff --git a/checkers/unlambda_checker.go b/checkers/unlambda_checker.go
index cce995d..6e5ed59 100644
--- a/checkers/unlambda_checker.go
+++ b/checkers/unlambda_checker.go
@@ -7,7 +7,8 @@ import (
 
 	"github.com/go-critic/go-critic/checkers/internal/astwalk"
 	"github.com/go-critic/go-critic/checkers/internal/lintutil"
-	"github.com/go-critic/go-critic/framework/linter"
+	"github.com/go-critic/go-critic/linter"
+
 	"github.com/go-toolsmith/astcast"
 	"github.com/go-toolsmith/astequal"
 	"github.com/go-toolsmith/typep"
diff --git a/checkers/unnamedResult_checker.go b/checkers/unnamedResult_checker.go
index 3149d9e..5f1afbd 100644
--- a/checkers/unnamedResult_checker.go
+++ b/checkers/unnamedResult_checker.go
@@ -5,7 +5,7 @@ import (
 	"go/types"
 
 	"github.com/go-critic/go-critic/checkers/internal/astwalk"
-	"github.com/go-critic/go-critic/framework/linter"
+	"github.com/go-critic/go-critic/linter"
 )
 
 func init() {
diff --git a/checkers/unnecessaryBlock_checker.go b/checkers/unnecessaryBlock_checker.go
index 6cbdfdf..b1d1728 100644
--- a/checkers/unnecessaryBlock_checker.go
+++ b/checkers/unnecessaryBlock_checker.go
@@ -5,7 +5,8 @@ import (
 	"go/token"
 
 	"github.com/go-critic/go-critic/checkers/internal/astwalk"
-	"github.com/go-critic/go-critic/framework/linter"
+	"github.com/go-critic/go-critic/linter"
+
 	"github.com/go-toolsmith/astp"
 )
 
diff --git a/checkers/unnecessaryDefer_checker.go b/checkers/unnecessaryDefer_checker.go
index ef72142..a3863e6 100644
--- a/checkers/unnecessaryDefer_checker.go
+++ b/checkers/unnecessaryDefer_checker.go
@@ -4,7 +4,8 @@ import (
 	"go/ast"
 
 	"github.com/go-critic/go-critic/checkers/internal/astwalk"
-	"github.com/go-critic/go-critic/framework/linter"
+	"github.com/go-critic/go-critic/linter"
+
 	"github.com/go-toolsmith/astfmt"
 )
 
diff --git a/checkers/utils.go b/checkers/utils.go
index b71f24d..e912335 100644
--- a/checkers/utils.go
+++ b/checkers/utils.go
@@ -5,7 +5,7 @@ import (
 	"go/types"
 	"strings"
 
-	"github.com/go-critic/go-critic/framework/linter"
+	"github.com/go-critic/go-critic/linter"
 )
 
 // goStdlib contains `go list std` command output list.
diff --git a/checkers/weakCond_checker.go b/checkers/weakCond_checker.go
index 831857c..42f63e2 100644
--- a/checkers/weakCond_checker.go
+++ b/checkers/weakCond_checker.go
@@ -6,7 +6,8 @@ import (
 
 	"github.com/go-critic/go-critic/checkers/internal/astwalk"
 	"github.com/go-critic/go-critic/checkers/internal/lintutil"
-	"github.com/go-critic/go-critic/framework/linter"
+	"github.com/go-critic/go-critic/linter"
+
 	"github.com/go-toolsmith/astcast"
 	"github.com/go-toolsmith/astequal"
 	"github.com/go-toolsmith/typep"
diff --git a/checkers/whyNoLint_checker.go b/checkers/whyNoLint_checker.go
index 6829433..4840018 100644
--- a/checkers/whyNoLint_checker.go
+++ b/checkers/whyNoLint_checker.go
@@ -6,7 +6,7 @@ import (
 	"strings"
 
 	"github.com/go-critic/go-critic/checkers/internal/astwalk"
-	"github.com/go-critic/go-critic/framework/linter"
+	"github.com/go-critic/go-critic/linter"
 )
 
 func init() {
diff --git a/framework/lintmain/internal/check/check.go b/cmd/gocritic/check.go
similarity index 91%
rename from framework/lintmain/internal/check/check.go
rename to cmd/gocritic/check.go
index 467a2d5..2bd93eb 100644
--- a/framework/lintmain/internal/check/check.go
+++ b/cmd/gocritic/check.go
@@ -1,4 +1,4 @@
-package check
+package main
 
 import (
 	"bytes"
@@ -16,19 +16,17 @@ import (
 	"regexp"
 	"runtime"
 	"runtime/pprof"
-	"sort"
 	"strings"
 	"sync"
 
+	"github.com/go-critic/go-critic/linter"
+
 	"github.com/go-toolsmith/pkgload"
 	"golang.org/x/tools/go/packages"
-
-	"github.com/go-critic/go-critic/framework/linter"
-	"github.com/go-critic/go-critic/framework/lintmain/internal/hotload"
 )
 
 // Main implements sub-command entry point.
-func Main(_ context.Context, args []string) error {
+func runCheck(_ context.Context, args []string) error {
 	var p program
 	p.flagSet = flag.NewFlagSet("gocritic", flag.ContinueOnError)
 	p.infoList = linter.GetCheckersInfo()
@@ -37,7 +35,6 @@ func Main(_ context.Context, args []string) error {
 		name string
 		fn   func() error
 	}{
-		{"load plugin", p.loadPlugin},
 		{"bind checker params", p.bindCheckerParams},
 		{"bind default enabled list", p.bindDefaultEnabledList},
 		{"parse args", func() error { return p.parseArgs(args) }},
@@ -276,13 +273,10 @@ func (p *program) loadProgram() error {
 		Tests: true,
 		Fset:  p.fset,
 	}
-	pkgs, err := loadPackages(&cfg, p.packages)
+	pkgs, err := pkgload.LoadPackages(&cfg, p.packages)
 	if err != nil {
 		log.Fatalf("load packages: %v", err)
 	}
-	sort.SliceStable(pkgs, func(i, j int) bool {
-		return pkgs[i].PkgPath < pkgs[j].PkgPath
-	})
 
 	p.loadedPackages = pkgs
 	p.ctx = linter.NewContext(p.fset, sizes)
@@ -291,16 +285,6 @@ func (p *program) loadProgram() error {
 	return nil
 }
 
-func (p *program) loadPlugin() error {
-	const pluginFilename = "gocritic-plugin.so"
-	if _, err := os.Stat(pluginFilename); os.IsNotExist(err) {
-		return nil
-	}
-	infoList, err := hotload.CheckersFromDylib(p.infoList, pluginFilename)
-	p.infoList = infoList
-	return err
-}
-
 type boundCheckerParams struct {
 	ints    map[string]*int
 	bools   map[string]*bool
@@ -505,25 +489,3 @@ func (p *program) shortenLocation(loc string) string {
 	}
 	return loc
 }
-
-func loadPackages(cfg *packages.Config, patterns []string) ([]*packages.Package, error) {
-	pkgs, err := packages.Load(cfg, patterns...)
-	if err != nil {
-		return nil, err
-	}
-
-	result := pkgs[:0]
-	pkgload.VisitUnits(pkgs, func(u *pkgload.Unit) {
-		if u.ExternalTest != nil {
-			result = append(result, u.ExternalTest)
-		}
-
-		if u.Test != nil {
-			// Prefer tests to the base package, if present.
-			result = append(result, u.Test)
-		} else {
-			result = append(result, u.Base)
-		}
-	})
-	return result, nil
-}
diff --git a/framework/lintmain/internal/lintdoc/doc.go b/cmd/gocritic/doc.go
similarity index 94%
rename from framework/lintmain/internal/lintdoc/doc.go
rename to cmd/gocritic/doc.go
index 4609114..81acdbc 100644
--- a/framework/lintmain/internal/lintdoc/doc.go
+++ b/cmd/gocritic/doc.go
@@ -1,4 +1,4 @@
-package lintdoc
+package main
 
 import (
 	"context"
@@ -9,11 +9,11 @@ import (
 	"reflect"
 	"text/template"
 
-	"github.com/go-critic/go-critic/framework/linter"
+	"github.com/go-critic/go-critic/linter"
 )
 
 // Main implements sub-command entry point.
-func Main(_ context.Context, args []string) error {
+func runDocs(_ context.Context, args []string) error {
 	flagSet := flag.NewFlagSet("gocritic", flag.ContinueOnError)
 	if err := flagSet.Parse(args); err != nil {
 		return err
diff --git a/cmd/gocritic/main.go b/cmd/gocritic/main.go
index 47e8644..f8a87b7 100644
--- a/cmd/gocritic/main.go
+++ b/cmd/gocritic/main.go
@@ -1,8 +1,11 @@
 package main
 
 import (
-	"github.com/go-critic/go-critic/checkers" // Register go-critic checkers
-	"github.com/go-critic/go-critic/framework/lintmain"
+	"log"
+
+	"github.com/go-critic/go-critic/checkers"
+
+	"github.com/cristalhq/acmd"
 )
 
 var Version = "v0.0.0-SNAPSHOT"
@@ -13,8 +16,41 @@ func main() {
 		panic(err)
 	}
 
-	lintmain.Run(lintmain.Config{
+	run(config{
 		Name:    "gocritic",
 		Version: Version,
 	})
 }
+
+// config is used to parametrize the linter.
+type config struct {
+	Version string
+	Name    string
+}
+
+// Run executes corresponding main after sub-command resolving.
+// Does not return.
+func run(cfg config) {
+	log.SetFlags(0)
+
+	cmds := []acmd.Command{
+		{
+			Name:        "check",
+			Description: "run linter over specified targets",
+			ExecFunc:    runCheck,
+		},
+		{
+			Name:        "doc",
+			Description: "get installed checkers documentation",
+			ExecFunc:    runDocs,
+		},
+	}
+
+	r := acmd.RunnerOf(cmds, acmd.Config{
+		AppName: cfg.Name,
+		Version: cfg.Version,
+	})
+	if err := r.Run(); err != nil {
+		log.Print(err.Error())
+	}
+}
diff --git a/cmd/makedocs/main.go b/cmd/makedocs/main.go
index dcd81ba..3336316 100644
--- a/cmd/makedocs/main.go
+++ b/cmd/makedocs/main.go
@@ -8,7 +8,7 @@ import (
 	"text/template"
 
 	_ "github.com/go-critic/go-critic/checkers"
-	"github.com/go-critic/go-critic/framework/linter"
+	"github.com/go-critic/go-critic/linter"
 )
 
 const (
diff --git a/dependabot.yml b/dependabot.yml
deleted file mode 100644
index 20d5f35..0000000
--- a/dependabot.yml
+++ /dev/null
@@ -1,10 +0,0 @@
-version: 2
-updates:
-  - package-ecosystem: gomod
-    directory: "/"
-    schedule:
-      interval: daily
-  - package-ecosystem: github-actions
-    directory: "/"
-    schedule:
-      interval: daily
diff --git a/framework/linter/context.go b/framework/linter/context.go
deleted file mode 100644
index 6e108ab..0000000
--- a/framework/linter/context.go
+++ /dev/null
@@ -1,35 +0,0 @@
-package linter
-
-import (
-	"go/ast"
-	"go/types"
-	"strconv"
-)
-
-func resolvePkgObjects(ctx *Context, f *ast.File) {
-	ctx.PkgObjects = make(map[*types.PkgName]string, len(f.Imports))
-
-	for _, spec := range f.Imports {
-		if spec.Name != nil {
-			obj := ctx.TypesInfo.ObjectOf(spec.Name)
-			ctx.PkgObjects[obj.(*types.PkgName)] = spec.Name.Name
-		} else {
-			obj := ctx.TypesInfo.Implicits[spec]
-			ctx.PkgObjects[obj.(*types.PkgName)] = obj.Name()
-		}
-	}
-}
-
-func resolvePkgRenames(ctx *Context, f *ast.File) {
-	ctx.PkgRenames = make(map[string]string)
-
-	for _, spec := range f.Imports {
-		if spec.Name != nil {
-			path, err := strconv.Unquote(spec.Path.Value)
-			if err != nil {
-				panic(err)
-			}
-			ctx.PkgRenames[path] = spec.Name.Name
-		}
-	}
-}
diff --git a/framework/linter/linter.go b/framework/linter/linter.go
index 094e157..c2a7a07 100644
--- a/framework/linter/linter.go
+++ b/framework/linter/linter.go
@@ -1,342 +1,95 @@
 package linter
 
 import (
-	"go/ast"
 	"go/token"
 	"go/types"
 
-	"github.com/go-toolsmith/astfmt"
-	"golang.org/x/exp/typeparams"
+	"github.com/go-critic/go-critic/linter"
 )
 
-// CheckerCollection provides additional information for a group of checkers.
-type CheckerCollection struct {
-	// URL is a link for a main source of information on the collection.
-	URL string
-}
+// UnknownType is a special sentinel value that is returned from the CheckerContext.TypeOf
+// method instead of the nil type.
+// Deprecated: use linter.UnknownType.
+var UnknownType = linter.UnknownType
 
-// AddChecker registers a new checker into a checkers pool.
-// Constructor is used to create a new checker instance.
-// Checker name (defined in CheckerInfo.Name) must be unique.
-//
-// CheckerInfo.Collection is automatically set to the coll (the receiver).
-//
-// If checker is never needed, for example if it is disabled,
-// constructor will not be called.
-func (coll *CheckerCollection) AddChecker(info *CheckerInfo, constructor func(*CheckerContext) (FileWalker, error)) {
-	if coll == nil {
-		panic("adding checker to a nil collection")
-	}
-	info.Collection = coll
-	addChecker(info, constructor)
-}
+// Deprecated: use linter.GoVersion.
+type GoVersion = linter.GoVersion
 
-// CheckerParam describes a single checker customizable parameter.
-type CheckerParam struct {
-	// Value holds parameter bound value.
-	// It might be overwritten by the integrating linter.
-	//
-	// Permitted types include:
-	//	- int
-	//	- bool
-	//	- string
-	Value interface{}
+// CheckerCollection provides additional information for a group of checkers.
+// Deprecated: use linter.CheckerCollection.
+type CheckerCollection = linter.CheckerCollection
 
-	// Usage gives an overview about what parameter does.
-	Usage string
-}
+// CheckerParam describes a single checker customizable parameter.
+// Deprecated: use linter.CheckerParam.
+type CheckerParam = linter.CheckerParam
 
 // CheckerParams holds all checker-specific parameters.
 //
 // Provides convenient access to the loosely typed underlying map.
-type CheckerParams map[string]*CheckerParam
-
-// Int lookups pname key in underlying map and type-asserts it to int.
-func (params CheckerParams) Int(pname string) int { return params[pname].Value.(int) }
-
-// Bool lookups pname key in underlying map and type-asserts it to bool.
-func (params CheckerParams) Bool(pname string) bool { return params[pname].Value.(bool) }
-
-// String lookups pname key in underlying map and type-asserts it to string.
-func (params CheckerParams) String(pname string) string { return params[pname].Value.(string) }
+// Deprecated: use linter.CheckerParams.
+type CheckerParams = linter.CheckerParams
 
 // CheckerInfo holds checker metadata and structured documentation.
-type CheckerInfo struct {
-	// Name is a checker name.
-	Name string
-
-	// Tags is a list of labels that can be used to enable or disable checker.
-	// Common tags are "experimental" and "performance".
-	Tags []string
-
-	// Params declares checker-specific parameters. Optional.
-	Params CheckerParams
-
-	// Summary is a short one sentence description.
-	// Should not end with a period.
-	Summary string
-
-	// Details extends summary with additional info. Optional.
-	Details string
-
-	// Before is a code snippet of code that will violate rule.
-	Before string
-
-	// After is a code snippet of fixed code that complies to the rule.
-	After string
-
-	// Note is an optional caution message or advice.
-	Note string
-
-	// EmbeddedRuleguard tells whether this checker is auto-generated
-	// from the embedded ruleguard rules.
-	EmbeddedRuleguard bool
-
-	// Collection establishes a checker-to-collection relationship.
-	Collection *CheckerCollection
-}
-
-// GetCheckersInfo returns a checkers info list for all registered checkers.
-// The slice is sorted by a checker name.
-//
-// Info objects can be used to instantiate checkers with NewChecker function.
-func GetCheckersInfo() []*CheckerInfo {
-	return getCheckersInfo()
-}
-
-// HasTag reports whether checker described by the info has specified tag.
-func (info *CheckerInfo) HasTag(tag string) bool {
-	for i := range info.Tags {
-		if info.Tags[i] == tag {
-			return true
-		}
-	}
-	return false
-}
+// Deprecated: use linter.CheckerInfo.
+type CheckerInfo = linter.CheckerInfo
 
 // Checker is an implementation of a check that is described by the associated info.
-type Checker struct {
-	// Info is an info object that was used to instantiate this checker.
-	Info *CheckerInfo
-
-	ctx CheckerContext
-
-	fileWalker FileWalker
-}
-
-// Check runs rule checker over file f.
-func (c *Checker) Check(f *ast.File) []Warning {
-	c.ctx.warnings = c.ctx.warnings[:0]
-	c.fileWalker.WalkFile(f)
-	return c.ctx.warnings
-}
+// Deprecated: use linter.Checker.
+type Checker = linter.Checker
 
 // QuickFix is our analysis.TextEdit; we're using it here to avoid
 // direct analysis package dependency for now.
-type QuickFix struct {
-	From        token.Pos
-	To          token.Pos
-	Replacement []byte
-}
+// Deprecated: use linter.QuickFix.
+type QuickFix = linter.QuickFix
 
 // Warning represents issue that is found by checker.
-type Warning struct {
-	Pos token.Pos
+// Deprecated: use linter.Warning.
+type Warning = linter.Warning
 
-	// Text is warning message without source location info.
-	Text string
+// Context is a readonly state shared among every checker.
+// Deprecated: use linter.Context.
+type Context = linter.Context
 
-	// Suggestion is a quick fix for a given problem.
-	// QuickFix is analysis.TextEdit and can be used to
-	// construct an analysis.SuggestedFix object.
-	//
-	// For convenience, there is Warning.HasQuickFix() method
-	// that reports whether Suggestion has something meaningful.
-	Suggestion QuickFix
-}
+// CheckerContext is checker-local context copy.
+// Fields that are not from Context itself are writeable.
+// Deprecated: use linter.CheckerContext.
+type CheckerContext = linter.CheckerContext
+
+// FileWalker is an interface every checker should implement.
+//
+// The WalkFile method is executed for every Go file inside the
+// package that is being checked.
+// Deprecated: use linter.FileWalker.
+type FileWalker = linter.FileWalker
 
-// HasQuickFix reports whether this warning has a suggested fix.
-func (warn Warning) HasQuickFix() bool {
-	return warn.Suggestion.Replacement != nil
+// Deprecated: use linter.ParseGoVersion.
+func ParseGoVersion(version string) (GoVersion, error) {
+	return linter.ParseGoVersion(version)
 }
 
 // NewChecker returns initialized checker identified by an info.
 // info must be non-nil.
 // Returns an error if info describes a checker that was not properly registered,
 // or if checker fails to initialize.
+// Deprecated: use linter.NewChecker.
 func NewChecker(ctx *Context, info *CheckerInfo) (*Checker, error) {
-	return newChecker(ctx, info)
-}
-
-// Context is a readonly state shared among every checker.
-type Context struct {
-	// TypesInfo carries parsed packages types information.
-	TypesInfo *types.Info
-
-	// SizesInfo carries alignment and type size information.
-	// Arch-dependent.
-	SizesInfo types.Sizes
-
-	// GoVersion is a target Go version.
-	GoVersion GoVersion
-
-	// FileSet is a file set that was used during the program loading.
-	FileSet *token.FileSet
-
-	// Pkg describes package that is being checked.
-	Pkg *types.Package
-
-	// Filename is a currently checked file name.
-	Filename string
-
-	// Require records what optional resources are required
-	// by the checkers set that use this context.
-	//
-	// Every require fields makes associated context field
-	// to be properly initialized.
-	// For example, Context.require.PkgObjects => Context.PkgObjects.
-	Require struct {
-		PkgObjects bool
-		PkgRenames bool
-	}
-
-	// PkgObjects stores all imported packages and their local names.
-	PkgObjects map[*types.PkgName]string
-
-	// PkgRenames maps package path to its local renaming.
-	// Contains no entries for packages that were imported without
-	// explicit local names.
-	PkgRenames map[string]string
+	return linter.NewChecker(ctx, info)
 }
 
 // NewContext returns new shared context to be used by every checker.
 //
 // All data carried by the context is readonly for checkers,
 // but can be modified by the integrating application.
+// Deprecated: use linter.NewContext.
 func NewContext(fset *token.FileSet, sizes types.Sizes) *Context {
-	return &Context{
-		FileSet:   fset,
-		SizesInfo: sizes,
-		TypesInfo: &types.Info{},
-	}
-}
-
-// SetGoVersion adjust the target Go language version.
-//
-// The format is like "1.5", "1.8", etc.
-// It's permitted to have "go" prefix (e.g. "go1.5").
-//
-// Empty string (the default) means that we make no
-// Go version assumptions and (like gocritic does) behave
-// like all features are available. To make gocritic
-// more conservative, the upper Go version level should be adjusted.
-func (c *Context) SetGoVersion(version string) {
-	v, err := ParseGoVersion(version)
-	if err != nil {
-		panic(err)
-	}
-	c.GoVersion = v
-}
-
-// SetPackageInfo sets package-related metadata.
-//
-// Must be called for every package being checked.
-func (c *Context) SetPackageInfo(info *types.Info, pkg *types.Package) {
-	if info != nil {
-		// We do this kind of assignment to avoid
-		// changing c.typesInfo field address after
-		// every re-assignment.
-		*c.TypesInfo = *info
-	}
-	c.Pkg = pkg
-}
-
-// SetFileInfo sets file-related metadata.
-//
-// Must be called for every source code file being checked.
-func (c *Context) SetFileInfo(name string, f *ast.File) {
-	c.Filename = name
-	if c.Require.PkgObjects {
-		resolvePkgObjects(c, f)
-	}
-	if c.Require.PkgRenames {
-		resolvePkgRenames(c, f)
-	}
-}
-
-// CheckerContext is checker-local context copy.
-// Fields that are not from Context itself are writeable.
-type CheckerContext struct {
-	*Context
-
-	// printer used to format warning text.
-	printer *astfmt.Printer
-
-	warnings []Warning
+	return linter.NewContext(fset, sizes)
 }
 
-// Warn adds a Warning to checker output.
-func (ctx *CheckerContext) Warn(node ast.Node, format string, args ...interface{}) {
-	ctx.WarnWithPos(node.Pos(), format, args...)
-}
-
-// WarnFixable emits a warning with a fix suggestion provided by the caller.
-func (ctx *CheckerContext) WarnFixable(node ast.Node, fix QuickFix, format string, args ...interface{}) {
-	ctx.WarnFixableWithPos(node.Pos(), fix, format, args...)
-}
-
-// WarnWithPos adds a Warning to checker output. Useful for ruleguard's Report func.
-func (ctx *CheckerContext) WarnWithPos(pos token.Pos, format string, args ...interface{}) {
-	ctx.warnings = append(ctx.warnings, Warning{
-		Text: ctx.printer.Sprintf(format, args...),
-		Pos:  pos,
-	})
-}
-
-// WarnFixableWithPos adds a Warning to checker output. Useful for ruleguard's Report func.
-func (ctx *CheckerContext) WarnFixableWithPos(pos token.Pos, fix QuickFix, format string, args ...interface{}) {
-	ctx.warnings = append(ctx.warnings, Warning{
-		Text:       ctx.printer.Sprintf(format, args...),
-		Pos:        pos,
-		Suggestion: fix,
-	})
-}
-
-// UnknownType is a special sentinel value that is returned from the CheckerContext.TypeOf
-// method instead of the nil type.
-var UnknownType types.Type = types.Typ[types.Invalid]
-
-// TypeOf returns the type of expression x.
-//
-// Unlike TypesInfo.TypeOf, it never returns nil.
-// Instead, it returns the Invalid type as a sentinel UnknownType value.
-func (ctx *CheckerContext) TypeOf(x ast.Expr) types.Type {
-	typ := ctx.TypesInfo.TypeOf(x)
-	if typ != nil {
-		return typ
-	}
-	// Usually it means that some incorrect type info was loaded
-	// or the analyzed package was only partially (?) correct.
-	// To avoid nil pointer panics we can return a sentinel value
-	// that will fail most type assertions as well as kind checks
-	// (if the call side expects a *types.Basic).
-	return UnknownType
-}
-
-// SizeOf returns the size of the typ in bytes.
-//
-// Unlike SizesInfo.SizeOf, it will not panic on generic types.
-func (ctx *CheckerContext) SizeOf(typ types.Type) (int64, bool) {
-	if _, ok := typ.(*typeparams.TypeParam); ok {
-		return 0, false
-	}
-	return ctx.SizesInfo.Sizeof(typ), true
-}
-
-// FileWalker is an interface every checker should implement.
+// GetCheckersInfo returns a checkers info list for all registered checkers.
+// The slice is sorted by a checker name.
 //
-// The WalkFile method is executed for every Go file inside the
-// package that is being checked.
-type FileWalker interface {
-	WalkFile(*ast.File)
+// Info objects can be used to instantiate checkers with NewChecker function.
+// Deprecated: use linter.GetCheckersInfo.
+func GetCheckersInfo() []*CheckerInfo {
+	return linter.GetCheckersInfo()
 }
diff --git a/framework/lintmain/internal/hotload/hotload.go b/framework/lintmain/internal/hotload/hotload.go
deleted file mode 100644
index 5e5567a..0000000
--- a/framework/lintmain/internal/hotload/hotload.go
+++ /dev/null
@@ -1,30 +0,0 @@
-package hotload
-
-import (
-	"fmt"
-	"plugin"
-
-	"github.com/go-critic/go-critic/framework/linter"
-)
-
-// CheckersFromDylib loads checkers provided by a dynamic library found under path.
-//
-// The returned info slice must be re-assigned to the original info slice,
-// since there will be new entries there.
-func CheckersFromDylib(infoList []*linter.CheckerInfo, path string) ([]*linter.CheckerInfo, error) {
-	if path == "" {
-		return infoList, nil // Nothing to do
-	}
-	checkersBefore := len(infoList)
-	// Open plugin only for side-effects (init functions).
-	_, err := plugin.Open(path)
-	if err != nil {
-		return infoList, err
-	}
-	maybeUpdatedList := linter.GetCheckersInfo()
-	checkersAfter := len(maybeUpdatedList)
-	if checkersBefore == checkersAfter {
-		return infoList, fmt.Errorf("loaded plugin doesn't provide any gocritic-compatible checkers")
-	}
-	return maybeUpdatedList, nil
-}
diff --git a/framework/lintmain/lintmain.go b/framework/lintmain/lintmain.go
deleted file mode 100644
index 519a565..0000000
--- a/framework/lintmain/lintmain.go
+++ /dev/null
@@ -1,43 +0,0 @@
-package lintmain
-
-import (
-	"log"
-
-	"github.com/go-critic/go-critic/framework/lintmain/internal/check"
-	"github.com/go-critic/go-critic/framework/lintmain/internal/lintdoc"
-
-	"github.com/cristalhq/acmd"
-)
-
-// Config is used to parametrize the linter.
-type Config struct {
-	Version string
-	Name    string
-}
-
-// Run executes corresponding main after sub-command resolving.
-// Does not return.
-func Run(cfg Config) {
-	log.SetFlags(0)
-
-	r := acmd.RunnerOf(cmds, acmd.Config{
-		AppName: cfg.Name,
-		Version: cfg.Version,
-	})
-	if err := r.Run(); err != nil {
-		log.Print(err.Error())
-	}
-}
-
-var cmds = []acmd.Command{
-	{
-		Name:        "check",
-		Description: "run linter over specified targets",
-		ExecFunc:    check.Main,
-	},
-	{
-		Name:        "doc",
-		Description: "get installed checkers documentation",
-		ExecFunc:    lintdoc.Main,
-	},
-}
diff --git a/go.mod b/go.mod
index a306df5..60d776b 100644
--- a/go.mod
+++ b/go.mod
@@ -1,21 +1,28 @@
 module github.com/go-critic/go-critic
 
-go 1.16
+go 1.18
 
 require (
-	github.com/cristalhq/acmd v0.8.1
-	github.com/go-toolsmith/astcast v1.0.0
-	github.com/go-toolsmith/astcopy v1.0.2
-	github.com/go-toolsmith/astequal v1.0.3
-	github.com/go-toolsmith/astfmt v1.0.0
-	github.com/go-toolsmith/astp v1.0.0
-	github.com/go-toolsmith/pkgload v1.0.2-0.20220101231613-e814995d17c5
-	github.com/go-toolsmith/strparse v1.0.0
-	github.com/go-toolsmith/typep v1.0.2
+	github.com/cristalhq/acmd v0.11.1
+	github.com/go-toolsmith/astcast v1.1.0
+	github.com/go-toolsmith/astcopy v1.1.0
+	github.com/go-toolsmith/astequal v1.1.0
+	github.com/go-toolsmith/astfmt v1.1.0
+	github.com/go-toolsmith/astp v1.1.0
+	github.com/go-toolsmith/pkgload v1.2.1
+	github.com/go-toolsmith/strparse v1.1.0
+	github.com/go-toolsmith/typep v1.1.0
 	github.com/google/go-cmp v0.5.9
-	github.com/quasilyte/go-ruleguard v0.3.18
-	github.com/quasilyte/go-ruleguard/dsl v0.3.21
-	github.com/quasilyte/regex/syntax v0.0.0-20200407221936-30656e2c4a95
-	golang.org/x/exp/typeparams v0.0.0-20220827204233-334a2380cb91
-	golang.org/x/tools v0.1.12
+	github.com/quasilyte/go-ruleguard v0.3.19
+	github.com/quasilyte/go-ruleguard/dsl v0.3.22
+	github.com/quasilyte/regex/syntax v0.0.0-20210819130434-b3f0c404a727
+	golang.org/x/tools v0.6.0
+)
+
+require (
+	github.com/quasilyte/gogrep v0.5.0 // indirect
+	github.com/quasilyte/stdinfo v0.0.0-20220114132959-f7386bf02567 // indirect
+	golang.org/x/exp/typeparams v0.0.0-20230213192124-5e25df0256eb // indirect
+	golang.org/x/mod v0.8.0 // indirect
+	golang.org/x/sys v0.5.0 // indirect
 )
diff --git a/go.sum b/go.sum
index 4b29339..26b1680 100644
--- a/go.sum
+++ b/go.sum
@@ -1,93 +1,44 @@
-github.com/cristalhq/acmd v0.8.1 h1:mtFp/cbeJNY5jokF9zPz5mRllGHropRrOkOVxeGS6FI=
-github.com/cristalhq/acmd v0.8.1/go.mod h1:LG5oa43pE/BbxtfMoImHCQN++0Su7dzipdgBjMCBVDQ=
-github.com/go-toolsmith/astcast v1.0.0 h1:JojxlmI6STnFVG9yOImLeGREv8W2ocNUM+iOhR6jE7g=
-github.com/go-toolsmith/astcast v1.0.0/go.mod h1:mt2OdQTeAQcY4DQgPSArJjHCcOwlX+Wl/kwN+LbLGQ4=
-github.com/go-toolsmith/astcopy v1.0.2 h1:YnWf5Rnh1hUudj11kei53kI57quN/VH6Hp1n+erozn0=
-github.com/go-toolsmith/astcopy v1.0.2/go.mod h1:4TcEdbElGc9twQEYpVo/aieIXfHhiuLh4aLAck6dO7Y=
-github.com/go-toolsmith/astequal v1.0.0/go.mod h1:H+xSiq0+LtiDC11+h1G32h7Of5O3CYFJ99GVbS5lDKY=
-github.com/go-toolsmith/astequal v1.0.2/go.mod h1:9Ai4UglvtR+4up+bAD4+hCj7iTo4m/OXVTSLnCyTAx4=
-github.com/go-toolsmith/astequal v1.0.3 h1:+LVdyRatFS+XO78SGV4I3TCEA0AC7fKEGma+fH+674o=
+github.com/cristalhq/acmd v0.11.1 h1:DJ4fh2Pv0nPKmqT646IU/0Vh5FNdGblxvF+3/W3NAUI=
+github.com/cristalhq/acmd v0.11.1/go.mod h1:LG5oa43pE/BbxtfMoImHCQN++0Su7dzipdgBjMCBVDQ=
+github.com/go-toolsmith/astcast v1.1.0 h1:+JN9xZV1A+Re+95pgnMgDboWNVnIMMQXwfBwLRPgSC8=
+github.com/go-toolsmith/astcast v1.1.0/go.mod h1:qdcuFWeGGS2xX5bLM/c3U9lewg7+Zu4mr+xPwZIB4ZU=
+github.com/go-toolsmith/astcopy v1.1.0 h1:YGwBN0WM+ekI/6SS6+52zLDEf8Yvp3n2seZITCUBt5s=
+github.com/go-toolsmith/astcopy v1.1.0/go.mod h1:hXM6gan18VA1T/daUEHCFcYiW8Ai1tIwIzHY6srfEAw=
 github.com/go-toolsmith/astequal v1.0.3/go.mod h1:9Ai4UglvtR+4up+bAD4+hCj7iTo4m/OXVTSLnCyTAx4=
-github.com/go-toolsmith/astfmt v1.0.0 h1:A0vDDXt+vsvLEdbMFJAUBI/uTbRw1ffOPnxsILnFL6k=
-github.com/go-toolsmith/astfmt v1.0.0/go.mod h1:cnWmsOAuq4jJY6Ct5YWlVLmcmLMn1JUPuQIHCY7CJDw=
-github.com/go-toolsmith/astp v1.0.0 h1:alXE75TXgcmupDsMK1fRAy0YUzLzqPVvBKoyWV+KPXg=
-github.com/go-toolsmith/astp v1.0.0/go.mod h1:RSyrtpVlfTFGDYRbrjyWP1pYu//tSFcvdYrA8meBmLI=
-github.com/go-toolsmith/pkgload v1.0.2-0.20220101231613-e814995d17c5 h1:eD9POs68PHkwrx7hAB78z1cb6PfGq/jyWn3wJywsH1o=
-github.com/go-toolsmith/pkgload v1.0.2-0.20220101231613-e814995d17c5/go.mod h1:3NAwwmD4uY/yggRxoEjk/S00MIV3A+H7rrE3i87eYxM=
-github.com/go-toolsmith/strparse v1.0.0 h1:Vcw78DnpCAKlM20kSbAyO4mPfJn/lyYA4BJUDxe2Jb4=
+github.com/go-toolsmith/astequal v1.1.0 h1:kHKm1AWqClYn15R0K1KKE4RG614D46n+nqUQ06E1dTw=
+github.com/go-toolsmith/astequal v1.1.0/go.mod h1:sedf7VIdCL22LD8qIvv7Nn9MuWJruQA/ysswh64lffQ=
+github.com/go-toolsmith/astfmt v1.1.0 h1:iJVPDPp6/7AaeLJEruMsBUlOYCmvg0MoCfJprsOmcco=
+github.com/go-toolsmith/astfmt v1.1.0/go.mod h1:OrcLlRwu0CuiIBp/8b5PYF9ktGVZUjlNMV634mhwuQ4=
+github.com/go-toolsmith/astp v1.1.0 h1:dXPuCl6u2llURjdPLLDxJeZInAeZ0/eZwFJmqZMnpQA=
+github.com/go-toolsmith/astp v1.1.0/go.mod h1:0T1xFGz9hicKs8Z5MfAqSUitoUYS30pDMsRVIDHs8CA=
+github.com/go-toolsmith/pkgload v1.2.1 h1:Sq5hNDvejKVN+ICM+YA2Z8wpzvuJR96rDYo39qRJeyU=
+github.com/go-toolsmith/pkgload v1.2.1/go.mod h1:mZzAgRL5cWJqcFsK/C0P8WroZX5kvhwnsV28iCDIqHY=
 github.com/go-toolsmith/strparse v1.0.0/go.mod h1:YI2nUKP9YGZnL/L1/DLFBfixrcjslWct4wyljWhSRy8=
-github.com/go-toolsmith/typep v1.0.2 h1:8xdsa1+FSIH/RhEkgnD1j2CJOy5mNllW1Q9tRiYwvlk=
-github.com/go-toolsmith/typep v1.0.2/go.mod h1:JSQCQMUPdRlMZFswiq3TGpNp1GMktqkR2Ns5AIQkATU=
-github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
+github.com/go-toolsmith/strparse v1.1.0 h1:GAioeZUK9TGxnLS+qfdqNbA4z0SSm5zVNtCQiyP2Bvw=
+github.com/go-toolsmith/strparse v1.1.0/go.mod h1:7ksGy58fsaQkGQlY8WVoBFNyEPMGuJin1rfoPS4lBSQ=
+github.com/go-toolsmith/typep v1.1.0 h1:fIRYDyF+JywLfqzyhdiHzRop/GQDxxNhLGQ6gFUNHus=
+github.com/go-toolsmith/typep v1.1.0/go.mod h1:fVIw+7zjdsMxDA3ITWnH1yOiw1rnTQKCsF/sk2H/qig=
 github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
 github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
 github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
-github.com/quasilyte/go-ruleguard v0.3.1-0.20210203134552-1b5a410e1cc8/go.mod h1:KsAh3x0e7Fkpgs+Q9pNLS5XpFSvYCEVl5gP9Pp1xp30=
-github.com/quasilyte/go-ruleguard v0.3.18 h1:sd+abO1PEI9fkYennwzHn9kl3nqP6M5vE7FiOzZ+5CE=
-github.com/quasilyte/go-ruleguard v0.3.18/go.mod h1:lOIzcYlgxrQ2sGJ735EHXmf/e9MJ516j16K/Ifcttvs=
-github.com/quasilyte/go-ruleguard/dsl v0.3.0/go.mod h1:KeCP03KrjuSO0H1kTuZQCWlQPulDV6YMIXmpQss17rU=
-github.com/quasilyte/go-ruleguard/dsl v0.3.21 h1:vNkC6fC6qMLzCOGbnIHOd5ixUGgTbp3Z4fGnUgULlDA=
-github.com/quasilyte/go-ruleguard/dsl v0.3.21/go.mod h1:KeCP03KrjuSO0H1kTuZQCWlQPulDV6YMIXmpQss17rU=
-github.com/quasilyte/go-ruleguard/rules v0.0.0-20201231183845-9e62ed36efe1/go.mod h1:7JTjp89EGyU1d6XfBiXihJNG37wB2VRkd125Q1u7Plc=
-github.com/quasilyte/go-ruleguard/rules v0.0.0-20211022131956-028d6511ab71/go.mod h1:4cgAphtvu7Ftv7vOT2ZOYhC6CvBxZixcasr8qIOTA50=
-github.com/quasilyte/gogrep v0.0.0-20220828223005-86e4605de09f h1:6Gtn2i04RD0gVyYf2/IUMTIs+qYleBt4zxDqkLTcu4U=
-github.com/quasilyte/gogrep v0.0.0-20220828223005-86e4605de09f/go.mod h1:Cm9lpz9NZjEoL1tgZ2OgeUKPIxL1meE7eo60Z6Sk+Ng=
-github.com/quasilyte/regex/syntax v0.0.0-20200407221936-30656e2c4a95 h1:L8QM9bvf68pVdQ3bCFZMDmnt9yqcMBro1pC7F+IPYMY=
-github.com/quasilyte/regex/syntax v0.0.0-20200407221936-30656e2c4a95/go.mod h1:rlzQ04UMyJXu/aOvhd8qT+hvDrFpiwqp8MRXDY9szc0=
+github.com/quasilyte/go-ruleguard v0.3.19 h1:tfMnabXle/HzOb5Xe9CUZYWXKfkS1KwRmZyPmD9nVcc=
+github.com/quasilyte/go-ruleguard v0.3.19/go.mod h1:lHSn69Scl48I7Gt9cX3VrbsZYvYiBYszZOZW4A+oTEw=
+github.com/quasilyte/go-ruleguard/dsl v0.3.22 h1:wd8zkOhSNr+I+8Qeciml08ivDt1pSXe60+5DqOpCjPE=
+github.com/quasilyte/go-ruleguard/dsl v0.3.22/go.mod h1:KeCP03KrjuSO0H1kTuZQCWlQPulDV6YMIXmpQss17rU=
+github.com/quasilyte/gogrep v0.5.0 h1:eTKODPXbI8ffJMN+W2aE0+oL0z/nh8/5eNdiO34SOAo=
+github.com/quasilyte/gogrep v0.5.0/go.mod h1:Cm9lpz9NZjEoL1tgZ2OgeUKPIxL1meE7eo60Z6Sk+Ng=
+github.com/quasilyte/regex/syntax v0.0.0-20210819130434-b3f0c404a727 h1:TCg2WBOl980XxGFEZSS6KlBGIV0diGdySzxATTWoqaU=
+github.com/quasilyte/regex/syntax v0.0.0-20210819130434-b3f0c404a727/go.mod h1:rlzQ04UMyJXu/aOvhd8qT+hvDrFpiwqp8MRXDY9szc0=
 github.com/quasilyte/stdinfo v0.0.0-20220114132959-f7386bf02567 h1:M8mH9eK4OUR4lu7Gd+PU1fV2/qnDNfzT635KRSObncs=
 github.com/quasilyte/stdinfo v0.0.0-20220114132959-f7386bf02567/go.mod h1:DWNGW8A4Y+GyBgPuaQJuWiy0XYftx4Xm/y5Jqk9I6VQ=
-github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
-github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
-github.com/yuin/goldmark v1.4.1/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
-github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
-golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
-golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
-golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
-golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
 golang.org/x/exp/typeparams v0.0.0-20220428152302-39d4317da171/go.mod h1:AbB0pIl9nAr9wVwH+Z2ZpaocVmF5I4GyWCDIsVjR0bk=
-golang.org/x/exp/typeparams v0.0.0-20220827204233-334a2380cb91 h1:Ic/qN6TEifvObMGQy72k0n1LlJr7DjWWEi+MOsDOiSk=
-golang.org/x/exp/typeparams v0.0.0-20220827204233-334a2380cb91/go.mod h1:AbB0pIl9nAr9wVwH+Z2ZpaocVmF5I4GyWCDIsVjR0bk=
-golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
-golang.org/x/mod v0.5.1/go.mod h1:5OXOZSfqPIIbmVBIIKWRFfZjPR0E5r58TLhUjH0a2Ro=
-golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4 h1:6zppjxzCulZykYSLyVDYbneBfbaBIQPYMevg0bEwv2s=
-golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
-golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
-golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
-golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
-golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
-golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
-golang.org/x/net v0.0.0-20211015210444-4f30a5c0130f/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
-golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
-golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
-golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
-golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
-golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
-golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
-golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
-golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.0.0-20211019181941-9d821ace8654/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f h1:v4INt8xihDGvnrfjMDVXGxw9wrfxYyCjk0KbXjhR55s=
-golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
-golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
-golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
-golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
-golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
-golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
-golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
-golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
-golang.org/x/tools v0.0.0-20200812195022-5ae4c3c160a0/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
-golang.org/x/tools v0.0.0-20201230224404-63754364767c/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
-golang.org/x/tools v0.1.9-0.20211228192929-ee1ca4ffc4da/go.mod h1:nABZi5QlRsZVlzPpHl034qft6wpY4eDcsTt5AaioBiU=
-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/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
-golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
-golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
-golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+golang.org/x/exp/typeparams v0.0.0-20230203172020-98cc5a0785f9/go.mod h1:AbB0pIl9nAr9wVwH+Z2ZpaocVmF5I4GyWCDIsVjR0bk=
+golang.org/x/exp/typeparams v0.0.0-20230213192124-5e25df0256eb h1:WGs/bGIWYyAY5PVgGGMXqGGCxSJz4fpoUExb/vgqNCU=
+golang.org/x/exp/typeparams v0.0.0-20230213192124-5e25df0256eb/go.mod h1:AbB0pIl9nAr9wVwH+Z2ZpaocVmF5I4GyWCDIsVjR0bk=
+golang.org/x/mod v0.8.0 h1:LUYupSeNrTNCGzR/hVBk2NHZO4hXcVaW1k4Qx7rjPx8=
+golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
+golang.org/x/sync v0.1.0 h1:wsuoTGHzEhffawBOhz5CYhcrV4IdKZbEyZjBMuTp12o=
+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/tools v0.6.0 h1:BOw41kyTf3PuCW1pVQf8+Cyg8pMlkYB1oo9iJ6D/lKM=
+golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
diff --git a/framework/linter/go_version.go b/linter/go_version.go
similarity index 91%
rename from framework/linter/go_version.go
rename to linter/go_version.go
index d8091d4..b5ef2f7 100644
--- a/framework/linter/go_version.go
+++ b/linter/go_version.go
@@ -18,13 +18,14 @@ type GoVersion struct {
 //
 // As a special case, Major=0 covers all versions.
 func (v GoVersion) GreaterOrEqual(other GoVersion) bool {
-	if v.Major == 0 {
+	switch {
+	case v.Major == 0:
 		return true
-	}
-	if v.Major == other.Major {
+	case v.Major == other.Major:
 		return v.Minor >= other.Minor
+	default:
+		return v.Major >= other.Major
 	}
-	return v.Major >= other.Major
 }
 
 func ParseGoVersion(version string) (GoVersion, error) {
diff --git a/framework/linter/go_version_test.go b/linter/go_version_test.go
similarity index 100%
rename from framework/linter/go_version_test.go
rename to linter/go_version_test.go
diff --git a/framework/linter/checkers_db.go b/linter/helpers.go
similarity index 100%
rename from framework/linter/checkers_db.go
rename to linter/helpers.go
diff --git a/linter/linter.go b/linter/linter.go
new file mode 100644
index 0000000..12f0a1c
--- /dev/null
+++ b/linter/linter.go
@@ -0,0 +1,373 @@
+package linter
+
+import (
+	"go/ast"
+	"go/token"
+	"go/types"
+	"strconv"
+
+	"github.com/go-toolsmith/astfmt"
+)
+
+// UnknownType is a special sentinel value that is returned from the CheckerContext.TypeOf
+// method instead of the nil type.
+var UnknownType types.Type = types.Typ[types.Invalid]
+
+// FileWalker is an interface every checker should implement.
+//
+// The WalkFile method is executed for every Go file inside the
+// package that is being checked.
+type FileWalker interface {
+	WalkFile(*ast.File)
+}
+
+// CheckerCollection provides additional information for a group of checkers.
+type CheckerCollection struct {
+	// URL is a link for a main source of information on the collection.
+	URL string
+}
+
+// AddChecker registers a new checker into a checkers pool.
+// Constructor is used to create a new checker instance.
+// Checker name (defined in CheckerInfo.Name) must be unique.
+//
+// CheckerInfo.Collection is automatically set to the coll (the receiver).
+//
+// If checker is never needed, for example if it is disabled,
+// constructor will not be called.
+func (coll *CheckerCollection) AddChecker(info *CheckerInfo, constructor func(*CheckerContext) (FileWalker, error)) {
+	if coll == nil {
+		panic("adding checker to a nil collection")
+	}
+	info.Collection = coll
+	addChecker(info, constructor)
+}
+
+// CheckerParam describes a single checker customizable parameter.
+type CheckerParam struct {
+	// Value holds parameter bound value.
+	// It might be overwritten by the integrating linter.
+	//
+	// Permitted types include:
+	//	- int
+	//	- bool
+	//	- string
+	Value interface{}
+
+	// Usage gives an overview about what parameter does.
+	Usage string
+}
+
+// CheckerParams holds all checker-specific parameters.
+//
+// Provides convenient access to the loosely typed underlying map.
+type CheckerParams map[string]*CheckerParam
+
+// Int lookups pname key in underlying map and type-asserts it to int.
+func (params CheckerParams) Int(pname string) int { return params[pname].Value.(int) }
+
+// Bool lookups pname key in underlying map and type-asserts it to bool.
+func (params CheckerParams) Bool(pname string) bool { return params[pname].Value.(bool) }
+
+// String lookups pname key in underlying map and type-asserts it to string.
+func (params CheckerParams) String(pname string) string { return params[pname].Value.(string) }
+
+// CheckerInfo holds checker metadata and structured documentation.
+type CheckerInfo struct {
+	// Name is a checker name.
+	Name string
+
+	// Tags is a list of labels that can be used to enable or disable checker.
+	// Common tags are "experimental" and "performance".
+	Tags []string
+
+	// Params declares checker-specific parameters. Optional.
+	Params CheckerParams
+
+	// Summary is a short one sentence description.
+	// Should not end with a period.
+	Summary string
+
+	// Details extends summary with additional info. Optional.
+	Details string
+
+	// Before is a code snippet of code that will violate rule.
+	Before string
+
+	// After is a code snippet of fixed code that complies to the rule.
+	After string
+
+	// Note is an optional caution message or advice.
+	Note string
+
+	// EmbeddedRuleguard tells whether this checker is auto-generated
+	// from the embedded ruleguard rules.
+	EmbeddedRuleguard bool
+
+	// Collection establishes a checker-to-collection relationship.
+	Collection *CheckerCollection
+}
+
+// GetCheckersInfo returns a checkers info list for all registered checkers.
+// The slice is sorted by a checker name.
+//
+// Info objects can be used to instantiate checkers with NewChecker function.
+func GetCheckersInfo() []*CheckerInfo {
+	return getCheckersInfo()
+}
+
+// HasTag reports whether checker described by the info has specified tag.
+func (info *CheckerInfo) HasTag(tag string) bool {
+	for i := range info.Tags {
+		if info.Tags[i] == tag {
+			return true
+		}
+	}
+	return false
+}
+
+// Checker is an implementation of a check that is described by the associated info.
+type Checker struct {
+	// Info is an info object that was used to instantiate this checker.
+	Info *CheckerInfo
+
+	ctx CheckerContext
+
+	fileWalker FileWalker
+}
+
+// NewChecker returns initialized checker identified by an info.
+// info must be non-nil.
+// Returns an error if info describes a checker that was not properly registered,
+// or if checker fails to initialize.
+func NewChecker(ctx *Context, info *CheckerInfo) (*Checker, error) {
+	return newChecker(ctx, info)
+}
+
+// Check runs rule checker over file f.
+func (c *Checker) Check(f *ast.File) []Warning {
+	c.ctx.warnings = c.ctx.warnings[:0]
+	c.fileWalker.WalkFile(f)
+	return c.ctx.warnings
+}
+
+// QuickFix is our analysis.TextEdit; we're using it here to avoid
+// direct analysis package dependency for now.
+type QuickFix struct {
+	From        token.Pos
+	To          token.Pos
+	Replacement []byte
+}
+
+// Warning represents issue that is found by checker.
+type Warning struct {
+	Pos token.Pos
+
+	// Text is warning message without source location info.
+	Text string
+
+	// Suggestion is a quick fix for a given problem.
+	// QuickFix is analysis.TextEdit and can be used to
+	// construct an analysis.SuggestedFix object.
+	//
+	// For convenience, there is Warning.HasQuickFix() method
+	// that reports whether Suggestion has something meaningful.
+	Suggestion QuickFix
+}
+
+// HasQuickFix reports whether this warning has a suggested fix.
+func (warn Warning) HasQuickFix() bool {
+	return warn.Suggestion.Replacement != nil
+}
+
+// Context is a readonly state shared among every checker.
+type Context struct {
+	// TypesInfo carries parsed packages types information.
+	TypesInfo *types.Info
+
+	// SizesInfo carries alignment and type size information.
+	// Arch-dependent.
+	SizesInfo types.Sizes
+
+	// GoVersion is a target Go version.
+	GoVersion GoVersion
+
+	// FileSet is a file set that was used during the program loading.
+	FileSet *token.FileSet
+
+	// Pkg describes package that is being checked.
+	Pkg *types.Package
+
+	// Filename is a currently checked file name.
+	Filename string
+
+	// Require records what optional resources are required
+	// by the checkers set that use this context.
+	//
+	// Every require fields makes associated context field
+	// to be properly initialized.
+	// For example, Context.require.PkgObjects => Context.PkgObjects.
+	Require struct {
+		PkgObjects bool
+		PkgRenames bool
+	}
+
+	// PkgObjects stores all imported packages and their local names.
+	PkgObjects map[*types.PkgName]string
+
+	// PkgRenames maps package path to its local renaming.
+	// Contains no entries for packages that were imported without
+	// explicit local names.
+	PkgRenames map[string]string
+}
+
+// NewContext returns new shared context to be used by every checker.
+//
+// All data carried by the context is readonly for checkers,
+// but can be modified by the integrating application.
+func NewContext(fset *token.FileSet, sizes types.Sizes) *Context {
+	return &Context{
+		FileSet:   fset,
+		SizesInfo: sizes,
+		TypesInfo: &types.Info{},
+	}
+}
+
+// SetGoVersion adjust the target Go language version.
+//
+// The format is like "1.5", "1.8", etc.
+// It's permitted to have "go" prefix (e.g. "go1.5").
+//
+// Empty string (the default) means that we make no
+// Go version assumptions and (like gocritic does) behave
+// like all features are available. To make gocritic
+// more conservative, the upper Go version level should be adjusted.
+func (c *Context) SetGoVersion(version string) {
+	v, err := ParseGoVersion(version)
+	if err != nil {
+		panic(err)
+	}
+	c.GoVersion = v
+}
+
+// SetPackageInfo sets package-related metadata.
+//
+// Must be called for every package being checked.
+func (c *Context) SetPackageInfo(info *types.Info, pkg *types.Package) {
+	if info != nil {
+		// We do this kind of assignment to avoid
+		// changing c.typesInfo field address after
+		// every re-assignment.
+		*c.TypesInfo = *info
+	}
+	c.Pkg = pkg
+}
+
+// SetFileInfo sets file-related metadata.
+//
+// Must be called for every source code file being checked.
+func (c *Context) SetFileInfo(name string, f *ast.File) {
+	c.Filename = name
+	if c.Require.PkgObjects {
+		resolvePkgObjects(c, f)
+	}
+	if c.Require.PkgRenames {
+		resolvePkgRenames(c, f)
+	}
+}
+
+// CheckerContext is checker-local context copy.
+// Fields that are not from Context itself are writeable.
+type CheckerContext struct {
+	*Context
+
+	// printer used to format warning text.
+	printer *astfmt.Printer
+
+	warnings []Warning
+}
+
+// Warn adds a Warning to checker output.
+func (ctx *CheckerContext) Warn(node ast.Node, format string, args ...interface{}) {
+	ctx.WarnWithPos(node.Pos(), format, args...)
+}
+
+// WarnFixable emits a warning with a fix suggestion provided by the caller.
+func (ctx *CheckerContext) WarnFixable(node ast.Node, fix QuickFix, format string, args ...interface{}) {
+	ctx.WarnFixableWithPos(node.Pos(), fix, format, args...)
+}
+
+// WarnWithPos adds a Warning to checker output. Useful for ruleguard's Report func.
+func (ctx *CheckerContext) WarnWithPos(pos token.Pos, format string, args ...interface{}) {
+	ctx.warnings = append(ctx.warnings, Warning{
+		Text: ctx.printer.Sprintf(format, args...),
+		Pos:  pos,
+	})
+}
+
+// WarnFixableWithPos adds a Warning to checker output. Useful for ruleguard's Report func.
+func (ctx *CheckerContext) WarnFixableWithPos(pos token.Pos, fix QuickFix, format string, args ...interface{}) {
+	ctx.warnings = append(ctx.warnings, Warning{
+		Text:       ctx.printer.Sprintf(format, args...),
+		Pos:        pos,
+		Suggestion: fix,
+	})
+}
+
+// TypeOf returns the type of expression x.
+//
+// Unlike TypesInfo.TypeOf, it never returns nil.
+// Instead, it returns the Invalid type as a sentinel UnknownType value.
+func (ctx *CheckerContext) TypeOf(x ast.Expr) types.Type {
+	typ := ctx.TypesInfo.TypeOf(x)
+	if typ != nil {
+		return typ
+	}
+	// Usually it means that some incorrect type info was loaded
+	// or the analyzed package was only partially (?) correct.
+	// To avoid nil pointer panics we can return a sentinel value
+	// that will fail most type assertions as well as kind checks
+	// (if the call side expects a *types.Basic).
+	return UnknownType
+}
+
+// SizeOf returns the size of the typ in bytes.
+//
+// Unlike SizesInfo.SizeOf, it will not panic on generic types.
+func (ctx *CheckerContext) SizeOf(typ types.Type) (int64, bool) {
+	if _, ok := typ.(*types.TypeParam); ok {
+		return 0, false
+	}
+	if named, ok := typ.(*types.Named); ok && named.TypeParams() != nil {
+		return 0, false
+	}
+	return ctx.SizesInfo.Sizeof(typ), true
+}
+
+func resolvePkgObjects(ctx *Context, f *ast.File) {
+	ctx.PkgObjects = make(map[*types.PkgName]string, len(f.Imports))
+
+	for _, spec := range f.Imports {
+		if spec.Name != nil {
+			obj := ctx.TypesInfo.ObjectOf(spec.Name)
+			ctx.PkgObjects[obj.(*types.PkgName)] = spec.Name.Name
+		} else {
+			obj := ctx.TypesInfo.Implicits[spec]
+			ctx.PkgObjects[obj.(*types.PkgName)] = obj.Name()
+		}
+	}
+}
+
+func resolvePkgRenames(ctx *Context, f *ast.File) {
+	ctx.PkgRenames = make(map[string]string)
+
+	for _, spec := range f.Imports {
+		if spec.Name != nil {
+			path, err := strconv.Unquote(spec.Path.Value)
+			if err != nil {
+				panic(err)
+			}
+			ctx.PkgRenames[path] = spec.Name.Name
+		}
+	}
+}
diff --git a/tools/tools.go b/tools/tools.go
index e49081a..a68db79 100644
--- a/tools/tools.go
+++ b/tools/tools.go
@@ -1,3 +1,4 @@
+//go:build tools
 // +build tools
 
 package critic


Test patch:
diff --git a/checkers/checkers_test.go b/checkers/checkers_test.go
index 072d9fb..ead511f 100644
--- a/checkers/checkers_test.go
+++ b/checkers/checkers_test.go
@@ -7,8 +7,9 @@ import (
 	"strings"
 	"testing"
 
-	"github.com/go-critic/go-critic/framework/linter"
-	"github.com/go-critic/go-critic/framework/linttest"
+	"github.com/go-critic/go-critic/checkers/internal/linttest"
+	"github.com/go-critic/go-critic/linter"
+
 	"github.com/google/go-cmp/cmp"
 )
 
@@ -160,6 +161,8 @@ func TestStableList(t *testing.T) {
 }
 
 func TestExternal(t *testing.T) {
+	t.Skip("temporary disabled during bump to Go 1.20")
+
 	// Don't run these tests normally, unless asked to.
 	// Note that CI tests do enable GOCRITIC_EXTERNAL_TESTS.
 	if os.Getenv("GOCRITIC_EXTERNAL_TESTS") == "" {
diff --git a/framework/linttest/end2end.go b/checkers/internal/linttest/end2end.go
similarity index 100%
rename from framework/linttest/end2end.go
rename to checkers/internal/linttest/end2end.go
diff --git a/framework/linttest/integration.go b/checkers/internal/linttest/integration.go
similarity index 100%
rename from framework/linttest/integration.go
rename to checkers/internal/linttest/integration.go
diff --git a/framework/linttest/linttest.go b/checkers/internal/linttest/linttest.go
similarity index 88%
rename from framework/linttest/linttest.go
rename to checkers/internal/linttest/linttest.go
index 37d8e8e..4670d7d 100644
--- a/framework/linttest/linttest.go
+++ b/checkers/internal/linttest/linttest.go
@@ -11,7 +11,8 @@ import (
 	"strings"
 	"testing"
 
-	"github.com/go-critic/go-critic/framework/linter"
+	"github.com/go-critic/go-critic/linter"
+
 	"github.com/go-toolsmith/pkgload"
 	"golang.org/x/tools/go/packages"
 )
@@ -217,32 +218,9 @@ func newPackages(t *testing.T, pattern string, fset *token.FileSet) []*packages.
 		Tests: true,
 		Fset:  fset,
 	}
-	pkgs, err := loadPackages(&cfg, []string{pattern})
+	pkgs, err := pkgload.LoadPackages(&cfg, []string{pattern})
 	if err != nil {
 		t.Fatalf("load package: %v", err)
 	}
 	return pkgs
 }
-
-// TODO(quasilyte): copied from check.go. Should it be added to pkgload?
-func loadPackages(cfg *packages.Config, patterns []string) ([]*packages.Package, error) {
-	pkgs, err := packages.Load(cfg, patterns...)
-	if err != nil {
-		return nil, err
-	}
-
-	result := pkgs[:0]
-	pkgload.VisitUnits(pkgs, func(u *pkgload.Unit) {
-		if u.ExternalTest != nil {
-			result = append(result, u.ExternalTest)
-		}
-
-		if u.Test != nil {
-			// Prefer tests to the base package, if present.
-			result = append(result, u.Test)
-		} else if u.Base != nil {
-			result = append(result, u.Base)
-		}
-	})
-	return result, nil
-}
diff --git a/checkers/testdata/badCond/positive_tests.go b/checkers/testdata/badCond/positive_tests.go
index b55493c..aa93ae7 100644
--- a/checkers/testdata/badCond/positive_tests.go
+++ b/checkers/testdata/badCond/positive_tests.go
@@ -7,6 +7,11 @@ func bad1(retVal []int, start int) {
 	for i := 0; i > start; i++ {
 		retVal[i] = 0
 	}
+
+	/*! `start < i` in loop; probably meant `start > i`? */
+	for i := 0; start < i; i++ {
+		retVal[i] = 0
+	}
 }
 
 func bad2(x int) {
diff --git a/checkers/testdata/commentFormatting/negative_tests.go b/checkers/testdata/commentFormatting/negative_tests.go
index 00d27b1..45cbc6e 100644
--- a/checkers/testdata/commentFormatting/negative_tests.go
+++ b/checkers/testdata/commentFormatting/negative_tests.go
@@ -32,8 +32,8 @@ are ignored
 func myfunc() {
 }
 
+//go:noinline
 func example() {
-	//go:noinline
 }
 
 //go-sumtype:decl Data
diff --git a/checkers/testdata/hugeParam/negative_tests_go118.go b/checkers/testdata/hugeParam/negative_tests_go118.go
index bbf391c..9114a0f 100644
--- a/checkers/testdata/hugeParam/negative_tests_go118.go
+++ b/checkers/testdata/hugeParam/negative_tests_go118.go
@@ -4,3 +4,9 @@
 package checker_test
 
 func genericFunc[T comparable](x T) {}
+
+type Maybe[V any] struct {
+	value V
+}
+
+func (m Maybe[V]) Fn() {}
diff --git a/checkers/testdata/importShadow/negative_tests.go b/checkers/testdata/importShadow/negative_tests.go
index b7dc827..c2d1c34 100644
--- a/checkers/testdata/importShadow/negative_tests.go
+++ b/checkers/testdata/importShadow/negative_tests.go
@@ -4,9 +4,9 @@ import (
 	"fmt"
 	"math"
 
-	_ "github.com/go-toolsmith/astfmt" // To reproduce #665
+	"github.com/go-critic/go-critic/linter"
 
-	"github.com/go-critic/go-critic/framework/linter"
+	_ "github.com/go-toolsmith/astfmt" // To reproduce #665
 )
 
 func noWarnings() {
diff --git a/checkers/testdata/importShadow/positive_tests.go b/checkers/testdata/importShadow/positive_tests.go
index 94d9e65..b302ee9 100644
--- a/checkers/testdata/importShadow/positive_tests.go
+++ b/checkers/testdata/importShadow/positive_tests.go
@@ -7,7 +7,7 @@ import (
 	mymath1 "math"
 	mymath2 "math"
 
-	"github.com/go-critic/go-critic/framework/linter"
+	"github.com/go-critic/go-critic/linter"
 )
 
 var _ = mymath1.E
@@ -18,7 +18,7 @@ func shadowImportedPackages() {
 
 	/*! shadow of imported package 'math' */
 	math := "some math"
-	/*! shadow of imported from 'github.com/go-critic/go-critic/framework/linter' package 'linter' */
+	/*! shadow of imported from 'github.com/go-critic/go-critic/linter' package 'linter' */
 	linter := "some lint"
 
 	fmt.Printf("Hello math=%v, lint=%v", math, linter)
@@ -30,7 +30,7 @@ func genDeclShadow() {
 	var (
 		/*! shadow of imported package 'fmt' */
 		fmt = 2
-		/*! shadow of imported from 'github.com/go-critic/go-critic/framework/linter' package 'linter' */
+		/*! shadow of imported from 'github.com/go-critic/go-critic/linter' package 'linter' */
 		linter = 3
 	)
 	_, _ = fmt, linter
diff --git a/checkers/testdata/paramTypeCombine/negative_tests.go b/checkers/testdata/paramTypeCombine/negative_tests.go
index 1e1fcbe..4f265ca 100644
--- a/checkers/testdata/paramTypeCombine/negative_tests.go
+++ b/checkers/testdata/paramTypeCombine/negative_tests.go
@@ -1,6 +1,6 @@
 package checker_test
 
-func goodExtern(a, b, c int)
+func goodExtern(a, b, c int) {}
 
 func good1(a, b int)                {}
 func good2(a, b int, c int32)       {}
diff --git a/checkers/testdata/paramTypeCombine/positive_tests.go b/checkers/testdata/paramTypeCombine/positive_tests.go
index 3492276..b2ab61b 100644
--- a/checkers/testdata/paramTypeCombine/positive_tests.go
+++ b/checkers/testdata/paramTypeCombine/positive_tests.go
@@ -1,7 +1,7 @@
 package checker_test
 
 /*! func(a int, b int, c int) could be replaced with func(a, b, c int) */
-func extern(a int, b int, c int)
+func extern(a int, b int, c int) {}
 
 /*! func(a int, b int, c int) could be replaced with func(a, b, c int) */
 func simple1(a int, b int, c int) {}
diff --git a/checkers/testdata/rangeValCopy/negative_tests_go118.go b/checkers/testdata/rangeValCopy/negative_tests_go118.go
index 658ed32..0bb3c50 100644
--- a/checkers/testdata/rangeValCopy/negative_tests_go118.go
+++ b/checkers/testdata/rangeValCopy/negative_tests_go118.go
@@ -9,3 +9,9 @@ func genericSlice[T any](original []T, f func(T)) {
 		f(v)
 	}
 }
+
+type Maybe[V any] struct {
+	value V
+}
+
+func (m Maybe[V]) Fn() {}
diff --git a/checkers/testdata/wrapperFunc/negative_tests.go b/checkers/testdata/wrapperFunc/negative_tests.go
index 6fee0db..3dc3789 100644
--- a/checkers/testdata/wrapperFunc/negative_tests.go
+++ b/checkers/testdata/wrapperFunc/negative_tests.go
@@ -42,12 +42,20 @@ func nonMatchingArgs(s string, b []byte, i draw.Image, r image.Rectangle, p imag
 
 	strings.Replace(s, "a", "b", 1)
 
+	strings.Index(s, ":")
+	strings.IndexAny(s, ":")
+	strings.IndexRune(s, ':')
+
 	bytes.Map(nil, b)
 	bytes.Map(nil, b)
 	bytes.Map(nil, b)
 
 	bytes.Replace(b, b, b, 1)
 
+	bytes.Index(b, []byte(":"))
+	bytes.IndexAny(b, ":")
+	bytes.IndexRune(b, ':')
+
 	_ = http.HandlerFunc(nil)
 
 	draw.DrawMask(i, r, i, p, i, image.Point{}, o)
diff --git a/checkers/testdata/wrapperFunc/positive_tests.go b/checkers/testdata/wrapperFunc/positive_tests.go
index 6d753f1..d349a45 100644
--- a/checkers/testdata/wrapperFunc/positive_tests.go
+++ b/checkers/testdata/wrapperFunc/positive_tests.go
@@ -28,9 +28,21 @@ func f(s string, b []byte, i draw.Image, r image.Rectangle, p image.Point, o dra
 	/*! use strings.ReplaceAll method in `strings.Replace(s, "a", "b", -1)` */
 	strings.Replace(s, "a", "b", -1)
 
+	/*! suggestion: strings.Contains(s, ":") */
+	_ = strings.Index(s, ":") >= 0
+	/*! suggestion: strings.Contains(s, ":") */
+	_ = strings.Index(s, ":") != -1
+	/*! suggestion: strings.ContainsAny(s, ":") */
+	_ = strings.IndexAny(s, ":") >= 0
+	/*! suggestion: strings.ContainsAny(s, ":") */
+	_ = strings.IndexAny(s, ":") != -1
+	/*! suggestion: strings.ContainsRune(s, ':') */
+	_ = strings.IndexRune(s, ':') >= 0
+	/*! suggestion: strings.ContainsRune(s, ':') */
+	_ = strings.IndexRune(s, ':') != -1
+
 	/*! use bytes.Split method in `bytes.SplitN(b, []byte("."), -1)` */
 	bytes.SplitN(b, []byte("."), -1)
-
 	/*! use bytes.ToUpper method in `bytes.Map(unicode.ToUpper, b)` */
 	bytes.Map(unicode.ToUpper, b)
 	/*! use bytes.ToLower method in `bytes.Map(unicode.ToLower, b)` */
@@ -41,6 +53,19 @@ func f(s string, b []byte, i draw.Image, r image.Rectangle, p image.Point, o dra
 	/*! use bytes.ReplaceAll method in `bytes.Replace(b, b, b, -1)` */
 	bytes.Replace(b, b, b, -1)
 
+	/*! suggestion: bytes.Contains(b, []byte(":")) */
+	_ = bytes.Index(b, []byte(":")) >= 0
+	/*! suggestion: bytes.Contains(b, []byte(":")) */
+	_ = bytes.Index(b, []byte(":")) != -1
+	/*! suggestion: bytes.ContainsAny(b, ":") */
+	_ = bytes.IndexAny(b, ":") >= 0
+	/*! suggestion: bytes.ContainsAny(b, ":") */
+	_ = bytes.IndexAny(b, ":") != -1
+	/*! suggestion: bytes.ContainsRune(b, ':') */
+	_ = bytes.IndexRune(b, ':') >= 0
+	/*! suggestion: bytes.ContainsRune(b, ':') */
+	_ = bytes.IndexRune(b, ':') != -1
+
 	/*! use http.NotFoundHandler method in `http.HandlerFunc(http.NotFound)` */
 	_ = http.HandlerFunc(http.NotFound)
 
diff --git a/framework/lintmain/internal/check/check_test.go b/cmd/gocritic/check_test.go
similarity index 98%
rename from framework/lintmain/internal/check/check_test.go
rename to cmd/gocritic/check_test.go
index e6707b8..d1e4428 100644
--- a/framework/lintmain/internal/check/check_test.go
+++ b/cmd/gocritic/check_test.go
@@ -1,4 +1,4 @@
-package check
+package main
 
 import (
 	"testing"