From de7feb5aefe2d2eeddea0884056c93c969523f4f Mon Sep 17 00:00:00 2001
From: Dominik Honnef
Date: Fri, 21 Feb 2020 13:40:35 +0100
Subject: [PATCH 001/111] doc: update 2020.1 release notes to include minor
versions
---
doc/2020.1.html | 14 ++++++++++++++
1 file changed, 14 insertions(+)
diff --git a/doc/2020.1.html b/doc/2020.1.html
index bc1279578..f3570e8d9 100644
--- a/doc/2020.1.html
+++ b/doc/2020.1.html
@@ -157,3 +157,17 @@
General bug fixes
Some files generated by goyacc weren't recognized as being generated.
staticcheck no longer fails to check packages that consist exclusively of tests.
+
+
+
Staticcheck 2020.1.1 release notes
+
+
+ The 2020.1 release neglected to update the version string stored in
+ the binary, causing staticcheck -version to incorrectly emit (no version).
+
+
+
Staticcheck 2020.1.2 release notes
+
+
+ The 2020.1.1 release incorrectly identified itself as version 2020.1.
+
From 5bf9fe98906dff156bdb9de7b4e393a7ef60c242 Mon Sep 17 00:00:00 2001
From: Dominik Honnef
Date: Fri, 21 Feb 2020 21:33:16 +0100
Subject: [PATCH 002/111] lint: don't store token.Pos in the cache
The token.Pos stored in the cache will not be correct once we load it
from the cache, because the fset will have changed. This would lead to
bogus position information for unhandled linter directives.
---
lint/lint.go | 4 ++--
lint/runner.go | 2 +-
2 files changed, 3 insertions(+), 3 deletions(-)
diff --git a/lint/lint.go b/lint/lint.go
index d1f095eb6..73fa322f9 100644
--- a/lint/lint.go
+++ b/lint/lint.go
@@ -64,7 +64,7 @@ type LineIgnore struct {
Line int
Checks []string
Matched bool
- Pos token.Pos
+ Pos token.Position
}
func (li *LineIgnore) Match(p Problem) bool {
@@ -374,7 +374,7 @@ func (l *Linter) Lint(cfg *packages.Config, patterns []string) ([]Problem, error
continue
}
p := Problem{
- Pos: DisplayPosition(pkg.Fset, ig.Pos),
+ Pos: ig.Pos,
Message: "this linter directive didn't match anything; should it be removed?",
Check: "",
}
diff --git a/lint/runner.go b/lint/runner.go
index 3235dce82..74106ced8 100644
--- a/lint/runner.go
+++ b/lint/runner.go
@@ -1031,7 +1031,7 @@ func parseDirectives(pkg *packages.Package) ([]Ignore, []Problem) {
File: pos.Filename,
Line: pos.Line,
Checks: checks,
- Pos: c.Pos(),
+ Pos: DisplayPosition(pkg.Fset, c.Pos()),
}
case "file-ignore":
ig = &FileIgnore{
From 37fcbfbb57c58f156decf06c733719a8f3005045 Mon Sep 17 00:00:00 2001
From: Dominik Honnef
Date: Fri, 21 Feb 2020 23:23:57 +0100
Subject: [PATCH 003/111] lint: deduplicate line ignores
Account for analyses (U1000 specifically) that don't emit problems for
both a package and its test variant.
---
lint/lint.go | 16 ++++++++++++++++
1 file changed, 16 insertions(+)
diff --git a/lint/lint.go b/lint/lint.go
index 73fa322f9..1a70e0c29 100644
--- a/lint/lint.go
+++ b/lint/lint.go
@@ -325,8 +325,23 @@ func (l *Linter) Lint(cfg *packages.Config, patterns []string) ([]Problem, error
}
var problems []Problem
+ // Deduplicate line ignores. When U1000 processes a package and
+ // its test variant, it will only emit a single problem for an
+ // unused object, not two problems. We will, however, have two
+ // line ignores, one per package. Without deduplication, one line
+ // ignore will be marked as matched, while the other one won't,
+ // subsequently reporting a "this linter directive didn't match
+ // anything" error.
+ ignores := map[token.Position]Ignore{}
for _, pkg := range pkgs {
for _, ig := range pkg.ignores {
+ if lig, ok := ig.(*LineIgnore); ok {
+ ig = ignores[lig.Pos]
+ if ig == nil {
+ ignores[lig.Pos] = lig
+ ig = lig
+ }
+ }
for i := range pkg.problems {
p := &pkg.problems[i]
if ig.Match(*p) {
@@ -354,6 +369,7 @@ func (l *Linter) Lint(cfg *packages.Config, patterns []string) ([]Problem, error
if !ok {
continue
}
+ ig = ignores[ig.Pos].(*LineIgnore)
if ig.Matched {
continue
}
From 709e8b461c1be7861a110889599bcdd09677a272 Mon Sep 17 00:00:00 2001
From: Dominik Honnef
Date: Sat, 22 Feb 2020 12:45:24 +0100
Subject: [PATCH 004/111] doc: add 2020.1.3 release notes
---
doc/2020.1.html | 22 ++++++++++++++++++++++
1 file changed, 22 insertions(+)
diff --git a/doc/2020.1.html b/doc/2020.1.html
index f3570e8d9..8c4ce6680 100644
--- a/doc/2020.1.html
+++ b/doc/2020.1.html
@@ -7,6 +7,9 @@
The 2020.1.1 release incorrectly identified itself as version 2020.1.
+
+
Staticcheck 2020.1.3 release notes
+
+
+ This release fixes two bugs involving //lint:ignore directives:
+
+
+
+ When ignoring U1000 and checking a package that contains tests,
+ Staticcheck would incorrectly complain that the linter directive
+ didn't match any problems, even when it did.
+
+
+
+ On repeated runs, the position information for a this linter directive didn't match anything report
+ would either be missing, or be wildly incorrect.
+
If you use Go modules, you can simply run go get honnef.co/go/tools/cmd/staticcheck to obtain the latest released version.
If you're still using a GOPATH-based workflow, then the above command will instead fetch the master branch.
- It is suggested that you explicitly check out the latest release branch instead, which is currently 2020.1.
+ It is suggested that you explicitly check out the latest release branch instead, which is currently 2020.1.3.
One way of doing so would be as follows:
cd $GOPATH/src/honnef.co/go/tools/cmd/staticcheck
-git checkout 2020.1
+git checkout 2020.1.3
go get
go install
+ This release adds special handling for imports of the
+ deprecated github.com/golang/protobuf/proto package.
+
+
+
+ github.com/golang/protobuf
+ has deprecated the proto package, but
+ their protoc-gen-go still imports the package and uses
+ one of its constants, to enforce a weak dependency on a
+ sufficiently new version of the legacy package.
+
+
+
+ Staticcheck would flag the import of this deprecated package in all
+ code generated by protoc-gen-go. Instead of forcing the project to
+ change their project structure, we choose to ignore such imports in
+ code generated by protoc-gen-go. The import still gets flagged in code
+ not generated by protoc-gen-go.
+
+
+
+ You can find more information about this in the upstream issue.
+
If you use Go modules, you can simply run go get honnef.co/go/tools/cmd/staticcheck to obtain the latest released version.
If you're still using a GOPATH-based workflow, then the above command will instead fetch the master branch.
- It is suggested that you explicitly check out the latest release branch instead, which is currently 2020.1.3.
+ It is suggested that you explicitly check out the latest release branch instead, which is currently 2020.1.4.
One way of doing so would be as follows:
cd $GOPATH/src/honnef.co/go/tools/cmd/staticcheck
-git checkout 2020.1.3
+git checkout 2020.1.4
go get
go install
From 73c30d736020abd7fc1f69ff9b08d2fdbee86550 Mon Sep 17 00:00:00 2001
From: Dominik Honnef
Date: Fri, 15 May 2020 07:07:09 +0200
Subject: [PATCH 049/111] S1029, SA6003: also check types with underlying type
string
Closes gh-743
---
internal/sharedcheck/lint.go | 2 +-
.../src/CheckRangeStringRunes/LintRangeStringRunes.go | 8 +++++++-
.../src/CheckRangeStringRunes/CheckRangeStringRunes.go | 8 +++++++-
3 files changed, 15 insertions(+), 3 deletions(-)
diff --git a/internal/sharedcheck/lint.go b/internal/sharedcheck/lint.go
index e9abf0d89..d1433c4f1 100644
--- a/internal/sharedcheck/lint.go
+++ b/internal/sharedcheck/lint.go
@@ -26,7 +26,7 @@ func CheckRangeStringRunes(pass *analysis.Pass) (interface{}, error) {
if val == nil {
return true
}
- Tsrc, ok := val.X.Type().(*types.Basic)
+ Tsrc, ok := val.X.Type().Underlying().(*types.Basic)
if !ok || Tsrc.Kind() != types.String {
return true
}
diff --git a/simple/testdata/src/CheckRangeStringRunes/LintRangeStringRunes.go b/simple/testdata/src/CheckRangeStringRunes/LintRangeStringRunes.go
index b6761a187..7facfc5c9 100644
--- a/simple/testdata/src/CheckRangeStringRunes/LintRangeStringRunes.go
+++ b/simple/testdata/src/CheckRangeStringRunes/LintRangeStringRunes.go
@@ -1,6 +1,8 @@
package pkg
-func fn(s string) {
+type String string
+
+func fn(s string, s2 String) {
for _, r := range s {
println(r)
}
@@ -24,4 +26,8 @@ func fn(s string) {
println(r)
}
println(y[0])
+
+ for _, r := range []rune(s2) { // want `should range over string`
+ println(r)
+ }
}
diff --git a/staticcheck/testdata/src/CheckRangeStringRunes/CheckRangeStringRunes.go b/staticcheck/testdata/src/CheckRangeStringRunes/CheckRangeStringRunes.go
index b6761a187..7facfc5c9 100644
--- a/staticcheck/testdata/src/CheckRangeStringRunes/CheckRangeStringRunes.go
+++ b/staticcheck/testdata/src/CheckRangeStringRunes/CheckRangeStringRunes.go
@@ -1,6 +1,8 @@
package pkg
-func fn(s string) {
+type String string
+
+func fn(s string, s2 String) {
for _, r := range s {
println(r)
}
@@ -24,4 +26,8 @@ func fn(s string) {
println(r)
}
println(y[0])
+
+ for _, r := range []rune(s2) { // want `should range over string`
+ println(r)
+ }
}
From 87cefbaa37b829d7c336e580e22527d97604c941 Mon Sep 17 00:00:00 2001
From: Dominik Honnef
Date: Sat, 16 May 2020 00:16:17 +0200
Subject: [PATCH 050/111] S1030: don't flag m[string(buf.Bytes()]
The compiler optimizes []byte to string conversions in map lookups,
making them much cheaper than the actual conversion. This optimization
does not, however, apply to buf.String().
---
code/code.go | 9 +++++++++
simple/doc.go | 8 ++++++--
simple/lint.go | 11 +++++++++--
.../LintBytesBufferConversions.go | 3 +++
.../LintBytesBufferConversions.go.golden | 3 +++
5 files changed, 30 insertions(+), 4 deletions(-)
diff --git a/code/code.go b/code/code.go
index 73aebea60..711b6d097 100644
--- a/code/code.go
+++ b/code/code.go
@@ -532,3 +532,12 @@ func IsGoVersion(pass *analysis.Pass, minor int) bool {
func Preorder(pass *analysis.Pass, fn func(ast.Node), types ...ast.Node) {
pass.ResultOf[inspect.Analyzer].(*inspector.Inspector).Preorder(types, fn)
}
+
+func PreorderStack(pass *analysis.Pass, fn func(ast.Node, []ast.Node), types ...ast.Node) {
+ pass.ResultOf[inspect.Analyzer].(*inspector.Inspector).WithStack(types, func(n ast.Node, push bool, stack []ast.Node) (proceed bool) {
+ if push {
+ fn(n, stack)
+ }
+ return true
+ })
+}
diff --git a/simple/doc.go b/simple/doc.go
index 27297bf61..de0edc5bf 100644
--- a/simple/doc.go
+++ b/simple/doc.go
@@ -368,9 +368,13 @@ After:
"S1030": {
Title: `Use bytes.Buffer.String or bytes.Buffer.Bytes`,
- Text: `bytes.Buffer has both a String and a Bytes method. It is never
+ Text: `bytes.Buffer has both a String and a Bytes method. It is almost never
necessary to use string(buf.Bytes()) or []byte(buf.String()) – simply
-use the other method.`,
+use the other method.
+
+The only exception to this are map lookups. Due to a compiler optimization,
+m[string(buf.Bytes())] is more efficient than m[buf.String()].
+`,
Since: "2017.1",
},
diff --git a/simple/lint.go b/simple/lint.go
index 5a9571deb..585c25c6e 100644
--- a/simple/lint.go
+++ b/simple/lint.go
@@ -160,7 +160,7 @@ func CheckBytesBufferConversions(pass *analysis.Pass) (interface{}, error) {
// The bytes package can use itself however it wants
return nil, nil
}
- fn := func(node ast.Node) {
+ fn := func(node ast.Node, stack []ast.Node) {
m, ok := Match(pass, checkBytesBufferConversionsQ, node)
if !ok {
return
@@ -170,6 +170,13 @@ func CheckBytesBufferConversions(pass *analysis.Pass) (interface{}, error) {
typ := pass.TypesInfo.TypeOf(call.Fun)
if typ == types.Universe.Lookup("string").Type() && code.IsCallToAST(pass, call.Args[0], "(*bytes.Buffer).Bytes") {
+ if _, ok := stack[len(stack)-2].(*ast.IndexExpr); ok {
+ // Don't flag m[string(buf.Bytes())] – thanks to a
+ // compiler optimization, this is actually faster than
+ // m[buf.String()]
+ return
+ }
+
report.Report(pass, call, fmt.Sprintf("should use %v.String() instead of %v", report.Render(pass, sel.X), report.Render(pass, call)),
report.FilterGenerated(),
report.Fixes(edit.Fix("simplify conversion", edit.ReplaceWithPattern(pass, checkBytesBufferConversionsRs, m.State, node))))
@@ -180,7 +187,7 @@ func CheckBytesBufferConversions(pass *analysis.Pass) (interface{}, error) {
}
}
- code.Preorder(pass, fn, (*ast.CallExpr)(nil))
+ code.PreorderStack(pass, fn, (*ast.CallExpr)(nil))
return nil, nil
}
diff --git a/simple/testdata/src/CheckBytesBufferConversions/LintBytesBufferConversions.go b/simple/testdata/src/CheckBytesBufferConversions/LintBytesBufferConversions.go
index de5d1b7da..c79e40b93 100644
--- a/simple/testdata/src/CheckBytesBufferConversions/LintBytesBufferConversions.go
+++ b/simple/testdata/src/CheckBytesBufferConversions/LintBytesBufferConversions.go
@@ -13,6 +13,9 @@ func fn() {
_ = string(m["key"].Bytes()) // want `should use m\["key"\]\.String\(\) instead of string\(m\["key"\]\.Bytes\(\)\)`
_ = []byte(m["key"].String()) // want `should use m\["key"\]\.Bytes\(\) instead of \[\]byte\(m\["key"\]\.String\(\)\)`
+ var m2 map[string]int
+ _ = m2[string(buf.Bytes())] // no warning, this is more efficient than buf.String()
+
string := func(_ interface{}) interface{} {
return nil
}
diff --git a/simple/testdata/src/CheckBytesBufferConversions/LintBytesBufferConversions.go.golden b/simple/testdata/src/CheckBytesBufferConversions/LintBytesBufferConversions.go.golden
index 7c2be7db5..6c29a4d12 100644
--- a/simple/testdata/src/CheckBytesBufferConversions/LintBytesBufferConversions.go.golden
+++ b/simple/testdata/src/CheckBytesBufferConversions/LintBytesBufferConversions.go.golden
@@ -13,6 +13,9 @@ func fn() {
_ = m["key"].String() // want `should use m\["key"\]\.String\(\) instead of string\(m\["key"\]\.Bytes\(\)\)`
_ = m["key"].Bytes() // want `should use m\["key"\]\.Bytes\(\) instead of \[\]byte\(m\["key"\]\.String\(\)\)`
+ var m2 map[string]int
+ _ = m2[string(buf.Bytes())] // no warning, this is more efficient than buf.String()
+
string := func(_ interface{}) interface{} {
return nil
}
From aa8d49aa598f91880415ab96667ee61beca81f25 Mon Sep 17 00:00:00 2001
From: Dominik Honnef
Date: Sat, 16 May 2020 01:41:29 +0200
Subject: [PATCH 051/111] simple: use PreorderStack where appropriate
---
simple/lint.go | 22 ++++++----------------
staticcheck/lint.go | 1 +
2 files changed, 7 insertions(+), 16 deletions(-)
diff --git a/simple/lint.go b/simple/lint.go
index 585c25c6e..9d1455c87 100644
--- a/simple/lint.go
+++ b/simple/lint.go
@@ -799,24 +799,13 @@ func CheckUnnecessaryBlank(pass *analysis.Pass) (interface{}, error) {
}
func CheckSimplerStructConversion(pass *analysis.Pass) (interface{}, error) {
- var skip ast.Node
- fn := func(node ast.Node) {
- // Do not suggest type conversion between pointers
- if unary, ok := node.(*ast.UnaryExpr); ok && unary.Op == token.AND {
- if lit, ok := unary.X.(*ast.CompositeLit); ok {
- skip = lit
- }
- return
- }
-
- if node == skip {
+ fn := func(node ast.Node, stack []ast.Node) {
+ if unary, ok := stack[len(stack)-2].(*ast.UnaryExpr); ok && unary.Op == token.AND {
+ // Do not suggest type conversion between pointers
return
}
- lit, ok := node.(*ast.CompositeLit)
- if !ok {
- return
- }
+ lit := node.(*ast.CompositeLit)
typ1, _ := pass.TypesInfo.TypeOf(lit.Type).(*types.Named)
if typ1 == nil {
return
@@ -924,7 +913,7 @@ func CheckSimplerStructConversion(pass *analysis.Pass) (interface{}, error) {
report.FilterGenerated(),
report.Fixes(edit.Fix("use type conversion", edit.ReplaceWithNode(pass.Fset, node, r))))
}
- code.Preorder(pass, fn, (*ast.UnaryExpr)(nil), (*ast.CompositeLit)(nil))
+ code.PreorderStack(pass, fn, (*ast.CompositeLit)(nil))
return nil, nil
}
@@ -1257,6 +1246,7 @@ func CheckAssertNotNil(pass *analysis.Pass) (interface{}, error) {
report.ShortRange(),
report.FilterGenerated())
}
+ // OPT(dh): merge fn1 and fn2
code.Preorder(pass, fn1, (*ast.IfStmt)(nil))
code.Preorder(pass, fn2, (*ast.IfStmt)(nil))
return nil, nil
diff --git a/staticcheck/lint.go b/staticcheck/lint.go
index 54202d9b6..fc9863708 100644
--- a/staticcheck/lint.go
+++ b/staticcheck/lint.go
@@ -900,6 +900,7 @@ func CheckUntrappableSignal(pass *analysis.Pass) (interface{}, error) {
func CheckTemplate(pass *analysis.Pass) (interface{}, error) {
fn := func(node ast.Node) {
call := node.(*ast.CallExpr)
+ // OPT(dh): use integer for kind
var kind string
switch code.CallNameAST(pass, call) {
case "(*text/template.Template).Parse":
From 9a1fe455724d08fc49829ac1e655fb2a0299d206 Mon Sep 17 00:00:00 2001
From: Dominik Honnef
Date: Sat, 16 May 2020 02:01:28 +0200
Subject: [PATCH 052/111] Clean up project structure
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
- merge the deprecated and arg packages into the knowledge package
- merge the functions package into analysis/code
- merge the lint/lintdsl package into analysis/lint, analysis/code and analysis/edit
- move the callgraph package to go/callgraph
- move the code package to analysis/code
- move the edit package to analysis/edit
- move the facts package to analysis/facts
- move the gcsizes package to go/gcsizes
- move the ir package to go/ir
- move the loader package to go/loader
- move the report package to analysis/report
- move the runner package to lintcmd/runner
- move the version package to lintcmd/version
- remove "// import ..." comments – modules solve this in a cleaner manner
- reorganize the lint and lint/lintutil packages into the
analysis/lint and lintcmd packages
- move code between analysis/code, analysis/facts and analysis/lint
---
{code => analysis/code}/code.go | 22 +-
{functions => analysis/code}/loops.go | 4 +-
analysis/code/stub.go | 10 +
{functions => analysis/code}/terminates.go | 4 +-
.../lintdsl.go => analysis/code/visit.go | 39 +-
{edit => analysis/edit}/edit.go | 7 +
{facts => analysis/facts}/deprecated.go | 0
analysis/facts/directives.go | 20 +
{facts => analysis/facts}/facts_test.go | 0
{facts => analysis/facts}/generated.go | 0
{facts => analysis/facts}/purity.go | 35 +-
.../testdata/src/Deprecated/Deprecated.go | 0
.../testdata/src/Purity/CheckPureFunctions.go | 0
{facts => analysis/facts}/token.go | 0
analysis/lint/lint.go | 158 ++++
{report => analysis/report}/report.go | 3 +-
cmd/keyify/keyify.go | 2 +-
cmd/rdeps/rdeps.go | 2 +-
cmd/staticcheck/staticcheck.go | 8 +-
cmd/structlayout-optimize/main.go | 2 +-
cmd/structlayout-pretty/main.go | 2 +-
cmd/structlayout/main.go | 4 +-
facts/directives.go | 107 ---
functions/stub.go | 32 -
{callgraph => go/callgraph}/callgraph.go | 4 +-
{callgraph => go/callgraph}/cha/cha.go | 9 +-
{callgraph => go/callgraph}/cha/cha_test.go | 7 +-
.../callgraph}/cha/testdata/func.go | 0
.../callgraph}/cha/testdata/iface.go | 0
.../callgraph}/cha/testdata/recv.go | 0
{callgraph => go/callgraph}/rta/rta.go | 7 +-
{callgraph => go/callgraph}/rta/rta_test.go | 9 +-
.../callgraph}/rta/testdata/func.go | 0
.../callgraph}/rta/testdata/iface.go | 0
.../callgraph}/rta/testdata/rtype.go | 0
{callgraph => go/callgraph}/static/static.go | 8 +-
.../callgraph}/static/static_test.go | 7 +-
{callgraph => go/callgraph}/util.go | 2 +-
{gcsizes => go/gcsizes}/LICENSE | 0
{gcsizes => go/gcsizes}/sizes.go | 2 +-
{ir => go/ir}/LICENSE | 0
{ir => go/ir}/blockopt.go | 0
{ir => go/ir}/builder.go | 0
{ir => go/ir}/builder_test.go | 5 +-
{ir => go/ir}/const.go | 0
{ir => go/ir}/create.go | 0
{ir => go/ir}/doc.go | 2 +-
{ir => go/ir}/dom.go | 0
{ir => go/ir}/emit.go | 0
{ir => go/ir}/example_test.go | 5 +-
{ir => go/ir}/exits.go | 0
{ir => go/ir}/func.go | 0
{ir => go/ir}/html.go | 0
{ir => go/ir}/identical.go | 0
{ir => go/ir}/identical_17.go | 0
{ir => go/ir}/identical_test.go | 0
{ir => go/ir}/irutil/load.go | 3 +-
{ir => go/ir}/irutil/load_test.go | 3 +-
{ir => go/ir}/irutil/switch.go | 2 +-
{ir => go/ir}/irutil/switch_test.go | 3 +-
{ir => go/ir}/irutil/testdata/switches.go | 0
{ir => go/ir}/irutil/util.go | 2 +-
{ir => go/ir}/irutil/visit.go | 4 +-
{ir => go/ir}/lift.go | 0
{ir => go/ir}/lvalue.go | 0
{ir => go/ir}/methods.go | 0
{ir => go/ir}/mode.go | 0
{ir => go/ir}/print.go | 0
{ir => go/ir}/sanity.go | 0
{ir => go/ir}/source.go | 0
{ir => go/ir}/source_test.go | 5 +-
{ir => go/ir}/ssa.go | 0
{ir => go/ir}/staticcheck.conf | 0
{ir => go/ir}/stdlib_test.go | 5 +-
{ir => go/ir}/testdata/objlookup.go | 0
{ir => go/ir}/testdata/structconv.go | 0
{ir => go/ir}/testdata/valueforexpr.go | 0
{ir => go/ir}/util.go | 0
{ir => go/ir}/wrappers.go | 0
{ir => go/ir}/write.go | 0
{loader => go/loader}/buildid.go | 0
{loader => go/loader}/hash.go | 0
{loader => go/loader}/loader.go | 0
{loader => go/loader}/note.go | 0
internal/cmd/irdump/main.go | 5 +-
internal/passes/buildir/buildir.go | 3 +-
internal/sharedcheck/lint.go | 12 +-
{arg => knowledge}/arg.go | 2 +-
.../stdlib.go => knowledge/deprecated.go | 4 +-
lint/LICENSE | 28 -
lint/lint.go | 476 -----------
lint/lintutil/util.go | 443 ----------
lintcmd/cmd.go | 796 ++++++++++++++++++
.../util_test.go => lintcmd/cmd_test.go | 4 +-
{lint => lintcmd}/directives.go | 13 +-
{lint/lintutil/format => lintcmd}/format.go | 47 +-
{lint => lintcmd}/lint_test.go | 15 +-
{runner => lintcmd/runner}/runner.go | 36 +-
{runner => lintcmd/runner}/stats.go | 3 +-
{lint/lintutil => lintcmd}/stats.go | 2 +-
{lint/lintutil => lintcmd}/stats_bsd.go | 2 +-
{lint/lintutil => lintcmd}/stats_posix.go | 2 +-
.../testdata/src/Test/file-ignores.go | 0
.../testdata/src/Test/line-ignores.go | 0
.../testdata/src/broken_dep/pkg.go | 0
.../testdata/src/broken_parse/pkg.go | 0
.../testdata/src/broken_pkgerror/broken.go | 0
.../testdata/src/broken_typeerror/pkg.go | 0
{version => lintcmd/version}/buildinfo.go | 0
{version => lintcmd/version}/buildinfo111.go | 0
{version => lintcmd/version}/version.go | 0
pattern/match.go | 8 +-
simple/analysis.go | 6 +-
simple/doc.go | 2 +-
simple/lint.go | 80 +-
staticcheck.conf | 1 -
staticcheck/analysis.go | 6 +-
staticcheck/buildtag.go | 2 +-
staticcheck/doc.go | 2 +-
staticcheck/lint.go | 126 +--
staticcheck/rules.go | 5 +-
stylecheck/analysis.go | 6 +-
stylecheck/doc.go | 2 +-
stylecheck/lint.go | 20 +-
stylecheck/names.go | 7 +-
unused/unused.go | 16 +-
126 files changed, 1338 insertions(+), 1399 deletions(-)
rename {code => analysis/code}/code.go (95%)
rename {functions => analysis/code}/loops.go (95%)
create mode 100644 analysis/code/stub.go
rename {functions => analysis/code}/terminates.go (97%)
rename lint/lintdsl/lintdsl.go => analysis/code/visit.go (59%)
rename {edit => analysis/edit}/edit.go (91%)
rename {facts => analysis/facts}/deprecated.go (100%)
create mode 100644 analysis/facts/directives.go
rename {facts => analysis/facts}/facts_test.go (100%)
rename {facts => analysis/facts}/generated.go (100%)
rename {facts => analysis/facts}/purity.go (83%)
rename {facts => analysis/facts}/testdata/src/Deprecated/Deprecated.go (100%)
rename {facts => analysis/facts}/testdata/src/Purity/CheckPureFunctions.go (100%)
rename {facts => analysis/facts}/token.go (100%)
create mode 100644 analysis/lint/lint.go
rename {report => analysis/report}/report.go (99%)
delete mode 100644 facts/directives.go
delete mode 100644 functions/stub.go
rename {callgraph => go/callgraph}/callgraph.go (97%)
rename {callgraph => go/callgraph}/cha/cha.go (96%)
rename {callgraph => go/callgraph}/cha/cha_test.go (95%)
rename {callgraph => go/callgraph}/cha/testdata/func.go (100%)
rename {callgraph => go/callgraph}/cha/testdata/iface.go (100%)
rename {callgraph => go/callgraph}/cha/testdata/recv.go (100%)
rename {callgraph => go/callgraph}/rta/rta.go (99%)
rename {callgraph => go/callgraph}/rta/rta_test.go (95%)
rename {callgraph => go/callgraph}/rta/testdata/func.go (100%)
rename {callgraph => go/callgraph}/rta/testdata/iface.go (100%)
rename {callgraph => go/callgraph}/rta/testdata/rtype.go (100%)
rename {callgraph => go/callgraph}/static/static.go (85%)
rename {callgraph => go/callgraph}/static/static_test.go (92%)
rename {callgraph => go/callgraph}/util.go (99%)
rename {gcsizes => go/gcsizes}/LICENSE (100%)
rename {gcsizes => go/gcsizes}/sizes.go (98%)
rename {ir => go/ir}/LICENSE (100%)
rename {ir => go/ir}/blockopt.go (100%)
rename {ir => go/ir}/builder.go (100%)
rename {ir => go/ir}/builder_test.go (99%)
rename {ir => go/ir}/const.go (100%)
rename {ir => go/ir}/create.go (100%)
rename {ir => go/ir}/doc.go (99%)
rename {ir => go/ir}/dom.go (100%)
rename {ir => go/ir}/emit.go (100%)
rename {ir => go/ir}/example_test.go (98%)
rename {ir => go/ir}/exits.go (100%)
rename {ir => go/ir}/func.go (100%)
rename {ir => go/ir}/html.go (100%)
rename {ir => go/ir}/identical.go (100%)
rename {ir => go/ir}/identical_17.go (100%)
rename {ir => go/ir}/identical_test.go (100%)
rename {ir => go/ir}/irutil/load.go (99%)
rename {ir => go/ir}/irutil/load_test.go (98%)
rename {ir => go/ir}/irutil/switch.go (99%)
rename {ir => go/ir}/irutil/switch_test.go (98%)
rename {ir => go/ir}/irutil/testdata/switches.go (100%)
rename {ir => go/ir}/irutil/util.go (97%)
rename {ir => go/ir}/irutil/visit.go (95%)
rename {ir => go/ir}/lift.go (100%)
rename {ir => go/ir}/lvalue.go (100%)
rename {ir => go/ir}/methods.go (100%)
rename {ir => go/ir}/mode.go (100%)
rename {ir => go/ir}/print.go (100%)
rename {ir => go/ir}/sanity.go (100%)
rename {ir => go/ir}/source.go (100%)
rename {ir => go/ir}/source_test.go (99%)
rename {ir => go/ir}/ssa.go (100%)
rename {ir => go/ir}/staticcheck.conf (100%)
rename {ir => go/ir}/stdlib_test.go (98%)
rename {ir => go/ir}/testdata/objlookup.go (100%)
rename {ir => go/ir}/testdata/structconv.go (100%)
rename {ir => go/ir}/testdata/valueforexpr.go (100%)
rename {ir => go/ir}/util.go (100%)
rename {ir => go/ir}/wrappers.go (100%)
rename {ir => go/ir}/write.go (100%)
rename {loader => go/loader}/buildid.go (100%)
rename {loader => go/loader}/hash.go (100%)
rename {loader => go/loader}/loader.go (100%)
rename {loader => go/loader}/note.go (100%)
rename {arg => knowledge}/arg.go (98%)
rename deprecated/stdlib.go => knowledge/deprecated.go (98%)
delete mode 100644 lint/LICENSE
delete mode 100644 lint/lint.go
delete mode 100644 lint/lintutil/util.go
create mode 100644 lintcmd/cmd.go
rename lint/lintutil/util_test.go => lintcmd/cmd_test.go (94%)
rename {lint => lintcmd}/directives.go (80%)
rename {lint/lintutil/format => lintcmd}/format.go (77%)
rename {lint => lintcmd}/lint_test.go (93%)
rename {runner => lintcmd/runner}/runner.go (97%)
rename {runner => lintcmd/runner}/stats.go (98%)
rename {lint/lintutil => lintcmd}/stats.go (88%)
rename {lint/lintutil => lintcmd}/stats_bsd.go (88%)
rename {lint/lintutil => lintcmd}/stats_posix.go (87%)
rename {lint => lintcmd}/testdata/src/Test/file-ignores.go (100%)
rename {lint => lintcmd}/testdata/src/Test/line-ignores.go (100%)
rename {lint => lintcmd}/testdata/src/broken_dep/pkg.go (100%)
rename {lint => lintcmd}/testdata/src/broken_parse/pkg.go (100%)
rename {lint => lintcmd}/testdata/src/broken_pkgerror/broken.go (100%)
rename {lint => lintcmd}/testdata/src/broken_typeerror/pkg.go (100%)
rename {version => lintcmd/version}/buildinfo.go (100%)
rename {version => lintcmd/version}/buildinfo111.go (100%)
rename {version => lintcmd/version}/version.go (100%)
delete mode 100644 staticcheck.conf
diff --git a/code/code.go b/analysis/code/code.go
similarity index 95%
rename from code/code.go
rename to analysis/code/code.go
index 711b6d097..515310b2a 100644
--- a/code/code.go
+++ b/analysis/code/code.go
@@ -12,13 +12,12 @@ import (
"strings"
"sync"
+ "honnef.co/go/tools/analysis/facts"
+ "honnef.co/go/tools/go/ir"
+ "honnef.co/go/tools/go/types/typeutil"
+
"golang.org/x/tools/go/analysis"
- "golang.org/x/tools/go/analysis/passes/inspect"
"golang.org/x/tools/go/ast/astutil"
- "golang.org/x/tools/go/ast/inspector"
- "honnef.co/go/tools/facts"
- "honnef.co/go/tools/go/types/typeutil"
- "honnef.co/go/tools/ir"
)
var bufferPool = &sync.Pool{
@@ -528,16 +527,3 @@ func IsGoVersion(pass *analysis.Pass, minor int) bool {
version := f.Get().(int)
return version >= minor
}
-
-func Preorder(pass *analysis.Pass, fn func(ast.Node), types ...ast.Node) {
- pass.ResultOf[inspect.Analyzer].(*inspector.Inspector).Preorder(types, fn)
-}
-
-func PreorderStack(pass *analysis.Pass, fn func(ast.Node, []ast.Node), types ...ast.Node) {
- pass.ResultOf[inspect.Analyzer].(*inspector.Inspector).WithStack(types, func(n ast.Node, push bool, stack []ast.Node) (proceed bool) {
- if push {
- fn(n, stack)
- }
- return true
- })
-}
diff --git a/functions/loops.go b/analysis/code/loops.go
similarity index 95%
rename from functions/loops.go
rename to analysis/code/loops.go
index a8af70100..e2f263a84 100644
--- a/functions/loops.go
+++ b/analysis/code/loops.go
@@ -1,6 +1,6 @@
-package functions
+package code
-import "honnef.co/go/tools/ir"
+import "honnef.co/go/tools/go/ir"
type Loop struct{ *ir.BlockSet }
diff --git a/analysis/code/stub.go b/analysis/code/stub.go
new file mode 100644
index 000000000..284827409
--- /dev/null
+++ b/analysis/code/stub.go
@@ -0,0 +1,10 @@
+package code
+
+import (
+ "honnef.co/go/tools/analysis/facts"
+ "honnef.co/go/tools/go/ir"
+)
+
+func IsStub(fn *ir.Function) bool {
+ return facts.IsStub(fn)
+}
diff --git a/functions/terminates.go b/analysis/code/terminates.go
similarity index 97%
rename from functions/terminates.go
rename to analysis/code/terminates.go
index c4984673f..39d93129e 100644
--- a/functions/terminates.go
+++ b/analysis/code/terminates.go
@@ -1,9 +1,9 @@
-package functions
+package code
import (
"go/types"
- "honnef.co/go/tools/ir"
+ "honnef.co/go/tools/go/ir"
)
// Terminates reports whether fn is supposed to return, that is if it
diff --git a/lint/lintdsl/lintdsl.go b/analysis/code/visit.go
similarity index 59%
rename from lint/lintdsl/lintdsl.go
rename to analysis/code/visit.go
index 4408aff25..f8bf2d169 100644
--- a/lint/lintdsl/lintdsl.go
+++ b/analysis/code/visit.go
@@ -1,22 +1,28 @@
-// Package lintdsl provides helpers for implementing static analysis
-// checks. Dot-importing this package is encouraged.
-package lintdsl
+package code
import (
"bytes"
- "fmt"
"go/ast"
"go/format"
- "golang.org/x/tools/go/analysis"
"honnef.co/go/tools/pattern"
+
+ "golang.org/x/tools/go/analysis"
+ "golang.org/x/tools/go/analysis/passes/inspect"
+ "golang.org/x/tools/go/ast/inspector"
)
-func Inspect(node ast.Node, fn func(node ast.Node) bool) {
- if node == nil {
- return
- }
- ast.Inspect(node, fn)
+func Preorder(pass *analysis.Pass, fn func(ast.Node), types ...ast.Node) {
+ pass.ResultOf[inspect.Analyzer].(*inspector.Inspector).Preorder(types, fn)
+}
+
+func PreorderStack(pass *analysis.Pass, fn func(ast.Node, []ast.Node), types ...ast.Node) {
+ pass.ResultOf[inspect.Analyzer].(*inspector.Inspector).WithStack(types, func(n ast.Node, push bool, stack []ast.Node) (proceed bool) {
+ if push {
+ fn(n, stack)
+ }
+ return true
+ })
}
func Match(pass *analysis.Pass, q pattern.Pattern, node ast.Node) (*pattern.Matcher, bool) {
@@ -43,16 +49,3 @@ func MatchAndEdit(pass *analysis.Pass, before, after pattern.Pattern, node ast.N
}}
return m, edit, true
}
-
-func Selector(x, sel string) *ast.SelectorExpr {
- return &ast.SelectorExpr{
- X: &ast.Ident{Name: x},
- Sel: &ast.Ident{Name: sel},
- }
-}
-
-// ExhaustiveTypeSwitch panics when called. It can be used to ensure
-// that type switches are exhaustive.
-func ExhaustiveTypeSwitch(v interface{}) {
- panic(fmt.Sprintf("internal error: unhandled case %T", v))
-}
diff --git a/edit/edit.go b/analysis/edit/edit.go
similarity index 91%
rename from edit/edit.go
rename to analysis/edit/edit.go
index f4cfba234..90bc5f8cc 100644
--- a/edit/edit.go
+++ b/analysis/edit/edit.go
@@ -65,3 +65,10 @@ func Fix(msg string, edits ...analysis.TextEdit) analysis.SuggestedFix {
TextEdits: edits,
}
}
+
+func Selector(x, sel string) *ast.SelectorExpr {
+ return &ast.SelectorExpr{
+ X: &ast.Ident{Name: x},
+ Sel: &ast.Ident{Name: sel},
+ }
+}
diff --git a/facts/deprecated.go b/analysis/facts/deprecated.go
similarity index 100%
rename from facts/deprecated.go
rename to analysis/facts/deprecated.go
diff --git a/analysis/facts/directives.go b/analysis/facts/directives.go
new file mode 100644
index 000000000..800fce2e0
--- /dev/null
+++ b/analysis/facts/directives.go
@@ -0,0 +1,20 @@
+package facts
+
+import (
+ "reflect"
+
+ "golang.org/x/tools/go/analysis"
+ "honnef.co/go/tools/analysis/lint"
+)
+
+func directives(pass *analysis.Pass) (interface{}, error) {
+ return lint.ParseDirectives(pass.Files, pass.Fset), nil
+}
+
+var Directives = &analysis.Analyzer{
+ Name: "directives",
+ Doc: "extracts linter directives",
+ Run: directives,
+ RunDespiteErrors: true,
+ ResultType: reflect.TypeOf([]lint.Directive{}),
+}
diff --git a/facts/facts_test.go b/analysis/facts/facts_test.go
similarity index 100%
rename from facts/facts_test.go
rename to analysis/facts/facts_test.go
diff --git a/facts/generated.go b/analysis/facts/generated.go
similarity index 100%
rename from facts/generated.go
rename to analysis/facts/generated.go
diff --git a/facts/purity.go b/analysis/facts/purity.go
similarity index 83%
rename from facts/purity.go
rename to analysis/facts/purity.go
index 099ee23e3..d708c841c 100644
--- a/facts/purity.go
+++ b/analysis/facts/purity.go
@@ -4,10 +4,10 @@ import (
"go/types"
"reflect"
- "golang.org/x/tools/go/analysis"
- "honnef.co/go/tools/functions"
+ "honnef.co/go/tools/go/ir"
"honnef.co/go/tools/internal/passes/buildir"
- "honnef.co/go/tools/ir"
+
+ "golang.org/x/tools/go/analysis"
)
type IsPure struct{}
@@ -54,6 +54,33 @@ var pureStdlib = map[string]struct{}{
"(*net/http.Request).WithContext": {},
}
+// IsStub reports whether a function is a stub. A function is
+// considered a stub if it has no instructions or if all it does is
+// return a constant value.
+func IsStub(fn *ir.Function) bool {
+ for _, b := range fn.Blocks {
+ for _, instr := range b.Instrs {
+ switch instr.(type) {
+ case *ir.Const:
+ // const naturally has no side-effects
+ case *ir.Panic:
+ // panic is a stub if it only uses constants
+ case *ir.Return:
+ // return is a stub if it only uses constants
+ case *ir.DebugRef:
+ case *ir.Jump:
+ // if there are no disallowed instructions, then we're
+ // only jumping to the exit block (or possibly
+ // somewhere else that's stubby?)
+ default:
+ // all other instructions are assumed to do actual work
+ return false
+ }
+ }
+ }
+ return true
+}
+
func purity(pass *analysis.Pass) (interface{}, error) {
seen := map[*ir.Function]struct{}{}
irpkg := pass.ResultOf[buildir.Analyzer].(*buildir.IR).Pkg
@@ -83,7 +110,7 @@ func purity(pass *analysis.Pass) (interface{}, error) {
}
}()
- if functions.IsStub(fn) {
+ if IsStub(fn) {
return false
}
diff --git a/facts/testdata/src/Deprecated/Deprecated.go b/analysis/facts/testdata/src/Deprecated/Deprecated.go
similarity index 100%
rename from facts/testdata/src/Deprecated/Deprecated.go
rename to analysis/facts/testdata/src/Deprecated/Deprecated.go
diff --git a/facts/testdata/src/Purity/CheckPureFunctions.go b/analysis/facts/testdata/src/Purity/CheckPureFunctions.go
similarity index 100%
rename from facts/testdata/src/Purity/CheckPureFunctions.go
rename to analysis/facts/testdata/src/Purity/CheckPureFunctions.go
diff --git a/facts/token.go b/analysis/facts/token.go
similarity index 100%
rename from facts/token.go
rename to analysis/facts/token.go
diff --git a/analysis/lint/lint.go b/analysis/lint/lint.go
new file mode 100644
index 000000000..fc256d747
--- /dev/null
+++ b/analysis/lint/lint.go
@@ -0,0 +1,158 @@
+// Package lint provides abstractions on top of go/analysis.
+package lint
+
+import (
+ "errors"
+ "flag"
+ "fmt"
+ "go/ast"
+ "go/build"
+ "go/token"
+ "strconv"
+ "strings"
+
+ "golang.org/x/tools/go/analysis"
+)
+
+type Documentation struct {
+ Title string
+ Text string
+ Since string
+ NonDefault bool
+ Options []string
+}
+
+func (doc *Documentation) String() string {
+ b := &strings.Builder{}
+ fmt.Fprintf(b, "%s\n\n", doc.Title)
+ if doc.Text != "" {
+ fmt.Fprintf(b, "%s\n\n", doc.Text)
+ }
+ fmt.Fprint(b, "Available since\n ")
+ if doc.Since == "" {
+ fmt.Fprint(b, "unreleased")
+ } else {
+ fmt.Fprintf(b, "%s", doc.Since)
+ }
+ if doc.NonDefault {
+ fmt.Fprint(b, ", non-default")
+ }
+ fmt.Fprint(b, "\n")
+ if len(doc.Options) > 0 {
+ fmt.Fprintf(b, "\nOptions\n")
+ for _, opt := range doc.Options {
+ fmt.Fprintf(b, " %s", opt)
+ }
+ fmt.Fprint(b, "\n")
+ }
+ return b.String()
+}
+
+func newVersionFlag() flag.Getter {
+ tags := build.Default.ReleaseTags
+ v := tags[len(tags)-1][2:]
+ version := new(VersionFlag)
+ if err := version.Set(v); err != nil {
+ panic(fmt.Sprintf("internal error: %s", err))
+ }
+ return version
+}
+
+type VersionFlag int
+
+func (v *VersionFlag) String() string {
+ return fmt.Sprintf("1.%d", *v)
+}
+
+func (v *VersionFlag) Set(s string) error {
+ if len(s) < 3 {
+ return errors.New("invalid Go version")
+ }
+ if s[0] != '1' {
+ return errors.New("invalid Go version")
+ }
+ if s[1] != '.' {
+ return errors.New("invalid Go version")
+ }
+ i, err := strconv.Atoi(s[2:])
+ *v = VersionFlag(i)
+ return err
+}
+
+func (v *VersionFlag) Get() interface{} {
+ return int(*v)
+}
+
+func InitializeAnalyzers(docs map[string]*Documentation, analyzers map[string]*analysis.Analyzer) map[string]*analysis.Analyzer {
+ out := make(map[string]*analysis.Analyzer, len(analyzers))
+ for k, v := range analyzers {
+ vc := *v
+ out[k] = &vc
+
+ vc.Name = k
+ doc, ok := docs[k]
+ if !ok {
+ panic(fmt.Sprintf("missing documentation for check %s", k))
+ }
+ vc.Doc = doc.String()
+ if vc.Flags.Usage == nil {
+ fs := flag.NewFlagSet("", flag.PanicOnError)
+ fs.Var(newVersionFlag(), "go", "Target Go version")
+ vc.Flags = *fs
+ }
+ }
+ return out
+}
+
+// ExhaustiveTypeSwitch panics when called. It can be used to ensure
+// that type switches are exhaustive.
+func ExhaustiveTypeSwitch(v interface{}) {
+ panic(fmt.Sprintf("internal error: unhandled case %T", v))
+}
+
+// A directive is a comment of the form '//lint:
+// [arguments...]'. It represents instructions to the static analysis
+// tool.
+type Directive struct {
+ Command string
+ Arguments []string
+ Directive *ast.Comment
+ Node ast.Node
+}
+
+func parseDirective(s string) (cmd string, args []string) {
+ if !strings.HasPrefix(s, "//lint:") {
+ return "", nil
+ }
+ s = strings.TrimPrefix(s, "//lint:")
+ fields := strings.Split(s, " ")
+ return fields[0], fields[1:]
+}
+
+func ParseDirectives(files []*ast.File, fset *token.FileSet) []Directive {
+ var dirs []Directive
+ for _, f := range files {
+ // OPT(dh): in our old code, we skip all the commentmap work if we
+ // couldn't find any directives, benchmark if that's actually
+ // worth doing
+ cm := ast.NewCommentMap(fset, f, f.Comments)
+ for node, cgs := range cm {
+ for _, cg := range cgs {
+ for _, c := range cg.List {
+ if !strings.HasPrefix(c.Text, "//lint:") {
+ continue
+ }
+ cmd, args := parseDirective(c.Text)
+ d := Directive{
+ Command: cmd,
+ Arguments: args,
+ Directive: c,
+ Node: node,
+ }
+ dirs = append(dirs, d)
+ }
+ }
+ }
+ }
+ return dirs
+}
diff --git a/report/report.go b/analysis/report/report.go
similarity index 99%
rename from report/report.go
rename to analysis/report/report.go
index 5b5343617..2985334d3 100644
--- a/report/report.go
+++ b/analysis/report/report.go
@@ -8,9 +8,10 @@ import (
"path/filepath"
"strings"
+ "honnef.co/go/tools/analysis/facts"
+
"golang.org/x/tools/go/analysis"
"golang.org/x/tools/go/ast/astutil"
- "honnef.co/go/tools/facts"
)
type Options struct {
diff --git a/cmd/keyify/keyify.go b/cmd/keyify/keyify.go
index 95c920e6e..45f8ed8a0 100644
--- a/cmd/keyify/keyify.go
+++ b/cmd/keyify/keyify.go
@@ -16,7 +16,7 @@ import (
"os"
"path/filepath"
- "honnef.co/go/tools/version"
+ "honnef.co/go/tools/lintcmd/version"
"golang.org/x/tools/go/ast/astutil"
"golang.org/x/tools/go/buildutil"
diff --git a/cmd/rdeps/rdeps.go b/cmd/rdeps/rdeps.go
index f4a3d0c74..711e35ee2 100644
--- a/cmd/rdeps/rdeps.go
+++ b/cmd/rdeps/rdeps.go
@@ -12,7 +12,7 @@ import (
"go/build"
"os"
- "honnef.co/go/tools/version"
+ "honnef.co/go/tools/lintcmd/version"
"github.com/kisielk/gotool"
"golang.org/x/tools/go/buildutil"
diff --git a/cmd/staticcheck/staticcheck.go b/cmd/staticcheck/staticcheck.go
index 87bed55c0..d3871a432 100644
--- a/cmd/staticcheck/staticcheck.go
+++ b/cmd/staticcheck/staticcheck.go
@@ -1,12 +1,12 @@
// staticcheck analyses Go code and makes it better.
-package main // import "honnef.co/go/tools/cmd/staticcheck"
+package main
import (
"log"
"os"
"golang.org/x/tools/go/analysis"
- "honnef.co/go/tools/lint/lintutil"
+ "honnef.co/go/tools/lintcmd"
"honnef.co/go/tools/simple"
"honnef.co/go/tools/staticcheck"
"honnef.co/go/tools/stylecheck"
@@ -14,7 +14,7 @@ import (
)
func main() {
- fs := lintutil.FlagSet("staticcheck")
+ fs := lintcmd.FlagSet("staticcheck")
debug := fs.String("debug.unused-graph", "", "Write unused's object graph to `file`")
fs.Parse(os.Args[1:])
@@ -38,5 +38,5 @@ func main() {
unused.Debug = f
}
- lintutil.ProcessFlagSet(cs, fs)
+ lintcmd.ProcessFlagSet(cs, fs)
}
diff --git a/cmd/structlayout-optimize/main.go b/cmd/structlayout-optimize/main.go
index dd9c1f3b5..0c9571cf0 100644
--- a/cmd/structlayout-optimize/main.go
+++ b/cmd/structlayout-optimize/main.go
@@ -11,8 +11,8 @@ import (
"sort"
"strings"
+ "honnef.co/go/tools/lintcmd/version"
st "honnef.co/go/tools/structlayout"
- "honnef.co/go/tools/version"
)
var (
diff --git a/cmd/structlayout-pretty/main.go b/cmd/structlayout-pretty/main.go
index a75192c9d..3c2123e5b 100644
--- a/cmd/structlayout-pretty/main.go
+++ b/cmd/structlayout-pretty/main.go
@@ -10,8 +10,8 @@ import (
"os"
"strings"
+ "honnef.co/go/tools/lintcmd/version"
st "honnef.co/go/tools/structlayout"
- "honnef.co/go/tools/version"
)
var (
diff --git a/cmd/structlayout/main.go b/cmd/structlayout/main.go
index 157920767..6818422d7 100644
--- a/cmd/structlayout/main.go
+++ b/cmd/structlayout/main.go
@@ -10,9 +10,9 @@ import (
"log"
"os"
- "honnef.co/go/tools/gcsizes"
+ "honnef.co/go/tools/go/gcsizes"
+ "honnef.co/go/tools/lintcmd/version"
st "honnef.co/go/tools/structlayout"
- "honnef.co/go/tools/version"
"golang.org/x/tools/go/packages"
)
diff --git a/facts/directives.go b/facts/directives.go
deleted file mode 100644
index 04cee52aa..000000000
--- a/facts/directives.go
+++ /dev/null
@@ -1,107 +0,0 @@
-package facts
-
-import (
- "go/ast"
- "go/token"
- "path/filepath"
- "reflect"
- "strings"
-
- "golang.org/x/tools/go/analysis"
-)
-
-// A directive is a comment of the form '//lint:
-// [arguments...]'. It represents instructions to the static analysis
-// tool.
-type Directive struct {
- Command string
- Arguments []string
- Directive *ast.Comment
- Node ast.Node
-}
-
-type SerializedDirective struct {
- Command string
- Arguments []string
- // The position of the comment
- DirectivePosition token.Position
- // The position of the node that the comment is attached to
- NodePosition token.Position
-}
-
-func parseDirective(s string) (cmd string, args []string) {
- if !strings.HasPrefix(s, "//lint:") {
- return "", nil
- }
- s = strings.TrimPrefix(s, "//lint:")
- fields := strings.Split(s, " ")
- return fields[0], fields[1:]
-}
-
-func directives(pass *analysis.Pass) (interface{}, error) {
- return ParseDirectives(pass.Files, pass.Fset), nil
-}
-
-func ParseDirectives(files []*ast.File, fset *token.FileSet) []Directive {
- var dirs []Directive
- for _, f := range files {
- // OPT(dh): in our old code, we skip all the commentmap work if we
- // couldn't find any directives, benchmark if that's actually
- // worth doing
- cm := ast.NewCommentMap(fset, f, f.Comments)
- for node, cgs := range cm {
- for _, cg := range cgs {
- for _, c := range cg.List {
- if !strings.HasPrefix(c.Text, "//lint:") {
- continue
- }
- cmd, args := parseDirective(c.Text)
- d := Directive{
- Command: cmd,
- Arguments: args,
- Directive: c,
- Node: node,
- }
- dirs = append(dirs, d)
- }
- }
- }
- }
- return dirs
-}
-
-// duplicated from report.DisplayPosition to break import cycle
-func displayPosition(fset *token.FileSet, p token.Pos) token.Position {
- if p == token.NoPos {
- return token.Position{}
- }
-
- // Only use the adjusted position if it points to another Go file.
- // This means we'll point to the original file for cgo files, but
- // we won't point to a YACC grammar file.
- pos := fset.PositionFor(p, false)
- adjPos := fset.PositionFor(p, true)
-
- if filepath.Ext(adjPos.Filename) == ".go" {
- return adjPos
- }
-
- return pos
-}
-
-var Directives = &analysis.Analyzer{
- Name: "directives",
- Doc: "extracts linter directives",
- Run: directives,
- RunDespiteErrors: true,
- ResultType: reflect.TypeOf([]Directive{}),
-}
-
-func SerializeDirective(dir Directive, fset *token.FileSet) SerializedDirective {
- return SerializedDirective{
- Command: dir.Command,
- Arguments: dir.Arguments,
- DirectivePosition: displayPosition(fset, dir.Directive.Pos()),
- NodePosition: displayPosition(fset, dir.Node.Pos()),
- }
-}
diff --git a/functions/stub.go b/functions/stub.go
deleted file mode 100644
index 4d5de10b8..000000000
--- a/functions/stub.go
+++ /dev/null
@@ -1,32 +0,0 @@
-package functions
-
-import (
- "honnef.co/go/tools/ir"
-)
-
-// IsStub reports whether a function is a stub. A function is
-// considered a stub if it has no instructions or if all it does is
-// return a constant value.
-func IsStub(fn *ir.Function) bool {
- for _, b := range fn.Blocks {
- for _, instr := range b.Instrs {
- switch instr.(type) {
- case *ir.Const:
- // const naturally has no side-effects
- case *ir.Panic:
- // panic is a stub if it only uses constants
- case *ir.Return:
- // return is a stub if it only uses constants
- case *ir.DebugRef:
- case *ir.Jump:
- // if there are no disallowed instructions, then we're
- // only jumping to the exit block (or possibly
- // somewhere else that's stubby?)
- default:
- // all other instructions are assumed to do actual work
- return false
- }
- }
- }
- return true
-}
diff --git a/callgraph/callgraph.go b/go/callgraph/callgraph.go
similarity index 97%
rename from callgraph/callgraph.go
rename to go/callgraph/callgraph.go
index 9539df9a4..ef1a0d01f 100644
--- a/callgraph/callgraph.go
+++ b/go/callgraph/callgraph.go
@@ -32,7 +32,7 @@ in the call graph; they are treated like built-in operators of the
language.
*/
-package callgraph // import "honnef.co/go/tools/callgraph"
+package callgraph
// TODO(adonovan): add a function to eliminate wrappers from the
// callgraph, preserving topology.
@@ -43,7 +43,7 @@ import (
"fmt"
"go/token"
- "honnef.co/go/tools/ir"
+ "honnef.co/go/tools/go/ir"
)
// A Graph represents a call graph.
diff --git a/callgraph/cha/cha.go b/go/callgraph/cha/cha.go
similarity index 96%
rename from callgraph/cha/cha.go
rename to go/callgraph/cha/cha.go
index 985d4dd9c..449cec4e7 100644
--- a/callgraph/cha/cha.go
+++ b/go/callgraph/cha/cha.go
@@ -21,15 +21,16 @@
// and all concrete types are put into interfaces, it is sound to run on
// partial programs, such as libraries without a main or test function.
//
-package cha // import "honnef.co/go/tools/callgraph/cha"
+package cha
import (
"go/types"
+ "honnef.co/go/tools/go/callgraph"
+ "honnef.co/go/tools/go/ir"
+ "honnef.co/go/tools/go/ir/irutil"
+
"golang.org/x/tools/go/types/typeutil"
- "honnef.co/go/tools/callgraph"
- "honnef.co/go/tools/ir"
- "honnef.co/go/tools/ir/irutil"
)
// CallGraph computes the call graph of the specified program using the
diff --git a/callgraph/cha/cha_test.go b/go/callgraph/cha/cha_test.go
similarity index 95%
rename from callgraph/cha/cha_test.go
rename to go/callgraph/cha/cha_test.go
index f2b770ee3..7efd096af 100644
--- a/callgraph/cha/cha_test.go
+++ b/go/callgraph/cha/cha_test.go
@@ -22,10 +22,11 @@ import (
"strings"
"testing"
+ "honnef.co/go/tools/go/callgraph"
+ "honnef.co/go/tools/go/callgraph/cha"
+ "honnef.co/go/tools/go/ir/irutil"
+
"golang.org/x/tools/go/loader"
- "honnef.co/go/tools/callgraph"
- "honnef.co/go/tools/callgraph/cha"
- "honnef.co/go/tools/ir/irutil"
)
var inputs = []string{
diff --git a/callgraph/cha/testdata/func.go b/go/callgraph/cha/testdata/func.go
similarity index 100%
rename from callgraph/cha/testdata/func.go
rename to go/callgraph/cha/testdata/func.go
diff --git a/callgraph/cha/testdata/iface.go b/go/callgraph/cha/testdata/iface.go
similarity index 100%
rename from callgraph/cha/testdata/iface.go
rename to go/callgraph/cha/testdata/iface.go
diff --git a/callgraph/cha/testdata/recv.go b/go/callgraph/cha/testdata/recv.go
similarity index 100%
rename from callgraph/cha/testdata/recv.go
rename to go/callgraph/cha/testdata/recv.go
diff --git a/callgraph/rta/rta.go b/go/callgraph/rta/rta.go
similarity index 99%
rename from callgraph/rta/rta.go
rename to go/callgraph/rta/rta.go
index bf2995eb8..fe71a15fd 100644
--- a/callgraph/rta/rta.go
+++ b/go/callgraph/rta/rta.go
@@ -40,7 +40,7 @@
// cmd/callgraph tool on its own source takes ~2.1s for RTA and ~5.4s
// for points-to analysis.
//
-package rta // import "honnef.co/go/tools/callgraph/rta"
+package rta
// TODO(adonovan): test it by connecting it to the interpreter and
// replacing all "unreachable" functions by a special intrinsic, and
@@ -50,9 +50,10 @@ import (
"fmt"
"go/types"
+ "honnef.co/go/tools/go/callgraph"
+ "honnef.co/go/tools/go/ir"
+
"golang.org/x/tools/go/types/typeutil"
- "honnef.co/go/tools/callgraph"
- "honnef.co/go/tools/ir"
)
// A Result holds the results of Rapid Type Analysis, which includes the
diff --git a/callgraph/rta/rta_test.go b/go/callgraph/rta/rta_test.go
similarity index 95%
rename from callgraph/rta/rta_test.go
rename to go/callgraph/rta/rta_test.go
index e586db72b..c37356b7c 100644
--- a/callgraph/rta/rta_test.go
+++ b/go/callgraph/rta/rta_test.go
@@ -22,11 +22,12 @@ import (
"strings"
"testing"
+ "honnef.co/go/tools/go/callgraph"
+ "honnef.co/go/tools/go/callgraph/rta"
+ "honnef.co/go/tools/go/ir"
+ "honnef.co/go/tools/go/ir/irutil"
+
"golang.org/x/tools/go/loader"
- "honnef.co/go/tools/callgraph"
- "honnef.co/go/tools/callgraph/rta"
- "honnef.co/go/tools/ir"
- "honnef.co/go/tools/ir/irutil"
)
var inputs = []string{
diff --git a/callgraph/rta/testdata/func.go b/go/callgraph/rta/testdata/func.go
similarity index 100%
rename from callgraph/rta/testdata/func.go
rename to go/callgraph/rta/testdata/func.go
diff --git a/callgraph/rta/testdata/iface.go b/go/callgraph/rta/testdata/iface.go
similarity index 100%
rename from callgraph/rta/testdata/iface.go
rename to go/callgraph/rta/testdata/iface.go
diff --git a/callgraph/rta/testdata/rtype.go b/go/callgraph/rta/testdata/rtype.go
similarity index 100%
rename from callgraph/rta/testdata/rtype.go
rename to go/callgraph/rta/testdata/rtype.go
diff --git a/callgraph/static/static.go b/go/callgraph/static/static.go
similarity index 85%
rename from callgraph/static/static.go
rename to go/callgraph/static/static.go
index a4c73b76b..415f5c712 100644
--- a/callgraph/static/static.go
+++ b/go/callgraph/static/static.go
@@ -1,11 +1,11 @@
// Package static computes the call graph of a Go program containing
// only static call edges.
-package static // import "honnef.co/go/tools/callgraph/static"
+package static
import (
- "honnef.co/go/tools/callgraph"
- "honnef.co/go/tools/ir"
- "honnef.co/go/tools/ir/irutil"
+ "honnef.co/go/tools/go/callgraph"
+ "honnef.co/go/tools/go/ir"
+ "honnef.co/go/tools/go/ir/irutil"
)
// CallGraph computes the call graph of the specified program
diff --git a/callgraph/static/static_test.go b/go/callgraph/static/static_test.go
similarity index 92%
rename from callgraph/static/static_test.go
rename to go/callgraph/static/static_test.go
index a50c60324..e0a0bf415 100644
--- a/callgraph/static/static_test.go
+++ b/go/callgraph/static/static_test.go
@@ -13,10 +13,11 @@ import (
"sort"
"testing"
+ "honnef.co/go/tools/go/callgraph"
+ "honnef.co/go/tools/go/callgraph/static"
+ "honnef.co/go/tools/go/ir/irutil"
+
"golang.org/x/tools/go/loader"
- "honnef.co/go/tools/callgraph"
- "honnef.co/go/tools/callgraph/static"
- "honnef.co/go/tools/ir/irutil"
)
const input = `package P
diff --git a/callgraph/util.go b/go/callgraph/util.go
similarity index 99%
rename from callgraph/util.go
rename to go/callgraph/util.go
index 7f81964f7..d86b53ec8 100644
--- a/callgraph/util.go
+++ b/go/callgraph/util.go
@@ -4,7 +4,7 @@
package callgraph
-import "honnef.co/go/tools/ir"
+import "honnef.co/go/tools/go/ir"
// This file provides various utilities over call graphs, such as
// visitation and path search.
diff --git a/gcsizes/LICENSE b/go/gcsizes/LICENSE
similarity index 100%
rename from gcsizes/LICENSE
rename to go/gcsizes/LICENSE
diff --git a/gcsizes/sizes.go b/go/gcsizes/sizes.go
similarity index 98%
rename from gcsizes/sizes.go
rename to go/gcsizes/sizes.go
index 4857418c9..d6bbf4734 100644
--- a/gcsizes/sizes.go
+++ b/go/gcsizes/sizes.go
@@ -4,7 +4,7 @@
// Package gcsizes provides a types.Sizes implementation that adheres
// to the rules used by the gc compiler.
-package gcsizes // import "honnef.co/go/tools/gcsizes"
+package gcsizes
import (
"go/build"
diff --git a/ir/LICENSE b/go/ir/LICENSE
similarity index 100%
rename from ir/LICENSE
rename to go/ir/LICENSE
diff --git a/ir/blockopt.go b/go/ir/blockopt.go
similarity index 100%
rename from ir/blockopt.go
rename to go/ir/blockopt.go
diff --git a/ir/builder.go b/go/ir/builder.go
similarity index 100%
rename from ir/builder.go
rename to go/ir/builder.go
diff --git a/ir/builder_test.go b/go/ir/builder_test.go
similarity index 99%
rename from ir/builder_test.go
rename to go/ir/builder_test.go
index 0648bacc1..17e204bbb 100644
--- a/ir/builder_test.go
+++ b/go/ir/builder_test.go
@@ -18,9 +18,10 @@ import (
"sort"
"testing"
+ "honnef.co/go/tools/go/ir"
+ "honnef.co/go/tools/go/ir/irutil"
+
"golang.org/x/tools/go/loader"
- "honnef.co/go/tools/ir"
- "honnef.co/go/tools/ir/irutil"
)
func isEmpty(f *ir.Function) bool { return f.Blocks == nil }
diff --git a/ir/const.go b/go/ir/const.go
similarity index 100%
rename from ir/const.go
rename to go/ir/const.go
diff --git a/ir/create.go b/go/ir/create.go
similarity index 100%
rename from ir/create.go
rename to go/ir/create.go
diff --git a/ir/doc.go b/go/ir/doc.go
similarity index 99%
rename from ir/doc.go
rename to go/ir/doc.go
index a5f42e4f4..87c54c55e 100644
--- a/ir/doc.go
+++ b/go/ir/doc.go
@@ -126,4 +126,4 @@
// domains of source locations, ast.Nodes, types.Objects,
// ir.Values/Instructions.
//
-package ir // import "honnef.co/go/tools/ir"
+package ir
diff --git a/ir/dom.go b/go/ir/dom.go
similarity index 100%
rename from ir/dom.go
rename to go/ir/dom.go
diff --git a/ir/emit.go b/go/ir/emit.go
similarity index 100%
rename from ir/emit.go
rename to go/ir/emit.go
diff --git a/ir/example_test.go b/go/ir/example_test.go
similarity index 98%
rename from ir/example_test.go
rename to go/ir/example_test.go
index 0e016aaf9..216e02b0e 100644
--- a/ir/example_test.go
+++ b/go/ir/example_test.go
@@ -14,9 +14,10 @@ import (
"log"
"os"
+ "honnef.co/go/tools/go/ir"
+ "honnef.co/go/tools/go/ir/irutil"
+
"golang.org/x/tools/go/packages"
- "honnef.co/go/tools/ir"
- "honnef.co/go/tools/ir/irutil"
)
const hello = `
diff --git a/ir/exits.go b/go/ir/exits.go
similarity index 100%
rename from ir/exits.go
rename to go/ir/exits.go
diff --git a/ir/func.go b/go/ir/func.go
similarity index 100%
rename from ir/func.go
rename to go/ir/func.go
diff --git a/ir/html.go b/go/ir/html.go
similarity index 100%
rename from ir/html.go
rename to go/ir/html.go
diff --git a/ir/identical.go b/go/ir/identical.go
similarity index 100%
rename from ir/identical.go
rename to go/ir/identical.go
diff --git a/ir/identical_17.go b/go/ir/identical_17.go
similarity index 100%
rename from ir/identical_17.go
rename to go/ir/identical_17.go
diff --git a/ir/identical_test.go b/go/ir/identical_test.go
similarity index 100%
rename from ir/identical_test.go
rename to go/ir/identical_test.go
diff --git a/ir/irutil/load.go b/go/ir/irutil/load.go
similarity index 99%
rename from ir/irutil/load.go
rename to go/ir/irutil/load.go
index a62df49ea..1e83effa1 100644
--- a/ir/irutil/load.go
+++ b/go/ir/irutil/load.go
@@ -11,9 +11,10 @@ import (
"go/token"
"go/types"
+ "honnef.co/go/tools/go/ir"
+
"golang.org/x/tools/go/loader"
"golang.org/x/tools/go/packages"
- "honnef.co/go/tools/ir"
)
type Options struct {
diff --git a/ir/irutil/load_test.go b/go/ir/irutil/load_test.go
similarity index 98%
rename from ir/irutil/load_test.go
rename to go/ir/irutil/load_test.go
index fce0add3f..970411cb2 100644
--- a/ir/irutil/load_test.go
+++ b/go/ir/irutil/load_test.go
@@ -15,8 +15,9 @@ import (
"strings"
"testing"
+ "honnef.co/go/tools/go/ir/irutil"
+
"golang.org/x/tools/go/packages"
- "honnef.co/go/tools/ir/irutil"
)
const hello = `package main
diff --git a/ir/irutil/switch.go b/go/ir/irutil/switch.go
similarity index 99%
rename from ir/irutil/switch.go
rename to go/ir/irutil/switch.go
index f44cbca9e..e7654e008 100644
--- a/ir/irutil/switch.go
+++ b/go/ir/irutil/switch.go
@@ -24,7 +24,7 @@ import (
"go/token"
"go/types"
- "honnef.co/go/tools/ir"
+ "honnef.co/go/tools/go/ir"
)
// A ConstCase represents a single constant comparison.
diff --git a/ir/irutil/switch_test.go b/go/ir/irutil/switch_test.go
similarity index 98%
rename from ir/irutil/switch_test.go
rename to go/ir/irutil/switch_test.go
index ef4527078..beeb9823e 100644
--- a/ir/irutil/switch_test.go
+++ b/go/ir/irutil/switch_test.go
@@ -15,8 +15,9 @@ import (
"strings"
"testing"
+ "honnef.co/go/tools/go/ir"
+
"golang.org/x/tools/go/loader"
- "honnef.co/go/tools/ir"
)
func TestSwitches(t *testing.T) {
diff --git a/ir/irutil/testdata/switches.go b/go/ir/irutil/testdata/switches.go
similarity index 100%
rename from ir/irutil/testdata/switches.go
rename to go/ir/irutil/testdata/switches.go
diff --git a/ir/irutil/util.go b/go/ir/irutil/util.go
similarity index 97%
rename from ir/irutil/util.go
rename to go/ir/irutil/util.go
index 04b25f5f9..badff17f2 100644
--- a/ir/irutil/util.go
+++ b/go/ir/irutil/util.go
@@ -1,7 +1,7 @@
package irutil
import (
- "honnef.co/go/tools/ir"
+ "honnef.co/go/tools/go/ir"
)
func Reachable(from, to *ir.BasicBlock) bool {
diff --git a/ir/irutil/visit.go b/go/ir/irutil/visit.go
similarity index 95%
rename from ir/irutil/visit.go
rename to go/ir/irutil/visit.go
index 657c9cde7..f6d0503dd 100644
--- a/ir/irutil/visit.go
+++ b/go/ir/irutil/visit.go
@@ -2,9 +2,9 @@
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
-package irutil // import "honnef.co/go/tools/ir/irutil"
+package irutil
-import "honnef.co/go/tools/ir"
+import "honnef.co/go/tools/go/ir"
// This file defines utilities for visiting the IR of
// a Program.
diff --git a/ir/lift.go b/go/ir/lift.go
similarity index 100%
rename from ir/lift.go
rename to go/ir/lift.go
diff --git a/ir/lvalue.go b/go/ir/lvalue.go
similarity index 100%
rename from ir/lvalue.go
rename to go/ir/lvalue.go
diff --git a/ir/methods.go b/go/ir/methods.go
similarity index 100%
rename from ir/methods.go
rename to go/ir/methods.go
diff --git a/ir/mode.go b/go/ir/mode.go
similarity index 100%
rename from ir/mode.go
rename to go/ir/mode.go
diff --git a/ir/print.go b/go/ir/print.go
similarity index 100%
rename from ir/print.go
rename to go/ir/print.go
diff --git a/ir/sanity.go b/go/ir/sanity.go
similarity index 100%
rename from ir/sanity.go
rename to go/ir/sanity.go
diff --git a/ir/source.go b/go/ir/source.go
similarity index 100%
rename from ir/source.go
rename to go/ir/source.go
diff --git a/ir/source_test.go b/go/ir/source_test.go
similarity index 99%
rename from ir/source_test.go
rename to go/ir/source_test.go
index c3f010f4b..16aa0561b 100644
--- a/ir/source_test.go
+++ b/go/ir/source_test.go
@@ -21,11 +21,12 @@ import (
"strings"
"testing"
+ "honnef.co/go/tools/go/ir"
+ "honnef.co/go/tools/go/ir/irutil"
+
"golang.org/x/tools/go/ast/astutil"
"golang.org/x/tools/go/expect"
"golang.org/x/tools/go/loader"
- "honnef.co/go/tools/ir"
- "honnef.co/go/tools/ir/irutil"
)
func TestObjValueLookup(t *testing.T) {
diff --git a/ir/ssa.go b/go/ir/ssa.go
similarity index 100%
rename from ir/ssa.go
rename to go/ir/ssa.go
diff --git a/ir/staticcheck.conf b/go/ir/staticcheck.conf
similarity index 100%
rename from ir/staticcheck.conf
rename to go/ir/staticcheck.conf
diff --git a/ir/stdlib_test.go b/go/ir/stdlib_test.go
similarity index 98%
rename from ir/stdlib_test.go
rename to go/ir/stdlib_test.go
index f6495e8f4..232f52ccf 100644
--- a/ir/stdlib_test.go
+++ b/go/ir/stdlib_test.go
@@ -22,9 +22,10 @@ import (
"testing"
"time"
+ "honnef.co/go/tools/go/ir"
+ "honnef.co/go/tools/go/ir/irutil"
+
"golang.org/x/tools/go/packages"
- "honnef.co/go/tools/ir"
- "honnef.co/go/tools/ir/irutil"
)
func bytesAllocated() uint64 {
diff --git a/ir/testdata/objlookup.go b/go/ir/testdata/objlookup.go
similarity index 100%
rename from ir/testdata/objlookup.go
rename to go/ir/testdata/objlookup.go
diff --git a/ir/testdata/structconv.go b/go/ir/testdata/structconv.go
similarity index 100%
rename from ir/testdata/structconv.go
rename to go/ir/testdata/structconv.go
diff --git a/ir/testdata/valueforexpr.go b/go/ir/testdata/valueforexpr.go
similarity index 100%
rename from ir/testdata/valueforexpr.go
rename to go/ir/testdata/valueforexpr.go
diff --git a/ir/util.go b/go/ir/util.go
similarity index 100%
rename from ir/util.go
rename to go/ir/util.go
diff --git a/ir/wrappers.go b/go/ir/wrappers.go
similarity index 100%
rename from ir/wrappers.go
rename to go/ir/wrappers.go
diff --git a/ir/write.go b/go/ir/write.go
similarity index 100%
rename from ir/write.go
rename to go/ir/write.go
diff --git a/loader/buildid.go b/go/loader/buildid.go
similarity index 100%
rename from loader/buildid.go
rename to go/loader/buildid.go
diff --git a/loader/hash.go b/go/loader/hash.go
similarity index 100%
rename from loader/hash.go
rename to go/loader/hash.go
diff --git a/loader/loader.go b/go/loader/loader.go
similarity index 100%
rename from loader/loader.go
rename to go/loader/loader.go
diff --git a/loader/note.go b/go/loader/note.go
similarity index 100%
rename from loader/note.go
rename to go/loader/note.go
diff --git a/internal/cmd/irdump/main.go b/internal/cmd/irdump/main.go
index 8727715c3..b39ddc3c9 100644
--- a/internal/cmd/irdump/main.go
+++ b/internal/cmd/irdump/main.go
@@ -12,10 +12,11 @@ import (
"os"
"runtime/pprof"
+ "honnef.co/go/tools/go/ir"
+ "honnef.co/go/tools/go/ir/irutil"
+
"golang.org/x/tools/go/buildutil"
"golang.org/x/tools/go/packages"
- "honnef.co/go/tools/ir"
- "honnef.co/go/tools/ir/irutil"
)
// flags
diff --git a/internal/passes/buildir/buildir.go b/internal/passes/buildir/buildir.go
index 884884f55..645e216a9 100644
--- a/internal/passes/buildir/buildir.go
+++ b/internal/passes/buildir/buildir.go
@@ -15,8 +15,9 @@ import (
"go/types"
"reflect"
+ "honnef.co/go/tools/go/ir"
+
"golang.org/x/tools/go/analysis"
- "honnef.co/go/tools/ir"
)
type willExit struct{}
diff --git a/internal/sharedcheck/lint.go b/internal/sharedcheck/lint.go
index d1433c4f1..6b0d31ba8 100644
--- a/internal/sharedcheck/lint.go
+++ b/internal/sharedcheck/lint.go
@@ -4,11 +4,11 @@ import (
"go/ast"
"go/types"
- "golang.org/x/tools/go/analysis"
- "honnef.co/go/tools/code"
+ "honnef.co/go/tools/analysis/code"
+ "honnef.co/go/tools/go/ir"
"honnef.co/go/tools/internal/passes/buildir"
- "honnef.co/go/tools/ir"
- . "honnef.co/go/tools/lint/lintdsl"
+
+ "golang.org/x/tools/go/analysis"
)
func CheckRangeStringRunes(pass *analysis.Pass) (interface{}, error) {
@@ -65,7 +65,9 @@ func CheckRangeStringRunes(pass *analysis.Pass) (interface{}, error) {
return true
}
- Inspect(fn.Source(), cb)
+ if source := fn.Source(); source != nil {
+ ast.Inspect(source, cb)
+ }
}
return nil, nil
}
diff --git a/arg/arg.go b/knowledge/arg.go
similarity index 98%
rename from arg/arg.go
rename to knowledge/arg.go
index 1e7f30db4..c14ed73e4 100644
--- a/arg/arg.go
+++ b/knowledge/arg.go
@@ -1,4 +1,4 @@
-package arg
+package knowledge
var args = map[string]int{
"(*encoding/json.Decoder).Decode.v": 0,
diff --git a/deprecated/stdlib.go b/knowledge/deprecated.go
similarity index 98%
rename from deprecated/stdlib.go
rename to knowledge/deprecated.go
index cabb8500a..ffed387c9 100644
--- a/deprecated/stdlib.go
+++ b/knowledge/deprecated.go
@@ -1,11 +1,11 @@
-package deprecated
+package knowledge
type Deprecation struct {
DeprecatedSince int
AlternativeAvailableSince int
}
-var Stdlib = map[string]Deprecation{
+var StdlibDeprecations = map[string]Deprecation{
// FIXME(dh): AllowBinary isn't being detected as deprecated
// because the comment has a newline right after "Deprecated:"
"go/build.AllowBinary": {7, 7},
diff --git a/lint/LICENSE b/lint/LICENSE
deleted file mode 100644
index 796130a12..000000000
--- a/lint/LICENSE
+++ /dev/null
@@ -1,28 +0,0 @@
-Copyright (c) 2013 The Go Authors. All rights reserved.
-Copyright (c) 2016 Dominik Honnef. All rights reserved.
-
-Redistribution and use in source and binary forms, with or without
-modification, are permitted provided that the following conditions are
-met:
-
- * Redistributions of source code must retain the above copyright
-notice, this list of conditions and the following disclaimer.
- * Redistributions in binary form must reproduce the above
-copyright notice, this list of conditions and the following disclaimer
-in the documentation and/or other materials provided with the
-distribution.
- * Neither the name of Google Inc. nor the names of its
-contributors may be used to endorse or promote products derived from
-this software without specific prior written permission.
-
-THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
-"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
-LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
-A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
-OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
-SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
-LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
-DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
-THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
-(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
-OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
diff --git a/lint/lint.go b/lint/lint.go
deleted file mode 100644
index f2a4f0114..000000000
--- a/lint/lint.go
+++ /dev/null
@@ -1,476 +0,0 @@
-// Package lint provides the foundation for tools like staticcheck
-package lint // import "honnef.co/go/tools/lint"
-
-import (
- "fmt"
- "go/token"
- "os"
- "path/filepath"
- "regexp"
- "sort"
- "strconv"
- "strings"
- "unicode"
-
- "honnef.co/go/tools/config"
- "honnef.co/go/tools/runner"
- "honnef.co/go/tools/unused"
-
- "golang.org/x/tools/go/analysis"
- "golang.org/x/tools/go/packages"
-)
-
-type Documentation struct {
- Title string
- Text string
- Since string
- NonDefault bool
- Options []string
-}
-
-func (doc *Documentation) String() string {
- b := &strings.Builder{}
- fmt.Fprintf(b, "%s\n\n", doc.Title)
- if doc.Text != "" {
- fmt.Fprintf(b, "%s\n\n", doc.Text)
- }
- fmt.Fprint(b, "Available since\n ")
- if doc.Since == "" {
- fmt.Fprint(b, "unreleased")
- } else {
- fmt.Fprintf(b, "%s", doc.Since)
- }
- if doc.NonDefault {
- fmt.Fprint(b, ", non-default")
- }
- fmt.Fprint(b, "\n")
- if len(doc.Options) > 0 {
- fmt.Fprintf(b, "\nOptions\n")
- for _, opt := range doc.Options {
- fmt.Fprintf(b, " %s", opt)
- }
- fmt.Fprint(b, "\n")
- }
- return b.String()
-}
-
-type ignore interface {
- Match(p Problem) bool
-}
-
-type lineIgnore struct {
- File string
- Line int
- Checks []string
- Matched bool
- Pos token.Position
-}
-
-func (li *lineIgnore) Match(p Problem) bool {
- pos := p.Position
- if pos.Filename != li.File || pos.Line != li.Line {
- return false
- }
- for _, c := range li.Checks {
- if m, _ := filepath.Match(c, p.Category); m {
- li.Matched = true
- return true
- }
- }
- return false
-}
-
-func (li *lineIgnore) String() string {
- matched := "not matched"
- if li.Matched {
- matched = "matched"
- }
- return fmt.Sprintf("%s:%d %s (%s)", li.File, li.Line, strings.Join(li.Checks, ", "), matched)
-}
-
-type fileIgnore struct {
- File string
- Checks []string
-}
-
-func (fi *fileIgnore) Match(p Problem) bool {
- if p.Position.Filename != fi.File {
- return false
- }
- for _, c := range fi.Checks {
- if m, _ := filepath.Match(c, p.Category); m {
- return true
- }
- }
- return false
-}
-
-type Severity uint8
-
-const (
- Error Severity = iota
- Warning
- Ignored
-)
-
-// Problem represents a problem in some source code.
-type Problem struct {
- runner.Diagnostic
- Severity Severity
-}
-
-func (p Problem) equal(o Problem) bool {
- return p.Position == o.Position &&
- p.End == o.End &&
- p.Message == o.Message &&
- p.Category == o.Category &&
- p.Severity == o.Severity
-}
-
-func (p *Problem) String() string {
- return fmt.Sprintf("%s (%s)", p.Message, p.Category)
-}
-
-// A Linter lints Go source code.
-type Linter struct {
- Checkers []*analysis.Analyzer
- Config config.Config
- Runner *runner.Runner
-}
-
-func failed(res runner.Result) []Problem {
- var problems []Problem
-
- for _, e := range res.Errors {
- switch e := e.(type) {
- case packages.Error:
- msg := e.Msg
- if len(msg) != 0 && msg[0] == '\n' {
- // TODO(dh): See https://github.com/golang/go/issues/32363
- msg = msg[1:]
- }
-
- var posn token.Position
- if e.Pos == "" {
- // Under certain conditions (malformed package
- // declarations, multiple packages in the same
- // directory), go list emits an error on stderr
- // instead of JSON. Those errors do not have
- // associated position information in
- // go/packages.Error, even though the output on
- // stderr may contain it.
- if p, n, err := parsePos(msg); err == nil {
- if abs, err := filepath.Abs(p.Filename); err == nil {
- p.Filename = abs
- }
- posn = p
- msg = msg[n+2:]
- }
- } else {
- var err error
- posn, _, err = parsePos(e.Pos)
- if err != nil {
- panic(fmt.Sprintf("internal error: %s", e))
- }
- }
- p := Problem{
- Diagnostic: runner.Diagnostic{
- Position: posn,
- Message: msg,
- Category: "compile",
- },
- Severity: Error,
- }
- problems = append(problems, p)
- case error:
- p := Problem{
- Diagnostic: runner.Diagnostic{
- Position: token.Position{},
- Message: e.Error(),
- Category: "compile",
- },
- Severity: Error,
- }
- problems = append(problems, p)
- }
- }
-
- return problems
-}
-
-type unusedKey struct {
- pkgPath string
- base string
- line int
- name string
-}
-
-type unusedPair struct {
- key unusedKey
- obj unused.SerializedObject
-}
-
-func success(allowedChecks map[string]bool, res runner.ResultData) []Problem {
- diags := res.Diagnostics
- var problems []Problem
- for _, diag := range diags {
- if !allowedChecks[diag.Category] {
- continue
- }
- problems = append(problems, Problem{Diagnostic: diag})
- }
- return problems
-}
-
-func filterIgnored(problems []Problem, res runner.ResultData, allowedAnalyzers map[string]bool) ([]Problem, error) {
- couldveMatched := func(ig *lineIgnore) bool {
- for _, c := range ig.Checks {
- if c == "U1000" {
- // We never want to flag ignores for U1000,
- // because U1000 isn't local to a single
- // package. For example, an identifier may
- // only be used by tests, in which case an
- // ignore would only fire when not analyzing
- // tests. To avoid spurious "useless ignore"
- // warnings, just never flag U1000.
- return false
- }
-
- // Even though the runner always runs all analyzers, we
- // still only flag unmatched ignores for the set of
- // analyzers the user has expressed interest in. That way,
- // `staticcheck -checks=SA1000` won't complain about an
- // unmatched ignore for an unrelated check.
- if allowedAnalyzers[c] {
- return true
- }
- }
-
- return false
- }
-
- ignores, moreProblems := parseDirectives(res.Directives)
-
- for _, ig := range ignores {
- for i := range problems {
- p := &problems[i]
- if ig.Match(*p) {
- p.Severity = Ignored
- }
- }
-
- if ig, ok := ig.(*lineIgnore); ok && !ig.Matched && couldveMatched(ig) {
- p := Problem{
- Diagnostic: runner.Diagnostic{
- Position: ig.Pos,
- Message: "this linter directive didn't match anything; should it be removed?",
- Category: "",
- },
- }
- moreProblems = append(moreProblems, p)
- }
- }
-
- return append(problems, moreProblems...), nil
-}
-
-func NewLinter(cfg config.Config) (*Linter, error) {
- r, err := runner.New(cfg)
- if err != nil {
- return nil, err
- }
- return &Linter{
- Config: cfg,
- Runner: r,
- }, nil
-}
-
-func (l *Linter) SetGoVersion(n int) {
- l.Runner.GoVersion = n
-}
-
-func (l *Linter) Lint(cfg *packages.Config, patterns []string) (problems []Problem, warnings []string, err error) {
- results, err := l.Runner.Run(cfg, l.Checkers, patterns)
- if err != nil {
- return nil, nil, err
- }
-
- if len(results) == 0 && err == nil {
- // TODO(dh): emulate Go's behavior more closely once we have
- // access to go list's Match field.
- fmt.Fprintf(os.Stderr, "warning: %q matched no packages\n", patterns)
- }
-
- analyzerNames := make([]string, len(l.Checkers))
- for i, a := range l.Checkers {
- analyzerNames[i] = a.Name
- }
-
- used := map[unusedKey]bool{}
- var unuseds []unusedPair
- for _, res := range results {
- if len(res.Errors) > 0 && !res.Failed {
- panic("package has errors but isn't marked as failed")
- }
- if res.Failed {
- problems = append(problems, failed(res)...)
- } else {
- if res.Skipped {
- warnings = append(warnings, fmt.Sprintf("skipped package %s because it is too large", res.Package))
- continue
- }
-
- if !res.Initial {
- continue
- }
-
- allowedAnalyzers := FilterAnalyzerNames(analyzerNames, res.Config.Checks)
- resd, err := res.Load()
- if err != nil {
- return nil, nil, err
- }
- ps := success(allowedAnalyzers, resd)
- filtered, err := filterIgnored(ps, resd, allowedAnalyzers)
- if err != nil {
- return nil, nil, err
- }
- problems = append(problems, filtered...)
-
- for _, obj := range resd.Unused.Used {
- // FIXME(dh): pick the object whose filename does not include $GOROOT
- key := unusedKey{
- pkgPath: res.Package.PkgPath,
- base: filepath.Base(obj.Position.Filename),
- line: obj.Position.Line,
- name: obj.Name,
- }
- used[key] = true
- }
-
- if allowedAnalyzers["U1000"] {
- for _, obj := range resd.Unused.Unused {
- key := unusedKey{
- pkgPath: res.Package.PkgPath,
- base: filepath.Base(obj.Position.Filename),
- line: obj.Position.Line,
- name: obj.Name,
- }
- unuseds = append(unuseds, unusedPair{key, obj})
- if _, ok := used[key]; !ok {
- used[key] = false
- }
- }
- }
- }
- }
-
- for _, uo := range unuseds {
- if used[uo.key] {
- continue
- }
- if uo.obj.InGenerated {
- continue
- }
- problems = append(problems, Problem{
- Diagnostic: runner.Diagnostic{
- Position: uo.obj.DisplayPosition,
- Message: fmt.Sprintf("%s %s is unused", uo.obj.Kind, uo.obj.Name),
- Category: "U1000",
- },
- })
- }
-
- if len(problems) == 0 {
- return nil, warnings, nil
- }
-
- sort.Slice(problems, func(i, j int) bool {
- pi := problems[i].Position
- pj := problems[j].Position
-
- if pi.Filename != pj.Filename {
- return pi.Filename < pj.Filename
- }
- if pi.Line != pj.Line {
- return pi.Line < pj.Line
- }
- if pi.Column != pj.Column {
- return pi.Column < pj.Column
- }
-
- return problems[i].Message < problems[j].Message
- })
-
- var out []Problem
- out = append(out, problems[0])
- for i, p := range problems[1:] {
- // We may encounter duplicate problems because one file
- // can be part of many packages.
- if !problems[i].equal(p) {
- out = append(out, p)
- }
- }
- return out, warnings, nil
-}
-
-func FilterAnalyzerNames(analyzers []string, checks []string) map[string]bool {
- allowedChecks := map[string]bool{}
-
- for _, check := range checks {
- b := true
- if len(check) > 1 && check[0] == '-' {
- b = false
- check = check[1:]
- }
- if check == "*" || check == "all" {
- // Match all
- for _, c := range analyzers {
- allowedChecks[c] = b
- }
- } else if strings.HasSuffix(check, "*") {
- // Glob
- prefix := check[:len(check)-1]
- isCat := strings.IndexFunc(prefix, func(r rune) bool { return unicode.IsNumber(r) }) == -1
-
- for _, a := range analyzers {
- idx := strings.IndexFunc(a, func(r rune) bool { return unicode.IsNumber(r) })
- if isCat {
- // Glob is S*, which should match S1000 but not SA1000
- cat := a[:idx]
- if prefix == cat {
- allowedChecks[a] = b
- }
- } else {
- // Glob is S1*
- if strings.HasPrefix(a, prefix) {
- allowedChecks[a] = b
- }
- }
- }
- } else {
- // Literal check name
- allowedChecks[check] = b
- }
- }
- return allowedChecks
-}
-
-var posRe = regexp.MustCompile(`^(.+?):(\d+)(?::(\d+)?)?`)
-
-func parsePos(pos string) (token.Position, int, error) {
- if pos == "-" || pos == "" {
- return token.Position{}, 0, nil
- }
- parts := posRe.FindStringSubmatch(pos)
- if parts == nil {
- return token.Position{}, 0, fmt.Errorf("malformed position %q", pos)
- }
- file := parts[1]
- line, _ := strconv.Atoi(parts[2])
- col, _ := strconv.Atoi(parts[3])
- return token.Position{
- Filename: file,
- Line: line,
- Column: col,
- }, len(parts[0]), nil
-}
diff --git a/lint/lintutil/util.go b/lint/lintutil/util.go
deleted file mode 100644
index 98e3fa43c..000000000
--- a/lint/lintutil/util.go
+++ /dev/null
@@ -1,443 +0,0 @@
-// Copyright (c) 2013 The Go Authors. All rights reserved.
-//
-// Use of this source code is governed by a BSD-style
-// license that can be found in the LICENSE file or at
-// https://developers.google.com/open-source/licenses/bsd.
-
-// Package lintutil provides helpers for writing linter command lines.
-package lintutil // import "honnef.co/go/tools/lint/lintutil"
-
-import (
- "crypto/sha256"
- "errors"
- "flag"
- "fmt"
- "go/build"
- "go/token"
- "io"
- "log"
- "os"
- "os/signal"
- "regexp"
- "runtime"
- "runtime/pprof"
- "strconv"
- "strings"
- "sync"
- "time"
-
- "honnef.co/go/tools/config"
- "honnef.co/go/tools/internal/cache"
- "honnef.co/go/tools/lint"
- "honnef.co/go/tools/lint/lintutil/format"
- "honnef.co/go/tools/loader"
- "honnef.co/go/tools/runner"
- "honnef.co/go/tools/version"
-
- "golang.org/x/tools/go/analysis"
- "golang.org/x/tools/go/buildutil"
- "golang.org/x/tools/go/packages"
-)
-
-func newVersionFlag() flag.Getter {
- tags := build.Default.ReleaseTags
- v := tags[len(tags)-1][2:]
- version := new(VersionFlag)
- if err := version.Set(v); err != nil {
- panic(fmt.Sprintf("internal error: %s", err))
- }
- return version
-}
-
-type VersionFlag int
-
-func (v *VersionFlag) String() string {
- return fmt.Sprintf("1.%d", *v)
-}
-
-func (v *VersionFlag) Set(s string) error {
- if len(s) < 3 {
- return errors.New("invalid Go version")
- }
- if s[0] != '1' {
- return errors.New("invalid Go version")
- }
- if s[1] != '.' {
- return errors.New("invalid Go version")
- }
- i, err := strconv.Atoi(s[2:])
- *v = VersionFlag(i)
- return err
-}
-
-func (v *VersionFlag) Get() interface{} {
- return int(*v)
-}
-
-func usage(name string, flags *flag.FlagSet) func() {
- return func() {
- fmt.Fprintf(os.Stderr, "Usage of %s:\n", name)
- fmt.Fprintf(os.Stderr, "\t%s [flags] # runs on package in current directory\n", name)
- fmt.Fprintf(os.Stderr, "\t%s [flags] packages\n", name)
- fmt.Fprintf(os.Stderr, "\t%s [flags] directory\n", name)
- fmt.Fprintf(os.Stderr, "\t%s [flags] files... # must be a single package\n", name)
- fmt.Fprintf(os.Stderr, "Flags:\n")
- flags.PrintDefaults()
- }
-}
-
-type list []string
-
-func (list *list) String() string {
- return `"` + strings.Join(*list, ",") + `"`
-}
-
-func (list *list) Set(s string) error {
- if s == "" {
- *list = nil
- return nil
- }
-
- *list = strings.Split(s, ",")
- return nil
-}
-
-func FlagSet(name string) *flag.FlagSet {
- flags := flag.NewFlagSet("", flag.ExitOnError)
- flags.Usage = usage(name, flags)
- flags.String("tags", "", "List of `build tags`")
- flags.Bool("tests", true, "Include tests")
- flags.Bool("version", false, "Print version and exit")
- flags.Bool("show-ignored", false, "Don't filter ignored problems")
- flags.String("f", "text", "Output `format` (valid choices are 'stylish', 'text' and 'json')")
- flags.String("explain", "", "Print description of `check`")
-
- flags.String("debug.cpuprofile", "", "Write CPU profile to `file`")
- flags.String("debug.memprofile", "", "Write memory profile to `file`")
- flags.Bool("debug.version", false, "Print detailed version information about this program")
- flags.Bool("debug.no-compile-errors", false, "Don't print compile errors")
- flags.String("debug.measure-analyzers", "", "Write analysis measurements to `file`. `file` will be opened for appending if it already exists.")
-
- checks := list{"inherit"}
- fail := list{"all"}
- flags.Var(&checks, "checks", "Comma-separated list of `checks` to enable.")
- flags.Var(&fail, "fail", "Comma-separated list of `checks` that can cause a non-zero exit status.")
-
- tags := build.Default.ReleaseTags
- v := tags[len(tags)-1][2:]
- version := new(VersionFlag)
- if err := version.Set(v); err != nil {
- panic(fmt.Sprintf("internal error: %s", err))
- }
-
- flags.Var(version, "go", "Target Go `version` in the format '1.x'")
- return flags
-}
-
-func findCheck(cs []*analysis.Analyzer, check string) (*analysis.Analyzer, bool) {
- for _, c := range cs {
- if c.Name == check {
- return c, true
- }
- }
- return nil, false
-}
-
-func ProcessFlagSet(cs []*analysis.Analyzer, fs *flag.FlagSet) {
- tags := fs.Lookup("tags").Value.(flag.Getter).Get().(string)
- tests := fs.Lookup("tests").Value.(flag.Getter).Get().(bool)
- goVersion := fs.Lookup("go").Value.(flag.Getter).Get().(int)
- formatter := fs.Lookup("f").Value.(flag.Getter).Get().(string)
- printVersion := fs.Lookup("version").Value.(flag.Getter).Get().(bool)
- showIgnored := fs.Lookup("show-ignored").Value.(flag.Getter).Get().(bool)
- explain := fs.Lookup("explain").Value.(flag.Getter).Get().(string)
-
- cpuProfile := fs.Lookup("debug.cpuprofile").Value.(flag.Getter).Get().(string)
- memProfile := fs.Lookup("debug.memprofile").Value.(flag.Getter).Get().(string)
- debugVersion := fs.Lookup("debug.version").Value.(flag.Getter).Get().(bool)
- debugNoCompile := fs.Lookup("debug.no-compile-errors").Value.(flag.Getter).Get().(bool)
-
- var measureAnalyzers func(analysis *analysis.Analyzer, pkg *loader.PackageSpec, d time.Duration)
- if path := fs.Lookup("debug.measure-analyzers").Value.(flag.Getter).Get().(string); path != "" {
- f, err := os.OpenFile(path, os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0600)
- if err != nil {
- log.Fatal(err)
- }
-
- mu := &sync.Mutex{}
- measureAnalyzers = func(analysis *analysis.Analyzer, pkg *loader.PackageSpec, d time.Duration) {
- mu.Lock()
- defer mu.Unlock()
- // FIXME(dh): print pkg.ID
- if _, err := fmt.Fprintf(f, "%s\t%s\t%d\n", analysis.Name, pkg, d.Nanoseconds()); err != nil {
- log.Println("error writing analysis measurements:", err)
- }
- }
- }
-
- cfg := config.Config{}
- cfg.Checks = *fs.Lookup("checks").Value.(*list)
-
- exit := func(code int) {
- if cpuProfile != "" {
- pprof.StopCPUProfile()
- }
- if memProfile != "" {
- f, err := os.Create(memProfile)
- if err != nil {
- panic(err)
- }
- runtime.GC()
- pprof.WriteHeapProfile(f)
- }
- os.Exit(code)
- }
- if cpuProfile != "" {
- f, err := os.Create(cpuProfile)
- if err != nil {
- log.Fatal(err)
- }
- pprof.StartCPUProfile(f)
- }
-
- if debugVersion {
- version.Verbose()
- exit(0)
- }
-
- if printVersion {
- version.Print()
- exit(0)
- }
-
- // Validate that the tags argument is well-formed. go/packages
- // doesn't detect malformed build flags and returns unhelpful
- // errors.
- tf := buildutil.TagsFlag{}
- if err := tf.Set(tags); err != nil {
- fmt.Fprintln(os.Stderr, fmt.Errorf("invalid value %q for flag -tags: %s", tags, err))
- exit(1)
- }
-
- if explain != "" {
- var haystack []*analysis.Analyzer
- haystack = append(haystack, cs...)
- check, ok := findCheck(haystack, explain)
- if !ok {
- fmt.Fprintln(os.Stderr, "Couldn't find check", explain)
- exit(1)
- }
- if check.Doc == "" {
- fmt.Fprintln(os.Stderr, explain, "has no documentation")
- exit(1)
- }
- fmt.Println(check.Doc)
- exit(0)
- }
-
- var f format.Formatter
- switch formatter {
- case "text":
- f = format.Text{W: os.Stdout}
- case "stylish":
- f = &format.Stylish{W: os.Stdout}
- case "json":
- f = format.JSON{W: os.Stdout}
- default:
- fmt.Fprintf(os.Stderr, "unsupported output format %q\n", formatter)
- exit(2)
- }
-
- ps, warnings, err := doLint(cs, fs.Args(), &Options{
- Tags: tags,
- LintTests: tests,
- GoVersion: goVersion,
- Config: cfg,
- PrintAnalyzerMeasurement: measureAnalyzers,
- })
- if err != nil {
- fmt.Fprintln(os.Stderr, err)
- exit(1)
- }
-
- for _, w := range warnings {
- fmt.Fprintln(os.Stderr, "warning:", w)
- }
-
- var (
- numErrors int
- numWarnings int
- numIgnored int
- )
-
- fail := *fs.Lookup("fail").Value.(*list)
- analyzerNames := make([]string, len(cs))
- for i, a := range cs {
- analyzerNames[i] = a.Name
- }
- shouldExit := lint.FilterAnalyzerNames(analyzerNames, fail)
- shouldExit["compile"] = true
-
- for _, p := range ps {
- if p.Category == "compile" && debugNoCompile {
- continue
- }
- if p.Severity == lint.Ignored && !showIgnored {
- numIgnored++
- continue
- }
- if shouldExit[p.Category] {
- numErrors++
- } else {
- p.Severity = lint.Warning
- numWarnings++
- }
- f.Format(p)
- }
- if f, ok := f.(format.Statter); ok {
- f.Stats(len(ps), numErrors, numWarnings, numIgnored)
- }
-
- if f, ok := f.(format.DocumentationMentioner); ok && (numErrors > 0 || numWarnings > 0) && len(os.Args) > 0 {
- f.MentionCheckDocumentation(os.Args[0])
- }
-
- if numErrors > 0 {
- exit(1)
- }
- exit(0)
-}
-
-type Options struct {
- Config config.Config
-
- Tags string
- LintTests bool
- GoVersion int
- PrintAnalyzerMeasurement func(analysis *analysis.Analyzer, pkg *loader.PackageSpec, d time.Duration)
-}
-
-func computeSalt() ([]byte, error) {
- if version.Version != "devel" {
- return []byte(version.Version), nil
- }
- p, err := os.Executable()
- if err != nil {
- return nil, err
- }
- f, err := os.Open(p)
- if err != nil {
- return nil, err
- }
- defer f.Close()
- h := sha256.New()
- if _, err := io.Copy(h, f); err != nil {
- return nil, err
- }
- return h.Sum(nil), nil
-}
-
-func doLint(cs []*analysis.Analyzer, paths []string, opt *Options) ([]lint.Problem, []string, error) {
- salt, err := computeSalt()
- if err != nil {
- return nil, nil, fmt.Errorf("could not compute salt for cache: %s", err)
- }
- cache.SetSalt(salt)
-
- if opt == nil {
- opt = &Options{}
- }
-
- l, err := lint.NewLinter(opt.Config)
- if err != nil {
- return nil, nil, err
- }
- l.Checkers = cs
- l.SetGoVersion(opt.GoVersion)
- l.Runner.Stats.PrintAnalyzerMeasurement = opt.PrintAnalyzerMeasurement
-
- cfg := &packages.Config{}
- if opt.LintTests {
- cfg.Tests = true
- }
- if opt.Tags != "" {
- cfg.BuildFlags = append(cfg.BuildFlags, "-tags", opt.Tags)
- }
-
- printStats := func() {
- // Individual stats are read atomically, but overall there
- // is no synchronisation. For printing rough progress
- // information, this doesn't matter.
- switch l.Runner.Stats.State() {
- case runner.StateInitializing:
- fmt.Fprintln(os.Stderr, "Status: initializing")
- case runner.StateLoadPackageGraph:
- fmt.Fprintln(os.Stderr, "Status: loading package graph")
- case runner.StateBuildActionGraph:
- fmt.Fprintln(os.Stderr, "Status: building action graph")
- case runner.StateProcessing:
- fmt.Fprintf(os.Stderr, "Packages: %d/%d initial, %d/%d total; Workers: %d/%d\n",
- l.Runner.Stats.ProcessedInitialPackages(),
- l.Runner.Stats.InitialPackages(),
- l.Runner.Stats.ProcessedPackages(),
- l.Runner.Stats.TotalPackages(),
- l.Runner.ActiveWorkers(),
- l.Runner.TotalWorkers(),
- )
- case runner.StateFinalizing:
- fmt.Fprintln(os.Stderr, "Status: finalizing")
- }
- }
- if len(infoSignals) > 0 {
- ch := make(chan os.Signal, 1)
- signal.Notify(ch, infoSignals...)
- defer signal.Stop(ch)
- go func() {
- for range ch {
- printStats()
- }
- }()
- }
- return l.Lint(cfg, paths)
-}
-
-var posRe = regexp.MustCompile(`^(.+?):(\d+)(?::(\d+)?)?$`)
-
-func parsePos(pos string) token.Position {
- if pos == "-" || pos == "" {
- return token.Position{}
- }
- parts := posRe.FindStringSubmatch(pos)
- if parts == nil {
- panic(fmt.Sprintf("internal error: malformed position %q", pos))
- }
- file := parts[1]
- line, _ := strconv.Atoi(parts[2])
- col, _ := strconv.Atoi(parts[3])
- return token.Position{
- Filename: file,
- Line: line,
- Column: col,
- }
-}
-
-func InitializeAnalyzers(docs map[string]*lint.Documentation, analyzers map[string]*analysis.Analyzer) map[string]*analysis.Analyzer {
- out := make(map[string]*analysis.Analyzer, len(analyzers))
- for k, v := range analyzers {
- vc := *v
- out[k] = &vc
-
- vc.Name = k
- doc, ok := docs[k]
- if !ok {
- panic(fmt.Sprintf("missing documentation for check %s", k))
- }
- vc.Doc = doc.String()
- if vc.Flags.Usage == nil {
- fs := flag.NewFlagSet("", flag.PanicOnError)
- fs.Var(newVersionFlag(), "go", "Target Go version")
- vc.Flags = *fs
- }
- }
- return out
-}
diff --git a/lintcmd/cmd.go b/lintcmd/cmd.go
new file mode 100644
index 000000000..f44b3c726
--- /dev/null
+++ b/lintcmd/cmd.go
@@ -0,0 +1,796 @@
+package lintcmd
+
+import (
+ "crypto/sha256"
+ "flag"
+ "fmt"
+ "go/build"
+ "go/token"
+ "io"
+ "log"
+ "os"
+ "os/signal"
+ "path/filepath"
+ "regexp"
+ "runtime"
+ "runtime/pprof"
+ "sort"
+ "strconv"
+ "strings"
+ "sync"
+ "time"
+ "unicode"
+
+ "honnef.co/go/tools/analysis/lint"
+ "honnef.co/go/tools/config"
+ "honnef.co/go/tools/go/loader"
+ "honnef.co/go/tools/internal/cache"
+ "honnef.co/go/tools/lintcmd/runner"
+ "honnef.co/go/tools/lintcmd/version"
+ "honnef.co/go/tools/unused"
+
+ "golang.org/x/tools/go/analysis"
+ "golang.org/x/tools/go/buildutil"
+ "golang.org/x/tools/go/packages"
+)
+
+type ignore interface {
+ Match(p problem) bool
+}
+
+type lineIgnore struct {
+ File string
+ Line int
+ Checks []string
+ Matched bool
+ Pos token.Position
+}
+
+func (li *lineIgnore) Match(p problem) bool {
+ pos := p.Position
+ if pos.Filename != li.File || pos.Line != li.Line {
+ return false
+ }
+ for _, c := range li.Checks {
+ if m, _ := filepath.Match(c, p.Category); m {
+ li.Matched = true
+ return true
+ }
+ }
+ return false
+}
+
+func (li *lineIgnore) String() string {
+ matched := "not matched"
+ if li.Matched {
+ matched = "matched"
+ }
+ return fmt.Sprintf("%s:%d %s (%s)", li.File, li.Line, strings.Join(li.Checks, ", "), matched)
+}
+
+type fileIgnore struct {
+ File string
+ Checks []string
+}
+
+func (fi *fileIgnore) Match(p problem) bool {
+ if p.Position.Filename != fi.File {
+ return false
+ }
+ for _, c := range fi.Checks {
+ if m, _ := filepath.Match(c, p.Category); m {
+ return true
+ }
+ }
+ return false
+}
+
+type severity uint8
+
+const (
+ severityError severity = iota
+ severityWarning
+ severityIgnored
+)
+
+func (s severity) String() string {
+ switch s {
+ case severityError:
+ return "error"
+ case severityWarning:
+ return "warning"
+ case severityIgnored:
+ return "ignored"
+ default:
+ return fmt.Sprintf("Severity(%d)", s)
+ }
+}
+
+// problem represents a problem in some source code.
+type problem struct {
+ runner.Diagnostic
+ Severity severity
+}
+
+func (p problem) equal(o problem) bool {
+ return p.Position == o.Position &&
+ p.End == o.End &&
+ p.Message == o.Message &&
+ p.Category == o.Category &&
+ p.Severity == o.Severity
+}
+
+func (p *problem) String() string {
+ return fmt.Sprintf("%s (%s)", p.Message, p.Category)
+}
+
+// A linter lints Go source code.
+type linter struct {
+ Checkers []*analysis.Analyzer
+ Config config.Config
+ Runner *runner.Runner
+}
+
+func failed(res runner.Result) []problem {
+ var problems []problem
+
+ for _, e := range res.Errors {
+ switch e := e.(type) {
+ case packages.Error:
+ msg := e.Msg
+ if len(msg) != 0 && msg[0] == '\n' {
+ // TODO(dh): See https://github.com/golang/go/issues/32363
+ msg = msg[1:]
+ }
+
+ var posn token.Position
+ if e.Pos == "" {
+ // Under certain conditions (malformed package
+ // declarations, multiple packages in the same
+ // directory), go list emits an error on stderr
+ // instead of JSON. Those errors do not have
+ // associated position information in
+ // go/packages.Error, even though the output on
+ // stderr may contain it.
+ if p, n, err := parsePos(msg); err == nil {
+ if abs, err := filepath.Abs(p.Filename); err == nil {
+ p.Filename = abs
+ }
+ posn = p
+ msg = msg[n+2:]
+ }
+ } else {
+ var err error
+ posn, _, err = parsePos(e.Pos)
+ if err != nil {
+ panic(fmt.Sprintf("internal error: %s", e))
+ }
+ }
+ p := problem{
+ Diagnostic: runner.Diagnostic{
+ Position: posn,
+ Message: msg,
+ Category: "compile",
+ },
+ Severity: severityError,
+ }
+ problems = append(problems, p)
+ case error:
+ p := problem{
+ Diagnostic: runner.Diagnostic{
+ Position: token.Position{},
+ Message: e.Error(),
+ Category: "compile",
+ },
+ Severity: severityError,
+ }
+ problems = append(problems, p)
+ }
+ }
+
+ return problems
+}
+
+type unusedKey struct {
+ pkgPath string
+ base string
+ line int
+ name string
+}
+
+type unusedPair struct {
+ key unusedKey
+ obj unused.SerializedObject
+}
+
+func success(allowedChecks map[string]bool, res runner.ResultData) []problem {
+ diags := res.Diagnostics
+ var problems []problem
+ for _, diag := range diags {
+ if !allowedChecks[diag.Category] {
+ continue
+ }
+ problems = append(problems, problem{Diagnostic: diag})
+ }
+ return problems
+}
+
+func filterIgnored(problems []problem, res runner.ResultData, allowedAnalyzers map[string]bool) ([]problem, error) {
+ couldveMatched := func(ig *lineIgnore) bool {
+ for _, c := range ig.Checks {
+ if c == "U1000" {
+ // We never want to flag ignores for U1000,
+ // because U1000 isn't local to a single
+ // package. For example, an identifier may
+ // only be used by tests, in which case an
+ // ignore would only fire when not analyzing
+ // tests. To avoid spurious "useless ignore"
+ // warnings, just never flag U1000.
+ return false
+ }
+
+ // Even though the runner always runs all analyzers, we
+ // still only flag unmatched ignores for the set of
+ // analyzers the user has expressed interest in. That way,
+ // `staticcheck -checks=SA1000` won't complain about an
+ // unmatched ignore for an unrelated check.
+ if allowedAnalyzers[c] {
+ return true
+ }
+ }
+
+ return false
+ }
+
+ ignores, moreProblems := parseDirectives(res.Directives)
+
+ for _, ig := range ignores {
+ for i := range problems {
+ p := &problems[i]
+ if ig.Match(*p) {
+ p.Severity = severityIgnored
+ }
+ }
+
+ if ig, ok := ig.(*lineIgnore); ok && !ig.Matched && couldveMatched(ig) {
+ p := problem{
+ Diagnostic: runner.Diagnostic{
+ Position: ig.Pos,
+ Message: "this linter directive didn't match anything; should it be removed?",
+ Category: "",
+ },
+ }
+ moreProblems = append(moreProblems, p)
+ }
+ }
+
+ return append(problems, moreProblems...), nil
+}
+
+func newLinter(cfg config.Config) (*linter, error) {
+ r, err := runner.New(cfg)
+ if err != nil {
+ return nil, err
+ }
+ return &linter{
+ Config: cfg,
+ Runner: r,
+ }, nil
+}
+
+func (l *linter) SetGoVersion(n int) {
+ l.Runner.GoVersion = n
+}
+
+func (l *linter) Lint(cfg *packages.Config, patterns []string) (problems []problem, warnings []string, err error) {
+ results, err := l.Runner.Run(cfg, l.Checkers, patterns)
+ if err != nil {
+ return nil, nil, err
+ }
+
+ if len(results) == 0 && err == nil {
+ // TODO(dh): emulate Go's behavior more closely once we have
+ // access to go list's Match field.
+ fmt.Fprintf(os.Stderr, "warning: %q matched no packages\n", patterns)
+ }
+
+ analyzerNames := make([]string, len(l.Checkers))
+ for i, a := range l.Checkers {
+ analyzerNames[i] = a.Name
+ }
+
+ used := map[unusedKey]bool{}
+ var unuseds []unusedPair
+ for _, res := range results {
+ if len(res.Errors) > 0 && !res.Failed {
+ panic("package has errors but isn't marked as failed")
+ }
+ if res.Failed {
+ problems = append(problems, failed(res)...)
+ } else {
+ if res.Skipped {
+ warnings = append(warnings, fmt.Sprintf("skipped package %s because it is too large", res.Package))
+ continue
+ }
+
+ if !res.Initial {
+ continue
+ }
+
+ allowedAnalyzers := filterAnalyzerNames(analyzerNames, res.Config.Checks)
+ resd, err := res.Load()
+ if err != nil {
+ return nil, nil, err
+ }
+ ps := success(allowedAnalyzers, resd)
+ filtered, err := filterIgnored(ps, resd, allowedAnalyzers)
+ if err != nil {
+ return nil, nil, err
+ }
+ problems = append(problems, filtered...)
+
+ for _, obj := range resd.Unused.Used {
+ // FIXME(dh): pick the object whose filename does not include $GOROOT
+ key := unusedKey{
+ pkgPath: res.Package.PkgPath,
+ base: filepath.Base(obj.Position.Filename),
+ line: obj.Position.Line,
+ name: obj.Name,
+ }
+ used[key] = true
+ }
+
+ if allowedAnalyzers["U1000"] {
+ for _, obj := range resd.Unused.Unused {
+ key := unusedKey{
+ pkgPath: res.Package.PkgPath,
+ base: filepath.Base(obj.Position.Filename),
+ line: obj.Position.Line,
+ name: obj.Name,
+ }
+ unuseds = append(unuseds, unusedPair{key, obj})
+ if _, ok := used[key]; !ok {
+ used[key] = false
+ }
+ }
+ }
+ }
+ }
+
+ for _, uo := range unuseds {
+ if used[uo.key] {
+ continue
+ }
+ if uo.obj.InGenerated {
+ continue
+ }
+ problems = append(problems, problem{
+ Diagnostic: runner.Diagnostic{
+ Position: uo.obj.DisplayPosition,
+ Message: fmt.Sprintf("%s %s is unused", uo.obj.Kind, uo.obj.Name),
+ Category: "U1000",
+ },
+ })
+ }
+
+ if len(problems) == 0 {
+ return nil, warnings, nil
+ }
+
+ sort.Slice(problems, func(i, j int) bool {
+ pi := problems[i].Position
+ pj := problems[j].Position
+
+ if pi.Filename != pj.Filename {
+ return pi.Filename < pj.Filename
+ }
+ if pi.Line != pj.Line {
+ return pi.Line < pj.Line
+ }
+ if pi.Column != pj.Column {
+ return pi.Column < pj.Column
+ }
+
+ return problems[i].Message < problems[j].Message
+ })
+
+ var out []problem
+ out = append(out, problems[0])
+ for i, p := range problems[1:] {
+ // We may encounter duplicate problems because one file
+ // can be part of many packages.
+ if !problems[i].equal(p) {
+ out = append(out, p)
+ }
+ }
+ return out, warnings, nil
+}
+
+func filterAnalyzerNames(analyzers []string, checks []string) map[string]bool {
+ allowedChecks := map[string]bool{}
+
+ for _, check := range checks {
+ b := true
+ if len(check) > 1 && check[0] == '-' {
+ b = false
+ check = check[1:]
+ }
+ if check == "*" || check == "all" {
+ // Match all
+ for _, c := range analyzers {
+ allowedChecks[c] = b
+ }
+ } else if strings.HasSuffix(check, "*") {
+ // Glob
+ prefix := check[:len(check)-1]
+ isCat := strings.IndexFunc(prefix, func(r rune) bool { return unicode.IsNumber(r) }) == -1
+
+ for _, a := range analyzers {
+ idx := strings.IndexFunc(a, func(r rune) bool { return unicode.IsNumber(r) })
+ if isCat {
+ // Glob is S*, which should match S1000 but not SA1000
+ cat := a[:idx]
+ if prefix == cat {
+ allowedChecks[a] = b
+ }
+ } else {
+ // Glob is S1*
+ if strings.HasPrefix(a, prefix) {
+ allowedChecks[a] = b
+ }
+ }
+ }
+ } else {
+ // Literal check name
+ allowedChecks[check] = b
+ }
+ }
+ return allowedChecks
+}
+
+var posRe = regexp.MustCompile(`^(.+?):(\d+)(?::(\d+)?)?`)
+
+func parsePos(pos string) (token.Position, int, error) {
+ if pos == "-" || pos == "" {
+ return token.Position{}, 0, nil
+ }
+ parts := posRe.FindStringSubmatch(pos)
+ if parts == nil {
+ return token.Position{}, 0, fmt.Errorf("internal error: malformed position %q", pos)
+ }
+ file := parts[1]
+ line, _ := strconv.Atoi(parts[2])
+ col, _ := strconv.Atoi(parts[3])
+ return token.Position{
+ Filename: file,
+ Line: line,
+ Column: col,
+ }, len(parts[0]), nil
+}
+
+func usage(name string, flags *flag.FlagSet) func() {
+ return func() {
+ fmt.Fprintf(os.Stderr, "Usage of %s:\n", name)
+ fmt.Fprintf(os.Stderr, "\t%s [flags] # runs on package in current directory\n", name)
+ fmt.Fprintf(os.Stderr, "\t%s [flags] packages\n", name)
+ fmt.Fprintf(os.Stderr, "\t%s [flags] directory\n", name)
+ fmt.Fprintf(os.Stderr, "\t%s [flags] files... # must be a single package\n", name)
+ fmt.Fprintf(os.Stderr, "Flags:\n")
+ flags.PrintDefaults()
+ }
+}
+
+type list []string
+
+func (list *list) String() string {
+ return `"` + strings.Join(*list, ",") + `"`
+}
+
+func (list *list) Set(s string) error {
+ if s == "" {
+ *list = nil
+ return nil
+ }
+
+ *list = strings.Split(s, ",")
+ return nil
+}
+
+func FlagSet(name string) *flag.FlagSet {
+ flags := flag.NewFlagSet("", flag.ExitOnError)
+ flags.Usage = usage(name, flags)
+ flags.String("tags", "", "List of `build tags`")
+ flags.Bool("tests", true, "Include tests")
+ flags.Bool("version", false, "Print version and exit")
+ flags.Bool("show-ignored", false, "Don't filter ignored problems")
+ flags.String("f", "text", "Output `format` (valid choices are 'stylish', 'text' and 'json')")
+ flags.String("explain", "", "Print description of `check`")
+
+ flags.String("debug.cpuprofile", "", "Write CPU profile to `file`")
+ flags.String("debug.memprofile", "", "Write memory profile to `file`")
+ flags.Bool("debug.version", false, "Print detailed version information about this program")
+ flags.Bool("debug.no-compile-errors", false, "Don't print compile errors")
+ flags.String("debug.measure-analyzers", "", "Write analysis measurements to `file`. `file` will be opened for appending if it already exists.")
+
+ checks := list{"inherit"}
+ fail := list{"all"}
+ flags.Var(&checks, "checks", "Comma-separated list of `checks` to enable.")
+ flags.Var(&fail, "fail", "Comma-separated list of `checks` that can cause a non-zero exit status.")
+
+ tags := build.Default.ReleaseTags
+ v := tags[len(tags)-1][2:]
+ version := new(lint.VersionFlag)
+ if err := version.Set(v); err != nil {
+ panic(fmt.Sprintf("internal error: %s", err))
+ }
+
+ flags.Var(version, "go", "Target Go `version` in the format '1.x'")
+ return flags
+}
+
+func findCheck(cs []*analysis.Analyzer, check string) (*analysis.Analyzer, bool) {
+ for _, c := range cs {
+ if c.Name == check {
+ return c, true
+ }
+ }
+ return nil, false
+}
+
+func ProcessFlagSet(cs []*analysis.Analyzer, fs *flag.FlagSet) {
+ tags := fs.Lookup("tags").Value.(flag.Getter).Get().(string)
+ tests := fs.Lookup("tests").Value.(flag.Getter).Get().(bool)
+ goVersion := fs.Lookup("go").Value.(flag.Getter).Get().(int)
+ theFormatter := fs.Lookup("f").Value.(flag.Getter).Get().(string)
+ printVersion := fs.Lookup("version").Value.(flag.Getter).Get().(bool)
+ showIgnored := fs.Lookup("show-ignored").Value.(flag.Getter).Get().(bool)
+ explain := fs.Lookup("explain").Value.(flag.Getter).Get().(string)
+
+ cpuProfile := fs.Lookup("debug.cpuprofile").Value.(flag.Getter).Get().(string)
+ memProfile := fs.Lookup("debug.memprofile").Value.(flag.Getter).Get().(string)
+ debugVersion := fs.Lookup("debug.version").Value.(flag.Getter).Get().(bool)
+ debugNoCompile := fs.Lookup("debug.no-compile-errors").Value.(flag.Getter).Get().(bool)
+
+ var measureAnalyzers func(analysis *analysis.Analyzer, pkg *loader.PackageSpec, d time.Duration)
+ if path := fs.Lookup("debug.measure-analyzers").Value.(flag.Getter).Get().(string); path != "" {
+ f, err := os.OpenFile(path, os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0600)
+ if err != nil {
+ log.Fatal(err)
+ }
+
+ mu := &sync.Mutex{}
+ measureAnalyzers = func(analysis *analysis.Analyzer, pkg *loader.PackageSpec, d time.Duration) {
+ mu.Lock()
+ defer mu.Unlock()
+ // FIXME(dh): print pkg.ID
+ if _, err := fmt.Fprintf(f, "%s\t%s\t%d\n", analysis.Name, pkg, d.Nanoseconds()); err != nil {
+ log.Println("error writing analysis measurements:", err)
+ }
+ }
+ }
+
+ cfg := config.Config{}
+ cfg.Checks = *fs.Lookup("checks").Value.(*list)
+
+ exit := func(code int) {
+ if cpuProfile != "" {
+ pprof.StopCPUProfile()
+ }
+ if memProfile != "" {
+ f, err := os.Create(memProfile)
+ if err != nil {
+ panic(err)
+ }
+ runtime.GC()
+ pprof.WriteHeapProfile(f)
+ }
+ os.Exit(code)
+ }
+ if cpuProfile != "" {
+ f, err := os.Create(cpuProfile)
+ if err != nil {
+ log.Fatal(err)
+ }
+ pprof.StartCPUProfile(f)
+ }
+
+ if debugVersion {
+ version.Verbose()
+ exit(0)
+ }
+
+ if printVersion {
+ version.Print()
+ exit(0)
+ }
+
+ // Validate that the tags argument is well-formed. go/packages
+ // doesn't detect malformed build flags and returns unhelpful
+ // errors.
+ tf := buildutil.TagsFlag{}
+ if err := tf.Set(tags); err != nil {
+ fmt.Fprintln(os.Stderr, fmt.Errorf("invalid value %q for flag -tags: %s", tags, err))
+ exit(1)
+ }
+
+ if explain != "" {
+ var haystack []*analysis.Analyzer
+ haystack = append(haystack, cs...)
+ check, ok := findCheck(haystack, explain)
+ if !ok {
+ fmt.Fprintln(os.Stderr, "Couldn't find check", explain)
+ exit(1)
+ }
+ if check.Doc == "" {
+ fmt.Fprintln(os.Stderr, explain, "has no documentation")
+ exit(1)
+ }
+ fmt.Println(check.Doc)
+ exit(0)
+ }
+
+ var f formatter
+ switch theFormatter {
+ case "text":
+ f = textFormatter{W: os.Stdout}
+ case "stylish":
+ f = &stylishFormatter{W: os.Stdout}
+ case "json":
+ f = jsonFormatter{W: os.Stdout}
+ default:
+ fmt.Fprintf(os.Stderr, "unsupported output format %q\n", theFormatter)
+ exit(2)
+ }
+
+ ps, warnings, err := doLint(cs, fs.Args(), &options{
+ Tags: tags,
+ LintTests: tests,
+ GoVersion: goVersion,
+ Config: cfg,
+ PrintAnalyzerMeasurement: measureAnalyzers,
+ })
+ if err != nil {
+ fmt.Fprintln(os.Stderr, err)
+ exit(1)
+ }
+
+ for _, w := range warnings {
+ fmt.Fprintln(os.Stderr, "warning:", w)
+ }
+
+ var (
+ numErrors int
+ numWarnings int
+ numIgnored int
+ )
+
+ fail := *fs.Lookup("fail").Value.(*list)
+ analyzerNames := make([]string, len(cs))
+ for i, a := range cs {
+ analyzerNames[i] = a.Name
+ }
+ shouldExit := filterAnalyzerNames(analyzerNames, fail)
+ shouldExit["compile"] = true
+
+ for _, p := range ps {
+ if p.Category == "compile" && debugNoCompile {
+ continue
+ }
+ if p.Severity == severityIgnored && !showIgnored {
+ numIgnored++
+ continue
+ }
+ if shouldExit[p.Category] {
+ numErrors++
+ } else {
+ p.Severity = severityWarning
+ numWarnings++
+ }
+ f.Format(p)
+ }
+ if f, ok := f.(statter); ok {
+ f.Stats(len(ps), numErrors, numWarnings, numIgnored)
+ }
+
+ if f, ok := f.(documentationMentioner); ok && (numErrors > 0 || numWarnings > 0) && len(os.Args) > 0 {
+ f.MentionCheckDocumentation(os.Args[0])
+ }
+
+ if numErrors > 0 {
+ exit(1)
+ }
+ exit(0)
+}
+
+type options struct {
+ Config config.Config
+
+ Tags string
+ LintTests bool
+ GoVersion int
+ PrintAnalyzerMeasurement func(analysis *analysis.Analyzer, pkg *loader.PackageSpec, d time.Duration)
+}
+
+func computeSalt() ([]byte, error) {
+ if version.Version != "devel" {
+ return []byte(version.Version), nil
+ }
+ p, err := os.Executable()
+ if err != nil {
+ return nil, err
+ }
+ f, err := os.Open(p)
+ if err != nil {
+ return nil, err
+ }
+ defer f.Close()
+ h := sha256.New()
+ if _, err := io.Copy(h, f); err != nil {
+ return nil, err
+ }
+ return h.Sum(nil), nil
+}
+
+func doLint(cs []*analysis.Analyzer, paths []string, opt *options) ([]problem, []string, error) {
+ salt, err := computeSalt()
+ if err != nil {
+ return nil, nil, fmt.Errorf("could not compute salt for cache: %s", err)
+ }
+ cache.SetSalt(salt)
+
+ if opt == nil {
+ opt = &options{}
+ }
+
+ l, err := newLinter(opt.Config)
+ if err != nil {
+ return nil, nil, err
+ }
+ l.Checkers = cs
+ l.SetGoVersion(opt.GoVersion)
+ l.Runner.Stats.PrintAnalyzerMeasurement = opt.PrintAnalyzerMeasurement
+
+ cfg := &packages.Config{}
+ if opt.LintTests {
+ cfg.Tests = true
+ }
+ if opt.Tags != "" {
+ cfg.BuildFlags = append(cfg.BuildFlags, "-tags", opt.Tags)
+ }
+
+ printStats := func() {
+ // Individual stats are read atomically, but overall there
+ // is no synchronisation. For printing rough progress
+ // information, this doesn't matter.
+ switch l.Runner.Stats.State() {
+ case runner.StateInitializing:
+ fmt.Fprintln(os.Stderr, "Status: initializing")
+ case runner.StateLoadPackageGraph:
+ fmt.Fprintln(os.Stderr, "Status: loading package graph")
+ case runner.StateBuildActionGraph:
+ fmt.Fprintln(os.Stderr, "Status: building action graph")
+ case runner.StateProcessing:
+ fmt.Fprintf(os.Stderr, "Packages: %d/%d initial, %d/%d total; Workers: %d/%d\n",
+ l.Runner.Stats.ProcessedInitialPackages(),
+ l.Runner.Stats.InitialPackages(),
+ l.Runner.Stats.ProcessedPackages(),
+ l.Runner.Stats.TotalPackages(),
+ l.Runner.ActiveWorkers(),
+ l.Runner.TotalWorkers(),
+ )
+ case runner.StateFinalizing:
+ fmt.Fprintln(os.Stderr, "Status: finalizing")
+ }
+ }
+ if len(infoSignals) > 0 {
+ ch := make(chan os.Signal, 1)
+ signal.Notify(ch, infoSignals...)
+ defer signal.Stop(ch)
+ go func() {
+ for range ch {
+ printStats()
+ }
+ }()
+ }
+ return l.Lint(cfg, paths)
+}
diff --git a/lint/lintutil/util_test.go b/lintcmd/cmd_test.go
similarity index 94%
rename from lint/lintutil/util_test.go
rename to lintcmd/cmd_test.go
index b348ba1bf..868260317 100644
--- a/lint/lintutil/util_test.go
+++ b/lintcmd/cmd_test.go
@@ -1,4 +1,4 @@
-package lintutil
+package lintcmd
import (
"go/token"
@@ -37,7 +37,7 @@ func TestParsePos(t *testing.T) {
}
for _, tt := range tests {
- res := parsePos(tt.in)
+ res, _, _ := parsePos(tt.in)
if res != tt.out {
t.Errorf("failed to parse %q correctly", tt.in)
}
diff --git a/lint/directives.go b/lintcmd/directives.go
similarity index 80%
rename from lint/directives.go
rename to lintcmd/directives.go
index 1ca8d5acf..e35b9c7ae 100644
--- a/lint/directives.go
+++ b/lintcmd/directives.go
@@ -1,15 +1,14 @@
-package lint
+package lintcmd
import (
"strings"
- "honnef.co/go/tools/facts"
- "honnef.co/go/tools/runner"
+ "honnef.co/go/tools/lintcmd/runner"
)
-func parseDirectives(dirs []facts.SerializedDirective) ([]ignore, []Problem) {
+func parseDirectives(dirs []runner.SerializedDirective) ([]ignore, []problem) {
var ignores []ignore
- var problems []Problem
+ var problems []problem
for _, dir := range dirs {
cmd := dir.Command
@@ -17,13 +16,13 @@ func parseDirectives(dirs []facts.SerializedDirective) ([]ignore, []Problem) {
switch cmd {
case "ignore", "file-ignore":
if len(args) < 2 {
- p := Problem{
+ p := problem{
Diagnostic: runner.Diagnostic{
Position: dir.NodePosition,
Message: "malformed linter directive; missing the required reason field?",
Category: "compile",
},
- Severity: Error,
+ Severity: severityError,
}
problems = append(problems, p)
continue
diff --git a/lint/lintutil/format/format.go b/lintcmd/format.go
similarity index 77%
rename from lint/lintutil/format/format.go
rename to lintcmd/format.go
index 60aa6165c..f3d95345a 100644
--- a/lint/lintutil/format/format.go
+++ b/lintcmd/format.go
@@ -1,5 +1,4 @@
-// Package format provides formatters for linter problems.
-package format
+package lintcmd
import (
"encoding/json"
@@ -9,8 +8,6 @@ import (
"os"
"path/filepath"
"text/tabwriter"
-
- "honnef.co/go/tools/lint"
)
func shortPath(path string) string {
@@ -38,50 +35,38 @@ func relativePositionString(pos token.Position) string {
return s
}
-type Statter interface {
+type statter interface {
Stats(total, errors, warnings, ignored int)
}
-type Formatter interface {
- Format(p lint.Problem)
+type formatter interface {
+ Format(p problem)
}
-type DocumentationMentioner interface {
+type documentationMentioner interface {
MentionCheckDocumentation(cmd string)
}
-type Text struct {
+type textFormatter struct {
W io.Writer
}
-func (o Text) Format(p lint.Problem) {
+func (o textFormatter) Format(p problem) {
fmt.Fprintf(o.W, "%s: %s\n", relativePositionString(p.Position), p.String())
for _, r := range p.Related {
fmt.Fprintf(o.W, "\t%s: %s\n", relativePositionString(r.Position), r.Message)
}
}
-func (o Text) MentionCheckDocumentation(cmd string) {
+func (o textFormatter) MentionCheckDocumentation(cmd string) {
fmt.Fprintf(o.W, "\nRun '%s -explain ' or visit https://staticcheck.io/docs/checks for documentation on checks.\n", cmd)
}
-type JSON struct {
+type jsonFormatter struct {
W io.Writer
}
-func severity(s lint.Severity) string {
- switch s {
- case lint.Error:
- return "error"
- case lint.Warning:
- return "warning"
- case lint.Ignored:
- return "ignored"
- }
- return ""
-}
-
-func (o JSON) Format(p lint.Problem) {
+func (o jsonFormatter) Format(p problem) {
type location struct {
File string `json:"file"`
Line int `json:"line"`
@@ -101,7 +86,7 @@ func (o JSON) Format(p lint.Problem) {
Related []related `json:"related,omitempty"`
}{
Code: p.Category,
- Severity: severity(p.Severity),
+ Severity: p.Severity.String(),
Location: location{
File: p.Position.Filename,
Line: p.Position.Line,
@@ -132,14 +117,14 @@ func (o JSON) Format(p lint.Problem) {
_ = json.NewEncoder(o.W).Encode(jp)
}
-type Stylish struct {
+type stylishFormatter struct {
W io.Writer
prevFile string
tw *tabwriter.Writer
}
-func (o *Stylish) Format(p lint.Problem) {
+func (o *stylishFormatter) Format(p problem) {
pos := p.Position
if pos.Filename == "" {
pos.Filename = "-"
@@ -160,11 +145,11 @@ func (o *Stylish) Format(p lint.Problem) {
}
}
-func (o *Stylish) MentionCheckDocumentation(cmd string) {
- Text{W: o.W}.MentionCheckDocumentation(cmd)
+func (o *stylishFormatter) MentionCheckDocumentation(cmd string) {
+ textFormatter{W: o.W}.MentionCheckDocumentation(cmd)
}
-func (o *Stylish) Stats(total, errors, warnings, ignored int) {
+func (o *stylishFormatter) Stats(total, errors, warnings, ignored int) {
if o.tw != nil {
o.tw.Flush()
fmt.Fprintln(o.W)
diff --git a/lint/lint_test.go b/lintcmd/lint_test.go
similarity index 93%
rename from lint/lint_test.go
rename to lintcmd/lint_test.go
index 08be73b30..1b302e77f 100644
--- a/lint/lint_test.go
+++ b/lintcmd/lint_test.go
@@ -1,4 +1,4 @@
-package lint
+package lintcmd
import (
"go/token"
@@ -9,9 +9,10 @@ import (
"strings"
"testing"
- "golang.org/x/tools/go/packages"
"honnef.co/go/tools/config"
- "honnef.co/go/tools/runner"
+ "honnef.co/go/tools/lintcmd/runner"
+
+ "golang.org/x/tools/go/packages"
)
func testdata() string {
@@ -22,8 +23,8 @@ func testdata() string {
return testdata
}
-func lintPackage(t *testing.T, name string) []Problem {
- l, err := NewLinter(config.Config{})
+func lintPackage(t *testing.T, name string) []problem {
+ l, err := newLinter(config.Config{})
if err != nil {
t.Fatal(err)
}
@@ -67,7 +68,7 @@ func TestErrors(t *testing.T) {
t.Fatalf("got %d problems, want 1", len(ps))
}
trimPosition(&ps[0].Position)
- want := Problem{
+ want := problem{
Diagnostic: runner.Diagnostic{
Position: token.Position{
Filename: "broken_typeerror/pkg.go",
@@ -99,7 +100,7 @@ func TestErrors(t *testing.T) {
}
trimPosition(&ps[0].Position)
- want := Problem{
+ want := problem{
Diagnostic: runner.Diagnostic{
Position: token.Position{
Filename: "broken_parse/pkg.go",
diff --git a/runner/runner.go b/lintcmd/runner/runner.go
similarity index 97%
rename from runner/runner.go
rename to lintcmd/runner/runner.go
index add837dfb..b3c245a43 100644
--- a/runner/runner.go
+++ b/lintcmd/runner/runner.go
@@ -126,12 +126,12 @@ import (
"sync/atomic"
"time"
+ "honnef.co/go/tools/analysis/lint"
+ "honnef.co/go/tools/analysis/report"
"honnef.co/go/tools/config"
- "honnef.co/go/tools/facts"
+ "honnef.co/go/tools/go/loader"
"honnef.co/go/tools/internal/cache"
tsync "honnef.co/go/tools/internal/sync"
- "honnef.co/go/tools/loader"
- "honnef.co/go/tools/report"
"honnef.co/go/tools/unused"
"golang.org/x/tools/go/analysis"
@@ -183,8 +183,26 @@ type Result struct {
results string
}
+type SerializedDirective struct {
+ Command string
+ Arguments []string
+ // The position of the comment
+ DirectivePosition token.Position
+ // The position of the node that the comment is attached to
+ NodePosition token.Position
+}
+
+func serializeDirective(dir lint.Directive, fset *token.FileSet) SerializedDirective {
+ return SerializedDirective{
+ Command: dir.Command,
+ Arguments: dir.Arguments,
+ DirectivePosition: report.DisplayPosition(fset, dir.Directive.Pos()),
+ NodePosition: report.DisplayPosition(fset, dir.Node.Pos()),
+ }
+}
+
type ResultData struct {
- Directives []facts.SerializedDirective
+ Directives []SerializedDirective
Diagnostics []Diagnostic
Unused unused.SerializedResult
}
@@ -543,9 +561,9 @@ func (r *subrunner) do(act action) error {
}
var out ResultData
- out.Directives = make([]facts.SerializedDirective, len(result.dirs))
+ out.Directives = make([]SerializedDirective, len(result.dirs))
for i, dir := range result.dirs {
- out.Directives[i] = facts.SerializeDirective(dir, result.lpkg.Fset)
+ out.Directives[i] = serializeDirective(dir, result.lpkg.Fset)
}
out.Diagnostics = result.diags
@@ -597,7 +615,7 @@ type packageActionResult struct {
facts []gobFact
diags []Diagnostic
unused unused.SerializedResult
- dirs []facts.Directive
+ dirs []lint.Directive
lpkg *loader.Package
skipped bool
}
@@ -629,9 +647,9 @@ func (r *subrunner) doUncached(a *packageAction) (packageActionResult, error) {
// OPT(dh): instead of parsing directives twice (twice because
// U1000 depends on the facts.Directives analyzer), reuse the
// existing result
- var dirs []facts.Directive
+ var dirs []lint.Directive
if !a.factsOnly {
- dirs = facts.ParseDirectives(pkg.Syntax, pkg.Fset)
+ dirs = lint.ParseDirectives(pkg.Syntax, pkg.Fset)
}
res, err := r.runAnalyzers(a, pkg)
diff --git a/runner/stats.go b/lintcmd/runner/stats.go
similarity index 98%
rename from runner/stats.go
rename to lintcmd/runner/stats.go
index ce3d589df..fd7da2fa4 100644
--- a/runner/stats.go
+++ b/lintcmd/runner/stats.go
@@ -4,8 +4,9 @@ import (
"sync/atomic"
"time"
+ "honnef.co/go/tools/go/loader"
+
"golang.org/x/tools/go/analysis"
- "honnef.co/go/tools/loader"
)
const (
diff --git a/lint/lintutil/stats.go b/lintcmd/stats.go
similarity index 88%
rename from lint/lintutil/stats.go
rename to lintcmd/stats.go
index ba8caf0af..198ef0ba7 100644
--- a/lint/lintutil/stats.go
+++ b/lintcmd/stats.go
@@ -1,6 +1,6 @@
// +build !aix,!android,!darwin,!dragonfly,!freebsd,!linux,!netbsd,!openbsd,!solaris
-package lintutil
+package lintcmd
import "os"
diff --git a/lint/lintutil/stats_bsd.go b/lintcmd/stats_bsd.go
similarity index 88%
rename from lint/lintutil/stats_bsd.go
rename to lintcmd/stats_bsd.go
index 3a62ede03..0395a36d3 100644
--- a/lint/lintutil/stats_bsd.go
+++ b/lintcmd/stats_bsd.go
@@ -1,6 +1,6 @@
// +build darwin dragonfly freebsd netbsd openbsd
-package lintutil
+package lintcmd
import (
"os"
diff --git a/lint/lintutil/stats_posix.go b/lintcmd/stats_posix.go
similarity index 87%
rename from lint/lintutil/stats_posix.go
rename to lintcmd/stats_posix.go
index 53f21c666..1737692b2 100644
--- a/lint/lintutil/stats_posix.go
+++ b/lintcmd/stats_posix.go
@@ -1,6 +1,6 @@
// +build aix android linux solaris
-package lintutil
+package lintcmd
import (
"os"
diff --git a/lint/testdata/src/Test/file-ignores.go b/lintcmd/testdata/src/Test/file-ignores.go
similarity index 100%
rename from lint/testdata/src/Test/file-ignores.go
rename to lintcmd/testdata/src/Test/file-ignores.go
diff --git a/lint/testdata/src/Test/line-ignores.go b/lintcmd/testdata/src/Test/line-ignores.go
similarity index 100%
rename from lint/testdata/src/Test/line-ignores.go
rename to lintcmd/testdata/src/Test/line-ignores.go
diff --git a/lint/testdata/src/broken_dep/pkg.go b/lintcmd/testdata/src/broken_dep/pkg.go
similarity index 100%
rename from lint/testdata/src/broken_dep/pkg.go
rename to lintcmd/testdata/src/broken_dep/pkg.go
diff --git a/lint/testdata/src/broken_parse/pkg.go b/lintcmd/testdata/src/broken_parse/pkg.go
similarity index 100%
rename from lint/testdata/src/broken_parse/pkg.go
rename to lintcmd/testdata/src/broken_parse/pkg.go
diff --git a/lint/testdata/src/broken_pkgerror/broken.go b/lintcmd/testdata/src/broken_pkgerror/broken.go
similarity index 100%
rename from lint/testdata/src/broken_pkgerror/broken.go
rename to lintcmd/testdata/src/broken_pkgerror/broken.go
diff --git a/lint/testdata/src/broken_typeerror/pkg.go b/lintcmd/testdata/src/broken_typeerror/pkg.go
similarity index 100%
rename from lint/testdata/src/broken_typeerror/pkg.go
rename to lintcmd/testdata/src/broken_typeerror/pkg.go
diff --git a/version/buildinfo.go b/lintcmd/version/buildinfo.go
similarity index 100%
rename from version/buildinfo.go
rename to lintcmd/version/buildinfo.go
diff --git a/version/buildinfo111.go b/lintcmd/version/buildinfo111.go
similarity index 100%
rename from version/buildinfo111.go
rename to lintcmd/version/buildinfo111.go
diff --git a/version/version.go b/lintcmd/version/version.go
similarity index 100%
rename from version/version.go
rename to lintcmd/version/version.go
diff --git a/pattern/match.go b/pattern/match.go
index 0c42178f7..f0fda0619 100644
--- a/pattern/match.go
+++ b/pattern/match.go
@@ -6,8 +6,6 @@ import (
"go/token"
"go/types"
"reflect"
-
- "honnef.co/go/tools/code"
)
var tokensByString = map[string]Token{
@@ -452,7 +450,8 @@ func (fn Function) Match(m *Matcher, node interface{}) (interface{}, bool) {
obj = m.TypesInfo.ObjectOf(node)
switch obj := obj.(type) {
case *types.Func:
- name = code.FuncName(obj)
+ // OPT(dh): optimize this similar to code.FuncName
+ name = obj.FullName()
case *types.Builtin:
name = obj.Name()
default:
@@ -464,7 +463,8 @@ func (fn Function) Match(m *Matcher, node interface{}) (interface{}, bool) {
if !ok {
return nil, false
}
- name = code.FuncName(obj.(*types.Func))
+ // OPT(dh): optimize this similar to code.FuncName
+ name = obj.(*types.Func).FullName()
default:
return nil, false
}
diff --git a/simple/analysis.go b/simple/analysis.go
index 9f554c310..bf8f31942 100644
--- a/simple/analysis.go
+++ b/simple/analysis.go
@@ -3,12 +3,12 @@ package simple
import (
"golang.org/x/tools/go/analysis"
"golang.org/x/tools/go/analysis/passes/inspect"
- "honnef.co/go/tools/facts"
+ "honnef.co/go/tools/analysis/facts"
+ "honnef.co/go/tools/analysis/lint"
"honnef.co/go/tools/internal/passes/buildir"
- "honnef.co/go/tools/lint/lintutil"
)
-var Analyzers = lintutil.InitializeAnalyzers(Docs, map[string]*analysis.Analyzer{
+var Analyzers = lint.InitializeAnalyzers(Docs, map[string]*analysis.Analyzer{
"S1000": {
Run: CheckSingleCaseSelect,
Requires: []*analysis.Analyzer{inspect.Analyzer, facts.Generated},
diff --git a/simple/doc.go b/simple/doc.go
index de0edc5bf..dce35e332 100644
--- a/simple/doc.go
+++ b/simple/doc.go
@@ -1,6 +1,6 @@
package simple
-import "honnef.co/go/tools/lint"
+import "honnef.co/go/tools/analysis/lint"
var Docs = map[string]*lint.Documentation{
"S1000": {
diff --git a/simple/lint.go b/simple/lint.go
index 9d1455c87..0ade9bff8 100644
--- a/simple/lint.go
+++ b/simple/lint.go
@@ -1,5 +1,5 @@
// Package simple contains a linter for Go source code.
-package simple // import "honnef.co/go/tools/simple"
+package simple
import (
"fmt"
@@ -14,14 +14,14 @@ import (
"golang.org/x/tools/go/analysis"
"golang.org/x/tools/go/types/typeutil"
- . "honnef.co/go/tools/arg"
- "honnef.co/go/tools/code"
- "honnef.co/go/tools/edit"
+ "honnef.co/go/tools/analysis/code"
+ "honnef.co/go/tools/analysis/edit"
+ "honnef.co/go/tools/analysis/lint"
+ "honnef.co/go/tools/analysis/report"
"honnef.co/go/tools/internal/passes/buildir"
"honnef.co/go/tools/internal/sharedcheck"
- . "honnef.co/go/tools/lint/lintdsl"
+ "honnef.co/go/tools/knowledge"
"honnef.co/go/tools/pattern"
- "honnef.co/go/tools/report"
)
var (
@@ -40,10 +40,10 @@ var (
func CheckSingleCaseSelect(pass *analysis.Pass) (interface{}, error) {
seen := map[ast.Node]struct{}{}
fn := func(node ast.Node) {
- if m, ok := Match(pass, checkSingleCaseSelectQ1, node); ok {
+ if m, ok := code.Match(pass, checkSingleCaseSelectQ1, node); ok {
seen[m.State["select"].(ast.Node)] = struct{}{}
report.Report(pass, node, "should use for range instead of for { select {} }", report.FilterGenerated())
- } else if _, ok := Match(pass, checkSingleCaseSelectQ2, node); ok {
+ } else if _, ok := code.Match(pass, checkSingleCaseSelectQ2, node); ok {
if _, ok := seen[node]; !ok {
report.Report(pass, node, "should use a simple channel send/receive instead of select with a single case",
report.ShortRange(),
@@ -75,7 +75,7 @@ var (
func CheckLoopCopy(pass *analysis.Pass) (interface{}, error) {
fn := func(node ast.Node) {
- m, edits, ok := MatchAndEdit(pass, checkLoopCopyQ, checkLoopCopyR, node)
+ m, edits, ok := code.MatchAndEdit(pass, checkLoopCopyQ, checkLoopCopyR, node)
if !ok {
return
}
@@ -161,7 +161,7 @@ func CheckBytesBufferConversions(pass *analysis.Pass) (interface{}, error) {
return nil, nil
}
fn := func(node ast.Node, stack []ast.Node) {
- m, ok := Match(pass, checkBytesBufferConversionsQ, node)
+ m, ok := code.Match(pass, checkBytesBufferConversionsQ, node)
if !ok {
return
}
@@ -288,7 +288,7 @@ func CheckBytesCompare(pass *analysis.Pass) (interface{}, error) {
return nil, nil
}
fn := func(node ast.Node) {
- m, ok := Match(pass, checkBytesCompareQ, node)
+ m, ok := code.Match(pass, checkBytesCompareQ, node)
if !ok {
return
}
@@ -341,7 +341,7 @@ func CheckRegexpRaw(pass *analysis.Pass) (interface{}, error) {
if !ok {
return
}
- lit, ok := call.Args[Arg("regexp.Compile.expr")].(*ast.BasicLit)
+ lit, ok := call.Args[knowledge.Arg("regexp.Compile.expr")].(*ast.BasicLit)
if !ok {
// TODO(dominikh): support string concat, maybe support constants
return
@@ -404,11 +404,11 @@ func CheckIfReturn(pass *analysis.Pass) (interface{}, error) {
return
}
}
- m1, ok := Match(pass, checkIfReturnQIf, n1)
+ m1, ok := code.Match(pass, checkIfReturnQIf, n1)
if !ok {
return
}
- m2, ok := Match(pass, checkIfReturnQRet, n2)
+ m2, ok := code.Match(pass, checkIfReturnQRet, n2)
if !ok {
return
}
@@ -553,7 +553,7 @@ func CheckRedundantNilCheckWithLen(pass *analysis.Pass) (interface{}, error) {
if !ok || yxFun.Name != "len" || len(yx.Args) != 1 {
return
}
- yxArg, ok := yx.Args[Arg("len.v")].(*ast.Ident)
+ yxArg, ok := yx.Args[knowledge.Arg("len.v")].(*ast.Ident)
if !ok {
return
}
@@ -616,7 +616,7 @@ var checkSlicingQ = pattern.MustParse(`(SliceExpr x@(Object _) low (CallExpr (Bu
func CheckSlicing(pass *analysis.Pass) (interface{}, error) {
fn := func(node ast.Node) {
- if _, ok := Match(pass, checkSlicingQ, node); ok {
+ if _, ok := code.Match(pass, checkSlicingQ, node); ok {
expr := node.(*ast.SliceExpr)
report.Report(pass, expr.High,
"should omit second index in slice, s[a:len(s)] is identical to s[a:]",
@@ -655,7 +655,7 @@ var checkLoopAppendQ = pattern.MustParse(`
func CheckLoopAppend(pass *analysis.Pass) (interface{}, error) {
fn := func(node ast.Node) {
- m, ok := Match(pass, checkLoopAppendQ, node)
+ m, ok := code.Match(pass, checkLoopAppendQ, node)
if !ok {
return
}
@@ -702,7 +702,7 @@ var (
func CheckTimeSince(pass *analysis.Pass) (interface{}, error) {
fn := func(node ast.Node) {
- if _, edits, ok := MatchAndEdit(pass, checkTimeSinceQ, checkTimeSinceR, node); ok {
+ if _, edits, ok := code.MatchAndEdit(pass, checkTimeSinceQ, checkTimeSinceR, node); ok {
report.Report(pass, node, "should use time.Since instead of time.Now().Sub",
report.FilterGenerated(),
report.Fixes(edit.Fix("replace with call to time.Since", edits...)))
@@ -722,7 +722,7 @@ func CheckTimeUntil(pass *analysis.Pass) (interface{}, error) {
return nil, nil
}
fn := func(node ast.Node) {
- if _, ok := Match(pass, checkTimeUntilQ, node); ok {
+ if _, ok := code.Match(pass, checkTimeUntilQ, node); ok {
if sel, ok := node.(*ast.CallExpr).Fun.(*ast.SelectorExpr); ok {
r := pattern.NodeToAST(checkTimeUntilR.Root, map[string]interface{}{"arg": sel.X}).(ast.Node)
report.Report(pass, node, "should use time.Until instead of t.Sub(time.Now())",
@@ -752,13 +752,13 @@ var (
func CheckUnnecessaryBlank(pass *analysis.Pass) (interface{}, error) {
fn1 := func(node ast.Node) {
- if _, ok := Match(pass, checkUnnecessaryBlankQ1, node); ok {
+ if _, ok := code.Match(pass, checkUnnecessaryBlankQ1, node); ok {
r := *node.(*ast.AssignStmt)
r.Lhs = r.Lhs[0:1]
report.Report(pass, node, "unnecessary assignment to the blank identifier",
report.FilterGenerated(),
report.Fixes(edit.Fix("remove assignment to blank identifier", edit.ReplaceWithNode(pass.Fset, node, &r))))
- } else if m, ok := Match(pass, checkUnnecessaryBlankQ2, node); ok {
+ } else if m, ok := code.Match(pass, checkUnnecessaryBlankQ2, node); ok {
report.Report(pass, node, "unnecessary assignment to the blank identifier",
report.FilterGenerated(),
report.Fixes(edit.Fix("simplify channel receive operation", edit.ReplaceWithNode(pass.Fset, node, m.State["recv"].(ast.Node)))))
@@ -945,7 +945,7 @@ func CheckTrim(pass *analysis.Pass) (interface{}, error) {
if len(call.Args) != 1 {
return false
}
- return sameNonDynamic(call.Args[Arg("len.v")], ident)
+ return sameNonDynamic(call.Args[knowledge.Arg("len.v")], ident)
}
fn := func(node ast.Node) {
@@ -1062,7 +1062,7 @@ func CheckTrim(pass *analysis.Pass) (interface{}, error) {
if len(index.Args) != 1 {
return
}
- id3 := index.Args[Arg("len.v")]
+ id3 := index.Args[knowledge.Arg("len.v")]
switch oid3 := condCall.Args[1].(type) {
case *ast.BasicLit:
if pkg != "strings" {
@@ -1150,7 +1150,7 @@ func CheckLoopSlide(pass *analysis.Pass) (interface{}, error) {
fn := func(node ast.Node) {
loop := node.(*ast.ForStmt)
- m, edits, ok := MatchAndEdit(pass, checkLoopSlideQ, checkLoopSlideR, loop)
+ m, edits, ok := code.MatchAndEdit(pass, checkLoopSlideQ, checkLoopSlideR, loop)
if !ok {
return
}
@@ -1178,14 +1178,14 @@ func CheckMakeLenCap(pass *analysis.Pass) (interface{}, error) {
// special case of runtime tests testing map creation
return
}
- if m, ok := Match(pass, checkMakeLenCapQ1, node); ok {
+ if m, ok := code.Match(pass, checkMakeLenCapQ1, node); ok {
T := m.State["typ"].(ast.Expr)
size := m.State["size"].(ast.Node)
if _, ok := pass.TypesInfo.TypeOf(T).Underlying().(*types.Slice); ok {
return
}
report.Report(pass, size, fmt.Sprintf("should use make(%s) instead", report.Render(pass, T)), report.FilterGenerated())
- } else if m, ok := Match(pass, checkMakeLenCapQ2, node); ok {
+ } else if m, ok := code.Match(pass, checkMakeLenCapQ2, node); ok {
// TODO(dh): don't consider sizes identical if they're
// dynamic. for example: make(T, <-ch, <-ch).
T := m.State["typ"].(ast.Expr)
@@ -1224,7 +1224,7 @@ var (
func CheckAssertNotNil(pass *analysis.Pass) (interface{}, error) {
fn1 := func(node ast.Node) {
- m, ok := Match(pass, checkAssertNotNilFn1Q, node)
+ m, ok := code.Match(pass, checkAssertNotNilFn1Q, node)
if !ok {
return
}
@@ -1235,7 +1235,7 @@ func CheckAssertNotNil(pass *analysis.Pass) (interface{}, error) {
report.FilterGenerated())
}
fn2 := func(node ast.Node) {
- m, ok := Match(pass, checkAssertNotNilFn2Q, node)
+ m, ok := code.Match(pass, checkAssertNotNilFn2Q, node)
if !ok {
return
}
@@ -1359,7 +1359,7 @@ func CheckRedundantBreak(pass *analysis.Pass) (interface{}, error) {
ret = x.Type.Results
body = x.Body
default:
- ExhaustiveTypeSwitch(node)
+ lint.ExhaustiveTypeSwitch(node)
}
// if the func has results, a return can't be redundant.
// similarly, if there are no statements, there can be
@@ -1408,7 +1408,7 @@ var checkRedundantSprintfQ = pattern.MustParse(`(CallExpr (Function "fmt.Sprintf
func CheckRedundantSprintf(pass *analysis.Pass) (interface{}, error) {
fn := func(node ast.Node) {
- m, ok := Match(pass, checkRedundantSprintfQ, node)
+ m, ok := code.Match(pass, checkRedundantSprintfQ, node)
if !ok {
return
}
@@ -1460,7 +1460,7 @@ var (
func CheckErrorsNewSprintf(pass *analysis.Pass) (interface{}, error) {
fn := func(node ast.Node) {
- if _, edits, ok := MatchAndEdit(pass, checkErrorsNewSprintfQ, checkErrorsNewSprintfR, node); ok {
+ if _, edits, ok := code.MatchAndEdit(pass, checkErrorsNewSprintfQ, checkErrorsNewSprintfR, node); ok {
// TODO(dh): the suggested fix may leave an unused import behind
report.Report(pass, node, "should use fmt.Errorf(...) instead of errors.New(fmt.Sprintf(...))",
report.FilterGenerated(),
@@ -1484,7 +1484,7 @@ var checkNilCheckAroundRangeQ = pattern.MustParse(`
func CheckNilCheckAroundRange(pass *analysis.Pass) (interface{}, error) {
fn := func(node ast.Node) {
- m, ok := Match(pass, checkNilCheckAroundRangeQ, node)
+ m, ok := code.Match(pass, checkNilCheckAroundRangeQ, node)
if !ok {
return
}
@@ -1535,7 +1535,7 @@ func CheckSortHelpers(pass *analysis.Pass) (interface{}, error) {
case *ast.FuncDecl:
body = node.Body
default:
- ExhaustiveTypeSwitch(node)
+ lint.ExhaustiveTypeSwitch(node)
}
if body == nil {
return
@@ -1555,7 +1555,7 @@ func CheckSortHelpers(pass *analysis.Pass) (interface{}, error) {
return false
}
call := node.(*ast.CallExpr)
- typeconv := call.Args[Arg("sort.Sort.data")].(*ast.CallExpr)
+ typeconv := call.Args[knowledge.Arg("sort.Sort.data")].(*ast.CallExpr)
sel := typeconv.Fun.(*ast.SelectorExpr)
name := code.SelectorName(pass, sel)
@@ -1603,7 +1603,7 @@ var checkGuardedDeleteQ = pattern.MustParse(`
func CheckGuardedDelete(pass *analysis.Pass) (interface{}, error) {
fn := func(node ast.Node) {
- if m, ok := Match(pass, checkGuardedDeleteQ, node); ok {
+ if m, ok := code.Match(pass, checkGuardedDeleteQ, node); ok {
report.Report(pass, node, "unnecessary guard around call to delete",
report.ShortRange(),
report.FilterGenerated(),
@@ -1626,7 +1626,7 @@ var (
func CheckSimplifyTypeSwitch(pass *analysis.Pass) (interface{}, error) {
fn := func(node ast.Node) {
- m, ok := Match(pass, checkSimplifyTypeSwitchQ, node)
+ m, ok := code.Match(pass, checkSimplifyTypeSwitchQ, node)
if !ok {
return
}
@@ -1745,7 +1745,7 @@ var checkUnnecessaryGuardQ = pattern.MustParse(`
func CheckUnnecessaryGuard(pass *analysis.Pass) (interface{}, error) {
fn := func(node ast.Node) {
- if m, ok := Match(pass, checkUnnecessaryGuardQ, node); ok {
+ if m, ok := code.Match(pass, checkUnnecessaryGuardQ, node); ok {
if code.MayHaveSideEffects(pass, m.State["indexexpr"].(ast.Expr), nil) {
return
}
@@ -1765,7 +1765,7 @@ var (
func CheckElaborateSleep(pass *analysis.Pass) (interface{}, error) {
fn := func(node ast.Node) {
- if m, ok := Match(pass, checkElaborateSleepQ, node); ok {
+ if m, ok := code.Match(pass, checkElaborateSleepQ, node); ok {
if body, ok := m.State["body"].([]ast.Stmt); ok && len(body) == 0 {
report.Report(pass, node, "should use time.Sleep instead of elaborate way of sleeping",
report.ShortRange(),
@@ -1801,7 +1801,7 @@ var checkPrintSprintQ = pattern.MustParse(`
func CheckPrintSprintf(pass *analysis.Pass) (interface{}, error) {
fn := func(node ast.Node) {
- m, ok := Match(pass, checkPrintSprintQ, node)
+ m, ok := code.Match(pass, checkPrintSprintQ, node)
if !ok {
return
}
@@ -1844,7 +1844,7 @@ func CheckSprintLiteral(pass *analysis.Pass) (interface{}, error) {
// for copying strings, which may be useful when extracing a small
// substring from a large string.
fn := func(node ast.Node) {
- m, ok := Match(pass, checkSprintLiteralQ, node)
+ m, ok := code.Match(pass, checkSprintLiteralQ, node)
if !ok {
return
}
diff --git a/staticcheck.conf b/staticcheck.conf
deleted file mode 100644
index 56ae61df9..000000000
--- a/staticcheck.conf
+++ /dev/null
@@ -1 +0,0 @@
-dot_import_whitelist = ["honnef.co/go/tools/lint/lintdsl", "honnef.co/go/tools/arg"]
diff --git a/staticcheck/analysis.go b/staticcheck/analysis.go
index 6590312d2..37f973388 100644
--- a/staticcheck/analysis.go
+++ b/staticcheck/analysis.go
@@ -1,9 +1,9 @@
package staticcheck
import (
- "honnef.co/go/tools/facts"
+ "honnef.co/go/tools/analysis/facts"
+ "honnef.co/go/tools/analysis/lint"
"honnef.co/go/tools/internal/passes/buildir"
- "honnef.co/go/tools/lint/lintutil"
"golang.org/x/tools/go/analysis"
"golang.org/x/tools/go/analysis/passes/inspect"
@@ -18,7 +18,7 @@ func makeCallCheckerAnalyzer(rules map[string]CallCheck, extraReqs ...*analysis.
}
}
-var Analyzers = lintutil.InitializeAnalyzers(Docs, map[string]*analysis.Analyzer{
+var Analyzers = lint.InitializeAnalyzers(Docs, map[string]*analysis.Analyzer{
"SA1000": makeCallCheckerAnalyzer(checkRegexpRules),
"SA1001": {
Run: CheckTemplate,
diff --git a/staticcheck/buildtag.go b/staticcheck/buildtag.go
index 58e1e4ae1..0ed3a93a6 100644
--- a/staticcheck/buildtag.go
+++ b/staticcheck/buildtag.go
@@ -4,7 +4,7 @@ import (
"go/ast"
"strings"
- "honnef.co/go/tools/code"
+ "honnef.co/go/tools/analysis/code"
)
func buildTags(f *ast.File) [][]string {
diff --git a/staticcheck/doc.go b/staticcheck/doc.go
index 17d28ad60..911b00001 100644
--- a/staticcheck/doc.go
+++ b/staticcheck/doc.go
@@ -1,6 +1,6 @@
package staticcheck
-import "honnef.co/go/tools/lint"
+import "honnef.co/go/tools/analysis/lint"
var Docs = map[string]*lint.Documentation{
"SA1000": {
diff --git a/staticcheck/lint.go b/staticcheck/lint.go
index fc9863708..a344c36c3 100644
--- a/staticcheck/lint.go
+++ b/staticcheck/lint.go
@@ -1,5 +1,5 @@
// Package staticcheck contains a linter for Go source code.
-package staticcheck // import "honnef.co/go/tools/staticcheck"
+package staticcheck
import (
"fmt"
@@ -18,20 +18,18 @@ import (
texttemplate "text/template"
"unicode"
- . "honnef.co/go/tools/arg"
- "honnef.co/go/tools/code"
- "honnef.co/go/tools/deprecated"
- "honnef.co/go/tools/edit"
- "honnef.co/go/tools/facts"
- "honnef.co/go/tools/functions"
+ "honnef.co/go/tools/analysis/code"
+ "honnef.co/go/tools/analysis/edit"
+ "honnef.co/go/tools/analysis/facts"
+ "honnef.co/go/tools/analysis/lint"
+ "honnef.co/go/tools/analysis/report"
+ "honnef.co/go/tools/go/ir"
+ "honnef.co/go/tools/go/ir/irutil"
"honnef.co/go/tools/internal/passes/buildir"
"honnef.co/go/tools/internal/sharedcheck"
- "honnef.co/go/tools/ir"
- "honnef.co/go/tools/ir/irutil"
- . "honnef.co/go/tools/lint/lintdsl"
+ "honnef.co/go/tools/knowledge"
"honnef.co/go/tools/pattern"
"honnef.co/go/tools/printf"
- "honnef.co/go/tools/report"
"golang.org/x/tools/go/analysis"
"golang.org/x/tools/go/analysis/passes/inspect"
@@ -122,7 +120,7 @@ var (
checkTimeParseRules = map[string]CallCheck{
"time.Parse": func(call *Call) {
- arg := call.Args[Arg("time.Parse.layout")]
+ arg := call.Args[knowledge.Arg("time.Parse.layout")]
err := ValidateTimeLayout(arg.Value)
if err != nil {
arg.Invalid(err.Error())
@@ -132,7 +130,7 @@ var (
checkEncodingBinaryRules = map[string]CallCheck{
"encoding/binary.Write": func(call *Call) {
- arg := call.Args[Arg("encoding/binary.Write.data")]
+ arg := call.Args[knowledge.Arg("encoding/binary.Write.data")]
if !CanBinaryMarshal(call.Pass, arg.Value) {
arg.Invalid(fmt.Sprintf("value of type %s cannot be used with binary.Write", arg.Value.Value.Type()))
}
@@ -141,7 +139,7 @@ var (
checkURLsRules = map[string]CallCheck{
"net/url.Parse": func(call *Call) {
- arg := call.Args[Arg("net/url.Parse.rawurl")]
+ arg := call.Args[knowledge.Arg("net/url.Parse.rawurl")]
err := ValidateURL(arg.Value)
if err != nil {
arg.Invalid(err.Error())
@@ -151,7 +149,7 @@ var (
checkSyncPoolValueRules = map[string]CallCheck{
"(*sync.Pool).Put": func(call *Call) {
- arg := call.Args[Arg("(*sync.Pool).Put.x")]
+ arg := call.Args[knowledge.Arg("(*sync.Pool).Put.x")]
typ := arg.Value.Value.Type()
if !code.IsPointerLike(typ) {
arg.Invalid("argument should be pointer-like to avoid allocations")
@@ -195,7 +193,7 @@ var (
checkUnbufferedSignalChanRules = map[string]CallCheck{
"os/signal.Notify": func(call *Call) {
- arg := call.Args[Arg("os/signal.Notify.c")]
+ arg := call.Args[knowledge.Arg("os/signal.Notify.c")]
if UnbufferedChannel(arg.Value) {
arg.Invalid("the channel used with signal.Notify should be buffered")
}
@@ -222,8 +220,8 @@ var (
checkBytesEqualIPRules = map[string]CallCheck{
"bytes.Equal": func(call *Call) {
- if ConvertedFrom(call.Args[Arg("bytes.Equal.a")].Value, "net.IP") &&
- ConvertedFrom(call.Args[Arg("bytes.Equal.b")].Value, "net.IP") {
+ if ConvertedFrom(call.Args[knowledge.Arg("bytes.Equal.a")].Value, "net.IP") &&
+ ConvertedFrom(call.Args[knowledge.Arg("bytes.Equal.b")].Value, "net.IP") {
call.Invalid("use net.IP.Equal to compare net.IPs, not bytes.Equal")
}
},
@@ -241,22 +239,22 @@ var (
// Let's see if we encounter any false positives.
//
// Also, should we flag gob?
- "encoding/json.Marshal": checkNoopMarshalImpl(Arg("json.Marshal.v"), "MarshalJSON", "MarshalText"),
- "encoding/xml.Marshal": checkNoopMarshalImpl(Arg("xml.Marshal.v"), "MarshalXML", "MarshalText"),
- "(*encoding/json.Encoder).Encode": checkNoopMarshalImpl(Arg("(*encoding/json.Encoder).Encode.v"), "MarshalJSON", "MarshalText"),
- "(*encoding/xml.Encoder).Encode": checkNoopMarshalImpl(Arg("(*encoding/xml.Encoder).Encode.v"), "MarshalXML", "MarshalText"),
+ "encoding/json.Marshal": checkNoopMarshalImpl(knowledge.Arg("json.Marshal.v"), "MarshalJSON", "MarshalText"),
+ "encoding/xml.Marshal": checkNoopMarshalImpl(knowledge.Arg("xml.Marshal.v"), "MarshalXML", "MarshalText"),
+ "(*encoding/json.Encoder).Encode": checkNoopMarshalImpl(knowledge.Arg("(*encoding/json.Encoder).Encode.v"), "MarshalJSON", "MarshalText"),
+ "(*encoding/xml.Encoder).Encode": checkNoopMarshalImpl(knowledge.Arg("(*encoding/xml.Encoder).Encode.v"), "MarshalXML", "MarshalText"),
- "encoding/json.Unmarshal": checkNoopMarshalImpl(Arg("json.Unmarshal.v"), "UnmarshalJSON", "UnmarshalText"),
- "encoding/xml.Unmarshal": checkNoopMarshalImpl(Arg("xml.Unmarshal.v"), "UnmarshalXML", "UnmarshalText"),
- "(*encoding/json.Decoder).Decode": checkNoopMarshalImpl(Arg("(*encoding/json.Decoder).Decode.v"), "UnmarshalJSON", "UnmarshalText"),
- "(*encoding/xml.Decoder).Decode": checkNoopMarshalImpl(Arg("(*encoding/xml.Decoder).Decode.v"), "UnmarshalXML", "UnmarshalText"),
+ "encoding/json.Unmarshal": checkNoopMarshalImpl(knowledge.Arg("json.Unmarshal.v"), "UnmarshalJSON", "UnmarshalText"),
+ "encoding/xml.Unmarshal": checkNoopMarshalImpl(knowledge.Arg("xml.Unmarshal.v"), "UnmarshalXML", "UnmarshalText"),
+ "(*encoding/json.Decoder).Decode": checkNoopMarshalImpl(knowledge.Arg("(*encoding/json.Decoder).Decode.v"), "UnmarshalJSON", "UnmarshalText"),
+ "(*encoding/xml.Decoder).Decode": checkNoopMarshalImpl(knowledge.Arg("(*encoding/xml.Decoder).Decode.v"), "UnmarshalXML", "UnmarshalText"),
}
checkUnsupportedMarshal = map[string]CallCheck{
- "encoding/json.Marshal": checkUnsupportedMarshalImpl(Arg("json.Marshal.v"), "json", "MarshalJSON", "MarshalText"),
- "encoding/xml.Marshal": checkUnsupportedMarshalImpl(Arg("xml.Marshal.v"), "xml", "MarshalXML", "MarshalText"),
- "(*encoding/json.Encoder).Encode": checkUnsupportedMarshalImpl(Arg("(*encoding/json.Encoder).Encode.v"), "json", "MarshalJSON", "MarshalText"),
- "(*encoding/xml.Encoder).Encode": checkUnsupportedMarshalImpl(Arg("(*encoding/xml.Encoder).Encode.v"), "xml", "MarshalXML", "MarshalText"),
+ "encoding/json.Marshal": checkUnsupportedMarshalImpl(knowledge.Arg("json.Marshal.v"), "json", "MarshalJSON", "MarshalText"),
+ "encoding/xml.Marshal": checkUnsupportedMarshalImpl(knowledge.Arg("xml.Marshal.v"), "xml", "MarshalXML", "MarshalText"),
+ "(*encoding/json.Encoder).Encode": checkUnsupportedMarshalImpl(knowledge.Arg("(*encoding/json.Encoder).Encode.v"), "json", "MarshalJSON", "MarshalText"),
+ "(*encoding/xml.Encoder).Encode": checkUnsupportedMarshalImpl(knowledge.Arg("(*encoding/xml.Encoder).Encode.v"), "xml", "MarshalXML", "MarshalText"),
}
checkAtomicAlignment = map[string]CallCheck{
@@ -818,7 +816,7 @@ func fieldPath(start types.Type, indices []int) string {
}
func isInLoop(b *ir.BasicBlock) bool {
- sets := functions.FindLoops(b.Parent())
+ sets := code.FindLoops(b.Parent())
for _, set := range sets {
if set.Has(b) {
return true
@@ -858,7 +856,7 @@ func CheckUntrappableSignal(pass *analysis.Pass) (interface{}, error) {
nargs := make([]ast.Expr, len(call.Args))
for j, a := range call.Args {
if i == j {
- nargs[j] = Selector("syscall", "SIGTERM")
+ nargs[j] = edit.Selector("syscall", "SIGTERM")
} else {
nargs[j] = a
}
@@ -918,7 +916,7 @@ func CheckTemplate(pass *analysis.Pass) (interface{}, error) {
// template comes from and where it has been
return
}
- s, ok := code.ExprToString(pass, call.Args[Arg("(*text/template.Template).Parse.text")])
+ s, ok := code.ExprToString(pass, call.Args[knowledge.Arg("(*text/template.Template).Parse.text")])
if !ok {
return
}
@@ -932,7 +930,7 @@ func CheckTemplate(pass *analysis.Pass) (interface{}, error) {
if err != nil {
// TODO(dominikh): whitelist other parse errors, if any
if strings.Contains(err.Error(), "unexpected") {
- report.Report(pass, call.Args[Arg("(*text/template.Template).Parse.text")], err.Error())
+ report.Report(pass, call.Args[knowledge.Arg("(*text/template.Template).Parse.text")], err.Error())
}
}
}
@@ -951,7 +949,7 @@ func CheckTimeSleepConstant(pass *analysis.Pass) (interface{}, error) {
if !code.IsCallToAST(pass, call, "time.Sleep") {
return
}
- lit, ok := call.Args[Arg("time.Sleep.d")].(*ast.BasicLit)
+ lit, ok := call.Args[knowledge.Arg("time.Sleep.d")].(*ast.BasicLit)
if !ok {
return
}
@@ -984,7 +982,7 @@ var checkWaitgroupAddQ = pattern.MustParse(`
func CheckWaitgroupAdd(pass *analysis.Pass) (interface{}, error) {
fn := func(node ast.Node) {
- if m, ok := Match(pass, checkWaitgroupAddQ, node); ok {
+ if m, ok := code.Match(pass, checkWaitgroupAddQ, node); ok {
call := m.State["call"].(ast.Node)
report.Report(pass, call, fmt.Sprintf("should call %s before starting the goroutine to avoid a race", report.Render(pass, call)))
}
@@ -1163,7 +1161,7 @@ func CheckTestMainExit(pass *analysis.Pass) (interface{}, error) {
}
return true
default:
- ExhaustiveTypeSwitch(node)
+ lint.ExhaustiveTypeSwitch(node)
return true
}
}
@@ -1191,14 +1189,14 @@ func CheckExec(pass *analysis.Pass) (interface{}, error) {
if !code.IsCallToAST(pass, call, "os/exec.Command") {
return
}
- val, ok := code.ExprToString(pass, call.Args[Arg("os/exec.Command.name")])
+ val, ok := code.ExprToString(pass, call.Args[knowledge.Arg("os/exec.Command.name")])
if !ok {
return
}
if !strings.Contains(val, " ") || strings.Contains(val, `\`) || strings.Contains(val, "/") {
return
}
- report.Report(pass, call.Args[Arg("os/exec.Command.name")],
+ report.Report(pass, call.Args[knowledge.Arg("os/exec.Command.name")],
"first argument to exec.Command looks like a shell command, but a program name or path are expected")
}
code.Preorder(pass, fn, (*ast.CallExpr)(nil))
@@ -1308,7 +1306,7 @@ func CheckScopedBreak(pass *analysis.Pass) (interface{}, error) {
case *ast.RangeStmt:
body = node.Body
default:
- ExhaustiveTypeSwitch(node)
+ lint.ExhaustiveTypeSwitch(node)
}
for _, stmt := range body.List {
var blocks [][]ast.Stmt
@@ -1366,9 +1364,9 @@ func CheckUnsafePrintf(pass *analysis.Pass) (interface{}, error) {
switch name {
case "fmt.Printf", "fmt.Sprintf", "log.Printf":
- arg = Arg("fmt.Printf.format")
+ arg = knowledge.Arg("fmt.Printf.format")
case "fmt.Fprintf":
- arg = Arg("fmt.Fprintf.format")
+ arg = knowledge.Arg("fmt.Fprintf.format")
default:
return
}
@@ -1543,11 +1541,11 @@ var (
func CheckIneffectiveCopy(pass *analysis.Pass) (interface{}, error) {
fn := func(node ast.Node) {
- if m, ok := Match(pass, checkIneffectiveCopyQ1, node); ok {
+ if m, ok := code.Match(pass, checkIneffectiveCopyQ1, node); ok {
if ident, ok := m.State["obj"].(*ast.Ident); !ok || !cgoIdent.MatchString(ident.Name) {
report.Report(pass, node, "&*x will be simplified to x. It will not copy x.")
}
- } else if _, ok := Match(pass, checkIneffectiveCopyQ2, node); ok {
+ } else if _, ok := code.Match(pass, checkIneffectiveCopyQ2, node); ok {
report.Report(pass, node, "*&x will be simplified to x. It will not copy x.")
}
}
@@ -1596,7 +1594,7 @@ func CheckCanonicalHeaderKey(pass *analysis.Pass) (interface{}, error) {
fix = edit.Fix("canonicalize header key", edit.ReplaceWithString(pass.Fset, op.Index, strconv.Quote(canonical)))
case *ast.Ident:
call := &ast.CallExpr{
- Fun: Selector("http", "CanonicalHeaderKey"),
+ Fun: edit.Selector("http", "CanonicalHeaderKey"),
Args: []ast.Expr{op.Index},
}
fix = edit.Fix("wrap in http.CanonicalHeaderKey", edit.ReplaceWithNode(pass.Fset, op.Index, call))
@@ -2009,7 +2007,9 @@ func CheckLoopCondition(pass *analysis.Pass) (interface{}, error) {
return true
}
- Inspect(fn.Source(), cb)
+ if source := fn.Source(); source != nil {
+ ast.Inspect(source, cb)
+ }
}
return nil, nil
}
@@ -2083,7 +2083,9 @@ func CheckArgOverwritten(pass *analysis.Pass) (interface{}, error) {
}
return true
}
- Inspect(fn.Source(), cb)
+ if source := fn.Source(); source != nil {
+ ast.Inspect(source, cb)
+ }
}
return nil, nil
}
@@ -2105,7 +2107,7 @@ func CheckIneffectiveLoop(pass *analysis.Pass) (interface{}, error) {
case *ast.FuncLit:
body = fn.Body
default:
- ExhaustiveTypeSwitch(node)
+ lint.ExhaustiveTypeSwitch(node)
}
if body == nil {
return
@@ -2201,13 +2203,13 @@ var checkNilContextQ = pattern.MustParse(`(CallExpr fun@(Function _) (Builtin "n
func CheckNilContext(pass *analysis.Pass) (interface{}, error) {
todo := &ast.CallExpr{
- Fun: Selector("context", "TODO"),
+ Fun: edit.Selector("context", "TODO"),
}
bg := &ast.CallExpr{
- Fun: Selector("context", "Background"),
+ Fun: edit.Selector("context", "Background"),
}
fn := func(node ast.Node) {
- m, ok := Match(pass, checkNilContextQ, node)
+ m, ok := code.Match(pass, checkNilContextQ, node)
if !ok {
return
}
@@ -2244,7 +2246,7 @@ var (
func CheckSeeker(pass *analysis.Pass) (interface{}, error) {
fn := func(node ast.Node) {
- if _, edits, ok := MatchAndEdit(pass, checkSeekerQ, checkSeekerR, node); ok {
+ if _, edits, ok := code.MatchAndEdit(pass, checkSeekerQ, checkSeekerR, node); ok {
report.Report(pass, node, "the first argument of io.Seeker is the offset, but an io.Seek* constant is being used instead",
report.Fixes(edit.Fix("swap arguments", edits...)))
}
@@ -2402,7 +2404,7 @@ func CheckCyclicFinalizer(pass *analysis.Pass) (interface{}, error) {
if callee.RelString(nil) != "runtime.SetFinalizer" {
return
}
- arg0 := site.Common().Args[Arg("runtime.SetFinalizer.obj")]
+ arg0 := site.Common().Args[knowledge.Arg("runtime.SetFinalizer.obj")]
if iface, ok := arg0.(*ir.MakeInterface); ok {
arg0 = iface.X
}
@@ -2414,7 +2416,7 @@ func CheckCyclicFinalizer(pass *analysis.Pass) (interface{}, error) {
if !ok {
return
}
- arg1 := site.Common().Args[Arg("runtime.SetFinalizer.finalizer")]
+ arg1 := site.Common().Args[knowledge.Arg("runtime.SetFinalizer.finalizer")]
if iface, ok := arg1.(*ir.MakeInterface); ok {
arg1 = iface.X
}
@@ -2598,7 +2600,7 @@ func CheckLeakyTimeTick(pass *analysis.Pass) (interface{}, error) {
if !ok || !code.IsCallTo(call.Common(), "time.Tick") {
continue
}
- if !functions.Terminates(call.Parent()) {
+ if !code.Terminates(call.Parent()) {
continue
}
report.Report(pass, call, "using time.Tick leaks the underlying ticker, consider using it only in endless functions, tests and the main package, and use time.NewTicker here")
@@ -2612,7 +2614,7 @@ var checkDoubleNegationQ = pattern.MustParse(`(UnaryExpr "!" single@(UnaryExpr "
func CheckDoubleNegation(pass *analysis.Pass) (interface{}, error) {
fn := func(node ast.Node) {
- if m, ok := Match(pass, checkDoubleNegationQ, node); ok {
+ if m, ok := code.Match(pass, checkDoubleNegationQ, node); ok {
report.Report(pass, node, "negating a boolean twice has no effect; is this a typo?", report.Fixes(
edit.Fix("turn into single negation", edit.ReplaceWithNode(pass.Fset, node, m.State["single"].(ast.Node))),
edit.Fix("remove double negation", edit.ReplaceWithNode(pass.Fset, node, m.State["x"].(ast.Node)))))
@@ -2928,7 +2930,7 @@ func CheckDeprecated(pass *analysis.Pass) (interface{}, error) {
// already in 1.0, and we're targeting 1.2, it still
// makes sense to use the alternative from 1.0, to be
// future-proof.
- minVersion := deprecated.Stdlib[code.SelectorName(pass, sel)].AlternativeAvailableSince
+ minVersion := knowledge.StdlibDeprecations[code.SelectorName(pass, sel)].AlternativeAvailableSince
if !code.IsGoVersion(pass, minVersion) {
return true
}
@@ -3140,7 +3142,9 @@ func CheckEmptyBranch(pass *analysis.Pass) (interface{}, error) {
report.Report(pass, ifstmt, "empty branch", report.FilterGenerated(), report.ShortRange())
return true
}
- Inspect(fn.Source(), cb)
+ if source := fn.Source(); source != nil {
+ ast.Inspect(source, cb)
+ }
}
return nil, nil
}
@@ -3430,7 +3434,7 @@ var (
func CheckToLowerToUpperComparison(pass *analysis.Pass) (interface{}, error) {
fn := func(node ast.Node) {
- m, ok := Match(pass, checkToLowerToUpperComparisonQ, node)
+ m, ok := code.Match(pass, checkToLowerToUpperComparisonQ, node)
if !ok {
return
}
@@ -3522,7 +3526,7 @@ var checkSingleArgAppendQ = pattern.MustParse(`(CallExpr (Builtin "append") [_])
func CheckSingleArgAppend(pass *analysis.Pass) (interface{}, error) {
fn := func(node ast.Node) {
- _, ok := Match(pass, checkSingleArgAppendQ, node)
+ _, ok := code.Match(pass, checkSingleArgAppendQ, node)
if !ok {
return
}
@@ -3845,7 +3849,7 @@ var checkAddressIsNilQ = pattern.MustParse(
func CheckAddressIsNil(pass *analysis.Pass) (interface{}, error) {
fn := func(node ast.Node) {
- _, ok := Match(pass, checkAddressIsNilQ, node)
+ _, ok := code.Match(pass, checkAddressIsNilQ, node)
if !ok {
return
}
diff --git a/staticcheck/rules.go b/staticcheck/rules.go
index 57f7282de..b3200362f 100644
--- a/staticcheck/rules.go
+++ b/staticcheck/rules.go
@@ -13,9 +13,10 @@ import (
"time"
"unicode/utf8"
+ "honnef.co/go/tools/analysis/code"
+ "honnef.co/go/tools/go/ir"
+
"golang.org/x/tools/go/analysis"
- "honnef.co/go/tools/code"
- "honnef.co/go/tools/ir"
)
const (
diff --git a/stylecheck/analysis.go b/stylecheck/analysis.go
index 7d8e6f3e0..cb722e815 100644
--- a/stylecheck/analysis.go
+++ b/stylecheck/analysis.go
@@ -1,16 +1,16 @@
package stylecheck
import (
+ "honnef.co/go/tools/analysis/facts"
+ "honnef.co/go/tools/analysis/lint"
"honnef.co/go/tools/config"
- "honnef.co/go/tools/facts"
"honnef.co/go/tools/internal/passes/buildir"
- "honnef.co/go/tools/lint/lintutil"
"golang.org/x/tools/go/analysis"
"golang.org/x/tools/go/analysis/passes/inspect"
)
-var Analyzers = lintutil.InitializeAnalyzers(Docs, map[string]*analysis.Analyzer{
+var Analyzers = lint.InitializeAnalyzers(Docs, map[string]*analysis.Analyzer{
"ST1000": {
Run: CheckPackageComment,
},
diff --git a/stylecheck/doc.go b/stylecheck/doc.go
index b8e7f3f9e..bceba59c8 100644
--- a/stylecheck/doc.go
+++ b/stylecheck/doc.go
@@ -1,6 +1,6 @@
package stylecheck
-import "honnef.co/go/tools/lint"
+import "honnef.co/go/tools/analysis/lint"
var Docs = map[string]*lint.Documentation{
"ST1000": {
diff --git a/stylecheck/lint.go b/stylecheck/lint.go
index 2055a3968..d8edd720e 100644
--- a/stylecheck/lint.go
+++ b/stylecheck/lint.go
@@ -1,4 +1,4 @@
-package stylecheck // import "honnef.co/go/tools/stylecheck"
+package stylecheck
import (
"fmt"
@@ -12,14 +12,14 @@ import (
"unicode"
"unicode/utf8"
- "honnef.co/go/tools/code"
+ "honnef.co/go/tools/analysis/code"
+ "honnef.co/go/tools/analysis/edit"
+ "honnef.co/go/tools/analysis/lint"
+ "honnef.co/go/tools/analysis/report"
"honnef.co/go/tools/config"
- "honnef.co/go/tools/edit"
+ "honnef.co/go/tools/go/ir"
"honnef.co/go/tools/internal/passes/buildir"
- "honnef.co/go/tools/ir"
- . "honnef.co/go/tools/lint/lintdsl"
"honnef.co/go/tools/pattern"
- "honnef.co/go/tools/report"
"golang.org/x/tools/go/analysis"
"golang.org/x/tools/go/analysis/passes/inspect"
@@ -651,7 +651,7 @@ var (
func CheckYodaConditions(pass *analysis.Pass) (interface{}, error) {
fn := func(node ast.Node) {
- if _, edits, ok := MatchAndEdit(pass, checkYodaConditionsQ, checkYodaConditionsR, node); ok {
+ if _, edits, ok := code.MatchAndEdit(pass, checkYodaConditionsQ, checkYodaConditionsR, node); ok {
report.Report(pass, node, "don't use Yoda conditions",
report.FilterGenerated(),
report.Fixes(edit.Fix("un-Yoda-fy", edits...)))
@@ -788,7 +788,7 @@ func CheckExportedFunctionDocs(pass *analysis.Pass) (interface{}, error) {
return
}
default:
- ExhaustiveTypeSwitch(T)
+ lint.ExhaustiveTypeSwitch(T)
}
}
prefix := decl.Name.Name + " "
@@ -856,7 +856,7 @@ func CheckExportedTypeDocs(pass *analysis.Pass) (interface{}, error) {
case *ast.FuncLit, *ast.FuncDecl:
return false
default:
- ExhaustiveTypeSwitch(node)
+ lint.ExhaustiveTypeSwitch(node)
return false
}
}
@@ -907,7 +907,7 @@ func CheckExportedVarDocs(pass *analysis.Pass) (interface{}, error) {
case *ast.FuncLit, *ast.FuncDecl:
return false
default:
- ExhaustiveTypeSwitch(node)
+ lint.ExhaustiveTypeSwitch(node)
return false
}
}
diff --git a/stylecheck/names.go b/stylecheck/names.go
index ffc689e98..594bdf1f4 100644
--- a/stylecheck/names.go
+++ b/stylecheck/names.go
@@ -10,10 +10,11 @@ import (
"strings"
"unicode"
- "golang.org/x/tools/go/analysis"
- "honnef.co/go/tools/code"
+ "honnef.co/go/tools/analysis/code"
+ "honnef.co/go/tools/analysis/report"
"honnef.co/go/tools/config"
- "honnef.co/go/tools/report"
+
+ "golang.org/x/tools/go/analysis"
)
// knownNameExceptions is a set of names that are known to be exempt from naming checks.
diff --git a/unused/unused.go b/unused/unused.go
index fc4130d73..17d840230 100644
--- a/unused/unused.go
+++ b/unused/unused.go
@@ -9,13 +9,15 @@ import (
"reflect"
"strings"
- "golang.org/x/tools/go/analysis"
- "honnef.co/go/tools/code"
- "honnef.co/go/tools/facts"
+ "honnef.co/go/tools/analysis/code"
+ "honnef.co/go/tools/analysis/facts"
+ "honnef.co/go/tools/analysis/lint"
+ "honnef.co/go/tools/analysis/report"
+ "honnef.co/go/tools/go/ir"
"honnef.co/go/tools/go/types/typeutil"
"honnef.co/go/tools/internal/passes/buildir"
- "honnef.co/go/tools/ir"
- "honnef.co/go/tools/report"
+
+ "golang.org/x/tools/go/analysis"
)
var Debug io.Writer
@@ -397,7 +399,7 @@ type pkg struct {
TypesSizes types.Sizes
IR *ir.Package
SrcFuncs []*ir.Function
- Directives []facts.Directive
+ Directives []lint.Directive
}
// TODO(dh): should we return a map instead of two slices?
@@ -502,7 +504,7 @@ func debugf(f string, v ...interface{}) {
func run(pass *analysis.Pass) (interface{}, error) {
irpkg := pass.ResultOf[buildir.Analyzer].(*buildir.IR)
- dirs := pass.ResultOf[facts.Directives].([]facts.Directive)
+ dirs := pass.ResultOf[facts.Directives].([]lint.Directive)
pkg := &pkg{
Fset: pass.Fset,
Files: pass.Files,
From e306c661babf7e7dc9f0d784f83d820d7999c20e Mon Sep 17 00:00:00 2001
From: Alexey Surikov
Date: Sun, 9 Feb 2020 05:01:06 +0100
Subject: [PATCH 053/111] staticcheck: flag dubious bit shifting of fixed size
integers
---
staticcheck/analysis.go | 4 +
staticcheck/doc.go | 23 +++++
staticcheck/lint.go | 54 ++++++++++++
staticcheck/lint_test.go | 1 +
.../CheckStaticBitShift.go | 87 +++++++++++++++++++
5 files changed, 169 insertions(+)
create mode 100644 staticcheck/testdata/src/CheckStaticBitShift/CheckStaticBitShift.go
diff --git a/staticcheck/analysis.go b/staticcheck/analysis.go
index 37f973388..76dcd4964 100644
--- a/staticcheck/analysis.go
+++ b/staticcheck/analysis.go
@@ -259,6 +259,10 @@ var Analyzers = lint.InitializeAnalyzers(Docs, map[string]*analysis.Analyzer{
},
// Filtering generated code because it may include empty structs generated from data models.
"SA9005": makeCallCheckerAnalyzer(checkNoopMarshal, facts.Generated),
+ "SA9006": {
+ Run: CheckStaticBitShift,
+ Requires: []*analysis.Analyzer{inspect.Analyzer},
+ },
"SA4022": {
Run: CheckAddressIsNil,
diff --git a/staticcheck/doc.go b/staticcheck/doc.go
index 911b00001..a9b89fa31 100644
--- a/staticcheck/doc.go
+++ b/staticcheck/doc.go
@@ -877,4 +877,27 @@ marshaling behavior, e.g. via MarshalJSON methods. It will also not
flag empty structs.`,
Since: "2019.2",
},
+
+ "SA9006": {
+ Title: `Dubious bit shifting of a fixed size integer value`,
+ Text: `Bit shifting a value past its size will always clear the value.
+
+For instance:
+
+ v := int8(42)
+ v >>= 8
+
+will always result in 0.
+
+This check flags bit shifiting operations on fixed size integer values only.
+That is, int, uint and uintptr are never flagged to avoid potential false
+positives in somewhat exotic but valid bit twiddling tricks:
+
+ // Clear any value above 32 bits if integers are more than 32 bits.
+ func f(i int) int {
+ v := i >> 32
+ v = v << 32
+ return i-v
+ }`,
+ },
}
diff --git a/staticcheck/lint.go b/staticcheck/lint.go
index a344c36c3..3603d3a72 100644
--- a/staticcheck/lint.go
+++ b/staticcheck/lint.go
@@ -3858,3 +3858,57 @@ func CheckAddressIsNil(pass *analysis.Pass) (interface{}, error) {
code.Preorder(pass, fn, (*ast.BinaryExpr)(nil))
return nil, nil
}
+
+var (
+ checkFixedLengthTypeShiftQ = pattern.MustParse(`
+ (Or
+ (AssignStmt _ (Or ">>=" "<<=") _)
+ (BinaryExpr _ (Or ">>" "<<") _))
+ `)
+)
+
+func CheckStaticBitShift(pass *analysis.Pass) (interface{}, error) {
+ isDubiousShift := func(x, y ast.Expr) (int64, int64, bool) {
+ typ, ok := pass.TypesInfo.TypeOf(x).(*types.Basic)
+ if !ok {
+ return 0, 0, false
+ }
+ switch typ.Kind() {
+ case types.Int8, types.Int16, types.Int32, types.Int64,
+ types.Uint8, types.Uint16, types.Uint32, types.Uint64:
+ // We're only interested in fixed–size types.
+ default:
+ return 0, 0, false
+ }
+
+ const bitsInByte = 8
+ typeBits := pass.TypesSizes.Sizeof(typ) * bitsInByte
+
+ shiftLength, ok := code.ExprToInt(pass, y)
+ if !ok {
+ return 0, 0, false
+ }
+
+ return typeBits, shiftLength, shiftLength >= typeBits
+ }
+
+ fn := func(node ast.Node) {
+ if _, ok := code.Match(pass, checkFixedLengthTypeShiftQ, node); !ok {
+ return
+ }
+
+ switch e := node.(type) {
+ case *ast.AssignStmt:
+ if size, shift, yes := isDubiousShift(e.Lhs[0], e.Rhs[0]); yes {
+ report.Report(pass, e, fmt.Sprintf("shifting %d-bit value by %d bits will always clear it", size, shift))
+ }
+ case *ast.BinaryExpr:
+ if size, shift, yes := isDubiousShift(e.X, e.Y); yes {
+ report.Report(pass, e, fmt.Sprintf("shifting %d-bit value by %d bits will always clear it", size, shift))
+ }
+ }
+ }
+ code.Preorder(pass, fn, (*ast.AssignStmt)(nil), (*ast.BinaryExpr)(nil))
+
+ return nil, nil
+}
diff --git a/staticcheck/lint_test.go b/staticcheck/lint_test.go
index c5ea70161..2a1615696 100644
--- a/staticcheck/lint_test.go
+++ b/staticcheck/lint_test.go
@@ -90,6 +90,7 @@ func TestAll(t *testing.T) {
"SA9003": {{Dir: "CheckEmptyBranch"}},
"SA9004": {{Dir: "CheckMissingEnumTypesInDeclaration"}},
"SA9005": {{Dir: "CheckNoopMarshal"}},
+ "SA9006": {{Dir: "CheckStaticBitShift"}},
}
testutil.Run(t, Analyzers, checks)
diff --git a/staticcheck/testdata/src/CheckStaticBitShift/CheckStaticBitShift.go b/staticcheck/testdata/src/CheckStaticBitShift/CheckStaticBitShift.go
new file mode 100644
index 000000000..efa583308
--- /dev/null
+++ b/staticcheck/testdata/src/CheckStaticBitShift/CheckStaticBitShift.go
@@ -0,0 +1,87 @@
+package pkg
+
+// Partially copied from go vet's test suite.
+
+// Copyright 2014 The Go Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE-THIRD-PARTY file.
+
+func fn() {
+ var i8 int8
+ _ = i8 << 7
+ _ = (i8 + 1) << 8 // want `will always clear it`
+ _ = i8 << (7 + 1) // want `will always clear it`
+ _ = i8 >> 8 // want `will always clear it`
+ i8 <<= 8 // want `will always clear it`
+ i8 >>= 8 // want `will always clear it`
+
+ var i16 int16
+ _ = i16 << 15
+ _ = i16 << 16 // want `will always clear it`
+ _ = i16 >> 16 // want `will always clear it`
+ i16 <<= 16 // want `will always clear it`
+ i16 >>= 16 // want `will always clear it`
+
+ var i32 int32
+ _ = i32 << 31
+ _ = i32 << 32 // want `will always clear it`
+ _ = i32 >> 32 // want `will always clear it`
+ i32 <<= 32 // want `will always clear it`
+ i32 >>= 32 // want `will always clear it`
+
+ var i64 int64
+ _ = i64 << 63
+ _ = i64 << 64 // want `will always clear it`
+ _ = i64 >> 64 // want `will always clear it`
+ i64 <<= 64 // want `will always clear it`
+ i64 >>= 64 // want `will always clear it`
+
+ var u8 uint8
+ _ = u8 << 7
+ _ = u8 << 8 // want `will always clear it`
+ _ = u8 >> 8 // want `will always clear it`
+ u8 <<= 8 // want `will always clear it`
+ u8 >>= 8 // want `will always clear it`
+
+ var u16 uint16
+ _ = u16 << 15
+ _ = u16 << 16 // want `will always clear it`
+ _ = u16 >> 16 // want `will always clear it`
+ u16 <<= 16 // want `will always clear it`
+ u16 >>= 16 // want `will always clear it`
+
+ var u32 uint32
+ _ = u32 << 31
+ _ = u32 << 32 // want `will always clear it`
+ _ = u32 >> 32 // want `will always clear it`
+ u32 <<= 32 // want `will always clear it`
+ u32 >>= 32 // want `will always clear it`
+
+ var u64 uint64
+ _ = u64 << 63
+ _ = u64 << 64 // want `will always clear it`
+ _ = u64 >> 64 // want `will always clear it`
+ u64 <<= 64 // want `will always clear it`
+ u64 >>= 64 // want `will always clear it`
+ _ = u64 << u64
+}
+
+func fn1() {
+ var ui uint
+ _ = ui << 64
+ _ = ui >> 64
+ ui <<= 64
+ ui >>= 64
+
+ var uptr uintptr
+ _ = uptr << 64
+ _ = uptr >> 64
+ uptr <<= 64
+ uptr >>= 64
+
+ var i int
+ _ = i << 64
+ _ = i >> 64
+ i <<= 64
+ i >>= 64
+}
From dc48e03c6a7c6073a75a10cf87d90b0eea73b24b Mon Sep 17 00:00:00 2001
From: Dominik Honnef
Date: Sat, 16 May 2020 16:54:39 +0200
Subject: [PATCH 054/111] SA9006: also check custom types
---
staticcheck/lint.go | 2 +-
.../testdata/src/CheckStaticBitShift/CheckStaticBitShift.go | 5 +++++
2 files changed, 6 insertions(+), 1 deletion(-)
diff --git a/staticcheck/lint.go b/staticcheck/lint.go
index 3603d3a72..72c2bb5bf 100644
--- a/staticcheck/lint.go
+++ b/staticcheck/lint.go
@@ -3869,7 +3869,7 @@ var (
func CheckStaticBitShift(pass *analysis.Pass) (interface{}, error) {
isDubiousShift := func(x, y ast.Expr) (int64, int64, bool) {
- typ, ok := pass.TypesInfo.TypeOf(x).(*types.Basic)
+ typ, ok := pass.TypesInfo.TypeOf(x).Underlying().(*types.Basic)
if !ok {
return 0, 0, false
}
diff --git a/staticcheck/testdata/src/CheckStaticBitShift/CheckStaticBitShift.go b/staticcheck/testdata/src/CheckStaticBitShift/CheckStaticBitShift.go
index efa583308..c9db0c908 100644
--- a/staticcheck/testdata/src/CheckStaticBitShift/CheckStaticBitShift.go
+++ b/staticcheck/testdata/src/CheckStaticBitShift/CheckStaticBitShift.go
@@ -6,7 +6,12 @@ package pkg
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE-THIRD-PARTY file.
+type Number int8
+
func fn() {
+ var n8 Number
+ n8 <<= 8 // want `will always clear it`
+
var i8 int8
_ = i8 << 7
_ = (i8 + 1) << 8 // want `will always clear it`
From 1dc5519e1dce5be065fb2ec52c5a87e40e11029e Mon Sep 17 00:00:00 2001
From: Dominik Honnef
Date: Sat, 16 May 2020 16:58:34 +0200
Subject: [PATCH 055/111] SA9005: add more unit tests
Check for values that are larger than the type. All existing tests
only checked with values that matched the size.
---
.../src/CheckStaticBitShift/CheckStaticBitShift.go | 8 ++++++++
1 file changed, 8 insertions(+)
diff --git a/staticcheck/testdata/src/CheckStaticBitShift/CheckStaticBitShift.go b/staticcheck/testdata/src/CheckStaticBitShift/CheckStaticBitShift.go
index c9db0c908..d69fc0991 100644
--- a/staticcheck/testdata/src/CheckStaticBitShift/CheckStaticBitShift.go
+++ b/staticcheck/testdata/src/CheckStaticBitShift/CheckStaticBitShift.go
@@ -19,6 +19,7 @@ func fn() {
_ = i8 >> 8 // want `will always clear it`
i8 <<= 8 // want `will always clear it`
i8 >>= 8 // want `will always clear it`
+ i8 <<= 12 // want `will always clear it`
var i16 int16
_ = i16 << 15
@@ -26,6 +27,7 @@ func fn() {
_ = i16 >> 16 // want `will always clear it`
i16 <<= 16 // want `will always clear it`
i16 >>= 16 // want `will always clear it`
+ i16 <<= 18 // want `will always clear it`
var i32 int32
_ = i32 << 31
@@ -33,6 +35,7 @@ func fn() {
_ = i32 >> 32 // want `will always clear it`
i32 <<= 32 // want `will always clear it`
i32 >>= 32 // want `will always clear it`
+ i32 <<= 40 // want `will always clear it`
var i64 int64
_ = i64 << 63
@@ -40,6 +43,7 @@ func fn() {
_ = i64 >> 64 // want `will always clear it`
i64 <<= 64 // want `will always clear it`
i64 >>= 64 // want `will always clear it`
+ i64 <<= 70 // want `will always clear it`
var u8 uint8
_ = u8 << 7
@@ -47,6 +51,7 @@ func fn() {
_ = u8 >> 8 // want `will always clear it`
u8 <<= 8 // want `will always clear it`
u8 >>= 8 // want `will always clear it`
+ u8 <<= 12 // want `will always clear it`
var u16 uint16
_ = u16 << 15
@@ -54,6 +59,7 @@ func fn() {
_ = u16 >> 16 // want `will always clear it`
u16 <<= 16 // want `will always clear it`
u16 >>= 16 // want `will always clear it`
+ u16 <<= 18 // want `will always clear it`
var u32 uint32
_ = u32 << 31
@@ -61,6 +67,7 @@ func fn() {
_ = u32 >> 32 // want `will always clear it`
u32 <<= 32 // want `will always clear it`
u32 >>= 32 // want `will always clear it`
+ u32 <<= 40 // want `will always clear it`
var u64 uint64
_ = u64 << 63
@@ -68,6 +75,7 @@ func fn() {
_ = u64 >> 64 // want `will always clear it`
u64 <<= 64 // want `will always clear it`
u64 >>= 64 // want `will always clear it`
+ u64 <<= 70 // want `will always clear it`
_ = u64 << u64
}
From 21d01cff3791d79442e81bc489b2f09082af2638 Mon Sep 17 00:00:00 2001
From: Dominik Honnef
Date: Wed, 20 May 2020 12:08:21 +0200
Subject: [PATCH 056/111] Move most helpers out of analysis/code
Most helpers that lived in analysis/code acted purely on either type
information, the AST, or the IR. Such code should live in typeutil,
astutil, and irutil respectively. This simplifies the import graph and
avoids potential circular dependencies, where typeutil couldn't reuse
helpers that existed in analysos/code, because analysis/code itself
needed typeutil.
At this point, analysis/code only contains helpers that act on both
the AST and information derived from the analysis framework (most
often a handle to TypeInfo). Most of these helpers could probably be
turned into shims, to move the actual implementations into another
package. We just use the *analysis.Pass as an easy way of getting to
the type information of a package.
---
analysis/code/code.go | 241 +-----------------
analysis/code/stub.go | 10 -
analysis/facts/purity.go | 30 +--
go/ast/astutil/util.go | 65 +++++
{analysis/code => go/ir/irutil}/loops.go | 2 +-
go/ir/irutil/stub.go | 32 +++
{analysis/code => go/ir/irutil}/terminates.go | 2 +-
go/ir/irutil/util.go | 54 ++++
go/types/typeutil/util.go | 131 ++++++++++
internal/sharedcheck/lint.go | 7 +-
simple/lint.go | 21 +-
staticcheck/buildtag.go | 4 +-
staticcheck/lint.go | 106 ++++----
staticcheck/rules.go | 3 +-
stylecheck/lint.go | 27 +-
unused/unused.go | 15 +-
16 files changed, 386 insertions(+), 364 deletions(-)
delete mode 100644 analysis/code/stub.go
create mode 100644 go/ast/astutil/util.go
rename {analysis/code => go/ir/irutil}/loops.go (98%)
create mode 100644 go/ir/irutil/stub.go
rename {analysis/code => go/ir/irutil}/terminates.go (99%)
create mode 100644 go/types/typeutil/util.go
diff --git a/analysis/code/code.go b/analysis/code/code.go
index 515310b2a..cf7fd61e1 100644
--- a/analysis/code/code.go
+++ b/analysis/code/code.go
@@ -2,7 +2,6 @@
package code
import (
- "bytes"
"flag"
"fmt"
"go/ast"
@@ -10,154 +9,20 @@ import (
"go/token"
"go/types"
"strings"
- "sync"
"honnef.co/go/tools/analysis/facts"
- "honnef.co/go/tools/go/ir"
"honnef.co/go/tools/go/types/typeutil"
"golang.org/x/tools/go/analysis"
"golang.org/x/tools/go/ast/astutil"
)
-var bufferPool = &sync.Pool{
- New: func() interface{} {
- buf := bytes.NewBuffer(nil)
- buf.Grow(64)
- return buf
- },
-}
-
-func FuncName(f *types.Func) string {
- buf := bufferPool.Get().(*bytes.Buffer)
- buf.Reset()
- if f.Type() != nil {
- sig := f.Type().(*types.Signature)
- if recv := sig.Recv(); recv != nil {
- buf.WriteByte('(')
- if _, ok := recv.Type().(*types.Interface); ok {
- // gcimporter creates abstract methods of
- // named interfaces using the interface type
- // (not the named type) as the receiver.
- // Don't print it in full.
- buf.WriteString("interface")
- } else {
- types.WriteType(buf, recv.Type(), nil)
- }
- buf.WriteByte(')')
- buf.WriteByte('.')
- } else if f.Pkg() != nil {
- writePackage(buf, f.Pkg())
- }
- }
- buf.WriteString(f.Name())
- s := buf.String()
- bufferPool.Put(buf)
- return s
-}
-
-func writePackage(buf *bytes.Buffer, pkg *types.Package) {
- if pkg == nil {
- return
- }
- s := pkg.Path()
- if s != "" {
- buf.WriteString(s)
- buf.WriteByte('.')
- }
-}
-
type Positioner interface {
Pos() token.Pos
}
-func CallName(call *ir.CallCommon) string {
- if call.IsInvoke() {
- return ""
- }
- switch v := call.Value.(type) {
- case *ir.Function:
- fn, ok := v.Object().(*types.Func)
- if !ok {
- return ""
- }
- return FuncName(fn)
- case *ir.Builtin:
- return v.Name()
- }
- return ""
-}
-
-func IsCallTo(call *ir.CallCommon, name string) bool { return CallName(call) == name }
-
-func IsCallToAny(call *ir.CallCommon, names ...string) bool {
- q := CallName(call)
- for _, name := range names {
- if q == name {
- return true
- }
- }
- return false
-}
-
-// OPT(dh): IsType is kind of expensive; should we really use it?
-func IsType(T types.Type, name string) bool { return types.TypeString(T, nil) == name }
-
-func FilterDebug(instr []ir.Instruction) []ir.Instruction {
- var out []ir.Instruction
- for _, ins := range instr {
- if _, ok := ins.(*ir.DebugRef); !ok {
- out = append(out, ins)
- }
- }
- return out
-}
-
-func IsExample(fn *ir.Function) bool {
- if !strings.HasPrefix(fn.Name(), "Example") {
- return false
- }
- f := fn.Prog.Fset.File(fn.Pos())
- if f == nil {
- return false
- }
- return strings.HasSuffix(f.Name(), "_test.go")
-}
-
-func IsPointerLike(T types.Type) bool {
- switch T := T.Underlying().(type) {
- case *types.Interface, *types.Chan, *types.Map, *types.Signature, *types.Pointer:
- return true
- case *types.Basic:
- return T.Kind() == types.UnsafePointer
- }
- return false
-}
-
-func IsIdent(expr ast.Expr, ident string) bool {
- id, ok := expr.(*ast.Ident)
- return ok && id.Name == ident
-}
-
-// isBlank returns whether id is the blank identifier "_".
-// If id == nil, the answer is false.
-func IsBlank(id ast.Expr) bool {
- ident, _ := id.(*ast.Ident)
- return ident != nil && ident.Name == "_"
-}
-
-func IsIntLiteral(expr ast.Expr, literal string) bool {
- lit, ok := expr.(*ast.BasicLit)
- return ok && lit.Kind == token.INT && lit.Value == literal
-}
-
-// Deprecated: use IsIntLiteral instead
-func IsZero(expr ast.Expr) bool {
- return IsIntLiteral(expr, "0")
-}
-
func IsOfType(pass *analysis.Pass, expr ast.Expr, name string) bool {
- return IsType(pass.TypesInfo.TypeOf(expr), name)
+ return typeutil.IsType(pass.TypesInfo.TypeOf(expr), name)
}
func IsInTest(pass *analysis.Pass, node Positioner) bool {
@@ -266,25 +131,6 @@ func ExprToString(pass *analysis.Pass, expr ast.Expr) (string, bool) {
return constant.StringVal(val), true
}
-// Dereference returns a pointer's element type; otherwise it returns
-// T.
-func Dereference(T types.Type) types.Type {
- if p, ok := T.Underlying().(*types.Pointer); ok {
- return p.Elem()
- }
- return T
-}
-
-// DereferenceR returns a pointer's element type; otherwise it returns
-// T. If the element type is itself a pointer, DereferenceR will be
-// applied recursively.
-func DereferenceR(T types.Type) types.Type {
- if p, ok := T.Underlying().(*types.Pointer); ok {
- return DereferenceR(p.Elem())
- }
- return T
-}
-
func CallNameAST(pass *analysis.Pass, call *ast.CallExpr) string {
switch fun := astutil.Unparen(call.Fun).(type) {
case *ast.SelectorExpr:
@@ -292,12 +138,12 @@ func CallNameAST(pass *analysis.Pass, call *ast.CallExpr) string {
if !ok {
return ""
}
- return FuncName(fn)
+ return typeutil.FuncName(fn)
case *ast.Ident:
obj := pass.TypesInfo.ObjectOf(fun)
switch obj := obj.(type) {
case *types.Func:
- return FuncName(obj)
+ return typeutil.FuncName(obj)
case *types.Builtin:
return obj.Name()
default:
@@ -330,87 +176,6 @@ func IsCallToAnyAST(pass *analysis.Pass, node ast.Node, names ...string) bool {
return false
}
-func Preamble(f *ast.File) string {
- cutoff := f.Package
- if f.Doc != nil {
- cutoff = f.Doc.Pos()
- }
- var out []string
- for _, cmt := range f.Comments {
- if cmt.Pos() >= cutoff {
- break
- }
- out = append(out, cmt.Text())
- }
- return strings.Join(out, "\n")
-}
-
-func GroupSpecs(fset *token.FileSet, specs []ast.Spec) [][]ast.Spec {
- if len(specs) == 0 {
- return nil
- }
- groups := make([][]ast.Spec, 1)
- groups[0] = append(groups[0], specs[0])
-
- for _, spec := range specs[1:] {
- g := groups[len(groups)-1]
- if fset.PositionFor(spec.Pos(), false).Line-1 !=
- fset.PositionFor(g[len(g)-1].End(), false).Line {
-
- groups = append(groups, nil)
- }
-
- groups[len(groups)-1] = append(groups[len(groups)-1], spec)
- }
-
- return groups
-}
-
-func IsObject(obj types.Object, name string) bool {
- var path string
- if pkg := obj.Pkg(); pkg != nil {
- path = pkg.Path() + "."
- }
- return path+obj.Name() == name
-}
-
-type Field struct {
- Var *types.Var
- Tag string
- Path []int
-}
-
-// FlattenFields recursively flattens T and embedded structs,
-// returning a list of fields. If multiple fields with the same name
-// exist, all will be returned.
-func FlattenFields(T *types.Struct) []Field {
- return flattenFields(T, nil, nil)
-}
-
-func flattenFields(T *types.Struct, path []int, seen map[types.Type]bool) []Field {
- if seen == nil {
- seen = map[types.Type]bool{}
- }
- if seen[T] {
- return nil
- }
- seen[T] = true
- var out []Field
- for i := 0; i < T.NumFields(); i++ {
- field := T.Field(i)
- tag := T.Tag(i)
- np := append(path[:len(path):len(path)], i)
- if field.Anonymous() {
- if s, ok := Dereference(field.Type()).Underlying().(*types.Struct); ok {
- out = append(out, flattenFields(s, np, seen)...)
- }
- } else {
- out = append(out, Field{field, tag, np})
- }
- }
- return out
-}
-
func File(pass *analysis.Pass, node Positioner) *ast.File {
m := pass.ResultOf[facts.TokenFile].(map[*token.File]*ast.File)
return m[pass.Fset.File(node.Pos())]
diff --git a/analysis/code/stub.go b/analysis/code/stub.go
deleted file mode 100644
index 284827409..000000000
--- a/analysis/code/stub.go
+++ /dev/null
@@ -1,10 +0,0 @@
-package code
-
-import (
- "honnef.co/go/tools/analysis/facts"
- "honnef.co/go/tools/go/ir"
-)
-
-func IsStub(fn *ir.Function) bool {
- return facts.IsStub(fn)
-}
diff --git a/analysis/facts/purity.go b/analysis/facts/purity.go
index d708c841c..582b6209e 100644
--- a/analysis/facts/purity.go
+++ b/analysis/facts/purity.go
@@ -5,6 +5,7 @@ import (
"reflect"
"honnef.co/go/tools/go/ir"
+ "honnef.co/go/tools/go/ir/irutil"
"honnef.co/go/tools/internal/passes/buildir"
"golang.org/x/tools/go/analysis"
@@ -54,33 +55,6 @@ var pureStdlib = map[string]struct{}{
"(*net/http.Request).WithContext": {},
}
-// IsStub reports whether a function is a stub. A function is
-// considered a stub if it has no instructions or if all it does is
-// return a constant value.
-func IsStub(fn *ir.Function) bool {
- for _, b := range fn.Blocks {
- for _, instr := range b.Instrs {
- switch instr.(type) {
- case *ir.Const:
- // const naturally has no side-effects
- case *ir.Panic:
- // panic is a stub if it only uses constants
- case *ir.Return:
- // return is a stub if it only uses constants
- case *ir.DebugRef:
- case *ir.Jump:
- // if there are no disallowed instructions, then we're
- // only jumping to the exit block (or possibly
- // somewhere else that's stubby?)
- default:
- // all other instructions are assumed to do actual work
- return false
- }
- }
- }
- return true
-}
-
func purity(pass *analysis.Pass) (interface{}, error) {
seen := map[*ir.Function]struct{}{}
irpkg := pass.ResultOf[buildir.Analyzer].(*buildir.IR).Pkg
@@ -110,7 +84,7 @@ func purity(pass *analysis.Pass) (interface{}, error) {
}
}()
- if IsStub(fn) {
+ if irutil.IsStub(fn) {
return false
}
diff --git a/go/ast/astutil/util.go b/go/ast/astutil/util.go
new file mode 100644
index 000000000..6edd5df6f
--- /dev/null
+++ b/go/ast/astutil/util.go
@@ -0,0 +1,65 @@
+package astutil
+
+import (
+ "go/ast"
+ "go/token"
+ "strings"
+)
+
+func IsIdent(expr ast.Expr, ident string) bool {
+ id, ok := expr.(*ast.Ident)
+ return ok && id.Name == ident
+}
+
+// isBlank returns whether id is the blank identifier "_".
+// If id == nil, the answer is false.
+func IsBlank(id ast.Expr) bool {
+ ident, _ := id.(*ast.Ident)
+ return ident != nil && ident.Name == "_"
+}
+
+func IsIntLiteral(expr ast.Expr, literal string) bool {
+ lit, ok := expr.(*ast.BasicLit)
+ return ok && lit.Kind == token.INT && lit.Value == literal
+}
+
+// Deprecated: use IsIntLiteral instead
+func IsZero(expr ast.Expr) bool {
+ return IsIntLiteral(expr, "0")
+}
+
+func Preamble(f *ast.File) string {
+ cutoff := f.Package
+ if f.Doc != nil {
+ cutoff = f.Doc.Pos()
+ }
+ var out []string
+ for _, cmt := range f.Comments {
+ if cmt.Pos() >= cutoff {
+ break
+ }
+ out = append(out, cmt.Text())
+ }
+ return strings.Join(out, "\n")
+}
+
+func GroupSpecs(fset *token.FileSet, specs []ast.Spec) [][]ast.Spec {
+ if len(specs) == 0 {
+ return nil
+ }
+ groups := make([][]ast.Spec, 1)
+ groups[0] = append(groups[0], specs[0])
+
+ for _, spec := range specs[1:] {
+ g := groups[len(groups)-1]
+ if fset.PositionFor(spec.Pos(), false).Line-1 !=
+ fset.PositionFor(g[len(g)-1].End(), false).Line {
+
+ groups = append(groups, nil)
+ }
+
+ groups[len(groups)-1] = append(groups[len(groups)-1], spec)
+ }
+
+ return groups
+}
diff --git a/analysis/code/loops.go b/go/ir/irutil/loops.go
similarity index 98%
rename from analysis/code/loops.go
rename to go/ir/irutil/loops.go
index e2f263a84..751cc680b 100644
--- a/analysis/code/loops.go
+++ b/go/ir/irutil/loops.go
@@ -1,4 +1,4 @@
-package code
+package irutil
import "honnef.co/go/tools/go/ir"
diff --git a/go/ir/irutil/stub.go b/go/ir/irutil/stub.go
new file mode 100644
index 000000000..4311c7dbe
--- /dev/null
+++ b/go/ir/irutil/stub.go
@@ -0,0 +1,32 @@
+package irutil
+
+import (
+ "honnef.co/go/tools/go/ir"
+)
+
+// IsStub reports whether a function is a stub. A function is
+// considered a stub if it has no instructions or if all it does is
+// return a constant value.
+func IsStub(fn *ir.Function) bool {
+ for _, b := range fn.Blocks {
+ for _, instr := range b.Instrs {
+ switch instr.(type) {
+ case *ir.Const:
+ // const naturally has no side-effects
+ case *ir.Panic:
+ // panic is a stub if it only uses constants
+ case *ir.Return:
+ // return is a stub if it only uses constants
+ case *ir.DebugRef:
+ case *ir.Jump:
+ // if there are no disallowed instructions, then we're
+ // only jumping to the exit block (or possibly
+ // somewhere else that's stubby?)
+ default:
+ // all other instructions are assumed to do actual work
+ return false
+ }
+ }
+ }
+ return true
+}
diff --git a/analysis/code/terminates.go b/go/ir/irutil/terminates.go
similarity index 99%
rename from analysis/code/terminates.go
rename to go/ir/irutil/terminates.go
index 39d93129e..84e7503bb 100644
--- a/analysis/code/terminates.go
+++ b/go/ir/irutil/terminates.go
@@ -1,4 +1,4 @@
-package code
+package irutil
import (
"go/types"
diff --git a/go/ir/irutil/util.go b/go/ir/irutil/util.go
index badff17f2..dace40be0 100644
--- a/go/ir/irutil/util.go
+++ b/go/ir/irutil/util.go
@@ -1,7 +1,11 @@
package irutil
import (
+ "go/types"
+ "strings"
+
"honnef.co/go/tools/go/ir"
+ "honnef.co/go/tools/go/types/typeutil"
)
func Reachable(from, to *ir.BasicBlock) bool {
@@ -68,3 +72,53 @@ func Vararg(x *ir.Slice) ([]ir.Value, bool) {
}
return out, true
}
+
+func CallName(call *ir.CallCommon) string {
+ if call.IsInvoke() {
+ return ""
+ }
+ switch v := call.Value.(type) {
+ case *ir.Function:
+ fn, ok := v.Object().(*types.Func)
+ if !ok {
+ return ""
+ }
+ return typeutil.FuncName(fn)
+ case *ir.Builtin:
+ return v.Name()
+ }
+ return ""
+}
+
+func IsCallTo(call *ir.CallCommon, name string) bool { return CallName(call) == name }
+
+func IsCallToAny(call *ir.CallCommon, names ...string) bool {
+ q := CallName(call)
+ for _, name := range names {
+ if q == name {
+ return true
+ }
+ }
+ return false
+}
+
+func FilterDebug(instr []ir.Instruction) []ir.Instruction {
+ var out []ir.Instruction
+ for _, ins := range instr {
+ if _, ok := ins.(*ir.DebugRef); !ok {
+ out = append(out, ins)
+ }
+ }
+ return out
+}
+
+func IsExample(fn *ir.Function) bool {
+ if !strings.HasPrefix(fn.Name(), "Example") {
+ return false
+ }
+ f := fn.Prog.Fset.File(fn.Pos())
+ if f == nil {
+ return false
+ }
+ return strings.HasSuffix(f.Name(), "_test.go")
+}
diff --git a/go/types/typeutil/util.go b/go/types/typeutil/util.go
new file mode 100644
index 000000000..c96c1a7d3
--- /dev/null
+++ b/go/types/typeutil/util.go
@@ -0,0 +1,131 @@
+package typeutil
+
+import (
+ "bytes"
+ "go/types"
+ "sync"
+)
+
+var bufferPool = &sync.Pool{
+ New: func() interface{} {
+ buf := bytes.NewBuffer(nil)
+ buf.Grow(64)
+ return buf
+ },
+}
+
+func FuncName(f *types.Func) string {
+ buf := bufferPool.Get().(*bytes.Buffer)
+ buf.Reset()
+ if f.Type() != nil {
+ sig := f.Type().(*types.Signature)
+ if recv := sig.Recv(); recv != nil {
+ buf.WriteByte('(')
+ if _, ok := recv.Type().(*types.Interface); ok {
+ // gcimporter creates abstract methods of
+ // named interfaces using the interface type
+ // (not the named type) as the receiver.
+ // Don't print it in full.
+ buf.WriteString("interface")
+ } else {
+ types.WriteType(buf, recv.Type(), nil)
+ }
+ buf.WriteByte(')')
+ buf.WriteByte('.')
+ } else if f.Pkg() != nil {
+ writePackage(buf, f.Pkg())
+ }
+ }
+ buf.WriteString(f.Name())
+ s := buf.String()
+ bufferPool.Put(buf)
+ return s
+}
+
+func writePackage(buf *bytes.Buffer, pkg *types.Package) {
+ if pkg == nil {
+ return
+ }
+ s := pkg.Path()
+ if s != "" {
+ buf.WriteString(s)
+ buf.WriteByte('.')
+ }
+}
+
+// Dereference returns a pointer's element type; otherwise it returns
+// T.
+func Dereference(T types.Type) types.Type {
+ if p, ok := T.Underlying().(*types.Pointer); ok {
+ return p.Elem()
+ }
+ return T
+}
+
+// DereferenceR returns a pointer's element type; otherwise it returns
+// T. If the element type is itself a pointer, DereferenceR will be
+// applied recursively.
+func DereferenceR(T types.Type) types.Type {
+ if p, ok := T.Underlying().(*types.Pointer); ok {
+ return DereferenceR(p.Elem())
+ }
+ return T
+}
+
+func IsObject(obj types.Object, name string) bool {
+ var path string
+ if pkg := obj.Pkg(); pkg != nil {
+ path = pkg.Path() + "."
+ }
+ return path+obj.Name() == name
+}
+
+// OPT(dh): IsType is kind of expensive; should we really use it?
+func IsType(T types.Type, name string) bool { return types.TypeString(T, nil) == name }
+
+func IsPointerLike(T types.Type) bool {
+ switch T := T.Underlying().(type) {
+ case *types.Interface, *types.Chan, *types.Map, *types.Signature, *types.Pointer:
+ return true
+ case *types.Basic:
+ return T.Kind() == types.UnsafePointer
+ }
+ return false
+}
+
+type Field struct {
+ Var *types.Var
+ Tag string
+ Path []int
+}
+
+// FlattenFields recursively flattens T and embedded structs,
+// returning a list of fields. If multiple fields with the same name
+// exist, all will be returned.
+func FlattenFields(T *types.Struct) []Field {
+ return flattenFields(T, nil, nil)
+}
+
+func flattenFields(T *types.Struct, path []int, seen map[types.Type]bool) []Field {
+ if seen == nil {
+ seen = map[types.Type]bool{}
+ }
+ if seen[T] {
+ return nil
+ }
+ seen[T] = true
+ var out []Field
+ for i := 0; i < T.NumFields(); i++ {
+ field := T.Field(i)
+ tag := T.Tag(i)
+ np := append(path[:len(path):len(path)], i)
+ if field.Anonymous() {
+ if s, ok := Dereference(field.Type()).Underlying().(*types.Struct); ok {
+ out = append(out, flattenFields(s, np, seen)...)
+ }
+ } else {
+ out = append(out, Field{field, tag, np})
+ }
+ }
+ return out
+}
diff --git a/internal/sharedcheck/lint.go b/internal/sharedcheck/lint.go
index 6b0d31ba8..df6c82fb9 100644
--- a/internal/sharedcheck/lint.go
+++ b/internal/sharedcheck/lint.go
@@ -4,8 +4,9 @@ import (
"go/ast"
"go/types"
- "honnef.co/go/tools/analysis/code"
+ "honnef.co/go/tools/go/ast/astutil"
"honnef.co/go/tools/go/ir"
+ "honnef.co/go/tools/go/ir/irutil"
"honnef.co/go/tools/internal/passes/buildir"
"golang.org/x/tools/go/analysis"
@@ -15,7 +16,7 @@ func CheckRangeStringRunes(pass *analysis.Pass) (interface{}, error) {
for _, fn := range pass.ResultOf[buildir.Analyzer].(*buildir.IR).SrcFuncs {
cb := func(node ast.Node) bool {
rng, ok := node.(*ast.RangeStmt)
- if !ok || !code.IsBlank(rng.Key) {
+ if !ok || !astutil.IsBlank(rng.Key) {
return true
}
@@ -48,7 +49,7 @@ func CheckRangeStringRunes(pass *analysis.Pass) (interface{}, error) {
// Expect two refs: one for obtaining the length of the slice,
// one for accessing the elements
- if len(code.FilterDebug(*refs)) != 2 {
+ if len(irutil.FilterDebug(*refs)) != 2 {
// TODO(dh): right now, we check that only one place
// refers to our slice. This will miss cases such as
// ranging over the slice twice. Ideally, we'd ensure that
diff --git a/simple/lint.go b/simple/lint.go
index 0ade9bff8..9aca1b369 100644
--- a/simple/lint.go
+++ b/simple/lint.go
@@ -12,16 +12,19 @@ import (
"sort"
"strings"
- "golang.org/x/tools/go/analysis"
- "golang.org/x/tools/go/types/typeutil"
"honnef.co/go/tools/analysis/code"
"honnef.co/go/tools/analysis/edit"
"honnef.co/go/tools/analysis/lint"
"honnef.co/go/tools/analysis/report"
+ "honnef.co/go/tools/go/ast/astutil"
+ "honnef.co/go/tools/go/types/typeutil"
"honnef.co/go/tools/internal/passes/buildir"
"honnef.co/go/tools/internal/sharedcheck"
"honnef.co/go/tools/knowledge"
"honnef.co/go/tools/pattern"
+
+ "golang.org/x/tools/go/analysis"
+ gotypeutil "golang.org/x/tools/go/types/typeutil"
)
var (
@@ -497,7 +500,7 @@ func CheckRedundantNilCheckWithLen(pass *analysis.Pass) (interface{}, error) {
isConstZero := func(expr ast.Expr) (isConst bool, isZero bool) {
_, ok := expr.(*ast.BasicLit)
if ok {
- return true, code.IsIntLiteral(expr, "0")
+ return true, astutil.IsIntLiteral(expr, "0")
}
id, ok := expr.(*ast.Ident)
if !ok {
@@ -561,7 +564,7 @@ func CheckRedundantNilCheckWithLen(pass *analysis.Pass) (interface{}, error) {
return
}
- if eqNil && !code.IsIntLiteral(y.Y, "0") { // must be len(x) == *0*
+ if eqNil && !astutil.IsIntLiteral(y.Y, "0") { // must be len(x) == *0*
return
}
@@ -769,14 +772,14 @@ func CheckUnnecessaryBlank(pass *analysis.Pass) (interface{}, error) {
rs := node.(*ast.RangeStmt)
// for _
- if rs.Value == nil && code.IsBlank(rs.Key) {
+ if rs.Value == nil && astutil.IsBlank(rs.Key) {
report.Report(pass, rs.Key, "unnecessary assignment to the blank identifier",
report.FilterGenerated(),
report.Fixes(edit.Fix("remove assignment to blank identifier", edit.Delete(edit.Range{rs.Key.Pos(), rs.TokPos + 1}))))
}
// for _, _
- if code.IsBlank(rs.Key) && code.IsBlank(rs.Value) {
+ if astutil.IsBlank(rs.Key) && astutil.IsBlank(rs.Value) {
// FIXME we should mark both key and value
report.Report(pass, rs.Key, "unnecessary assignment to the blank identifier",
report.FilterGenerated(),
@@ -784,7 +787,7 @@ func CheckUnnecessaryBlank(pass *analysis.Pass) (interface{}, error) {
}
// for x, _
- if !code.IsBlank(rs.Key) && code.IsBlank(rs.Value) {
+ if !astutil.IsBlank(rs.Key) && astutil.IsBlank(rs.Value) {
report.Report(pass, rs.Value, "unnecessary assignment to the blank identifier",
report.FilterGenerated(),
report.Fixes(edit.Fix("remove assignment to blank identifier", edit.Delete(edit.Range{rs.Key.End(), rs.Value.End()}))))
@@ -1380,7 +1383,7 @@ func CheckRedundantBreak(pass *analysis.Pass) (interface{}, error) {
return nil, nil
}
-func isStringer(T types.Type, msCache *typeutil.MethodSetCache) bool {
+func isStringer(T types.Type, msCache *gotypeutil.MethodSetCache) bool {
ms := msCache.MethodSet(T)
sel := ms.Lookup(nil, "String")
if sel == nil {
@@ -1398,7 +1401,7 @@ func isStringer(T types.Type, msCache *typeutil.MethodSetCache) bool {
if sig.Results().Len() != 1 {
return false
}
- if !code.IsType(sig.Results().At(0).Type(), "string") {
+ if !typeutil.IsType(sig.Results().At(0).Type(), "string") {
return false
}
return true
diff --git a/staticcheck/buildtag.go b/staticcheck/buildtag.go
index 0ed3a93a6..97ccf77b4 100644
--- a/staticcheck/buildtag.go
+++ b/staticcheck/buildtag.go
@@ -4,12 +4,12 @@ import (
"go/ast"
"strings"
- "honnef.co/go/tools/analysis/code"
+ "honnef.co/go/tools/go/ast/astutil"
)
func buildTags(f *ast.File) [][]string {
var out [][]string
- for _, line := range strings.Split(code.Preamble(f), "\n") {
+ for _, line := range strings.Split(astutil.Preamble(f), "\n") {
if !strings.HasPrefix(line, "+build ") {
continue
}
diff --git a/staticcheck/lint.go b/staticcheck/lint.go
index 72c2bb5bf..2b3b5e867 100644
--- a/staticcheck/lint.go
+++ b/staticcheck/lint.go
@@ -23,8 +23,10 @@ import (
"honnef.co/go/tools/analysis/facts"
"honnef.co/go/tools/analysis/lint"
"honnef.co/go/tools/analysis/report"
+ "honnef.co/go/tools/go/ast/astutil"
"honnef.co/go/tools/go/ir"
"honnef.co/go/tools/go/ir/irutil"
+ "honnef.co/go/tools/go/types/typeutil"
"honnef.co/go/tools/internal/passes/buildir"
"honnef.co/go/tools/internal/sharedcheck"
"honnef.co/go/tools/knowledge"
@@ -33,9 +35,9 @@ import (
"golang.org/x/tools/go/analysis"
"golang.org/x/tools/go/analysis/passes/inspect"
- "golang.org/x/tools/go/ast/astutil"
+ goastutil "golang.org/x/tools/go/ast/astutil"
"golang.org/x/tools/go/ast/inspector"
- "golang.org/x/tools/go/types/typeutil"
+ gotypeutil "golang.org/x/tools/go/types/typeutil"
)
func checkSortSlice(call *Call) {
@@ -97,7 +99,7 @@ func unmarshalPointer(name string, arg int) CallCheck {
func pointlessIntMath(call *Call) {
if ConvertedFromInt(call.Args[0].Value) {
- call.Invalid(fmt.Sprintf("calling %s on a converted integer is pointless", code.CallName(call.Instr.Common())))
+ call.Invalid(fmt.Sprintf("calling %s on a converted integer is pointless", irutil.CallName(call.Instr.Common())))
}
}
@@ -151,7 +153,7 @@ var (
"(*sync.Pool).Put": func(call *Call) {
arg := call.Args[knowledge.Arg("(*sync.Pool).Put.x")]
typ := arg.Value.Value.Type()
- if !code.IsPointerLike(typ) {
+ if !typeutil.IsPointerLike(typ) {
arg.Invalid("argument should be pointer-like to avoid allocations")
}
},
@@ -351,7 +353,7 @@ var verbs = [...]verbFlag{
}
func checkPrintfCallImpl(carg *Argument, f ir.Value, args []ir.Value) {
- var msCache *typeutil.MethodSetCache
+ var msCache *gotypeutil.MethodSetCache
if f.Parent() != nil {
msCache = &f.Parent().Prog.MethodSets
}
@@ -365,7 +367,7 @@ func checkPrintfCallImpl(carg *Argument, f ir.Value, args []ir.Value) {
if verbs[verb]&isSlice != 0 {
return []types.Type{T}, false
}
- if verbs[verb]&isString != 0 && code.IsType(T.Elem().Underlying(), "byte") {
+ if verbs[verb]&isString != 0 && typeutil.IsType(T.Elem().Underlying(), "byte") {
return []types.Type{T}, false
}
return []types.Type{T.Elem()}, true
@@ -407,7 +409,7 @@ func checkPrintfCallImpl(carg *Argument, f ir.Value, args []ir.Value) {
if sig.Results().Len() != 1 {
return false
}
- if !code.IsType(sig.Results().At(0).Type(), "string") {
+ if !typeutil.IsType(sig.Results().At(0).Type(), "string") {
return false
}
return true
@@ -429,7 +431,7 @@ func checkPrintfCallImpl(carg *Argument, f ir.Value, args []ir.Value) {
if sig.Results().Len() != 1 {
return false
}
- if !code.IsType(sig.Results().At(0).Type(), "string") {
+ if !typeutil.IsType(sig.Results().At(0).Type(), "string") {
return false
}
return true
@@ -493,10 +495,10 @@ func checkPrintfCallImpl(carg *Argument, f ir.Value, args []ir.Value) {
T = T.Underlying()
if flags&(isPointer|isPseudoPointer) == 0 && top {
- T = code.Dereference(T)
+ T = typeutil.Dereference(T)
}
if flags&isPseudoPointer != 0 && top {
- t := code.Dereference(T)
+ t := typeutil.Dereference(T)
if _, ok := t.Underlying().(*types.Struct); ok {
T = t
}
@@ -547,7 +549,7 @@ func checkPrintfCallImpl(carg *Argument, f ir.Value, args []ir.Value) {
}
}
- if flags&isPointer != 0 && code.IsPointerLike(T) {
+ if flags&isPointer != 0 && typeutil.IsPointerLike(T) {
return true
}
if flags&isPseudoPointer != 0 {
@@ -724,7 +726,7 @@ func checkAtomicAlignmentImpl(call *Call) {
if off%8 != 0 {
msg := fmt.Sprintf("address of non 64-bit aligned field %s passed to %s",
T.Field(v.Field).Name(),
- code.CallName(call.Instr.Common()))
+ irutil.CallName(call.Instr.Common()))
call.Invalid(msg)
}
}
@@ -736,14 +738,14 @@ func checkNoopMarshalImpl(argN int, meths ...string) CallCheck {
}
arg := call.Args[argN]
T := arg.Value.Value.Type()
- Ts, ok := code.Dereference(T).Underlying().(*types.Struct)
+ Ts, ok := typeutil.Dereference(T).Underlying().(*types.Struct)
if !ok {
return
}
if Ts.NumFields() == 0 {
return
}
- fields := code.FlattenFields(Ts)
+ fields := typeutil.FlattenFields(Ts)
for _, field := range fields {
if field.Var.Exported() {
return
@@ -769,7 +771,7 @@ func checkUnsupportedMarshalImpl(argN int, tag string, meths ...string) CallChec
arg := call.Args[argN]
T := arg.Value.Value.Type()
- Ts, ok := code.Dereference(T).Underlying().(*types.Struct)
+ Ts, ok := typeutil.Dereference(T).Underlying().(*types.Struct)
if !ok {
return
}
@@ -781,7 +783,7 @@ func checkUnsupportedMarshalImpl(argN int, tag string, meths ...string) CallChec
return
}
}
- fields := code.FlattenFields(Ts)
+ fields := typeutil.FlattenFields(Ts)
for _, field := range fields {
if !(field.Var.Exported()) {
continue
@@ -808,7 +810,7 @@ func checkUnsupportedMarshalImpl(argN int, tag string, meths ...string) CallChec
func fieldPath(start types.Type, indices []int) string {
p := start.String()
for _, idx := range indices {
- field := code.Dereference(start).Underlying().(*types.Struct).Field(idx)
+ field := typeutil.Dereference(start).Underlying().(*types.Struct).Field(idx)
start = field.Type()
p += "." + field.Name()
}
@@ -816,7 +818,7 @@ func fieldPath(start types.Type, indices []int) string {
}
func isInLoop(b *ir.BasicBlock) bool {
- sets := code.FindLoops(b.Parent())
+ sets := irutil.FindLoops(b.Parent())
for _, set := range sets {
if set.Has(b) {
return true
@@ -1635,7 +1637,7 @@ func CheckBenchmarkN(pass *analysis.Pass) (interface{}, error) {
func CheckUnreadVariableValues(pass *analysis.Pass) (interface{}, error) {
for _, fn := range pass.ResultOf[buildir.Analyzer].(*buildir.IR).SrcFuncs {
- if code.IsExample(fn) {
+ if irutil.IsExample(fn) {
continue
}
node := fn.Source()
@@ -1828,7 +1830,7 @@ func CheckExtremeComparison(pass *analysis.Pass) (interface{}, error) {
if !ok {
return false
}
- return code.IsObject(pass.TypesInfo.ObjectOf(sel.Sel), name)
+ return typeutil.IsObject(pass.TypesInfo.ObjectOf(sel.Sel), name)
}
fn := func(node ast.Node) {
@@ -1881,12 +1883,12 @@ func CheckExtremeComparison(pass *analysis.Pass) (interface{}, error) {
}
if (basic.Info() & types.IsUnsigned) != 0 {
- if (expr.Op == token.LSS && code.IsIntLiteral(expr.Y, "0")) ||
- (expr.Op == token.GTR && code.IsIntLiteral(expr.X, "0")) {
+ if (expr.Op == token.LSS && astutil.IsIntLiteral(expr.Y, "0")) ||
+ (expr.Op == token.GTR && astutil.IsIntLiteral(expr.X, "0")) {
report.Report(pass, expr, fmt.Sprintf("no value of type %s is less than 0", basic))
}
- if expr.Op == token.GEQ && code.IsIntLiteral(expr.Y, "0") ||
- expr.Op == token.LEQ && code.IsIntLiteral(expr.X, "0") {
+ if expr.Op == token.GEQ && astutil.IsIntLiteral(expr.Y, "0") ||
+ expr.Op == token.LEQ && astutil.IsIntLiteral(expr.X, "0") {
report.Report(pass, expr, fmt.Sprintf("every value of type %s is >= 0", basic))
}
} else {
@@ -2050,7 +2052,7 @@ func CheckArgOverwritten(pass *analysis.Pass) (interface{}, error) {
if refs == nil {
continue
}
- if len(code.FilterDebug(*refs)) != 0 {
+ if len(irutil.FilterDebug(*refs)) != 0 {
continue
}
@@ -2227,7 +2229,7 @@ func CheckNilContext(pass *analysis.Pass) (interface{}, error) {
// the Foo method, but the method receiver.
return
}
- if !code.IsType(sig.Params().At(0).Type(), "context.Context") {
+ if !typeutil.IsType(sig.Params().At(0).Type(), "context.Context") {
return
}
report.Report(pass, call.Args[0],
@@ -2361,7 +2363,7 @@ func CheckConcurrentTesting(pass *analysis.Pass) (interface{}, error) {
if recv == nil {
continue
}
- if !code.IsType(recv.Type(), "*testing.common") {
+ if !typeutil.IsType(recv.Type(), "*testing.common") {
continue
}
fn, ok := call.Call.StaticCallee().Object().(*types.Func)
@@ -2467,7 +2469,7 @@ func CheckSliceOutOfBounds(pass *analysis.Pass) (interface{}, error) {
func CheckDeferLock(pass *analysis.Pass) (interface{}, error) {
for _, fn := range pass.ResultOf[buildir.Analyzer].(*buildir.IR).SrcFuncs {
for _, block := range fn.Blocks {
- instrs := code.FilterDebug(block.Instrs)
+ instrs := irutil.FilterDebug(block.Instrs)
if len(instrs) < 2 {
continue
}
@@ -2476,14 +2478,14 @@ func CheckDeferLock(pass *analysis.Pass) (interface{}, error) {
if !ok {
continue
}
- if !code.IsCallToAny(call.Common(), "(*sync.Mutex).Lock", "(*sync.RWMutex).RLock") {
+ if !irutil.IsCallToAny(call.Common(), "(*sync.Mutex).Lock", "(*sync.RWMutex).RLock") {
continue
}
nins, ok := instrs[i+1].(*ir.Defer)
if !ok {
continue
}
- if !code.IsCallToAny(&nins.Call, "(*sync.Mutex).Lock", "(*sync.RWMutex).RLock") {
+ if !irutil.IsCallToAny(&nins.Call, "(*sync.Mutex).Lock", "(*sync.RWMutex).RLock") {
continue
}
if call.Common().Args[0] != nins.Call.Args[0] {
@@ -2510,7 +2512,7 @@ func CheckNaNComparison(pass *analysis.Pass) (interface{}, error) {
if !ok {
return false
}
- return code.IsCallTo(call.Common(), "math.NaN")
+ return irutil.IsCallTo(call.Common(), "math.NaN")
}
for _, fn := range pass.ResultOf[buildir.Analyzer].(*buildir.IR).SrcFuncs {
for _, block := range fn.Blocks {
@@ -2597,10 +2599,10 @@ func CheckLeakyTimeTick(pass *analysis.Pass) (interface{}, error) {
for _, block := range fn.Blocks {
for _, ins := range block.Instrs {
call, ok := ins.(*ir.Call)
- if !ok || !code.IsCallTo(call.Common(), "time.Tick") {
+ if !ok || !irutil.IsCallTo(call.Common(), "time.Tick") {
continue
}
- if !code.Terminates(call.Parent()) {
+ if !irutil.Terminates(call.Parent()) {
continue
}
report.Report(pass, call, "using time.Tick leaks the underlying ticker, consider using it only in endless functions, tests and the main package, and use time.NewTicker here")
@@ -2695,12 +2697,12 @@ func CheckSillyBitwiseOps(pass *analysis.Pass) (interface{}, error) {
// of a pattern, x<<0, x<<8, x<<16, ...
continue
}
- path, _ := astutil.PathEnclosingInterval(code.File(pass, ins), ins.Pos(), ins.Pos())
+ path, _ := goastutil.PathEnclosingInterval(code.File(pass, ins), ins.Pos(), ins.Pos())
if len(path) == 0 {
continue
}
- if node, ok := path[0].(*ast.BinaryExpr); !ok || !code.IsIntLiteral(node.Y, "0") {
+ if node, ok := path[0].(*ast.BinaryExpr); !ok || !astutil.IsIntLiteral(node.Y, "0") {
continue
}
@@ -2743,7 +2745,7 @@ func CheckSillyBitwiseOps(pass *analysis.Pass) (interface{}, error) {
if v, _ := constant.Int64Val(obj.Val()); v != 0 {
return
}
- path, _ := astutil.PathEnclosingInterval(code.File(pass, obj), obj.Pos(), obj.Pos())
+ path, _ := goastutil.PathEnclosingInterval(code.File(pass, obj), obj.Pos(), obj.Pos())
if len(path) < 2 {
return
}
@@ -2771,7 +2773,7 @@ func CheckSillyBitwiseOps(pass *analysis.Pass) (interface{}, error) {
fmt.Sprintf("%s always equals %s; %s is defined as iota and has value 0, maybe %s is meant to be 1 << iota?", report.Render(pass, binop), report.Render(pass, binop.X), report.Render(pass, binop.Y), report.Render(pass, binop.Y)))
}
case *ast.BasicLit:
- if !code.IsIntLiteral(binop.Y, "0") {
+ if !astutil.IsIntLiteral(binop.Y, "0") {
return
}
switch binop.Op {
@@ -2809,7 +2811,7 @@ func CheckNonOctalFileMode(pass *analysis.Pass) (interface{}, error) {
n := sig.Params().Len()
for i := 0; i < n; i++ {
typ := sig.Params().At(i).Type()
- if !code.IsType(typ, "os.FileMode") {
+ if !typeutil.IsType(typ, "os.FileMode") {
continue
}
@@ -2845,7 +2847,7 @@ fnLoop:
params := fn.Signature.Params()
for i := 0; i < params.Len(); i++ {
param := params.At(i)
- if code.IsType(param.Type(), "*testing.B") {
+ if typeutil.IsType(param.Type(), "*testing.B") {
// Ignore discarded pure functions in code related
// to benchmarks. Instead of matching BenchmarkFoo
// functions, we match any function accepting a
@@ -2865,7 +2867,7 @@ fnLoop:
continue
}
refs := ins.Referrers()
- if refs == nil || len(code.FilterDebug(*refs)) > 0 {
+ if refs == nil || len(irutil.FilterDebug(*refs)) > 0 {
continue
}
@@ -2987,7 +2989,7 @@ func checkCalls(pass *analysis.Pass, rules map[string]CallCheck) (interface{}, e
return
}
- r, ok := rules[code.FuncName(obj)]
+ r, ok := rules[typeutil.FuncName(obj)]
if !ok {
return
}
@@ -3009,7 +3011,7 @@ func checkCalls(pass *analysis.Pass, rules map[string]CallCheck) (interface{}, e
Parent: site.Parent(),
}
r(call)
- path, _ := astutil.PathEnclosingInterval(code.File(pass, site), site.Pos(), site.Pos())
+ path, _ := goastutil.PathEnclosingInterval(code.File(pass, site), site.Pos(), site.Pos())
var astcall *ast.CallExpr
for _, el := range path {
if expr, ok := el.(*ast.CallExpr); ok {
@@ -3073,7 +3075,7 @@ func CheckWriterBufferModified(pass *analysis.Pass) (interface{}, error) {
if basic, ok := sig.Results().At(0).Type().(*types.Basic); !ok || basic.Kind() != types.Int {
continue
}
- if named, ok := sig.Results().At(1).Type().(*types.Named); !ok || !code.IsType(named, "error") {
+ if named, ok := sig.Results().At(1).Type().(*types.Named); !ok || !typeutil.IsType(named, "error") {
continue
}
@@ -3090,7 +3092,7 @@ func CheckWriterBufferModified(pass *analysis.Pass) (interface{}, error) {
}
report.Report(pass, ins, "io.Writer.Write must not modify the provided buffer, not even temporarily")
case *ir.Call:
- if !code.IsCallTo(ins.Common(), "append") {
+ if !irutil.IsCallTo(ins.Common(), "append") {
continue
}
if ins.Common().Args[0] != fn.Params[1] {
@@ -3121,7 +3123,7 @@ func CheckEmptyBranch(pass *analysis.Pass) (interface{}, error) {
if fn.Source() == nil {
continue
}
- if code.IsExample(fn) {
+ if irutil.IsExample(fn) {
continue
}
cb := func(node ast.Node) bool {
@@ -3281,7 +3283,7 @@ func CheckSillyRegexp(pass *analysis.Pass) (interface{}, error) {
if !ok {
continue
}
- if !code.IsCallToAny(call.Common(), "regexp.MustCompile", "regexp.Compile", "regexp.Match", "regexp.MatchReader", "regexp.MatchString") {
+ if !irutil.IsCallToAny(call.Common(), "regexp.MustCompile", "regexp.Compile", "regexp.Match", "regexp.MatchReader", "regexp.MatchString") {
continue
}
c, ok := call.Common().Args[0].(*ir.Const)
@@ -3313,7 +3315,7 @@ func CheckMissingEnumTypesInDeclaration(pass *analysis.Pass) (interface{}, error
return
}
- groups := code.GroupSpecs(pass.Fset, decl.Specs)
+ groups := astutil.GroupSpecs(pass.Fset, decl.Specs)
groupLoop:
for _, group := range groups {
if len(group) < 2 {
@@ -3368,14 +3370,14 @@ func CheckTimerResetReturnValue(pass *analysis.Pass) (interface{}, error) {
if !ok {
continue
}
- if !code.IsCallTo(call.Common(), "(*time.Timer).Reset") {
+ if !irutil.IsCallTo(call.Common(), "(*time.Timer).Reset") {
continue
}
refs := call.Referrers()
if refs == nil {
continue
}
- for _, ref := range code.FilterDebug(*refs) {
+ for _, ref := range irutil.FilterDebug(*refs) {
ifstmt, ok := ref.(*ir.If)
if !ok {
continue
@@ -3404,7 +3406,7 @@ func CheckTimerResetReturnValue(pass *analysis.Pass) (interface{}, error) {
// priority, considering the rarity of
// Reset and the tiny likeliness of a
// false positive
- if ins, ok := ins.(*ir.Recv); ok && code.IsType(ins.Chan.Type(), "<-chan time.Time") {
+ if ins, ok := ins.(*ir.Recv); ok && typeutil.IsType(ins.Chan.Type(), "<-chan time.Time") {
found = true
return false
}
@@ -3610,7 +3612,7 @@ func checkJSONTag(pass *analysis.Pass, field *ast.Field, tag string) {
case "string":
cs++
// only for string, floating point, integer and bool
- T := code.Dereference(pass.TypesInfo.TypeOf(field.Type).Underlying()).Underlying()
+ T := typeutil.Dereference(pass.TypesInfo.TypeOf(field.Type).Underlying()).Underlying()
basic, ok := T.(*types.Basic)
if !ok || (basic.Info()&(types.IsBoolean|types.IsInteger|types.IsFloat|types.IsString)) == 0 {
report.Report(pass, field.Tag, "the JSON string option only applies to fields of type string, floating point, integer or bool, or pointers to those")
@@ -3786,7 +3788,7 @@ func CheckMaybeNil(pass *analysis.Pass) (interface{}, error) {
// We choose to err on the side of false negatives.
isNilConst := func(v ir.Value) bool {
- if code.IsPointerLike(v.Type()) {
+ if typeutil.IsPointerLike(v.Type()) {
if k, ok := v.(*ir.Const); ok {
return k.IsNil()
}
diff --git a/staticcheck/rules.go b/staticcheck/rules.go
index b3200362f..bcadabfaf 100644
--- a/staticcheck/rules.go
+++ b/staticcheck/rules.go
@@ -15,6 +15,7 @@ import (
"honnef.co/go/tools/analysis/code"
"honnef.co/go/tools/go/ir"
+ "honnef.co/go/tools/go/types/typeutil"
"golang.org/x/tools/go/analysis"
)
@@ -289,7 +290,7 @@ func ValidHostPort(v Value) bool {
// ConvertedFrom reports whether value v was converted from type typ.
func ConvertedFrom(v Value, typ string) bool {
change, ok := v.Value.(*ir.ChangeType)
- return ok && code.IsType(change.X.Type(), typ)
+ return ok && typeutil.IsType(change.X.Type(), typ)
}
func UniqueStringCutset(v Value) bool {
diff --git a/stylecheck/lint.go b/stylecheck/lint.go
index d8edd720e..8e551fdf0 100644
--- a/stylecheck/lint.go
+++ b/stylecheck/lint.go
@@ -17,14 +17,17 @@ import (
"honnef.co/go/tools/analysis/lint"
"honnef.co/go/tools/analysis/report"
"honnef.co/go/tools/config"
+ "honnef.co/go/tools/go/ast/astutil"
"honnef.co/go/tools/go/ir"
+ "honnef.co/go/tools/go/ir/irutil"
+ "honnef.co/go/tools/go/types/typeutil"
"honnef.co/go/tools/internal/passes/buildir"
"honnef.co/go/tools/pattern"
"golang.org/x/tools/go/analysis"
"golang.org/x/tools/go/analysis/passes/inspect"
"golang.org/x/tools/go/ast/inspector"
- "golang.org/x/tools/go/types/typeutil"
+ gotypeutil "golang.org/x/tools/go/types/typeutil"
)
func CheckPackageComment(pass *analysis.Pass) (interface{}, error) {
@@ -151,7 +154,7 @@ func CheckBlankImports(pass *analysis.Pass) (interface{}, error) {
for i, imp := range f.Imports {
pos := fset.Position(imp.Pos())
- if !code.IsBlank(imp.Name) {
+ if !astutil.IsBlank(imp.Name) {
continue
}
// Only flag the first blank import in a group of imports,
@@ -160,7 +163,7 @@ func CheckBlankImports(pass *analysis.Pass) (interface{}, error) {
if i > 0 {
prev := f.Imports[i-1]
prevPos := fset.Position(prev.Pos())
- if pos.Line-1 == prevPos.Line && code.IsBlank(prev.Name) {
+ if pos.Line-1 == prevPos.Line && astutil.IsBlank(prev.Name) {
continue
}
}
@@ -186,7 +189,7 @@ func CheckIncDec(pass *analysis.Pass) (interface{}, error) {
return
}
if (len(assign.Lhs) != 1 || len(assign.Rhs) != 1) ||
- !code.IsIntLiteral(assign.Rhs[0], "1") {
+ !astutil.IsIntLiteral(assign.Rhs[0], "1") {
return
}
@@ -239,12 +242,12 @@ func CheckUnexportedReturn(pass *analysis.Pass) (interface{}, error) {
continue
}
sig := fn.Type().(*types.Signature)
- if sig.Recv() != nil && !ast.IsExported(code.Dereference(sig.Recv().Type()).(*types.Named).Obj().Name()) {
+ if sig.Recv() != nil && !ast.IsExported(typeutil.Dereference(sig.Recv().Type()).(*types.Named).Obj().Name()) {
continue
}
res := sig.Results()
for i := 0; i < res.Len(); i++ {
- if named, ok := code.DereferenceR(res.At(i).Type()).(*types.Named); ok &&
+ if named, ok := typeutil.DereferenceR(res.At(i).Type()).(*types.Named); ok &&
!ast.IsExported(named.Obj().Name()) &&
named != types.Universe.Lookup("error").Type() {
report.Report(pass, fn, "should not return unexported type")
@@ -258,11 +261,11 @@ func CheckReceiverNames(pass *analysis.Pass) (interface{}, error) {
irpkg := pass.ResultOf[buildir.Analyzer].(*buildir.IR).Pkg
for _, m := range irpkg.Members {
if T, ok := m.Object().(*types.TypeName); ok && !T.IsAlias() {
- ms := typeutil.IntuitiveMethodSet(T.Type(), nil)
+ ms := gotypeutil.IntuitiveMethodSet(T.Type(), nil)
for _, sel := range ms {
fn := sel.Obj().(*types.Func)
recv := fn.Type().(*types.Signature).Recv()
- if code.Dereference(recv.Type()) != T.Type() {
+ if typeutil.Dereference(recv.Type()) != T.Type() {
// skip embedded methods
continue
}
@@ -285,7 +288,7 @@ func CheckReceiverNamesIdentical(pass *analysis.Pass) (interface{}, error) {
var firstFn *types.Func
if T, ok := m.Object().(*types.TypeName); ok && !T.IsAlias() {
- ms := typeutil.IntuitiveMethodSet(T.Type(), nil)
+ ms := gotypeutil.IntuitiveMethodSet(T.Type(), nil)
for _, sel := range ms {
fn := sel.Obj().(*types.Func)
recv := fn.Type().(*types.Signature).Recv()
@@ -293,7 +296,7 @@ func CheckReceiverNamesIdentical(pass *analysis.Pass) (interface{}, error) {
// Don't concern ourselves with methods in generated code
continue
}
- if code.Dereference(recv.Type()) != T.Type() {
+ if typeutil.Dereference(recv.Type()) != T.Type() {
// skip embedded methods
continue
}
@@ -372,7 +375,7 @@ func CheckErrorStrings(pass *analysis.Pass) (interface{}, error) {
if !ok {
continue
}
- if !code.IsCallToAny(call.Common(), "errors.New", "fmt.Errorf") {
+ if !irutil.IsCallToAny(call.Common(), "errors.New", "fmt.Errorf") {
continue
}
@@ -446,7 +449,7 @@ func CheckTimeNames(pass *analysis.Pass) (interface{}, error) {
continue
}
T := pass.TypesInfo.TypeOf(name)
- if !code.IsType(T, "time.Duration") && !code.IsType(T, "*time.Duration") {
+ if !typeutil.IsType(T, "time.Duration") && !typeutil.IsType(T, "*time.Duration") {
continue
}
for _, suffix := range suffixes {
diff --git a/unused/unused.go b/unused/unused.go
index 17d840230..c0cb7fc98 100644
--- a/unused/unused.go
+++ b/unused/unused.go
@@ -13,6 +13,7 @@ import (
"honnef.co/go/tools/analysis/facts"
"honnef.co/go/tools/analysis/lint"
"honnef.co/go/tools/analysis/report"
+ "honnef.co/go/tools/go/ast/astutil"
"honnef.co/go/tools/go/ir"
"honnef.co/go/tools/go/types/typeutil"
"honnef.co/go/tools/internal/passes/buildir"
@@ -987,7 +988,7 @@ func (g *graph) entry(pkg *pkg) {
case *ast.GenDecl:
switch n.Tok {
case token.CONST:
- groups := code.GroupSpecs(pkg.Fset, n.Specs)
+ groups := astutil.GroupSpecs(pkg.Fset, n.Specs)
for _, specs := range groups {
if len(specs) > 1 {
cg := &constGroup{}
@@ -1230,13 +1231,13 @@ func (g *graph) useMethod(t types.Type, sel *types.Selection, by interface{}, ki
path := sel.Index()
assert(obj != nil)
if len(path) > 1 {
- base := code.Dereference(t).Underlying().(*types.Struct)
+ base := typeutil.Dereference(t).Underlying().(*types.Struct)
for _, idx := range path[:len(path)-1] {
next := base.Field(idx)
// (6.3) structs use embedded fields that help implement interfaces
g.see(base)
g.seeAndUse(next, base, edgeProvidesMethod)
- base, _ = code.Dereference(next.Type()).Underlying().(*types.Struct)
+ base, _ = typeutil.Dereference(next.Type()).Underlying().(*types.Struct)
}
}
g.seeAndUse(obj, by, kind)
@@ -1329,7 +1330,7 @@ func (g *graph) typ(t types.Type, parent types.Type) {
seen := map[*types.Struct]struct{}{}
var hasExportedField func(t types.Type) bool
hasExportedField = func(T types.Type) bool {
- t, ok := code.Dereference(T).Underlying().(*types.Struct)
+ t, ok := typeutil.Dereference(T).Underlying().(*types.Struct)
if !ok {
return false
}
@@ -1512,7 +1513,7 @@ func (g *graph) instructions(fn *ir.Function) {
// (4.7) functions use fields they access
g.seeAndUse(field, fnObj, edgeFieldAccess)
case *ir.FieldAddr:
- st := code.Dereference(instr.X.Type()).Underlying().(*types.Struct)
+ st := typeutil.Dereference(instr.X.Type()).Underlying().(*types.Struct)
field := st.Field(instr.Field)
// (4.7) functions use fields they access
g.seeAndUse(field, fnObj, edgeFieldAccess)
@@ -1531,8 +1532,8 @@ func (g *graph) instructions(fn *ir.Function) {
case *ir.ChangeType:
// conversion type handled generically
- s1, ok1 := code.Dereference(instr.Type()).Underlying().(*types.Struct)
- s2, ok2 := code.Dereference(instr.X.Type()).Underlying().(*types.Struct)
+ s1, ok1 := typeutil.Dereference(instr.Type()).Underlying().(*types.Struct)
+ s2, ok2 := typeutil.Dereference(instr.X.Type()).Underlying().(*types.Struct)
if ok1 && ok2 {
// Converting between two structs. The fields are
// relevant for the conversion, but only if the
From 18aa9f1a8c7467b591c8fc08abbae28b6637fe5f Mon Sep 17 00:00:00 2001
From: Dominik Honnef
Date: Fri, 5 Jun 2020 13:17:06 +0200
Subject: [PATCH 057/111] analysis/code: remove AST suffices from function
names
Since we have moved most helpers into astutil and typeutil, we no
longer need the AST suffix to differentiate helpers.
---
analysis/code/code.go | 10 +++++-----
simple/lint.go | 16 ++++++++--------
staticcheck/lint.go | 14 +++++++-------
stylecheck/lint.go | 4 ++--
4 files changed, 22 insertions(+), 22 deletions(-)
diff --git a/analysis/code/code.go b/analysis/code/code.go
index cf7fd61e1..74ea6962c 100644
--- a/analysis/code/code.go
+++ b/analysis/code/code.go
@@ -131,7 +131,7 @@ func ExprToString(pass *analysis.Pass, expr ast.Expr) (string, bool) {
return constant.StringVal(val), true
}
-func CallNameAST(pass *analysis.Pass, call *ast.CallExpr) string {
+func CallName(pass *analysis.Pass, call *ast.CallExpr) string {
switch fun := astutil.Unparen(call.Fun).(type) {
case *ast.SelectorExpr:
fn, ok := pass.TypesInfo.ObjectOf(fun.Sel).(*types.Func)
@@ -154,20 +154,20 @@ func CallNameAST(pass *analysis.Pass, call *ast.CallExpr) string {
}
}
-func IsCallToAST(pass *analysis.Pass, node ast.Node, name string) bool {
+func IsCallTo(pass *analysis.Pass, node ast.Node, name string) bool {
call, ok := node.(*ast.CallExpr)
if !ok {
return false
}
- return CallNameAST(pass, call) == name
+ return CallName(pass, call) == name
}
-func IsCallToAnyAST(pass *analysis.Pass, node ast.Node, names ...string) bool {
+func IsCallToAny(pass *analysis.Pass, node ast.Node, names ...string) bool {
call, ok := node.(*ast.CallExpr)
if !ok {
return false
}
- q := CallNameAST(pass, call)
+ q := CallName(pass, call)
for _, name := range names {
if q == name {
return true
diff --git a/simple/lint.go b/simple/lint.go
index 9aca1b369..8bff7b08d 100644
--- a/simple/lint.go
+++ b/simple/lint.go
@@ -172,7 +172,7 @@ func CheckBytesBufferConversions(pass *analysis.Pass) (interface{}, error) {
sel := m.State["sel"].(*ast.SelectorExpr)
typ := pass.TypesInfo.TypeOf(call.Fun)
- if typ == types.Universe.Lookup("string").Type() && code.IsCallToAST(pass, call.Args[0], "(*bytes.Buffer).Bytes") {
+ if typ == types.Universe.Lookup("string").Type() && code.IsCallTo(pass, call.Args[0], "(*bytes.Buffer).Bytes") {
if _, ok := stack[len(stack)-2].(*ast.IndexExpr); ok {
// Don't flag m[string(buf.Bytes())] – thanks to a
// compiler optimization, this is actually faster than
@@ -183,7 +183,7 @@ func CheckBytesBufferConversions(pass *analysis.Pass) (interface{}, error) {
report.Report(pass, call, fmt.Sprintf("should use %v.String() instead of %v", report.Render(pass, sel.X), report.Render(pass, call)),
report.FilterGenerated(),
report.Fixes(edit.Fix("simplify conversion", edit.ReplaceWithPattern(pass, checkBytesBufferConversionsRs, m.State, node))))
- } else if typ, ok := typ.(*types.Slice); ok && typ.Elem() == types.Universe.Lookup("byte").Type() && code.IsCallToAST(pass, call.Args[0], "(*bytes.Buffer).String") {
+ } else if typ, ok := typ.(*types.Slice); ok && typ.Elem() == types.Universe.Lookup("byte").Type() && code.IsCallTo(pass, call.Args[0], "(*bytes.Buffer).String") {
report.Report(pass, call, fmt.Sprintf("should use %v.Bytes() instead of %v", report.Render(pass, sel.X), report.Render(pass, call)),
report.FilterGenerated(),
report.Fixes(edit.Fix("simplify conversion", edit.ReplaceWithPattern(pass, checkBytesBufferConversionsRb, m.State, node))))
@@ -337,7 +337,7 @@ func CheckForTrue(pass *analysis.Pass) (interface{}, error) {
func CheckRegexpRaw(pass *analysis.Pass) (interface{}, error) {
fn := func(node ast.Node) {
call := node.(*ast.CallExpr)
- if !code.IsCallToAnyAST(pass, call, "regexp.MustCompile", "regexp.Compile") {
+ if !code.IsCallToAny(pass, call, "regexp.MustCompile", "regexp.Compile") {
return
}
sel, ok := call.Fun.(*ast.SelectorExpr)
@@ -970,7 +970,7 @@ func CheckTrim(pass *analysis.Pass) (interface{}, error) {
return
}
- condCallName := code.CallNameAST(pass, condCall)
+ condCallName := code.CallName(pass, condCall)
switch condCallName {
case "strings.HasPrefix":
pkg = "strings"
@@ -1014,7 +1014,7 @@ func CheckTrim(pass *analysis.Pass) (interface{}, error) {
return
}
- rhsName := code.CallNameAST(pass, rhs)
+ rhsName := code.CallName(pass, rhs)
if condCallName == "strings.HasPrefix" && rhsName == "strings.TrimPrefix" ||
condCallName == "strings.HasSuffix" && rhsName == "strings.TrimSuffix" ||
condCallName == "strings.Contains" && rhsName == "strings.Replace" ||
@@ -1550,7 +1550,7 @@ func CheckSortHelpers(pass *analysis.Pass) (interface{}, error) {
if permissible {
return false
}
- if !code.IsCallToAST(pass, node, "sort.Sort") {
+ if !code.IsCallTo(pass, node, "sort.Sort") {
return true
}
if isPermissibleSort(pass, node) {
@@ -1708,14 +1708,14 @@ func CheckSimplifyTypeSwitch(pass *analysis.Pass) (interface{}, error) {
func CheckRedundantCanonicalHeaderKey(pass *analysis.Pass) (interface{}, error) {
fn := func(node ast.Node) {
call := node.(*ast.CallExpr)
- callName := code.CallNameAST(pass, call)
+ callName := code.CallName(pass, call)
switch callName {
case "(net/http.Header).Add", "(net/http.Header).Del", "(net/http.Header).Get", "(net/http.Header).Set":
default:
return
}
- if !code.IsCallToAST(pass, call.Args[0], "net/http.CanonicalHeaderKey") {
+ if !code.IsCallTo(pass, call.Args[0], "net/http.CanonicalHeaderKey") {
return
}
diff --git a/staticcheck/lint.go b/staticcheck/lint.go
index 2b3b5e867..5430d9ea8 100644
--- a/staticcheck/lint.go
+++ b/staticcheck/lint.go
@@ -830,7 +830,7 @@ func isInLoop(b *ir.BasicBlock) bool {
func CheckUntrappableSignal(pass *analysis.Pass) (interface{}, error) {
fn := func(node ast.Node) {
call := node.(*ast.CallExpr)
- if !code.IsCallToAnyAST(pass, call,
+ if !code.IsCallToAny(pass, call,
"os/signal.Ignore", "os/signal.Notify", "os/signal.Reset") {
return
}
@@ -902,7 +902,7 @@ func CheckTemplate(pass *analysis.Pass) (interface{}, error) {
call := node.(*ast.CallExpr)
// OPT(dh): use integer for kind
var kind string
- switch code.CallNameAST(pass, call) {
+ switch code.CallName(pass, call) {
case "(*text/template.Template).Parse":
kind = "text"
case "(*html/template.Template).Parse":
@@ -911,7 +911,7 @@ func CheckTemplate(pass *analysis.Pass) (interface{}, error) {
return
}
sel := call.Fun.(*ast.SelectorExpr)
- if !code.IsCallToAnyAST(pass, sel.X, "text/template.New", "html/template.New") {
+ if !code.IsCallToAny(pass, sel.X, "text/template.New", "html/template.New") {
// TODO(dh): this is a cheap workaround for templates with
// different delims. A better solution with less false
// negatives would use data flow analysis to see where the
@@ -948,7 +948,7 @@ var (
func CheckTimeSleepConstant(pass *analysis.Pass) (interface{}, error) {
fn := func(node ast.Node) {
call := node.(*ast.CallExpr)
- if !code.IsCallToAST(pass, call, "time.Sleep") {
+ if !code.IsCallTo(pass, call, "time.Sleep") {
return
}
lit, ok := call.Args[knowledge.Arg("time.Sleep.d")].(*ast.BasicLit)
@@ -1142,7 +1142,7 @@ func CheckTestMainExit(pass *analysis.Pass) (interface{}, error) {
arg = pass.TypesInfo.ObjectOf(node.Type.Params.List[0].Names[0])
return true
case *ast.CallExpr:
- if code.IsCallToAST(pass, node, "os.Exit") {
+ if code.IsCallTo(pass, node, "os.Exit") {
callsExit = true
return false
}
@@ -1188,7 +1188,7 @@ func isTestMain(pass *analysis.Pass, decl *ast.FuncDecl) bool {
func CheckExec(pass *analysis.Pass) (interface{}, error) {
fn := func(node ast.Node) {
call := node.(*ast.CallExpr)
- if !code.IsCallToAST(pass, call, "os/exec.Command") {
+ if !code.IsCallTo(pass, call, "os/exec.Command") {
return
}
val, ok := code.ExprToString(pass, call.Args[knowledge.Arg("os/exec.Command.name")])
@@ -1361,7 +1361,7 @@ func CheckScopedBreak(pass *analysis.Pass) (interface{}, error) {
func CheckUnsafePrintf(pass *analysis.Pass) (interface{}, error) {
fn := func(node ast.Node) {
call := node.(*ast.CallExpr)
- name := code.CallNameAST(pass, call)
+ name := code.CallName(pass, call)
var arg int
switch name {
diff --git a/stylecheck/lint.go b/stylecheck/lint.go
index 8e551fdf0..8ed7ad5af 100644
--- a/stylecheck/lint.go
+++ b/stylecheck/lint.go
@@ -502,7 +502,7 @@ func CheckErrorVarNames(pass *analysis.Pass) (interface{}, error) {
for i, name := range spec.Names {
val := spec.Values[i]
- if !code.IsCallToAnyAST(pass, val, "errors.New", "fmt.Errorf") {
+ if !code.IsCallToAny(pass, val, "errors.New", "fmt.Errorf") {
continue
}
@@ -596,7 +596,7 @@ func CheckHTTPStatusCodes(pass *analysis.Pass) (interface{}, error) {
call := node.(*ast.CallExpr)
var arg int
- switch code.CallNameAST(pass, call) {
+ switch code.CallName(pass, call) {
case "net/http.Error":
arg = 2
case "net/http.Redirect":
From 41bedbdbf9e6154d6edae49010d46bd375b076f2 Mon Sep 17 00:00:00 2001
From: Dominik Honnef
Date: Sat, 6 Jun 2020 14:01:35 +0200
Subject: [PATCH 058/111] unused: don't use typeutil.Map
Under the current architecture of 'unused', we no longer need
typeutil.Map to deduplicate certain types. Named types are canonical
by default, we already didn't deduplicate structs or interfaces in our
fork of typeutil.Map, and deduplicating any other type is merely an
optimization. This optimization, too, doesn't matter anymore, because
we no longer keep a single graph around for all packages, and the size
of individual graphs isn't as impacted by the optimization.
---
go/types/typeutil/example_test.go | 67 -------
go/types/typeutil/identical.go | 149 --------------
go/types/typeutil/map.go | 319 ------------------------------
go/types/typeutil/map_test.go | 174 ----------------
unused/unused.go | 39 ++--
5 files changed, 20 insertions(+), 728 deletions(-)
delete mode 100644 go/types/typeutil/example_test.go
delete mode 100644 go/types/typeutil/identical.go
delete mode 100644 go/types/typeutil/map.go
delete mode 100644 go/types/typeutil/map_test.go
diff --git a/go/types/typeutil/example_test.go b/go/types/typeutil/example_test.go
deleted file mode 100644
index 60c4cb5a0..000000000
--- a/go/types/typeutil/example_test.go
+++ /dev/null
@@ -1,67 +0,0 @@
-// Copyright 2014 The Go Authors. All rights reserved.
-// Use of this source code is governed by a BSD-style
-// license that can be found in the LICENSE file.
-
-package typeutil_test
-
-import (
- "fmt"
- "go/ast"
- "go/parser"
- "go/token"
- "go/types"
- "sort"
-
- "honnef.co/go/tools/go/types/typeutil"
-)
-
-func ExampleMap() {
- const source = `package P
-
-var X []string
-var Y []string
-
-const p, q = 1.0, 2.0
-
-func f(offset int32) (value byte, ok bool)
-func g(rune) (uint8, bool)
-`
-
- // Parse and type-check the package.
- fset := token.NewFileSet()
- f, err := parser.ParseFile(fset, "P.go", source, 0)
- if err != nil {
- panic(err)
- }
- pkg, err := new(types.Config).Check("P", fset, []*ast.File{f}, nil)
- if err != nil {
- panic(err)
- }
-
- scope := pkg.Scope()
-
- // Group names of package-level objects by their type.
- var namesByType typeutil.Map // value is []string
- for _, name := range scope.Names() {
- T := scope.Lookup(name).Type()
-
- names, _ := namesByType.At(T).([]string)
- names = append(names, name)
- namesByType.Set(T, names)
- }
-
- // Format, sort, and print the map entries.
- var lines []string
- namesByType.Iterate(func(T types.Type, names interface{}) {
- lines = append(lines, fmt.Sprintf("%s %s", names, T))
- })
- sort.Strings(lines)
- for _, line := range lines {
- fmt.Println(line)
- }
-
- // Output:
- // [X Y] []string
- // [f g] func(offset int32) (value byte, ok bool)
- // [p q] untyped float
-}
diff --git a/go/types/typeutil/identical.go b/go/types/typeutil/identical.go
deleted file mode 100644
index 0cd82e8c0..000000000
--- a/go/types/typeutil/identical.go
+++ /dev/null
@@ -1,149 +0,0 @@
-package typeutil
-
-import (
- "go/types"
-)
-
-// Unlike types.Identical, receivers of Signature types are not ignored.
-// Unlike types.Identical, interfaces are compared via pointer equality (except for the empty interface, which gets deduplicated).
-// Unlike types.Identical, structs are compared via pointer equality.
-func identical0(x, y types.Type) bool {
- if x == y {
- return true
- }
-
- switch x := x.(type) {
- case *types.Basic:
- // Basic types are singletons except for the rune and byte
- // aliases, thus we cannot solely rely on the x == y check
- // above. See also comment in TypeName.IsAlias.
- if y, ok := y.(*types.Basic); ok {
- return x.Kind() == y.Kind()
- }
-
- case *types.Array:
- // Two array types are identical if they have identical element types
- // and the same array length.
- if y, ok := y.(*types.Array); ok {
- // If one or both array lengths are unknown (< 0) due to some error,
- // assume they are the same to avoid spurious follow-on errors.
- return (x.Len() < 0 || y.Len() < 0 || x.Len() == y.Len()) && identical0(x.Elem(), y.Elem())
- }
-
- case *types.Slice:
- // Two slice types are identical if they have identical element types.
- if y, ok := y.(*types.Slice); ok {
- return identical0(x.Elem(), y.Elem())
- }
-
- case *types.Struct:
- if y, ok := y.(*types.Struct); ok {
- return x == y
- }
-
- case *types.Pointer:
- // Two pointer types are identical if they have identical base types.
- if y, ok := y.(*types.Pointer); ok {
- return identical0(x.Elem(), y.Elem())
- }
-
- case *types.Tuple:
- // Two tuples types are identical if they have the same number of elements
- // and corresponding elements have identical types.
- if y, ok := y.(*types.Tuple); ok {
- if x.Len() == y.Len() {
- if x != nil {
- for i := 0; i < x.Len(); i++ {
- v := x.At(i)
- w := y.At(i)
- if !identical0(v.Type(), w.Type()) {
- return false
- }
- }
- }
- return true
- }
- }
-
- case *types.Signature:
- // Two function types are identical if they have the same number of parameters
- // and result values, corresponding parameter and result types are identical,
- // and either both functions are variadic or neither is. Parameter and result
- // names are not required to match.
- if y, ok := y.(*types.Signature); ok {
-
- return x.Variadic() == y.Variadic() &&
- identical0(x.Params(), y.Params()) &&
- identical0(x.Results(), y.Results()) &&
- (x.Recv() != nil && y.Recv() != nil && identical0(x.Recv().Type(), y.Recv().Type()) || x.Recv() == nil && y.Recv() == nil)
- }
-
- case *types.Interface:
- // The issue with interfaces, typeutil.Map and types.Identical
- //
- // types.Identical, when comparing two interfaces, only looks at the set
- // of all methods, not differentiating between implicit (embedded) and
- // explicit methods.
- //
- // When we see the following two types, in source order
- //
- // type I1 interface { foo() }
- // type I2 interface { I1 }
- //
- // then we will first correctly process I1 and its underlying type. When
- // we get to I2, we will see that its underlying type is identical to
- // that of I1 and not process it again. This, however, means that we will
- // not record the fact that I2 embeds I1. If only I2 is reachable via the
- // graph root, then I1 will not be considered used.
- //
- // We choose to be lazy and compare interfaces by their
- // pointers. This will obviously miss identical interfaces,
- // but this only has a runtime cost, it doesn't affect
- // correctness.
- if y, ok := y.(*types.Interface); ok {
- if x.NumEmbeddeds() == 0 &&
- y.NumEmbeddeds() == 0 &&
- x.NumMethods() == 0 &&
- y.NumMethods() == 0 {
- // all truly empty interfaces are the same
- return true
- }
- return x == y
- }
-
- case *types.Map:
- // Two map types are identical if they have identical key and value types.
- if y, ok := y.(*types.Map); ok {
- return identical0(x.Key(), y.Key()) && identical0(x.Elem(), y.Elem())
- }
-
- case *types.Chan:
- // Two channel types are identical if they have identical value types
- // and the same direction.
- if y, ok := y.(*types.Chan); ok {
- return x.Dir() == y.Dir() && identical0(x.Elem(), y.Elem())
- }
-
- case *types.Named:
- // Two named types are identical if their type names originate
- // in the same type declaration.
- if y, ok := y.(*types.Named); ok {
- return x.Obj() == y.Obj()
- }
-
- case nil:
-
- default:
- panic("unreachable")
- }
-
- return false
-}
-
-// Identical reports whether x and y are identical types.
-// Unlike types.Identical, receivers of Signature types are not ignored.
-// Unlike types.Identical, interfaces are compared via pointer equality (except for the empty interface, which gets deduplicated).
-// Unlike types.Identical, structs are compared via pointer equality.
-func Identical(x, y types.Type) (ret bool) {
- return identical0(x, y)
-}
diff --git a/go/types/typeutil/map.go b/go/types/typeutil/map.go
deleted file mode 100644
index f929353cc..000000000
--- a/go/types/typeutil/map.go
+++ /dev/null
@@ -1,319 +0,0 @@
-// Copyright 2014 The Go Authors. All rights reserved.
-// Use of this source code is governed by a BSD-style
-// license that can be found in the LICENSE file.
-
-// Package typeutil defines various utilities for types, such as Map,
-// a mapping from types.Type to interface{} values.
-package typeutil
-
-import (
- "bytes"
- "fmt"
- "go/types"
- "reflect"
-)
-
-// Map is a hash-table-based mapping from types (types.Type) to
-// arbitrary interface{} values. The concrete types that implement
-// the Type interface are pointers. Since they are not canonicalized,
-// == cannot be used to check for equivalence, and thus we cannot
-// simply use a Go map.
-//
-// Just as with map[K]V, a nil *Map is a valid empty map.
-//
-// Not thread-safe.
-//
-// This fork handles Signatures correctly, respecting method
-// receivers. Furthermore, it doesn't deduplicate interfaces or
-// structs. Interfaces aren't deduplicated as not to conflate implicit
-// and explicit methods. Structs aren't deduplicated because we track
-// fields of each type separately.
-//
-type Map struct {
- hasher Hasher // shared by many Maps
- table map[uint32][]entry // maps hash to bucket; entry.key==nil means unused
- length int // number of map entries
-}
-
-// entry is an entry (key/value association) in a hash bucket.
-type entry struct {
- key types.Type
- value interface{}
-}
-
-// SetHasher sets the hasher used by Map.
-//
-// All Hashers are functionally equivalent but contain internal state
-// used to cache the results of hashing previously seen types.
-//
-// A single Hasher created by MakeHasher() may be shared among many
-// Maps. This is recommended if the instances have many keys in
-// common, as it will amortize the cost of hash computation.
-//
-// A Hasher may grow without bound as new types are seen. Even when a
-// type is deleted from the map, the Hasher never shrinks, since other
-// types in the map may reference the deleted type indirectly.
-//
-// Hashers are not thread-safe, and read-only operations such as
-// Map.Lookup require updates to the hasher, so a full Mutex lock (not a
-// read-lock) is require around all Map operations if a shared
-// hasher is accessed from multiple threads.
-//
-// If SetHasher is not called, the Map will create a private hasher at
-// the first call to Insert.
-//
-func (m *Map) SetHasher(hasher Hasher) {
- m.hasher = hasher
-}
-
-// Delete removes the entry with the given key, if any.
-// It returns true if the entry was found.
-//
-func (m *Map) Delete(key types.Type) bool {
- if m != nil && m.table != nil {
- hash := m.hasher.Hash(key)
- bucket := m.table[hash]
- for i, e := range bucket {
- if e.key != nil && Identical(key, e.key) {
- // We can't compact the bucket as it
- // would disturb iterators.
- bucket[i] = entry{}
- m.length--
- return true
- }
- }
- }
- return false
-}
-
-// At returns the map entry for the given key.
-// The result is nil if the entry is not present.
-//
-func (m *Map) At(key types.Type) interface{} {
- if m != nil && m.table != nil {
- for _, e := range m.table[m.hasher.Hash(key)] {
- if e.key != nil && Identical(key, e.key) {
- return e.value
- }
- }
- }
- return nil
-}
-
-// Set sets the map entry for key to val,
-// and returns the previous entry, if any.
-func (m *Map) Set(key types.Type, value interface{}) (prev interface{}) {
- if m.table != nil {
- hash := m.hasher.Hash(key)
- bucket := m.table[hash]
- var hole *entry
- for i, e := range bucket {
- if e.key == nil {
- hole = &bucket[i]
- } else if Identical(key, e.key) {
- prev = e.value
- bucket[i].value = value
- return
- }
- }
-
- if hole != nil {
- *hole = entry{key, value} // overwrite deleted entry
- } else {
- m.table[hash] = append(bucket, entry{key, value})
- }
- } else {
- if m.hasher.memo == nil {
- m.hasher = MakeHasher()
- }
- hash := m.hasher.Hash(key)
- m.table = map[uint32][]entry{hash: {entry{key, value}}}
- }
-
- m.length++
- return
-}
-
-// Len returns the number of map entries.
-func (m *Map) Len() int {
- if m != nil {
- return m.length
- }
- return 0
-}
-
-// Iterate calls function f on each entry in the map in unspecified order.
-//
-// If f should mutate the map, Iterate provides the same guarantees as
-// Go maps: if f deletes a map entry that Iterate has not yet reached,
-// f will not be invoked for it, but if f inserts a map entry that
-// Iterate has not yet reached, whether or not f will be invoked for
-// it is unspecified.
-//
-func (m *Map) Iterate(f func(key types.Type, value interface{})) {
- if m != nil {
- for _, bucket := range m.table {
- for _, e := range bucket {
- if e.key != nil {
- f(e.key, e.value)
- }
- }
- }
- }
-}
-
-// Keys returns a new slice containing the set of map keys.
-// The order is unspecified.
-func (m *Map) Keys() []types.Type {
- keys := make([]types.Type, 0, m.Len())
- m.Iterate(func(key types.Type, _ interface{}) {
- keys = append(keys, key)
- })
- return keys
-}
-
-func (m *Map) toString(values bool) string {
- if m == nil {
- return "{}"
- }
- var buf bytes.Buffer
- fmt.Fprint(&buf, "{")
- sep := ""
- m.Iterate(func(key types.Type, value interface{}) {
- fmt.Fprint(&buf, sep)
- sep = ", "
- fmt.Fprint(&buf, key)
- if values {
- fmt.Fprintf(&buf, ": %q", value)
- }
- })
- fmt.Fprint(&buf, "}")
- return buf.String()
-}
-
-// String returns a string representation of the map's entries.
-// Values are printed using fmt.Sprintf("%v", v).
-// Order is unspecified.
-//
-func (m *Map) String() string {
- return m.toString(true)
-}
-
-// KeysString returns a string representation of the map's key set.
-// Order is unspecified.
-//
-func (m *Map) KeysString() string {
- return m.toString(false)
-}
-
-////////////////////////////////////////////////////////////////////////
-// Hasher
-
-// A Hasher maps each type to its hash value.
-// For efficiency, a hasher uses memoization; thus its memory
-// footprint grows monotonically over time.
-// Hashers are not thread-safe.
-// Hashers have reference semantics.
-// Call MakeHasher to create a Hasher.
-type Hasher struct {
- memo map[types.Type]uint32
-}
-
-// MakeHasher returns a new Hasher instance.
-func MakeHasher() Hasher {
- return Hasher{make(map[types.Type]uint32)}
-}
-
-// Hash computes a hash value for the given type t such that
-// Identical(t, t') => Hash(t) == Hash(t').
-func (h Hasher) Hash(t types.Type) uint32 {
- hash, ok := h.memo[t]
- if !ok {
- hash = h.hashFor(t)
- h.memo[t] = hash
- }
- return hash
-}
-
-// hashString computes the Fowler–Noll–Vo hash of s.
-func hashString(s string) uint32 {
- var h uint32
- for i := 0; i < len(s); i++ {
- h ^= uint32(s[i])
- h *= 16777619
- }
- return h
-}
-
-// hashFor computes the hash of t.
-func (h Hasher) hashFor(t types.Type) uint32 {
- // See Identical for rationale.
- switch t := t.(type) {
- case *types.Basic:
- return uint32(t.Kind())
-
- case *types.Array:
- return 9043 + 2*uint32(t.Len()) + 3*h.Hash(t.Elem())
-
- case *types.Slice:
- return 9049 + 2*h.Hash(t.Elem())
-
- case *types.Struct:
- var hash uint32 = 9059
- for i, n := 0, t.NumFields(); i < n; i++ {
- f := t.Field(i)
- if f.Anonymous() {
- hash += 8861
- }
- hash += hashString(t.Tag(i))
- hash += hashString(f.Name()) // (ignore f.Pkg)
- hash += h.Hash(f.Type())
- }
- return hash
-
- case *types.Pointer:
- return 9067 + 2*h.Hash(t.Elem())
-
- case *types.Signature:
- var hash uint32 = 9091
- if t.Variadic() {
- hash *= 8863
- }
- return hash + 3*h.hashTuple(t.Params()) + 5*h.hashTuple(t.Results())
-
- case *types.Interface:
- var hash uint32 = 9103
- for i, n := 0, t.NumMethods(); i < n; i++ {
- // See go/types.identicalMethods for rationale.
- // Method order is not significant.
- // Ignore m.Pkg().
- m := t.Method(i)
- hash += 3*hashString(m.Name()) + 5*h.Hash(m.Type())
- }
- return hash
-
- case *types.Map:
- return 9109 + 2*h.Hash(t.Key()) + 3*h.Hash(t.Elem())
-
- case *types.Chan:
- return 9127 + 2*uint32(t.Dir()) + 3*h.Hash(t.Elem())
-
- case *types.Named:
- // Not safe with a copying GC; objects may move.
- return uint32(reflect.ValueOf(t.Obj()).Pointer())
-
- case *types.Tuple:
- return h.hashTuple(t)
- }
- panic(t)
-}
-
-func (h Hasher) hashTuple(tuple *types.Tuple) uint32 {
- // See go/types.identicalTypes for rationale.
- n := tuple.Len()
- var hash uint32 = 9137 + 2*uint32(n)
- for i := 0; i < n; i++ {
- hash += 3 * h.Hash(tuple.At(i).Type())
- }
- return hash
-}
diff --git a/go/types/typeutil/map_test.go b/go/types/typeutil/map_test.go
deleted file mode 100644
index dcc10d9a0..000000000
--- a/go/types/typeutil/map_test.go
+++ /dev/null
@@ -1,174 +0,0 @@
-// Copyright 2014 The Go Authors. All rights reserved.
-// Use of this source code is governed by a BSD-style
-// license that can be found in the LICENSE file.
-
-package typeutil_test
-
-// TODO(adonovan):
-// - test use of explicit hasher across two maps.
-// - test hashcodes are consistent with equals for a range of types
-// (e.g. all types generated by type-checking some body of real code).
-
-import (
- "go/types"
- "testing"
-
- "honnef.co/go/tools/go/types/typeutil"
-)
-
-var (
- tStr = types.Typ[types.String] // string
- tPStr1 = types.NewPointer(tStr) // *string
- tPStr2 = types.NewPointer(tStr) // *string, again
- tInt = types.Typ[types.Int] // int
- tChanInt1 = types.NewChan(types.RecvOnly, tInt) // <-chan int
- tChanInt2 = types.NewChan(types.RecvOnly, tInt) // <-chan int, again
-)
-
-func checkEqualButNotIdentical(t *testing.T, x, y types.Type, comment string) {
- if !types.Identical(x, y) {
- t.Errorf("%s: not equal: %s, %s", comment, x, y)
- }
- if x == y {
- t.Errorf("%s: identical: %v, %v", comment, x, y)
- }
-}
-
-func TestAxioms(t *testing.T) {
- checkEqualButNotIdentical(t, tPStr1, tPStr2, "tPstr{1,2}")
- checkEqualButNotIdentical(t, tChanInt1, tChanInt2, "tChanInt{1,2}")
-}
-
-func TestMap(t *testing.T) {
- var tmap *typeutil.Map
-
- // All methods but Set are safe on on (*T)(nil).
- _ = tmap.Len()
- _ = tmap.At(tPStr1)
- _ = tmap.Delete(tPStr1)
- _ = tmap.KeysString()
- _ = tmap.String()
-
- tmap = new(typeutil.Map)
-
- // Length of empty map.
- if l := tmap.Len(); l != 0 {
- t.Errorf("Len() on empty Map: got %d, want 0", l)
- }
- // At of missing key.
- if v := tmap.At(tPStr1); v != nil {
- t.Errorf("At() on empty Map: got %v, want nil", v)
- }
- // Deletion of missing key.
- if tmap.Delete(tPStr1) {
- t.Errorf("Delete() on empty Map: got true, want false")
- }
- // Set of new key.
- if prev := tmap.Set(tPStr1, "*string"); prev != nil {
- t.Errorf("Set() on empty Map returned non-nil previous value %s", prev)
- }
-
- // Now: {*string: "*string"}
-
- // Length of non-empty map.
- if l := tmap.Len(); l != 1 {
- t.Errorf("Len(): got %d, want 1", l)
- }
- // At via insertion key.
- if v := tmap.At(tPStr1); v != "*string" {
- t.Errorf("At(): got %q, want \"*string\"", v)
- }
- // At via equal key.
- if v := tmap.At(tPStr2); v != "*string" {
- t.Errorf("At(): got %q, want \"*string\"", v)
- }
- // Iteration over sole entry.
- tmap.Iterate(func(key types.Type, value interface{}) {
- if key != tPStr1 {
- t.Errorf("Iterate: key: got %s, want %s", key, tPStr1)
- }
- if want := "*string"; value != want {
- t.Errorf("Iterate: value: got %s, want %s", value, want)
- }
- })
-
- // Setion with key equal to present one.
- if prev := tmap.Set(tPStr2, "*string again"); prev != "*string" {
- t.Errorf("Set() previous value: got %s, want \"*string\"", prev)
- }
-
- // Setion of another association.
- if prev := tmap.Set(tChanInt1, "<-chan int"); prev != nil {
- t.Errorf("Set() previous value: got %s, want nil", prev)
- }
-
- // Now: {*string: "*string again", <-chan int: "<-chan int"}
-
- want1 := "{*string: \"*string again\", <-chan int: \"<-chan int\"}"
- want2 := "{<-chan int: \"<-chan int\", *string: \"*string again\"}"
- if s := tmap.String(); s != want1 && s != want2 {
- t.Errorf("String(): got %s, want %s", s, want1)
- }
-
- want1 = "{*string, <-chan int}"
- want2 = "{<-chan int, *string}"
- if s := tmap.KeysString(); s != want1 && s != want2 {
- t.Errorf("KeysString(): got %s, want %s", s, want1)
- }
-
- // Keys().
- I := types.Identical
- switch k := tmap.Keys(); {
- case I(k[0], tChanInt1) && I(k[1], tPStr1): // ok
- case I(k[1], tChanInt1) && I(k[0], tPStr1): // ok
- default:
- t.Errorf("Keys(): got %v, want %s", k, want2)
- }
-
- if l := tmap.Len(); l != 2 {
- t.Errorf("Len(): got %d, want 1", l)
- }
- // At via original key.
- if v := tmap.At(tPStr1); v != "*string again" {
- t.Errorf("At(): got %q, want \"*string again\"", v)
- }
- hamming := 1
- tmap.Iterate(func(key types.Type, value interface{}) {
- switch {
- case I(key, tChanInt1):
- hamming *= 2 // ok
- case I(key, tPStr1):
- hamming *= 3 // ok
- }
- })
- if hamming != 6 {
- t.Errorf("Iterate: hamming: got %d, want %d", hamming, 6)
- }
-
- if v := tmap.At(tChanInt2); v != "<-chan int" {
- t.Errorf("At(): got %q, want \"<-chan int\"", v)
- }
- // Deletion with key equal to present one.
- if !tmap.Delete(tChanInt2) {
- t.Errorf("Delete() of existing key: got false, want true")
- }
-
- // Now: {*string: "*string again"}
-
- if l := tmap.Len(); l != 1 {
- t.Errorf("Len(): got %d, want 1", l)
- }
- // Deletion again.
- if !tmap.Delete(tPStr2) {
- t.Errorf("Delete() of existing key: got false, want true")
- }
-
- // Now: {}
-
- if l := tmap.Len(); l != 0 {
- t.Errorf("Len(): got %d, want %d", l, 0)
- }
- if s := tmap.String(); s != "{}" {
- t.Errorf("Len(): got %q, want %q", s, "")
- }
-}
diff --git a/unused/unused.go b/unused/unused.go
index c0cb7fc98..6c49574d3 100644
--- a/unused/unused.go
+++ b/unused/unused.go
@@ -549,9 +549,9 @@ func run(pass *analysis.Pass) (interface{}, error) {
for _, v := range c.graph.Nodes {
debugNode(v)
}
- c.graph.TypeNodes.Iterate(func(key types.Type, value interface{}) {
- debugNode(value.(*node))
- })
+ for _, node := range c.graph.TypeNodes {
+ debugNode(node)
+ }
debugf("}\n")
}
@@ -561,10 +561,9 @@ func run(pass *analysis.Pass) (interface{}, error) {
func (c *checker) results() (used, unused []types.Object) {
c.graph.color(c.graph.Root)
- c.graph.TypeNodes.Iterate(func(_ types.Type, value interface{}) {
- node := value.(*node)
+ for _, node := range c.graph.TypeNodes {
if node.seen {
- return
+ continue
}
switch obj := node.obj.(type) {
case *types.Struct:
@@ -581,7 +580,7 @@ func (c *checker) results() (used, unused []types.Object) {
}
}
}
- })
+ }
// OPT(dh): can we find meaningful initial capacities for the used and unused slices?
@@ -617,9 +616,9 @@ func (c *checker) results() (used, unused []types.Object) {
type graph struct {
Root *node
- seenTypes typeutil.Map
+ seenTypes map[types.Type]struct{}
- TypeNodes typeutil.Map
+ TypeNodes map[types.Type]*node
Nodes map[interface{}]*node
// context
@@ -630,9 +629,11 @@ type graph struct {
func newGraph(pkg *pkg) *graph {
g := &graph{
- Nodes: map[interface{}]*node{},
- seenFns: map[string]struct{}{},
- pkg: pkg,
+ Nodes: map[interface{}]*node{},
+ seenFns: map[string]struct{}{},
+ seenTypes: map[types.Type]struct{}{},
+ TypeNodes: map[types.Type]*node{},
+ pkg: pkg,
}
g.Root = g.newNode(nil)
return g
@@ -683,11 +684,11 @@ func (g *graph) nodeMaybe(obj types.Object) (*node, bool) {
func (g *graph) node(obj interface{}) (n *node, new bool) {
switch obj := obj.(type) {
case types.Type:
- if v := g.TypeNodes.At(obj); v != nil {
- return v.(*node), false
+ if v := g.TypeNodes[obj]; v != nil {
+ return v, false
}
n = g.newNode(obj)
- g.TypeNodes.Set(obj, n)
+ g.TypeNodes[obj] = n
return n, true
case types.Object:
// OPT(dh): the types.Object and default cases are identical
@@ -1128,7 +1129,7 @@ func (g *graph) entry(pkg *pkg) {
var ifaces []*types.Interface
var notIfaces []types.Type
- g.seenTypes.Iterate(func(t types.Type, _ interface{}) {
+ for t := range g.seenTypes {
switch t := t.(type) {
case *types.Interface:
// OPT(dh): (8.1) we only need interfaces that have unexported methods
@@ -1138,7 +1139,7 @@ func (g *graph) entry(pkg *pkg) {
notIfaces = append(notIfaces, t)
}
}
- })
+ }
// (8.0) handle interfaces
for _, t := range notIfaces {
@@ -1278,7 +1279,7 @@ func (g *graph) function(fn *ir.Function) {
}
func (g *graph) typ(t types.Type, parent types.Type) {
- if g.seenTypes.At(t) != nil {
+ if _, ok := g.seenTypes[t]; ok {
return
}
@@ -1288,7 +1289,7 @@ func (g *graph) typ(t types.Type, parent types.Type) {
}
}
- g.seenTypes.Set(t, struct{}{})
+ g.seenTypes[t] = struct{}{}
if isIrrelevant(t) {
return
}
From 13ec319710618d910d2e3bf5ff1396d2092ee6c9 Mon Sep 17 00:00:00 2001
From: Dominik Honnef
Date: Wed, 20 May 2020 12:08:36 +0200
Subject: [PATCH 059/111] SA5012: check for slice parameters that have to be of
even length
---
staticcheck/analysis.go | 14 +-
staticcheck/doc.go | 9 +
staticcheck/lint.go | 246 ++++++++++++++++++
staticcheck/lint_test.go | 1 +
.../CheckEvenSliceLength.go | 50 ++++
5 files changed, 315 insertions(+), 5 deletions(-)
create mode 100644 staticcheck/testdata/src/CheckEvenSliceLength/CheckEvenSliceLength.go
diff --git a/staticcheck/analysis.go b/staticcheck/analysis.go
index 76dcd4964..a860e8af3 100644
--- a/staticcheck/analysis.go
+++ b/staticcheck/analysis.go
@@ -183,6 +183,10 @@ var Analyzers = lint.InitializeAnalyzers(Docs, map[string]*analysis.Analyzer{
Run: CheckSingleArgAppend,
Requires: []*analysis.Analyzer{inspect.Analyzer, facts.Generated, facts.TokenFile},
},
+ "SA4022": {
+ Run: CheckAddressIsNil,
+ Requires: []*analysis.Analyzer{inspect.Analyzer},
+ },
"SA5000": {
Run: CheckNilMaps,
@@ -225,6 +229,11 @@ var Analyzers = lint.InitializeAnalyzers(Docs, map[string]*analysis.Analyzer{
Run: CheckMaybeNil,
Requires: []*analysis.Analyzer{buildir.Analyzer},
},
+ "SA5012": {
+ Run: CheckEvenSliceLength,
+ FactTypes: []analysis.Fact{new(evenElements)},
+ Requires: []*analysis.Analyzer{buildir.Analyzer},
+ },
"SA6000": makeCallCheckerAnalyzer(checkRegexpMatchLoopRules),
"SA6001": {
@@ -263,9 +272,4 @@ var Analyzers = lint.InitializeAnalyzers(Docs, map[string]*analysis.Analyzer{
Run: CheckStaticBitShift,
Requires: []*analysis.Analyzer{inspect.Analyzer},
},
-
- "SA4022": {
- Run: CheckAddressIsNil,
- Requires: []*analysis.Analyzer{inspect.Analyzer},
- },
})
diff --git a/staticcheck/doc.go b/staticcheck/doc.go
index a9b89fa31..cc04d77b7 100644
--- a/staticcheck/doc.go
+++ b/staticcheck/doc.go
@@ -661,6 +661,15 @@ popular package.`,
Since: "2020.1",
},
+ "SA5012": {
+ Title: "Passing odd-sized slice to function expecting even size",
+ Text: `Some functions that take slices as parameters expect the slices to have an even number of elements.
+Often, these functions treat elements in a slice as pairs.
+For example, strings.NewReplacer takes pairs of old and new strings,
+and calling it with an odd number of elements would be an error.`,
+ Since: "Unreleased",
+ },
+
"SA6000": {
Title: `Using regexp.Match or related in a loop, should use regexp.Compile`,
Since: "2017.1",
diff --git a/staticcheck/lint.go b/staticcheck/lint.go
index 5430d9ea8..481a504bc 100644
--- a/staticcheck/lint.go
+++ b/staticcheck/lint.go
@@ -3914,3 +3914,249 @@ func CheckStaticBitShift(pass *analysis.Pass) (interface{}, error) {
return nil, nil
}
+
+func findSliceLenChecks(pass *analysis.Pass) {
+ // mark all function parameters that have to be of even length
+ for _, fn := range pass.ResultOf[buildir.Analyzer].(*buildir.IR).SrcFuncs {
+ for _, b := range fn.Blocks {
+ // all paths go through this block
+ if !b.Dominates(fn.Exit) {
+ continue
+ }
+
+ // if foo % 2 != 0
+ ifi, ok := b.Control().(*ir.If)
+ if !ok {
+ continue
+ }
+ cmp, ok := ifi.Cond.(*ir.BinOp)
+ if !ok {
+ continue
+ }
+ var needle uint64
+ switch cmp.Op {
+ case token.NEQ:
+ // look for != 0
+ needle = 0
+ case token.EQL:
+ // look for == 1
+ needle = 1
+ default:
+ continue
+ }
+
+ rem, ok1 := cmp.X.(*ir.BinOp)
+ k, ok2 := cmp.Y.(*ir.Const)
+ if ok1 != ok2 {
+ continue
+ }
+ if !ok1 {
+ rem, ok1 = cmp.Y.(*ir.BinOp)
+ k, ok2 = cmp.X.(*ir.Const)
+ }
+ if !ok1 || !ok2 || rem.Op != token.REM || k.Value.Kind() != constant.Int || k.Uint64() != needle {
+ continue
+ }
+ k, ok = rem.Y.(*ir.Const)
+ if !ok || k.Value.Kind() != constant.Int || k.Uint64() != 2 {
+ continue
+ }
+
+ // if len(foo) % 2 != 0
+ call, ok := rem.X.(*ir.Call)
+ if !ok || !irutil.IsCallTo(call.Common(), "len") {
+ continue
+ }
+
+ // we're checking the length of a parameter that is a slice
+ // TODO(dh): support parameters that have flown through sigmas and phis
+ param, ok := call.Call.Args[0].(*ir.Parameter)
+ if !ok {
+ continue
+ }
+ if _, ok := param.Type().Underlying().(*types.Slice); !ok {
+ continue
+ }
+
+ // if len(foo) % 2 != 0 then panic
+ if _, ok := b.Succs[0].Control().(*ir.Panic); !ok {
+ continue
+ }
+
+ pass.ExportObjectFact(param.Object(), new(evenElements))
+ }
+ }
+}
+
+func findIndirectSliceLenChecks(pass *analysis.Pass) {
+ seen := map[*ir.Function]struct{}{}
+
+ var doFunction func(fn *ir.Function)
+ doFunction = func(fn *ir.Function) {
+ if _, ok := seen[fn]; ok {
+ return
+ }
+ seen[fn] = struct{}{}
+
+ for _, b := range fn.Blocks {
+ // all paths go through this block
+ if !b.Dominates(fn.Exit) {
+ continue
+ }
+
+ for _, instr := range b.Instrs {
+ call, ok := instr.(*ir.Call)
+ if !ok {
+ continue
+ }
+ callee := call.Call.StaticCallee()
+ if callee == nil {
+ continue
+ }
+
+ if callee.Pkg == fn.Pkg {
+ // TODO(dh): are we missing interesting wrappers
+ // because wrappers don't have Pkg set?
+ doFunction(callee)
+ }
+
+ for argi, arg := range call.Call.Args {
+ if callee.Signature.Recv() != nil {
+ if argi == 0 {
+ continue
+ }
+ argi--
+ }
+
+ // TODO(dh): support parameters that have flown through sigmas and phis
+ param, ok := arg.(*ir.Parameter)
+ if !ok {
+ continue
+ }
+ if _, ok := param.Type().Underlying().(*types.Slice); !ok {
+ continue
+ }
+
+ // We can't use callee.Params to look up the
+ // parameter, because Params is not populated for
+ // external functions. In our modular analysis.
+ // any function in any package that isn't the
+ // current package is consided "external", as it
+ // has been loaded from export data only.
+ sigParams := callee.Signature.Params()
+
+ if !pass.ImportObjectFact(sigParams.At(argi), new(evenElements)) {
+ continue
+ }
+ pass.ExportObjectFact(param.Object(), new(evenElements))
+ }
+ }
+ }
+ }
+
+ for _, fn := range pass.ResultOf[buildir.Analyzer].(*buildir.IR).SrcFuncs {
+ doFunction(fn)
+ }
+}
+
+func findSliceLength(v ir.Value) int {
+ // TODO(dh): VRP would help here
+
+ val := func(v ir.Value) int {
+ if v, ok := v.(*ir.Const); ok {
+ return int(v.Int64())
+ }
+ return -1
+ }
+ switch v := v.(type) {
+ case *ir.Slice:
+ low := 0
+ high := -1
+ if v.Low != nil {
+ low = val(v.Low)
+ }
+ if v.High != nil {
+ high = val(v.High)
+ } else {
+ switch vv := v.X.(type) {
+ case *ir.Alloc:
+ high = int(typeutil.Dereference(vv.Type()).Underlying().(*types.Array).Len())
+ case *ir.Slice:
+ high = findSliceLength(vv)
+ }
+ }
+ if low == -1 || high == -1 {
+ return -1
+ }
+ return high - low
+ default:
+ return -1
+ }
+}
+
+type evenElements struct{}
+
+func (evenElements) AFact() {}
+
+func (evenElements) String() string { return "needs even elements" }
+
+func flagSliceLens(pass *analysis.Pass) {
+ var tag evenElements
+
+ for _, fn := range pass.ResultOf[buildir.Analyzer].(*buildir.IR).SrcFuncs {
+ for _, b := range fn.Blocks {
+ for _, instr := range b.Instrs {
+ call, ok := instr.(ir.CallInstruction)
+ if !ok {
+ continue
+ }
+ callee := call.Common().StaticCallee()
+ if callee == nil {
+ continue
+ }
+ for argi, arg := range call.Common().Args {
+ if callee.Signature.Recv() != nil {
+ if argi == 0 {
+ continue
+ }
+ argi--
+ }
+
+ _, ok := arg.Type().Underlying().(*types.Slice)
+ if !ok {
+ continue
+ }
+ param := callee.Signature.Params().At(argi)
+ if !pass.ImportObjectFact(param, &tag) {
+ continue
+ }
+
+ // we know the argument has to have even length.
+ // now let's try to find its length
+ if n := findSliceLength(arg); n > -1 && n%2 != 0 {
+ src := call.Source().(*ast.CallExpr).Args[argi]
+ sig := call.Common().Signature()
+ var label string
+ if argi == sig.Params().Len()-1 && sig.Variadic() {
+ label = "variadic argument"
+ } else {
+ label = "argument"
+ }
+ // Note that param.Name() is guaranteed to not
+ // be empty, otherwise the function couldn't
+ // have enforced its length.
+ report.Report(pass, src, fmt.Sprintf("%s %q is expected to have even number of elements, but has %d elements", label, param.Name(), n))
+ }
+ }
+ }
+ }
+ }
+}
+
+func CheckEvenSliceLength(pass *analysis.Pass) (interface{}, error) {
+ findSliceLenChecks(pass)
+ findIndirectSliceLenChecks(pass)
+ flagSliceLens(pass)
+
+ return nil, nil
+}
diff --git a/staticcheck/lint_test.go b/staticcheck/lint_test.go
index 2a1615696..c79aaa0e5 100644
--- a/staticcheck/lint_test.go
+++ b/staticcheck/lint_test.go
@@ -80,6 +80,7 @@ func TestAll(t *testing.T) {
"SA5009": {{Dir: "CheckPrintf"}},
"SA5010": {{Dir: "CheckImpossibleTypeAssertion"}},
"SA5011": {{Dir: "CheckMaybeNil"}},
+ "SA5012": {{Dir: "CheckEvenSliceLength"}},
"SA6000": {{Dir: "CheckRegexpMatchLoop"}},
"SA6001": {{Dir: "CheckMapBytesKey"}},
"SA6002": {{Dir: "CheckSyncPoolValue"}},
diff --git a/staticcheck/testdata/src/CheckEvenSliceLength/CheckEvenSliceLength.go b/staticcheck/testdata/src/CheckEvenSliceLength/CheckEvenSliceLength.go
new file mode 100644
index 000000000..1de808980
--- /dev/null
+++ b/staticcheck/testdata/src/CheckEvenSliceLength/CheckEvenSliceLength.go
@@ -0,0 +1,50 @@
+package pkg
+
+import "strings"
+
+func fnVariadic(s string, args ...interface{}) { // want args:"needs even elements"
+ if len(args)%2 != 0 {
+ panic("I'm one of those annoying logging APIs")
+ }
+}
+
+func fnSlice(s string, args []interface{}) { // want args:"needs even elements"
+ if len(args)%2 != 0 {
+ panic("I'm one of those annoying logging APIs")
+ }
+}
+
+func fnIndirect(s string, args ...interface{}) { // want args:"needs even elements"
+ fnSlice(s, args)
+}
+
+func fn2(bleh []interface{}, arr1 [3]interface{}) { // want bleh:"needs even elements"
+ fnVariadic("%s", 1, 2, 3) // want `variadic argument "args".+ but has 3 elements`
+ args := []interface{}{1, 2, 3}
+ fnVariadic("", args...) // want `variadic argument "args".+ but has 3 elements`
+ fnVariadic("", args[:1]...) // want `variadic argument "args".+ but has 1 elements`
+ fnVariadic("", args[:2]...)
+ fnVariadic("", args[0:1]...) // want `variadic argument "args".+ but has 1 elements`
+ fnVariadic("", args[0:]...) // want `variadic argument "args".+ but has 3 elements`
+ fnVariadic("", args[:]...) // want `variadic argument "args".+ but has 3 elements`
+ fnVariadic("", bleh...)
+ fnVariadic("", bleh[:1]...) // want `variadic argument "args".+ but has 1 elements`
+ fnVariadic("", bleh[0:1]...) // want `variadic argument "args".+ but has 1 elements`
+ fnVariadic("", bleh[0:]...)
+ fnVariadic("", bleh[:]...)
+ fnVariadic("", bleh) // want `variadic argument "args".+ but has 1 elements`
+ fnVariadic("", make([]interface{}, 3)...) // want `variadic argument "args".+ but has 3 elements`
+ fnVariadic("", make([]interface{}, 4)...)
+ var arr2 [3]interface{}
+ fnVariadic("", arr1[:]...) // want `variadic argument "args".+ but has 3 elements`
+ fnVariadic("", arr2[:]...) // want `variadic argument "args".+ but has 3 elements`
+
+ fnSlice("", []interface{}{1, 2, 3}) // want `argument "args".+ but has 3 elements`
+ fnSlice("", []interface{}{1, 2, 3, 4})
+
+ fnIndirect("%s", 1, 2, 3) // want `argument "args".+ but has 3 elements`
+ fnIndirect("%s", 1, 2)
+
+ strings.NewReplacer("one") // want `variadic argument "oldnew".+ but has 1 elements`
+ strings.NewReplacer("one", "two")
+}
From 9b21780a6dea54d84dd05263ec284266081c9679 Mon Sep 17 00:00:00 2001
From: Dominik Honnef
Date: Mon, 8 Jun 2020 11:50:12 +0200
Subject: [PATCH 060/111] lintcmd: don't mention -explain flag if we only
encountered compile errors
The -explain flag does not provide any useful information on compiler
errors, so don't mention it.
Closes gh-776
---
lintcmd/cmd.go | 10 ++++++----
1 file changed, 6 insertions(+), 4 deletions(-)
diff --git a/lintcmd/cmd.go b/lintcmd/cmd.go
index f44b3c726..ccb49757d 100644
--- a/lintcmd/cmd.go
+++ b/lintcmd/cmd.go
@@ -659,6 +659,7 @@ func ProcessFlagSet(cs []*analysis.Analyzer, fs *flag.FlagSet) {
}
var (
+ numCompiles int
numErrors int
numWarnings int
numIgnored int
@@ -670,7 +671,6 @@ func ProcessFlagSet(cs []*analysis.Analyzer, fs *flag.FlagSet) {
analyzerNames[i] = a.Name
}
shouldExit := filterAnalyzerNames(analyzerNames, fail)
- shouldExit["compile"] = true
for _, p := range ps {
if p.Category == "compile" && debugNoCompile {
@@ -680,7 +680,9 @@ func ProcessFlagSet(cs []*analysis.Analyzer, fs *flag.FlagSet) {
numIgnored++
continue
}
- if shouldExit[p.Category] {
+ if p.Category == "compile" {
+ numCompiles++
+ } else if shouldExit[p.Category] {
numErrors++
} else {
p.Severity = severityWarning
@@ -689,14 +691,14 @@ func ProcessFlagSet(cs []*analysis.Analyzer, fs *flag.FlagSet) {
f.Format(p)
}
if f, ok := f.(statter); ok {
- f.Stats(len(ps), numErrors, numWarnings, numIgnored)
+ f.Stats(len(ps), numErrors+numCompiles, numWarnings, numIgnored)
}
if f, ok := f.(documentationMentioner); ok && (numErrors > 0 || numWarnings > 0) && len(os.Args) > 0 {
f.MentionCheckDocumentation(os.Args[0])
}
- if numErrors > 0 {
+ if numErrors > 0 || numCompiles > 0 {
exit(1)
}
exit(0)
From eb569e2d3e52015681a2ec1afe4ed09c9fc40b3d Mon Sep 17 00:00:00 2001
From: Dominik Honnef
Date: Mon, 8 Jun 2020 12:21:28 +0200
Subject: [PATCH 061/111] internal/robustio: reimport from upstream
This imports robustio from the Go project at commit 608cdcaede1e7133dc994b5e8894272c2dce744b.
This switches to using errors.As, and increases the timeout from 500ms
to 2000ms as per commit 093049b3709eda7537ece92a2991918cf53782d6.
Updates gh-558
---
internal/robustio/robustio_darwin.go | 14 +++-----------
internal/robustio/robustio_flaky.go | 13 ++++++-------
internal/robustio/robustio_other.go | 2 +-
internal/robustio/robustio_windows.go | 14 ++++----------
4 files changed, 14 insertions(+), 29 deletions(-)
diff --git a/internal/robustio/robustio_darwin.go b/internal/robustio/robustio_darwin.go
index 1ac0d10d7..99fd8ebc2 100644
--- a/internal/robustio/robustio_darwin.go
+++ b/internal/robustio/robustio_darwin.go
@@ -5,7 +5,7 @@
package robustio
import (
- "os"
+ "errors"
"syscall"
)
@@ -13,16 +13,8 @@ const errFileNotFound = syscall.ENOENT
// isEphemeralError returns true if err may be resolved by waiting.
func isEphemeralError(err error) bool {
- switch werr := err.(type) {
- case *os.PathError:
- err = werr.Err
- case *os.LinkError:
- err = werr.Err
- case *os.SyscallError:
- err = werr.Err
-
- }
- if errno, ok := err.(syscall.Errno); ok {
+ var errno syscall.Errno
+ if errors.As(err, &errno) {
return errno == errFileNotFound
}
return false
diff --git a/internal/robustio/robustio_flaky.go b/internal/robustio/robustio_flaky.go
index e0bf5b9b3..d4cb7e645 100644
--- a/internal/robustio/robustio_flaky.go
+++ b/internal/robustio/robustio_flaky.go
@@ -7,6 +7,7 @@
package robustio
import (
+ "errors"
"io/ioutil"
"math/rand"
"os"
@@ -14,9 +15,7 @@ import (
"time"
)
-const arbitraryTimeout = 500 * time.Millisecond
-
-const ERROR_SHARING_VIOLATION = 32
+const arbitraryTimeout = 2000 * time.Millisecond
// retry retries ephemeral errors from f up to an arbitrary timeout
// to work around filesystem flakiness on Windows and Darwin.
@@ -33,7 +32,8 @@ func retry(f func() (err error, mayRetry bool)) error {
return err
}
- if errno, ok := err.(syscall.Errno); ok && (lowestErrno == 0 || errno < lowestErrno) {
+ var errno syscall.Errno
+ if errors.As(err, &errno) && (lowestErrno == 0 || errno < lowestErrno) {
bestErr = err
lowestErrno = errno
} else if bestErr == nil {
@@ -54,7 +54,7 @@ func retry(f func() (err error, mayRetry bool)) error {
// rename is like os.Rename, but retries ephemeral errors.
//
-// On windows it wraps os.Rename, which (as of 2019-06-04) uses MoveFileEx with
+// On Windows it wraps os.Rename, which (as of 2019-06-04) uses MoveFileEx with
// MOVEFILE_REPLACE_EXISTING.
//
// Windows also provides a different system call, ReplaceFile,
@@ -79,8 +79,7 @@ func readFile(filename string) ([]byte, error) {
// Unlike in rename, we do not retry errFileNotFound here: it can occur
// as a spurious error, but the file may also genuinely not exist, so the
// increase in robustness is probably not worth the extra latency.
-
- return err, isEphemeralError(err) && err != errFileNotFound
+ return err, isEphemeralError(err) && !errors.Is(err, errFileNotFound)
})
return b, err
}
diff --git a/internal/robustio/robustio_other.go b/internal/robustio/robustio_other.go
index a2428856f..907b55685 100644
--- a/internal/robustio/robustio_other.go
+++ b/internal/robustio/robustio_other.go
@@ -2,7 +2,7 @@
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
-//+build !windows,!darwin
+// +build !windows,!darwin
package robustio
diff --git a/internal/robustio/robustio_windows.go b/internal/robustio/robustio_windows.go
index a35237d44..200070a9e 100644
--- a/internal/robustio/robustio_windows.go
+++ b/internal/robustio/robustio_windows.go
@@ -5,23 +5,17 @@
package robustio
import (
- "os"
+ "errors"
"syscall"
)
+const ERROR_SHARING_VIOLATION = 32
const errFileNotFound = syscall.ERROR_FILE_NOT_FOUND
// isEphemeralError returns true if err may be resolved by waiting.
func isEphemeralError(err error) bool {
- switch werr := err.(type) {
- case *os.PathError:
- err = werr.Err
- case *os.LinkError:
- err = werr.Err
- case *os.SyscallError:
- err = werr.Err
- }
- if errno, ok := err.(syscall.Errno); ok {
+ var errno syscall.Errno
+ if errors.As(err, &errno) {
switch errno {
case syscall.ERROR_ACCESS_DENIED,
syscall.ERROR_FILE_NOT_FOUND,
From 6731d4c46f514e60d8d184fce025b94a40ecdad5 Mon Sep 17 00:00:00 2001
From: Dominik Honnef
Date: Mon, 8 Jun 2020 12:30:10 +0200
Subject: [PATCH 062/111] internal/renameio: reimport from upstream
This imports renameio from the Go project at commit
608cdcaede1e7133dc994b5e8894272c2dce744b. Primarily, this switches us
to using errors.As.
Updates gh-558
---
internal/renameio/renameio.go | 26 +++++++++++++-------------
internal/renameio/renameio_test.go | 11 +++++++----
2 files changed, 20 insertions(+), 17 deletions(-)
diff --git a/internal/renameio/renameio.go b/internal/renameio/renameio.go
index a279d1a1e..5e0dfa2f8 100644
--- a/internal/renameio/renameio.go
+++ b/internal/renameio/renameio.go
@@ -66,6 +66,19 @@ func WriteToFile(filename string, data io.Reader, perm os.FileMode) (err error)
return robustio.Rename(f.Name(), filename)
}
+// ReadFile is like ioutil.ReadFile, but on Windows retries spurious errors that
+// may occur if the file is concurrently replaced.
+//
+// Errors are classified heuristically and retries are bounded, so even this
+// function may occasionally return a spurious error on Windows.
+// If so, the error will likely wrap one of:
+// - syscall.ERROR_ACCESS_DENIED
+// - syscall.ERROR_FILE_NOT_FOUND
+// - internal/syscall/windows.ERROR_SHARING_VIOLATION
+func ReadFile(filename string) ([]byte, error) {
+ return robustio.ReadFile(filename)
+}
+
// tempFile creates a new temporary file with given permission bits.
func tempFile(dir, prefix string, perm os.FileMode) (f *os.File, err error) {
for i := 0; i < 10000; i++ {
@@ -78,16 +91,3 @@ func tempFile(dir, prefix string, perm os.FileMode) (f *os.File, err error) {
}
return
}
-
-// ReadFile is like ioutil.ReadFile, but on Windows retries spurious errors that
-// may occur if the file is concurrently replaced.
-//
-// Errors are classified heuristically and retries are bounded, so even this
-// function may occasionally return a spurious error on Windows.
-// If so, the error will likely wrap one of:
-// - syscall.ERROR_ACCESS_DENIED
-// - syscall.ERROR_FILE_NOT_FOUND
-// - internal/syscall/windows.ERROR_SHARING_VIOLATION
-func ReadFile(filename string) ([]byte, error) {
- return robustio.ReadFile(filename)
-}
diff --git a/internal/renameio/renameio_test.go b/internal/renameio/renameio_test.go
index afd6eec9e..2f69be4c7 100644
--- a/internal/renameio/renameio_test.go
+++ b/internal/renameio/renameio_test.go
@@ -8,6 +8,7 @@ package renameio
import (
"encoding/binary"
+ "errors"
"io/ioutil"
"math/rand"
"os"
@@ -61,9 +62,10 @@ func TestConcurrentReadsAndWrites(t *testing.T) {
atomic.AddInt64(&writeSuccesses, 1)
} else if robustio.IsEphemeralError(err) {
var (
- dup bool
+ errno syscall.Errno
+ dup bool
)
- if errno, ok := err.(syscall.Errno); ok {
+ if errors.As(err, &errno) {
_, dup = writeErrnoSeen.LoadOrStore(errno, true)
}
if !dup {
@@ -79,9 +81,10 @@ func TestConcurrentReadsAndWrites(t *testing.T) {
atomic.AddInt64(&readSuccesses, 1)
} else if robustio.IsEphemeralError(err) {
var (
- dup bool
+ errno syscall.Errno
+ dup bool
)
- if errno, ok := err.(syscall.Errno); ok {
+ if errors.As(err, &errno) {
_, dup = readErrnoSeen.LoadOrStore(errno, true)
}
if !dup {
From bb930f5b1454f7f8280e9c865ccad124715ac4c9 Mon Sep 17 00:00:00 2001
From: Dominik Honnef
Date: Mon, 8 Jun 2020 12:46:51 +0200
Subject: [PATCH 063/111] internal/cache: reimport from upstream
This imports cache from the Go project at commit
608cdcaede1e7133dc994b5e8894272c2dce744b..
---
internal/cache/cache.go | 70 ++++++++++++++++++++++++++----------
internal/cache/cache_test.go | 2 +-
2 files changed, 52 insertions(+), 20 deletions(-)
diff --git a/internal/cache/cache.go b/internal/cache/cache.go
index cfd4241f9..0031edc69 100644
--- a/internal/cache/cache.go
+++ b/internal/cache/cache.go
@@ -77,7 +77,22 @@ func (c *Cache) fileName(id [HashSize]byte, key string) string {
return filepath.Join(c.dir, fmt.Sprintf("%02x", id[0]), fmt.Sprintf("%x", id)+"-"+key)
}
-var ErrMissing = errors.New("cache entry not found")
+// An entryNotFoundError indicates that a cache entry was not found, with an
+// optional underlying reason.
+type entryNotFoundError struct {
+ Err error
+}
+
+func (e *entryNotFoundError) Error() string {
+ if e.Err == nil {
+ return "cache entry not found"
+ }
+ return fmt.Sprintf("cache entry not found: %v", e.Err)
+}
+
+func (e *entryNotFoundError) Unwrap() error {
+ return e.Err
+}
const (
// action entry file is "v1 \n"
@@ -96,6 +111,8 @@ const (
// GODEBUG=gocacheverify=1.
var verify = false
+var errVerifyMode = errors.New("gocacheverify=1")
+
// DebugTest is set when GODEBUG=gocachetest=1 is in the environment.
var DebugTest = false
@@ -124,7 +141,7 @@ func initEnv() {
// saved file for that output ID is still available.
func (c *Cache) Get(id ActionID) (Entry, error) {
if verify {
- return Entry{}, ErrMissing
+ return Entry{}, &entryNotFoundError{Err: errVerifyMode}
}
return c.get(id)
}
@@ -137,20 +154,27 @@ type Entry struct {
// get is Get but does not respect verify mode, so that Put can use it.
func (c *Cache) get(id ActionID) (Entry, error) {
- missing := func() (Entry, error) {
- return Entry{}, ErrMissing
+ missing := func(reason error) (Entry, error) {
+ return Entry{}, &entryNotFoundError{Err: reason}
}
f, err := os.Open(c.fileName(id, "a"))
if err != nil {
- return missing()
+ return missing(err)
}
defer f.Close()
entry := make([]byte, entrySize+1) // +1 to detect whether f is too long
- if n, err := io.ReadFull(f, entry); n != entrySize || err != io.ErrUnexpectedEOF {
- return missing()
+ if n, err := io.ReadFull(f, entry); n > entrySize {
+ return missing(errors.New("too long"))
+ } else if err != io.ErrUnexpectedEOF {
+ if err == io.EOF {
+ return missing(errors.New("file is empty"))
+ }
+ return missing(err)
+ } else if n < entrySize {
+ return missing(errors.New("entry file incomplete"))
}
if entry[0] != 'v' || entry[1] != '1' || entry[2] != ' ' || entry[3+hexSize] != ' ' || entry[3+hexSize+1+hexSize] != ' ' || entry[3+hexSize+1+hexSize+1+20] != ' ' || entry[entrySize-1] != '\n' {
- return missing()
+ return missing(errors.New("invalid header"))
}
eid, entry := entry[3:3+hexSize], entry[3+hexSize:]
eout, entry := entry[1:1+hexSize], entry[1+hexSize:]
@@ -158,27 +182,33 @@ func (c *Cache) get(id ActionID) (Entry, error) {
//lint:ignore SA4006 See https://github.com/dominikh/go-tools/issues/465
etime, entry := entry[1:1+20], entry[1+20:]
var buf [HashSize]byte
- if _, err := hex.Decode(buf[:], eid); err != nil || buf != id {
- return missing()
+ if _, err := hex.Decode(buf[:], eid); err != nil {
+ return missing(fmt.Errorf("decoding ID: %v", err))
+ } else if buf != id {
+ return missing(errors.New("mismatched ID"))
}
if _, err := hex.Decode(buf[:], eout); err != nil {
- return missing()
+ return missing(fmt.Errorf("decoding output ID: %v", err))
}
i := 0
for i < len(esize) && esize[i] == ' ' {
i++
}
size, err := strconv.ParseInt(string(esize[i:]), 10, 64)
- if err != nil || size < 0 {
- return missing()
+ if err != nil {
+ return missing(fmt.Errorf("parsing size: %v", err))
+ } else if size < 0 {
+ return missing(errors.New("negative size"))
}
i = 0
for i < len(etime) && etime[i] == ' ' {
i++
}
tm, err := strconv.ParseInt(string(etime[i:]), 10, 64)
- if err != nil || tm < 0 {
- return missing()
+ if err != nil {
+ return missing(fmt.Errorf("parsing timestamp: %v", err))
+ } else if tm < 0 {
+ return missing(errors.New("negative timestamp"))
}
c.used(c.fileName(id, "a"))
@@ -195,8 +225,11 @@ func (c *Cache) GetFile(id ActionID) (file string, entry Entry, err error) {
}
file = c.OutputFile(entry.OutputID)
info, err := os.Stat(file)
- if err != nil || info.Size() != entry.Size {
- return "", Entry{}, ErrMissing
+ if err != nil {
+ return "", Entry{}, &entryNotFoundError{Err: err}
+ }
+ if info.Size() != entry.Size {
+ return "", Entry{}, &entryNotFoundError{Err: errors.New("file incomplete")}
}
return file, entry, nil
}
@@ -211,7 +244,7 @@ func (c *Cache) GetBytes(id ActionID) ([]byte, Entry, error) {
}
data, _ := ioutil.ReadFile(c.OutputFile(entry.OutputID))
if sha256.Sum256(data) != entry.OutputID {
- return nil, entry, ErrMissing
+ return nil, entry, &entryNotFoundError{Err: errors.New("bad checksum")}
}
return data, entry, nil
}
@@ -327,7 +360,6 @@ func (c *Cache) putIndexEntry(id ActionID, out OutputID, size int64, allowVerify
// are entirely reproducible. As just noted, this may be unrealistic
// in some cases but the check is also useful for shaking out real bugs.
entry := fmt.Sprintf("v1 %x %x %20d %20d\n", id, out, size, time.Now().UnixNano())
-
if verify && allowVerify {
old, err := c.get(id)
if err == nil && (old.OutputID != out || old.Size != size) {
diff --git a/internal/cache/cache_test.go b/internal/cache/cache_test.go
index 7229bc4ce..1988c3450 100644
--- a/internal/cache/cache_test.go
+++ b/internal/cache/cache_test.go
@@ -78,7 +78,7 @@ func TestGrowth(t *testing.T) {
n := 10000
if testing.Short() {
- n = 1000
+ n = 10
}
for i := 0; i < n; i++ {
From d803454e4d17b11dfd56d966e85db98402fce9fe Mon Sep 17 00:00:00 2001
From: Dominik Honnef
Date: Sat, 13 Jun 2020 18:43:02 +0200
Subject: [PATCH 064/111] Make benchmarks more automatic
- Benchmark multiple package patterns at once.
- Support CSV output
- Test varying values of GOGC
---
_benchmarks/bench.sh | 81 +++++++++++++++++++++++++++++++++-----------
1 file changed, 61 insertions(+), 20 deletions(-)
diff --git a/_benchmarks/bench.sh b/_benchmarks/bench.sh
index 4471a7dd6..5f3c9c024 100755
--- a/_benchmarks/bench.sh
+++ b/_benchmarks/bench.sh
@@ -1,31 +1,72 @@
-#!/usr/bin/env sh
+#!/usr/bin/env bash
set -e
-# PKG="k8s.io/kubernetes/pkg/..."
-# LABEL="k8s"
-PKG="std"
-LABEL=$PKG
-MIN_CORES=16
+
+declare -A PKGS=(
+ ["strconv"]="strconv"
+ ["std"]="std"
+ ["k8s"]="k8s.io/kubernetes/pkg/..."
+)
+
+MIN_CORES=1
MAX_CORES=16
+MIN_GOGC=10
+MAX_GOGC=100
SAMPLES=5
WIPE_CACHE=1
+FORMAT=csv
BIN=$(realpath ./silent-staticcheck.sh)
+SMT=1
-go build ../cmd/staticcheck
-export GO111MODULE=off
+runBenchmark() {
+ local pkg="$1"
+ local label="$2"
+ local gc="$3"
+ local cores="$4"
+ local wipe="$5"
+
+ if [ $wipe -ne 0 ]; then
+ rm -rf ~/.cache/staticcheck
+ fi
-for cores in $(seq $MIN_CORES $MAX_CORES); do
- for i in $(seq 1 $SAMPLES); do
+ local procs
+ if [ $SMT -ne 0 ]; then
procs=$((cores*2))
- if [ $WIPE_CACHE -ne 0 ]; then
- rm -rf ~/.cache/staticcheck
- fi
-
- out=$(env time -f "%e %M" taskset -c 0-$((procs-1)) $BIN $PKG 2>&1)
- t=$(echo "$out" | cut -f1 -d" ")
- m=$(echo "$out" | cut -f2 -d" ")
- ns=$(printf "%s 1000000000 * p" $t | dc)
- b=$((m * 1024))
- printf "BenchmarkStaticcheck-%s-%d 1 %.0f ns/op %.0f B/op\n" "$LABEL" "$procs" "$ns" "$b"
+ else
+ procs=$cores
+ fi
+
+ local out=$(GOGC=$gc env time -f "%e %M" taskset -c 0-$((procs-1)) $BIN $pkg 2>&1)
+ local t=$(echo "$out" | cut -f1 -d" ")
+ local m=$(echo "$out" | cut -f2 -d" ")
+ local ns=$(printf "%s 1000000000 * p" $t | dc)
+ local b=$((m * 1024))
+
+ case $FORMAT in
+ bench)
+ printf "BenchmarkStaticcheck-%s-GOGC%d-wiped%d-%d 1 %.0f ns/op %.0f B/op\n" "$label" "$gc" "$wipe" "$procs" "$ns" "$b"
+ ;;
+ csv)
+ printf "%s,%d,%d,%d,%.0f,%.0f\n" "$label" "$gc" "$procs" "$wipe" "$ns" "$b"
+ ;;
+ esac
+}
+
+go build ../cmd/staticcheck
+export GO111MODULE=off
+
+if [ "$FORMAT" = "csv" ]; then
+ printf "packages,gogc,procs,wipe-cache,time,memory\n"
+fi
+
+for label in "${!PKGS[@]}"; do
+ pkg=${PKGS[$label]}
+ for gc in $(seq $MIN_GOGC 10 $MAX_GOGC); do
+ for cores in $(seq $MIN_CORES $MAX_CORES); do
+ for i in $(seq 1 $SAMPLES); do
+ runBenchmark "$pkg" "$label" "$gc" "$cores" 1
+ runBenchmark "$pkg" "$label" "$gc" "$cores" 0
+ done
+ done
done
done
From 8477a449cafdad3b31eec8a7daf735c2b23520af Mon Sep 17 00:00:00 2001
From: Sourya Vatsyayan
Date: Mon, 1 Jun 2020 10:39:27 +0530
Subject: [PATCH 065/111] SA1012: fix tests for suggested fixes
Signed-off-by: Sourya Vatsyayan
Closes: gh-771 [via git-merge-pr]
---
.../checkStdlibUsageNilContext.go.golden | 26 +++++++++++++++++++
1 file changed, 26 insertions(+)
diff --git a/staticcheck/testdata/src/checkStdlibUsageNilContext/checkStdlibUsageNilContext.go.golden b/staticcheck/testdata/src/checkStdlibUsageNilContext/checkStdlibUsageNilContext.go.golden
index cef6d7a0b..4f6d38a5b 100644
--- a/staticcheck/testdata/src/checkStdlibUsageNilContext/checkStdlibUsageNilContext.go.golden
+++ b/staticcheck/testdata/src/checkStdlibUsageNilContext/checkStdlibUsageNilContext.go.golden
@@ -1,3 +1,4 @@
+-- use context.Background --
package pkg
import "context"
@@ -22,3 +23,28 @@ func fn3() {
_ = (func())(nil)
(*T).Foo(nil)
}
+-- use context.TODO --
+package pkg
+
+import "context"
+
+func fn1(ctx context.Context) {}
+func fn2(x string, ctx context.Context) {}
+func fn4() {}
+
+type T struct{}
+
+func (*T) Foo() {}
+
+func fn3() {
+ fn1(context.TODO()) // want `do not pass a nil Context`
+ fn1(context.TODO())
+ fn2("", nil)
+ fn4()
+
+ // don't flag this conversion
+ _ = (func(context.Context))(nil)
+ // and don't crash on these
+ _ = (func())(nil)
+ (*T).Foo(nil)
+}
From ea50789e60ce4e9dd60f65f49d7ff5217e0586e4 Mon Sep 17 00:00:00 2001
From: Dominik Honnef
Date: Sat, 13 Jun 2020 18:52:25 +0200
Subject: [PATCH 066/111] Update version of golang.org/x/tools
---
go.mod | 2 +-
go.sum | 2 ++
2 files changed, 3 insertions(+), 1 deletion(-)
diff --git a/go.mod b/go.mod
index 3b3c8b956..044b4f28e 100644
--- a/go.mod
+++ b/go.mod
@@ -8,5 +8,5 @@ require (
github.com/kisielk/gotool v1.0.0
github.com/rogpeppe/go-internal v1.3.0
golang.org/x/mod v0.2.0
- golang.org/x/tools v0.0.0-20200427214658-4697a2867c88
+ golang.org/x/tools v0.0.0-20200609164405-eb789aa7ce50
)
diff --git a/go.sum b/go.sum
index 6e1c076f7..fce3b1197 100644
--- a/go.sum
+++ b/go.sum
@@ -42,6 +42,8 @@ golang.org/x/tools v0.0.0-20200422022333-3d57cf2e726e h1:3Dzrrxi54Io7Aoyb0PYLsI4
golang.org/x/tools v0.0.0-20200422022333-3d57cf2e726e/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.0.0-20200427214658-4697a2867c88 h1:Nj7oNnL9tSACMt2JvszZN6P4IXiy1t6E/YRMr7YtaSw=
golang.org/x/tools v0.0.0-20200427214658-4697a2867c88/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
+golang.org/x/tools v0.0.0-20200609164405-eb789aa7ce50 h1:59syOWj4+Fl+op4LL8fX1kO7HmbdEWfxlw4tcGvH+y0=
+golang.org/x/tools v0.0.0-20200609164405-eb789aa7ce50/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
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 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4=
From 863fc4d130ce5853512b2c97884469e360f3c96a Mon Sep 17 00:00:00 2001
From: Dominik Honnef
Date: Sat, 13 Jun 2020 19:13:48 +0200
Subject: [PATCH 067/111] CI: target Go 1.13 as lowest supported version
---
.github/workflows/ci.yml | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index fe36b76d1..96122a813 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -30,4 +30,4 @@ jobs:
with:
fetch-depth: 1
- run: "go vet ./..."
- - run: "$(go env GOPATH)/bin/staticcheck -go 1.11 ./..."
+ - run: "$(go env GOPATH)/bin/staticcheck -go 1.13 ./..."
From 66f0fd38314f0dbc92c1e7144070553f0c161047 Mon Sep 17 00:00:00 2001
From: Dominik Honnef
Date: Sat, 13 Jun 2020 19:26:53 +0200
Subject: [PATCH 068/111] lintcmd: exit non-zero if we encounter unmatched
ignore directives
This has been the behavior when we originally added ignore directives.
In Staticcheck 2019.2 we somehow lost the behavior. Restore it.
---
lintcmd/cmd.go | 3 ++-
1 file changed, 2 insertions(+), 1 deletion(-)
diff --git a/lintcmd/cmd.go b/lintcmd/cmd.go
index ccb49757d..b5ce6d8fa 100644
--- a/lintcmd/cmd.go
+++ b/lintcmd/cmd.go
@@ -257,7 +257,7 @@ func filterIgnored(problems []problem, res runner.ResultData, allowedAnalyzers m
Diagnostic: runner.Diagnostic{
Position: ig.Pos,
Message: "this linter directive didn't match anything; should it be removed?",
- Category: "",
+ Category: "staticcheck",
},
}
moreProblems = append(moreProblems, p)
@@ -671,6 +671,7 @@ func ProcessFlagSet(cs []*analysis.Analyzer, fs *flag.FlagSet) {
analyzerNames[i] = a.Name
}
shouldExit := filterAnalyzerNames(analyzerNames, fail)
+ shouldExit["staticcheck"] = true
for _, p := range ps {
if p.Category == "compile" && debugNoCompile {
From 9cc924ae9826f784f093cce37c36a4dd01b48371 Mon Sep 17 00:00:00 2001
From: Dominik Honnef
Date: Sun, 14 Jun 2020 02:51:21 +0200
Subject: [PATCH 069/111] lintcmd: print reference to -explain check to stderr
Separate diagnostics from help output. The output of the plain
formatter is supposed to be machine readable, so don't emit text meant
strictly for humans.
For the stylish formatter we could've gone either way, because its
output is not meant to be processed by machines, but we copy the
behavior of the plain formatter for the sake of consistency.
---
lintcmd/cmd.go | 4 ++--
lintcmd/format.go | 24 +++++++++++++-----------
2 files changed, 15 insertions(+), 13 deletions(-)
diff --git a/lintcmd/cmd.go b/lintcmd/cmd.go
index b5ce6d8fa..6699fd9f7 100644
--- a/lintcmd/cmd.go
+++ b/lintcmd/cmd.go
@@ -632,9 +632,9 @@ func ProcessFlagSet(cs []*analysis.Analyzer, fs *flag.FlagSet) {
var f formatter
switch theFormatter {
case "text":
- f = textFormatter{W: os.Stdout}
+ f = textFormatter{Diagnostics: os.Stdout, UI: os.Stderr}
case "stylish":
- f = &stylishFormatter{W: os.Stdout}
+ f = &stylishFormatter{Diagnostics: os.Stdout, UI: os.Stderr}
case "json":
f = jsonFormatter{W: os.Stdout}
default:
diff --git a/lintcmd/format.go b/lintcmd/format.go
index f3d95345a..7f47e1d30 100644
--- a/lintcmd/format.go
+++ b/lintcmd/format.go
@@ -48,18 +48,19 @@ type documentationMentioner interface {
}
type textFormatter struct {
- W io.Writer
+ Diagnostics io.Writer
+ UI io.Writer
}
func (o textFormatter) Format(p problem) {
- fmt.Fprintf(o.W, "%s: %s\n", relativePositionString(p.Position), p.String())
+ fmt.Fprintf(o.Diagnostics, "%s: %s\n", relativePositionString(p.Position), p.String())
for _, r := range p.Related {
- fmt.Fprintf(o.W, "\t%s: %s\n", relativePositionString(r.Position), r.Message)
+ fmt.Fprintf(o.Diagnostics, "\t%s: %s\n", relativePositionString(r.Position), r.Message)
}
}
func (o textFormatter) MentionCheckDocumentation(cmd string) {
- fmt.Fprintf(o.W, "\nRun '%s -explain ' or visit https://staticcheck.io/docs/checks for documentation on checks.\n", cmd)
+ fmt.Fprintf(o.UI, "\nRun '%s -explain ' or visit https://staticcheck.io/docs/checks for documentation on checks.\n", cmd)
}
type jsonFormatter struct {
@@ -118,7 +119,8 @@ func (o jsonFormatter) Format(p problem) {
}
type stylishFormatter struct {
- W io.Writer
+ Diagnostics io.Writer
+ UI io.Writer
prevFile string
tw *tabwriter.Writer
@@ -133,11 +135,11 @@ func (o *stylishFormatter) Format(p problem) {
if pos.Filename != o.prevFile {
if o.prevFile != "" {
o.tw.Flush()
- fmt.Fprintln(o.W)
+ fmt.Fprintln(o.Diagnostics)
}
- fmt.Fprintln(o.W, pos.Filename)
+ fmt.Fprintln(o.Diagnostics, pos.Filename)
o.prevFile = pos.Filename
- o.tw = tabwriter.NewWriter(o.W, 0, 4, 2, ' ', 0)
+ o.tw = tabwriter.NewWriter(o.Diagnostics, 0, 4, 2, ' ', 0)
}
fmt.Fprintf(o.tw, " (%d, %d)\t%s\t%s\n", pos.Line, pos.Column, p.Category, p.Message)
for _, r := range p.Related {
@@ -146,14 +148,14 @@ func (o *stylishFormatter) Format(p problem) {
}
func (o *stylishFormatter) MentionCheckDocumentation(cmd string) {
- textFormatter{W: o.W}.MentionCheckDocumentation(cmd)
+ textFormatter{UI: o.UI}.MentionCheckDocumentation(cmd)
}
func (o *stylishFormatter) Stats(total, errors, warnings, ignored int) {
if o.tw != nil {
o.tw.Flush()
- fmt.Fprintln(o.W)
+ fmt.Fprintln(o.Diagnostics)
}
- fmt.Fprintf(o.W, " ✖ %d problems (%d errors, %d warnings, %d ignored)\n",
+ fmt.Fprintf(o.Diagnostics, " ✖ %d problems (%d errors, %d warnings, %d ignored)\n",
total, errors, warnings, ignored)
}
From d49b8deb7c27ceff53894fbbbb0f604587efb075 Mon Sep 17 00:00:00 2001
From: Dominik Honnef
Date: Sun, 14 Jun 2020 18:52:52 +0200
Subject: [PATCH 070/111] lintcmd/runner: use GOMAXPROCS instead of NumCPU
GOMAXPROCS subsumes NumCPU for the purpose of sizing semaphores. If
users set CPU affinity, then GOMAXPROCS will reflect that. If users
only set GOMAXPROCS, then NumCPU would be inaccurate. Additionally,
there are plans to make GOMAXPROCS aware of CPU quotas
(https://github.com/golang/go/issues/33803).
Users are still advised to set CPU affinity instead of relying on
GOMAXPROCS to limit CPU usage, because Staticcheck shells out to the
underlying build system, which together with Staticcheck would be able
to use more CPU than intended if limited by just GOMAXPROCS.
---
lintcmd/runner/runner.go | 8 ++++----
1 file changed, 4 insertions(+), 4 deletions(-)
diff --git a/lintcmd/runner/runner.go b/lintcmd/runner/runner.go
index b3c245a43..867d192f0 100644
--- a/lintcmd/runner/runner.go
+++ b/lintcmd/runner/runner.go
@@ -42,13 +42,13 @@
//
// Actions are executed in parallel where the dependency graph allows.
// Overall parallelism is bounded by a semaphore, sized according to
-// runtime.NumCPU(). Each concurrently processed package takes up a
+// GOMAXPROCS. Each concurrently processed package takes up a
// token, as does each analyzer – but a package can always execute at
// least one analyzer, using the package's token.
//
-// Depending on the overall shape of the graph, there may be NumCPU
+// Depending on the overall shape of the graph, there may be GOMAXPROCS
// packages running a single analyzer each, a single package running
-// NumCPU analyzers, or anything in between.
+// GOMAXPROCS analyzers, or anything in between.
//
// Total memory consumption grows roughly linearly with the number of
// CPUs, while total execution time is inversely proportional to the
@@ -353,7 +353,7 @@ func New(cfg config.Config) (*Runner, error) {
return &Runner{
cfg: cfg,
cache: cache,
- semaphore: tsync.NewSemaphore(runtime.NumCPU()),
+ semaphore: tsync.NewSemaphore(runtime.GOMAXPROCS(0)),
}, nil
}
From 925e3c1d1b98eafb5e5e2678b6545d4da084325a Mon Sep 17 00:00:00 2001
From: Dominik Honnef
Date: Mon, 15 Jun 2020 03:48:26 +0200
Subject: [PATCH 071/111] Cache staticcheck export data between CI runs
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
This doesn't really make our CI run any faster, because running the
tests takes much longer than running staticcheck, even uncached.
However, our approach to caching should serve as a good reference to
other people using staticcheck, hopefully with faster tests.
We experimented with caching test results, but that was complicated by
how cache keys for tests are computed. The test binary generates a log
of interactions with the environment, such as statting files, and the
environment gets captured in the cache key. In the case of stat, this
includes the ctime and mtime, which will be different for every run of
CI. We could set all ctimes and mtimes to a fixed timestamp, but that
would require different scripts for different operating systems – or a
helper utility.
We also experimented with caching $GOPATH/pkg/mod, but action/setup-go
doesn't make it easy to figure out what GOPATH is, and downloading our
dependencies is not enough of a bottleneck to warrant complicating our
workflows further.
---
.github/workflows/ci.yml | 6 ++++++
1 file changed, 6 insertions(+)
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 96122a813..852081232 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -29,5 +29,11 @@ jobs:
- uses: actions/checkout@v1
with:
fetch-depth: 1
+ - uses: actions/cache@v2
+ with:
+ path: ~/.cache/staticcheck
+ key: staticcheck-${{ github.sha }}
+ restore-keys: |
+ staticcheck-
- run: "go vet ./..."
- run: "$(go env GOPATH)/bin/staticcheck -go 1.13 ./..."
From c6b60f9125c8c162016777adc4d8b1d9e65c319f Mon Sep 17 00:00:00 2001
From: Dominik Honnef
Date: Fri, 19 Jun 2020 07:49:04 +0200
Subject: [PATCH 072/111] unused: remove unnecessary checker type
---
unused/unused.go | 33 +++++++++++++--------------------
1 file changed, 13 insertions(+), 20 deletions(-)
diff --git a/unused/unused.go b/unused/unused.go
index 6c49574d3..ae203b13c 100644
--- a/unused/unused.go
+++ b/unused/unused.go
@@ -493,10 +493,6 @@ func serializeObject(pass *analysis.Pass, fset *token.FileSet, obj types.Object)
}
}
-type checker struct {
- graph *graph
-}
-
func debugf(f string, v ...interface{}) {
if Debug != nil {
fmt.Fprintf(Debug, f, v...)
@@ -517,12 +513,9 @@ func run(pass *analysis.Pass) (interface{}, error) {
Directives: dirs,
}
- c := &checker{
- graph: newGraph(pkg),
- }
-
- c.graph.entry(pkg)
- used, unused := c.results()
+ g := newGraph(pkg)
+ g.entry(pkg)
+ used, unused := results(g)
if Debug != nil {
debugNode := func(n *node) {
@@ -545,11 +538,11 @@ func run(pass *analysis.Pass) (interface{}, error) {
}
debugf("digraph{\n")
- debugNode(c.graph.Root)
- for _, v := range c.graph.Nodes {
+ debugNode(g.Root)
+ for _, v := range g.Nodes {
debugNode(v)
}
- for _, node := range c.graph.TypeNodes {
+ for _, node := range g.TypeNodes {
debugNode(node)
}
@@ -559,23 +552,23 @@ func run(pass *analysis.Pass) (interface{}, error) {
return Result{Used: used, Unused: unused}, nil
}
-func (c *checker) results() (used, unused []types.Object) {
- c.graph.color(c.graph.Root)
- for _, node := range c.graph.TypeNodes {
+func results(g *graph) (used, unused []types.Object) {
+ g.color(g.Root)
+ for _, node := range g.TypeNodes {
if node.seen {
continue
}
switch obj := node.obj.(type) {
case *types.Struct:
for i := 0; i < obj.NumFields(); i++ {
- if node, ok := c.graph.nodeMaybe(obj.Field(i)); ok {
+ if node, ok := g.nodeMaybe(obj.Field(i)); ok {
node.quiet = true
}
}
case *types.Interface:
for i := 0; i < obj.NumExplicitMethods(); i++ {
m := obj.ExplicitMethod(i)
- if node, ok := c.graph.nodeMaybe(m); ok {
+ if node, ok := g.nodeMaybe(m); ok {
node.quiet = true
}
}
@@ -584,7 +577,7 @@ func (c *checker) results() (used, unused []types.Object) {
// OPT(dh): can we find meaningful initial capacities for the used and unused slices?
- for _, n := range c.graph.Nodes {
+ for _, n := range g.Nodes {
if obj, ok := n.obj.(types.Object); ok {
switch obj := obj.(type) {
case *types.Var:
@@ -602,7 +595,7 @@ func (c *checker) results() (used, unused []types.Object) {
if n.seen {
used = append(used, obj)
} else if !n.quiet {
- if obj.Pkg() != c.graph.pkg.Pkg {
+ if obj.Pkg() != g.pkg.Pkg {
continue
}
unused = append(unused, obj)
From e9bf2c7b0e3c3a391b27acfdc19980c757834734 Mon Sep 17 00:00:00 2001
From: Dominik Honnef
Date: Fri, 19 Jun 2020 08:01:04 +0200
Subject: [PATCH 073/111] unused: set g.pkg in entry, reduce uses of g.pkg
---
unused/unused.go | 14 +++++++-------
1 file changed, 7 insertions(+), 7 deletions(-)
diff --git a/unused/unused.go b/unused/unused.go
index ae203b13c..13ff4245f 100644
--- a/unused/unused.go
+++ b/unused/unused.go
@@ -513,7 +513,7 @@ func run(pass *analysis.Pass) (interface{}, error) {
Directives: dirs,
}
- g := newGraph(pkg)
+ g := newGraph()
g.entry(pkg)
used, unused := results(g)
@@ -620,13 +620,12 @@ type graph struct {
nodeCounter uint64
}
-func newGraph(pkg *pkg) *graph {
+func newGraph() *graph {
g := &graph{
Nodes: map[interface{}]*node{},
seenFns: map[string]struct{}{},
seenTypes: map[types.Type]struct{}{},
TypeNodes: map[types.Type]*node{},
- pkg: pkg,
}
g.Root = g.newNode(nil)
return g
@@ -828,6 +827,7 @@ func (g *graph) seeAndUse(used, by interface{}, kind edgeKind) *node {
}
func (g *graph) entry(pkg *pkg) {
+ g.pkg = pkg
scopes := map[*types.Scope]*ir.Function{}
for _, fn := range pkg.SrcFuncs {
if fn.Object() != nil {
@@ -929,7 +929,7 @@ func (g *graph) entry(pkg *pkg) {
if obj == nil {
continue
}
- path := g.pkg.Fset.File(obj.Pos()).Name()
+ path := pkg.Fset.File(obj.Pos()).Name()
if strings.HasSuffix(path, "_test.go") {
if obj.Parent() != nil && obj.Parent().Parent() != nil && obj.Parent().Parent().Parent() == nil {
// object's scope is the package, whose
@@ -1151,7 +1151,7 @@ func (g *graph) entry(pkg *pkg) {
line int
}
ignores := map[ignoredKey]struct{}{}
- for _, dir := range g.pkg.Directives {
+ for _, dir := range pkg.Directives {
if dir.Command != "ignore" && dir.Command != "file-ignore" {
continue
}
@@ -1160,7 +1160,7 @@ func (g *graph) entry(pkg *pkg) {
}
for _, check := range strings.Split(dir.Arguments[0], ",") {
if check == "U1000" {
- pos := g.pkg.Fset.PositionFor(dir.Node.Pos(), false)
+ pos := pkg.Fset.PositionFor(dir.Node.Pos(), false)
var key ignoredKey
switch dir.Command {
case "ignore":
@@ -1185,7 +1185,7 @@ func (g *graph) entry(pkg *pkg) {
// all objects annotated with a //lint:ignore U1000 are considered used
for obj := range g.Nodes {
if obj, ok := obj.(types.Object); ok {
- pos := g.pkg.Fset.PositionFor(obj.Pos(), false)
+ pos := pkg.Fset.PositionFor(obj.Pos(), false)
key1 := ignoredKey{
pos.Filename,
pos.Line,
From 326afc66c7e7984b4ac78f6782b27b971397c904 Mon Sep 17 00:00:00 2001
From: Dominik Honnef
Date: Sat, 20 Jun 2020 06:23:05 +0200
Subject: [PATCH 074/111] unused: index seenFns by *ir.Function, not a string
representation
---
unused/unused.go | 9 ++++-----
1 file changed, 4 insertions(+), 5 deletions(-)
diff --git a/unused/unused.go b/unused/unused.go
index 13ff4245f..9392c1ffb 100644
--- a/unused/unused.go
+++ b/unused/unused.go
@@ -616,14 +616,14 @@ type graph struct {
// context
pkg *pkg
- seenFns map[string]struct{}
+ seenFns map[*ir.Function]struct{}
nodeCounter uint64
}
func newGraph() *graph {
g := &graph{
Nodes: map[interface{}]*node{},
- seenFns: map[string]struct{}{},
+ seenFns: map[*ir.Function]struct{}{},
seenTypes: map[types.Type]struct{}{},
TypeNodes: map[types.Type]*node{},
}
@@ -1252,11 +1252,10 @@ func (g *graph) function(fn *ir.Function) {
return
}
- name := fn.RelString(nil)
- if _, ok := g.seenFns[name]; ok {
+ if _, ok := g.seenFns[fn]; ok {
return
}
- g.seenFns[name] = struct{}{}
+ g.seenFns[fn] = struct{}{}
// (4.1) functions use all their arguments, return parameters and receivers
g.signature(fn.Signature, owningObject(fn))
From 37af0885a7190cbe4065c18e51fa1a22463bcde6 Mon Sep 17 00:00:00 2001
From: Dominik Honnef
Date: Sat, 20 Jun 2020 06:40:10 +0200
Subject: [PATCH 075/111] analysis/lint: include link to online documentation
in output of -explain
Closes gh-773
---
analysis/lint/lint.go | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/analysis/lint/lint.go b/analysis/lint/lint.go
index fc256d747..fe3dc6863 100644
--- a/analysis/lint/lint.go
+++ b/analysis/lint/lint.go
@@ -94,7 +94,7 @@ func InitializeAnalyzers(docs map[string]*Documentation, analyzers map[string]*a
if !ok {
panic(fmt.Sprintf("missing documentation for check %s", k))
}
- vc.Doc = doc.String()
+ vc.Doc = fmt.Sprintf("%s\nOnline documentation\n https://staticcheck.io/docs/checks#%s", doc.String(), k)
if vc.Flags.Usage == nil {
fs := flag.NewFlagSet("", flag.PanicOnError)
fs.Var(newVersionFlag(), "go", "Target Go version")
From a36e9300ad415d2b1946e7eeac3039e104ab61b3 Mon Sep 17 00:00:00 2001
From: Dominik Honnef
Date: Mon, 27 Jul 2020 01:27:38 +0200
Subject: [PATCH 076/111] lintcmd: add null formatter
The null formatter discards all output. It is useful for debugging.
---
lintcmd/cmd.go | 2 ++
lintcmd/format.go | 4 ++++
2 files changed, 6 insertions(+)
diff --git a/lintcmd/cmd.go b/lintcmd/cmd.go
index 6699fd9f7..d80a3ed8b 100644
--- a/lintcmd/cmd.go
+++ b/lintcmd/cmd.go
@@ -637,6 +637,8 @@ func ProcessFlagSet(cs []*analysis.Analyzer, fs *flag.FlagSet) {
f = &stylishFormatter{Diagnostics: os.Stdout, UI: os.Stderr}
case "json":
f = jsonFormatter{W: os.Stdout}
+ case "null":
+ f = nullFormatter{}
default:
fmt.Fprintf(os.Stderr, "unsupported output format %q\n", theFormatter)
exit(2)
diff --git a/lintcmd/format.go b/lintcmd/format.go
index 7f47e1d30..2d6afded0 100644
--- a/lintcmd/format.go
+++ b/lintcmd/format.go
@@ -63,6 +63,10 @@ func (o textFormatter) MentionCheckDocumentation(cmd string) {
fmt.Fprintf(o.UI, "\nRun '%s -explain ' or visit https://staticcheck.io/docs/checks for documentation on checks.\n", cmd)
}
+type nullFormatter struct{}
+
+func (nullFormatter) Format(problem) {}
+
type jsonFormatter struct {
W io.Writer
}
From 3b5b85696619f20fcdd0def0c457afbf096fe904 Mon Sep 17 00:00:00 2001
From: Dominik Honnef
Date: Mon, 27 Jul 2020 03:48:54 +0200
Subject: [PATCH 077/111] lintcmd: add -debug.trace flag
---
lintcmd/cmd.go | 13 +++++++++++++
1 file changed, 13 insertions(+)
diff --git a/lintcmd/cmd.go b/lintcmd/cmd.go
index d80a3ed8b..4bda6e62d 100644
--- a/lintcmd/cmd.go
+++ b/lintcmd/cmd.go
@@ -14,6 +14,7 @@ import (
"regexp"
"runtime"
"runtime/pprof"
+ "runtime/trace"
"sort"
"strconv"
"strings"
@@ -511,6 +512,7 @@ func FlagSet(name string) *flag.FlagSet {
flags.Bool("debug.version", false, "Print detailed version information about this program")
flags.Bool("debug.no-compile-errors", false, "Don't print compile errors")
flags.String("debug.measure-analyzers", "", "Write analysis measurements to `file`. `file` will be opened for appending if it already exists.")
+ flags.String("debug.trace", "", "Write trace to `file`")
checks := list{"inherit"}
fail := list{"all"}
@@ -550,6 +552,7 @@ func ProcessFlagSet(cs []*analysis.Analyzer, fs *flag.FlagSet) {
memProfile := fs.Lookup("debug.memprofile").Value.(flag.Getter).Get().(string)
debugVersion := fs.Lookup("debug.version").Value.(flag.Getter).Get().(bool)
debugNoCompile := fs.Lookup("debug.no-compile-errors").Value.(flag.Getter).Get().(bool)
+ traceOut := fs.Lookup("debug.trace").Value.(flag.Getter).Get().(string)
var measureAnalyzers func(analysis *analysis.Analyzer, pkg *loader.PackageSpec, d time.Duration)
if path := fs.Lookup("debug.measure-analyzers").Value.(flag.Getter).Get().(string); path != "" {
@@ -584,6 +587,9 @@ func ProcessFlagSet(cs []*analysis.Analyzer, fs *flag.FlagSet) {
runtime.GC()
pprof.WriteHeapProfile(f)
}
+ if traceOut != "" {
+ trace.Stop()
+ }
os.Exit(code)
}
if cpuProfile != "" {
@@ -593,6 +599,13 @@ func ProcessFlagSet(cs []*analysis.Analyzer, fs *flag.FlagSet) {
}
pprof.StartCPUProfile(f)
}
+ if traceOut != "" {
+ f, err := os.Create(traceOut)
+ if err != nil {
+ log.Fatal(err)
+ }
+ trace.Start(f)
+ }
if debugVersion {
version.Verbose()
From 4d9a72ff003a05c69f1301c1d7e414a885263af9 Mon Sep 17 00:00:00 2001
From: Dominik Honnef
Date: Thu, 30 Jul 2020 22:10:23 +0200
Subject: [PATCH 078/111] pattern: support matching field lists
Closes gh-806
---
pattern/match.go | 22 +++++++++++++++++++
.../LintUnnecessaryGuard.go | 9 ++++++++
.../LintUnnecessaryGuard.go.golden | 9 ++++++++
3 files changed, 40 insertions(+)
diff --git a/pattern/match.go b/pattern/match.go
index f0fda0619..88c0818fb 100644
--- a/pattern/match.go
+++ b/pattern/match.go
@@ -240,6 +240,28 @@ func match(m *Matcher, l, r interface{}) (interface{}, bool) {
}
}
+ {
+ ln, ok1 := l.([]*ast.Field)
+ rn, ok2 := r.([]*ast.Field)
+ if ok1 || ok2 {
+ if ok1 && !ok2 {
+ rn = []*ast.Field{r.(*ast.Field)}
+ } else if !ok1 && ok2 {
+ ln = []*ast.Field{l.(*ast.Field)}
+ }
+
+ if len(ln) != len(rn) {
+ return nil, false
+ }
+ for i, ll := range ln {
+ if _, ok := match(m, ll, rn[i]); !ok {
+ return nil, false
+ }
+ }
+ return r, true
+ }
+ }
+
panic(fmt.Sprintf("unsupported comparison: %T and %T", l, r))
}
diff --git a/simple/testdata/src/CheckUnnecessaryGuard/LintUnnecessaryGuard.go b/simple/testdata/src/CheckUnnecessaryGuard/LintUnnecessaryGuard.go
index 662b3be63..18530cd44 100644
--- a/simple/testdata/src/CheckUnnecessaryGuard/LintUnnecessaryGuard.go
+++ b/simple/testdata/src/CheckUnnecessaryGuard/LintUnnecessaryGuard.go
@@ -61,3 +61,12 @@ func fn() {
m2["k"] = 1
}
}
+
+// this used to cause a panic in the pattern package
+func fn2() {
+ var obj interface{}
+
+ if _, ok := obj.(map[string]interface{})["items"]; ok {
+ obj.(map[string]interface{})["version"] = 1
+ }
+}
diff --git a/simple/testdata/src/CheckUnnecessaryGuard/LintUnnecessaryGuard.go.golden b/simple/testdata/src/CheckUnnecessaryGuard/LintUnnecessaryGuard.go.golden
index 85446b042..1876b3c52 100644
--- a/simple/testdata/src/CheckUnnecessaryGuard/LintUnnecessaryGuard.go.golden
+++ b/simple/testdata/src/CheckUnnecessaryGuard/LintUnnecessaryGuard.go.golden
@@ -45,3 +45,12 @@ func fn() {
m2["k"] = 1
}
}
+
+// this used to cause a panic in the pattern package
+func fn2() {
+ var obj interface{}
+
+ if _, ok := obj.(map[string]interface{})["items"]; ok {
+ obj.(map[string]interface{})["version"] = 1
+ }
+}
From 2c5dee2fb55d663d8730567654c2ff5a0c285609 Mon Sep 17 00:00:00 2001
From: Dominik Honnef
Date: Thu, 30 Jul 2020 22:21:20 +0200
Subject: [PATCH 079/111] Add 2020.1.5 release notes
---
doc/2020.1.html | 8 ++++++++
doc/staticcheck.html | 2 +-
2 files changed, 9 insertions(+), 1 deletion(-)
diff --git a/doc/2020.1.html b/doc/2020.1.html
index b908002ad..3df791b92 100644
--- a/doc/2020.1.html
+++ b/doc/2020.1.html
@@ -11,6 +11,7 @@
If you use Go modules, you can simply run go get honnef.co/go/tools/cmd/staticcheck to obtain the latest released version.
If you're still using a GOPATH-based workflow, then the above command will instead fetch the master branch.
- It is suggested that you explicitly check out the latest release branch instead, which is currently 2020.1.4.
+ It is suggested that you explicitly check out the latest release branch instead, which is currently 2020.1.5.
One way of doing so would be as follows:
From ab2caa318cd8bff808b7fcdb9b98ac6049f51c0a Mon Sep 17 00:00:00 2001
From: Dominik Honnef
Date: Sat, 22 Aug 2020 19:53:22 +0200
Subject: [PATCH 080/111] Commit our new logo
The engineer, making sure your software is safe and sound.
Made by @egonelbre and based on the Go gopher by Renee French, which is licensed
under the Creative Commons Attribution 3.0.
---
images/logo.svg | 483 ++++++++++++++++++++++++++++++++++++++++++++++++
1 file changed, 483 insertions(+)
create mode 100644 images/logo.svg
diff --git a/images/logo.svg b/images/logo.svg
new file mode 100644
index 000000000..d92523702
--- /dev/null
+++ b/images/logo.svg
@@ -0,0 +1,483 @@
+
+
+
From 81508471876c7902b8ca236ae35f897b1777c65a Mon Sep 17 00:00:00 2001
From: Dominik Honnef
Date: Sat, 22 Aug 2020 21:10:40 +0200
Subject: [PATCH 081/111] Widen the sign in the logo
Unfortunately, the sign was a bit too small to be readable in all
contexts. On the flip side, his feet are more protected now.
---
images/logo.svg | 12 +++++++-----
1 file changed, 7 insertions(+), 5 deletions(-)
diff --git a/images/logo.svg b/images/logo.svg
index d92523702..f1755464d 100644
--- a/images/logo.svg
+++ b/images/logo.svg
@@ -60,7 +60,7 @@
-
+
@@ -72,7 +72,7 @@
-
+
@@ -89,7 +89,7 @@
-
+
@@ -202,9 +202,11 @@
-
+
+
+
-
+
From de0c0b8534e058390f43e48debd8ed32e5c9dec9 Mon Sep 17 00:00:00 2001
From: Valentin Deleplace
Date: Mon, 24 Aug 2020 16:38:54 +0200
Subject: [PATCH 082/111] Latest tag 2020.1.5
---
doc/staticcheck.html | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/doc/staticcheck.html b/doc/staticcheck.html
index c56a52142..230778a20 100644
--- a/doc/staticcheck.html
+++ b/doc/staticcheck.html
@@ -22,12 +22,12 @@
Installation
If you use Go modules, you can simply run go get honnef.co/go/tools/cmd/staticcheck to obtain the latest released version.
If you're still using a GOPATH-based workflow, then the above command will instead fetch the master branch.
- It is suggested that you explicitly check out the latest release branch instead, which is currently 2020.1.5.
+ It is suggested that you explicitly check out the latest release tag instead, which is currently 2020.1.5.
One way of doing so would be as follows:
cd $GOPATH/src/honnef.co/go/tools/cmd/staticcheck
-git checkout 2020.1.4
+git checkout 2020.1.5
go get
go install
+ This release makes the following fixes and improvements:
+
+
+
+
Staticcheck no longer panics when encountering files that have the following comment: // Code generated DO NOT EDIT.
+
{{ check "SA4016" }} no longer panics when checking bitwise operations that involve dot-imported identifiers.
+
Fixed the suggested fix offered by {{ check "S1004" }}.
+
Fixed a false positive involving byte arrays in {{ check "SA5009" }}.
+
Fixed a false positive involving named byte slice types in {{ check "SA5009" }}.
+
Added another heuristic to avoid flagging function names in error messages in {{ check "ST1005" }}.
+
{{ check "SA3000" }} will no longer flag missing calls to os.Exit in TestMain functions if targeting Go 1.15 or newer.
+
+
+
From 9742087923ba67d71443bf066c6823d088506d53 Mon Sep 17 00:00:00 2001
From: Dominik Honnef
Date: Mon, 12 Oct 2020 06:19:06 +0200
Subject: [PATCH 090/111] doc: the latest version is 2020.1.6
(cherry picked from commit d12f52a99afdb5e442dd0e318063f985459ddbf7)
---
doc/staticcheck.html | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/doc/staticcheck.html b/doc/staticcheck.html
index 230778a20..81f28f52b 100644
--- a/doc/staticcheck.html
+++ b/doc/staticcheck.html
@@ -22,12 +22,12 @@
Installation
If you use Go modules, you can simply run go get honnef.co/go/tools/cmd/staticcheck to obtain the latest released version.
If you're still using a GOPATH-based workflow, then the above command will instead fetch the master branch.
- It is suggested that you explicitly check out the latest release tag instead, which is currently 2020.1.5.
+ It is suggested that you explicitly check out the latest release tag instead, which is currently 2020.1.6.
One way of doing so would be as follows:
cd $GOPATH/src/honnef.co/go/tools/cmd/staticcheck
-git checkout 2020.1.5
+git checkout 2020.1.6
go get
go install
From 3ab5b7ad5849456ca4c682a3fea0586ff948130a Mon Sep 17 00:00:00 2001
From: Dominik Honnef
Date: Mon, 12 Oct 2020 08:40:16 +0200
Subject: [PATCH 091/111] unused: remove superfluous nil check
---
unused/unused.go | 10 ++++------
1 file changed, 4 insertions(+), 6 deletions(-)
diff --git a/unused/unused.go b/unused/unused.go
index 9392c1ffb..797697719 100644
--- a/unused/unused.go
+++ b/unused/unused.go
@@ -1105,12 +1105,10 @@ func (g *graph) entry(pkg *pkg) {
}
g.function(m)
case *ir.Type:
- if m.Object() != nil {
- g.see(m.Object())
- if m.Object().Exported() {
- // (1.1) packages use exported named types
- g.use(m.Object(), nil, edgeExportedType)
- }
+ g.see(m.Object())
+ if m.Object().Exported() {
+ // (1.1) packages use exported named types
+ g.use(m.Object(), nil, edgeExportedType)
}
g.typ(m.Type(), nil)
default:
From 542e8c572ba90537f28d6272df560105e64d5e9c Mon Sep 17 00:00:00 2001
From: Dominik Honnef
Date: Tue, 13 Oct 2020 01:02:42 +0200
Subject: [PATCH 092/111] staticcheck: run CheckTestMain tests with explicit Go
version
Updates gh-846
---
staticcheck/lint_test.go | 10 +++++-----
.../CheckTestMainExit-1.go | 0
.../CheckTestMainExit-2.go | 0
.../CheckTestMainExit-3.go | 0
.../CheckTestMainExit-4.go | 0
.../CheckTestMainExit-5.go | 0
6 files changed, 5 insertions(+), 5 deletions(-)
rename staticcheck/testdata/src/{CheckTestMainExit-1 => CheckTestMainExit-1_go14}/CheckTestMainExit-1.go (100%)
rename staticcheck/testdata/src/{CheckTestMainExit-2 => CheckTestMainExit-2_go14}/CheckTestMainExit-2.go (100%)
rename staticcheck/testdata/src/{CheckTestMainExit-3 => CheckTestMainExit-3_go14}/CheckTestMainExit-3.go (100%)
rename staticcheck/testdata/src/{CheckTestMainExit-4 => CheckTestMainExit-4_go14}/CheckTestMainExit-4.go (100%)
rename staticcheck/testdata/src/{CheckTestMainExit-5 => CheckTestMainExit-5_go14}/CheckTestMainExit-5.go (100%)
diff --git a/staticcheck/lint_test.go b/staticcheck/lint_test.go
index 122c2f56e..b351a9a14 100644
--- a/staticcheck/lint_test.go
+++ b/staticcheck/lint_test.go
@@ -41,11 +41,11 @@ func TestAll(t *testing.T) {
"SA2002": {{Dir: "CheckConcurrentTesting"}},
"SA2003": {{Dir: "CheckDeferLock"}},
"SA3000": {
- {Dir: "CheckTestMainExit-1"},
- {Dir: "CheckTestMainExit-2"},
- {Dir: "CheckTestMainExit-3"},
- {Dir: "CheckTestMainExit-4"},
- {Dir: "CheckTestMainExit-5"},
+ {Dir: "CheckTestMainExit-1_go14", Version: "1.4"},
+ {Dir: "CheckTestMainExit-2_go14", Version: "1.4"},
+ {Dir: "CheckTestMainExit-3_go14", Version: "1.4"},
+ {Dir: "CheckTestMainExit-4_go14", Version: "1.4"},
+ {Dir: "CheckTestMainExit-5_go14", Version: "1.4"},
{Dir: "CheckTestMainExit-1_go115", Version: "1.15"},
},
"SA3001": {{Dir: "CheckBenchmarkN"}},
diff --git a/staticcheck/testdata/src/CheckTestMainExit-1/CheckTestMainExit-1.go b/staticcheck/testdata/src/CheckTestMainExit-1_go14/CheckTestMainExit-1.go
similarity index 100%
rename from staticcheck/testdata/src/CheckTestMainExit-1/CheckTestMainExit-1.go
rename to staticcheck/testdata/src/CheckTestMainExit-1_go14/CheckTestMainExit-1.go
diff --git a/staticcheck/testdata/src/CheckTestMainExit-2/CheckTestMainExit-2.go b/staticcheck/testdata/src/CheckTestMainExit-2_go14/CheckTestMainExit-2.go
similarity index 100%
rename from staticcheck/testdata/src/CheckTestMainExit-2/CheckTestMainExit-2.go
rename to staticcheck/testdata/src/CheckTestMainExit-2_go14/CheckTestMainExit-2.go
diff --git a/staticcheck/testdata/src/CheckTestMainExit-3/CheckTestMainExit-3.go b/staticcheck/testdata/src/CheckTestMainExit-3_go14/CheckTestMainExit-3.go
similarity index 100%
rename from staticcheck/testdata/src/CheckTestMainExit-3/CheckTestMainExit-3.go
rename to staticcheck/testdata/src/CheckTestMainExit-3_go14/CheckTestMainExit-3.go
diff --git a/staticcheck/testdata/src/CheckTestMainExit-4/CheckTestMainExit-4.go b/staticcheck/testdata/src/CheckTestMainExit-4_go14/CheckTestMainExit-4.go
similarity index 100%
rename from staticcheck/testdata/src/CheckTestMainExit-4/CheckTestMainExit-4.go
rename to staticcheck/testdata/src/CheckTestMainExit-4_go14/CheckTestMainExit-4.go
diff --git a/staticcheck/testdata/src/CheckTestMainExit-5/CheckTestMainExit-5.go b/staticcheck/testdata/src/CheckTestMainExit-5_go14/CheckTestMainExit-5.go
similarity index 100%
rename from staticcheck/testdata/src/CheckTestMainExit-5/CheckTestMainExit-5.go
rename to staticcheck/testdata/src/CheckTestMainExit-5_go14/CheckTestMainExit-5.go
From 8ea76608cc772cf10723cda3c7b57dd104e4ef95 Mon Sep 17 00:00:00 2001
From: Dominik Honnef
Date: Tue, 13 Oct 2020 01:07:25 +0200
Subject: [PATCH 093/111] CI: target Go 1.14 as the lowest supported version,
also run with Go 1.15
---
.github/workflows/ci.yml | 6 +++---
1 file changed, 3 insertions(+), 3 deletions(-)
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 852081232..e93260999 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -7,7 +7,7 @@ jobs:
strategy:
matrix:
os: ["windows-latest", "ubuntu-latest", "macOS-latest"]
- go: ["1.13.x", "1.14.x"]
+ go: ["1.14.x", "1.15.x"]
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v1
@@ -24,7 +24,7 @@ jobs:
steps:
- uses: actions/setup-go@v1
with:
- go-version: "1.13.x"
+ go-version: "1.14.x"
- run: "GO111MODULE=on go get honnef.co/go/tools/cmd/staticcheck"
- uses: actions/checkout@v1
with:
@@ -36,4 +36,4 @@ jobs:
restore-keys: |
staticcheck-
- run: "go vet ./..."
- - run: "$(go env GOPATH)/bin/staticcheck -go 1.13 ./..."
+ - run: "$(go env GOPATH)/bin/staticcheck -go 1.14 ./..."
From b5d04209c44b041579df5cc420df89dac1dd2a00 Mon Sep 17 00:00:00 2001
From: Dominik Honnef
Date: Wed, 28 Oct 2020 03:38:19 +0100
Subject: [PATCH 094/111] cmd/go-module-query: delete
No useful tool materialized here. Lets remove the prototype.
---
cmd/go-module-query/main.go | 189 ---------------------------
cmd/go-module-query/staticcheck.conf | 2 -
2 files changed, 191 deletions(-)
delete mode 100644 cmd/go-module-query/main.go
delete mode 100644 cmd/go-module-query/staticcheck.conf
diff --git a/cmd/go-module-query/main.go b/cmd/go-module-query/main.go
deleted file mode 100644
index aa59ba09e..000000000
--- a/cmd/go-module-query/main.go
+++ /dev/null
@@ -1,189 +0,0 @@
-package main
-
-import (
- "encoding/json"
- "fmt"
- "io"
- "io/ioutil"
- "log"
- "net/http"
- "os"
- "path"
- "path/filepath"
- "strings"
- "sync"
- "sync/atomic"
- "time"
-
- "github.com/google/renameio"
- "github.com/rogpeppe/go-internal/modfile"
- "golang.org/x/mod/module"
-)
-
-/*
-Q: which versions of our module are being used
-A: find the latest version of every Go module, find the dependency on our module
-
-Q: what modules have stopped using our module
-A: find every module where a version [0..N) uses us, but version N doesn't.
-*/
-
-func Fetch(since time.Time) ([]module.Version, time.Time, error) {
- var out []module.Version
- for {
- out2, since2, err := fetch(since, out)
- if err != nil {
- return nil, since, err
- }
- if len(out) == len(out2) {
- break
- }
- out = out2
- since = since2
- }
- return out, since, nil
-}
-
-func fetch(since time.Time, out []module.Version) ([]module.Version, time.Time, error) {
- // +1µs because of bug in index.golang.org that returns results
- // >=since instead of >since
- ts := since.Add(1 * time.Microsecond)
- u := `https://index.golang.org/index?since=` + ts.Format(time.RFC3339Nano)
- resp, err := http.Get(u)
- if err != nil {
- return nil, since, err
- }
- defer resp.Body.Close()
- dec := json.NewDecoder(resp.Body)
-
- var entry struct {
- module.Version
- Timestamp time.Time
- }
- for {
- if err := dec.Decode(&entry); err != nil {
- if err == io.EOF {
- break
- }
- return out, since, err
- }
-
- out = append(out, entry.Version)
- since = entry.Timestamp
- }
-
- return out, since, nil
-}
-
-func main() {
- cache, err := os.UserCacheDir()
- if err != nil {
- log.Fatal(err)
- }
-
- var since time.Time
- b, err := ioutil.ReadFile(filepath.Join(cache, "go-module-query", "last"))
- if err == nil {
- t, err := time.Parse(time.RFC3339Nano, string(b))
- if err != nil {
- log.Fatal(err)
- }
- since = t
- log.Println("Resuming at", since)
- } else if !os.IsNotExist(err) {
- log.Fatal(err)
- }
-
- out, since, err := Fetch(since)
- if err != nil {
- log.Fatal(err)
- }
-
- sem := make(chan struct{}, 8)
- var wg sync.WaitGroup
- var errs uint64
- for _, v := range out {
- mpath, _ := module.EscapePath(v.Path)
- p := filepath.Join(cache, "go-module-query", mpath, "@v", v.Version+".mod")
- // XXX is this atomic?
- if err := os.MkdirAll(filepath.Join(cache, "go-module-query", mpath, "@v"), 0777); err != nil {
- log.Println(err)
- continue
- }
- if _, err := os.Stat(p); os.IsNotExist(err) {
- fmt.Println("Fetching", v)
- sem <- struct{}{}
- wg.Add(1)
- go func(p string, v module.Version) {
- defer wg.Done()
- defer func() { <-sem }()
- resp, err := http.Get("https://proxy.golang.org/" + path.Join(mpath, "@v", v.Version+".mod"))
- if err != nil {
- atomic.AddUint64(&errs, 1)
- log.Println(err)
- return
- }
- defer resp.Body.Close()
- // XXX handle response code
- pf, err := renameio.TempFile("", p)
- if err != nil {
- atomic.AddUint64(&errs, 1)
- log.Println(err)
- return
- }
- defer pf.Cleanup()
- if _, err := io.Copy(pf, resp.Body); err != nil {
- atomic.AddUint64(&errs, 1)
- log.Println(err)
- return
- }
- if err := pf.CloseAtomicallyReplace(); err != nil {
- atomic.AddUint64(&errs, 1)
- log.Println("Couldn't store go.mod:", err)
- }
- }(p, v)
- }
- }
-
- wg.Wait()
-
- if errs > 0 {
- log.Println("Couldn't download all go.mod, not storing timestamp")
- return
- }
-
- if err := renameio.WriteFile(filepath.Join(cache, "go-module-query", "last"), []byte(since.Format(time.RFC3339Nano)), 0666); err != nil {
- log.Println("Couldn't store timestamp:", err)
- }
-}
-
-func printGraph() {
- cache, err := os.UserCacheDir()
- if err != nil {
- log.Fatal(err)
- }
- filepath.Walk(filepath.Join(cache, "go-module-query"), func(path string, info os.FileInfo, err error) error {
- if err != nil {
- return nil
- }
- if strings.HasSuffix(path, ".mod") {
- name := filepath.Base(path)
- name = name[:len(name)-4]
- b, err := ioutil.ReadFile(path)
- if err != nil {
- log.Println(err)
- return nil
- }
- f, err := modfile.Parse(path, b, nil)
- if err != nil {
- log.Println(err)
- return nil
- }
- f.Module.Mod.Version = name
- for _, dep := range f.Require {
- fmt.Printf("%s@%s %s@%s\n", f.Module.Mod.Path, f.Module.Mod.Version, dep.Mod.Path, dep.Mod.Version)
- }
- }
- return nil
- })
-}
diff --git a/cmd/go-module-query/staticcheck.conf b/cmd/go-module-query/staticcheck.conf
deleted file mode 100644
index 16403869c..000000000
--- a/cmd/go-module-query/staticcheck.conf
+++ /dev/null
@@ -1,2 +0,0 @@
-# this package is WIP, unused code will occur.
-checks = ["inherit", "-U1000"]
From dc546986a51bad12b9de769571a7bc1b6b98da28 Mon Sep 17 00:00:00 2001
From: Dominik Honnef
Date: Wed, 28 Oct 2020 03:42:39 +0100
Subject: [PATCH 095/111] Delete temporary files that Emacs created
Closes gh-861
---
.../#CheckUntrappableSignal.go.golden# | 83 -------------------
.../.#CheckUntrappableSignal.go.golden | 1 -
2 files changed, 84 deletions(-)
delete mode 100644 staticcheck/testdata/src/CheckUntrappableSignal/#CheckUntrappableSignal.go.golden#
delete mode 120000 staticcheck/testdata/src/CheckUntrappableSignal/.#CheckUntrappableSignal.go.golden
diff --git a/staticcheck/testdata/src/CheckUntrappableSignal/#CheckUntrappableSignal.go.golden# b/staticcheck/testdata/src/CheckUntrappableSignal/#CheckUntrappableSignal.go.golden#
deleted file mode 100644
index aff90a10c..000000000
--- a/staticcheck/testdata/src/CheckUntrappableSignal/#CheckUntrappableSignal.go.golden#
+++ /dev/null
@@ -1,83 +0,0 @@
--- remove syscall.SIGKILL from list of arguments --
-package main
-
-import (
- "os"
- "os/signal"
- "syscall"
-)
-
-func fn() {
- c := make(chan os.Signal, 1)
- signal.Notify(c, os.Interrupt)
- signal.Ignore() // want `cannot be trapped`
- signal.Ignore(os.Kill) // want `cannot be trapped`
- signal.Notify(c, os.Kill) // want `cannot be trapped`
- signal.Reset(os.Kill) // want `cannot be trapped`
- signal.Ignore() // want `cannot be trapped`
- signal.Notify(c) // want `cannot be trapped`
- signal.Reset() // want `cannot be trapped`
-}
-
--- remove os.Kill from list of arguments --
-package main
-
-import (
- "os"
- "os/signal"
- "syscall"
-)
-
-func fn() {
- c := make(chan os.Signal, 1)
- signal.Notify(c, os.Interrupt)
- signal.Ignore(os.Signal(syscall.SIGKILL)) // want `cannot be trapped`
- signal.Ignore() // want `cannot be trapped`
- signal.Notify(c) // want `cannot be trapped`
- signal.Reset() // want `cannot be trapped`
- signal.Ignore(syscall.SIGKILL) // want `cannot be trapped`
- signal.Notify(c, syscall.SIGKILL) // want `cannot be trapped`
- signal.Reset(syscall.SIGKILL) // want `cannot be trapped`
-}
-
--- use syscall.SIGTERM instead of syscall.SIGKILL --
-package main
-
-import (
- "os"
- "os/signal"
- "syscall"
-)
-
-func fn() {
- c := make(chan os.Signal, 1)
- signal.Notify(c, os.Interrupt)
- signal.Ignore(syscall.SIGTERM) // want `cannot be trapped`
- signal.Ignore(os.Kill) // want `cannot be trapped`
- signal.Notify(c, os.Kill) // want `cannot be trapped`
- signal.Reset(os.Kill) // want `cannot be trapped`
- signal.Ignore(syscall.SIGTERM) // want `cannot be trapped`
- signal.Notify(c, syscall.SIGTERM) // want `cannot be trapped`
- signal.Reset(syscall.SIGTERM) // want `cannot be trapped`
-}
-
--- use syscall.SIGTERM instead of os.Kill --
-package main
-
-import (
- "os"
- "os/signal"
- "syscall"
-)
-
-func fn() {
- c := make(chan os.Signal, 1)
- signal.Notify(c, os.Interrupt)
- signal.Ignore(os.Signal(syscall.SIGKILL)) // want `cannot be trapped`
- signal.Ignore(syscall.SIGTERM) // want `cannot be trapped`
- signal.Notify(c, syscall.SIGTERM) // want `cannot be trapped`
- signal.Reset(syscall.SIGTERM) // want `cannot be trapped`
- signal.Ignore(syscall.SIGKILL) // want `cannot be trapped`
- signal.Notify(c, syscall.SIGKILL) // want `cannot be trapped`
- signal.Reset(syscall.SIGKILL) // want `cannot be trapped`
-}
diff --git a/staticcheck/testdata/src/CheckUntrappableSignal/.#CheckUntrappableSignal.go.golden b/staticcheck/testdata/src/CheckUntrappableSignal/.#CheckUntrappableSignal.go.golden
deleted file mode 120000
index ebff53d0d..000000000
--- a/staticcheck/testdata/src/CheckUntrappableSignal/.#CheckUntrappableSignal.go.golden
+++ /dev/null
@@ -1 +0,0 @@
-dominikh@nixos.1490:1586915592
\ No newline at end of file
From a6ed285e57c7d3a0fb2922170942f39c5cd6eadb Mon Sep 17 00:00:00 2001
From: Dominik Honnef
Date: Tue, 10 Nov 2020 00:22:51 +0100
Subject: [PATCH 096/111] go/ir: improve analysis of functions that don't
return
This change adds support for functions that abort control flow, but
use a mixture of exiting the process and panicking. In the past, we
would only detect functions that either always exit or always panic.
That is, we now correctly detect that the following function
affects control flow:
func fn(b bool) {
if b {
panic("")
} else {
syscall.Exit(1)
}
}
This change requires the introduction of a new intrinsic,
ir:noreturnWasPanic, which we use to encode the runtime behavior of
functions that may either panic or exit.
As part of these changes we happen to fix a bug: given two functions A
and B, where A panics unconditionally and B calls A, but recovers from
the panic, we would previously incorrectly mark B as always panicking
also, ignoring the fact that it recovered from the panic.
This change was triggered by os.Exit in Go 1.16 conditionally
panicking or exiting. Instead of special-casing os.Exit, I decided to
improve handling of such functions, so that user-written code may
benefit from the improved analysis.
Closes gh-872
---
go/ir/builder.go | 2 +-
go/ir/exits.go | 213 ++++++++++--------
go/ir/ssa.go | 38 ++--
internal/passes/buildir/buildir.go | 30 +--
.../src/CheckMaybeNil/CheckMaybeNil.go | 27 ++-
5 files changed, 185 insertions(+), 125 deletions(-)
diff --git a/go/ir/builder.go b/go/ir/builder.go
index 407a10f81..24ff69988 100644
--- a/go/ir/builder.go
+++ b/go/ir/builder.go
@@ -2261,7 +2261,7 @@ func (b *builder) buildFunction(fn *Function) {
// However, they aren't stubs, so buildExits ends up getting
// called on them, so that's where we handle those special
// cases.
- fn.WillExit = true
+ fn.NoReturn = AlwaysExits
}
if body == nil {
diff --git a/go/ir/exits.go b/go/ir/exits.go
index 10cda7bb6..2166f74d6 100644
--- a/go/ir/exits.go
+++ b/go/ir/exits.go
@@ -10,13 +10,13 @@ func (b *builder) buildExits(fn *Function) {
case "runtime":
switch obj.Name() {
case "exit":
- fn.WillExit = true
+ fn.NoReturn = AlwaysExits
return
case "throw":
- fn.WillExit = true
+ fn.NoReturn = AlwaysExits
return
case "Goexit":
- fn.WillUnwind = true
+ fn.NoReturn = AlwaysUnwinds
return
}
case "github.com/sirupsen/logrus":
@@ -31,7 +31,7 @@ func (b *builder) buildExits(fn *Function) {
// process, and that's what the vast majority of people
// will use it for. We'll happily accept some false
// negatives to avoid a lot of false positives.
- fn.WillExit = true
+ fn.NoReturn = AlwaysExits
return
case "(*github.com/sirupsen/logrus.Logger).Panic",
"(*github.com/sirupsen/logrus.Logger).Panicf",
@@ -40,7 +40,7 @@ func (b *builder) buildExits(fn *Function) {
// These methods will always panic, but that's not
// statically known from the code alone, because they
// take a detour through the generic Log methods.
- fn.WillUnwind = true
+ fn.NoReturn = AlwaysUnwinds
return
case "(*github.com/sirupsen/logrus.Entry).Panicf",
"(*github.com/sirupsen/logrus.Entry).Panicln":
@@ -48,12 +48,12 @@ func (b *builder) buildExits(fn *Function) {
// Entry.Panic has an explicit panic, but Panicf and
// Panicln do not, relying fully on the generic Log
// method.
- fn.WillUnwind = true
+ fn.NoReturn = AlwaysUnwinds
return
case "(*github.com/sirupsen/logrus.Logger).Log",
"(*github.com/sirupsen/logrus.Logger).Logf",
"(*github.com/sirupsen/logrus.Logger).Logln":
- // TODO(dh): we cannot handle these case. Whether they
+ // TODO(dh): we cannot handle these cases. Whether they
// exit or unwind depends on the level, which is set
// via the first argument. We don't currently support
// call-site-specific exit information.
@@ -61,8 +61,6 @@ func (b *builder) buildExits(fn *Function) {
}
}
- buildDomTree(fn)
-
isRecoverCall := func(instr Instruction) bool {
if instr, ok := instr.(*Call); ok {
if builtin, ok := instr.Call.Value.(*Builtin); ok {
@@ -74,66 +72,54 @@ func (b *builder) buildExits(fn *Function) {
return false
}
- // All panics branch to the exit block, which means that if every
- // possible path through the function panics, then all
- // predecessors of the exit block must panic.
- willPanic := true
- for _, pred := range fn.Exit.Preds {
- if _, ok := pred.Control().(*Panic); !ok {
- willPanic = false
- }
- }
- if willPanic {
- recovers := false
- recoverLoop:
- for _, u := range fn.Blocks {
- for _, instr := range u.Instrs {
- if instr, ok := instr.(*Defer); ok {
- call := instr.Call.StaticCallee()
- if call == nil {
- // not a static call, so we can't be sure the
- // deferred call isn't calling recover
- recovers = true
- break recoverLoop
- }
- if len(call.Blocks) == 0 {
- // external function, we don't know what's
- // happening inside it
- //
- // TODO(dh): this includes functions from
- // imported packages, due to how go/analysis
- // works. We could introduce another fact,
- // like we've done for exiting and unwinding,
- // but it doesn't seem worth it. Virtually all
- // uses of recover will be in closures.
- recovers = true
- break recoverLoop
- }
- for _, y := range call.Blocks {
- for _, instr2 := range y.Instrs {
- if isRecoverCall(instr2) {
- recovers = true
- break recoverLoop
- }
- }
- }
- }
- }
- }
- if !recovers {
- fn.WillUnwind = true
- return
- }
- }
-
- // TODO(dh): don't check that any specific call dominates the exit
- // block. instead, check that all calls combined cover every
- // possible path through the function.
+ both := NewBlockSet(len(fn.Blocks))
exits := NewBlockSet(len(fn.Blocks))
unwinds := NewBlockSet(len(fn.Blocks))
+ recovers := false
for _, u := range fn.Blocks {
for _, instr := range u.Instrs {
- if instr, ok := instr.(CallInstruction); ok {
+ instrSwitch:
+ switch instr := instr.(type) {
+ case *Defer:
+ if recovers {
+ // avoid doing extra work, we already know that this function calls recover
+ continue
+ }
+ call := instr.Call.StaticCallee()
+ if call == nil {
+ // not a static call, so we can't be sure the
+ // deferred call isn't calling recover
+ recovers = true
+ break
+ }
+ if call.Package() == fn.Package() {
+ b.buildFunction(call)
+ }
+ if len(call.Blocks) == 0 {
+ // external function, we don't know what's
+ // happening inside it
+ //
+ // TODO(dh): this includes functions from
+ // imported packages, due to how go/analysis
+ // works. We could introduce another fact,
+ // like we've done for exiting and unwinding.
+ recovers = true
+ break
+ }
+ for _, y := range call.Blocks {
+ for _, instr2 := range y.Instrs {
+ if isRecoverCall(instr2) {
+ recovers = true
+ break instrSwitch
+ }
+ }
+ }
+
+ case *Panic:
+ both.Add(u)
+ unwinds.Add(u)
+
+ case CallInstruction:
switch instr.(type) {
case *Defer, *Call:
default:
@@ -162,19 +148,15 @@ func (b *builder) buildExits(fn *Function) {
if call.Package() == fn.Package() {
b.buildFunction(call)
}
- dom := u.Dominates(fn.Exit)
- if call.WillExit {
- if dom {
- fn.WillExit = true
- return
- }
+ switch call.NoReturn {
+ case AlwaysExits:
+ both.Add(u)
exits.Add(u)
- } else if call.WillUnwind {
- if dom {
- fn.WillUnwind = true
- return
- }
+ case AlwaysUnwinds:
+ both.Add(u)
unwinds.Add(u)
+ case NeverReturns:
+ both.Add(u)
}
}
}
@@ -202,24 +184,38 @@ func (b *builder) buildExits(fn *Function) {
}
return false
}
-
- if exits.Num() > 0 {
- if !findPath(fn.Blocks[0], exits) {
- fn.WillExit = true
- return
+ findPathEntry := func(root *BasicBlock, bl *BlockSet) bool {
+ if bl.Num() == 0 {
+ return true
}
- }
- if unwinds.Num() > 0 {
seen.Clear()
- if !findPath(fn.Blocks[0], unwinds) {
- fn.WillUnwind = true
- return
+ return findPath(root, bl)
+ }
+
+ if !findPathEntry(fn.Blocks[0], exits) {
+ fn.NoReturn = AlwaysExits
+ } else if !recovers {
+ // Only consider unwinding and "never returns" if we don't
+ // call recover. If we do call recover, then panics don't
+ // bubble up the stack.
+
+ // TODO(dh): the position of the defer matters. If we
+ // unconditionally terminate before we defer a recover, then
+ // the recover is ineffective.
+
+ if !findPathEntry(fn.Blocks[0], unwinds) {
+ fn.NoReturn = AlwaysUnwinds
+ } else if !findPathEntry(fn.Blocks[0], both) {
+ fn.NoReturn = NeverReturns
}
}
}
func (b *builder) addUnreachables(fn *Function) {
+ var unreachable *BasicBlock
+
for _, bb := range fn.Blocks {
+ instrLoop:
for i, instr := range bb.Instrs {
if instr, ok := instr.(*Call); ok {
var call *Function
@@ -236,7 +232,8 @@ func (b *builder) addUnreachables(fn *Function) {
// make sure we have information on all functions in this package
b.buildFunction(call)
}
- if call.WillExit {
+ switch call.NoReturn {
+ case AlwaysExits:
// This call will cause the process to terminate.
// Remove remaining instructions in the block and
// replace any control flow with Unreachable.
@@ -248,8 +245,9 @@ func (b *builder) addUnreachables(fn *Function) {
bb.Instrs = bb.Instrs[:i+1]
bb.emit(new(Unreachable), instr.Source())
addEdge(bb, fn.Exit)
- break
- } else if call.WillUnwind {
+ break instrLoop
+
+ case AlwaysUnwinds:
// This call will cause the goroutine to terminate
// and defers to run (i.e. a panic or
// runtime.Goexit). Remove remaining instructions
@@ -263,7 +261,42 @@ func (b *builder) addUnreachables(fn *Function) {
bb.Instrs = bb.Instrs[:i+1]
bb.emit(new(Jump), instr.Source())
addEdge(bb, fn.Exit)
- break
+ break instrLoop
+
+ case NeverReturns:
+ // This call will either cause the goroutine to
+ // terminate, or the process to terminate. Remove
+ // remaining instructions in the block and replace
+ // any control flow with a conditional jump to
+ // either the exit block, or Unreachable.
+ for _, succ := range bb.Succs {
+ succ.removePred(bb)
+ }
+ bb.Succs = bb.Succs[:0]
+
+ bb.Instrs = bb.Instrs[:i+1]
+ var c Call
+ c.Call.Value = &Builtin{
+ name: "ir:noreturnWasPanic",
+ sig: types.NewSignature(nil,
+ types.NewTuple(),
+ types.NewTuple(anonVar(types.Typ[types.Bool])),
+ false,
+ ),
+ }
+ c.setType(types.Typ[types.Bool])
+
+ if unreachable == nil {
+ unreachable = fn.newBasicBlock("unreachable")
+ unreachable.emit(&Unreachable{}, nil)
+ addEdge(unreachable, fn.Exit)
+ }
+
+ bb.emit(&c, instr.Source())
+ bb.emit(&If{Cond: &c}, instr.Source())
+ addEdge(bb, fn.Exit)
+ addEdge(bb, unreachable)
+ break instrLoop
}
}
}
diff --git a/go/ir/ssa.go b/go/ir/ssa.go
index fc8e84114..2fea3e587 100644
--- a/go/ir/ssa.go
+++ b/go/ir/ssa.go
@@ -349,23 +349,31 @@ type Function struct {
method *types.Selection // info about provenance of synthetic methods
Signature *types.Signature
- Synthetic Synthetic
- parent *Function // enclosing function if anon; nil if global
- Pkg *Package // enclosing package; nil for shared funcs (wrappers and error.Error)
- Prog *Program // enclosing program
- Params []*Parameter // function parameters; for methods, includes receiver
- FreeVars []*FreeVar // free variables whose values must be supplied by closure
- Locals []*Alloc // local variables of this function
- Blocks []*BasicBlock // basic blocks of the function; nil => external
- Exit *BasicBlock // The function's exit block
- AnonFuncs []*Function // anonymous functions directly beneath this one
- referrers []Instruction // referring instructions (iff Parent() != nil)
- WillExit bool // Calling this function will always terminate the process
- WillUnwind bool // Calling this function will always unwind (it will call runtime.Goexit or panic)
+ Synthetic Synthetic
+ parent *Function // enclosing function if anon; nil if global
+ Pkg *Package // enclosing package; nil for shared funcs (wrappers and error.Error)
+ Prog *Program // enclosing program
+ Params []*Parameter // function parameters; for methods, includes receiver
+ FreeVars []*FreeVar // free variables whose values must be supplied by closure
+ Locals []*Alloc // local variables of this function
+ Blocks []*BasicBlock // basic blocks of the function; nil => external
+ Exit *BasicBlock // The function's exit block
+ AnonFuncs []*Function // anonymous functions directly beneath this one
+ referrers []Instruction // referring instructions (iff Parent() != nil)
+ NoReturn NoReturn // Calling this function will always terminate control flow.
*functionBody
}
+type NoReturn uint8
+
+const (
+ Returns NoReturn = iota
+ AlwaysExits
+ AlwaysUnwinds
+ NeverReturns
+)
+
type functionBody struct {
// The following fields are set transiently during building,
// then cleared.
@@ -518,6 +526,10 @@ type Global struct {
// // (For use in indirection wrappers.)
// func ir:wrapnilchk(ptr *T, recvType, methodName string) *T
//
+// // noreturnWasPanic returns true if the previously called
+// // function panicked, false if it exited the process.
+// func ir:noreturnWasPanic() bool
+//
// Object() returns a *types.Builtin for built-ins defined by the spec,
// nil for others.
//
diff --git a/internal/passes/buildir/buildir.go b/internal/passes/buildir/buildir.go
index 645e216a9..51dfaef53 100644
--- a/internal/passes/buildir/buildir.go
+++ b/internal/passes/buildir/buildir.go
@@ -20,21 +20,18 @@ import (
"golang.org/x/tools/go/analysis"
)
-type willExit struct{}
-type willUnwind struct{}
-
-func (*willExit) AFact() {}
-func (*willUnwind) AFact() {}
+type noReturn struct {
+ Kind ir.NoReturn
+}
-func (*willExit) String() string { return "will exit" }
-func (*willUnwind) String() string { return "will unwind" }
+func (*noReturn) AFact() {}
var Analyzer = &analysis.Analyzer{
Name: "buildir",
Doc: "build IR for later passes",
Run: run,
ResultType: reflect.TypeOf(new(IR)),
- FactTypes: []analysis.Fact{new(willExit), new(willUnwind)},
+ FactTypes: []analysis.Fact{new(noReturn)},
}
// IR provides intermediate representation for all the
@@ -72,13 +69,9 @@ func run(pass *analysis.Pass) (interface{}, error) {
irpkg := prog.CreatePackage(p, nil, nil, true)
for _, fn := range irpkg.Functions {
if ast.IsExported(fn.Name()) {
- var exit willExit
- var unwind willUnwind
- if pass.ImportObjectFact(fn.Object(), &exit) {
- fn.WillExit = true
- }
- if pass.ImportObjectFact(fn.Object(), &unwind) {
- fn.WillUnwind = true
+ var noRet noReturn
+ if pass.ImportObjectFact(fn.Object(), &noRet) {
+ fn.NoReturn = noRet.Kind
}
}
}
@@ -105,11 +98,8 @@ func run(pass *analysis.Pass) (interface{}, error) {
}
for _, fn := range irpkg.Functions {
addAnons(fn)
- if fn.WillExit {
- pass.ExportObjectFact(fn.Object(), new(willExit))
- }
- if fn.WillUnwind {
- pass.ExportObjectFact(fn.Object(), new(willUnwind))
+ if fn.NoReturn > 0 {
+ pass.ExportObjectFact(fn.Object(), &noReturn{fn.NoReturn})
}
}
diff --git a/staticcheck/testdata/src/CheckMaybeNil/CheckMaybeNil.go b/staticcheck/testdata/src/CheckMaybeNil/CheckMaybeNil.go
index c44320874..b5fc1cb16 100644
--- a/staticcheck/testdata/src/CheckMaybeNil/CheckMaybeNil.go
+++ b/staticcheck/testdata/src/CheckMaybeNil/CheckMaybeNil.go
@@ -1,6 +1,9 @@
package pkg
-import "os"
+import (
+ "os"
+ "syscall"
+)
func fn1(x *int) {
_ = *x // want `possible nil pointer dereference`
@@ -106,3 +109,25 @@ func fn11(x *int) {
}
_ = *x // want `possible nil pointer dereference`
}
+
+func doPanic() { panic("") }
+func doExit() { syscall.Exit(1) }
+
+func fn12(arg bool) {
+ if arg {
+ doPanic()
+ } else {
+ doExit()
+ }
+}
+
+func fn13(arg bool) {
+ fn12(arg)
+}
+
+func fn14(x *int) {
+ if x == nil {
+ fn13(true)
+ }
+ _ = *x
+}
From 4342b0b981d840719f78d42faf12755176bc1343 Mon Sep 17 00:00:00 2001
From: Dominik Honnef
Date: Sun, 15 Nov 2020 09:11:26 +0100
Subject: [PATCH 097/111] staticcheck: don't use AST walking to find node
corresponding to IR call instruction
Before c14c261fd1fd7dec2e23b44bbf8c7b49cbd4e239, the only way to map
from an IR instruction to an AST node was by using position
information and walking the AST, usually by using
astutil.PathEnclosingInterval.
Back then, the position information stored with instructions was
designed to map uniquely to AST nodes. For example, in
'fn1().fn2().fn3()`, the call of fn3() would have its position on the
dot immediately before fn3. Since the change, however, The start
position will be at the beginning of the sequence of expressions.
Using that position to find the correct call is wrong and will yield
the wrong node. For defer and go statements, we might even
accidentally look at the surrounding code.
In the best case, this would lead to incorrect positions in our
output. In the worst case, it would lead to out of bounds crashes due
to a mismatch in argument counts.
Note that many IR instructions may map to the same AST node, for
example because of method calls of embedded methods. This, however,
shouldn't be a problem here, as the AST node should always correspond
to the one we're interested in.
Closes gh-873
---
staticcheck/lint.go | 21 +++++++++++++------
.../testdata/src/CheckRegexps/CheckRegexps.go | 4 ++++
.../CheckSyncPoolValue/CheckSyncPoolValue.go | 14 +++++++++++++
.../checkStdlibUsageRegexpFindAll.go | 4 ++++
4 files changed, 37 insertions(+), 6 deletions(-)
diff --git a/staticcheck/lint.go b/staticcheck/lint.go
index e190051ba..332ba6e4c 100644
--- a/staticcheck/lint.go
+++ b/staticcheck/lint.go
@@ -3014,14 +3014,23 @@ func checkCalls(pass *analysis.Pass, rules map[string]CallCheck) (interface{}, e
Parent: site.Parent(),
}
r(call)
- path, _ := goastutil.PathEnclosingInterval(code.File(pass, site), site.Pos(), site.Pos())
+
var astcall *ast.CallExpr
- for _, el := range path {
- if expr, ok := el.(*ast.CallExpr); ok {
- astcall = expr
- break
- }
+ switch source := site.Source().(type) {
+ case *ast.CallExpr:
+ astcall = source
+ case *ast.DeferStmt:
+ astcall = source.Call
+ case *ast.GoStmt:
+ astcall = source.Call
+ case nil:
+ // TODO(dh): I am not sure this can actually happen. If it
+ // can't, we should remove this case, and also stop
+ // checking for astcall == nil in the code that follows.
+ default:
+ panic(fmt.Sprintf("unhandled case %T", source))
}
+
for idx, arg := range call.Args {
for _, e := range arg.invalids {
if astcall != nil {
diff --git a/staticcheck/testdata/src/CheckRegexps/CheckRegexps.go b/staticcheck/testdata/src/CheckRegexps/CheckRegexps.go
index e5338d0c3..4719c6301 100644
--- a/staticcheck/testdata/src/CheckRegexps/CheckRegexps.go
+++ b/staticcheck/testdata/src/CheckRegexps/CheckRegexps.go
@@ -40,3 +40,7 @@ func (T) init() {}
// this will become a synthetic init function, that we don't want to
// ignore
var _ = regexp.MustCompile("(") // want `error parsing regexp`
+
+func fn2() {
+ regexp.MustCompile("foo(").FindAll(nil, 0) // want `error parsing regexp`
+}
diff --git a/staticcheck/testdata/src/CheckSyncPoolValue/CheckSyncPoolValue.go b/staticcheck/testdata/src/CheckSyncPoolValue/CheckSyncPoolValue.go
index 44d8d20b7..afff47379 100644
--- a/staticcheck/testdata/src/CheckSyncPoolValue/CheckSyncPoolValue.go
+++ b/staticcheck/testdata/src/CheckSyncPoolValue/CheckSyncPoolValue.go
@@ -36,3 +36,17 @@ func fn() {
var basic int
p.Put(basic) // want `argument should be pointer-like`
}
+
+func fn2() {
+ // https://github.com/dominikh/go-tools/issues/873
+ var pool sync.Pool
+ func() {
+ defer pool.Put([]byte{}) // want `argument should be pointer-like`
+ }()
+}
+
+func fn3() {
+ var pool sync.Pool
+ defer pool.Put([]byte{}) // want `argument should be pointer-like`
+ go pool.Put([]byte{}) // want `argument should be pointer-like`
+}
diff --git a/staticcheck/testdata/src/checkStdlibUsageRegexpFindAll/checkStdlibUsageRegexpFindAll.go b/staticcheck/testdata/src/checkStdlibUsageRegexpFindAll/checkStdlibUsageRegexpFindAll.go
index 86056e577..cfb7fdac3 100644
--- a/staticcheck/testdata/src/checkStdlibUsageRegexpFindAll/checkStdlibUsageRegexpFindAll.go
+++ b/staticcheck/testdata/src/checkStdlibUsageRegexpFindAll/checkStdlibUsageRegexpFindAll.go
@@ -6,3 +6,7 @@ func fn() {
var r *regexp.Regexp
_ = r.FindAll(nil, 0) //want `calling a FindAll method with n == 0 will return no results`
}
+
+func fn2() {
+ regexp.MustCompile("foo(").FindAll(nil, 0) // want `calling a FindAll`
+}
From b453a5fa9d66c93694354593db1bed0f87388499 Mon Sep 17 00:00:00 2001
From: pkositsyn
Date: Sat, 14 Nov 2020 01:48:42 +0300
Subject: [PATCH 098/111] simple: fix negate for GEQ from LEQ to LSS
Closes: gh-875 [via git-merge-pr]
Closes gh-874
---
simple/lint.go | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/simple/lint.go b/simple/lint.go
index 8bff7b08d..ea7248675 100644
--- a/simple/lint.go
+++ b/simple/lint.go
@@ -470,7 +470,7 @@ func negate(expr ast.Expr) ast.Expr {
case token.LEQ:
out.Op = token.GTR
case token.GEQ:
- out.Op = token.LEQ
+ out.Op = token.LSS
}
return &out
case *ast.Ident, *ast.CallExpr, *ast.IndexExpr:
From d0bdcc714807a26596953fa2dbf7cab4932b508f Mon Sep 17 00:00:00 2001
From: Dominik Honnef
Date: Sun, 15 Nov 2020 09:28:59 +0100
Subject: [PATCH 099/111] simple: add test case for correct negation of >=
---
simple/testdata/src/if-return/if-return.go | 7 +++++++
1 file changed, 7 insertions(+)
diff --git a/simple/testdata/src/if-return/if-return.go b/simple/testdata/src/if-return/if-return.go
index f8cf98f8d..6502c3bca 100644
--- a/simple/testdata/src/if-return/if-return.go
+++ b/simple/testdata/src/if-return/if-return.go
@@ -93,3 +93,10 @@ func fn13(a, b int) bool {
}
return true
}
+
+func fn14(a, b int) bool {
+ if a >= b { // want `should use 'return a < b' instead of 'if a >= b`
+ return false
+ }
+ return true
+}
From 6054ba0e94b60a7700a3eeb9e041319e172ceb9f Mon Sep 17 00:00:00 2001
From: Sourya Vatsyayan
Date: Tue, 3 Nov 2020 02:22:25 +0530
Subject: [PATCH 100/111] staticcheck: fix incorrect range on suggested edit
Signed-off-by: sourya
Closes: gh-867 [via git-merge-pr]
Closes gh-866
---
staticcheck/lint.go | 3 +++
.../CheckMissingEnumTypesInDeclaration.go | 2 +-
.../CheckMissingEnumTypesInDeclaration.go.golden | 2 +-
3 files changed, 5 insertions(+), 2 deletions(-)
diff --git a/staticcheck/lint.go b/staticcheck/lint.go
index 332ba6e4c..394f203c4 100644
--- a/staticcheck/lint.go
+++ b/staticcheck/lint.go
@@ -3381,6 +3381,9 @@ func CheckMissingEnumTypesInDeclaration(pass *analysis.Pass) (interface{}, error
for _, spec := range group[1:] {
nspec := *spec.(*ast.ValueSpec)
nspec.Type = typ
+ // The position of `spec` node excludes comments (if any).
+ // However, on generating the source back from the node, the comments are included. Setting `Comment` to nil ensures deduplication of comments.
+ nspec.Comment = nil
edits = append(edits, edit.ReplaceWithNode(pass.Fset, spec, &nspec))
}
report.Report(pass, group[0], "only the first constant in this group has an explicit type", report.Fixes(edit.Fix("add type to all constants in group", edits...)))
diff --git a/staticcheck/testdata/src/CheckMissingEnumTypesInDeclaration/CheckMissingEnumTypesInDeclaration.go b/staticcheck/testdata/src/CheckMissingEnumTypesInDeclaration/CheckMissingEnumTypesInDeclaration.go
index bc6f5f0ed..a393be80a 100644
--- a/staticcheck/testdata/src/CheckMissingEnumTypesInDeclaration/CheckMissingEnumTypesInDeclaration.go
+++ b/staticcheck/testdata/src/CheckMissingEnumTypesInDeclaration/CheckMissingEnumTypesInDeclaration.go
@@ -11,7 +11,7 @@ const (
const (
c6 int = 1 // want `only the first constant in this group has an explicit type`
- c7 = 2
+ c7 = 2 // comment for testing https://github.com/dominikh/go-tools/issues/866
c8 = 3
)
diff --git a/staticcheck/testdata/src/CheckMissingEnumTypesInDeclaration/CheckMissingEnumTypesInDeclaration.go.golden b/staticcheck/testdata/src/CheckMissingEnumTypesInDeclaration/CheckMissingEnumTypesInDeclaration.go.golden
index 371ccd2e5..3ed508f3b 100644
--- a/staticcheck/testdata/src/CheckMissingEnumTypesInDeclaration/CheckMissingEnumTypesInDeclaration.go.golden
+++ b/staticcheck/testdata/src/CheckMissingEnumTypesInDeclaration/CheckMissingEnumTypesInDeclaration.go.golden
@@ -11,7 +11,7 @@ const (
const (
c6 int = 1 // want `only the first constant in this group has an explicit type`
- c7 int = 2
+ c7 int = 2 // comment for testing https://github.com/dominikh/go-tools/issues/866
c8 int = 3
)
From e068dc2034e7ef39930d3bdaae516754596e5050 Mon Sep 17 00:00:00 2001
From: Ainar Garipov
Date: Wed, 21 Oct 2020 22:52:37 +0300
Subject: [PATCH 101/111] stylecheck: don't flag identifiers with no letters in
ST1003
Closes gh-858
Closes: gh-859 [via git-merge-pr]
---
stylecheck/names.go | 6 +++++-
stylecheck/testdata/src/CheckNames/CheckNames.go | 2 ++
2 files changed, 7 insertions(+), 1 deletion(-)
diff --git a/stylecheck/names.go b/stylecheck/names.go
index 594bdf1f4..f038d0632 100644
--- a/stylecheck/names.go
+++ b/stylecheck/names.go
@@ -31,12 +31,16 @@ func CheckNames(pass *analysis.Pass) (interface{}, error) {
// licensed under the BSD 3-clause license.
allCaps := func(s string) bool {
+ hasUppercaseLetters := false
for _, r := range s {
+ if !hasUppercaseLetters && r >= 'A' && r <= 'Z' {
+ hasUppercaseLetters = true
+ }
if !((r >= 'A' && r <= 'Z') || (r >= '0' && r <= '9') || r == '_') {
return false
}
}
- return true
+ return hasUppercaseLetters
}
check := func(id *ast.Ident, thing string, initialisms map[string]bool) {
diff --git a/stylecheck/testdata/src/CheckNames/CheckNames.go b/stylecheck/testdata/src/CheckNames/CheckNames.go
index a61b5d4a0..d4ffb591b 100644
--- a/stylecheck/testdata/src/CheckNames/CheckNames.go
+++ b/stylecheck/testdata/src/CheckNames/CheckNames.go
@@ -17,6 +17,8 @@ var Foo_BAR int // want `var Foo_BAR should be FooBAR`
var foo_bar int // want `foo_bar should be fooBar`
var kFoobar int // not a check we inherited from golint. more false positives than true ones.
+var _1000 int // issue 858
+
func fn(x []int) {
var (
a_b = 1 // want `var a_b should be aB`
From 56b7c78ddcd8b1d79be0507087b2b4ea5c74169f Mon Sep 17 00:00:00 2001
From: Dominik Honnef
Date: Tue, 24 Nov 2020 08:33:30 +0100
Subject: [PATCH 102/111] lintcmd: improve message when patterns matched no
packages
Updates gh-722
---
lintcmd/cmd.go | 4 +++-
1 file changed, 3 insertions(+), 1 deletion(-)
diff --git a/lintcmd/cmd.go b/lintcmd/cmd.go
index 4bda6e62d..603372b91 100644
--- a/lintcmd/cmd.go
+++ b/lintcmd/cmd.go
@@ -292,7 +292,9 @@ func (l *linter) Lint(cfg *packages.Config, patterns []string) (problems []probl
if len(results) == 0 && err == nil {
// TODO(dh): emulate Go's behavior more closely once we have
// access to go list's Match field.
- fmt.Fprintf(os.Stderr, "warning: %q matched no packages\n", patterns)
+ for _, pattern := range patterns {
+ fmt.Fprintf(os.Stderr, "warning: %q matched no packages\n", pattern)
+ }
}
analyzerNames := make([]string, len(l.Checkers))
From bde4814064e47542ef9fe3582a9e5914930ff103 Mon Sep 17 00:00:00 2001
From: Dominik Honnef
Date: Wed, 25 Nov 2020 03:20:28 +0100
Subject: [PATCH 103/111] SA9006: add missing "Since" field to documentation
---
staticcheck/doc.go | 1 +
1 file changed, 1 insertion(+)
diff --git a/staticcheck/doc.go b/staticcheck/doc.go
index aedd28348..0bc14eb39 100644
--- a/staticcheck/doc.go
+++ b/staticcheck/doc.go
@@ -970,5 +970,6 @@ positives in somewhat exotic but valid bit twiddling tricks:
v = v << 32
return i-v
}`,
+ Since: "Unreleased",
},
}
From 0767310fbbbb033e38429f2061261c6b7dbe7552 Mon Sep 17 00:00:00 2001
From: Dominik Honnef
Date: Tue, 8 Dec 2020 02:03:37 +0100
Subject: [PATCH 104/111] go/ir: rebuild fake exits after optimizing blocks
Block optimization can remove blocks, which means we need to recompute
fake exits to be able to build a post-dominator tree.
Before this change, building the IR form of the following program
caused a panic:
package pkg
import (
"syscall"
)
func fn() {
if true {
syscall.Exit(1)
} else {
_ = 0
}
var err error
if err != nil {
return
}
for {
}
}
Closes gh-882
---
go/ir/func.go | 1 +
1 file changed, 1 insertion(+)
diff --git a/go/ir/func.go b/go/ir/func.go
index 14ec132bc..dca883d4b 100644
--- a/go/ir/func.go
+++ b/go/ir/func.go
@@ -557,6 +557,7 @@ func (f *Function) finishBody() {
f.Locals = f.Locals[:j]
optimizeBlocks(f)
+ buildFakeExits(f)
buildReferrers(f)
buildDomTree(f)
buildPostDomTree(f)
From fd953f579ecb81254b82a79951702a5e211dd314 Mon Sep 17 00:00:00 2001
From: Dominik Honnef
Date: Sun, 13 Dec 2020 17:17:42 +0100
Subject: [PATCH 105/111] lintcmd/version: add support for having two versions
We are switching to using two versioning schemes: our original one and
a proper Semantic Versioning compatible one. See #777 for the
motivation.
---
lintcmd/version/version.go | 17 +++++++++--------
1 file changed, 9 insertions(+), 8 deletions(-)
diff --git a/lintcmd/version/version.go b/lintcmd/version/version.go
index a12f70fb4..cb12e2121 100644
--- a/lintcmd/version/version.go
+++ b/lintcmd/version/version.go
@@ -8,29 +8,30 @@ import (
)
const Version = "devel"
+const MachineVersion = "devel"
// version returns a version descriptor and reports whether the
// version is a known release.
-func version() (string, bool) {
+func version() (human, machine string, known bool) {
if Version != "devel" {
- return Version, true
+ return Version, MachineVersion, true
}
v, ok := buildInfoVersion()
if ok {
- return v, false
+ return v, "", false
}
- return "devel", false
+ return "devel", "", false
}
func Print() {
- v, release := version()
+ human, machine, release := version()
if release {
- fmt.Printf("%s %s\n", filepath.Base(os.Args[0]), v)
- } else if v == "devel" {
+ fmt.Printf("%s %s (%s)\n", filepath.Base(os.Args[0]), human, machine)
+ } else if human == "devel" {
fmt.Printf("%s (no version)\n", filepath.Base(os.Args[0]))
} else {
- fmt.Printf("%s (devel, %s)\n", filepath.Base(os.Args[0]), v)
+ fmt.Printf("%s (devel, %s)\n", filepath.Base(os.Args[0]), human)
}
}
From 93fa3e0cacb5fb41d78dcf4c73ddcb93c465b873 Mon Sep 17 00:00:00 2001
From: Dominik Honnef
Date: Mon, 14 Dec 2020 08:11:52 +0100
Subject: [PATCH 106/111] staticcheck: update Since field of checks that are
new in 2020.2
---
staticcheck/doc.go | 6 +++---
1 file changed, 3 insertions(+), 3 deletions(-)
diff --git a/staticcheck/doc.go b/staticcheck/doc.go
index 0bc14eb39..582b0ae18 100644
--- a/staticcheck/doc.go
+++ b/staticcheck/doc.go
@@ -557,7 +557,7 @@ Reflection (https://golang.org/doc/articles/laws_of_reflection.html).
This text has been copied from
https://golang.org/doc/faq#nil_error, licensed under the Creative
Commons Attribution 3.0 License.`,
- Since: "Unreleased",
+ Since: "2020.2",
},
"SA5000": {
@@ -729,7 +729,7 @@ popular package.`,
Often, these functions treat elements in a slice as pairs.
For example, strings.NewReplacer takes pairs of old and new strings,
and calling it with an odd number of elements would be an error.`,
- Since: "Unreleased",
+ Since: "2020.2",
},
"SA6000": {
@@ -970,6 +970,6 @@ positives in somewhat exotic but valid bit twiddling tricks:
v = v << 32
return i-v
}`,
- Since: "Unreleased",
+ Since: "2020.2",
},
}
From 900aaa96e309e18854b69137fcdfa848528f6a64 Mon Sep 17 00:00:00 2001
From: Dominik Honnef
Date: Mon, 14 Dec 2020 08:55:31 +0100
Subject: [PATCH 107/111] knowledge: update list of deprecated objects for Go
1.16
---
knowledge/deprecated.go | 17 ++++++++++++++++-
1 file changed, 16 insertions(+), 1 deletion(-)
diff --git a/knowledge/deprecated.go b/knowledge/deprecated.go
index ffed387c9..365abaf88 100644
--- a/knowledge/deprecated.go
+++ b/knowledge/deprecated.go
@@ -69,6 +69,13 @@ var StdlibDeprecations = map[string]Deprecation{
"net/http.CloseNotifier": {11, 7},
"net/http.ProtocolError": {8, 8},
"(crypto/x509.CertificateRequest).Attributes": {5, 3},
+
+ // These functions have no direct alternative, but they are insecure and should no longer be used.
+ "crypto/x509.IsEncryptedPEMBlock": {16, 0},
+ "crypto/x509.DecryptPEMBlock": {16, 0},
+ "crypto/x509.EncryptPEMBlock": {16, 0},
+ "crypto/dsa": {16, 0},
+
// This function has no alternative, but also no purpose.
"(*crypto/rc4.Cipher).Reset": {12, 0},
"(net/http/httptest.ResponseRecorder).HeaderMap": {11, 7},
@@ -79,7 +86,12 @@ var StdlibDeprecations = map[string]Deprecation{
"crypto/tls.VersionSSL30": {13, 0},
"(crypto/tls.Config).NameToCertificate": {14, 14},
"(*crypto/tls.Config).BuildNameToCertificate": {14, 14},
- "image/jpeg.Reader": {4, 0},
+ "(crypto/tls.Config).SessionTicketKey": {16, 5},
+ // No alternative, no use
+ "(crypto/tls.ConnectionState).NegotiatedProtocolIsMutual": {16, 0},
+ // No alternative, but insecure
+ "(crypto/tls.ConnectionState).TLSUnique": {16, 0},
+ "image/jpeg.Reader": {4, 0},
// All of these have been deprecated in favour of external libraries
"syscall.AttachLsf": {7, 0},
@@ -116,4 +128,7 @@ var StdlibDeprecations = map[string]Deprecation{
"syscall.InterfaceAnnounceMessage": {7, 0},
"syscall.InterfaceMulticastAddrMessage": {7, 0},
"syscall.FormatMessage": {5, 0},
+
+ // Not marked as deprecated with a recognizable header, but deprecated nonetheless.
+ "io/ioutil": {16, 16},
}
From c13dc1b8b68765fbd26395f96f7e3abfd2c0cdbf Mon Sep 17 00:00:00 2001
From: Dominik Honnef
Date: Mon, 14 Dec 2020 10:11:54 +0100
Subject: [PATCH 108/111] staticcheck: more tailored deprecation diagnostics
Print different diagnostics depending on the precise combination of
"deprecated since" and "alternative available since".
---
knowledge/deprecated.go | 148 +++++++++---------
staticcheck/lint.go | 52 ++++--
staticcheck/lint_test.go | 7 +-
.../CheckDeprecated_go13/CheckDeprecated.go | 14 ++
.../CheckDeprecated_go14/CheckDeprecated.go | 3 -
.../CheckDeprecated_go18/CheckDeprecated.go | 4 +-
6 files changed, 142 insertions(+), 86 deletions(-)
create mode 100644 staticcheck/testdata/src/CheckDeprecated_go13/CheckDeprecated.go
diff --git a/knowledge/deprecated.go b/knowledge/deprecated.go
index 365abaf88..439ab603d 100644
--- a/knowledge/deprecated.go
+++ b/knowledge/deprecated.go
@@ -1,5 +1,10 @@
package knowledge
+const (
+ DeprecatedNeverUse = -1
+ DeprecatedUseNoLonger = -2
+)
+
type Deprecation struct {
DeprecatedSince int
AlternativeAvailableSince int
@@ -8,90 +13,91 @@ type Deprecation struct {
var StdlibDeprecations = map[string]Deprecation{
// FIXME(dh): AllowBinary isn't being detected as deprecated
// because the comment has a newline right after "Deprecated:"
- "go/build.AllowBinary": {7, 7},
- "(archive/zip.FileHeader).CompressedSize": {1, 1},
- "(archive/zip.FileHeader).UncompressedSize": {1, 1},
- "(archive/zip.FileHeader).ModifiedTime": {10, 10},
- "(archive/zip.FileHeader).ModifiedDate": {10, 10},
- "(*archive/zip.FileHeader).ModTime": {10, 10},
- "(*archive/zip.FileHeader).SetModTime": {10, 10},
- "(go/doc.Package).Bugs": {1, 1},
- "os.SEEK_SET": {7, 7},
- "os.SEEK_CUR": {7, 7},
- "os.SEEK_END": {7, 7},
- "(net.Dialer).Cancel": {7, 7},
- "runtime.CPUProfile": {9, 0},
- "compress/flate.ReadError": {6, 6},
- "compress/flate.WriteError": {6, 6},
- "path/filepath.HasPrefix": {0, 0},
- "(net/http.Transport).Dial": {7, 7},
- "(*net/http.Transport).CancelRequest": {6, 5},
- "net/http.ErrWriteAfterFlush": {7, 0},
- "net/http.ErrHeaderTooLong": {8, 0},
- "net/http.ErrShortBody": {8, 0},
- "net/http.ErrMissingContentLength": {8, 0},
- "net/http/httputil.ErrPersistEOF": {0, 0},
- "net/http/httputil.ErrClosed": {0, 0},
- "net/http/httputil.ErrPipeline": {0, 0},
- "net/http/httputil.ServerConn": {0, 0},
- "net/http/httputil.NewServerConn": {0, 0},
- "net/http/httputil.ClientConn": {0, 0},
- "net/http/httputil.NewClientConn": {0, 0},
- "net/http/httputil.NewProxyClientConn": {0, 0},
- "(net/http.Request).Cancel": {7, 7},
- "(text/template/parse.PipeNode).Line": {1, 1},
- "(text/template/parse.ActionNode).Line": {1, 1},
- "(text/template/parse.BranchNode).Line": {1, 1},
- "(text/template/parse.TemplateNode).Line": {1, 1},
- "database/sql/driver.ColumnConverter": {9, 9},
- "database/sql/driver.Execer": {8, 8},
- "database/sql/driver.Queryer": {8, 8},
- "(database/sql/driver.Conn).Begin": {8, 8},
- "(database/sql/driver.Stmt).Exec": {8, 8},
- "(database/sql/driver.Stmt).Query": {8, 8},
- "syscall.StringByteSlice": {1, 1},
- "syscall.StringBytePtr": {1, 1},
- "syscall.StringSlicePtr": {1, 1},
- "syscall.StringToUTF16": {1, 1},
- "syscall.StringToUTF16Ptr": {1, 1},
- "(*regexp.Regexp).Copy": {12, 12},
- "(archive/tar.Header).Xattrs": {10, 10},
- "archive/tar.TypeRegA": {11, 1},
- "go/types.NewInterface": {11, 11},
- "(*go/types.Interface).Embedded": {11, 11},
- "go/importer.For": {12, 12},
- "encoding/json.InvalidUTF8Error": {2, 2},
- "encoding/json.UnmarshalFieldError": {2, 2},
- "encoding/csv.ErrTrailingComma": {2, 2},
- "(encoding/csv.Reader).TrailingComma": {2, 2},
- "(net.Dialer).DualStack": {12, 12},
- "net/http.ErrUnexpectedTrailer": {12, 12},
- "net/http.CloseNotifier": {11, 7},
- "net/http.ProtocolError": {8, 8},
+ "go/build.AllowBinary": {7, 7},
+ "(archive/zip.FileHeader).CompressedSize": {1, 1},
+ "(archive/zip.FileHeader).UncompressedSize": {1, 1},
+ "(archive/zip.FileHeader).ModifiedTime": {10, 10},
+ "(archive/zip.FileHeader).ModifiedDate": {10, 10},
+ "(*archive/zip.FileHeader).ModTime": {10, 10},
+ "(*archive/zip.FileHeader).SetModTime": {10, 10},
+ "(go/doc.Package).Bugs": {1, 1},
+ "os.SEEK_SET": {7, 7},
+ "os.SEEK_CUR": {7, 7},
+ "os.SEEK_END": {7, 7},
+ "(net.Dialer).Cancel": {7, 7},
+ "runtime.CPUProfile": {9, 0},
+ "compress/flate.ReadError": {6, DeprecatedUseNoLonger},
+ "compress/flate.WriteError": {6, DeprecatedUseNoLonger},
+ "path/filepath.HasPrefix": {0, DeprecatedNeverUse},
+ "(net/http.Transport).Dial": {7, 7},
+ "(*net/http.Transport).CancelRequest": {6, 5},
+ "net/http.ErrWriteAfterFlush": {7, DeprecatedUseNoLonger},
+ "net/http.ErrHeaderTooLong": {8, DeprecatedUseNoLonger},
+ "net/http.ErrShortBody": {8, DeprecatedUseNoLonger},
+ "net/http.ErrMissingContentLength": {8, DeprecatedUseNoLonger},
+ "net/http/httputil.ErrPersistEOF": {0, DeprecatedUseNoLonger},
+ "net/http/httputil.ErrClosed": {0, DeprecatedUseNoLonger},
+ "net/http/httputil.ErrPipeline": {0, DeprecatedUseNoLonger},
+ "net/http/httputil.ServerConn": {0, 0},
+ "net/http/httputil.NewServerConn": {0, 0},
+ "net/http/httputil.ClientConn": {0, 0},
+ "net/http/httputil.NewClientConn": {0, 0},
+ "net/http/httputil.NewProxyClientConn": {0, 0},
+ "(net/http.Request).Cancel": {7, 7},
+ "(text/template/parse.PipeNode).Line": {1, DeprecatedUseNoLonger},
+ "(text/template/parse.ActionNode).Line": {1, DeprecatedUseNoLonger},
+ "(text/template/parse.BranchNode).Line": {1, DeprecatedUseNoLonger},
+ "(text/template/parse.TemplateNode).Line": {1, DeprecatedUseNoLonger},
+ "database/sql/driver.ColumnConverter": {9, 9},
+ "database/sql/driver.Execer": {8, 8},
+ "database/sql/driver.Queryer": {8, 8},
+ "(database/sql/driver.Conn).Begin": {8, 8},
+ "(database/sql/driver.Stmt).Exec": {8, 8},
+ "(database/sql/driver.Stmt).Query": {8, 8},
+ "syscall.StringByteSlice": {1, 1},
+ "syscall.StringBytePtr": {1, 1},
+ "syscall.StringSlicePtr": {1, 1},
+ "syscall.StringToUTF16": {1, 1},
+ "syscall.StringToUTF16Ptr": {1, 1},
+ "(*regexp.Regexp).Copy": {12, DeprecatedUseNoLonger},
+ "(archive/tar.Header).Xattrs": {10, 10},
+ "archive/tar.TypeRegA": {11, 1},
+ "go/types.NewInterface": {11, 11},
+ "(*go/types.Interface).Embedded": {11, 11},
+ "go/importer.For": {12, 12},
+ "encoding/json.InvalidUTF8Error": {2, DeprecatedUseNoLonger},
+ "encoding/json.UnmarshalFieldError": {2, DeprecatedUseNoLonger},
+ "encoding/csv.ErrTrailingComma": {2, DeprecatedUseNoLonger},
+ "(encoding/csv.Reader).TrailingComma": {2, DeprecatedUseNoLonger},
+ "(net.Dialer).DualStack": {12, 12},
+ "net/http.ErrUnexpectedTrailer": {12, DeprecatedUseNoLonger},
+ "net/http.CloseNotifier": {11, 7},
+ // This is hairy. The notice says "Not all errors in the http package related to protocol errors are of type ProtocolError", but doesn't that imply that
+ "net/http.ProtocolError": {8, DeprecatedUseNoLonger},
"(crypto/x509.CertificateRequest).Attributes": {5, 3},
// These functions have no direct alternative, but they are insecure and should no longer be used.
- "crypto/x509.IsEncryptedPEMBlock": {16, 0},
- "crypto/x509.DecryptPEMBlock": {16, 0},
- "crypto/x509.EncryptPEMBlock": {16, 0},
- "crypto/dsa": {16, 0},
+ "crypto/x509.IsEncryptedPEMBlock": {16, DeprecatedNeverUse},
+ "crypto/x509.DecryptPEMBlock": {16, DeprecatedNeverUse},
+ "crypto/x509.EncryptPEMBlock": {16, DeprecatedNeverUse},
+ "crypto/dsa": {16, DeprecatedNeverUse},
// This function has no alternative, but also no purpose.
- "(*crypto/rc4.Cipher).Reset": {12, 0},
+ "(*crypto/rc4.Cipher).Reset": {12, DeprecatedNeverUse},
"(net/http/httptest.ResponseRecorder).HeaderMap": {11, 7},
"image.ZP": {13, 0},
"image.ZR": {13, 0},
"(*debug/gosym.LineTable).LineToPC": {2, 2},
"(*debug/gosym.LineTable).PCToLine": {2, 2},
- "crypto/tls.VersionSSL30": {13, 0},
- "(crypto/tls.Config).NameToCertificate": {14, 14},
- "(*crypto/tls.Config).BuildNameToCertificate": {14, 14},
+ "crypto/tls.VersionSSL30": {13, DeprecatedNeverUse},
+ "(crypto/tls.Config).NameToCertificate": {14, DeprecatedUseNoLonger},
+ "(*crypto/tls.Config).BuildNameToCertificate": {14, DeprecatedUseNoLonger},
"(crypto/tls.Config).SessionTicketKey": {16, 5},
// No alternative, no use
- "(crypto/tls.ConnectionState).NegotiatedProtocolIsMutual": {16, 0},
+ "(crypto/tls.ConnectionState).NegotiatedProtocolIsMutual": {16, DeprecatedNeverUse},
// No alternative, but insecure
- "(crypto/tls.ConnectionState).TLSUnique": {16, 0},
- "image/jpeg.Reader": {4, 0},
+ "(crypto/tls.ConnectionState).TLSUnique": {16, DeprecatedNeverUse},
+ "image/jpeg.Reader": {4, DeprecatedNeverUse},
// All of these have been deprecated in favour of external libraries
"syscall.AttachLsf": {7, 0},
diff --git a/staticcheck/lint.go b/staticcheck/lint.go
index 394f203c4..fd43bd790 100644
--- a/staticcheck/lint.go
+++ b/staticcheck/lint.go
@@ -2929,14 +2929,37 @@ func CheckDeprecated(pass *analysis.Pass) (interface{}, error) {
return true
}
if depr, ok := deprs.Objects[obj]; ok {
- // Look for the first available alternative, not the first
- // version something was deprecated in. If a function was
- // deprecated in Go 1.6, an alternative has been available
- // already in 1.0, and we're targeting 1.2, it still
- // makes sense to use the alternative from 1.0, to be
- // future-proof.
- minVersion := knowledge.StdlibDeprecations[code.SelectorName(pass, sel)].AlternativeAvailableSince
- if !code.IsGoVersion(pass, minVersion) {
+ std, ok := knowledge.StdlibDeprecations[code.SelectorName(pass, sel)]
+ if ok {
+ switch std.AlternativeAvailableSince {
+ case knowledge.DeprecatedNeverUse:
+ // This should never be used, regardless of the
+ // targeted Go version. Examples include insecure
+ // cryptography or inherently broken APIs.
+ //
+ // We always want to flag these.
+ case knowledge.DeprecatedUseNoLonger:
+ // This should no longer be used. Using it with
+ // older Go versions might still make sense.
+ if !code.IsGoVersion(pass, std.DeprecatedSince) {
+ return true
+ }
+ default:
+ if std.AlternativeAvailableSince < 0 {
+ panic(fmt.Sprintf("unhandled case %d", std.AlternativeAvailableSince))
+ }
+ // Look for the first available alternative, not the first
+ // version something was deprecated in. If a function was
+ // deprecated in Go 1.6, an alternative has been available
+ // already in 1.0, and we're targeting 1.2, it still
+ // makes sense to use the alternative from 1.0, to be
+ // future-proof.
+ if !code.IsGoVersion(pass, std.AlternativeAvailableSince) {
+ return true
+ }
+ }
+ }
+ if ok && !code.IsGoVersion(pass, std.AlternativeAvailableSince) {
return true
}
@@ -2947,7 +2970,18 @@ func CheckDeprecated(pass *analysis.Pass) (interface{}, error) {
return true
}
}
- report.Report(pass, sel, fmt.Sprintf("%s is deprecated: %s", report.Render(pass, sel), depr.Msg))
+
+ if ok {
+ if std.AlternativeAvailableSince == knowledge.DeprecatedNeverUse {
+ report.Report(pass, sel, fmt.Sprintf("%s has been deprecated since Go 1.%d because it shouldn't be used: %s", report.Render(pass, sel), std.DeprecatedSince, depr.Msg))
+ } else if std.AlternativeAvailableSince == std.DeprecatedSince || std.AlternativeAvailableSince == knowledge.DeprecatedUseNoLonger {
+ report.Report(pass, sel, fmt.Sprintf("%s has been deprecated since Go 1.%d: %s", report.Render(pass, sel), std.DeprecatedSince, depr.Msg))
+ } else {
+ report.Report(pass, sel, fmt.Sprintf("%s has been deprecated since Go 1.%d and an alternative has been available since Go 1.%d: %s", report.Render(pass, sel), std.DeprecatedSince, std.AlternativeAvailableSince, depr.Msg))
+ }
+ } else {
+ report.Report(pass, sel, fmt.Sprintf("%s is deprecated: %s", report.Render(pass, sel), depr.Msg))
+ }
return true
}
return true
diff --git a/staticcheck/lint_test.go b/staticcheck/lint_test.go
index b351a9a14..0aefdb04b 100644
--- a/staticcheck/lint_test.go
+++ b/staticcheck/lint_test.go
@@ -26,7 +26,12 @@ func TestAll(t *testing.T) {
"SA1016": {{Dir: "CheckUntrappableSignal"}},
"SA1017": {{Dir: "CheckUnbufferedSignalChan"}},
"SA1018": {{Dir: "CheckStringsReplaceZero"}},
- "SA1019": {{Dir: "CheckDeprecated"}, {Dir: "CheckDeprecated_go14", Version: "1.4"}, {Dir: "CheckDeprecated_go18", Version: "1.8"}},
+ "SA1019": {
+ {Dir: "CheckDeprecated"},
+ {Dir: "CheckDeprecated_go13", Version: "1.3"},
+ {Dir: "CheckDeprecated_go14", Version: "1.4"},
+ {Dir: "CheckDeprecated_go18", Version: "1.8"},
+ },
"SA1020": {{Dir: "CheckListenAddress"}},
"SA1021": {{Dir: "CheckBytesEqualIP"}},
"SA1023": {{Dir: "CheckWriterBufferModified"}},
diff --git a/staticcheck/testdata/src/CheckDeprecated_go13/CheckDeprecated.go b/staticcheck/testdata/src/CheckDeprecated_go13/CheckDeprecated.go
new file mode 100644
index 000000000..4558ef1f9
--- /dev/null
+++ b/staticcheck/testdata/src/CheckDeprecated_go13/CheckDeprecated.go
@@ -0,0 +1,14 @@
+package pkg
+
+import (
+ "crypto/x509"
+ "net/http/httputil"
+ "path/filepath"
+)
+
+func fn() {
+ filepath.HasPrefix("", "") // want `filepath.HasPrefix has been deprecated since Go 1.0 because it shouldn't be used:`
+ _ = httputil.ErrPersistEOF // want `httputil.ErrPersistEOF has been deprecated since Go 1.0:`
+ _ = httputil.ServerConn{} // want `httputil.ServerConn has been deprecated since Go 1.0:`
+ _ = x509.CertificateRequest{}.Attributes // want `x509.CertificateRequest{}.Attributes has been deprecated since Go 1.5 and an alternative has been available since Go 1.3:`
+}
diff --git a/staticcheck/testdata/src/CheckDeprecated_go14/CheckDeprecated.go b/staticcheck/testdata/src/CheckDeprecated_go14/CheckDeprecated.go
index 21aa784e2..e8473dd8b 100644
--- a/staticcheck/testdata/src/CheckDeprecated_go14/CheckDeprecated.go
+++ b/staticcheck/testdata/src/CheckDeprecated_go14/CheckDeprecated.go
@@ -15,9 +15,6 @@ func fn1(err error) {
_ = r.Cancel // want `If a Request's Cancel field and context are both`
_ = syscall.StringByteSlice("") // want `Use ByteSliceFromString instead`
_ = os.SEEK_SET
- if err == http.ErrWriteAfterFlush { // want `ErrWriteAfterFlush is no longer`
- println()
- }
var _ flate.ReadError
var tr *http.Transport
diff --git a/staticcheck/testdata/src/CheckDeprecated_go18/CheckDeprecated.go b/staticcheck/testdata/src/CheckDeprecated_go18/CheckDeprecated.go
index e67a9b1f7..f5dedb9da 100644
--- a/staticcheck/testdata/src/CheckDeprecated_go18/CheckDeprecated.go
+++ b/staticcheck/testdata/src/CheckDeprecated_go18/CheckDeprecated.go
@@ -21,10 +21,10 @@ func fn1(err error) {
var _ flate.ReadError // want `No longer returned`
var tr *http.Transport
- tr.CancelRequest(nil) // want `CancelRequest is deprecated`
+ tr.CancelRequest(nil) // want `CancelRequest has been deprecated`
var conn driver.Conn
- conn.Begin() // want `Begin is deprecated`
+ conn.Begin() // want `Begin has been deprecated`
}
// Deprecated: Don't use this.
From 911c7885a6d3cb88d8246c03de1ef070b49ccc7e Mon Sep 17 00:00:00 2001
From: Dominik Honnef
Date: Mon, 14 Dec 2020 16:55:34 +0100
Subject: [PATCH 109/111] Tweaks to our benchmarking script
---
_benchmarks/bench.sh | 33 +++++++++++++--------------------
1 file changed, 13 insertions(+), 20 deletions(-)
diff --git a/_benchmarks/bench.sh b/_benchmarks/bench.sh
index 5f3c9c024..89d8dc6c8 100755
--- a/_benchmarks/bench.sh
+++ b/_benchmarks/bench.sh
@@ -3,20 +3,21 @@ set -e
declare -A PKGS=(
["strconv"]="strconv"
+ ["net/http"]="net/http"
+ ["image/color"]="image/color"
["std"]="std"
["k8s"]="k8s.io/kubernetes/pkg/..."
)
-MIN_CORES=1
-MAX_CORES=16
-MIN_GOGC=10
+MIN_CORES=32
+MAX_CORES=32
+INCR_CORES=2
+MIN_GOGC=100
MAX_GOGC=100
-SAMPLES=5
+SAMPLES=10
WIPE_CACHE=1
-FORMAT=csv
+FORMAT=bench
BIN=$(realpath ./silent-staticcheck.sh)
-SMT=1
-
runBenchmark() {
local pkg="$1"
@@ -29,14 +30,7 @@ runBenchmark() {
rm -rf ~/.cache/staticcheck
fi
- local procs
- if [ $SMT -ne 0 ]; then
- procs=$((cores*2))
- else
- procs=$cores
- fi
-
- local out=$(GOGC=$gc env time -f "%e %M" taskset -c 0-$((procs-1)) $BIN $pkg 2>&1)
+ local out=$(GOGC=$gc GOMAXPROCS=$cores env time -f "%e %M" $BIN $pkg 2>&1)
local t=$(echo "$out" | cut -f1 -d" ")
local m=$(echo "$out" | cut -f2 -d" ")
local ns=$(printf "%s 1000000000 * p" $t | dc)
@@ -44,25 +38,24 @@ runBenchmark() {
case $FORMAT in
bench)
- printf "BenchmarkStaticcheck-%s-GOGC%d-wiped%d-%d 1 %.0f ns/op %.0f B/op\n" "$label" "$gc" "$wipe" "$procs" "$ns" "$b"
+ printf "BenchmarkStaticcheck-%s-GOGC%d-wiped%d-%d 1 %.0f ns/op %.0f B/op\n" "$label" "$gc" "$wipe" "$cores" "$ns" "$b"
;;
csv)
- printf "%s,%d,%d,%d,%.0f,%.0f\n" "$label" "$gc" "$procs" "$wipe" "$ns" "$b"
+ printf "%s,%d,%d,%d,%.0f,%.0f\n" "$label" "$gc" "$cores" "$wipe" "$ns" "$b"
;;
esac
}
-go build ../cmd/staticcheck
export GO111MODULE=off
if [ "$FORMAT" = "csv" ]; then
- printf "packages,gogc,procs,wipe-cache,time,memory\n"
+ printf "packages,gogc,gomaxprocs,wipe-cache,time,memory\n"
fi
for label in "${!PKGS[@]}"; do
pkg=${PKGS[$label]}
for gc in $(seq $MIN_GOGC 10 $MAX_GOGC); do
- for cores in $(seq $MIN_CORES $MAX_CORES); do
+ for cores in $(seq $MIN_CORES $INCR_CORES $MAX_CORES); do
for i in $(seq 1 $SAMPLES); do
runBenchmark "$pkg" "$label" "$gc" "$cores" 1
runBenchmark "$pkg" "$label" "$gc" "$cores" 0
From ead01d5c4d42534f47be66499c157a33ed05ee98 Mon Sep 17 00:00:00 2001
From: Dominik Honnef
Date: Mon, 14 Dec 2020 16:56:18 +0100
Subject: [PATCH 110/111] doc: add 2020.2 release notes
---
doc/2020.2.html | 562 +++++++++++++++++++++++++++++++++++++++++++
doc/staticcheck.html | 4 +-
2 files changed, 564 insertions(+), 2 deletions(-)
create mode 100644 doc/2020.2.html
diff --git a/doc/2020.2.html b/doc/2020.2.html
new file mode 100644
index 000000000..72955dae0
--- /dev/null
+++ b/doc/2020.2.html
@@ -0,0 +1,562 @@
+
+ Furthermore, Staticcheck 2020.2 will skip very large packages (currently packages that are 50 MiB or larger),
+ under the assumption that these packages contain bundled assets and aren't worth analyzing.
+ This might further reduce Staticcheck's memory usage in your projects.
+
+
+
Changes to the detection of unused code
+
+
Removal of whole-program mode and changes to the handling of exported identifiers
+ The most visible change is the removal of the whole program mode.
+ This mode, which analyzed an entire program and reported unused code even if it is exported,
+ did not work well with the kind of caching that we use in Staticcheck.
+ Even in previous versions, it didn't always work correctly and may have caused flaky results,
+ depending on the state of the cache and the order of staticcheck invocations.
+
+
+
+ The whole-program mode may be revived in the future as a standalone tool,
+ with the understanding that this mode of operation is inherently more expensive than staticcheck.
+ In the meantime, if you depend on this functionality and can tolerate its bugs, you should continue using Staticcheck 2020.1.
+
+
+
+ As part of improving the correctness of U1000, changes were made to the normal mode as well.
+ In particular, all exported package-level identifiers will be considered used from now on,
+ even if these identifiers are declared in package main or tests, even if they are otherwise unused.
+ Exported identifiers in package main can be used in ways invisible to us, for example via the plugin build mode.
+ For tests, we would run into the same kind of issues as we did with the whole program mode.
+
+
+
Improvements
+
+
+ The //lint:ignore directive now works more intelligently with the U1000 check.
+ In previous versions, the directive would only suppress the output of a diagnostic. For example, for the following example
+
+
+
package pkg
+
+//lint:ignore U1000 This is fine.
+func fn1() { fn2() }
+
+func fn2() {}
+
+
+ Staticcheck would emit the following output:
+
+
+
foo.go:6:6: func fn2 is unused (U1000)
+
+
+ as it would only suppress the diagnostic for fn1.
+
+
+
+ Beginning with this release, the directive instead actively marks the identifier as used,
+ which means that any transitively used code will also be considered used, and no diagnostic will be reported for fn2.
+ Similarly, the //lint:file-ignore directive will consider everything in a file used, which may transitively mark code in other files used, too.
+
+
+
UI improvements
+
+We've made some minor improvements to the output and behavior of the staticcheck command:
+
+
+
the command now prints instructions on how to look up documentation for checks
+
output of the -explain flag includes a link to the online documentation
+
+
a warning is emitted when a package pattern matches no packages
+
unmatched ignore directives cause staticcheck to exit with a non-zero status code
+
+
+
Changes to versioning scheme
+
+
+ Staticcheck releases have two version numbers: one meant for human consumption and one meant for consumption by machines, via Go modules.
+ For example, the previous release was both 2020.1.6 (for humans) and v0.0.1-2020.1.6 (for machines).
+
+
+
+ In previous releases, we've tried to include the human version in the machine version, by using the v0.0.1-<human version> scheme.
+ However, this scheme had various drawbacks.
+ For this and future releases we've switched to a more standard scheme for machine versions: v0.<minor>.<patch>.
+ Minor will increase by one for every feature release of Staticcheck,
+ and patch will increase by one for every bugfix release of Staticcheck,
+ resetting to zero on feature releases.
+
+
+
+ For example, this release is both 2020.2 and v0.1.0.
+ A hypothetical 2020.2.1 would be v0.1.1, and 2021.1 will be v0.2.0.
+ This new versioning scheme fixes various issues when trying to use Staticcheck as a Go module.
+ It will also allow us to make true pre-releases in the future.
+
+
+
+ Documentation on the website, as well as the output of staticcheck -version, will include both version numbers, to make it easier to associate the two.
+
+
+
+ For detailed information on how we arrived at this decision, see the discussion on issue 777.
+
+
+
Checks
+
New checks
+
+
+ The following new checks have been added:
+
+
+
+
{{ check "SA4023" }} flags impossible comparisons of interface values with untyped nils
+
{{ check "SA5012" }} flags function calls with slice arguments that aren't the right length
+
{{ check "SA9006" }} flags dubious bit shifts of fixed size integers
+
+
+
Changed checks
+
+
+ Several checks have been improved:
+
+
+
+
{{ check "S1030" }} no longer recommends replacing m[string(buf.Bytes())] with m[buf.String()], as the former gets optimized by the compiler
+
{{ check "S1008" }} no longer incorrectly suggests that the negation of >= is <=
+
{{ check "S1029" }} and {{ check "SA6003" }} now also check custom types with underlying type string
+
{{ check "SA1019" }} now recognizes deprecation notices that aren't in the last paragraph of a comment
+
{{ check "SA1019" }} now emits more precise diagnostics for deprecated code in the standard library
+
{{ check "SA4006" }} no longer flags assignments where the value is a typed nil
+
{{ check "SA5011" }} is now able to detect more functions that never return, thus reducing the number of false positives
+
{{ check "SA9004" }} no longer assumes that constants belong to the same group when they have different types
+
Automatic fixes for {{ check "SA9004" }} inside gopls no longer incorrectly duplicate comments
+
{{ check "ST1003" }} no longer complains about ALL_CAPS in variable names that don't contain any letters
+
Incorrect position information in various checks have been fixed
If you use Go modules, you can simply run go get honnef.co/go/tools/cmd/staticcheck to obtain the latest released version.
If you're still using a GOPATH-based workflow, then the above command will instead fetch the master branch.
- It is suggested that you explicitly check out the latest release tag instead, which is currently 2020.1.6.
+ It is suggested that you explicitly check out the latest release tag instead, which is currently 2020.2.
One way of doing so would be as follows:
cd $GOPATH/src/honnef.co/go/tools/cmd/staticcheck
-git checkout 2020.1.6
+git checkout 2020.2
go get
go install
From c5b83c7d52ec0e1e12b8e776612955ef710247fb Mon Sep 17 00:00:00 2001
From: Dominik Honnef
Date: Mon, 14 Dec 2020 17:01:15 +0100
Subject: [PATCH 111/111] Version 2020.2 (v0.1.0)
---
lintcmd/version/version.go | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/lintcmd/version/version.go b/lintcmd/version/version.go
index cb12e2121..0acde9ea0 100644
--- a/lintcmd/version/version.go
+++ b/lintcmd/version/version.go
@@ -7,8 +7,8 @@ import (
"runtime"
)
-const Version = "devel"
-const MachineVersion = "devel"
+const Version = "2020.2"
+const MachineVersion = "v0.1.0"
// version returns a version descriptor and reports whether the
// version is a known release.