Merge pull request from golangci/feature/initial-improvements

Feature/initial improvements
This commit is contained in:
golangci 2018-05-11 22:16:10 +03:00 committed by GitHub
commit e8d7807944
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
995 changed files with 323818 additions and 998 deletions

3
.gitignore vendored

@ -1 +1,2 @@
/vendor/
/*.txt
/*.pprof

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

@ -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

@ -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

@ -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

@ -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
}

@ -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()
}

@ -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

@ -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

@ -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

@ -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)
}
}

@ -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

@ -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

@ -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

@ -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

@ -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

@ -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

@ -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

@ -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

@ -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

@ -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),
})
}

@ -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
}

@ -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

@ -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
}

@ -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
}

@ -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)
}

@ -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

@ -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

@ -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

@ -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

@ -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

@ -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
}

@ -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() {}

@ -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
}

@ -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
}

@ -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"))
}

@ -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)
}
})
}

@ -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
}

@ -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() {}

@ -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
}

@ -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)
}
}

@ -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
}

@ -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() {}

@ -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)
}

@ -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()
}

@ -0,0 +1,5 @@
package testdata
var nolintSpecific int // nolint:gofmt
var nolintAll int // nolint
var nolintAndAppendix int // nolint Some My Text

@ -0,0 +1,4 @@
// Code generated by ... DO NOT EDIT.
package testdata
var v int

@ -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
}

@ -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
}

@ -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

@ -0,0 +1,3 @@
package p
func F {

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

@ -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

@ -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

@ -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

@ -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

@ -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

@ -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

@ -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

@ -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

@ -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

@ -0,0 +1,10 @@
package testdata
func _() {
x := 0
for {
_ = x
x = 0 // ERROR "ineffectual assignment to `x`"
x = 0
}
}

@ -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

@ -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

@ -0,0 +1,6 @@
package testdata
func Megacheck() {
var x int
x = x // nolint:ineffassign // ERROR "self-assignment of x to x"
}

@ -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

@ -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

@ -0,0 +1,3 @@
package testdata
var v string // ERROR "`v` is unused"

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

@ -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

@ -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

@ -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

@ -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

@ -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

@ -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

@ -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
[![Build Status](https://travis-ci.org/GoASTScanner/gas.svg?branch=master)](https://travis-ci.org/GoASTScanner/gas)
[![GoDoc](https://godoc.org/github.com/GoASTScanner/gas?status.svg)](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