Merge pull request #1 from golangci/feature/initial-improvements
Feature/initial improvements
This commit is contained in:
commit
e8d7807944
.gitignore.golangci.example.yml.golangci.ymlGopkg.lockGopkg.tomlMakefile
cmd/golangci-lint
internal/commands
pkg
config
enabled_linters.goenabled_linters_test.gofsutils
golinters
context.godeadcode.godupl.goerrcheck.goerrcheck_test.gogas.gogoconst.gogocyclo.gogofmt.gogofmt_test.gogolint.gogolint_test.gogovet.gogovet_test.goineffassign.gointerfacer.golinter.gomaligned.gomegacheck.gostructcheck.gosupported_linters.gotest.gounconvert.goutils.govarcheck.go
linter.golinter_mock.goprinters
result
issue.go
runner.goprocessors
cgo.godiff.godiff_processor.goexclude.goexclude_processor.goexclude_test.gomax_from_linter.gomax_from_linter_test.gomax_per_file_from_linter.gomax_per_file_from_linter_test.gomax_per_file_processor.gomax_same_issues.gomax_same_issues_test.gonolint.gonolint_test.gopath_prettifier.goprocessor.go
testdata
uniq_by_line.gouniq_by_line_processor.gouniq_by_line_test.goutils.gotestdata
vendor/github.com/GoASTScanner/gas
3
.gitignore
vendored
3
.gitignore
vendored
@ -1 +1,2 @@
|
||||
/vendor/
|
||||
/*.txt
|
||||
/*.pprof
|
||||
|
58
.golangci.example.yml
Normal file
58
.golangci.example.yml
Normal file
@ -0,0 +1,58 @@
|
||||
run:
|
||||
args:
|
||||
- ./...
|
||||
verbose: true
|
||||
concurrency: 4
|
||||
deadline: 1m
|
||||
issues-exit-code: 1
|
||||
tests: true
|
||||
build-tags:
|
||||
- mytag
|
||||
|
||||
output:
|
||||
format: colored-line-number
|
||||
print-issued-lines: true
|
||||
print-linter-name: true
|
||||
print-welcome: true
|
||||
|
||||
linters-settings:
|
||||
errcheck:
|
||||
check-type-assertions: false
|
||||
check-blank: false
|
||||
govet:
|
||||
check-shadowing: true
|
||||
golint:
|
||||
min-confidence: 0.8
|
||||
gofmt:
|
||||
simplify: true
|
||||
gocyclo:
|
||||
min-complexity: 10
|
||||
maligned:
|
||||
suggest-new: true
|
||||
dupl:
|
||||
threshold: 50
|
||||
goconst:
|
||||
min-len: 3
|
||||
min-occurrences: 3
|
||||
|
||||
linters:
|
||||
enable:
|
||||
- megacheck
|
||||
- vet
|
||||
enable-all: true
|
||||
disable:
|
||||
maligned
|
||||
disable-all: false
|
||||
presets:
|
||||
- bugs
|
||||
- unused
|
||||
|
||||
issues:
|
||||
exclude:
|
||||
- abcdef
|
||||
exclude-use-default: true
|
||||
max-per-linter: 0
|
||||
max-same: 0
|
||||
new: false
|
||||
new-from-rev: ""
|
||||
new-from-patch: ""
|
31
.golangci.yml
Normal file
31
.golangci.yml
Normal file
@ -0,0 +1,31 @@
|
||||
run:
|
||||
verbose: true
|
||||
deadline: 30s
|
||||
tests: true
|
||||
|
||||
linters-settings:
|
||||
errcheck:
|
||||
check-type-assertions: true
|
||||
check-blank: true
|
||||
govet:
|
||||
check-shadowing: true
|
||||
golint:
|
||||
min-confidence: 0
|
||||
gocyclo:
|
||||
min-complexity: 10
|
||||
maligned:
|
||||
suggest-new: true
|
||||
dupl:
|
||||
threshold: 100
|
||||
goconst:
|
||||
min-len: 2
|
||||
min-occurrences: 2
|
||||
|
||||
linters:
|
||||
enable-all: true
|
||||
disable:
|
||||
- maligned
|
||||
|
||||
issues:
|
||||
exclude:
|
||||
- should have a package comment
|
368
Gopkg.lock
generated
368
Gopkg.lock
generated
@ -2,16 +2,14 @@
|
||||
|
||||
|
||||
[[projects]]
|
||||
name = "github.com/StackExchange/wmi"
|
||||
packages = ["."]
|
||||
revision = "5d049714c4a64225c3c79a7cf7d02f7fb5b96338"
|
||||
version = "1.0.0"
|
||||
|
||||
[[projects]]
|
||||
name = "github.com/bradleyfalzon/revgrep"
|
||||
packages = ["."]
|
||||
revision = "c04006dc3307c8768bda7a33c7c15d1c6f664e14"
|
||||
version = "v0.3"
|
||||
branch = "master"
|
||||
name = "github.com/GoASTScanner/gas"
|
||||
packages = [
|
||||
".",
|
||||
"rules"
|
||||
]
|
||||
revision = "a9de4d6c1589158e002cc336c495bf11fbf3ea06"
|
||||
source = "github.com/golangci/gas"
|
||||
|
||||
[[projects]]
|
||||
name = "github.com/davecgh/go-spew"
|
||||
@ -20,19 +18,16 @@
|
||||
version = "v1.1.0"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
name = "github.com/dukex/mixpanel"
|
||||
name = "github.com/fatih/color"
|
||||
packages = ["."]
|
||||
revision = "88b7bfd34643dce0c28a6b797652d6b5026091af"
|
||||
revision = "507f6050b8568533fb3f5504de8e5205fa62a114"
|
||||
version = "v1.6.0"
|
||||
|
||||
[[projects]]
|
||||
name = "github.com/go-ole/go-ole"
|
||||
packages = [
|
||||
".",
|
||||
"oleutil"
|
||||
]
|
||||
revision = "a41e3c4b706f6ae8dfbff342b06e40fa4d2d0506"
|
||||
version = "v1.2.1"
|
||||
name = "github.com/fsnotify/fsnotify"
|
||||
packages = ["."]
|
||||
revision = "c2828203cd70a50dcccfb2761f8b1f8ceef9a8e9"
|
||||
version = "v1.4.7"
|
||||
|
||||
[[projects]]
|
||||
name = "github.com/gogo/protobuf"
|
||||
@ -41,21 +36,204 @@
|
||||
version = "v1.0.0"
|
||||
|
||||
[[projects]]
|
||||
name = "github.com/golang/mock"
|
||||
packages = ["gomock"]
|
||||
revision = "c34cdb4725f4c3844d095133c6e40e448b86589b"
|
||||
version = "v1.1.1"
|
||||
branch = "master"
|
||||
name = "github.com/golang/lint"
|
||||
packages = ["."]
|
||||
revision = "470b6b0bb3005eda157f0275e2e4895055396a81"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
name = "github.com/golangci/golangci-shared"
|
||||
name = "github.com/golangci/check"
|
||||
packages = [
|
||||
"pkg/analytics",
|
||||
"pkg/executors",
|
||||
"pkg/runmode",
|
||||
"pkg/timeutils"
|
||||
"cmd/structcheck",
|
||||
"cmd/varcheck"
|
||||
]
|
||||
revision = "044f3332f2e8c38cfbb56bab29f65b4245bbe76b"
|
||||
revision = "cfe4005ccda277a820149d44d6ededc400cc99a2"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
name = "github.com/golangci/go-misc"
|
||||
packages = ["deadcode"]
|
||||
revision = "a61b005a223b50d3656798639dd87073ca5c1981"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
name = "github.com/golangci/go-tools"
|
||||
packages = [
|
||||
"callgraph",
|
||||
"callgraph/static",
|
||||
"cmd/megacheck",
|
||||
"deprecated",
|
||||
"functions",
|
||||
"internal/sharedcheck",
|
||||
"lint",
|
||||
"lint/lintutil",
|
||||
"simple",
|
||||
"ssa",
|
||||
"ssa/ssautil",
|
||||
"staticcheck",
|
||||
"staticcheck/vrp",
|
||||
"unused",
|
||||
"version"
|
||||
]
|
||||
revision = "b5abc2a37fd3a93a3716b0bc4da42d47b90ecaab"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
name = "github.com/golangci/goconst"
|
||||
packages = ["."]
|
||||
revision = "b67b9035d29a7561902d87eb3757dd490b31c1b1"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
name = "github.com/golangci/gocyclo"
|
||||
packages = ["."]
|
||||
revision = "687488c898ab673ee751943f7bcab53ce2371985"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
name = "github.com/golangci/gofmt"
|
||||
packages = [
|
||||
"gofmt",
|
||||
"goimports"
|
||||
]
|
||||
revision = "2076e05ced53480baa8ba14a05b80a415bec9377"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
name = "github.com/golangci/govet"
|
||||
packages = [
|
||||
".",
|
||||
"lib/cfg",
|
||||
"lib/whitelist"
|
||||
]
|
||||
revision = "1a9ab8120ec96a014df3afab2d6c47f7e12d8928"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
name = "github.com/golangci/ineffassign"
|
||||
packages = ["."]
|
||||
revision = "7b41b0f84881918dab3c16c5e5148d8aa55d27b4"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
name = "github.com/golangci/maligned"
|
||||
packages = ["."]
|
||||
revision = "b1d89398deca2fd3f8578e5a9551e819bd01ca5f"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
name = "github.com/golangci/revgrep"
|
||||
packages = ["."]
|
||||
revision = "dfd919b445ba350862d7a6e7fe81989000b84020"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
name = "github.com/golangci/unconvert"
|
||||
packages = ["."]
|
||||
revision = "28b1c447d1f4a810737ee6ab40ea6c1d0ceae4ad"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
name = "github.com/hashicorp/hcl"
|
||||
packages = [
|
||||
".",
|
||||
"hcl/ast",
|
||||
"hcl/parser",
|
||||
"hcl/printer",
|
||||
"hcl/scanner",
|
||||
"hcl/strconv",
|
||||
"hcl/token",
|
||||
"json/parser",
|
||||
"json/scanner",
|
||||
"json/token"
|
||||
]
|
||||
revision = "ef8a98b0bbce4a65b5aa4c368430a80ddc533168"
|
||||
|
||||
[[projects]]
|
||||
name = "github.com/inconshreveable/mousetrap"
|
||||
packages = ["."]
|
||||
revision = "76626ae9c91c4f2a10f34cad8ce83ea42c93bb75"
|
||||
version = "v1.0"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
name = "github.com/kisielk/errcheck"
|
||||
packages = [
|
||||
"golangci",
|
||||
"internal/errcheck"
|
||||
]
|
||||
revision = "78f2b28b23265c54a520979eceadf679f93fc92b"
|
||||
source = "github.com/golangci/errcheck"
|
||||
|
||||
[[projects]]
|
||||
name = "github.com/kisielk/gotool"
|
||||
packages = [
|
||||
".",
|
||||
"internal/load"
|
||||
]
|
||||
revision = "80517062f582ea3340cd4baf70e86d539ae7d84d"
|
||||
version = "v1.0.0"
|
||||
|
||||
[[projects]]
|
||||
name = "github.com/magiconair/properties"
|
||||
packages = ["."]
|
||||
revision = "c3beff4c2358b44d0493c7dda585e7db7ff28ae6"
|
||||
version = "v1.7.6"
|
||||
|
||||
[[projects]]
|
||||
name = "github.com/mattn/go-colorable"
|
||||
packages = ["."]
|
||||
revision = "167de6bfdfba052fa6b2d3664c8f5272e23c9072"
|
||||
version = "v0.0.9"
|
||||
|
||||
[[projects]]
|
||||
name = "github.com/mattn/go-isatty"
|
||||
packages = ["."]
|
||||
revision = "0360b2af4f38e8d38c7fce2a9f4e702702d73a39"
|
||||
version = "v0.0.3"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
name = "github.com/mibk/dupl"
|
||||
packages = [
|
||||
".",
|
||||
"job",
|
||||
"printer",
|
||||
"suffixtree",
|
||||
"syntax",
|
||||
"syntax/golang"
|
||||
]
|
||||
revision = "53b9af5a45362a6f2896cfa39cc17d17ba9667ea"
|
||||
source = "github.com/golangci/dupl"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
name = "github.com/mitchellh/mapstructure"
|
||||
packages = ["."]
|
||||
revision = "00c29f56e2386353d58c599509e8dc3801b0d716"
|
||||
|
||||
[[projects]]
|
||||
name = "github.com/nbutton23/zxcvbn-go"
|
||||
packages = [
|
||||
".",
|
||||
"adjacency",
|
||||
"data",
|
||||
"entropy",
|
||||
"frequency",
|
||||
"match",
|
||||
"matching",
|
||||
"scoring",
|
||||
"utils/math"
|
||||
]
|
||||
revision = "eafdab6b0663b4b528c35975c8b0e78be6e25261"
|
||||
version = "v0.1"
|
||||
|
||||
[[projects]]
|
||||
name = "github.com/pelletier/go-toml"
|
||||
packages = ["."]
|
||||
revision = "acdc4509485b587f5e675510c4f2c63e90ff68a8"
|
||||
version = "v1.1.0"
|
||||
|
||||
[[projects]]
|
||||
name = "github.com/pmezard/go-difflib"
|
||||
@ -63,64 +241,63 @@
|
||||
revision = "792786c7400a136282c1664665ae0a8db921c6c2"
|
||||
version = "v1.0.0"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
name = "github.com/savaki/amplitude-go"
|
||||
packages = ["."]
|
||||
revision = "f62e3b57c0e4d24da1fad27aa5181c59b0f7868b"
|
||||
|
||||
[[projects]]
|
||||
name = "github.com/shirou/gopsutil"
|
||||
packages = [
|
||||
"cpu",
|
||||
"host",
|
||||
"internal/common",
|
||||
"mem",
|
||||
"net",
|
||||
"process"
|
||||
]
|
||||
revision = "c95755e4bcd7a62bb8bd33f3a597a7c7f35e2cf3"
|
||||
version = "v2.18.04"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
name = "github.com/shirou/w32"
|
||||
packages = ["."]
|
||||
revision = "bb4de0191aa41b5507caa14b0650cdbddcd9280b"
|
||||
|
||||
[[projects]]
|
||||
name = "github.com/sirupsen/logrus"
|
||||
packages = ["."]
|
||||
revision = "c155da19408a8799da419ed3eeb0cb5db0ad5dbc"
|
||||
version = "v1.0.5"
|
||||
|
||||
[[projects]]
|
||||
name = "github.com/spf13/afero"
|
||||
packages = [
|
||||
".",
|
||||
"mem"
|
||||
]
|
||||
revision = "63644898a8da0bc22138abf860edaf5277b6102e"
|
||||
version = "v1.1.0"
|
||||
|
||||
[[projects]]
|
||||
name = "github.com/spf13/cast"
|
||||
packages = ["."]
|
||||
revision = "8965335b8c7107321228e3e3702cab9832751bac"
|
||||
version = "v1.2.0"
|
||||
|
||||
[[projects]]
|
||||
name = "github.com/spf13/cobra"
|
||||
packages = ["."]
|
||||
revision = "a1f051bc3eba734da4772d60e2d677f47cf93ef4"
|
||||
version = "v0.0.2"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
name = "github.com/spf13/jwalterweatherman"
|
||||
packages = ["."]
|
||||
revision = "7c0cea34c8ece3fbeb2b27ab9b59511d360fb394"
|
||||
|
||||
[[projects]]
|
||||
name = "github.com/spf13/pflag"
|
||||
packages = ["."]
|
||||
revision = "583c0c0531f06d5278b7d917446061adc344b5cd"
|
||||
version = "v1.0.1"
|
||||
|
||||
[[projects]]
|
||||
name = "github.com/spf13/viper"
|
||||
packages = ["."]
|
||||
revision = "b5e8006cbee93ec955a89ab31e0e3ce3204f3736"
|
||||
version = "v1.0.2"
|
||||
|
||||
[[projects]]
|
||||
name = "github.com/stretchr/testify"
|
||||
packages = ["assert"]
|
||||
revision = "12b6f73e6084dad08a7c6e575284b177ecafbc71"
|
||||
version = "v1.2.1"
|
||||
|
||||
[[projects]]
|
||||
name = "github.com/stvp/rollbar"
|
||||
packages = ["."]
|
||||
revision = "b20261800d8cda3be14dcef0d1a8320779bba61a"
|
||||
version = "v0.5.1"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
name = "golang.org/x/crypto"
|
||||
packages = ["ssh/terminal"]
|
||||
revision = "4ec37c66abab2c7e02ae775328b2ff001c3f025a"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
name = "golang.org/x/net"
|
||||
packages = [
|
||||
"context",
|
||||
"context/ctxhttp"
|
||||
]
|
||||
revision = "640f4622ab692b87c2f3a94265e6f579fe38263d"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
name = "golang.org/x/sys"
|
||||
@ -130,6 +307,55 @@
|
||||
]
|
||||
revision = "6f686a352de66814cdd080d970febae7767857a3"
|
||||
|
||||
[[projects]]
|
||||
name = "golang.org/x/text"
|
||||
packages = [
|
||||
"internal/gen",
|
||||
"internal/triegen",
|
||||
"internal/ucd",
|
||||
"transform",
|
||||
"unicode/cldr",
|
||||
"unicode/norm",
|
||||
"width"
|
||||
]
|
||||
revision = "f21a4dfb5e38f5895301dc265a8def02365cc3d0"
|
||||
version = "v0.3.0"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
name = "golang.org/x/tools"
|
||||
packages = [
|
||||
"go/ast/astutil",
|
||||
"go/buildutil",
|
||||
"go/gcexportdata",
|
||||
"go/internal/gcimporter",
|
||||
"go/loader",
|
||||
"go/types/typeutil",
|
||||
"imports",
|
||||
"internal/fastwalk"
|
||||
]
|
||||
revision = "87723262609ca8fd55d449c027454c29cadefd68"
|
||||
|
||||
[[projects]]
|
||||
name = "gopkg.in/yaml.v2"
|
||||
packages = ["."]
|
||||
revision = "5420a8b6744d3b0345ab293f6fcba19c978f1183"
|
||||
version = "v2.2.1"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
name = "mvdan.cc/interfacer"
|
||||
packages = ["check"]
|
||||
revision = "72c3fb5d5e5e0ca07e9a7c90bcd150b049920b3b"
|
||||
source = "github.com/golangci/interfacer"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
name = "mvdan.cc/lint"
|
||||
packages = ["."]
|
||||
revision = "8ff1696d5934157cea033c4b97cf5ea23fdb7a32"
|
||||
source = "github.com/golangci/lint"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
name = "sourcegraph.com/sourcegraph/go-diff"
|
||||
@ -145,6 +371,6 @@
|
||||
[solve-meta]
|
||||
analyzer-name = "dep"
|
||||
analyzer-version = 1
|
||||
inputs-digest = "fc3abded9121a32e3fc9de887b9f1dd7a4d64ee58e169053e9120881d6c54658"
|
||||
inputs-digest = "cef0930b43e9ca27cd15aba3d76237889dda24ad02fc56d95ae098d83d9420b9"
|
||||
solver-name = "gps-cdcl"
|
||||
solver-version = 1
|
||||
|
43
Gopkg.toml
43
Gopkg.toml
@ -26,16 +26,8 @@
|
||||
|
||||
|
||||
[[constraint]]
|
||||
name = "github.com/bradleyfalzon/revgrep"
|
||||
version = "0.3.0"
|
||||
|
||||
[[constraint]]
|
||||
name = "github.com/golang/mock"
|
||||
version = "1.1.1"
|
||||
|
||||
[[constraint]]
|
||||
name = "github.com/golangci/revgrep"
|
||||
branch = "master"
|
||||
name = "github.com/golangci/golangci-shared"
|
||||
|
||||
[[constraint]]
|
||||
name = "github.com/stretchr/testify"
|
||||
@ -45,6 +37,39 @@
|
||||
branch = "master"
|
||||
name = "sourcegraph.com/sourcegraph/go-diff"
|
||||
|
||||
[[constraint]]
|
||||
name = "github.com/kisielk/errcheck"
|
||||
branch = "master"
|
||||
source = "github.com/golangci/errcheck"
|
||||
|
||||
[[constraint]]
|
||||
name = "github.com/golangci/govet"
|
||||
branch = "master"
|
||||
|
||||
[[constraint]]
|
||||
name = "github.com/mibk/dupl"
|
||||
branch = "master"
|
||||
source = "github.com/golangci/dupl"
|
||||
|
||||
[[constraint]]
|
||||
name = "mvdan.cc/interfacer"
|
||||
branch = "master"
|
||||
source = "github.com/golangci/interfacer"
|
||||
|
||||
[[constraint]]
|
||||
name = "github.com/GoASTScanner/gas"
|
||||
branch = "master"
|
||||
source = "github.com/golangci/gas"
|
||||
|
||||
[[override]]
|
||||
name = "mvdan.cc/lint"
|
||||
branch = "master"
|
||||
source = "github.com/golangci/lint"
|
||||
|
||||
[prune]
|
||||
go-tests = true
|
||||
unused-packages = true
|
||||
|
||||
[[constraint]]
|
||||
name = "github.com/spf13/viper"
|
||||
version = "1.0.2"
|
||||
|
3
Makefile
Normal file
3
Makefile
Normal file
@ -0,0 +1,3 @@
|
||||
test:
|
||||
golangci-lint run
|
||||
go test -v -race ./...
|
@ -1,43 +1,18 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/golangci/golangci-lint/pkg/config"
|
||||
"github.com/golangci/golangci-lint/pkg/golinters"
|
||||
"github.com/golangci/golangci-shared/pkg/executors"
|
||||
"github.com/golangci/golangci-lint/internal/commands"
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
func main() {
|
||||
if err := run(); err != nil {
|
||||
log.SetFlags(0) // don't print time
|
||||
logrus.SetLevel(logrus.WarnLevel)
|
||||
|
||||
e := commands.NewExecutor()
|
||||
if err := e.Execute(); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
|
||||
func run() error {
|
||||
var cfg config.Config
|
||||
config.ReadFromCommandLine(&cfg)
|
||||
|
||||
linters := golinters.GetSupportedLinters()
|
||||
ctx := context.Background()
|
||||
|
||||
ex, err := os.Executable()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
exPath := filepath.Dir(ex)
|
||||
exec := executors.NewShell(exPath)
|
||||
|
||||
for _, linter := range linters {
|
||||
res, err := linter.Run(ctx, exec)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
log.Print(res)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
30
internal/commands/executor.go
Normal file
30
internal/commands/executor.go
Normal file
@ -0,0 +1,30 @@
|
||||
package commands
|
||||
|
||||
import (
|
||||
"github.com/golangci/golangci-lint/pkg/config"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
type Executor struct {
|
||||
rootCmd *cobra.Command
|
||||
|
||||
cfg *config.Config
|
||||
|
||||
exitCode int
|
||||
}
|
||||
|
||||
func NewExecutor() *Executor {
|
||||
e := &Executor{
|
||||
cfg: &config.Config{},
|
||||
}
|
||||
|
||||
e.initRoot()
|
||||
e.initRun()
|
||||
e.initLinters()
|
||||
|
||||
return e
|
||||
}
|
||||
|
||||
func (e Executor) Execute() error {
|
||||
return e.rootCmd.Execute()
|
||||
}
|
54
internal/commands/linters.go
Normal file
54
internal/commands/linters.go
Normal file
@ -0,0 +1,54 @@
|
||||
package commands
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/fatih/color"
|
||||
"github.com/golangci/golangci-lint/pkg"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
func (e *Executor) initLinters() {
|
||||
var lintersCmd = &cobra.Command{
|
||||
Use: "linters",
|
||||
Short: "List linters",
|
||||
Run: e.executeLinters,
|
||||
}
|
||||
e.rootCmd.AddCommand(lintersCmd)
|
||||
}
|
||||
|
||||
func printLinterConfigs(lcs []pkg.LinterConfig) {
|
||||
for _, lc := range lcs {
|
||||
fmt.Printf("%s: %s\n", color.YellowString(lc.Linter.Name()), lc.Linter.Desc())
|
||||
}
|
||||
}
|
||||
|
||||
func (e Executor) executeLinters(cmd *cobra.Command, args []string) {
|
||||
var enabledLCs, disabledLCs []pkg.LinterConfig
|
||||
for _, lc := range pkg.GetAllSupportedLinterConfigs() {
|
||||
if lc.EnabledByDefault {
|
||||
enabledLCs = append(enabledLCs, lc)
|
||||
} else {
|
||||
disabledLCs = append(disabledLCs, lc)
|
||||
}
|
||||
}
|
||||
|
||||
color.Green("Enabled by default linters:\n")
|
||||
printLinterConfigs(enabledLCs)
|
||||
color.Red("\nDisabled by default linters:\n")
|
||||
printLinterConfigs(disabledLCs)
|
||||
|
||||
color.Green("\nLinters presets:")
|
||||
for _, p := range pkg.AllPresets() {
|
||||
linters := pkg.GetAllLintersForPreset(p)
|
||||
linterNames := []string{}
|
||||
for _, linter := range linters {
|
||||
linterNames = append(linterNames, linter.Name())
|
||||
}
|
||||
fmt.Printf("%s: %s\n", color.YellowString(p), strings.Join(linterNames, ", "))
|
||||
}
|
||||
|
||||
os.Exit(0)
|
||||
}
|
52
internal/commands/root.go
Normal file
52
internal/commands/root.go
Normal file
@ -0,0 +1,52 @@
|
||||
package commands
|
||||
|
||||
import (
|
||||
"log"
|
||||
"os"
|
||||
"runtime"
|
||||
"runtime/pprof"
|
||||
|
||||
"github.com/sirupsen/logrus"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
func (e *Executor) initRoot() {
|
||||
rootCmd := &cobra.Command{
|
||||
Use: "golangci-lint",
|
||||
Short: "golangci-lint is a smart linters runner.",
|
||||
Long: `Smart, fast linters runner. Run it in cloud for every GitHub pull request on https://golangci.com`,
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
if err := cmd.Help(); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
},
|
||||
PersistentPreRun: func(cmd *cobra.Command, args []string) {
|
||||
runtime.GOMAXPROCS(e.cfg.Run.Concurrency)
|
||||
|
||||
if e.cfg.Run.IsVerbose {
|
||||
logrus.SetLevel(logrus.InfoLevel)
|
||||
}
|
||||
|
||||
if e.cfg.Run.CPUProfilePath != "" {
|
||||
f, err := os.Create(e.cfg.Run.CPUProfilePath)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
if err := pprof.StartCPUProfile(f); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
}
|
||||
},
|
||||
PersistentPostRun: func(cmd *cobra.Command, args []string) {
|
||||
if e.cfg.Run.CPUProfilePath != "" {
|
||||
pprof.StopCPUProfile()
|
||||
}
|
||||
os.Exit(e.exitCode)
|
||||
},
|
||||
}
|
||||
rootCmd.PersistentFlags().BoolVarP(&e.cfg.Run.IsVerbose, "verbose", "v", false, "verbose output")
|
||||
rootCmd.PersistentFlags().StringVar(&e.cfg.Run.CPUProfilePath, "cpu-profile-path", "", "Path to CPU profile output file")
|
||||
rootCmd.PersistentFlags().IntVarP(&e.cfg.Run.Concurrency, "concurrency", "j", runtime.NumCPU(), "Concurrency")
|
||||
|
||||
e.rootCmd = rootCmd
|
||||
}
|
321
internal/commands/run.go
Normal file
321
internal/commands/run.go
Normal file
@ -0,0 +1,321 @@
|
||||
package commands
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"go/build"
|
||||
"go/token"
|
||||
"log"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/golangci/go-tools/ssa"
|
||||
"github.com/golangci/go-tools/ssa/ssautil"
|
||||
"github.com/golangci/golangci-lint/pkg"
|
||||
"github.com/golangci/golangci-lint/pkg/config"
|
||||
"github.com/golangci/golangci-lint/pkg/fsutils"
|
||||
"github.com/golangci/golangci-lint/pkg/golinters"
|
||||
"github.com/golangci/golangci-lint/pkg/printers"
|
||||
"github.com/golangci/golangci-lint/pkg/result"
|
||||
"github.com/golangci/golangci-lint/pkg/result/processors"
|
||||
"github.com/sirupsen/logrus"
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/spf13/viper"
|
||||
"golang.org/x/tools/go/loader"
|
||||
)
|
||||
|
||||
const exitCodeIfFailure = 3
|
||||
|
||||
func (e *Executor) initRun() {
|
||||
var runCmd = &cobra.Command{
|
||||
Use: "run",
|
||||
Short: "Run linters",
|
||||
Run: e.executeRun,
|
||||
}
|
||||
e.rootCmd.AddCommand(runCmd)
|
||||
|
||||
// Output config
|
||||
oc := &e.cfg.Output
|
||||
runCmd.Flags().StringVar(&oc.Format, "out-format",
|
||||
config.OutFormatColoredLineNumber,
|
||||
fmt.Sprintf("Format of output: %s", strings.Join(config.OutFormats, "|")))
|
||||
runCmd.Flags().BoolVar(&oc.PrintIssuedLine, "print-issued-lines", true, "Print lines of code with issue")
|
||||
runCmd.Flags().BoolVar(&oc.PrintLinterName, "print-linter-name", true, "Print linter name in issue line")
|
||||
runCmd.Flags().BoolVar(&oc.PrintWelcomeMessage, "print-welcome", true, "Print welcome message")
|
||||
|
||||
// Run config
|
||||
rc := &e.cfg.Run
|
||||
runCmd.Flags().IntVar(&rc.ExitCodeIfIssuesFound, "issues-exit-code",
|
||||
1, "Exit code when issues were found")
|
||||
runCmd.Flags().StringSliceVar(&rc.BuildTags, "build-tags", []string{}, "Build tags (not all linters support them)")
|
||||
runCmd.Flags().DurationVar(&rc.Deadline, "deadline", time.Minute, "Deadline for total work")
|
||||
runCmd.Flags().BoolVar(&rc.AnalyzeTests, "tests", false, "Analyze tests (*_test.go)")
|
||||
|
||||
// Linters settings config
|
||||
lsc := &e.cfg.LintersSettings
|
||||
runCmd.Flags().BoolVar(&lsc.Errcheck.CheckTypeAssertions, "errcheck.check-type-assertions", false, "Errcheck: check for ignored type assertion results")
|
||||
runCmd.Flags().BoolVar(&lsc.Errcheck.CheckAssignToBlank, "errcheck.check-blank", false, "Errcheck: check for errors assigned to blank identifier: _ = errFunc()")
|
||||
|
||||
runCmd.Flags().BoolVar(&lsc.Govet.CheckShadowing, "govet.check-shadowing", false, "Govet: check for shadowed variables")
|
||||
|
||||
runCmd.Flags().Float64Var(&lsc.Golint.MinConfidence, "golint.min-confidence", 0.8, "Golint: minimum confidence of a problem to print it")
|
||||
|
||||
runCmd.Flags().BoolVar(&lsc.Gofmt.Simplify, "gofmt.simplify", true, "Gofmt: simplify code")
|
||||
|
||||
runCmd.Flags().IntVar(&lsc.Gocyclo.MinComplexity, "gocyclo.min-complexity",
|
||||
30, "Minimal complexity of function to report it")
|
||||
|
||||
runCmd.Flags().BoolVar(&lsc.Maligned.SuggestNewOrder, "maligned.suggest-new", false, "Maligned: print suggested more optimal struct fields ordering")
|
||||
|
||||
runCmd.Flags().IntVar(&lsc.Dupl.Threshold, "dupl.threshold",
|
||||
150, "Dupl: Minimal threshold to detect copy-paste")
|
||||
|
||||
runCmd.Flags().IntVar(&lsc.Goconst.MinStringLen, "goconst.min-len",
|
||||
3, "Goconst: minimum constant string length")
|
||||
runCmd.Flags().IntVar(&lsc.Goconst.MinOccurrencesCount, "goconst.min-occurrences",
|
||||
3, "Goconst: minimum occurences of constant string count to trigger issue")
|
||||
|
||||
// Linters config
|
||||
lc := &e.cfg.Linters
|
||||
runCmd.Flags().StringSliceVarP(&lc.Enable, "enable", "E", []string{}, "Enable specific linter")
|
||||
runCmd.Flags().StringSliceVarP(&lc.Disable, "disable", "D", []string{}, "Disable specific linter")
|
||||
runCmd.Flags().BoolVar(&lc.EnableAll, "enable-all", false, "Enable all linters")
|
||||
runCmd.Flags().BoolVar(&lc.DisableAll, "disable-all", false, "Disable all linters")
|
||||
runCmd.Flags().StringSliceVarP(&lc.Presets, "presets", "p", []string{},
|
||||
fmt.Sprintf("Enable presets (%s) of linters. Run 'golangci-lint linters' to see them. This option implies option --disable-all", strings.Join(pkg.AllPresets(), "|")))
|
||||
|
||||
// Issues config
|
||||
ic := &e.cfg.Issues
|
||||
runCmd.Flags().StringSliceVarP(&ic.ExcludePatterns, "exclude", "e", []string{}, "Exclude issue by regexp")
|
||||
runCmd.Flags().BoolVar(&ic.UseDefaultExcludes, "exclude-use-default", true,
|
||||
fmt.Sprintf("Use or not use default excludes: (%s)", strings.Join(config.DefaultExcludePatterns, "|")))
|
||||
|
||||
runCmd.Flags().IntVar(&ic.MaxIssuesPerLinter, "max-issues-per-linter", 50, "Maximum issues count per one linter. Set to 0 to disable")
|
||||
runCmd.Flags().IntVar(&ic.MaxSameIssues, "max-same-issues", 3, "Maximum count of issues with the same text. Set to 0 to disable")
|
||||
|
||||
runCmd.Flags().BoolVarP(&ic.Diff, "new", "n", false, "Show only new issues: if there are unstaged changes or untracked files, only those changes are shown, else only changes in HEAD~ are shown")
|
||||
runCmd.Flags().StringVar(&ic.DiffFromRevision, "new-from-rev", "", "Show only new issues created after git revision `REV`")
|
||||
runCmd.Flags().StringVar(&ic.DiffPatchFilePath, "new-from-patch", "", "Show only new issues created in git patch with file path `PATH`")
|
||||
|
||||
runCmd.Flags().StringVarP(&e.cfg.Run.Config, "config", "c", "", "Read config from file path `PATH`")
|
||||
|
||||
e.parseConfig(runCmd)
|
||||
}
|
||||
|
||||
func isFullImportNeeded(linters []pkg.Linter) bool {
|
||||
for _, linter := range linters {
|
||||
lc := pkg.GetLinterConfig(linter.Name())
|
||||
if lc.DoesFullImport {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
func isSSAReprNeeded(linters []pkg.Linter) bool {
|
||||
for _, linter := range linters {
|
||||
lc := pkg.GetLinterConfig(linter.Name())
|
||||
if lc.NeedsSSARepr {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
func loadWholeAppIfNeeded(ctx context.Context, linters []pkg.Linter, cfg *config.Run, paths *fsutils.ProjectPaths) (*loader.Program, *loader.Config, error) {
|
||||
if !isFullImportNeeded(linters) {
|
||||
return nil, nil, nil
|
||||
}
|
||||
|
||||
startedAt := time.Now()
|
||||
defer func() {
|
||||
logrus.Infof("Program loading took %s", time.Since(startedAt))
|
||||
}()
|
||||
|
||||
bctx := build.Default
|
||||
bctx.BuildTags = append(bctx.BuildTags, cfg.BuildTags...)
|
||||
loadcfg := &loader.Config{
|
||||
Build: &bctx,
|
||||
AllowErrors: true, // Try to analyze event partially
|
||||
}
|
||||
rest, err := loadcfg.FromArgs(paths.MixedPaths(), cfg.AnalyzeTests)
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("can't parepare load config with paths: %s", err)
|
||||
}
|
||||
if len(rest) > 0 {
|
||||
return nil, nil, fmt.Errorf("unhandled loading paths: %v", rest)
|
||||
}
|
||||
|
||||
prog, err := loadcfg.Load()
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("can't load paths: %s", err)
|
||||
}
|
||||
|
||||
return prog, loadcfg, nil
|
||||
}
|
||||
|
||||
func buildSSAProgram(ctx context.Context, lprog *loader.Program) *ssa.Program {
|
||||
startedAt := time.Now()
|
||||
defer func() {
|
||||
logrus.Infof("SSA repr building took %s", time.Since(startedAt))
|
||||
}()
|
||||
|
||||
ssaProg := ssautil.CreateProgram(lprog, ssa.GlobalDebug)
|
||||
ssaProg.Build()
|
||||
return ssaProg
|
||||
}
|
||||
|
||||
func buildLintCtx(ctx context.Context, linters []pkg.Linter, cfg *config.Config) (*golinters.Context, error) {
|
||||
args := cfg.Run.Args
|
||||
if len(args) == 0 {
|
||||
args = []string{"./..."}
|
||||
}
|
||||
|
||||
paths, err := fsutils.GetPathsForAnalysis(ctx, args, cfg.Run.AnalyzeTests)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
prog, loaderConfig, err := loadWholeAppIfNeeded(ctx, linters, &cfg.Run, paths)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var ssaProg *ssa.Program
|
||||
if prog != nil && isSSAReprNeeded(linters) {
|
||||
ssaProg = buildSSAProgram(ctx, prog)
|
||||
}
|
||||
|
||||
return &golinters.Context{
|
||||
Paths: paths,
|
||||
Cfg: cfg,
|
||||
Program: prog,
|
||||
SSAProgram: ssaProg,
|
||||
LoaderConfig: loaderConfig,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (e *Executor) runAnalysis(ctx context.Context, args []string) (chan result.Issue, error) {
|
||||
e.cfg.Run.Args = args
|
||||
|
||||
linters, err := pkg.GetEnabledLinters(e.cfg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
lintCtx, err := buildLintCtx(ctx, linters, e.cfg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
excludePatterns := e.cfg.Issues.ExcludePatterns
|
||||
if e.cfg.Issues.UseDefaultExcludes {
|
||||
excludePatterns = append(excludePatterns, config.DefaultExcludePatterns...)
|
||||
}
|
||||
var excludeTotalPattern string
|
||||
if len(excludePatterns) != 0 {
|
||||
excludeTotalPattern = fmt.Sprintf("(%s)", strings.Join(excludePatterns, "|"))
|
||||
}
|
||||
fset := token.NewFileSet()
|
||||
if lintCtx.Program != nil {
|
||||
fset = lintCtx.Program.Fset
|
||||
}
|
||||
runner := pkg.SimpleRunner{
|
||||
Processors: []processors.Processor{
|
||||
processors.NewExclude(excludeTotalPattern),
|
||||
processors.NewCgo(),
|
||||
processors.NewNolint(fset),
|
||||
processors.NewUniqByLine(),
|
||||
processors.NewDiff(e.cfg.Issues.Diff, e.cfg.Issues.DiffFromRevision, e.cfg.Issues.DiffPatchFilePath),
|
||||
processors.NewMaxPerFileFromLinter(),
|
||||
processors.NewMaxSameIssues(e.cfg.Issues.MaxSameIssues),
|
||||
processors.NewMaxFromLinter(e.cfg.Issues.MaxIssuesPerLinter),
|
||||
processors.NewPathPrettifier(),
|
||||
},
|
||||
}
|
||||
|
||||
return runner.Run(ctx, linters, lintCtx), nil
|
||||
}
|
||||
|
||||
func (e *Executor) executeRun(cmd *cobra.Command, args []string) {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), e.cfg.Run.Deadline)
|
||||
defer cancel()
|
||||
|
||||
defer func(startedAt time.Time) {
|
||||
logrus.Infof("Run took %s", time.Since(startedAt))
|
||||
}(time.Now())
|
||||
|
||||
if e.cfg.Output.PrintWelcomeMessage {
|
||||
fmt.Println("Run this tool in cloud on every github pull request in https://golangci.com for free (public repos)")
|
||||
}
|
||||
|
||||
f := func() error {
|
||||
issues, err := e.runAnalysis(ctx, args)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var p printers.Printer
|
||||
if e.cfg.Output.Format == config.OutFormatJSON {
|
||||
p = printers.NewJSON()
|
||||
} else {
|
||||
p = printers.NewText(e.cfg.Output.PrintIssuedLine,
|
||||
e.cfg.Output.Format == config.OutFormatColoredLineNumber, e.cfg.Output.PrintLinterName)
|
||||
}
|
||||
gotAnyIssues, err := p.Print(issues)
|
||||
if err != nil {
|
||||
return fmt.Errorf("can't print %d issues: %s", len(issues), err)
|
||||
}
|
||||
|
||||
if gotAnyIssues {
|
||||
e.exitCode = e.cfg.Run.ExitCodeIfIssuesFound
|
||||
return nil
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
if err := f(); err != nil {
|
||||
log.Print(err)
|
||||
if e.exitCode == 0 {
|
||||
e.exitCode = exitCodeIfFailure
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (e *Executor) parseConfig(cmd *cobra.Command) {
|
||||
// XXX: hack with double parsing to acces "config" option here
|
||||
if err := cmd.ParseFlags(os.Args); err != nil {
|
||||
log.Fatalf("Can't parse agrs: %s", err)
|
||||
}
|
||||
|
||||
if err := viper.BindPFlags(cmd.Flags()); err != nil {
|
||||
log.Fatalf("Can't bind cobra's flags to viper: %s", err)
|
||||
}
|
||||
|
||||
viper.SetEnvPrefix("GOLANGCI")
|
||||
viper.SetEnvKeyReplacer(strings.NewReplacer(".", "_"))
|
||||
viper.AutomaticEnv()
|
||||
|
||||
configFile := viper.GetString("config")
|
||||
if configFile == "" {
|
||||
viper.SetConfigName(".golangci")
|
||||
viper.AddConfigPath("./")
|
||||
} else {
|
||||
viper.SetConfigFile(configFile)
|
||||
}
|
||||
|
||||
if err := viper.ReadInConfig(); err != nil {
|
||||
if _, ok := err.(viper.ConfigFileNotFoundError); ok {
|
||||
return
|
||||
}
|
||||
log.Fatalf("Can't read viper config: %s", err)
|
||||
}
|
||||
|
||||
if err := viper.Unmarshal(&e.cfg); err != nil {
|
||||
log.Fatalf("Can't unmarshal config by viper: %s", err)
|
||||
}
|
||||
}
|
@ -1,11 +0,0 @@
|
||||
package config
|
||||
|
||||
import "flag"
|
||||
|
||||
func ReadFromCommandLine(cfg *Config) {
|
||||
flag.Parse()
|
||||
paths := flag.Args()
|
||||
if len(paths) != 0 {
|
||||
cfg.Paths = paths
|
||||
}
|
||||
}
|
@ -1,5 +1,126 @@
|
||||
package config
|
||||
|
||||
type Config struct {
|
||||
Paths []string
|
||||
import (
|
||||
"time"
|
||||
)
|
||||
|
||||
type OutFormat string
|
||||
|
||||
const (
|
||||
OutFormatJSON = "json"
|
||||
OutFormatLineNumber = "line-number"
|
||||
OutFormatColoredLineNumber = "colored-line-number"
|
||||
)
|
||||
|
||||
var OutFormats = []string{OutFormatColoredLineNumber, OutFormatLineNumber, OutFormatJSON}
|
||||
|
||||
var DefaultExcludePatterns = []string{
|
||||
// errcheck
|
||||
"Error return value of .((os\\.)?std(out|err)\\..*|.*Close|os\\.Remove(All)?|.*printf?|os\\.(Un)?Setenv). is not checked",
|
||||
|
||||
// golint
|
||||
"should have comment",
|
||||
"comment on exported method",
|
||||
|
||||
// gas
|
||||
"G103:", // Use of unsafe calls should be audited
|
||||
"G104:", // disable what errcheck does: it reports on Close etc
|
||||
"G204:", // Subprocess launching should be audited: too lot false positives
|
||||
"G301:", // Expect directory permissions to be 0750 or less
|
||||
"G302:", // Expect file permissions to be 0600 or less
|
||||
"G304:", // Potential file inclusion via variable: `src, err := ioutil.ReadFile(filename)`
|
||||
|
||||
// govet
|
||||
"possible misuse of unsafe.Pointer",
|
||||
"should have signature",
|
||||
|
||||
// megacheck
|
||||
"ineffective break statement. Did you mean to break out of the outer loop", // developers tend to write in C-style with break in switch
|
||||
}
|
||||
|
||||
type Run struct {
|
||||
IsVerbose bool `mapstructure:"verbose"`
|
||||
CPUProfilePath string
|
||||
Concurrency int
|
||||
|
||||
Config string
|
||||
|
||||
Args []string
|
||||
|
||||
BuildTags []string `mapstructure:"build-tags"`
|
||||
|
||||
ExitCodeIfIssuesFound int `mapstructure:"issues-exit-code"`
|
||||
AnalyzeTests bool `mapstructure:"tests"`
|
||||
Deadline time.Duration
|
||||
}
|
||||
|
||||
type LintersSettings struct {
|
||||
Errcheck struct {
|
||||
CheckTypeAssertions bool `mapstructure:"check-type-assertions"`
|
||||
CheckAssignToBlank bool `mapstructure:"check-blank"`
|
||||
}
|
||||
Govet struct {
|
||||
CheckShadowing bool `mapstructure:"check-shadowing"`
|
||||
}
|
||||
Golint struct {
|
||||
MinConfidence float64 `mapstructure:"min-confidence"`
|
||||
}
|
||||
Gofmt struct {
|
||||
Simplify bool
|
||||
}
|
||||
Gocyclo struct {
|
||||
MinComplexity int `mapstructure:"min-complexity"`
|
||||
}
|
||||
Varcheck struct {
|
||||
CheckExportedFields bool `mapstructure:"exported-fields"`
|
||||
}
|
||||
Structcheck struct {
|
||||
CheckExportedFields bool `mapstructure:"exported-fields"`
|
||||
}
|
||||
Maligned struct {
|
||||
SuggestNewOrder bool `mapstructure:"suggest-new"`
|
||||
}
|
||||
Dupl struct {
|
||||
Threshold int
|
||||
}
|
||||
Goconst struct {
|
||||
MinStringLen int `mapstructure:"min-len"`
|
||||
MinOccurrencesCount int `mapstructure:"min-occurrences"`
|
||||
}
|
||||
}
|
||||
|
||||
type Linters struct {
|
||||
Enable []string
|
||||
Disable []string
|
||||
EnableAll bool `mapstructure:"enable-all"`
|
||||
DisableAll bool `mapstructure:"disable-all"`
|
||||
|
||||
Presets []string
|
||||
}
|
||||
|
||||
type Issues struct {
|
||||
ExcludePatterns []string `mapstructure:"exclude"`
|
||||
UseDefaultExcludes bool `mapstructure:"exclude-use-default"`
|
||||
|
||||
MaxIssuesPerLinter int `mapstructure:"max-issues-per-linter"`
|
||||
MaxSameIssues int `mapstructure:"max-same-issues"`
|
||||
|
||||
DiffFromRevision string `mapstructure:"new-from-rev"`
|
||||
DiffPatchFilePath string `mapstructure:"new-from-patch"`
|
||||
Diff bool `mapstructure:"new"`
|
||||
}
|
||||
|
||||
type Config struct { // nolint:maligned
|
||||
Run Run
|
||||
|
||||
Output struct {
|
||||
Format string
|
||||
PrintIssuedLine bool `mapstructure:"print-issued-lines"`
|
||||
PrintLinterName bool `mapstructure:"print-linter-name"`
|
||||
PrintWelcomeMessage bool `mapstructure:"print-welcome"`
|
||||
}
|
||||
|
||||
LintersSettings LintersSettings `mapstructure:"linters-settings"`
|
||||
Linters Linters
|
||||
Issues Issues
|
||||
}
|
||||
|
347
pkg/enabled_linters.go
Normal file
347
pkg/enabled_linters.go
Normal file
@ -0,0 +1,347 @@
|
||||
package pkg
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/golangci/golangci-lint/pkg/config"
|
||||
"github.com/golangci/golangci-lint/pkg/golinters"
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
const (
|
||||
PresetFormatting = "format"
|
||||
PresetComplexity = "complexity"
|
||||
PresetStyle = "style"
|
||||
PresetBugs = "bugs"
|
||||
PresetUnused = "unused"
|
||||
PresetPerformance = "performance"
|
||||
)
|
||||
|
||||
func AllPresets() []string {
|
||||
return []string{PresetBugs, PresetUnused, PresetFormatting, PresetStyle, PresetComplexity, PresetPerformance}
|
||||
}
|
||||
|
||||
func allPresetsSet() map[string]bool {
|
||||
ret := map[string]bool{}
|
||||
for _, p := range AllPresets() {
|
||||
ret[p] = true
|
||||
}
|
||||
return ret
|
||||
}
|
||||
|
||||
type LinterConfig struct {
|
||||
Linter Linter
|
||||
EnabledByDefault bool
|
||||
DoesFullImport bool
|
||||
NeedsSSARepr bool
|
||||
InPresets []string
|
||||
}
|
||||
|
||||
func (lc LinterConfig) WithFullImport() LinterConfig {
|
||||
lc.DoesFullImport = true
|
||||
return lc
|
||||
}
|
||||
|
||||
func (lc LinterConfig) WithSSA() LinterConfig {
|
||||
lc.DoesFullImport = true
|
||||
lc.NeedsSSARepr = true
|
||||
return lc
|
||||
}
|
||||
|
||||
func (lc LinterConfig) WithPresets(presets ...string) LinterConfig {
|
||||
lc.InPresets = presets
|
||||
return lc
|
||||
}
|
||||
|
||||
func (lc LinterConfig) WithDisabledByDefault() LinterConfig {
|
||||
lc.EnabledByDefault = false
|
||||
return lc
|
||||
}
|
||||
|
||||
func newLinterConfig(linter Linter) LinterConfig {
|
||||
return LinterConfig{
|
||||
Linter: linter,
|
||||
EnabledByDefault: true,
|
||||
}
|
||||
}
|
||||
|
||||
var nameToLC map[string]LinterConfig
|
||||
var nameToLCOnce sync.Once
|
||||
|
||||
func GetLinterConfig(name string) *LinterConfig {
|
||||
nameToLCOnce.Do(func() {
|
||||
nameToLC = make(map[string]LinterConfig)
|
||||
for _, lc := range GetAllSupportedLinterConfigs() {
|
||||
nameToLC[lc.Linter.Name()] = lc
|
||||
}
|
||||
})
|
||||
|
||||
lc, ok := nameToLC[name]
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
|
||||
return &lc
|
||||
}
|
||||
|
||||
func GetAllSupportedLinterConfigs() []LinterConfig {
|
||||
return []LinterConfig{
|
||||
newLinterConfig(golinters.Govet{}).WithPresets(PresetBugs),
|
||||
newLinterConfig(golinters.Errcheck{}).WithFullImport().WithPresets(PresetBugs),
|
||||
newLinterConfig(golinters.Golint{}).WithDisabledByDefault().WithPresets(PresetStyle),
|
||||
|
||||
newLinterConfig(golinters.Megacheck{StaticcheckEnabled: true}).WithSSA().WithPresets(PresetBugs),
|
||||
newLinterConfig(golinters.Megacheck{UnusedEnabled: true}).WithSSA().WithPresets(PresetUnused),
|
||||
newLinterConfig(golinters.Megacheck{GosimpleEnabled: true}).WithSSA().WithPresets(PresetStyle),
|
||||
|
||||
newLinterConfig(golinters.Gas{}).WithFullImport().WithPresets(PresetBugs),
|
||||
newLinterConfig(golinters.Structcheck{}).WithFullImport().WithPresets(PresetUnused),
|
||||
newLinterConfig(golinters.Varcheck{}).WithFullImport().WithPresets(PresetUnused),
|
||||
newLinterConfig(golinters.Interfacer{}).WithDisabledByDefault().WithSSA().WithPresets(PresetStyle),
|
||||
newLinterConfig(golinters.Unconvert{}).WithDisabledByDefault().WithFullImport().WithPresets(PresetStyle),
|
||||
newLinterConfig(golinters.Ineffassign{}).WithPresets(PresetUnused),
|
||||
newLinterConfig(golinters.Dupl{}).WithDisabledByDefault().WithPresets(PresetStyle),
|
||||
newLinterConfig(golinters.Goconst{}).WithDisabledByDefault().WithPresets(PresetStyle),
|
||||
newLinterConfig(golinters.Deadcode{}).WithFullImport().WithPresets(PresetUnused),
|
||||
newLinterConfig(golinters.Gocyclo{}).WithDisabledByDefault().WithPresets(PresetComplexity),
|
||||
|
||||
newLinterConfig(golinters.Gofmt{}).WithDisabledByDefault().WithPresets(PresetFormatting),
|
||||
newLinterConfig(golinters.Gofmt{UseGoimports: true}).WithDisabledByDefault().WithPresets(PresetFormatting),
|
||||
newLinterConfig(golinters.Maligned{}).WithFullImport().WithDisabledByDefault().WithPresets(PresetPerformance),
|
||||
newLinterConfig(golinters.Megacheck{GosimpleEnabled: true, UnusedEnabled: true, StaticcheckEnabled: true}).
|
||||
WithSSA().WithPresets(PresetStyle, PresetBugs, PresetUnused).WithDisabledByDefault(),
|
||||
}
|
||||
}
|
||||
|
||||
func getAllSupportedLinters() []Linter {
|
||||
var ret []Linter
|
||||
for _, lc := range GetAllSupportedLinterConfigs() {
|
||||
ret = append(ret, lc.Linter)
|
||||
}
|
||||
|
||||
return ret
|
||||
}
|
||||
|
||||
func getAllEnabledByDefaultLinters() []Linter {
|
||||
var ret []Linter
|
||||
for _, lc := range GetAllSupportedLinterConfigs() {
|
||||
if lc.EnabledByDefault {
|
||||
ret = append(ret, lc.Linter)
|
||||
}
|
||||
}
|
||||
|
||||
return ret
|
||||
}
|
||||
|
||||
var supportedLintersByName map[string]Linter
|
||||
var linterByNameMapOnce sync.Once
|
||||
|
||||
func getLinterByName(name string) Linter {
|
||||
linterByNameMapOnce.Do(func() {
|
||||
supportedLintersByName = make(map[string]Linter)
|
||||
for _, lc := range GetAllSupportedLinterConfigs() {
|
||||
supportedLintersByName[lc.Linter.Name()] = lc.Linter
|
||||
}
|
||||
})
|
||||
|
||||
return supportedLintersByName[name]
|
||||
}
|
||||
|
||||
func lintersToMap(linters []Linter) map[string]Linter {
|
||||
ret := map[string]Linter{}
|
||||
for _, linter := range linters {
|
||||
ret[linter.Name()] = linter
|
||||
}
|
||||
|
||||
return ret
|
||||
}
|
||||
|
||||
func validateLintersNames(cfg *config.Linters) error {
|
||||
allNames := append([]string{}, cfg.Enable...)
|
||||
allNames = append(allNames, cfg.Disable...)
|
||||
for _, name := range allNames {
|
||||
if getLinterByName(name) == nil {
|
||||
return fmt.Errorf("no such linter %q", name)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func validatePresets(cfg *config.Linters) error {
|
||||
allPresets := allPresetsSet()
|
||||
for _, p := range cfg.Presets {
|
||||
if !allPresets[p] {
|
||||
return fmt.Errorf("no such preset %q: only next presets exist: (%s)", p, strings.Join(AllPresets(), "|"))
|
||||
}
|
||||
}
|
||||
|
||||
if len(cfg.Presets) != 0 && cfg.EnableAll {
|
||||
return fmt.Errorf("--presets is incompatible with --enable-all")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func validateAllDisableEnableOptions(cfg *config.Linters) error {
|
||||
if cfg.EnableAll && cfg.DisableAll {
|
||||
return fmt.Errorf("--enable-all and --disable-all options must not be combined")
|
||||
}
|
||||
|
||||
if cfg.DisableAll {
|
||||
if len(cfg.Enable) == 0 {
|
||||
return fmt.Errorf("all linters were disabled, but no one linter was enabled: must enable at least one")
|
||||
}
|
||||
|
||||
if len(cfg.Disable) != 0 {
|
||||
return fmt.Errorf("can't combine options --disable-all and --disable %s", cfg.Disable[0])
|
||||
}
|
||||
}
|
||||
|
||||
if cfg.EnableAll && len(cfg.Enable) != 0 {
|
||||
return fmt.Errorf("can't combine options --enable-all and --enable %s", cfg.Enable[0])
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func validateDisabledAndEnabledAtOneMoment(cfg *config.Linters) error {
|
||||
enabledLintersSet := map[string]bool{}
|
||||
for _, name := range cfg.Enable {
|
||||
enabledLintersSet[name] = true
|
||||
}
|
||||
|
||||
for _, name := range cfg.Disable {
|
||||
if enabledLintersSet[name] {
|
||||
return fmt.Errorf("linter %q can't be disabled and enabled at one moment", name)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func validateEnabledDisabledLintersConfig(cfg *config.Linters) error {
|
||||
validators := []func(cfg *config.Linters) error{
|
||||
validateLintersNames,
|
||||
validatePresets,
|
||||
validateAllDisableEnableOptions,
|
||||
validateDisabledAndEnabledAtOneMoment,
|
||||
}
|
||||
for _, v := range validators {
|
||||
if err := v(cfg); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func GetAllLintersForPreset(p string) []Linter {
|
||||
ret := []Linter{}
|
||||
for _, lc := range GetAllSupportedLinterConfigs() {
|
||||
for _, ip := range lc.InPresets {
|
||||
if p == ip {
|
||||
ret = append(ret, lc.Linter)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return ret
|
||||
}
|
||||
|
||||
func getEnabledLintersSet(cfg *config.Config) map[string]Linter {
|
||||
lcfg := &cfg.Linters
|
||||
|
||||
resultLintersSet := map[string]Linter{}
|
||||
switch {
|
||||
case len(lcfg.Presets) != 0:
|
||||
break // imply --disable-all
|
||||
case lcfg.EnableAll:
|
||||
resultLintersSet = lintersToMap(getAllSupportedLinters())
|
||||
case lcfg.DisableAll:
|
||||
break
|
||||
default:
|
||||
resultLintersSet = lintersToMap(getAllEnabledByDefaultLinters())
|
||||
}
|
||||
|
||||
for _, name := range lcfg.Enable {
|
||||
resultLintersSet[name] = getLinterByName(name)
|
||||
}
|
||||
|
||||
for _, p := range lcfg.Presets {
|
||||
for _, linter := range GetAllLintersForPreset(p) {
|
||||
resultLintersSet[linter.Name()] = linter
|
||||
}
|
||||
}
|
||||
|
||||
for _, name := range lcfg.Disable {
|
||||
delete(resultLintersSet, name)
|
||||
}
|
||||
|
||||
return resultLintersSet
|
||||
}
|
||||
|
||||
func optimizeLintersSet(linters map[string]Linter) {
|
||||
unusedName := golinters.Megacheck{UnusedEnabled: true}.Name()
|
||||
gosimpleName := golinters.Megacheck{GosimpleEnabled: true}.Name()
|
||||
staticcheckName := golinters.Megacheck{StaticcheckEnabled: true}.Name()
|
||||
fullName := golinters.Megacheck{GosimpleEnabled: true, UnusedEnabled: true, StaticcheckEnabled: true}.Name()
|
||||
allNames := []string{unusedName, gosimpleName, staticcheckName, fullName}
|
||||
|
||||
megacheckCount := 0
|
||||
for _, n := range allNames {
|
||||
if linters[n] != nil {
|
||||
megacheckCount++
|
||||
}
|
||||
}
|
||||
|
||||
if megacheckCount <= 1 {
|
||||
return
|
||||
}
|
||||
|
||||
isFullEnabled := linters[fullName] != nil
|
||||
m := golinters.Megacheck{
|
||||
UnusedEnabled: isFullEnabled || linters[unusedName] != nil,
|
||||
GosimpleEnabled: isFullEnabled || linters[gosimpleName] != nil,
|
||||
StaticcheckEnabled: isFullEnabled || linters[staticcheckName] != nil,
|
||||
}
|
||||
|
||||
for _, n := range allNames {
|
||||
delete(linters, n)
|
||||
}
|
||||
|
||||
linters[m.Name()] = m
|
||||
}
|
||||
|
||||
func GetEnabledLinters(cfg *config.Config) ([]Linter, error) {
|
||||
if err := validateEnabledDisabledLintersConfig(&cfg.Linters); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
resultLintersSet := getEnabledLintersSet(cfg)
|
||||
optimizeLintersSet(resultLintersSet)
|
||||
|
||||
var resultLinters []Linter
|
||||
for _, linter := range resultLintersSet {
|
||||
resultLinters = append(resultLinters, linter)
|
||||
}
|
||||
|
||||
verbosePrintLintersStatus(cfg, resultLinters)
|
||||
|
||||
return resultLinters, nil
|
||||
}
|
||||
|
||||
func verbosePrintLintersStatus(cfg *config.Config, linters []Linter) {
|
||||
var linterNames []string
|
||||
for _, linter := range linters {
|
||||
linterNames = append(linterNames, linter.Name())
|
||||
}
|
||||
logrus.Infof("Active linters: %s", linterNames)
|
||||
|
||||
if len(cfg.Linters.Presets) != 0 {
|
||||
logrus.Infof("Active presets: %s", cfg.Linters.Presets)
|
||||
}
|
||||
}
|
69
pkg/enabled_linters_test.go
Normal file
69
pkg/enabled_linters_test.go
Normal file
@ -0,0 +1,69 @@
|
||||
package pkg
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func runGoErrchk(c *exec.Cmd, t *testing.T) {
|
||||
output, err := c.CombinedOutput()
|
||||
assert.NoError(t, err, "Output:\n%s", output)
|
||||
|
||||
// Can't check exit code: tool only prints to output
|
||||
assert.False(t, bytes.Contains(output, []byte("BUG")), "Output:\n%s", output)
|
||||
}
|
||||
|
||||
const testdataDir = "testdata"
|
||||
|
||||
var testdataWithIssuesDir = filepath.Join(testdataDir, "with_issues")
|
||||
var testdataNotCompilingDir = filepath.Join(testdataDir, "not_compiles")
|
||||
|
||||
const binName = "golangci-lint"
|
||||
|
||||
func TestSourcesFromTestdataWithIssuesDir(t *testing.T) {
|
||||
t.Log(filepath.Join(testdataWithIssuesDir, "*.go"))
|
||||
sources, err := filepath.Glob(filepath.Join(testdataWithIssuesDir, "*.go"))
|
||||
assert.NoError(t, err)
|
||||
assert.NotEmpty(t, sources)
|
||||
|
||||
installBinary(t)
|
||||
|
||||
for _, s := range sources {
|
||||
s := s
|
||||
t.Run(s, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
testOneSource(t, s)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func installBinary(t *testing.T) {
|
||||
cmd := exec.Command("go", "install", filepath.Join("..", "cmd", binName))
|
||||
assert.NoError(t, cmd.Run(), "Can't go install %s", binName)
|
||||
}
|
||||
|
||||
func testOneSource(t *testing.T, sourcePath string) {
|
||||
goErrchkBin := filepath.Join(runtime.GOROOT(), "test", "errchk")
|
||||
cmd := exec.Command(goErrchkBin, binName, "run",
|
||||
"--enable-all",
|
||||
"--dupl.threshold=20",
|
||||
"--gocyclo.min-complexity=20",
|
||||
"--print-issued-lines=false",
|
||||
"--print-linter-name=false",
|
||||
"--out-format=line-number",
|
||||
"--print-welcome=false",
|
||||
"--govet.check-shadowing=true",
|
||||
sourcePath)
|
||||
runGoErrchk(cmd, t)
|
||||
}
|
||||
|
||||
func TestNotCompilingProgram(t *testing.T) {
|
||||
installBinary(t)
|
||||
err := exec.Command(binName, "run", "--enable-all", testdataNotCompilingDir).Run()
|
||||
assert.NoError(t, err)
|
||||
}
|
@ -1,10 +1,129 @@
|
||||
package fsutils
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"go/build"
|
||||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
var stdExcludeDirs = []string{"vendor", "testdata", "examples", "Godeps", "builtin"}
|
||||
|
||||
func GetProjectRoot() string {
|
||||
return path.Join(build.Default.GOPATH, "src", "github.com", "golangci", "golangci-worker")
|
||||
}
|
||||
|
||||
type ProjectPaths struct {
|
||||
Files []string
|
||||
Dirs []string
|
||||
IsDirsRun bool
|
||||
}
|
||||
|
||||
func (p ProjectPaths) MixedPaths() []string {
|
||||
if p.IsDirsRun {
|
||||
return p.Dirs
|
||||
}
|
||||
|
||||
return p.Files
|
||||
}
|
||||
|
||||
func (p ProjectPaths) FilesGrouppedByDirs() [][]string {
|
||||
dirToFiles := map[string][]string{}
|
||||
for _, f := range p.Files {
|
||||
dir := filepath.Dir(f)
|
||||
dirToFiles[dir] = append(dirToFiles[dir], f)
|
||||
}
|
||||
|
||||
ret := [][]string{}
|
||||
for _, files := range dirToFiles {
|
||||
ret = append(ret, files)
|
||||
}
|
||||
return ret
|
||||
}
|
||||
|
||||
func processPaths(root string, paths []string, maxPaths int) ([]string, error) {
|
||||
if len(paths) > maxPaths {
|
||||
logrus.Warnf("Gofmt: got too much paths (%d), analyze first %d", len(paths), maxPaths)
|
||||
paths = paths[:maxPaths]
|
||||
}
|
||||
|
||||
ret := []string{}
|
||||
for _, p := range paths {
|
||||
if !filepath.IsAbs(p) {
|
||||
ret = append(ret, p)
|
||||
continue
|
||||
}
|
||||
|
||||
relPath, err := filepath.Rel(root, p)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("can't get relative path for path %s and root %s: %s",
|
||||
p, root, err)
|
||||
}
|
||||
ret = append(ret, relPath)
|
||||
}
|
||||
|
||||
return ret, nil
|
||||
}
|
||||
|
||||
func processResolvedPaths(paths *PathResolveResult) (*ProjectPaths, error) {
|
||||
root, err := os.Getwd()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("can't get working dir: %s", err)
|
||||
}
|
||||
|
||||
files, err := processPaths(root, paths.Files(), 10000)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("can't process resolved files: %s", err)
|
||||
}
|
||||
|
||||
dirs, err := processPaths(root, paths.Dirs(), 1000)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("can't process resolved dirs: %s", err)
|
||||
}
|
||||
|
||||
for i := range dirs {
|
||||
dir := dirs[i]
|
||||
if dir != "." && !filepath.IsAbs(dir) {
|
||||
dirs[i] = "./" + dir
|
||||
}
|
||||
}
|
||||
|
||||
return &ProjectPaths{
|
||||
Files: files,
|
||||
Dirs: dirs,
|
||||
IsDirsRun: len(dirs) != 0,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func GetPathsForAnalysis(ctx context.Context, inputPaths []string, includeTests bool) (ret *ProjectPaths, err error) {
|
||||
defer func(startedAt time.Time) {
|
||||
if ret != nil {
|
||||
logrus.Infof("Found paths for analysis for %s: %s", time.Since(startedAt), ret.MixedPaths())
|
||||
}
|
||||
}(time.Now())
|
||||
|
||||
for _, path := range inputPaths {
|
||||
if strings.HasSuffix(path, ".go") && len(inputPaths) != 1 {
|
||||
return nil, fmt.Errorf("specific files for analysis are allowed only if one file is set")
|
||||
}
|
||||
}
|
||||
|
||||
pr := NewPathResolver(stdExcludeDirs, []string{".go"}, includeTests)
|
||||
paths, err := pr.Resolve(inputPaths...)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("can't resolve paths: %s", err)
|
||||
}
|
||||
|
||||
return processResolvedPaths(paths)
|
||||
}
|
||||
|
||||
func IsDir(filename string) bool {
|
||||
fi, err := os.Stat(filename)
|
||||
return err == nil && fi.IsDir()
|
||||
}
|
||||
|
@ -11,6 +11,7 @@ import (
|
||||
type PathResolver struct {
|
||||
excludeDirs map[string]bool
|
||||
allowedFileExtensions map[string]bool
|
||||
includeTests bool
|
||||
}
|
||||
|
||||
type pathResolveState struct {
|
||||
@ -56,7 +57,7 @@ func (s pathResolveState) toResult() *PathResolveResult {
|
||||
return res
|
||||
}
|
||||
|
||||
func NewPathResolver(excludeDirs, allowedFileExtensions []string) *PathResolver {
|
||||
func NewPathResolver(excludeDirs, allowedFileExtensions []string, includeTests bool) *PathResolver {
|
||||
excludeDirsMap := map[string]bool{}
|
||||
for _, dir := range excludeDirs {
|
||||
excludeDirsMap[dir] = true
|
||||
@ -70,6 +71,7 @@ func NewPathResolver(excludeDirs, allowedFileExtensions []string) *PathResolver
|
||||
return &PathResolver{
|
||||
excludeDirs: excludeDirsMap,
|
||||
allowedFileExtensions: allowedFileExtensionsMap,
|
||||
includeTests: includeTests,
|
||||
}
|
||||
}
|
||||
|
||||
@ -89,6 +91,10 @@ func (pr PathResolver) isIgnoredDir(dir string) bool {
|
||||
}
|
||||
|
||||
func (pr PathResolver) isAllowedFile(path string) bool {
|
||||
if !pr.includeTests && strings.HasSuffix(path, "_test.go") {
|
||||
return false
|
||||
}
|
||||
|
||||
return pr.allowedFileExtensions[filepath.Ext(path)]
|
||||
}
|
||||
|
||||
|
@ -54,7 +54,7 @@ func prepareFS(t *testing.T, paths ...string) *fsPreparer {
|
||||
}
|
||||
|
||||
func newPR() *PathResolver {
|
||||
return NewPathResolver([]string{}, []string{})
|
||||
return NewPathResolver([]string{}, []string{}, false)
|
||||
}
|
||||
|
||||
func TestPathResolverNoPaths(t *testing.T) {
|
||||
@ -72,11 +72,12 @@ func TestPathResolverNotExistingPath(t *testing.T) {
|
||||
|
||||
func TestPathResolverCommonCases(t *testing.T) {
|
||||
type testCase struct {
|
||||
name string
|
||||
prepare []string
|
||||
resolve []string
|
||||
expFiles []string
|
||||
expDirs []string
|
||||
name string
|
||||
prepare []string
|
||||
resolve []string
|
||||
expFiles []string
|
||||
expDirs []string
|
||||
includeTests bool
|
||||
}
|
||||
|
||||
testCases := []testCase{
|
||||
@ -154,6 +155,28 @@ func TestPathResolverCommonCases(t *testing.T) {
|
||||
resolve: []string{"./..."},
|
||||
expDirs: []string{"."},
|
||||
},
|
||||
{
|
||||
name: "include tests",
|
||||
prepare: []string{"a/b.go", "a/b_test.go"},
|
||||
resolve: []string{"./..."},
|
||||
expDirs: []string{".", "a"},
|
||||
expFiles: []string{"a/b.go", "a/b_test.go"},
|
||||
includeTests: true,
|
||||
},
|
||||
{
|
||||
name: "exclude tests",
|
||||
prepare: []string{"a/b.go", "a/b_test.go"},
|
||||
resolve: []string{"./..."},
|
||||
expDirs: []string{".", "a"},
|
||||
expFiles: []string{"a/b.go"},
|
||||
},
|
||||
{
|
||||
name: "exclude tests except explicitly set",
|
||||
prepare: []string{"a/b.go", "a/b_test.go", "a/c_test.go"},
|
||||
resolve: []string{"./...", "a/c_test.go"},
|
||||
expDirs: []string{".", "a"},
|
||||
expFiles: []string{"a/b.go", "a/c_test.go"},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
@ -161,7 +184,7 @@ func TestPathResolverCommonCases(t *testing.T) {
|
||||
fp := prepareFS(t, tc.prepare...)
|
||||
defer fp.clean()
|
||||
|
||||
pr := NewPathResolver([]string{"vendor"}, []string{".go"})
|
||||
pr := NewPathResolver([]string{"vendor"}, []string{".go"}, tc.includeTests)
|
||||
res, err := pr.Resolve(tc.resolve...)
|
||||
assert.NoError(t, err)
|
||||
|
||||
|
20
pkg/golinters/context.go
Normal file
20
pkg/golinters/context.go
Normal file
@ -0,0 +1,20 @@
|
||||
package golinters
|
||||
|
||||
import (
|
||||
"github.com/golangci/go-tools/ssa"
|
||||
"github.com/golangci/golangci-lint/pkg/config"
|
||||
"github.com/golangci/golangci-lint/pkg/fsutils"
|
||||
"golang.org/x/tools/go/loader"
|
||||
)
|
||||
|
||||
type Context struct {
|
||||
Paths *fsutils.ProjectPaths
|
||||
Cfg *config.Config
|
||||
Program *loader.Program
|
||||
SSAProgram *ssa.Program
|
||||
LoaderConfig *loader.Config
|
||||
}
|
||||
|
||||
func (c *Context) Settings() *config.LintersSettings {
|
||||
return &c.Cfg.LintersSettings
|
||||
}
|
36
pkg/golinters/deadcode.go
Normal file
36
pkg/golinters/deadcode.go
Normal file
@ -0,0 +1,36 @@
|
||||
package golinters
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
deadcodeAPI "github.com/golangci/go-misc/deadcode"
|
||||
"github.com/golangci/golangci-lint/pkg/result"
|
||||
)
|
||||
|
||||
type Deadcode struct{}
|
||||
|
||||
func (Deadcode) Name() string {
|
||||
return "deadcode"
|
||||
}
|
||||
|
||||
func (Deadcode) Desc() string {
|
||||
return "Finds unused code"
|
||||
}
|
||||
|
||||
func (d Deadcode) Run(ctx context.Context, lintCtx *Context) ([]result.Issue, error) {
|
||||
issues, err := deadcodeAPI.Run(lintCtx.Program)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var res []result.Issue
|
||||
for _, i := range issues {
|
||||
res = append(res, result.Issue{
|
||||
Pos: i.Pos,
|
||||
Text: fmt.Sprintf("%s is unused", formatCode(i.UnusedIdentName, lintCtx.Cfg)),
|
||||
FromLinter: d.Name(),
|
||||
})
|
||||
}
|
||||
return res, nil
|
||||
}
|
48
pkg/golinters/dupl.go
Normal file
48
pkg/golinters/dupl.go
Normal file
@ -0,0 +1,48 @@
|
||||
package golinters
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"go/token"
|
||||
|
||||
"github.com/golangci/golangci-lint/pkg/result"
|
||||
duplAPI "github.com/mibk/dupl"
|
||||
)
|
||||
|
||||
type Dupl struct{}
|
||||
|
||||
func (Dupl) Name() string {
|
||||
return "dupl"
|
||||
}
|
||||
|
||||
func (Dupl) Desc() string {
|
||||
return "Tool for code clone detection"
|
||||
}
|
||||
|
||||
func (d Dupl) Run(ctx context.Context, lintCtx *Context) ([]result.Issue, error) {
|
||||
issues, err := duplAPI.Run(lintCtx.Paths.Files, lintCtx.Settings().Dupl.Threshold)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var res []result.Issue
|
||||
for _, i := range issues {
|
||||
dupl := fmt.Sprintf("%s:%d-%d", i.To.Filename(), i.To.LineStart(), i.To.LineEnd())
|
||||
text := fmt.Sprintf("%d-%d lines are duplicate of %s",
|
||||
i.From.LineStart(), i.From.LineEnd(),
|
||||
formatCode(dupl, lintCtx.Cfg))
|
||||
res = append(res, result.Issue{
|
||||
Pos: token.Position{
|
||||
Filename: i.From.Filename(),
|
||||
Line: i.From.LineStart(),
|
||||
},
|
||||
LineRange: result.Range{
|
||||
From: i.From.LineStart(),
|
||||
To: i.From.LineEnd(),
|
||||
},
|
||||
Text: text,
|
||||
FromLinter: d.Name(),
|
||||
})
|
||||
}
|
||||
return res, nil
|
||||
}
|
44
pkg/golinters/errcheck.go
Normal file
44
pkg/golinters/errcheck.go
Normal file
@ -0,0 +1,44 @@
|
||||
package golinters
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/golangci/golangci-lint/pkg/result"
|
||||
errcheckAPI "github.com/kisielk/errcheck/golangci"
|
||||
)
|
||||
|
||||
type Errcheck struct{}
|
||||
|
||||
func (Errcheck) Name() string {
|
||||
return "errcheck"
|
||||
}
|
||||
|
||||
func (Errcheck) Desc() string {
|
||||
return "Errcheck is a program for checking for unchecked errors in go programs. These unchecked errors can be critical bugs in some cases"
|
||||
}
|
||||
|
||||
func (e Errcheck) Run(ctx context.Context, lintCtx *Context) ([]result.Issue, error) {
|
||||
errCfg := &lintCtx.Settings().Errcheck
|
||||
issues, err := errcheckAPI.Run(lintCtx.Program, errCfg.CheckAssignToBlank, errCfg.CheckTypeAssertions)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var res []result.Issue
|
||||
for _, i := range issues {
|
||||
var text string
|
||||
if i.FuncName != "" {
|
||||
text = fmt.Sprintf("Error return value of %s is not checked", formatCode(i.FuncName, lintCtx.Cfg))
|
||||
} else {
|
||||
text = "Error return value is not checked"
|
||||
}
|
||||
res = append(res, result.Issue{
|
||||
FromLinter: e.Name(),
|
||||
Text: text,
|
||||
Pos: i.Pos,
|
||||
})
|
||||
}
|
||||
|
||||
return res, nil
|
||||
}
|
@ -1,60 +0,0 @@
|
||||
package golinters
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/golangci/golangci-lint/pkg/result"
|
||||
)
|
||||
|
||||
func TestErrcheckSimple(t *testing.T) {
|
||||
const source = `package p
|
||||
|
||||
func retErr() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func missedErrorCheck() {
|
||||
retErr()
|
||||
}
|
||||
`
|
||||
|
||||
ExpectIssues(t, errCheck, source, []result.Issue{NewIssue("errcheck", "Error return value is not checked", 8)})
|
||||
}
|
||||
|
||||
func TestErrcheckIgnoreClose(t *testing.T) {
|
||||
sources := []string{`package p
|
||||
|
||||
import "os"
|
||||
|
||||
func ok() error {
|
||||
f, err := os.Open("t.go")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
f.Close()
|
||||
return nil
|
||||
}
|
||||
`,
|
||||
`package p
|
||||
|
||||
import "net/http"
|
||||
|
||||
func f() {
|
||||
resp, err := http.Get("http://example.com/")
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
panic(resp)
|
||||
}
|
||||
`}
|
||||
|
||||
for _, source := range sources {
|
||||
ExpectIssues(t, errCheck, source, []result.Issue{})
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: add cases of non-compiling code
|
||||
// TODO: don't report issues if got more than 20 issues
|
62
pkg/golinters/gas.go
Normal file
62
pkg/golinters/gas.go
Normal file
@ -0,0 +1,62 @@
|
||||
package golinters
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"go/token"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"strconv"
|
||||
|
||||
"github.com/GoASTScanner/gas"
|
||||
"github.com/GoASTScanner/gas/rules"
|
||||
"github.com/golangci/golangci-lint/pkg/result"
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
type Gas struct{}
|
||||
|
||||
func (Gas) Name() string {
|
||||
return "gas"
|
||||
}
|
||||
|
||||
func (Gas) Desc() string {
|
||||
return "Inspects source code for security problems"
|
||||
}
|
||||
|
||||
func (lint Gas) Run(ctx context.Context, lintCtx *Context) ([]result.Issue, error) {
|
||||
gasConfig := gas.NewConfig()
|
||||
enabledRules := rules.Generate()
|
||||
logger := log.New(ioutil.Discard, "", 0)
|
||||
analyzer := gas.NewAnalyzer(gasConfig, logger)
|
||||
analyzer.LoadRules(enabledRules.Builders())
|
||||
|
||||
analyzer.ProcessProgram(lintCtx.Program)
|
||||
issues, _ := analyzer.Report()
|
||||
|
||||
var res []result.Issue
|
||||
for _, i := range issues {
|
||||
text := fmt.Sprintf("%s: %s", i.RuleID, i.What) // TODO: use severity and confidence
|
||||
var r result.Range
|
||||
line, err := strconv.Atoi(i.Line)
|
||||
if err != nil {
|
||||
if n, rerr := fmt.Sscanf(i.Line, "%d-%d", &r.From, &r.To); rerr != nil || n != 2 {
|
||||
logrus.Infof("Can't convert gas line number %q of %v to int: %s", i.Line, i, err)
|
||||
continue
|
||||
}
|
||||
line = r.From
|
||||
}
|
||||
|
||||
res = append(res, result.Issue{
|
||||
Pos: token.Position{
|
||||
Filename: i.File,
|
||||
Line: line,
|
||||
},
|
||||
Text: text,
|
||||
LineRange: r,
|
||||
FromLinter: lint.Name(),
|
||||
})
|
||||
}
|
||||
|
||||
return res, nil
|
||||
}
|
53
pkg/golinters/goconst.go
Normal file
53
pkg/golinters/goconst.go
Normal file
@ -0,0 +1,53 @@
|
||||
package golinters
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
goconstAPI "github.com/golangci/goconst"
|
||||
"github.com/golangci/golangci-lint/pkg/result"
|
||||
)
|
||||
|
||||
type Goconst struct{}
|
||||
|
||||
func (Goconst) Name() string {
|
||||
return "goconst"
|
||||
}
|
||||
|
||||
func (Goconst) Desc() string {
|
||||
return "Finds repeated strings that could be replaced by a constant"
|
||||
}
|
||||
|
||||
func (lint Goconst) Run(ctx context.Context, lintCtx *Context) ([]result.Issue, error) {
|
||||
var goconstIssues []goconstAPI.Issue
|
||||
// TODO: make it cross-package: pass package names inside goconst
|
||||
for _, files := range lintCtx.Paths.FilesGrouppedByDirs() {
|
||||
issues, err := goconstAPI.Run(files, true,
|
||||
lintCtx.Settings().Goconst.MinStringLen,
|
||||
lintCtx.Settings().Goconst.MinOccurrencesCount,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
goconstIssues = append(goconstIssues, issues...)
|
||||
}
|
||||
|
||||
var res []result.Issue
|
||||
for _, i := range goconstIssues {
|
||||
textBegin := fmt.Sprintf("string %s has %d occurrences", formatCode(i.Str, lintCtx.Cfg), i.OccurencesCount)
|
||||
var textEnd string
|
||||
if i.MatchingConst == "" {
|
||||
textEnd = ", make it a constant"
|
||||
} else {
|
||||
textEnd = fmt.Sprintf(", but such constant %s already exists", formatCode(i.MatchingConst, lintCtx.Cfg))
|
||||
}
|
||||
res = append(res, result.Issue{
|
||||
Pos: i.Pos,
|
||||
Text: textBegin + textEnd,
|
||||
FromLinter: lint.Name(),
|
||||
})
|
||||
}
|
||||
|
||||
return res, nil
|
||||
}
|
39
pkg/golinters/gocyclo.go
Normal file
39
pkg/golinters/gocyclo.go
Normal file
@ -0,0 +1,39 @@
|
||||
package golinters
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
gocycloAPI "github.com/golangci/gocyclo"
|
||||
"github.com/golangci/golangci-lint/pkg/result"
|
||||
)
|
||||
|
||||
type Gocyclo struct{}
|
||||
|
||||
func (Gocyclo) Name() string {
|
||||
return "gocyclo"
|
||||
}
|
||||
|
||||
func (Gocyclo) Desc() string {
|
||||
return "Computes and checks the cyclomatic complexity of functions"
|
||||
}
|
||||
|
||||
func (g Gocyclo) Run(ctx context.Context, lintCtx *Context) ([]result.Issue, error) {
|
||||
stats := gocycloAPI.Run(lintCtx.Paths.MixedPaths())
|
||||
|
||||
var res []result.Issue
|
||||
for _, s := range stats {
|
||||
if s.Complexity <= lintCtx.Settings().Gocyclo.MinComplexity {
|
||||
continue
|
||||
}
|
||||
|
||||
res = append(res, result.Issue{
|
||||
Pos: s.Pos,
|
||||
Text: fmt.Sprintf("cyclomatic complexity %d of func %s is high (> %d)",
|
||||
s.Complexity, formatCode(s.FuncName, lintCtx.Cfg), lintCtx.Settings().Gocyclo.MinComplexity),
|
||||
FromLinter: g.Name(),
|
||||
})
|
||||
}
|
||||
|
||||
return res, nil
|
||||
}
|
@ -4,43 +4,57 @@ import (
|
||||
"bytes"
|
||||
"context"
|
||||
"fmt"
|
||||
"go/token"
|
||||
|
||||
gofmtAPI "github.com/golangci/gofmt/gofmt"
|
||||
goimportsAPI "github.com/golangci/gofmt/goimports"
|
||||
"github.com/golangci/golangci-lint/pkg/result"
|
||||
"github.com/golangci/golangci-shared/pkg/analytics"
|
||||
"github.com/golangci/golangci-shared/pkg/executors"
|
||||
"github.com/sirupsen/logrus"
|
||||
"sourcegraph.com/sourcegraph/go-diff/diff"
|
||||
)
|
||||
|
||||
type gofmt struct {
|
||||
useGoimports bool
|
||||
type Gofmt struct {
|
||||
UseGoimports bool
|
||||
}
|
||||
|
||||
func (g gofmt) Name() string {
|
||||
if g.useGoimports {
|
||||
func (g Gofmt) Name() string {
|
||||
if g.UseGoimports {
|
||||
return "goimports"
|
||||
}
|
||||
|
||||
return "gofmt"
|
||||
}
|
||||
|
||||
func getFirstDeletedLineNumberInHunk(h *diff.Hunk) (int, error) {
|
||||
func (g Gofmt) Desc() string {
|
||||
if g.UseGoimports {
|
||||
return "Goimports does everything that gofmt does. Additionally it checks unused imports"
|
||||
}
|
||||
|
||||
return "Gofmt checks whether code was gofmt-ed. By default this tool runs with -s option to check for code simplification"
|
||||
}
|
||||
|
||||
func getFirstDeletedAndAddedLineNumberInHunk(h *diff.Hunk) (int, int, error) {
|
||||
lines := bytes.Split(h.Body, []byte{'\n'})
|
||||
lineNumber := int(h.OrigStartLine - 1)
|
||||
firstAddedLineNumber := -1
|
||||
for _, line := range lines {
|
||||
lineNumber++
|
||||
|
||||
if len(line) == 0 {
|
||||
continue
|
||||
}
|
||||
if line[0] == '+' && firstAddedLineNumber == -1 {
|
||||
firstAddedLineNumber = lineNumber
|
||||
}
|
||||
if line[0] == '-' {
|
||||
return lineNumber, nil
|
||||
return lineNumber, firstAddedLineNumber, nil
|
||||
}
|
||||
}
|
||||
|
||||
return 0, fmt.Errorf("didn't find deletion line in hunk %s", string(h.Body))
|
||||
return 0, firstAddedLineNumber, fmt.Errorf("didn't find deletion line in hunk %s", string(h.Body))
|
||||
}
|
||||
|
||||
func (g gofmt) extractIssuesFromPatch(patch string) ([]result.Issue, error) {
|
||||
func (g Gofmt) extractIssuesFromPatch(patch string) ([]result.Issue, error) {
|
||||
diffs, err := diff.ParseMultiFileDiff([]byte(patch))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("can't parse patch: %s", err)
|
||||
@ -53,26 +67,32 @@ func (g gofmt) extractIssuesFromPatch(patch string) ([]result.Issue, error) {
|
||||
issues := []result.Issue{}
|
||||
for _, d := range diffs {
|
||||
if len(d.Hunks) == 0 {
|
||||
analytics.Log(context.TODO()).Warnf("Got no hunks in diff %+v", d)
|
||||
logrus.Warnf("Got no hunks in diff %+v", d)
|
||||
continue
|
||||
}
|
||||
|
||||
for _, hunk := range d.Hunks {
|
||||
lineNumber, err := getFirstDeletedLineNumberInHunk(hunk)
|
||||
deletedLine, addedLine, err := getFirstDeletedAndAddedLineNumberInHunk(hunk)
|
||||
if err != nil {
|
||||
analytics.Log(context.TODO()).Infof("Can't get first deleted line number for hunk: %s", err)
|
||||
lineNumber = int(hunk.OrigStartLine) // use first line if no deletions:
|
||||
logrus.Infof("Can't get first deleted line number for hunk: %s", err)
|
||||
if addedLine > 1 {
|
||||
deletedLine = addedLine - 1 // use previous line, TODO: use both prev and next lines
|
||||
} else {
|
||||
deletedLine = 1
|
||||
}
|
||||
}
|
||||
|
||||
text := "File is not gofmt-ed with -s"
|
||||
if g.useGoimports {
|
||||
if g.UseGoimports {
|
||||
text = "File is not goimports-ed"
|
||||
}
|
||||
i := result.Issue{
|
||||
FromLinter: g.Name(),
|
||||
File: d.NewName,
|
||||
LineNumber: lineNumber,
|
||||
Text: text,
|
||||
Pos: token.Position{
|
||||
Filename: d.NewName,
|
||||
Line: deletedLine,
|
||||
},
|
||||
Text: text,
|
||||
}
|
||||
issues = append(issues, i)
|
||||
}
|
||||
@ -81,35 +101,31 @@ func (g gofmt) extractIssuesFromPatch(patch string) ([]result.Issue, error) {
|
||||
return issues, nil
|
||||
}
|
||||
|
||||
func (g gofmt) Run(ctx context.Context, exec executors.Executor) (*result.Result, error) {
|
||||
paths, err := getPathsForGoProject(exec.WorkDir())
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("can't get files to analyze: %s", err)
|
||||
func (g Gofmt) Run(ctx context.Context, lintCtx *Context) ([]result.Issue, error) {
|
||||
var issues []result.Issue
|
||||
|
||||
for _, f := range lintCtx.Paths.Files {
|
||||
var diff []byte
|
||||
var err error
|
||||
if g.UseGoimports {
|
||||
diff, err = goimportsAPI.Run(f)
|
||||
} else {
|
||||
diff, err = gofmtAPI.Run(f, lintCtx.Settings().Gofmt.Simplify)
|
||||
}
|
||||
if err != nil { // TODO: skip
|
||||
return nil, err
|
||||
}
|
||||
if diff == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
is, err := g.extractIssuesFromPatch(string(diff))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("can't extract issues from gofmt diff output %q: %s", string(diff), err)
|
||||
}
|
||||
|
||||
issues = append(issues, is...)
|
||||
}
|
||||
|
||||
args := []string{"-d"}
|
||||
if !g.useGoimports {
|
||||
args = append(args, "-s")
|
||||
}
|
||||
args = append(args, paths.files...)
|
||||
out, err := exec.Run(ctx, g.Name(), args...)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("can't run gofmt: %s, %s", err, out)
|
||||
}
|
||||
|
||||
if len(out) == 0 { // no diff => no issues
|
||||
return &result.Result{
|
||||
Issues: []result.Issue{},
|
||||
}, nil
|
||||
}
|
||||
|
||||
issues, err := g.extractIssuesFromPatch(out)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("can't extract issues from gofmt diff output %q: %s", out, err)
|
||||
}
|
||||
|
||||
return &result.Result{
|
||||
Issues: issues,
|
||||
MaxIssuesPerFile: 1, // don't disturb user: show just first changed not gofmt-ed line
|
||||
}, nil
|
||||
return issues, nil
|
||||
}
|
||||
|
@ -1,50 +0,0 @@
|
||||
package golinters
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/golangci/golangci-lint/pkg/result"
|
||||
)
|
||||
|
||||
func TestGofmtIssueFound(t *testing.T) {
|
||||
const source = `package p
|
||||
|
||||
func noFmt() error {
|
||||
return nil
|
||||
}
|
||||
`
|
||||
|
||||
ExpectIssues(t, gofmt{}, source, []result.Issue{NewIssue("gofmt", "File is not gofmt-ed with -s", 4)})
|
||||
}
|
||||
|
||||
func TestGofmtNoIssue(t *testing.T) {
|
||||
const source = `package p
|
||||
|
||||
func fmted() error {
|
||||
return nil
|
||||
}
|
||||
`
|
||||
|
||||
ExpectIssues(t, gofmt{}, source, []result.Issue{})
|
||||
}
|
||||
|
||||
func TestGoimportsIssueFound(t *testing.T) {
|
||||
const source = `package p
|
||||
func noFmt() error {return nil}
|
||||
`
|
||||
|
||||
lint := gofmt{useGoimports: true}
|
||||
ExpectIssues(t, lint, source, []result.Issue{NewIssue("goimports", "File is not goimports-ed", 2)})
|
||||
}
|
||||
|
||||
func TestGoimportsNoIssue(t *testing.T) {
|
||||
const source = `package p
|
||||
|
||||
func fmted() error {
|
||||
return nil
|
||||
}
|
||||
`
|
||||
|
||||
lint := gofmt{useGoimports: true}
|
||||
ExpectIssues(t, lint, source, []result.Issue{})
|
||||
}
|
65
pkg/golinters/golint.go
Normal file
65
pkg/golinters/golint.go
Normal file
@ -0,0 +1,65 @@
|
||||
package golinters
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
|
||||
"github.com/golang/lint"
|
||||
"github.com/golangci/golangci-lint/pkg/result"
|
||||
)
|
||||
|
||||
type Golint struct{}
|
||||
|
||||
func (Golint) Name() string {
|
||||
return "golint"
|
||||
}
|
||||
|
||||
func (Golint) Desc() string {
|
||||
return "Golint differs from gofmt. Gofmt reformats Go source code, whereas golint prints out style mistakes"
|
||||
}
|
||||
|
||||
func (g Golint) Run(ctx context.Context, lintCtx *Context) ([]result.Issue, error) {
|
||||
var issues []result.Issue
|
||||
for _, pkgFiles := range lintCtx.Paths.FilesGrouppedByDirs() {
|
||||
i, err := g.lintFiles(lintCtx.Settings().Golint.MinConfidence, pkgFiles...)
|
||||
if err != nil {
|
||||
// TODO: skip and warn
|
||||
return nil, fmt.Errorf("can't lint files %s: %s", lintCtx.Paths.Files, err)
|
||||
}
|
||||
issues = append(issues, i...)
|
||||
}
|
||||
|
||||
return issues, nil
|
||||
}
|
||||
|
||||
func (g Golint) lintFiles(minConfidence float64, filenames ...string) ([]result.Issue, error) {
|
||||
files := make(map[string][]byte)
|
||||
for _, filename := range filenames {
|
||||
src, err := ioutil.ReadFile(filename)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("can't read file %s: %s", filename, err)
|
||||
}
|
||||
files[filename] = src
|
||||
}
|
||||
|
||||
l := new(lint.Linter)
|
||||
ps, err := l.LintFiles(files)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("can't lint files %s: %s", filenames, err)
|
||||
}
|
||||
|
||||
var issues []result.Issue
|
||||
for _, p := range ps {
|
||||
if p.Confidence >= minConfidence {
|
||||
issues = append(issues, result.Issue{
|
||||
Pos: p.Position,
|
||||
Text: p.Text,
|
||||
FromLinter: g.Name(),
|
||||
})
|
||||
// TODO: use p.Link and p.Category
|
||||
}
|
||||
}
|
||||
|
||||
return issues, nil
|
||||
}
|
@ -1,15 +0,0 @@
|
||||
package golinters
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/golangci/golangci-lint/pkg/result"
|
||||
)
|
||||
|
||||
func TestGolintSimple(t *testing.T) {
|
||||
const source = `package p
|
||||
var v_1 string`
|
||||
|
||||
ExpectIssues(t, golint, source,
|
||||
[]result.Issue{NewIssue("golint", "don't use underscores in Go names; var v_1 should be v1", 2)})
|
||||
}
|
40
pkg/golinters/govet.go
Normal file
40
pkg/golinters/govet.go
Normal file
@ -0,0 +1,40 @@
|
||||
package golinters
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/golangci/golangci-lint/pkg/result"
|
||||
govetAPI "github.com/golangci/govet"
|
||||
)
|
||||
|
||||
type Govet struct{}
|
||||
|
||||
func (Govet) Name() string {
|
||||
return "govet"
|
||||
}
|
||||
|
||||
func (Govet) Desc() string {
|
||||
return "Vet examines Go source code and reports suspicious constructs, such as Printf calls whose arguments do not align with the format string"
|
||||
}
|
||||
|
||||
func (g Govet) Run(ctx context.Context, lintCtx *Context) ([]result.Issue, error) {
|
||||
// TODO: check .S asm files: govet can do it if pass dirs
|
||||
var govetIssues []govetAPI.Issue
|
||||
for _, files := range lintCtx.Paths.FilesGrouppedByDirs() {
|
||||
issues, err := govetAPI.Run(files, lintCtx.Settings().Govet.CheckShadowing)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
govetIssues = append(govetIssues, issues...)
|
||||
}
|
||||
|
||||
var res []result.Issue
|
||||
for _, i := range govetIssues {
|
||||
res = append(res, result.Issue{
|
||||
Pos: i.Pos,
|
||||
Text: i.Message,
|
||||
FromLinter: g.Name(),
|
||||
})
|
||||
}
|
||||
return res, nil
|
||||
}
|
@ -1,22 +0,0 @@
|
||||
package golinters
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/golangci/golangci-lint/pkg/result"
|
||||
)
|
||||
|
||||
func TestGovetSimple(t *testing.T) {
|
||||
const source = `package p
|
||||
|
||||
import "os"
|
||||
|
||||
func f() error {
|
||||
return &os.PathError{"first", "path", os.ErrNotExist}
|
||||
}
|
||||
`
|
||||
|
||||
ExpectIssues(t, govet, source, []result.Issue{
|
||||
NewIssue("govet", "os.PathError composite literal uses unkeyed fields", 6),
|
||||
})
|
||||
}
|
33
pkg/golinters/ineffassign.go
Normal file
33
pkg/golinters/ineffassign.go
Normal file
@ -0,0 +1,33 @@
|
||||
package golinters
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/golangci/golangci-lint/pkg/result"
|
||||
ineffassignAPI "github.com/golangci/ineffassign"
|
||||
)
|
||||
|
||||
type Ineffassign struct{}
|
||||
|
||||
func (Ineffassign) Name() string {
|
||||
return "ineffassign"
|
||||
}
|
||||
|
||||
func (Ineffassign) Desc() string {
|
||||
return "Detects when assignments to existing variables are not used"
|
||||
}
|
||||
|
||||
func (lint Ineffassign) Run(ctx context.Context, lintCtx *Context) ([]result.Issue, error) {
|
||||
issues := ineffassignAPI.Run(lintCtx.Paths.Files)
|
||||
|
||||
var res []result.Issue
|
||||
for _, i := range issues {
|
||||
res = append(res, result.Issue{
|
||||
Pos: i.Pos,
|
||||
Text: fmt.Sprintf("ineffectual assignment to %s", formatCode(i.IdentName, lintCtx.Cfg)),
|
||||
FromLinter: lint.Name(),
|
||||
})
|
||||
}
|
||||
return res, nil
|
||||
}
|
42
pkg/golinters/interfacer.go
Normal file
42
pkg/golinters/interfacer.go
Normal file
@ -0,0 +1,42 @@
|
||||
package golinters
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"mvdan.cc/interfacer/check"
|
||||
|
||||
"github.com/golangci/golangci-lint/pkg/result"
|
||||
)
|
||||
|
||||
type Interfacer struct{}
|
||||
|
||||
func (Interfacer) Name() string {
|
||||
return "interfacer"
|
||||
}
|
||||
|
||||
func (Interfacer) Desc() string {
|
||||
return "Linter that suggests narrower interface types"
|
||||
}
|
||||
|
||||
func (lint Interfacer) Run(ctx context.Context, lintCtx *Context) ([]result.Issue, error) {
|
||||
c := new(check.Checker)
|
||||
c.Program(lintCtx.Program)
|
||||
c.ProgramSSA(lintCtx.SSAProgram)
|
||||
|
||||
issues, err := c.Check()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var res []result.Issue
|
||||
for _, i := range issues {
|
||||
pos := lintCtx.SSAProgram.Fset.Position(i.Pos())
|
||||
res = append(res, result.Issue{
|
||||
Pos: pos,
|
||||
Text: i.Message(),
|
||||
FromLinter: lint.Name(),
|
||||
})
|
||||
}
|
||||
|
||||
return res, nil
|
||||
}
|
@ -1,199 +0,0 @@
|
||||
package golinters
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"context"
|
||||
"fmt"
|
||||
"os/exec"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
"syscall"
|
||||
"text/template"
|
||||
|
||||
"github.com/golangci/golangci-lint/pkg/result"
|
||||
"github.com/golangci/golangci-shared/pkg/analytics"
|
||||
"github.com/golangci/golangci-shared/pkg/executors"
|
||||
)
|
||||
|
||||
type linterConfig struct {
|
||||
messageTemplate *template.Template
|
||||
pattern *regexp.Regexp
|
||||
excludeByMessagePattern *regexp.Regexp
|
||||
args []string
|
||||
issuesFoundExitCode int
|
||||
}
|
||||
|
||||
func newLinterConfig(messageTemplate, pattern, excludeByMessagePattern string, args ...string) *linterConfig {
|
||||
if messageTemplate == "" {
|
||||
messageTemplate = "{{.message}}"
|
||||
}
|
||||
|
||||
var excludeByMessagePatternRe *regexp.Regexp
|
||||
if excludeByMessagePattern != "" {
|
||||
excludeByMessagePatternRe = regexp.MustCompile(excludeByMessagePattern)
|
||||
}
|
||||
|
||||
return &linterConfig{
|
||||
messageTemplate: template.Must(template.New("message").Parse(messageTemplate)),
|
||||
pattern: regexp.MustCompile(pattern),
|
||||
excludeByMessagePattern: excludeByMessagePatternRe,
|
||||
args: args,
|
||||
issuesFoundExitCode: 1,
|
||||
}
|
||||
}
|
||||
|
||||
type linter struct {
|
||||
name string
|
||||
|
||||
linterConfig
|
||||
}
|
||||
|
||||
func newLinter(name string, cfg *linterConfig) *linter {
|
||||
return &linter{
|
||||
name: name,
|
||||
linterConfig: *cfg,
|
||||
}
|
||||
}
|
||||
|
||||
func (lint linter) Name() string {
|
||||
return lint.name
|
||||
}
|
||||
|
||||
func (lint linter) doesExitCodeMeansIssuesWereFound(err error) bool {
|
||||
ee, ok := err.(*exec.ExitError)
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
|
||||
status, ok := ee.Sys().(syscall.WaitStatus)
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
|
||||
exitCode := status.ExitStatus()
|
||||
return exitCode == lint.issuesFoundExitCode
|
||||
}
|
||||
|
||||
func getOutTail(out string, linesCount int) string {
|
||||
lines := strings.Split(out, "\n")
|
||||
if len(lines) <= linesCount {
|
||||
return out
|
||||
}
|
||||
|
||||
return strings.Join(lines[len(lines)-linesCount:], "\n")
|
||||
}
|
||||
|
||||
func (lint linter) Run(ctx context.Context, exec executors.Executor) (*result.Result, error) {
|
||||
paths, err := getPathsForGoProject(exec.WorkDir())
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("can't get files to analyze: %s", err)
|
||||
}
|
||||
|
||||
retIssues := []result.Issue{}
|
||||
|
||||
const maxDirsPerRun = 100 // run one linter multiple times with groups of dirs: limit memory usage in the cost of higher CPU usage
|
||||
|
||||
for len(paths.dirs) != 0 {
|
||||
args := append([]string{}, lint.args...)
|
||||
|
||||
dirsCount := len(paths.dirs)
|
||||
if dirsCount > maxDirsPerRun {
|
||||
dirsCount = maxDirsPerRun
|
||||
}
|
||||
dirs := paths.dirs[:dirsCount]
|
||||
args = append(args, dirs...)
|
||||
|
||||
out, err := exec.Run(ctx, lint.name, args...)
|
||||
if err != nil && !lint.doesExitCodeMeansIssuesWereFound(err) {
|
||||
out = getOutTail(out, 10)
|
||||
return nil, fmt.Errorf("can't run linter %s with args %v: %s, %s", lint.name, lint.args, err, out)
|
||||
}
|
||||
|
||||
issues := lint.parseLinterOut(out)
|
||||
retIssues = append(retIssues, issues...)
|
||||
|
||||
paths.dirs = paths.dirs[dirsCount:]
|
||||
}
|
||||
|
||||
return &result.Result{
|
||||
Issues: retIssues,
|
||||
}, nil
|
||||
}
|
||||
|
||||
type regexpVars map[string]string
|
||||
|
||||
func buildMatchedRegexpVars(match []string, pattern *regexp.Regexp) regexpVars {
|
||||
result := regexpVars{}
|
||||
for i, name := range pattern.SubexpNames() {
|
||||
if i != 0 && name != "" {
|
||||
result[name] = match[i]
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func (lint linter) parseLinterOutLine(line string) (regexpVars, error) {
|
||||
match := lint.pattern.FindStringSubmatch(line)
|
||||
if match == nil {
|
||||
return nil, fmt.Errorf("can't match line %q against regexp", line)
|
||||
}
|
||||
|
||||
return buildMatchedRegexpVars(match, lint.pattern), nil
|
||||
}
|
||||
|
||||
func (lint linter) makeIssue(vars regexpVars) (*result.Issue, error) {
|
||||
var messageBuffer bytes.Buffer
|
||||
err := lint.messageTemplate.Execute(&messageBuffer, vars)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("can't execute message template: %s", err)
|
||||
}
|
||||
|
||||
if vars["path"] == "" {
|
||||
return nil, fmt.Errorf("no path in vars %+v", vars)
|
||||
}
|
||||
|
||||
var line int
|
||||
if vars["line"] != "" {
|
||||
line, err = strconv.Atoi(vars["line"])
|
||||
if err != nil {
|
||||
analytics.Log(context.TODO()).Warnf("Can't parse line %q: %s", vars["line"], err)
|
||||
}
|
||||
}
|
||||
|
||||
return &result.Issue{
|
||||
FromLinter: lint.name,
|
||||
File: vars["path"],
|
||||
LineNumber: line,
|
||||
Text: messageBuffer.String(),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (lint linter) parseLinterOut(out string) []result.Issue {
|
||||
issues := []result.Issue{}
|
||||
scanner := bufio.NewScanner(strings.NewReader(out))
|
||||
for scanner.Scan() {
|
||||
vars, err := lint.parseLinterOutLine(scanner.Text())
|
||||
if err != nil {
|
||||
analytics.Log(context.TODO()).Warnf("Can't parse linter out line: %s", err)
|
||||
continue
|
||||
}
|
||||
|
||||
message := vars["message"]
|
||||
ex := lint.excludeByMessagePattern
|
||||
if message != "" && ex != nil && ex.MatchString(message) {
|
||||
continue
|
||||
}
|
||||
|
||||
issue, err := lint.makeIssue(vars)
|
||||
if err != nil {
|
||||
analytics.Log(context.TODO()).Warnf("Can't make issue: %s", err)
|
||||
continue
|
||||
}
|
||||
|
||||
issues = append(issues, *issue)
|
||||
}
|
||||
|
||||
return issues
|
||||
}
|
37
pkg/golinters/maligned.go
Normal file
37
pkg/golinters/maligned.go
Normal file
@ -0,0 +1,37 @@
|
||||
package golinters
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/golangci/golangci-lint/pkg/result"
|
||||
malignedAPI "github.com/golangci/maligned"
|
||||
)
|
||||
|
||||
type Maligned struct{}
|
||||
|
||||
func (Maligned) Name() string {
|
||||
return "maligned"
|
||||
}
|
||||
|
||||
func (Maligned) Desc() string {
|
||||
return "Tool to detect Go structs that would take less memory if their fields were sorted"
|
||||
}
|
||||
|
||||
func (m Maligned) Run(ctx context.Context, lintCtx *Context) ([]result.Issue, error) {
|
||||
issues := malignedAPI.Run(lintCtx.Program)
|
||||
|
||||
var res []result.Issue
|
||||
for _, i := range issues {
|
||||
text := fmt.Sprintf("struct of size %d bytes could be of size %d bytes", i.OldSize, i.NewSize)
|
||||
if lintCtx.Settings().Maligned.SuggestNewOrder {
|
||||
text += fmt.Sprintf(":\n%s", formatCodeBlock(i.NewStructDef, lintCtx.Cfg))
|
||||
}
|
||||
res = append(res, result.Issue{
|
||||
Pos: i.Pos,
|
||||
Text: text,
|
||||
FromLinter: m.Name(),
|
||||
})
|
||||
}
|
||||
return res, nil
|
||||
}
|
54
pkg/golinters/megacheck.go
Normal file
54
pkg/golinters/megacheck.go
Normal file
@ -0,0 +1,54 @@
|
||||
package golinters
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
megacheckAPI "github.com/golangci/go-tools/cmd/megacheck"
|
||||
"github.com/golangci/golangci-lint/pkg/result"
|
||||
)
|
||||
|
||||
type Megacheck struct {
|
||||
UnusedEnabled bool
|
||||
GosimpleEnabled bool
|
||||
StaticcheckEnabled bool
|
||||
}
|
||||
|
||||
func (m Megacheck) Name() string {
|
||||
if m.UnusedEnabled && !m.GosimpleEnabled && !m.StaticcheckEnabled {
|
||||
return "unused"
|
||||
}
|
||||
if m.GosimpleEnabled && !m.UnusedEnabled && !m.StaticcheckEnabled {
|
||||
return "gosimple"
|
||||
}
|
||||
if m.StaticcheckEnabled && !m.UnusedEnabled && !m.GosimpleEnabled {
|
||||
return "staticcheck"
|
||||
}
|
||||
|
||||
return "megacheck" // all enabled
|
||||
}
|
||||
|
||||
func (m Megacheck) Desc() string {
|
||||
descs := map[string]string{
|
||||
"unused": "Checks Go code for unused constants, variables, functions and types",
|
||||
"gosimple": "Linter for Go source code that specialises on simplifying code",
|
||||
"staticcheck": "Staticcheck is go vet on steroids, applying a ton of static analysis checks",
|
||||
"megacheck": "3 sub-linters in one: unused, gosimple and staticcheck",
|
||||
}
|
||||
|
||||
return descs[m.Name()]
|
||||
}
|
||||
|
||||
func (m Megacheck) Run(ctx context.Context, lintCtx *Context) ([]result.Issue, error) {
|
||||
issues := megacheckAPI.Run(lintCtx.Program, lintCtx.LoaderConfig, lintCtx.SSAProgram,
|
||||
m.StaticcheckEnabled, m.GosimpleEnabled, m.UnusedEnabled)
|
||||
|
||||
var res []result.Issue
|
||||
for _, i := range issues {
|
||||
res = append(res, result.Issue{
|
||||
Pos: i.Position,
|
||||
Text: i.Text,
|
||||
FromLinter: m.Name(),
|
||||
})
|
||||
}
|
||||
return res, nil
|
||||
}
|
33
pkg/golinters/structcheck.go
Normal file
33
pkg/golinters/structcheck.go
Normal file
@ -0,0 +1,33 @@
|
||||
package golinters // nolint:dupl
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
structcheckAPI "github.com/golangci/check/cmd/structcheck"
|
||||
"github.com/golangci/golangci-lint/pkg/result"
|
||||
)
|
||||
|
||||
type Structcheck struct{}
|
||||
|
||||
func (Structcheck) Name() string {
|
||||
return "structcheck"
|
||||
}
|
||||
|
||||
func (Structcheck) Desc() string {
|
||||
return "Finds unused struct fields"
|
||||
}
|
||||
|
||||
func (s Structcheck) Run(ctx context.Context, lintCtx *Context) ([]result.Issue, error) {
|
||||
issues := structcheckAPI.Run(lintCtx.Program, lintCtx.Settings().Structcheck.CheckExportedFields)
|
||||
|
||||
var res []result.Issue
|
||||
for _, i := range issues {
|
||||
res = append(res, result.Issue{
|
||||
Pos: i.Pos,
|
||||
Text: fmt.Sprintf("%s is unused", formatCode(i.FieldName, lintCtx.Cfg)),
|
||||
FromLinter: s.Name(),
|
||||
})
|
||||
}
|
||||
return res, nil
|
||||
}
|
@ -1,21 +0,0 @@
|
||||
package golinters
|
||||
|
||||
import "github.com/golangci/golangci-lint/pkg"
|
||||
|
||||
const pathLineColMessage = `^(?P<path>.*?\.go):(?P<line>\d+):(?P<col>\d+):\s*(?P<message>.*)$`
|
||||
const pathLineMessage = `^(?P<path>.*?\.go):(?P<line>\d+):\s*(?P<message>.*)$`
|
||||
|
||||
var errCheck = newLinter("errcheck",
|
||||
newLinterConfig(
|
||||
"Error return value is not checked",
|
||||
pathLineColMessage,
|
||||
"\\.Close()", // It's annoying and not critical error to ignore Close() errors),
|
||||
),
|
||||
)
|
||||
|
||||
var golint = newLinter("golint", newLinterConfig("", pathLineColMessage, ""))
|
||||
var govet = newLinter("govet", newLinterConfig("", pathLineMessage, "", "--no-recurse"))
|
||||
|
||||
func GetSupportedLinters() []linters.Linter {
|
||||
return []linters.Linter{gofmt{}, gofmt{useGoimports: true}, golint, govet}
|
||||
}
|
@ -1,39 +0,0 @@
|
||||
package golinters
|
||||
|
||||
import (
|
||||
"context"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path"
|
||||
"testing"
|
||||
|
||||
"github.com/golangci/golangci-lint/pkg"
|
||||
"github.com/golangci/golangci-lint/pkg/result"
|
||||
"github.com/golangci/golangci-shared/pkg/executors"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func NewIssue(linter, message string, line int) result.Issue {
|
||||
return result.Issue{
|
||||
FromLinter: linter,
|
||||
Text: message,
|
||||
File: "p/f.go",
|
||||
LineNumber: line,
|
||||
}
|
||||
}
|
||||
|
||||
func ExpectIssues(t *testing.T, linter linters.Linter, source string, issues []result.Issue) {
|
||||
exec, err := executors.NewTempDirShell("test.expectissues")
|
||||
assert.NoError(t, err)
|
||||
defer exec.Clean()
|
||||
|
||||
subDir := path.Join(exec.WorkDir(), "p")
|
||||
assert.NoError(t, os.Mkdir(subDir, os.ModePerm))
|
||||
err = ioutil.WriteFile(path.Join(subDir, "f.go"), []byte(source), os.ModePerm)
|
||||
assert.NoError(t, err)
|
||||
|
||||
res, err := linter.Run(context.Background(), exec)
|
||||
assert.NoError(t, err)
|
||||
|
||||
assert.Equal(t, issues, res.Issues)
|
||||
}
|
32
pkg/golinters/unconvert.go
Normal file
32
pkg/golinters/unconvert.go
Normal file
@ -0,0 +1,32 @@
|
||||
package golinters
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/golangci/golangci-lint/pkg/result"
|
||||
unconvertAPI "github.com/golangci/unconvert"
|
||||
)
|
||||
|
||||
type Unconvert struct{}
|
||||
|
||||
func (Unconvert) Name() string {
|
||||
return "unconvert"
|
||||
}
|
||||
|
||||
func (Unconvert) Desc() string {
|
||||
return "Remove unnecessary type conversions"
|
||||
}
|
||||
|
||||
func (lint Unconvert) Run(ctx context.Context, lintCtx *Context) ([]result.Issue, error) {
|
||||
positions := unconvertAPI.Run(lintCtx.Program)
|
||||
var res []result.Issue
|
||||
for _, pos := range positions {
|
||||
res = append(res, result.Issue{
|
||||
Pos: pos,
|
||||
Text: "unnecessary conversion",
|
||||
FromLinter: lint.Name(),
|
||||
})
|
||||
}
|
||||
|
||||
return res, nil
|
||||
}
|
@ -1,68 +1,24 @@
|
||||
package golinters
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"log"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/golangci/golangci-lint/pkg/fsutils"
|
||||
"github.com/golangci/golangci-shared/pkg/analytics"
|
||||
"github.com/golangci/golangci-lint/pkg/config"
|
||||
)
|
||||
|
||||
type ProjectPaths struct {
|
||||
files []string
|
||||
dirs []string
|
||||
func formatCode(code string, cfg *config.Config) string {
|
||||
if strings.Contains(code, "`") {
|
||||
return code // TODO: properly escape or remove
|
||||
}
|
||||
|
||||
return fmt.Sprintf("`%s`", code)
|
||||
}
|
||||
|
||||
func processPaths(root string, paths []string, maxPaths int) ([]string, error) {
|
||||
if len(paths) >= maxPaths {
|
||||
analytics.Log(context.TODO()).Warnf("Gofmt: got too much paths (%d), analyze first %d", len(paths), maxPaths)
|
||||
paths = paths[:maxPaths]
|
||||
func formatCodeBlock(code string, cfg *config.Config) string {
|
||||
if strings.Contains(code, "`") {
|
||||
return code // TODO: properly escape or remove
|
||||
}
|
||||
|
||||
ret := []string{}
|
||||
for i := range paths {
|
||||
relPath, err := filepath.Rel(root, paths[i])
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("can't get relative path for path %s and root %s: %s",
|
||||
paths[i], root, err)
|
||||
}
|
||||
ret = append(ret, relPath)
|
||||
}
|
||||
|
||||
return ret, nil
|
||||
}
|
||||
|
||||
func getPathsForGoProject(root string) (*ProjectPaths, error) {
|
||||
excludeDirs := []string{"vendor", "testdata", "examples", "Godeps"}
|
||||
pr := fsutils.NewPathResolver(excludeDirs, []string{".go"})
|
||||
log.Printf("root is %q, paths are %q", root, path.Join(root, "..."))
|
||||
paths, err := pr.Resolve(path.Join(root, "..."))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("can't resolve paths: %s", err)
|
||||
}
|
||||
|
||||
files, err := processPaths(root, paths.Files(), 10000)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("can't process resolved files: %s", err)
|
||||
}
|
||||
|
||||
dirs, err := processPaths(root, paths.Dirs(), 1000)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("can't process resolved dirs: %s", err)
|
||||
}
|
||||
|
||||
for i := range dirs {
|
||||
dir := dirs[i]
|
||||
if dir != "." {
|
||||
dirs[i] = "./" + dir
|
||||
}
|
||||
}
|
||||
|
||||
return &ProjectPaths{
|
||||
files: files,
|
||||
dirs: dirs,
|
||||
}, nil
|
||||
return fmt.Sprintf("```\n%s\n```", code)
|
||||
}
|
||||
|
33
pkg/golinters/varcheck.go
Normal file
33
pkg/golinters/varcheck.go
Normal file
@ -0,0 +1,33 @@
|
||||
package golinters // nolint:dupl
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
varcheckAPI "github.com/golangci/check/cmd/varcheck"
|
||||
"github.com/golangci/golangci-lint/pkg/result"
|
||||
)
|
||||
|
||||
type Varcheck struct{}
|
||||
|
||||
func (Varcheck) Name() string {
|
||||
return "varcheck"
|
||||
}
|
||||
|
||||
func (Varcheck) Desc() string {
|
||||
return "Finds unused global variables and constants"
|
||||
}
|
||||
|
||||
func (v Varcheck) Run(ctx context.Context, lintCtx *Context) ([]result.Issue, error) {
|
||||
issues := varcheckAPI.Run(lintCtx.Program, lintCtx.Settings().Varcheck.CheckExportedFields)
|
||||
|
||||
var res []result.Issue
|
||||
for _, i := range issues {
|
||||
res = append(res, result.Issue{
|
||||
Pos: i.Pos,
|
||||
Text: fmt.Sprintf("%s is unused", formatCode(i.VarName, lintCtx.Cfg)),
|
||||
FromLinter: v.Name(),
|
||||
})
|
||||
}
|
||||
return res, nil
|
||||
}
|
@ -1,13 +1,14 @@
|
||||
package linters
|
||||
package pkg
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/golangci/golangci-lint/pkg/golinters"
|
||||
"github.com/golangci/golangci-lint/pkg/result"
|
||||
"github.com/golangci/golangci-shared/pkg/executors"
|
||||
)
|
||||
|
||||
type Linter interface {
|
||||
Run(ctx context.Context, exec executors.Executor) (*result.Result, error)
|
||||
Run(ctx context.Context, lintCtx *golinters.Context) ([]result.Issue, error)
|
||||
Name() string
|
||||
Desc() string
|
||||
}
|
||||
|
@ -1,61 +0,0 @@
|
||||
// Code generated by MockGen. DO NOT EDIT.
|
||||
// Source: ./app/analyze/linters/linter.go
|
||||
|
||||
package linters
|
||||
|
||||
import (
|
||||
context "context"
|
||||
reflect "reflect"
|
||||
|
||||
gomock "github.com/golang/mock/gomock"
|
||||
result "github.com/golangci/golangci-lint/pkg/result"
|
||||
executors "github.com/golangci/golangci-shared/pkg/executors"
|
||||
)
|
||||
|
||||
// MockLinter is a mock of Linter interface
|
||||
type MockLinter struct {
|
||||
ctrl *gomock.Controller
|
||||
recorder *MockLinterMockRecorder
|
||||
}
|
||||
|
||||
// MockLinterMockRecorder is the mock recorder for MockLinter
|
||||
type MockLinterMockRecorder struct {
|
||||
mock *MockLinter
|
||||
}
|
||||
|
||||
// NewMockLinter creates a new mock instance
|
||||
func NewMockLinter(ctrl *gomock.Controller) *MockLinter {
|
||||
mock := &MockLinter{ctrl: ctrl}
|
||||
mock.recorder = &MockLinterMockRecorder{mock}
|
||||
return mock
|
||||
}
|
||||
|
||||
// EXPECT returns an object that allows the caller to indicate expected use
|
||||
func (_m *MockLinter) EXPECT() *MockLinterMockRecorder {
|
||||
return _m.recorder
|
||||
}
|
||||
|
||||
// Run mocks base method
|
||||
func (_m *MockLinter) Run(ctx context.Context, exec executors.Executor) (*result.Result, error) {
|
||||
ret := _m.ctrl.Call(_m, "Run", ctx, exec)
|
||||
ret0, _ := ret[0].(*result.Result)
|
||||
ret1, _ := ret[1].(error)
|
||||
return ret0, ret1
|
||||
}
|
||||
|
||||
// Run indicates an expected call of Run
|
||||
func (_mr *MockLinterMockRecorder) Run(arg0, arg1 interface{}) *gomock.Call {
|
||||
return _mr.mock.ctrl.RecordCallWithMethodType(_mr.mock, "Run", reflect.TypeOf((*MockLinter)(nil).Run), arg0, arg1)
|
||||
}
|
||||
|
||||
// Name mocks base method
|
||||
func (_m *MockLinter) Name() string {
|
||||
ret := _m.ctrl.Call(_m, "Name")
|
||||
ret0, _ := ret[0].(string)
|
||||
return ret0
|
||||
}
|
||||
|
||||
// Name indicates an expected call of Name
|
||||
func (_mr *MockLinterMockRecorder) Name() *gomock.Call {
|
||||
return _mr.mock.ctrl.RecordCallWithMethodType(_mr.mock, "Name", reflect.TypeOf((*MockLinter)(nil).Name))
|
||||
}
|
27
pkg/printers/json.go
Normal file
27
pkg/printers/json.go
Normal file
@ -0,0 +1,27 @@
|
||||
package printers
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
|
||||
"github.com/golangci/golangci-lint/pkg/result"
|
||||
)
|
||||
|
||||
type JSON struct{}
|
||||
|
||||
func NewJSON() *JSON {
|
||||
return &JSON{}
|
||||
}
|
||||
|
||||
func (JSON) Print(issues chan result.Issue) (bool, error) {
|
||||
var allIssues []result.Issue
|
||||
for i := range issues {
|
||||
allIssues = append(allIssues, i)
|
||||
}
|
||||
outputJSON, err := json.Marshal(allIssues)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
fmt.Fprint(getOutWriter(), string(outputJSON))
|
||||
return len(allIssues) != 0, nil
|
||||
}
|
7
pkg/printers/printer.go
Normal file
7
pkg/printers/printer.go
Normal file
@ -0,0 +1,7 @@
|
||||
package printers
|
||||
|
||||
import "github.com/golangci/golangci-lint/pkg/result"
|
||||
|
||||
type Printer interface {
|
||||
Print(issues chan result.Issue) (bool, error)
|
||||
}
|
147
pkg/printers/text.go
Normal file
147
pkg/printers/text.go
Normal file
@ -0,0 +1,147 @@
|
||||
package printers
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/fatih/color"
|
||||
"github.com/golangci/golangci-lint/pkg/result"
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
type linesCache [][]byte
|
||||
type filesCache map[string]linesCache
|
||||
|
||||
type Text struct {
|
||||
printIssuedLine bool
|
||||
useColors bool
|
||||
printLinterName bool
|
||||
|
||||
cache filesCache
|
||||
}
|
||||
|
||||
func NewText(printIssuedLine, useColors, printLinterName bool) *Text {
|
||||
return &Text{
|
||||
printIssuedLine: printIssuedLine,
|
||||
useColors: useColors,
|
||||
printLinterName: printLinterName,
|
||||
cache: filesCache{},
|
||||
}
|
||||
}
|
||||
|
||||
func (p Text) SprintfColored(ca color.Attribute, format string, args ...interface{}) string {
|
||||
if !p.useColors {
|
||||
return fmt.Sprintf(format, args...)
|
||||
}
|
||||
|
||||
c := color.New(ca)
|
||||
return c.Sprintf(format, args...)
|
||||
}
|
||||
|
||||
func (p *Text) getFileLinesForIssue(i *result.Issue) (linesCache, error) {
|
||||
fc := p.cache[i.FilePath()]
|
||||
if fc != nil {
|
||||
return fc, nil
|
||||
}
|
||||
|
||||
// TODO: make more optimal algorithm: don't load all files into memory
|
||||
fileBytes, err := ioutil.ReadFile(i.FilePath())
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("can't read file %s for printing issued line: %s", i.FilePath(), err)
|
||||
}
|
||||
lines := bytes.Split(fileBytes, []byte("\n")) // TODO: what about \r\n?
|
||||
fc = lines
|
||||
p.cache[i.FilePath()] = fc
|
||||
return fc, nil
|
||||
}
|
||||
|
||||
func (p *Text) Print(issues chan result.Issue) (bool, error) {
|
||||
var issuedLineExtractingDuration time.Duration
|
||||
defer func() {
|
||||
logrus.Infof("Extracting issued lines took %s", issuedLineExtractingDuration)
|
||||
}()
|
||||
|
||||
issuesN := 0
|
||||
for i := range issues {
|
||||
issuesN++
|
||||
p.printIssue(&i)
|
||||
|
||||
if !p.printIssuedLine {
|
||||
continue
|
||||
}
|
||||
|
||||
startedAt := time.Now()
|
||||
lines, err := p.getFileLinesForIssue(&i)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
issuedLineExtractingDuration += time.Since(startedAt)
|
||||
|
||||
p.printIssuedLines(&i, lines)
|
||||
if i.Line()-1 < len(lines) {
|
||||
p.printUnderLinePointer(&i, string(lines[i.Line()-1]))
|
||||
}
|
||||
}
|
||||
|
||||
if issuesN == 0 {
|
||||
outStr := p.SprintfColored(color.FgGreen, "Congrats! No issues were found.")
|
||||
fmt.Fprintln(getOutWriter(), outStr)
|
||||
} else {
|
||||
logrus.Infof("Found %d issues", issuesN)
|
||||
}
|
||||
|
||||
return issuesN != 0, nil
|
||||
}
|
||||
|
||||
func (p Text) printIssue(i *result.Issue) {
|
||||
text := p.SprintfColored(color.FgRed, "%s", i.Text)
|
||||
if p.printLinterName {
|
||||
text += fmt.Sprintf(" (%s)", i.FromLinter)
|
||||
}
|
||||
pos := p.SprintfColored(color.Bold, "%s:%d", i.FilePath(), i.Line())
|
||||
fmt.Fprintf(getOutWriter(), "%s: %s\n", pos, text)
|
||||
}
|
||||
|
||||
func (p Text) printIssuedLines(i *result.Issue, lines linesCache) {
|
||||
lineRange := i.GetLineRange()
|
||||
var lineStr string
|
||||
for line := lineRange.From; line <= lineRange.To; line++ {
|
||||
if line == 0 { // some linters, e.g. gas can do it: it really means first line
|
||||
line = 1
|
||||
}
|
||||
|
||||
zeroIndexedLine := line - 1
|
||||
if zeroIndexedLine >= len(lines) {
|
||||
logrus.Warnf("No line %d in file %s", line, i.FilePath())
|
||||
break
|
||||
}
|
||||
|
||||
lineStr = string(bytes.Trim(lines[zeroIndexedLine], "\r"))
|
||||
fmt.Fprintln(getOutWriter(), lineStr)
|
||||
}
|
||||
}
|
||||
|
||||
func (p Text) printUnderLinePointer(i *result.Issue, line string) {
|
||||
lineRange := i.GetLineRange()
|
||||
if lineRange.From != lineRange.To || i.Pos.Column == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
var j int
|
||||
for ; j < len(line) && line[j] == '\t'; j++ {
|
||||
}
|
||||
tabsCount := j
|
||||
spacesCount := i.Pos.Column - 1 - tabsCount
|
||||
prefix := ""
|
||||
if tabsCount != 0 {
|
||||
prefix += strings.Repeat("\t", tabsCount)
|
||||
}
|
||||
if spacesCount != 0 {
|
||||
prefix += strings.Repeat(" ", spacesCount)
|
||||
}
|
||||
|
||||
fmt.Fprintf(getOutWriter(), "%s%s\n", prefix, p.SprintfColored(color.FgYellow, "^"))
|
||||
}
|
11
pkg/printers/utils.go
Normal file
11
pkg/printers/utils.go
Normal file
@ -0,0 +1,11 @@
|
||||
package printers
|
||||
|
||||
import (
|
||||
"io"
|
||||
"os"
|
||||
"syscall"
|
||||
)
|
||||
|
||||
func getOutWriter() io.Writer {
|
||||
return os.NewFile(uintptr(syscall.Stdout), "/dev/stdout") // was set to /dev/null
|
||||
}
|
@ -1,19 +1,35 @@
|
||||
package result
|
||||
|
||||
import "go/token"
|
||||
|
||||
type Range struct {
|
||||
From, To int
|
||||
}
|
||||
|
||||
type Issue struct {
|
||||
FromLinter string
|
||||
Text string
|
||||
File string
|
||||
LineNumber int
|
||||
HunkPos int
|
||||
|
||||
Pos token.Position
|
||||
LineRange Range
|
||||
HunkPos int
|
||||
}
|
||||
|
||||
func NewIssue(fromLinter, text, file string, lineNumber, hunkPos int) Issue {
|
||||
return Issue{
|
||||
FromLinter: fromLinter,
|
||||
Text: text,
|
||||
File: file,
|
||||
LineNumber: lineNumber,
|
||||
HunkPos: hunkPos,
|
||||
}
|
||||
func (i Issue) FilePath() string {
|
||||
return i.Pos.Filename
|
||||
}
|
||||
|
||||
func (i Issue) Line() int {
|
||||
return i.Pos.Line
|
||||
}
|
||||
|
||||
func (i Issue) GetLineRange() Range {
|
||||
if i.LineRange.From == 0 {
|
||||
return Range{
|
||||
From: i.Line(),
|
||||
To: i.Line(),
|
||||
}
|
||||
}
|
||||
|
||||
return i.LineRange
|
||||
}
|
||||
|
30
pkg/result/processors/cgo.go
Normal file
30
pkg/result/processors/cgo.go
Normal file
@ -0,0 +1,30 @@
|
||||
package processors
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"github.com/golangci/golangci-lint/pkg/result"
|
||||
)
|
||||
|
||||
type Cgo struct {
|
||||
}
|
||||
|
||||
var _ Processor = Cgo{}
|
||||
|
||||
func NewCgo() *Cgo {
|
||||
return &Cgo{}
|
||||
}
|
||||
|
||||
func (p Cgo) Name() string {
|
||||
return "cgo"
|
||||
}
|
||||
|
||||
func (p Cgo) Process(issues []result.Issue) ([]result.Issue, error) {
|
||||
return filterIssues(issues, func(i *result.Issue) bool {
|
||||
// some linters (.e.g gas, deadcode) return incorrect filepaths for cgo issues,
|
||||
// it breaks next processing, so skip them
|
||||
return !strings.HasSuffix(i.FilePath(), "/C")
|
||||
}), nil
|
||||
}
|
||||
|
||||
func (Cgo) Finish() {}
|
66
pkg/result/processors/diff.go
Normal file
66
pkg/result/processors/diff.go
Normal file
@ -0,0 +1,66 @@
|
||||
package processors
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
|
||||
"github.com/golangci/golangci-lint/pkg/result"
|
||||
"github.com/golangci/revgrep"
|
||||
)
|
||||
|
||||
type Diff struct {
|
||||
onlyNew bool
|
||||
fromRev string
|
||||
patchFilePath string
|
||||
}
|
||||
|
||||
var _ Processor = Diff{}
|
||||
|
||||
func NewDiff(onlyNew bool, fromRev, patchFilePath string) *Diff {
|
||||
return &Diff{
|
||||
onlyNew: onlyNew,
|
||||
fromRev: fromRev,
|
||||
patchFilePath: patchFilePath,
|
||||
}
|
||||
}
|
||||
|
||||
func (p Diff) Name() string {
|
||||
return "diff"
|
||||
}
|
||||
|
||||
func (p Diff) Process(issues []result.Issue) ([]result.Issue, error) {
|
||||
if !p.onlyNew && p.fromRev == "" && p.patchFilePath == "" { // no need to work
|
||||
return issues, nil
|
||||
}
|
||||
|
||||
var patchReader io.Reader
|
||||
if p.patchFilePath != "" {
|
||||
patch, err := ioutil.ReadFile(p.patchFilePath)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("can't read from pathc file %s: %s", p.patchFilePath, err)
|
||||
}
|
||||
patchReader = bytes.NewReader(patch)
|
||||
}
|
||||
c := revgrep.Checker{
|
||||
Patch: patchReader,
|
||||
RevisionFrom: p.fromRev,
|
||||
}
|
||||
if err := c.Prepare(); err != nil {
|
||||
return nil, fmt.Errorf("can't prepare diff by revgrep: %s", err)
|
||||
}
|
||||
|
||||
return transformIssues(issues, func(i *result.Issue) *result.Issue {
|
||||
hunkPos, isNew := c.IsNewIssue(i)
|
||||
if !isNew {
|
||||
return nil
|
||||
}
|
||||
|
||||
newI := *i
|
||||
newI.HunkPos = hunkPos
|
||||
return &newI
|
||||
}), nil
|
||||
}
|
||||
|
||||
func (Diff) Finish() {}
|
@ -1,92 +0,0 @@
|
||||
package processors
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"strings"
|
||||
|
||||
"github.com/bradleyfalzon/revgrep"
|
||||
"github.com/golangci/golangci-lint/pkg/result"
|
||||
)
|
||||
|
||||
type DiffProcessor struct {
|
||||
patch string
|
||||
}
|
||||
|
||||
func NewDiffProcessor(patch string) *DiffProcessor {
|
||||
return &DiffProcessor{
|
||||
patch: patch,
|
||||
}
|
||||
}
|
||||
|
||||
func (p DiffProcessor) Name() string {
|
||||
return "diff"
|
||||
}
|
||||
|
||||
func (p DiffProcessor) processResult(res result.Result) (*result.Result, error) {
|
||||
// Make mapping to restore original issues metadata later
|
||||
fli := makeFilesToLinesToIssuesMap([]result.Result{res})
|
||||
|
||||
rIssues, err := p.runRevgrepOnIssues(res.Issues)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
newIssues := []result.Issue{}
|
||||
for _, ri := range rIssues {
|
||||
if fli[ri.File] == nil {
|
||||
return nil, fmt.Errorf("can't get original issue file for %v", ri)
|
||||
}
|
||||
|
||||
oi := fli[ri.File][ri.LineNo]
|
||||
if len(oi) != 1 {
|
||||
return nil, fmt.Errorf("can't get original issue for %v: %v", ri, oi)
|
||||
}
|
||||
|
||||
i := result.Issue{
|
||||
File: ri.File,
|
||||
LineNumber: ri.LineNo,
|
||||
Text: ri.Message,
|
||||
HunkPos: ri.HunkPos,
|
||||
FromLinter: oi[0].FromLinter,
|
||||
}
|
||||
newIssues = append(newIssues, i)
|
||||
}
|
||||
|
||||
res.Issues = newIssues
|
||||
return &res, nil
|
||||
}
|
||||
|
||||
func (p DiffProcessor) Process(results []result.Result) ([]result.Result, error) {
|
||||
retResults := []result.Result{}
|
||||
for _, res := range results {
|
||||
newRes, err := p.processResult(res)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("can't filter only new issues for result %+v: %s", res, err)
|
||||
}
|
||||
retResults = append(retResults, *newRes)
|
||||
}
|
||||
|
||||
return retResults, nil
|
||||
}
|
||||
|
||||
func (p DiffProcessor) runRevgrepOnIssues(issues []result.Issue) ([]revgrep.Issue, error) {
|
||||
// TODO: change revgrep to accept interface with line number, file name
|
||||
fakeIssuesLines := []string{}
|
||||
for _, i := range issues {
|
||||
line := fmt.Sprintf("%s:%d:%d: %s", i.File, i.LineNumber, 0, i.Text)
|
||||
fakeIssuesLines = append(fakeIssuesLines, line)
|
||||
}
|
||||
fakeIssuesOut := strings.Join(fakeIssuesLines, "\n")
|
||||
|
||||
checker := revgrep.Checker{
|
||||
Patch: strings.NewReader(p.patch),
|
||||
Regexp: `^([^:]+):(\d+):(\d+)?:?\s*(.*)$`,
|
||||
}
|
||||
rIssues, err := checker.Check(strings.NewReader(fakeIssuesOut), ioutil.Discard)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("can't filter only new issues by revgrep: %s", err)
|
||||
}
|
||||
|
||||
return rIssues, nil
|
||||
}
|
39
pkg/result/processors/exclude.go
Normal file
39
pkg/result/processors/exclude.go
Normal file
@ -0,0 +1,39 @@
|
||||
package processors
|
||||
|
||||
import (
|
||||
"regexp"
|
||||
|
||||
"github.com/golangci/golangci-lint/pkg/result"
|
||||
)
|
||||
|
||||
type Exclude struct {
|
||||
pattern *regexp.Regexp
|
||||
}
|
||||
|
||||
var _ Processor = Exclude{}
|
||||
|
||||
func NewExclude(pattern string) *Exclude {
|
||||
var patternRe *regexp.Regexp
|
||||
if pattern != "" {
|
||||
patternRe = regexp.MustCompile("(?i)" + pattern)
|
||||
}
|
||||
return &Exclude{
|
||||
pattern: patternRe,
|
||||
}
|
||||
}
|
||||
|
||||
func (p Exclude) Name() string {
|
||||
return "exclude"
|
||||
}
|
||||
|
||||
func (p Exclude) Process(issues []result.Issue) ([]result.Issue, error) {
|
||||
if p.pattern == nil {
|
||||
return issues, nil
|
||||
}
|
||||
|
||||
return filterIssues(issues, func(i *result.Issue) bool {
|
||||
return !p.pattern.MatchString(i.Text)
|
||||
}), nil
|
||||
}
|
||||
|
||||
func (p Exclude) Finish() {}
|
@ -1,44 +0,0 @@
|
||||
package processors
|
||||
|
||||
import (
|
||||
"regexp"
|
||||
|
||||
"github.com/golangci/golangci-lint/pkg/result"
|
||||
)
|
||||
|
||||
type ExcludeProcessor struct {
|
||||
pattern *regexp.Regexp
|
||||
}
|
||||
|
||||
var _ Processor = ExcludeProcessor{}
|
||||
|
||||
func NewExcludeProcessor(pattern string) *ExcludeProcessor {
|
||||
return &ExcludeProcessor{
|
||||
pattern: regexp.MustCompile(pattern),
|
||||
}
|
||||
}
|
||||
|
||||
func (p ExcludeProcessor) Name() string {
|
||||
return "exclude"
|
||||
}
|
||||
|
||||
func (p ExcludeProcessor) processResult(res result.Result) result.Result {
|
||||
newRes := res
|
||||
newRes.Issues = []result.Issue{}
|
||||
for _, i := range res.Issues {
|
||||
if !p.pattern.MatchString(i.Text) {
|
||||
newRes.Issues = append(newRes.Issues, i)
|
||||
}
|
||||
}
|
||||
|
||||
return newRes
|
||||
}
|
||||
|
||||
func (p ExcludeProcessor) Process(results []result.Result) ([]result.Result, error) {
|
||||
retResults := []result.Result{}
|
||||
for _, res := range results {
|
||||
retResults = append(retResults, p.processResult(res))
|
||||
}
|
||||
|
||||
return retResults, nil
|
||||
}
|
52
pkg/result/processors/exclude_test.go
Normal file
52
pkg/result/processors/exclude_test.go
Normal file
@ -0,0 +1,52 @@
|
||||
package processors
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/golangci/golangci-lint/pkg/result"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func newTextIssue(text string) result.Issue {
|
||||
return result.Issue{
|
||||
Text: text,
|
||||
}
|
||||
}
|
||||
|
||||
func process(t *testing.T, p Processor, issues ...result.Issue) []result.Issue {
|
||||
processedIssues, err := p.Process(issues)
|
||||
assert.NoError(t, err)
|
||||
return processedIssues
|
||||
}
|
||||
|
||||
func processAssertSame(t *testing.T, p Processor, issues ...result.Issue) {
|
||||
processedIssues := process(t, p, issues...)
|
||||
assert.Equal(t, issues, processedIssues)
|
||||
}
|
||||
|
||||
func processAssertEmpty(t *testing.T, p Processor, issues ...result.Issue) {
|
||||
processedIssues := process(t, p, issues...)
|
||||
assert.Empty(t, processedIssues)
|
||||
}
|
||||
|
||||
func TestExclude(t *testing.T) {
|
||||
p := NewExclude("^exclude$")
|
||||
texts := []string{"excLude", "1", "", "exclud", "notexclude"}
|
||||
var issues []result.Issue
|
||||
for _, t := range texts {
|
||||
issues = append(issues, newTextIssue(t))
|
||||
}
|
||||
|
||||
processedIssues := process(t, p, issues...)
|
||||
assert.Len(t, processedIssues, len(issues)-1)
|
||||
|
||||
var processedTexts []string
|
||||
for _, i := range processedIssues {
|
||||
processedTexts = append(processedTexts, i.Text)
|
||||
}
|
||||
assert.Equal(t, texts[1:], processedTexts)
|
||||
}
|
||||
|
||||
func TestNoExclude(t *testing.T) {
|
||||
processAssertSame(t, NewExclude(""), newTextIssue("test"))
|
||||
}
|
44
pkg/result/processors/max_from_linter.go
Normal file
44
pkg/result/processors/max_from_linter.go
Normal file
@ -0,0 +1,44 @@
|
||||
package processors
|
||||
|
||||
import (
|
||||
"github.com/golangci/golangci-lint/pkg/result"
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
type MaxFromLinter struct {
|
||||
lc linterToCountMap
|
||||
limit int
|
||||
}
|
||||
|
||||
var _ Processor = &MaxFromLinter{}
|
||||
|
||||
func NewMaxFromLinter(limit int) *MaxFromLinter {
|
||||
return &MaxFromLinter{
|
||||
lc: linterToCountMap{},
|
||||
limit: limit,
|
||||
}
|
||||
}
|
||||
|
||||
func (p MaxFromLinter) Name() string {
|
||||
return "max_from_linter"
|
||||
}
|
||||
|
||||
func (p *MaxFromLinter) Process(issues []result.Issue) ([]result.Issue, error) {
|
||||
if p.limit <= 0 { // no limit
|
||||
return issues, nil
|
||||
}
|
||||
|
||||
return filterIssues(issues, func(i *result.Issue) bool {
|
||||
p.lc[i.FromLinter]++ // always inc for stat
|
||||
return p.lc[i.FromLinter] <= p.limit
|
||||
}), nil
|
||||
}
|
||||
|
||||
func (p MaxFromLinter) Finish() {
|
||||
walkStringToIntMapSortedByValue(p.lc, func(linter string, count int) {
|
||||
if count > p.limit {
|
||||
logrus.Infof("%d/%d issues from linter %s were hidden, use --max-issues-per-linter",
|
||||
count-p.limit, count, linter)
|
||||
}
|
||||
})
|
||||
}
|
14
pkg/result/processors/max_from_linter_test.go
Normal file
14
pkg/result/processors/max_from_linter_test.go
Normal file
@ -0,0 +1,14 @@
|
||||
package processors
|
||||
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestMaxFromLinter(t *testing.T) {
|
||||
p := NewMaxFromLinter(1)
|
||||
gosimple := newFromLinterIssue("gosimple")
|
||||
gofmt := newFromLinterIssue("gofmt")
|
||||
processAssertSame(t, p, gosimple) // ok
|
||||
processAssertSame(t, p, gofmt) // ok: another
|
||||
processAssertEmpty(t, p, gosimple) // skip
|
||||
}
|
53
pkg/result/processors/max_per_file_from_linter.go
Normal file
53
pkg/result/processors/max_per_file_from_linter.go
Normal file
@ -0,0 +1,53 @@
|
||||
package processors
|
||||
|
||||
import (
|
||||
"github.com/golangci/golangci-lint/pkg/golinters"
|
||||
"github.com/golangci/golangci-lint/pkg/result"
|
||||
)
|
||||
|
||||
type linterToCountMap map[string]int
|
||||
type fileToLinterToCountMap map[string]linterToCountMap
|
||||
|
||||
type MaxPerFileFromLinter struct {
|
||||
flc fileToLinterToCountMap
|
||||
}
|
||||
|
||||
var _ Processor = &MaxPerFileFromLinter{}
|
||||
|
||||
func NewMaxPerFileFromLinter() *MaxPerFileFromLinter {
|
||||
return &MaxPerFileFromLinter{
|
||||
flc: fileToLinterToCountMap{},
|
||||
}
|
||||
}
|
||||
|
||||
func (p MaxPerFileFromLinter) Name() string {
|
||||
return "max_per_file_from_linter"
|
||||
}
|
||||
|
||||
var maxPerFileFromLinterConfig = map[string]int{
|
||||
golinters.Gofmt{}.Name(): 1,
|
||||
golinters.Gofmt{UseGoimports: true}.Name(): 1,
|
||||
}
|
||||
|
||||
func (p *MaxPerFileFromLinter) Process(issues []result.Issue) ([]result.Issue, error) {
|
||||
return filterIssues(issues, func(i *result.Issue) bool {
|
||||
limit := maxPerFileFromLinterConfig[i.FromLinter]
|
||||
if limit == 0 {
|
||||
return true
|
||||
}
|
||||
|
||||
lm := p.flc[i.FilePath()]
|
||||
if lm == nil {
|
||||
p.flc[i.FilePath()] = linterToCountMap{}
|
||||
}
|
||||
count := p.flc[i.FilePath()][i.FromLinter]
|
||||
if count >= limit {
|
||||
return false
|
||||
}
|
||||
|
||||
p.flc[i.FilePath()][i.FromLinter]++
|
||||
return true
|
||||
}), nil
|
||||
}
|
||||
|
||||
func (p MaxPerFileFromLinter) Finish() {}
|
31
pkg/result/processors/max_per_file_from_linter_test.go
Normal file
31
pkg/result/processors/max_per_file_from_linter_test.go
Normal file
@ -0,0 +1,31 @@
|
||||
package processors
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/golangci/golangci-lint/pkg/result"
|
||||
)
|
||||
|
||||
func newFromLinterIssue(linterName string) result.Issue {
|
||||
return result.Issue{
|
||||
FromLinter: linterName,
|
||||
}
|
||||
}
|
||||
|
||||
func TestMaxPerFileFromLinterUnlimited(t *testing.T) {
|
||||
p := NewMaxPerFileFromLinter()
|
||||
gosimple := newFromLinterIssue("gosimple")
|
||||
processAssertSame(t, p, gosimple) // collect stat
|
||||
processAssertSame(t, p, gosimple) // check not limits
|
||||
}
|
||||
|
||||
func TestMaxPerFileFromLinter(t *testing.T) {
|
||||
p := NewMaxPerFileFromLinter()
|
||||
for _, name := range []string{"gofmt", "goimports"} {
|
||||
limited := newFromLinterIssue(name)
|
||||
gosimple := newFromLinterIssue("gosimple")
|
||||
processAssertSame(t, p, limited)
|
||||
processAssertSame(t, p, gosimple)
|
||||
processAssertEmpty(t, p, limited)
|
||||
}
|
||||
}
|
@ -1,57 +0,0 @@
|
||||
package processors
|
||||
|
||||
import "github.com/golangci/golangci-lint/pkg/result"
|
||||
|
||||
type MaxLinterIssuesPerFile struct{}
|
||||
|
||||
var _ Processor = MaxLinterIssuesPerFile{}
|
||||
|
||||
type fileToIssuesMap map[string][]result.Issue
|
||||
|
||||
func (p MaxLinterIssuesPerFile) Name() string {
|
||||
return "max_issues_per_file"
|
||||
}
|
||||
|
||||
func (p MaxLinterIssuesPerFile) makeFileToIssuesMap(res result.Result) fileToIssuesMap {
|
||||
fti := fileToIssuesMap{}
|
||||
for _, i := range res.Issues {
|
||||
fti[i.File] = append(fti[i.File], i)
|
||||
}
|
||||
|
||||
return fti
|
||||
}
|
||||
|
||||
func (p MaxLinterIssuesPerFile) processResult(res result.Result) result.Result {
|
||||
if len(res.Issues) == 0 {
|
||||
return res
|
||||
}
|
||||
|
||||
if res.MaxIssuesPerFile == 0 {
|
||||
return res // Nothing to process
|
||||
}
|
||||
|
||||
fti := p.makeFileToIssuesMap(res)
|
||||
for file, fileIssues := range fti {
|
||||
if len(fileIssues) > res.MaxIssuesPerFile {
|
||||
fti[file] = fileIssues[:res.MaxIssuesPerFile]
|
||||
}
|
||||
}
|
||||
|
||||
filteredIssues := []result.Issue{}
|
||||
for _, issues := range fti {
|
||||
filteredIssues = append(filteredIssues, issues...)
|
||||
}
|
||||
|
||||
res.Issues = filteredIssues
|
||||
return res
|
||||
}
|
||||
|
||||
func (p MaxLinterIssuesPerFile) Process(results []result.Result) ([]result.Result, error) {
|
||||
newResults := []result.Result{}
|
||||
|
||||
for _, res := range results {
|
||||
newResults = append(newResults, p.processResult(res))
|
||||
}
|
||||
|
||||
return newResults, nil
|
||||
}
|
68
pkg/result/processors/max_same_issues.go
Normal file
68
pkg/result/processors/max_same_issues.go
Normal file
@ -0,0 +1,68 @@
|
||||
package processors
|
||||
|
||||
import (
|
||||
"sort"
|
||||
|
||||
"github.com/golangci/golangci-lint/pkg/result"
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
type textToCountMap map[string]int
|
||||
|
||||
type MaxSameIssues struct {
|
||||
tc textToCountMap
|
||||
limit int
|
||||
}
|
||||
|
||||
var _ Processor = &MaxSameIssues{}
|
||||
|
||||
func NewMaxSameIssues(limit int) *MaxSameIssues {
|
||||
return &MaxSameIssues{
|
||||
tc: textToCountMap{},
|
||||
limit: limit,
|
||||
}
|
||||
}
|
||||
|
||||
func (MaxSameIssues) Name() string {
|
||||
return "max_same_issues"
|
||||
}
|
||||
|
||||
func (p *MaxSameIssues) Process(issues []result.Issue) ([]result.Issue, error) {
|
||||
if p.limit <= 0 { // no limit
|
||||
return issues, nil
|
||||
}
|
||||
|
||||
return filterIssues(issues, func(i *result.Issue) bool {
|
||||
p.tc[i.Text]++ // always inc for stat
|
||||
return p.tc[i.Text] <= p.limit
|
||||
}), nil
|
||||
}
|
||||
|
||||
func (p MaxSameIssues) Finish() {
|
||||
walkStringToIntMapSortedByValue(p.tc, func(text string, count int) {
|
||||
if count > p.limit {
|
||||
logrus.Infof("%d/%d issues with text %q were hidden, use --max-same-issues",
|
||||
count-p.limit, count, text)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
type kv struct {
|
||||
Key string
|
||||
Value int
|
||||
}
|
||||
|
||||
func walkStringToIntMapSortedByValue(m map[string]int, walk func(k string, v int)) {
|
||||
var ss []kv
|
||||
for k, v := range m {
|
||||
ss = append(ss, kv{k, v})
|
||||
}
|
||||
|
||||
sort.Slice(ss, func(i, j int) bool {
|
||||
return ss[i].Value > ss[j].Value
|
||||
})
|
||||
|
||||
for _, kv := range ss {
|
||||
walk(kv.Key, kv.Value)
|
||||
}
|
||||
}
|
21
pkg/result/processors/max_same_issues_test.go
Normal file
21
pkg/result/processors/max_same_issues_test.go
Normal file
@ -0,0 +1,21 @@
|
||||
package processors
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/golangci/golangci-lint/pkg/result"
|
||||
)
|
||||
|
||||
func TestMaxSameIssues(t *testing.T) {
|
||||
p := NewMaxSameIssues(1)
|
||||
i1 := result.Issue{
|
||||
Text: "1",
|
||||
}
|
||||
i2 := result.Issue{
|
||||
Text: "2",
|
||||
}
|
||||
|
||||
processAssertSame(t, p, i1) // ok
|
||||
processAssertSame(t, p, i2) // ok: another
|
||||
processAssertEmpty(t, p, i1) // skip
|
||||
}
|
150
pkg/result/processors/nolint.go
Normal file
150
pkg/result/processors/nolint.go
Normal file
@ -0,0 +1,150 @@
|
||||
package processors
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"fmt"
|
||||
"go/ast"
|
||||
"go/parser"
|
||||
"go/token"
|
||||
"io/ioutil"
|
||||
"strings"
|
||||
|
||||
"github.com/golangci/golangci-lint/pkg/result"
|
||||
)
|
||||
|
||||
type comment struct {
|
||||
linters []string
|
||||
line int
|
||||
}
|
||||
type fileComments []comment
|
||||
type fileData struct {
|
||||
comments fileComments
|
||||
isGenerated bool
|
||||
}
|
||||
type filesCache map[string]*fileData
|
||||
|
||||
type Nolint struct {
|
||||
fset *token.FileSet
|
||||
cache filesCache
|
||||
}
|
||||
|
||||
func NewNolint(fset *token.FileSet) *Nolint {
|
||||
return &Nolint{
|
||||
fset: fset,
|
||||
cache: filesCache{},
|
||||
}
|
||||
}
|
||||
|
||||
var _ Processor = &Nolint{}
|
||||
|
||||
func (p Nolint) Name() string {
|
||||
return "nolint"
|
||||
}
|
||||
|
||||
func (p *Nolint) Process(issues []result.Issue) ([]result.Issue, error) {
|
||||
return filterIssuesErr(issues, p.shouldPassIssue)
|
||||
}
|
||||
|
||||
var (
|
||||
genHdr = []byte("// Code generated ")
|
||||
genFtr = []byte(" DO NOT EDIT.")
|
||||
)
|
||||
|
||||
// isGenerated reports whether the source file is generated code
|
||||
// according the rules from https://golang.org/s/generatedcode.
|
||||
func isGenerated(src []byte) bool {
|
||||
sc := bufio.NewScanner(bytes.NewReader(src))
|
||||
for sc.Scan() {
|
||||
b := sc.Bytes()
|
||||
if bytes.HasPrefix(b, genHdr) && bytes.HasSuffix(b, genFtr) && len(b) >= len(genHdr)+len(genFtr) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (p *Nolint) getOrCreateFileData(i *result.Issue) (*fileData, error) {
|
||||
fd := p.cache[i.FilePath()]
|
||||
if fd != nil {
|
||||
return fd, nil
|
||||
}
|
||||
|
||||
fd = &fileData{}
|
||||
p.cache[i.FilePath()] = fd
|
||||
|
||||
src, err := ioutil.ReadFile(i.FilePath())
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("can't read file %s: %s", i.FilePath(), err)
|
||||
}
|
||||
|
||||
fd.isGenerated = isGenerated(src)
|
||||
if fd.isGenerated { // don't report issues for autogenerated files
|
||||
return fd, nil
|
||||
}
|
||||
|
||||
file, err := parser.ParseFile(p.fset, i.FilePath(), src, parser.ParseComments)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("can't parse file %s", i.FilePath())
|
||||
}
|
||||
|
||||
fd.comments = extractFileComments(p.fset, file.Comments...)
|
||||
return fd, nil
|
||||
}
|
||||
|
||||
func (p *Nolint) shouldPassIssue(i *result.Issue) (bool, error) {
|
||||
fd, err := p.getOrCreateFileData(i)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
if fd.isGenerated { // don't report issues for autogenerated files
|
||||
return false, nil
|
||||
}
|
||||
|
||||
for _, comment := range fd.comments {
|
||||
if comment.line != i.Line() {
|
||||
continue
|
||||
}
|
||||
|
||||
if len(comment.linters) == 0 {
|
||||
return false, nil // skip all linters
|
||||
}
|
||||
|
||||
for _, linter := range comment.linters {
|
||||
if i.FromLinter == linter {
|
||||
return false, nil
|
||||
}
|
||||
// TODO: check linter name
|
||||
}
|
||||
}
|
||||
|
||||
return true, nil
|
||||
}
|
||||
|
||||
func extractFileComments(fset *token.FileSet, comments ...*ast.CommentGroup) fileComments {
|
||||
ret := fileComments{}
|
||||
for _, g := range comments {
|
||||
for _, c := range g.List {
|
||||
text := strings.TrimLeft(c.Text, "/ ")
|
||||
if strings.HasPrefix(text, "nolint") {
|
||||
var linters []string
|
||||
if strings.HasPrefix(text, "nolint:") {
|
||||
text = strings.Split(text, " ")[0] // allow arbitrary text after this comment
|
||||
for _, linter := range strings.Split(strings.TrimPrefix(text, "nolint:"), ",") {
|
||||
linters = append(linters, strings.TrimSpace(linter))
|
||||
}
|
||||
}
|
||||
pos := fset.Position(g.Pos())
|
||||
ret = append(ret, comment{
|
||||
linters: linters,
|
||||
line: pos.Line,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return ret
|
||||
}
|
||||
|
||||
func (p Nolint) Finish() {}
|
42
pkg/result/processors/nolint_test.go
Normal file
42
pkg/result/processors/nolint_test.go
Normal file
@ -0,0 +1,42 @@
|
||||
package processors
|
||||
|
||||
import (
|
||||
"go/token"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/golangci/golangci-lint/pkg/result"
|
||||
)
|
||||
|
||||
func newNolintFileIssue(line int, fromLinter string) result.Issue {
|
||||
return result.Issue{
|
||||
Pos: token.Position{
|
||||
Filename: filepath.Join("testdata", "nolint.go"),
|
||||
Line: line,
|
||||
},
|
||||
FromLinter: fromLinter,
|
||||
}
|
||||
}
|
||||
|
||||
func TestNolint(t *testing.T) {
|
||||
p := NewNolint(token.NewFileSet())
|
||||
processAssertEmpty(t, p, newNolintFileIssue(3, "gofmt"))
|
||||
processAssertEmpty(t, p, newNolintFileIssue(3, "gofmt")) // check cached is ok
|
||||
processAssertSame(t, p, newNolintFileIssue(3, "gofmtA")) // check different name
|
||||
|
||||
processAssertEmpty(t, p, newNolintFileIssue(4, "any"))
|
||||
processAssertEmpty(t, p, newNolintFileIssue(5, "any"))
|
||||
|
||||
processAssertSame(t, p, newNolintFileIssue(1, "golint"))
|
||||
}
|
||||
|
||||
func TestNoIssuesInAutogeneratedFile(t *testing.T) {
|
||||
i := result.Issue{
|
||||
Pos: token.Position{
|
||||
Filename: filepath.Join("testdata", "nolint_autogenerated.go"),
|
||||
Line: 4,
|
||||
},
|
||||
}
|
||||
p := NewNolint(token.NewFileSet())
|
||||
processAssertEmpty(t, p, i)
|
||||
}
|
48
pkg/result/processors/path_prettifier.go
Normal file
48
pkg/result/processors/path_prettifier.go
Normal file
@ -0,0 +1,48 @@
|
||||
package processors
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/golangci/golangci-lint/pkg/result"
|
||||
)
|
||||
|
||||
type PathPrettifier struct {
|
||||
root string
|
||||
}
|
||||
|
||||
var _ Processor = PathPrettifier{}
|
||||
|
||||
func NewPathPrettifier() *PathPrettifier {
|
||||
root, err := os.Getwd()
|
||||
if err != nil {
|
||||
panic(fmt.Sprintf("Can't get working dir: %s", err))
|
||||
}
|
||||
return &PathPrettifier{
|
||||
root: root,
|
||||
}
|
||||
}
|
||||
|
||||
func (p PathPrettifier) Name() string {
|
||||
return "path_prettifier"
|
||||
}
|
||||
|
||||
func (p PathPrettifier) Process(issues []result.Issue) ([]result.Issue, error) {
|
||||
return transformIssues(issues, func(i *result.Issue) *result.Issue {
|
||||
if !filepath.IsAbs(i.FilePath()) {
|
||||
return i
|
||||
}
|
||||
|
||||
rel, err := filepath.Rel(p.root, i.FilePath())
|
||||
if err != nil {
|
||||
return i
|
||||
}
|
||||
|
||||
newI := i
|
||||
newI.Pos.Filename = rel
|
||||
return newI
|
||||
}), nil
|
||||
}
|
||||
|
||||
func (p PathPrettifier) Finish() {}
|
@ -3,6 +3,7 @@ package processors
|
||||
import "github.com/golangci/golangci-lint/pkg/result"
|
||||
|
||||
type Processor interface {
|
||||
Process(results []result.Result) ([]result.Result, error)
|
||||
Process(issues []result.Issue) ([]result.Issue, error)
|
||||
Name() string
|
||||
Finish()
|
||||
}
|
||||
|
5
pkg/result/processors/testdata/nolint.go
vendored
Normal file
5
pkg/result/processors/testdata/nolint.go
vendored
Normal file
@ -0,0 +1,5 @@
|
||||
package testdata
|
||||
|
||||
var nolintSpecific int // nolint:gofmt
|
||||
var nolintAll int // nolint
|
||||
var nolintAndAppendix int // nolint Some My Text
|
4
pkg/result/processors/testdata/nolint_autogenerated.go
vendored
Normal file
4
pkg/result/processors/testdata/nolint_autogenerated.go
vendored
Normal file
@ -0,0 +1,4 @@
|
||||
// Code generated by ... DO NOT EDIT.
|
||||
package testdata
|
||||
|
||||
var v int
|
45
pkg/result/processors/uniq_by_line.go
Normal file
45
pkg/result/processors/uniq_by_line.go
Normal file
@ -0,0 +1,45 @@
|
||||
package processors
|
||||
|
||||
import (
|
||||
"github.com/golangci/golangci-lint/pkg/result"
|
||||
)
|
||||
|
||||
type lineToCount map[int]int
|
||||
type fileToLineToCount map[string]lineToCount
|
||||
|
||||
type UniqByLine struct {
|
||||
flc fileToLineToCount
|
||||
}
|
||||
|
||||
func NewUniqByLine() *UniqByLine {
|
||||
return &UniqByLine{
|
||||
flc: fileToLineToCount{},
|
||||
}
|
||||
}
|
||||
|
||||
var _ Processor = &UniqByLine{}
|
||||
|
||||
func (p UniqByLine) Name() string {
|
||||
return "uniq_by_line"
|
||||
}
|
||||
|
||||
func (p *UniqByLine) Process(issues []result.Issue) ([]result.Issue, error) {
|
||||
return filterIssues(issues, func(i *result.Issue) bool {
|
||||
lc := p.flc[i.FilePath()]
|
||||
if lc == nil {
|
||||
lc = lineToCount{}
|
||||
p.flc[i.FilePath()] = lc
|
||||
}
|
||||
|
||||
const limit = 1
|
||||
count := lc[i.Line()]
|
||||
if count == limit {
|
||||
return false
|
||||
}
|
||||
|
||||
lc[i.Line()]++
|
||||
return true
|
||||
}), nil
|
||||
}
|
||||
|
||||
func (p UniqByLine) Finish() {}
|
@ -1,38 +0,0 @@
|
||||
package processors
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/golangci/golangci-lint/pkg/result"
|
||||
)
|
||||
|
||||
type UniqByLineProcessor struct{}
|
||||
|
||||
var _ Processor = UniqByLineProcessor{}
|
||||
|
||||
func (p UniqByLineProcessor) Name() string {
|
||||
return "uniq_by_line"
|
||||
}
|
||||
|
||||
func (p UniqByLineProcessor) Process(results []result.Result) ([]result.Result, error) {
|
||||
fli := makeFilesToLinesToIssuesMap(results)
|
||||
|
||||
retResults := []result.Result{}
|
||||
for _, res := range results {
|
||||
newRes := res
|
||||
newRes.Issues = []result.Issue{}
|
||||
for _, i := range res.Issues {
|
||||
lineIssues := fli[i.File][i.LineNumber]
|
||||
if len(lineIssues) == 0 {
|
||||
return nil, fmt.Errorf("bug in by line uniqalization")
|
||||
}
|
||||
|
||||
if lineIssues[0] == i { // Use first issue for line
|
||||
newRes.Issues = append(newRes.Issues, i)
|
||||
}
|
||||
}
|
||||
retResults = append(retResults, newRes)
|
||||
}
|
||||
|
||||
return retResults, nil
|
||||
}
|
29
pkg/result/processors/uniq_by_line_test.go
Normal file
29
pkg/result/processors/uniq_by_line_test.go
Normal file
@ -0,0 +1,29 @@
|
||||
package processors
|
||||
|
||||
import (
|
||||
"go/token"
|
||||
"testing"
|
||||
|
||||
"github.com/golangci/golangci-lint/pkg/result"
|
||||
)
|
||||
|
||||
func newFLIssue(file string, line int) result.Issue {
|
||||
return result.Issue{
|
||||
Pos: token.Position{
|
||||
Filename: file,
|
||||
Line: line,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func TestUniqByLine(t *testing.T) {
|
||||
p := NewUniqByLine()
|
||||
i1 := newFLIssue("f1", 1)
|
||||
|
||||
processAssertSame(t, p, i1)
|
||||
processAssertEmpty(t, p, i1) // check skipping
|
||||
processAssertEmpty(t, p, i1) // check accumulated error
|
||||
|
||||
processAssertSame(t, p, newFLIssue("f1", 2)) // another line
|
||||
processAssertSame(t, p, newFLIssue("f2", 1)) // another file
|
||||
}
|
@ -1,21 +1,46 @@
|
||||
package processors
|
||||
|
||||
import "github.com/golangci/golangci-lint/pkg/result"
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
type linesToIssuesMap map[int][]result.Issue
|
||||
type filesToLinesToIssuesMap map[string]linesToIssuesMap
|
||||
"github.com/golangci/golangci-lint/pkg/result"
|
||||
)
|
||||
|
||||
func makeFilesToLinesToIssuesMap(results []result.Result) filesToLinesToIssuesMap {
|
||||
fli := filesToLinesToIssuesMap{}
|
||||
for _, res := range results {
|
||||
for _, i := range res.Issues {
|
||||
if fli[i.File] == nil {
|
||||
fli[i.File] = linesToIssuesMap{}
|
||||
}
|
||||
li := fli[i.File]
|
||||
li[i.LineNumber] = append(li[i.LineNumber], i)
|
||||
func filterIssues(issues []result.Issue, filter func(i *result.Issue) bool) []result.Issue {
|
||||
retIssues := make([]result.Issue, 0, len(issues))
|
||||
for _, i := range issues {
|
||||
if filter(&i) {
|
||||
retIssues = append(retIssues, i)
|
||||
}
|
||||
}
|
||||
|
||||
return fli
|
||||
return retIssues
|
||||
}
|
||||
|
||||
func filterIssuesErr(issues []result.Issue, filter func(i *result.Issue) (bool, error)) ([]result.Issue, error) {
|
||||
retIssues := make([]result.Issue, 0, len(issues))
|
||||
for _, i := range issues {
|
||||
ok, err := filter(&i)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("can't filter issue %#v: %s", i, err)
|
||||
}
|
||||
|
||||
if ok {
|
||||
retIssues = append(retIssues, i)
|
||||
}
|
||||
}
|
||||
|
||||
return retIssues, nil
|
||||
}
|
||||
|
||||
func transformIssues(issues []result.Issue, transform func(i *result.Issue) *result.Issue) []result.Issue {
|
||||
retIssues := make([]result.Issue, 0, len(issues))
|
||||
for _, i := range issues {
|
||||
newI := transform(&i)
|
||||
if newI != nil {
|
||||
retIssues = append(retIssues, *newI)
|
||||
}
|
||||
}
|
||||
|
||||
return retIssues
|
||||
}
|
||||
|
163
pkg/runner.go
163
pkg/runner.go
@ -1,67 +1,160 @@
|
||||
package linters
|
||||
package pkg
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"runtime/debug"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/golangci/golangci-lint/pkg/golinters"
|
||||
"github.com/golangci/golangci-lint/pkg/result"
|
||||
"github.com/golangci/golangci-lint/pkg/result/processors"
|
||||
"github.com/golangci/golangci-shared/pkg/analytics"
|
||||
"github.com/golangci/golangci-shared/pkg/executors"
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
type Runner interface {
|
||||
Run(ctx context.Context, linters []Linter, exec executors.Executor) ([]result.Issue, error)
|
||||
}
|
||||
|
||||
type SimpleRunner struct {
|
||||
Processors []processors.Processor
|
||||
}
|
||||
|
||||
func (r SimpleRunner) Run(ctx context.Context, linters []Linter, exec executors.Executor) ([]result.Issue, error) {
|
||||
results := []result.Result{}
|
||||
for _, linter := range linters {
|
||||
res, err := linter.Run(ctx, exec)
|
||||
if err != nil {
|
||||
analytics.Log(ctx).Warnf("Can't run linter %+v: %s", linter, err)
|
||||
continue
|
||||
}
|
||||
type lintRes struct {
|
||||
linter Linter
|
||||
err error
|
||||
issues []result.Issue
|
||||
}
|
||||
|
||||
if len(res.Issues) == 0 {
|
||||
continue
|
||||
func (r *SimpleRunner) runLinter(ctx context.Context, linter Linter, lintCtx *golinters.Context, i int) (res []result.Issue, err error) {
|
||||
defer func() {
|
||||
if panicData := recover(); panicData != nil {
|
||||
err = fmt.Errorf("panic occured: %s", panicData)
|
||||
logrus.Infof("Panic stack trace: %s", debug.Stack())
|
||||
}
|
||||
}()
|
||||
startedAt := time.Now()
|
||||
res, err = linter.Run(ctx, lintCtx)
|
||||
|
||||
results = append(results, *res)
|
||||
logrus.Infof("worker #%d: linter %s took %s and found %d issues (before processing them)", i, linter.Name(),
|
||||
time.Since(startedAt), len(res))
|
||||
return
|
||||
}
|
||||
|
||||
func (r *SimpleRunner) runLinters(ctx context.Context, wg *sync.WaitGroup, tasksCh chan Linter, lintResultsCh chan lintRes, lintCtx *golinters.Context, workersCount int) {
|
||||
for i := 0; i < workersCount; i++ {
|
||||
go func(i int) {
|
||||
defer wg.Done()
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return
|
||||
case linter, ok := <-tasksCh:
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
if ctx.Err() != nil {
|
||||
// XXX: if check it in only int a select
|
||||
// it's possible to not enter to this case until tasksCh is empty.
|
||||
return
|
||||
}
|
||||
issues, lerr := r.runLinter(ctx, linter, lintCtx, i)
|
||||
lintResultsCh <- lintRes{
|
||||
linter: linter,
|
||||
err: lerr,
|
||||
issues: issues,
|
||||
}
|
||||
}
|
||||
}
|
||||
}(i + 1)
|
||||
}
|
||||
}
|
||||
|
||||
results, err := r.processResults(results)
|
||||
func (r SimpleRunner) Run(ctx context.Context, linters []Linter, lintCtx *golinters.Context) chan result.Issue {
|
||||
retIssues := make(chan result.Issue, 1024)
|
||||
go func() {
|
||||
defer close(retIssues)
|
||||
if err := r.runGo(ctx, linters, lintCtx, retIssues); err != nil {
|
||||
logrus.Warnf("error running linters: %s", err)
|
||||
}
|
||||
}()
|
||||
|
||||
return retIssues
|
||||
}
|
||||
|
||||
func (r SimpleRunner) runGo(ctx context.Context, linters []Linter, lintCtx *golinters.Context, retIssues chan result.Issue) error {
|
||||
savedStdout, savedStderr := os.Stdout, os.Stderr
|
||||
devNull, err := os.Open(os.DevNull)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("can't process results: %s", err)
|
||||
return fmt.Errorf("can't open null device %q: %s", os.DevNull, err)
|
||||
}
|
||||
|
||||
return r.mergeResults(results), nil
|
||||
}
|
||||
// Don't allow linters to print anything
|
||||
os.Stdout, os.Stderr = devNull, devNull
|
||||
|
||||
func (r SimpleRunner) processResults(results []result.Result) ([]result.Result, error) {
|
||||
if len(r.Processors) == 0 {
|
||||
return results, nil
|
||||
lintResultsCh := make(chan lintRes, len(linters))
|
||||
tasksCh := make(chan Linter, len(linters))
|
||||
workersCount := lintCtx.Cfg.Run.Concurrency
|
||||
var wg sync.WaitGroup
|
||||
wg.Add(workersCount)
|
||||
r.runLinters(ctx, &wg, tasksCh, lintResultsCh, lintCtx, workersCount)
|
||||
|
||||
for _, linter := range linters {
|
||||
tasksCh <- linter
|
||||
}
|
||||
close(tasksCh)
|
||||
|
||||
for _, p := range r.Processors {
|
||||
var err error
|
||||
results, err = p.Process(results)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
go func() {
|
||||
wg.Wait()
|
||||
close(lintResultsCh)
|
||||
os.Stdout, os.Stderr = savedStdout, savedStderr
|
||||
}()
|
||||
|
||||
finishedN := 0
|
||||
for res := range lintResultsCh {
|
||||
if res.err != nil {
|
||||
logrus.Warnf("Can't run linter %s: %s", res.linter.Name(), res.err)
|
||||
continue
|
||||
}
|
||||
|
||||
finishedN++
|
||||
|
||||
if len(res.issues) != 0 {
|
||||
res.issues = r.processIssues(ctx, res.issues)
|
||||
}
|
||||
|
||||
for _, i := range res.issues {
|
||||
retIssues <- i
|
||||
}
|
||||
}
|
||||
|
||||
return results, nil
|
||||
// finalize processors: logging, clearing, no heavy work here
|
||||
for _, p := range r.Processors {
|
||||
p.Finish()
|
||||
}
|
||||
|
||||
if ctx.Err() != nil {
|
||||
return fmt.Errorf("%d/%d linters finished: deadline exceeded: try increase it by passing --deadline option",
|
||||
finishedN, len(linters))
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r SimpleRunner) mergeResults(results []result.Result) []result.Issue {
|
||||
issues := []result.Issue{}
|
||||
for _, r := range results {
|
||||
issues = append(issues, r.Issues...)
|
||||
func (r *SimpleRunner) processIssues(ctx context.Context, issues []result.Issue) []result.Issue {
|
||||
for _, p := range r.Processors {
|
||||
startedAt := time.Now()
|
||||
newIssues, err := p.Process(issues)
|
||||
elapsed := time.Since(startedAt)
|
||||
if elapsed > 50*time.Millisecond {
|
||||
logrus.Infof("Result processor %s took %s", p.Name(), elapsed)
|
||||
}
|
||||
if err != nil {
|
||||
logrus.Warnf("Can't process result by %s processor: %s", p.Name(), err)
|
||||
} else {
|
||||
issues = newIssues
|
||||
}
|
||||
if issues == nil {
|
||||
issues = []result.Issue{}
|
||||
}
|
||||
}
|
||||
|
||||
return issues
|
||||
|
3
pkg/testdata/not_compiles/main.go
vendored
Normal file
3
pkg/testdata/not_compiles/main.go
vendored
Normal file
@ -0,0 +1,3 @@
|
||||
package p
|
||||
|
||||
func F {
|
20
pkg/testdata/with_issues/deadcode.go
vendored
Normal file
20
pkg/testdata/with_issues/deadcode.go
vendored
Normal file
@ -0,0 +1,20 @@
|
||||
package testdata
|
||||
|
||||
var y int
|
||||
|
||||
var unused int // ERROR "`unused` is unused"
|
||||
|
||||
func f(x int) {
|
||||
}
|
||||
|
||||
func g(x int) { // ERROR "`g` is unused"
|
||||
}
|
||||
|
||||
func H(x int) {
|
||||
}
|
||||
|
||||
func init() {
|
||||
f(y)
|
||||
}
|
||||
|
||||
var _ int
|
32
pkg/testdata/with_issues/dupl.go
vendored
Normal file
32
pkg/testdata/with_issues/dupl.go
vendored
Normal file
@ -0,0 +1,32 @@
|
||||
package testdata
|
||||
|
||||
type DuplLogger struct{}
|
||||
|
||||
func (DuplLogger) level() int {
|
||||
return 1
|
||||
}
|
||||
|
||||
func (DuplLogger) Debug(args ...interface{}) {}
|
||||
func (DuplLogger) Info(args ...interface{}) {}
|
||||
|
||||
func (logger *DuplLogger) First(args ...interface{}) { // ERROR "12-21 lines are duplicate of `testdata/with_issues/dupl.go:23-32`"
|
||||
if logger.level() >= 0 {
|
||||
logger.Debug(args...)
|
||||
logger.Debug(args...)
|
||||
logger.Debug(args...)
|
||||
logger.Debug(args...)
|
||||
logger.Debug(args...)
|
||||
logger.Debug(args...)
|
||||
}
|
||||
}
|
||||
|
||||
func (logger *DuplLogger) Second(args ...interface{}) { // ERROR "23-32 lines are duplicate of `testdata/with_issues/dupl.go:12-21`"
|
||||
if logger.level() >= 1 {
|
||||
logger.Info(args...)
|
||||
logger.Info(args...)
|
||||
logger.Info(args...)
|
||||
logger.Info(args...)
|
||||
logger.Info(args...)
|
||||
logger.Info(args...)
|
||||
}
|
||||
}
|
44
pkg/testdata/with_issues/errcheck.go
vendored
Normal file
44
pkg/testdata/with_issues/errcheck.go
vendored
Normal file
@ -0,0 +1,44 @@
|
||||
package testdata
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"net/http"
|
||||
"os"
|
||||
)
|
||||
|
||||
func RetErr() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func MissedErrorCheck() {
|
||||
RetErr() // ERROR "Error return value of `RetErr` is not checked"
|
||||
}
|
||||
|
||||
func IgnoreCloseMissingErrHandling() error {
|
||||
f, err := os.Open("t.go")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
f.Close()
|
||||
return nil
|
||||
}
|
||||
|
||||
func IgnoreCloseInDeferMissingErrHandling() {
|
||||
resp, err := http.Get("http://example.com/")
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
panic(resp)
|
||||
}
|
||||
|
||||
func IgnoreStdxWrite() {
|
||||
os.Stdout.Write([]byte{})
|
||||
os.Stderr.Write([]byte{})
|
||||
}
|
||||
|
||||
func IgnoreBufferWrites(buf *bytes.Buffer) {
|
||||
buf.WriteString("x")
|
||||
}
|
11
pkg/testdata/with_issues/gas.go
vendored
Normal file
11
pkg/testdata/with_issues/gas.go
vendored
Normal file
@ -0,0 +1,11 @@
|
||||
package testdata
|
||||
|
||||
import (
|
||||
"crypto/md5" // ERROR "G501: Blacklisted import crypto/md5: weak cryptographic primitive"
|
||||
"log"
|
||||
)
|
||||
|
||||
func Gas() {
|
||||
h := md5.New() // ERROR "G401: Use of weak cryptographic primitive"
|
||||
log.Print(h)
|
||||
}
|
30
pkg/testdata/with_issues/goconst.go
vendored
Normal file
30
pkg/testdata/with_issues/goconst.go
vendored
Normal file
@ -0,0 +1,30 @@
|
||||
package testdata
|
||||
|
||||
import "fmt"
|
||||
|
||||
func GoconstA() { // nolint:dupl
|
||||
a := "needconst" // ERROR "string `needconst` has 5 occurrences, make it a constant"
|
||||
fmt.Print(a)
|
||||
b := "needconst"
|
||||
fmt.Print(b)
|
||||
c := "needconst"
|
||||
fmt.Print(c)
|
||||
}
|
||||
|
||||
func GoconstB() {
|
||||
a := "needconst"
|
||||
fmt.Print(a)
|
||||
b := "needconst"
|
||||
fmt.Print(b)
|
||||
}
|
||||
|
||||
const AlreadyHasConst = "alreadyhasconst"
|
||||
|
||||
func GoconstC() { // nolint:dupl
|
||||
a := "alreadyhasconst" // ERROR "string `alreadyhasconst` has 3 occurrences, but such constant `AlreadyHasConst` already exists"
|
||||
fmt.Print(a)
|
||||
b := "alreadyhasconst"
|
||||
fmt.Print(b)
|
||||
c := "alreadyhasconst"
|
||||
fmt.Print(c)
|
||||
}
|
15
pkg/testdata/with_issues/gocyclo.go
vendored
Normal file
15
pkg/testdata/with_issues/gocyclo.go
vendored
Normal file
@ -0,0 +1,15 @@
|
||||
package testdata
|
||||
|
||||
func GocycloBigComplexity(s string) { // ERROR "cyclomatic complexity .* of func .* is high .*"
|
||||
if s == "1" || s == "2" || s == "3" || s == "4" || s == "5" || s == "6" || s == "7" { // nolint:dupl
|
||||
return
|
||||
}
|
||||
|
||||
if s == "1" || s == "2" || s == "3" || s == "4" || s == "5" || s == "6" || s == "7" { // nolint:dupl
|
||||
return
|
||||
}
|
||||
|
||||
if s == "1" || s == "2" || s == "3" || s == "4" || s == "5" || s == "6" || s == "7" { // nolint:dupl
|
||||
return
|
||||
}
|
||||
}
|
8
pkg/testdata/with_issues/gofmt.go
vendored
Normal file
8
pkg/testdata/with_issues/gofmt.go
vendored
Normal file
@ -0,0 +1,8 @@
|
||||
package testdata
|
||||
|
||||
import "fmt"
|
||||
|
||||
func GofmtNotSimplified() {
|
||||
var x []string
|
||||
fmt.Print(x[1:len(x)]) // nolint:megacheck // ERROR "File is not gofmt-ed with -s"
|
||||
}
|
11
pkg/testdata/with_issues/goimports.go
vendored
Normal file
11
pkg/testdata/with_issues/goimports.go
vendored
Normal file
@ -0,0 +1,11 @@
|
||||
package testdata
|
||||
|
||||
import (
|
||||
"fmt" // ERROR "File is not goimports-ed"
|
||||
"github.com/golangci/golangci-lint/pkg/config"
|
||||
)
|
||||
|
||||
func Bar() {
|
||||
fmt.Print("x")
|
||||
_ = config.Config{}
|
||||
}
|
21
pkg/testdata/with_issues/golint.go
vendored
Normal file
21
pkg/testdata/with_issues/golint.go
vendored
Normal file
@ -0,0 +1,21 @@
|
||||
package testdata
|
||||
|
||||
var Go_lint string // ERROR "don't use underscores in Go names; var Go_lint should be GoLint"
|
||||
|
||||
func ExportedFuncWithNoComment() {
|
||||
}
|
||||
|
||||
var ExportedVarWithNoComment string
|
||||
|
||||
type ExportedStructWithNoComment struct{}
|
||||
|
||||
type ExportedInterfaceWithNoComment interface{}
|
||||
|
||||
// Bad comment // ERROR "comment on exported function ExportedFuncWithBadComment should be of the form .ExportedFuncWithBadComment \.\.\.."
|
||||
func ExportedFuncWithBadComment() {}
|
||||
|
||||
type GolintTest struct{}
|
||||
|
||||
func (receiver1 GolintTest) A() {}
|
||||
|
||||
func (receiver2 GolintTest) B() {} // ERROR "receiver name receiver2 should be consistent with previous receiver name receiver1 for GolintTest"
|
22
pkg/testdata/with_issues/govet.go
vendored
Normal file
22
pkg/testdata/with_issues/govet.go
vendored
Normal file
@ -0,0 +1,22 @@
|
||||
package testdata
|
||||
|
||||
import (
|
||||
"io"
|
||||
"os"
|
||||
)
|
||||
|
||||
func Govet() error {
|
||||
return &os.PathError{"first", "path", os.ErrNotExist} // ERROR "os.PathError composite literal uses unkeyed fields"
|
||||
}
|
||||
|
||||
func GovetShadow(f io.Reader, buf []byte) (err error) {
|
||||
if f != nil {
|
||||
_, err := f.Read(buf) // ERROR "declaration of .err. shadows declaration at testdata/with_issues/govet.go:\d+"
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
// Use variable to trigger shadowing error
|
||||
_ = err
|
||||
return
|
||||
}
|
10
pkg/testdata/with_issues/ineffassign.go
vendored
Normal file
10
pkg/testdata/with_issues/ineffassign.go
vendored
Normal file
@ -0,0 +1,10 @@
|
||||
package testdata
|
||||
|
||||
func _() {
|
||||
x := 0
|
||||
for {
|
||||
_ = x
|
||||
x = 0 // ERROR "ineffectual assignment to `x`"
|
||||
x = 0
|
||||
}
|
||||
}
|
7
pkg/testdata/with_issues/interfacer.go
vendored
Normal file
7
pkg/testdata/with_issues/interfacer.go
vendored
Normal file
@ -0,0 +1,7 @@
|
||||
package testdata
|
||||
|
||||
import "io"
|
||||
|
||||
func InterfacerCheck(f io.ReadCloser) { // ERROR "f can be io.Closer"
|
||||
f.Close()
|
||||
}
|
7
pkg/testdata/with_issues/maligned.go
vendored
Normal file
7
pkg/testdata/with_issues/maligned.go
vendored
Normal file
@ -0,0 +1,7 @@
|
||||
package testdata
|
||||
|
||||
type BadAlignedStruct struct { // ERROR "struct of size 24 bytes could be of size 16 bytes"
|
||||
B bool
|
||||
I int
|
||||
B2 bool
|
||||
}
|
6
pkg/testdata/with_issues/megacheck.go
vendored
Normal file
6
pkg/testdata/with_issues/megacheck.go
vendored
Normal file
@ -0,0 +1,6 @@
|
||||
package testdata
|
||||
|
||||
func Megacheck() {
|
||||
var x int
|
||||
x = x // nolint:ineffassign // ERROR "self-assignment of x to x"
|
||||
}
|
5
pkg/testdata/with_issues/structcheck.go
vendored
Normal file
5
pkg/testdata/with_issues/structcheck.go
vendored
Normal file
@ -0,0 +1,5 @@
|
||||
package testdata
|
||||
|
||||
type t struct { // ERROR "`t` is unused"
|
||||
unusedField int // ERROR "`unusedField` is unused"
|
||||
}
|
9
pkg/testdata/with_issues/unconvert.go
vendored
Normal file
9
pkg/testdata/with_issues/unconvert.go
vendored
Normal file
@ -0,0 +1,9 @@
|
||||
package testdata
|
||||
|
||||
import "log"
|
||||
|
||||
func Unconvert() {
|
||||
a := 1
|
||||
b := int(a) // ERROR "unnecessary conversion"
|
||||
log.Print(b)
|
||||
}
|
3
pkg/testdata/with_issues/varcheck.go
vendored
Normal file
3
pkg/testdata/with_issues/varcheck.go
vendored
Normal file
@ -0,0 +1,3 @@
|
||||
package testdata
|
||||
|
||||
var v string // ERROR "`v` is unused"
|
30
vendor/github.com/GoASTScanner/gas/.gitignore
generated
vendored
Normal file
30
vendor/github.com/GoASTScanner/gas/.gitignore
generated
vendored
Normal file
@ -0,0 +1,30 @@
|
||||
# Compiled Object files, Static and Dynamic libs (Shared Objects)
|
||||
*.o
|
||||
*.a
|
||||
*.so
|
||||
*.swp
|
||||
|
||||
# Folders
|
||||
_obj
|
||||
_test
|
||||
vendor
|
||||
|
||||
# Architecture specific extensions/prefixes
|
||||
*.[568vq]
|
||||
[568vq].out
|
||||
|
||||
*.cgo1.go
|
||||
*.cgo2.c
|
||||
_cgo_defun.c
|
||||
_cgo_gotypes.go
|
||||
_cgo_export.*
|
||||
|
||||
_testmain.go
|
||||
|
||||
*.exe
|
||||
*.test
|
||||
*.prof
|
||||
|
||||
.DS_Store
|
||||
|
||||
.vscode
|
20
vendor/github.com/GoASTScanner/gas/.travis.yml
generated
vendored
Normal file
20
vendor/github.com/GoASTScanner/gas/.travis.yml
generated
vendored
Normal file
@ -0,0 +1,20 @@
|
||||
language: go
|
||||
|
||||
go:
|
||||
- 1.8
|
||||
- 1.9
|
||||
- "1.10"
|
||||
- tip
|
||||
|
||||
install:
|
||||
- go get -u github.com/golang/dep/cmd/dep
|
||||
- go get -u github.com/golang/lint/golint
|
||||
- go get -u github.com/onsi/ginkgo/ginkgo
|
||||
- go get -u github.com/onsi/gomega
|
||||
- go get -u golang.org/x/crypto/ssh
|
||||
- go get -u github.com/GoASTScanner/gas/cmd/gas/...
|
||||
- go get -v -t ./...
|
||||
- export PATH=$PATH:$HOME/gopath/bin
|
||||
|
||||
script: make test
|
||||
|
8
vendor/github.com/GoASTScanner/gas/Dockerfile
generated
vendored
Normal file
8
vendor/github.com/GoASTScanner/gas/Dockerfile
generated
vendored
Normal file
@ -0,0 +1,8 @@
|
||||
FROM golang:1.9.4-alpine3.7
|
||||
|
||||
ENV BIN=gas
|
||||
|
||||
COPY build/*-linux-amd64 /go/bin/$BIN
|
||||
COPY docker-entrypoint.sh /usr/local/bin
|
||||
|
||||
ENTRYPOINT ["docker-entrypoint.sh"]
|
134
vendor/github.com/GoASTScanner/gas/Gopkg.lock
generated
vendored
Normal file
134
vendor/github.com/GoASTScanner/gas/Gopkg.lock
generated
vendored
Normal file
@ -0,0 +1,134 @@
|
||||
# This file is autogenerated, do not edit; changes may be undone by the next 'dep ensure'.
|
||||
|
||||
|
||||
[[projects]]
|
||||
name = "github.com/kisielk/gotool"
|
||||
packages = ["."]
|
||||
revision = "0de1eaf82fa3f583ce21fde859f1e7e0c5e9b220"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
name = "github.com/mozilla/tls-observatory"
|
||||
packages = ["constants"]
|
||||
revision = "8791a200eb40f8625a152bfb8336171305f5f35c"
|
||||
|
||||
[[projects]]
|
||||
name = "github.com/nbutton23/zxcvbn-go"
|
||||
packages = [
|
||||
".",
|
||||
"adjacency",
|
||||
"data",
|
||||
"entropy",
|
||||
"frequency",
|
||||
"match",
|
||||
"matching",
|
||||
"scoring",
|
||||
"utils/math"
|
||||
]
|
||||
revision = "a22cb81b2ecdde8b68e9ffb8824731cbf88e1de4"
|
||||
|
||||
[[projects]]
|
||||
name = "github.com/onsi/ginkgo"
|
||||
packages = [
|
||||
".",
|
||||
"config",
|
||||
"internal/codelocation",
|
||||
"internal/containernode",
|
||||
"internal/failer",
|
||||
"internal/leafnodes",
|
||||
"internal/remote",
|
||||
"internal/spec",
|
||||
"internal/spec_iterator",
|
||||
"internal/specrunner",
|
||||
"internal/suite",
|
||||
"internal/testingtproxy",
|
||||
"internal/writer",
|
||||
"reporters",
|
||||
"reporters/stenographer",
|
||||
"reporters/stenographer/support/go-colorable",
|
||||
"reporters/stenographer/support/go-isatty",
|
||||
"types"
|
||||
]
|
||||
revision = "11459a886d9cd66b319dac7ef1e917ee221372c9"
|
||||
|
||||
[[projects]]
|
||||
name = "github.com/onsi/gomega"
|
||||
packages = [
|
||||
".",
|
||||
"format",
|
||||
"internal/assertion",
|
||||
"internal/asyncassertion",
|
||||
"internal/oraclematcher",
|
||||
"internal/testingtsupport",
|
||||
"matchers",
|
||||
"matchers/support/goraph/bipartitegraph",
|
||||
"matchers/support/goraph/edge",
|
||||
"matchers/support/goraph/node",
|
||||
"matchers/support/goraph/util",
|
||||
"types"
|
||||
]
|
||||
revision = "dcabb60a477c2b6f456df65037cb6708210fbb02"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
name = "github.com/ryanuber/go-glob"
|
||||
packages = ["."]
|
||||
revision = "256dc444b735e061061cf46c809487313d5b0065"
|
||||
|
||||
[[projects]]
|
||||
name = "golang.org/x/net"
|
||||
packages = [
|
||||
"html",
|
||||
"html/atom",
|
||||
"html/charset"
|
||||
]
|
||||
revision = "8351a756f30f1297fe94bbf4b767ec589c6ea6d0"
|
||||
|
||||
[[projects]]
|
||||
name = "golang.org/x/sys"
|
||||
packages = ["unix"]
|
||||
revision = "164713f0dfcec4e80be8b53e1f0811f5f0d84578"
|
||||
|
||||
[[projects]]
|
||||
name = "golang.org/x/text"
|
||||
packages = [
|
||||
"encoding",
|
||||
"encoding/charmap",
|
||||
"encoding/htmlindex",
|
||||
"encoding/internal",
|
||||
"encoding/internal/identifier",
|
||||
"encoding/japanese",
|
||||
"encoding/korean",
|
||||
"encoding/simplifiedchinese",
|
||||
"encoding/traditionalchinese",
|
||||
"encoding/unicode",
|
||||
"internal/gen",
|
||||
"internal/tag",
|
||||
"internal/utf8internal",
|
||||
"language",
|
||||
"runes",
|
||||
"transform",
|
||||
"unicode/cldr"
|
||||
]
|
||||
revision = "1cbadb444a806fd9430d14ad08967ed91da4fa0a"
|
||||
|
||||
[[projects]]
|
||||
name = "golang.org/x/tools"
|
||||
packages = [
|
||||
"go/ast/astutil",
|
||||
"go/buildutil",
|
||||
"go/loader"
|
||||
]
|
||||
revision = "e531a2a1c15f94033f6fa87666caeb19a688175f"
|
||||
|
||||
[[projects]]
|
||||
name = "gopkg.in/yaml.v2"
|
||||
packages = ["."]
|
||||
revision = "eb3733d160e74a9c7e442f435eb3bea458e1d19f"
|
||||
|
||||
[solve-meta]
|
||||
analyzer-name = "dep"
|
||||
analyzer-version = 1
|
||||
inputs-digest = "706e049cd8b8db8705af09e7a375a999d01373a409beadc850c80d64de9849fd"
|
||||
solver-name = "gps-cdcl"
|
||||
solver-version = 1
|
38
vendor/github.com/GoASTScanner/gas/Gopkg.toml
generated
vendored
Normal file
38
vendor/github.com/GoASTScanner/gas/Gopkg.toml
generated
vendored
Normal file
@ -0,0 +1,38 @@
|
||||
# Gopkg.toml example
|
||||
#
|
||||
# Refer to https://golang.github.io/dep/docs/Gopkg.toml.html
|
||||
# for detailed Gopkg.toml documentation.
|
||||
#
|
||||
# required = ["github.com/user/thing/cmd/thing"]
|
||||
# ignored = ["github.com/user/project/pkgX", "bitbucket.org/user/project/pkgA/pkgY"]
|
||||
#
|
||||
# [[constraint]]
|
||||
# name = "github.com/user/project"
|
||||
# version = "1.0.0"
|
||||
#
|
||||
# [[constraint]]
|
||||
# name = "github.com/user/project2"
|
||||
# branch = "dev"
|
||||
# source = "github.com/myfork/project2"
|
||||
#
|
||||
# [[override]]
|
||||
# name = "github.com/x/y"
|
||||
# version = "2.4.0"
|
||||
#
|
||||
# [prune]
|
||||
# non-go = false
|
||||
# go-tests = true
|
||||
# unused-packages = true
|
||||
|
||||
|
||||
[[constraint]]
|
||||
branch = "master"
|
||||
name = "github.com/mozilla/tls-observatory"
|
||||
|
||||
[[constraint]]
|
||||
branch = "master"
|
||||
name = "github.com/ryanuber/go-glob"
|
||||
|
||||
[prune]
|
||||
go-tests = true
|
||||
unused-packages = true
|
154
vendor/github.com/GoASTScanner/gas/LICENSE.txt
generated
vendored
Normal file
154
vendor/github.com/GoASTScanner/gas/LICENSE.txt
generated
vendored
Normal file
@ -0,0 +1,154 @@
|
||||
Apache License
|
||||
|
||||
Version 2.0, January 2004
|
||||
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction, and
|
||||
distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by the copyright
|
||||
owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all other entities
|
||||
that control, are controlled by, or are under common control with that entity.
|
||||
For the purposes of this definition, "control" means (i) the power, direct or
|
||||
indirect, to cause the direction or management of such entity, whether by
|
||||
contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity exercising
|
||||
permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications, including
|
||||
but not limited to software source code, documentation source, and configuration
|
||||
files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical transformation or
|
||||
translation of a Source form, including but not limited to compiled object code,
|
||||
generated documentation, and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or Object form, made
|
||||
available under the License, as indicated by a copyright notice that is included
|
||||
in or attached to the work (an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object form, that
|
||||
is based on (or derived from) the Work and for which the editorial revisions,
|
||||
annotations, elaborations, or other modifications represent, as a whole, an
|
||||
original work of authorship. For the purposes of this License, Derivative Works
|
||||
shall not include works that remain separable from, or merely link (or bind by
|
||||
name) to the interfaces of, the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including the original version
|
||||
of the Work and any modifications or additions to that Work or Derivative Works
|
||||
thereof, that is intentionally submitted to Licensor for inclusion in the Work
|
||||
by the copyright owner or by an individual or Legal Entity authorized to submit
|
||||
on behalf of the copyright owner. For the purposes of this definition,
|
||||
"submitted" means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems, and
|
||||
issue tracking systems that are managed by, or on behalf of, the Licensor for
|
||||
the purpose of discussing and improving the Work, but excluding communication
|
||||
that is conspicuously marked or otherwise designated in writing by the copyright
|
||||
owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity on behalf
|
||||
of whom a Contribution has been received by Licensor and subsequently
|
||||
incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of this
|
||||
License, each Contributor hereby grants to You a perpetual, worldwide,
|
||||
non-exclusive, no-charge, royalty-free, irrevocable copyright license to
|
||||
reproduce, prepare Derivative Works of, publicly display, publicly perform,
|
||||
sublicense, and distribute the Work and such Derivative Works in Source or
|
||||
Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of this License,
|
||||
each Contributor hereby grants to You a perpetual, worldwide, non-exclusive,
|
||||
no-charge, royalty-free, irrevocable (except as stated in this section) patent
|
||||
license to make, have made, use, offer to sell, sell, import, and otherwise
|
||||
transfer the Work, where such license applies only to those patent claims
|
||||
licensable by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s) with the Work
|
||||
to which such Contribution(s) was submitted. If You institute patent litigation
|
||||
against any entity (including a cross-claim or counterclaim in a lawsuit)
|
||||
alleging that the Work or a Contribution incorporated within the Work
|
||||
constitutes direct or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate as of the date
|
||||
such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the Work or
|
||||
Derivative Works thereof in any medium, with or without modifications, and in
|
||||
Source or Object form, provided that You meet the following conditions:
|
||||
|
||||
You must give any other recipients of the Work or Derivative Works a copy of
|
||||
this License; and You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and You must retain, in the Source form of
|
||||
any Derivative Works that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work, excluding those notices
|
||||
that do not pertain to any part of the Derivative Works; and If the Work
|
||||
includes a "NOTICE" text file as part of its distribution, then any Derivative
|
||||
Works that You distribute must include a readable copy of the attribution
|
||||
notices contained within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one of the following
|
||||
places: within a NOTICE text file distributed as part of the Derivative Works;
|
||||
within the Source form or documentation, if provided along with the Derivative
|
||||
Works; or, within a display generated by the Derivative Works, if and wherever
|
||||
such third-party notices normally appear. The contents of the NOTICE file are
|
||||
for informational purposes only and do not modify the License. You may add Your
|
||||
own attribution notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided that such
|
||||
additional attribution notices cannot be construed as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and may provide
|
||||
additional or different license terms and conditions for use, reproduction, or
|
||||
distribution of Your modifications, or for any such Derivative Works as a whole,
|
||||
provided Your use, reproduction, and distribution of the Work otherwise complies
|
||||
with the conditions stated in this License. 5. Submission of Contributions.
|
||||
Unless You explicitly state otherwise, any Contribution intentionally submitted
|
||||
for inclusion in the Work by You to the Licensor shall be under the terms and
|
||||
conditions of this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify the terms of
|
||||
any separate license agreement you may have executed with Licensor regarding
|
||||
such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade names,
|
||||
trademarks, service marks, or product names of the Licensor, except as required
|
||||
for reasonable and customary use in describing the origin of the Work and
|
||||
reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or agreed to in
|
||||
writing, Licensor provides the Work (and each Contributor provides its
|
||||
Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
KIND, either express or implied, including, without limitation, any warranties
|
||||
or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any risks
|
||||
associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory, whether in
|
||||
tort (including negligence), contract, or otherwise, unless required by
|
||||
applicable law (such as deliberate and grossly negligent acts) or agreed to in
|
||||
writing, shall any Contributor be liable to You for damages, including any
|
||||
direct, indirect, special, incidental, or consequential damages of any character
|
||||
arising as a result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill, work stoppage,
|
||||
computer failure or malfunction, or any and all other commercial damages or
|
||||
losses), even if such Contributor has been advised of the possibility of such
|
||||
damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing the Work or
|
||||
Derivative Works thereof, You may choose to offer, and charge a fee for,
|
||||
acceptance of support, warranty, indemnity, or other liability obligations
|
||||
and/or rights consistent with this License. However, in accepting such
|
||||
obligations, You may act only on Your own behalf and on Your sole
|
||||
responsibility, not on behalf of any other Contributor, and only if You agree to
|
||||
indemnify, defend, and hold each Contributor harmless for any liability incurred
|
||||
by, or claims asserted against, such Contributor by reason of your accepting any
|
||||
such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
48
vendor/github.com/GoASTScanner/gas/Makefile
generated
vendored
Normal file
48
vendor/github.com/GoASTScanner/gas/Makefile
generated
vendored
Normal file
@ -0,0 +1,48 @@
|
||||
GIT_TAG?= $(shell git describe --always --tags)
|
||||
BUILD_DATE = $(shell date +%Y-%m-%d)
|
||||
BIN = gas
|
||||
BUILD_CMD = go build -ldflags "-X main.Version=${VERSION} -X main.GitTag=${GIT_TAG} -X main.BuildDate=${BUILD_DATE}" -o build/$(BIN)-$(VERSION)-$${GOOS}-$${GOARCH} ./cmd/gas/ &
|
||||
FMT_CMD = $(gofmt -s -l -w $(find . -type f -name '*.go' -not -path './vendor/*') | tee /dev/stderr)
|
||||
IMAGE_REPO = docker.io
|
||||
|
||||
default:
|
||||
$(MAKE) bootstrap
|
||||
$(MAKE) build
|
||||
|
||||
test: bootstrap
|
||||
test -z '$(FMT_CMD)'
|
||||
go vet $(go list ./... | grep -v /vendor/)
|
||||
golint -set_exit_status $(shell go list ./... | grep -v vendor)
|
||||
gas ./...
|
||||
ginkgo -r -v
|
||||
bootstrap:
|
||||
dep ensure
|
||||
build:
|
||||
go build -o $(BIN) ./cmd/gas/
|
||||
clean:
|
||||
rm -rf build vendor
|
||||
rm -f release image bootstrap $(BIN)
|
||||
release: bootstrap
|
||||
ifndef VERSION
|
||||
$(error VERSION flag is not set. Run 'make release VERSION=<YOUR VERSION>'.)
|
||||
endif
|
||||
@echo "Running build command..."
|
||||
bash -c '\
|
||||
export GOOS=linux; export GOARCH=amd64; export CGO_ENABLED=0; $(BUILD_CMD) \
|
||||
wait \
|
||||
'
|
||||
touch release
|
||||
|
||||
image: release
|
||||
@echo "Building the Docker image..."
|
||||
docker build -t $(IMAGE_REPO)/$(BIN):$(VERSION) .
|
||||
docker tag $(IMAGE_REPO)/$(BIN):$(VERSION) $(IMAGE_REPO)/$(BIN):latest
|
||||
touch image
|
||||
|
||||
image-push: image
|
||||
@echo "Pushing the Docker image..."
|
||||
docker push $(IMAGE_REPO)/$(BIN):$(VERSION)
|
||||
docker push $(IMAGE_REPO)/$(BIN):latest
|
||||
|
||||
.PHONY: test build clean image-push
|
||||
|
199
vendor/github.com/GoASTScanner/gas/README.md
generated
vendored
Normal file
199
vendor/github.com/GoASTScanner/gas/README.md
generated
vendored
Normal file
@ -0,0 +1,199 @@
|
||||
|
||||
|
||||
## GAS - Go AST Scanner
|
||||
|
||||
Inspects source code for security problems by scanning the Go AST.
|
||||
|
||||
### License
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License [here](http://www.apache.org/licenses/LICENSE-2.0).
|
||||
|
||||
### Project status
|
||||
|
||||
[](https://travis-ci.org/GoASTScanner/gas)
|
||||
[](https://godoc.org/github.com/GoASTScanner/gas)
|
||||
|
||||
Gas is still in alpha and accepting feedback from early adopters. We do
|
||||
not consider it production ready at this time.
|
||||
|
||||
### Install
|
||||
|
||||
`$ go get github.com/GoASTScanner/gas/cmd/gas/...`
|
||||
|
||||
### Usage
|
||||
|
||||
Gas can be configured to only run a subset of rules, to exclude certain file
|
||||
paths, and produce reports in different formats. By default all rules will be
|
||||
run against the supplied input files. To recursively scan from the current
|
||||
directory you can supply './...' as the input argument.
|
||||
|
||||
#### Selecting rules
|
||||
|
||||
By default Gas will run all rules against the supplied file paths. It is however possible to select a subset of rules to run via the '-include=' flag,
|
||||
or to specify a set of rules to explicitly exclude using the '-exclude=' flag.
|
||||
|
||||
##### Available rules
|
||||
|
||||
- G101: Look for hardcoded credentials
|
||||
- G102: Bind to all interfaces
|
||||
- G103: Audit the use of unsafe block
|
||||
- G104: Audit errors not checked
|
||||
- G105: Audit the use of math/big.Int.Exp
|
||||
- G106: Audit the use of ssh.InsecureIgnoreHostKey
|
||||
- G201: SQL query construction using format string
|
||||
- G202: SQL query construction using string concatenation
|
||||
- G203: Use of unescaped data in HTML templates
|
||||
- G204: Audit use of command execution
|
||||
- G301: Poor file permissions used when creating a directory
|
||||
- G302: Poor file permisions used with chmod
|
||||
- G303: Creating tempfile using a predictable path
|
||||
- G304: File path provided as taint input
|
||||
- G401: Detect the usage of DES, RC4, or MD5
|
||||
- G402: Look for bad TLS connection settings
|
||||
- G403: Ensure minimum RSA key length of 2048 bits
|
||||
- G404: Insecure random number source (rand)
|
||||
- G501: Import blacklist: crypto/md5
|
||||
- G502: Import blacklist: crypto/des
|
||||
- G503: Import blacklist: crypto/rc4
|
||||
- G504: Import blacklist: net/http/cgi
|
||||
|
||||
|
||||
```
|
||||
# Run a specific set of rules
|
||||
$ gas -include=G101,G203,G401 ./...
|
||||
|
||||
# Run everything except for rule G303
|
||||
$ gas -exclude=G303 ./...
|
||||
```
|
||||
|
||||
#### Excluding files:
|
||||
|
||||
Gas will ignore dependencies in your vendor directory any files
|
||||
that are not considered build artifacts by the compiler (so test files).
|
||||
|
||||
#### Annotating code
|
||||
|
||||
As with all automated detection tools there will be cases of false positives. In cases where Gas reports a failure that has been manually verified as being safe it is possible to annotate the code with a '#nosec' comment.
|
||||
|
||||
The annotation causes Gas to stop processing any further nodes within the
|
||||
AST so can apply to a whole block or more granularly to a single expression.
|
||||
|
||||
```go
|
||||
|
||||
import "md5" // #nosec
|
||||
|
||||
|
||||
func main(){
|
||||
|
||||
/* #nosec */
|
||||
if x > y {
|
||||
h := md5.New() // this will also be ignored
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
In some cases you may also want to revisit places where #nosec annotations
|
||||
have been used. To run the scanner and ignore any #nosec annotations you
|
||||
can do the following:
|
||||
|
||||
```
|
||||
$ gas -nosec=true ./...
|
||||
```
|
||||
#### Build tags
|
||||
|
||||
Gas is able to pass your [Go build tags](https://golang.org/pkg/go/build/) to the analyzer.
|
||||
They can be provided as a comma separated list as follows:
|
||||
|
||||
```
|
||||
$ gas -tag debug,ignore ./...
|
||||
```
|
||||
|
||||
### Output formats
|
||||
|
||||
Gas currently supports text, json, yaml, csv and JUnit XML output formats. By default
|
||||
results will be reported to stdout, but can also be written to an output
|
||||
file. The output format is controlled by the '-fmt' flag, and the output file is controlled by the '-out' flag as follows:
|
||||
|
||||
```
|
||||
# Write output in json format to results.json
|
||||
$ gas -fmt=json -out=results.json *.go
|
||||
```
|
||||
### Development
|
||||
|
||||
#### Prerequisites
|
||||
|
||||
Install dep according to the instructions here: https://github.com/golang/dep
|
||||
Install the latest version of golint: https://github.com/golang/lint
|
||||
|
||||
#### Build
|
||||
|
||||
```
|
||||
make
|
||||
```
|
||||
|
||||
#### Tests
|
||||
|
||||
```
|
||||
make test
|
||||
```
|
||||
|
||||
#### Release Build
|
||||
|
||||
Gas can be released as follows:
|
||||
|
||||
```bash
|
||||
make release VERSION=2.0.0
|
||||
```
|
||||
|
||||
The released version of the tool is available in the `build` folder. The build information should be displayed in the usage text.
|
||||
|
||||
```
|
||||
./build/gas-2.0.0-linux-amd64 -h
|
||||
|
||||
GAS - Go AST Scanner
|
||||
|
||||
Gas analyzes Go source code to look for common programming mistakes that
|
||||
can lead to security problems.
|
||||
|
||||
VERSION: 2.0.0
|
||||
GIT TAG: 96489ff
|
||||
BUILD DATE: 2018-02-21
|
||||
|
||||
```
|
||||
|
||||
#### Docker image
|
||||
|
||||
You can execute a release and build the docker image as follows:
|
||||
|
||||
```
|
||||
make image VERSION=2.0.0
|
||||
```
|
||||
|
||||
Now you can run the gas tool in a container against your local workspace:
|
||||
|
||||
```
|
||||
docker run -it -v <YOUR LOCAL WORKSPACE>:/workspace gas /workspace
|
||||
```
|
||||
|
||||
#### Generate TLS rule
|
||||
|
||||
The configuration of TLS rule can be generated from [Mozilla's TLS ciphers recommendation](https://statics.tls.security.mozilla.org/server-side-tls-conf.json).
|
||||
|
||||
|
||||
First you need to install the generator tool:
|
||||
|
||||
```
|
||||
go get github.com/GoASTScanner/gas/cmd/tlsconfig/...
|
||||
```
|
||||
|
||||
You can invoke now the `go generate` in the root of the project:
|
||||
|
||||
```
|
||||
go generate ./...
|
||||
```
|
||||
|
||||
This will generate the `rules/tls_config.go` file with will contain the current ciphers recommendation from Mozilla.
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user