From 0b4b007603a00a806d3a69faa810e8307c85991d Mon Sep 17 00:00:00 2001
From: "R. Aidan Campbell" <aidan.campbell@gmail.com>
Date: Sat, 17 Aug 2024 13:18:34 -0700
Subject: [PATCH] add scancheck linter

---
 .golangci.next.reference.yml                  |   2 +
 go.mod                                        |   1 +
 go.sum                                        |   2 +
 jsonschema/golangci.next.jsonschema.json      |   1 +
 pkg/golinters/scancheck/scancheck.go          |  18 ++
 pkg/golinters/scancheck/testdata/scancheck.go | 179 ++++++++++++++++++
 pkg/lint/lintersdb/builder_linter.go          |   6 +
 7 files changed, 209 insertions(+)
 create mode 100644 pkg/golinters/scancheck/scancheck.go
 create mode 100644 pkg/golinters/scancheck/testdata/scancheck.go

diff --git a/.golangci.next.reference.yml b/.golangci.next.reference.yml
index 2d4c9e10e009..0ff9ba331c2c 100644
--- a/.golangci.next.reference.yml
+++ b/.golangci.next.reference.yml
@@ -2674,6 +2674,7 @@ linters:
     - reassign
     - revive
     - rowserrcheck
+    - scancheck
     - sloglint
     - spancheck
     - sqlclosecheck
@@ -2789,6 +2790,7 @@ linters:
     - reassign
     - revive
     - rowserrcheck
+    - scancheck
     - sloglint
     - spancheck
     - sqlclosecheck
diff --git a/go.mod b/go.mod
index ecb2b9612bc8..45824ff16f01 100644
--- a/go.mod
+++ b/go.mod
@@ -86,6 +86,7 @@ require (
 	github.com/pelletier/go-toml/v2 v2.2.2
 	github.com/polyfloyd/go-errorlint v1.6.0
 	github.com/quasilyte/go-ruleguard/dsl v0.3.22
+	github.com/raidancampbell/scancheck v1.0.2
 	github.com/ryancurrah/gomodguard v1.3.3
 	github.com/ryanrolds/sqlclosecheck v0.5.1
 	github.com/sanposhiho/wastedassign/v2 v2.0.7
diff --git a/go.sum b/go.sum
index 73a0cc77faab..7cce242b492e 100644
--- a/go.sum
+++ b/go.sum
@@ -458,6 +458,8 @@ github.com/quasilyte/regex/syntax v0.0.0-20210819130434-b3f0c404a727 h1:TCg2WBOl
 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/raidancampbell/scancheck v1.0.2 h1:7ZZAB/ogNlmqLwNyV6UgJ/Hn+1BeuIWZ8m/WYGCI+hE=
+github.com/raidancampbell/scancheck v1.0.2/go.mod h1:tDBwTPKt6IvDRCCPAjh2zNRzVeh1zQm2tRMzVf8RAt4=
 github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
 github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8=
 github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4=
diff --git a/jsonschema/golangci.next.jsonschema.json b/jsonschema/golangci.next.jsonschema.json
index 510740d61660..f01ff07379a0 100644
--- a/jsonschema/golangci.next.jsonschema.json
+++ b/jsonschema/golangci.next.jsonschema.json
@@ -384,6 +384,7 @@
             "reassign",
             "revive",
             "rowserrcheck",
+            "scancheck",
             "scopelint",
             "sloglint",
             "sqlclosecheck",
diff --git a/pkg/golinters/scancheck/scancheck.go b/pkg/golinters/scancheck/scancheck.go
new file mode 100644
index 000000000000..847ed6e41e22
--- /dev/null
+++ b/pkg/golinters/scancheck/scancheck.go
@@ -0,0 +1,18 @@
+package scancheck
+
+import (
+	"github.com/golangci/golangci-lint/pkg/goanalysis"
+	"github.com/raidancampbell/scancheck/pkg/scancheck"
+	"golang.org/x/tools/go/analysis"
+)
+
+func New() *goanalysis.Linter {
+	a := scancheck.Analyzer
+
+	return goanalysis.NewLinter(
+		a.Name,
+		a.Doc,
+		[]*analysis.Analyzer{a},
+		nil,
+	).WithLoadMode(goanalysis.LoadModeTypesInfo)
+}
diff --git a/pkg/golinters/scancheck/testdata/scancheck.go b/pkg/golinters/scancheck/testdata/scancheck.go
new file mode 100644
index 000000000000..bdcfd24f84bf
--- /dev/null
+++ b/pkg/golinters/scancheck/testdata/scancheck.go
@@ -0,0 +1,179 @@
+//golangcitest:args -Escancheck
+package testdata
+
+import (
+	"bufio"
+	"fmt"
+	"io"
+)
+
+func incorrectErrorScanner(reader io.Reader) {
+	scanner := bufio.NewScanner(reader)
+
+	for scanner.Scan() {
+		_ = scanner.Bytes()
+
+		// this is incorrect behavior: if scanner.Scan() returns false, scanner.Err() should be checked.
+		// meaning that scanner.Err() should only be checked outside the loop.
+		if err := scanner.Err(); err != nil { // want "scanner.Err\\(\\) called inside a Scan\\(\\) loop"
+			fmt.Printf("oh no! %v", err)
+		}
+	}
+}
+
+func bufioRawScanner(reader io.Reader) {
+	var scanner = bufio.Scanner{}
+
+	for scanner.Scan() {
+		_ = scanner.Bytes()
+		if err := scanner.Err(); err != nil { // want "scanner.Err\\(\\) called inside a Scan\\(\\) loop"
+			fmt.Printf("oh no! %v", err)
+		}
+	}
+}
+
+func bufioRawNewScanner(reader io.Reader) {
+	scanner := new(bufio.Scanner)
+
+	for scanner.Scan() {
+		_ = scanner.Bytes()
+		if err := scanner.Err(); err != nil { // want "scanner.Err\\(\\) called inside a Scan\\(\\) loop"
+			fmt.Printf("oh no! %v", err)
+		}
+	}
+}
+
+func multipleAssignment(reader io.Reader) {
+	_, scanner := bufio.NewReader(reader), bufio.NewScanner(reader)
+
+	for scanner.Scan() {
+		_ = scanner.Bytes()
+		if err := scanner.Err(); err != nil { // want "scanner.Err\\(\\) called inside a Scan\\(\\) loop"
+			fmt.Printf("oh no! %v", err)
+		}
+	}
+}
+
+func unrelatedBufioScanner(reader io.Reader) {
+	x := func(_ bufio.Scanner) *notABufioScanner {
+		return newNotBufioScanner()
+	}
+	scanner := x(bufio.Scanner{})
+
+	for scanner.Scan() {
+		if err := scanner.Err(); err != nil {
+			fmt.Printf("oh no! %v", err)
+		}
+	}
+
+}
+
+func correctErrorScanner(reader io.Reader) {
+	scanner := bufio.NewScanner(reader)
+
+	for scanner.Scan() {
+		_ = scanner.Bytes()
+
+		// this is incorrect behavior: if scanner.Scan() returns false, scanner.Err() should be checked.
+		// meaning that scanner.Err() should only be checked outside the loop.
+	}
+
+	if err := scanner.Err(); err != nil {
+		fmt.Printf("oh no! %v", err)
+	}
+}
+
+func hasNoScanner() {}
+
+func scannerIsNotScanned(reader io.Reader) {
+	scanner := bufio.NewScanner(reader)
+	_ = scanner.Bytes()
+}
+
+func scannerScannedOutsideForLoop(reader io.Reader) {
+	scanner := bufio.NewScanner(reader)
+	_ = scanner.Scan()
+	if err := scanner.Err(); err != nil {
+		fmt.Printf("oh no! %v", err)
+	}
+	_ = scanner.Bytes()
+}
+
+func bufioNotScanner(reader io.Reader) {
+	r := bufio.NewReader(reader)
+
+	for _, err := r.ReadByte(); err != nil; {
+	}
+}
+
+func scannerNotBufio(reader io.Reader) {
+	sg := scannerGenerator{}
+	scanner := sg.NewScanner()
+
+	for scanner.Scan() {
+		if err := scanner.Err(); err != nil {
+			fmt.Printf("oh no! %v", err)
+		}
+	}
+}
+
+func scannerShadowingBufio(reader io.Reader) {
+	bufio := scannerGenerator{}
+	scanner := bufio.NewScanner()
+
+	for scanner.Scan() {
+		if err := scanner.Err(); err != nil {
+			fmt.Printf("oh no! %v", err)
+		}
+	}
+}
+
+func scannerAlmostShadowingBufio(reader io.Reader) {
+	bufio := scannerGenerator{}
+	scanner := bufio.NewScannerWithDifferentName()
+
+	for scanner.Scan() {
+		if err := scanner.Err(); err != nil {
+			fmt.Printf("oh no! %v", err)
+		}
+	}
+}
+
+func scanNotScanner(reader io.Reader) {
+	b := new(boolScanner)
+	for b.Scan() {
+		if err := newNotBufioScanner().Err(); err != nil {
+			fmt.Printf("oh no! %v", err)
+		}
+	}
+}
+
+func newNotBufioScanner() *notABufioScanner {
+	return new(notABufioScanner)
+}
+
+type notABufioScanner struct{}
+
+func (n notABufioScanner) Scan() bool {
+	return true
+}
+
+func (n notABufioScanner) Err() error {
+	return nil
+}
+
+type scannerGenerator struct{}
+
+func (s scannerGenerator) NewScanner() *notABufioScanner {
+	return newNotBufioScanner()
+}
+
+func (s scannerGenerator) NewScannerWithDifferentName() *notABufioScanner {
+	return newNotBufioScanner()
+}
+
+type boolScanner struct{}
+
+func (b boolScanner) Scan() bool {
+	return false
+}
diff --git a/pkg/lint/lintersdb/builder_linter.go b/pkg/lint/lintersdb/builder_linter.go
index 2e6c148e329c..1a257ec199d4 100644
--- a/pkg/lint/lintersdb/builder_linter.go
+++ b/pkg/lint/lintersdb/builder_linter.go
@@ -87,6 +87,7 @@ import (
 	"github.com/golangci/golangci-lint/pkg/golinters/reassign"
 	"github.com/golangci/golangci-lint/pkg/golinters/revive"
 	"github.com/golangci/golangci-lint/pkg/golinters/rowserrcheck"
+	"github.com/golangci/golangci-lint/pkg/golinters/scancheck"
 	"github.com/golangci/golangci-lint/pkg/golinters/sloglint"
 	"github.com/golangci/golangci-lint/pkg/golinters/spancheck"
 	"github.com/golangci/golangci-lint/pkg/golinters/sqlclosecheck"
@@ -668,6 +669,11 @@ func (LinterBuilder) Build(cfg *config.Config) ([]*linter.Config, error) {
 			WithPresets(linter.PresetBugs, linter.PresetSQL).
 			WithURL("https://github.com/jingyugao/rowserrcheck"),
 
+		linter.NewConfig(scancheck.New()).
+			WithSince("v1.61.0").
+			WithPresets(linter.PresetBugs, linter.PresetError).
+			WithURL("https://github.com/raidancampbell/scancheck"),
+
 		linter.NewConfig(sloglint.New(&cfg.LintersSettings.SlogLint)).
 			WithSince("v1.55.0").
 			WithLoadForGoAnalysis().