2018-05-05 09:24:37 +03:00
package commands
import (
"context"
"fmt"
2018-05-08 17:13:16 +03:00
"go/token"
2018-05-26 19:58:17 +03:00
"io/ioutil"
2018-05-05 09:24:37 +03:00
"log"
2018-05-11 22:01:57 +03:00
"os"
2018-05-12 10:13:37 +03:00
"runtime"
2018-05-05 09:24:37 +03:00
"strings"
2018-05-06 19:08:34 +03:00
"time"
2018-05-05 09:24:37 +03:00
2018-06-02 15:32:40 +03:00
"github.com/fatih/color"
2018-05-05 09:24:37 +03:00
"github.com/golangci/golangci-lint/pkg/config"
2018-05-31 23:53:01 +03:00
"github.com/golangci/golangci-lint/pkg/lint"
2018-06-02 11:36:50 +03:00
"github.com/golangci/golangci-lint/pkg/lint/lintersdb"
2018-05-08 11:54:30 +03:00
"github.com/golangci/golangci-lint/pkg/printers"
2018-05-05 09:24:37 +03:00
"github.com/golangci/golangci-lint/pkg/result"
2018-05-05 19:43:18 +03:00
"github.com/golangci/golangci-lint/pkg/result/processors"
2018-05-07 21:44:40 +03:00
"github.com/sirupsen/logrus"
2018-05-05 09:24:37 +03:00
"github.com/spf13/cobra"
2018-05-12 10:13:37 +03:00
"github.com/spf13/pflag"
2018-05-05 09:24:37 +03:00
)
2018-05-30 09:45:08 +03:00
const (
exitCodeIfFailure = 3
exitCodeIfTimeout = 4
)
2018-05-06 19:08:34 +03:00
2018-06-02 15:32:40 +03:00
func getDefaultExcludeHelp ( ) string {
parts := [ ] string { "Use or not use default excludes:" }
for _ , ep := range config . DefaultExcludePatterns {
parts = append ( parts , fmt . Sprintf ( " # %s: %s" , ep . Linter , ep . Why ) )
parts = append ( parts , fmt . Sprintf ( " - %s" , color . YellowString ( ep . Pattern ) ) )
parts = append ( parts , "" )
}
return strings . Join ( parts , "\n" )
}
const welcomeMessage = "Run this tool in cloud on every github pull request in https://golangci.com for free (public repos)"
func wh ( text string ) string {
return color . GreenString ( text )
}
2018-06-05 23:54:05 +03:00
func initFlagSet ( fs * pflag . FlagSet , cfg * config . Config ) {
2018-06-02 15:32:40 +03:00
hideFlag := func ( name string ) {
2018-06-02 17:50:15 +03:00
if err := fs . MarkHidden ( name ) ; err != nil {
2018-06-02 15:32:40 +03:00
panic ( err )
}
}
2018-05-11 22:01:57 +03:00
// Output config
2018-06-05 23:54:05 +03:00
oc := & cfg . Output
2018-06-02 17:50:15 +03:00
fs . StringVar ( & oc . Format , "out-format" ,
2018-05-05 09:24:37 +03:00
config . OutFormatColoredLineNumber ,
2018-06-02 15:32:40 +03:00
wh ( fmt . Sprintf ( "Format of output: %s" , strings . Join ( config . OutFormats , "|" ) ) ) )
2018-06-02 17:50:15 +03:00
fs . BoolVar ( & oc . PrintIssuedLine , "print-issued-lines" , true , wh ( "Print lines of code with issue" ) )
fs . BoolVar ( & oc . PrintLinterName , "print-linter-name" , true , wh ( "Print linter name in issue line" ) )
fs . BoolVar ( & oc . PrintWelcomeMessage , "print-welcome" , false , wh ( "Print welcome message" ) )
2018-06-02 15:32:40 +03:00
hideFlag ( "print-welcome" ) // no longer used
2018-05-08 11:54:30 +03:00
2018-05-11 22:01:57 +03:00
// Run config
2018-06-05 23:54:05 +03:00
rc := & cfg . Run
2018-06-02 17:50:15 +03:00
fs . IntVar ( & rc . ExitCodeIfIssuesFound , "issues-exit-code" ,
2018-06-02 15:32:40 +03:00
1 , wh ( "Exit code when issues were found" ) )
2018-06-05 23:54:05 +03:00
fs . StringSliceVar ( & rc . BuildTags , "build-tags" , nil , wh ( "Build tags (not all linters support them)" ) )
2018-06-02 17:50:15 +03:00
fs . DurationVar ( & rc . Deadline , "deadline" , time . Minute , wh ( "Deadline for total work" ) )
2018-06-02 18:13:55 +03:00
fs . BoolVar ( & rc . AnalyzeTests , "tests" , true , wh ( "Analyze tests (*_test.go)" ) )
2018-06-02 17:50:15 +03:00
fs . BoolVar ( & rc . PrintResourcesUsage , "print-resources-usage" , false , wh ( "Print avg and max memory usage of golangci-lint and total time" ) )
fs . StringVarP ( & rc . Config , "config" , "c" , "" , wh ( "Read config from file path `PATH`" ) )
fs . BoolVar ( & rc . NoConfig , "no-config" , false , wh ( "Don't read config" ) )
2018-06-07 09:28:37 +03:00
fs . StringSliceVar ( & rc . SkipDirs , "skip-dirs" , nil , wh ( "Regexps of directory names to skip" ) )
fs . StringSliceVar ( & rc . SkipFiles , "skip-files" , nil , wh ( "Regexps of file names to skip" ) )
2018-05-05 19:43:18 +03:00
2018-05-11 22:01:57 +03:00
// Linters settings config
2018-06-05 23:54:05 +03:00
lsc := & cfg . LintersSettings
2018-06-02 15:32:40 +03:00
// Hide all linters settings flags: they were initially visible,
// but when number of linters started to grow it became ovious that
// we can't fill 90% of flags by linters settings: common flags became hard to find.
// New linters settings should be done only through config file.
2018-06-02 17:50:15 +03:00
fs . BoolVar ( & lsc . Errcheck . CheckTypeAssertions , "errcheck.check-type-assertions" , false , "Errcheck: check for ignored type assertion results" )
2018-06-02 15:32:40 +03:00
hideFlag ( "errcheck.check-type-assertions" )
2018-06-02 17:50:15 +03:00
fs . BoolVar ( & lsc . Errcheck . CheckAssignToBlank , "errcheck.check-blank" , false , "Errcheck: check for errors assigned to blank identifier: _ = errFunc()" )
2018-06-02 15:32:40 +03:00
hideFlag ( "errcheck.check-blank" )
2018-05-05 22:22:21 +03:00
2018-06-02 17:50:15 +03:00
fs . BoolVar ( & lsc . Govet . CheckShadowing , "govet.check-shadowing" , false , "Govet: check for shadowed variables" )
2018-06-02 15:32:40 +03:00
hideFlag ( "govet.check-shadowing" )
2018-05-06 07:20:12 +03:00
2018-06-02 17:50:15 +03:00
fs . Float64Var ( & lsc . Golint . MinConfidence , "golint.min-confidence" , 0.8 , "Golint: minimum confidence of a problem to print it" )
2018-06-02 15:32:40 +03:00
hideFlag ( "golint.min-confidence" )
2018-05-06 09:41:48 +03:00
2018-06-02 17:50:15 +03:00
fs . BoolVar ( & lsc . Gofmt . Simplify , "gofmt.simplify" , true , "Gofmt: simplify code" )
2018-06-02 15:32:40 +03:00
hideFlag ( "gofmt.simplify" )
2018-05-06 13:25:50 +03:00
2018-06-02 17:50:15 +03:00
fs . IntVar ( & lsc . Gocyclo . MinComplexity , "gocyclo.min-complexity" ,
2018-05-08 13:33:00 +03:00
30 , "Minimal complexity of function to report it" )
2018-06-02 15:32:40 +03:00
hideFlag ( "gocyclo.min-complexity" )
2018-05-06 15:24:45 +03:00
2018-06-02 17:50:15 +03:00
fs . BoolVar ( & lsc . Maligned . SuggestNewOrder , "maligned.suggest-new" , false , "Maligned: print suggested more optimal struct fields ordering" )
2018-06-02 15:32:40 +03:00
hideFlag ( "maligned.suggest-new" )
2018-05-06 20:28:59 +03:00
2018-06-02 17:50:15 +03:00
fs . IntVar ( & lsc . Dupl . Threshold , "dupl.threshold" ,
2018-05-08 12:10:24 +03:00
150 , "Dupl: Minimal threshold to detect copy-paste" )
2018-06-02 15:32:40 +03:00
hideFlag ( "dupl.threshold" )
2018-05-07 12:43:52 +03:00
2018-06-02 17:50:15 +03:00
fs . IntVar ( & lsc . Goconst . MinStringLen , "goconst.min-len" ,
2018-05-07 12:43:52 +03:00
3 , "Goconst: minimum constant string length" )
2018-06-02 15:32:40 +03:00
hideFlag ( "goconst.min-len" )
2018-06-02 17:50:15 +03:00
fs . IntVar ( & lsc . Goconst . MinOccurrencesCount , "goconst.min-occurrences" ,
2018-05-26 21:59:09 +08:00
3 , "Goconst: minimum occurrences of constant string count to trigger issue" )
2018-06-02 15:32:40 +03:00
hideFlag ( "goconst.min-occurrences" )
2018-05-06 22:58:04 +03:00
2018-05-31 01:24:46 -04:00
// (@dixonwille) These flag is only used for testing purposes.
2018-06-02 17:50:15 +03:00
fs . StringSliceVar ( & lsc . Depguard . Packages , "depguard.packages" , nil ,
2018-05-31 01:24:46 -04:00
"Depguard: packages to add to the list" )
2018-06-02 15:32:40 +03:00
hideFlag ( "depguard.packages" )
2018-06-02 17:50:15 +03:00
fs . BoolVar ( & lsc . Depguard . IncludeGoRoot , "depguard.include-go-root" , false ,
2018-05-31 01:24:46 -04:00
"Depguard: check list against standard lib" )
2018-06-02 15:32:40 +03:00
hideFlag ( "depguard.include-go-root" )
2018-05-31 01:24:46 -04:00
2018-05-11 22:01:57 +03:00
// Linters config
2018-06-05 23:54:05 +03:00
lc := & cfg . Linters
fs . StringSliceVarP ( & lc . Enable , "enable" , "E" , nil , wh ( "Enable specific linter" ) )
fs . StringSliceVarP ( & lc . Disable , "disable" , "D" , nil , wh ( "Disable specific linter" ) )
2018-06-02 17:50:15 +03:00
fs . BoolVar ( & lc . EnableAll , "enable-all" , false , wh ( "Enable all linters" ) )
fs . BoolVar ( & lc . DisableAll , "disable-all" , false , wh ( "Disable all linters" ) )
2018-06-05 23:54:05 +03:00
fs . StringSliceVarP ( & lc . Presets , "presets" , "p" , nil ,
2018-06-02 15:32:40 +03:00
wh ( fmt . Sprintf ( "Enable presets (%s) of linters. Run 'golangci-lint linters' to see them. This option implies option --disable-all" , strings . Join ( lintersdb . AllPresets ( ) , "|" ) ) ) )
2018-06-02 17:50:15 +03:00
fs . BoolVar ( & lc . Fast , "fast" , false , wh ( "Run only fast linters from enabled linters set" ) )
2018-05-06 19:08:34 +03:00
2018-05-11 22:01:57 +03:00
// Issues config
2018-06-05 23:54:05 +03:00
ic := & cfg . Issues
fs . StringSliceVarP ( & ic . ExcludePatterns , "exclude" , "e" , nil , wh ( "Exclude issue by regexp" ) )
2018-06-02 17:50:15 +03:00
fs . BoolVar ( & ic . UseDefaultExcludes , "exclude-use-default" , true , getDefaultExcludeHelp ( ) )
2018-05-08 08:15:55 +03:00
2018-06-02 17:50:15 +03:00
fs . IntVar ( & ic . MaxIssuesPerLinter , "max-issues-per-linter" , 50 , wh ( "Maximum issues count per one linter. Set to 0 to disable" ) )
fs . IntVar ( & ic . MaxSameIssues , "max-same-issues" , 3 , wh ( "Maximum count of issues with the same text. Set to 0 to disable" ) )
2018-05-08 09:55:38 +03:00
2018-06-02 17:50:15 +03:00
fs . BoolVarP ( & ic . Diff , "new" , "n" , false ,
2018-06-02 15:32:40 +03:00
wh ( "Show only new issues: if there are unstaged changes or untracked files, only those changes are analyzed, else only changes in HEAD~ are analyzed.\nIt's a super-useful option for integration of golangci-lint into existing large codebase.\nIt's not practical to fix all existing issues at the moment of integration: much better don't allow issues in new code" ) )
2018-06-02 17:50:15 +03:00
fs . StringVar ( & ic . DiffFromRevision , "new-from-rev" , "" , wh ( "Show only new issues created after git revision `REV`" ) )
fs . StringVar ( & ic . DiffPatchFilePath , "new-from-patch" , "" , wh ( "Show only new issues created in git patch with file path `PATH`" ) )
}
func ( e * Executor ) initRun ( ) {
var runCmd = & cobra . Command {
Use : "run" ,
Short : welcomeMessage ,
Run : e . executeRun ,
}
e . rootCmd . AddCommand ( runCmd )
2018-05-08 17:13:16 +03:00
2018-06-02 17:50:15 +03:00
runCmd . SetOutput ( printers . StdOut ) // use custom output to properly color it in Windows terminals
fs := runCmd . Flags ( )
fs . SortFlags = false // sort them as they are defined here
2018-06-05 23:54:05 +03:00
initFlagSet ( fs , e . cfg )
2018-06-02 17:50:15 +03:00
2018-06-05 23:54:05 +03:00
// init e.cfg by values from config: flags parse will see these values
// like the default ones. It will overwrite them only if the same option
// is found in command-line: it's ok, command-line has higher priority.
2018-06-02 17:50:15 +03:00
e . parseConfig ( )
2018-06-05 23:54:05 +03:00
// Slice options must be explicitly set for properly merging.
fixSlicesFlags ( fs )
2018-05-05 09:24:37 +03:00
}
2018-05-19 16:18:23 +03:00
func ( e * Executor ) runAnalysis ( ctx context . Context , args [ ] string ) ( <- chan result . Issue , error ) {
2018-05-06 19:08:34 +03:00
e . cfg . Run . Args = args
2018-05-06 13:25:50 +03:00
2018-06-02 11:36:50 +03:00
linters , err := lintersdb . GetEnabledLinters ( e . cfg )
2018-05-06 22:58:04 +03:00
if err != nil {
return nil , err
}
2018-06-02 11:36:50 +03:00
lintCtx , err := lint . LoadContext ( ctx , linters , e . cfg )
2018-05-06 22:58:04 +03:00
if err != nil {
return nil , err
}
2018-05-11 22:01:57 +03:00
excludePatterns := e . cfg . Issues . ExcludePatterns
if e . cfg . Issues . UseDefaultExcludes {
2018-06-02 15:32:40 +03:00
excludePatterns = append ( excludePatterns , config . GetDefaultExcludePatternsStrings ( ) ... )
2018-05-08 12:10:24 +03:00
}
var excludeTotalPattern string
if len ( excludePatterns ) != 0 {
excludeTotalPattern = fmt . Sprintf ( "(%s)" , strings . Join ( excludePatterns , "|" ) )
}
2018-05-08 17:13:16 +03:00
fset := token . NewFileSet ( )
if lintCtx . Program != nil {
fset = lintCtx . Program . Fset
}
2018-06-07 09:28:37 +03:00
skipFilesProcessor , err := processors . NewSkipFiles ( e . cfg . Run . SkipFiles )
if err != nil {
return nil , err
}
2018-05-31 23:53:01 +03:00
runner := lint . SimpleRunner {
2018-05-06 19:08:34 +03:00
Processors : [ ] processors . Processor {
2018-05-13 12:05:34 +03:00
processors . NewPathPrettifier ( ) , // must be before diff processor at least
2018-05-08 17:13:16 +03:00
processors . NewCgo ( ) ,
2018-06-07 09:28:37 +03:00
skipFilesProcessor ,
processors . NewExclude ( excludeTotalPattern ) ,
2018-05-08 17:13:16 +03:00
processors . NewNolint ( fset ) ,
2018-06-07 09:28:37 +03:00
2018-05-07 21:44:40 +03:00
processors . NewUniqByLine ( ) ,
2018-05-11 22:01:57 +03:00
processors . NewDiff ( e . cfg . Issues . Diff , e . cfg . Issues . DiffFromRevision , e . cfg . Issues . DiffPatchFilePath ) ,
2018-05-08 09:55:38 +03:00
processors . NewMaxPerFileFromLinter ( ) ,
2018-05-11 22:01:57 +03:00
processors . NewMaxSameIssues ( e . cfg . Issues . MaxSameIssues ) ,
processors . NewMaxFromLinter ( e . cfg . Issues . MaxIssuesPerLinter ) ,
2018-05-06 19:08:34 +03:00
} ,
}
2018-05-05 19:43:18 +03:00
2018-06-02 11:36:50 +03:00
return runner . Run ( ctx , linters , lintCtx ) , nil
2018-05-06 19:08:34 +03:00
}
2018-05-05 19:43:18 +03:00
2018-05-26 19:58:17 +03:00
func setOutputToDevNull ( ) ( savedStdout , savedStderr * os . File ) {
savedStdout , savedStderr = os . Stdout , os . Stderr
devNull , err := os . Open ( os . DevNull )
if err != nil {
logrus . Warnf ( "can't open null device %q: %s" , os . DevNull , err )
return
}
os . Stdout , os . Stderr = devNull , devNull
return
}
2018-05-12 10:13:37 +03:00
func ( e * Executor ) runAndPrint ( ctx context . Context , args [ ] string ) error {
2018-05-26 19:58:17 +03:00
// Don't allow linters and loader to print anything
log . SetOutput ( ioutil . Discard )
savedStdout , savedStderr := setOutputToDevNull ( )
defer func ( ) {
os . Stdout , os . Stderr = savedStdout , savedStderr
} ( )
2018-05-12 10:13:37 +03:00
issues , err := e . runAnalysis ( ctx , args )
if err != nil {
return err
}
2018-05-06 13:25:50 +03:00
2018-05-12 10:13:37 +03:00
var p printers . Printer
2018-06-02 20:53:36 +03:00
format := e . cfg . Output . Format
switch format {
case config . OutFormatJSON :
2018-05-12 10:13:37 +03:00
p = printers . NewJSON ( )
2018-06-02 20:53:36 +03:00
case config . OutFormatColoredLineNumber , config . OutFormatLineNumber :
2018-05-12 10:13:37 +03:00
p = printers . NewText ( e . cfg . Output . PrintIssuedLine ,
2018-06-02 20:53:36 +03:00
format == config . OutFormatColoredLineNumber , e . cfg . Output . PrintLinterName )
case config . OutFormatTab :
p = printers . NewTab ( e . cfg . Output . PrintLinterName )
default :
return fmt . Errorf ( "unknown output format %s" , format )
2018-05-12 10:13:37 +03:00
}
2018-06-02 20:53:36 +03:00
2018-05-31 23:53:01 +03:00
gotAnyIssues , err := p . Print ( ctx , issues )
2018-05-12 10:13:37 +03:00
if err != nil {
return fmt . Errorf ( "can't print %d issues: %s" , len ( issues ) , err )
}
2018-05-07 21:44:40 +03:00
2018-05-12 10:13:37 +03:00
if gotAnyIssues {
e . exitCode = e . cfg . Run . ExitCodeIfIssuesFound
return nil
2018-05-08 17:13:16 +03:00
}
2018-05-12 10:13:37 +03:00
return nil
}
2018-05-05 09:24:37 +03:00
2018-05-12 10:13:37 +03:00
func ( e * Executor ) executeRun ( cmd * cobra . Command , args [ ] string ) {
needTrackResources := e . cfg . Run . IsVerbose || e . cfg . Run . PrintResourcesUsage
trackResourcesEndCh := make ( chan struct { } )
defer func ( ) { // XXX: this defer must be before ctx.cancel defer
if needTrackResources { // wait until resource tracking finished to print properly
<- trackResourcesEndCh
2018-05-05 09:24:37 +03:00
}
2018-05-12 10:13:37 +03:00
} ( )
2018-05-05 09:24:37 +03:00
2018-05-12 10:13:37 +03:00
ctx , cancel := context . WithTimeout ( context . Background ( ) , e . cfg . Run . Deadline )
defer cancel ( )
2018-05-05 11:08:14 +03:00
2018-05-12 10:13:37 +03:00
if needTrackResources {
go watchResources ( ctx , trackResourcesEndCh )
}
if err := e . runAndPrint ( ctx , args ) ; err != nil {
2018-05-26 19:58:17 +03:00
logrus . Warnf ( "running error: %s" , err )
2018-05-06 19:08:34 +03:00
if e . exitCode == 0 {
e . exitCode = exitCodeIfFailure
}
2018-05-05 09:24:37 +03:00
}
2018-05-30 09:45:08 +03:00
if e . exitCode == 0 && ctx . Err ( ) != nil {
e . exitCode = exitCodeIfTimeout
}
2018-05-05 09:24:37 +03:00
}
2018-05-11 22:01:57 +03:00
2018-05-12 10:13:37 +03:00
func watchResources ( ctx context . Context , done chan struct { } ) {
startedAt := time . Now ( )
rssValues := [ ] uint64 { }
ticker := time . NewTicker ( 100 * time . Millisecond )
defer ticker . Stop ( )
for {
var m runtime . MemStats
runtime . ReadMemStats ( & m )
rssValues = append ( rssValues , m . Sys )
stop := false
select {
case <- ctx . Done ( ) :
stop = true
case <- ticker . C : // track every second
}
if stop {
break
}
}
var avg , max uint64
for _ , v := range rssValues {
avg += v
if v > max {
max = v
}
}
avg /= uint64 ( len ( rssValues ) )
const MB = 1024 * 1024
maxMB := float64 ( max ) / MB
logrus . Infof ( "Memory: %d samples, avg is %.1fMB, max is %.1fMB" ,
len ( rssValues ) , float64 ( avg ) / MB , maxMB )
logrus . Infof ( "Execution took %s" , time . Since ( startedAt ) )
close ( done )
2018-05-11 22:01:57 +03:00
}