github.com/chenfeining/golangci-lint@v1.0.2-0.20230730162517-14c6c67868df/pkg/golinters/errcheck.go (about) 1 package golinters 2 3 import ( 4 "bufio" 5 "fmt" 6 "os" 7 "os/user" 8 "path/filepath" 9 "regexp" 10 "strings" 11 "sync" 12 13 "github.com/kisielk/errcheck/errcheck" 14 "golang.org/x/tools/go/analysis" 15 "golang.org/x/tools/go/packages" 16 17 "github.com/chenfeining/golangci-lint/pkg/config" 18 "github.com/chenfeining/golangci-lint/pkg/fsutils" 19 "github.com/chenfeining/golangci-lint/pkg/golinters/goanalysis" 20 "github.com/chenfeining/golangci-lint/pkg/lint/linter" 21 "github.com/chenfeining/golangci-lint/pkg/result" 22 ) 23 24 const errcheckName = "errcheck" 25 26 func NewErrcheck(settings *config.ErrcheckSettings) *goanalysis.Linter { 27 var mu sync.Mutex 28 var resIssues []goanalysis.Issue 29 30 analyzer := &analysis.Analyzer{ 31 Name: errcheckName, 32 Doc: goanalysis.TheOnlyanalyzerDoc, 33 Run: goanalysis.DummyRun, 34 } 35 36 return goanalysis.NewLinter( 37 errcheckName, 38 "errcheck is a program for checking for unchecked errors in Go code. "+ 39 "These unchecked errors can be critical bugs in some cases", 40 []*analysis.Analyzer{analyzer}, 41 nil, 42 ).WithContextSetter(func(lintCtx *linter.Context) { 43 // copied from errcheck 44 checker, err := getChecker(settings) 45 if err != nil { 46 lintCtx.Log.Errorf("failed to get checker: %v", err) 47 return 48 } 49 50 checker.Tags = lintCtx.Cfg.Run.BuildTags 51 52 analyzer.Run = func(pass *analysis.Pass) (any, error) { 53 issues := runErrCheck(lintCtx, pass, checker) 54 if err != nil { 55 return nil, err 56 } 57 58 if len(issues) == 0 { 59 return nil, nil 60 } 61 62 mu.Lock() 63 resIssues = append(resIssues, issues...) 64 mu.Unlock() 65 66 return nil, nil 67 } 68 }).WithIssuesReporter(func(*linter.Context) []goanalysis.Issue { 69 return resIssues 70 }).WithLoadMode(goanalysis.LoadModeTypesInfo) 71 } 72 73 func runErrCheck(lintCtx *linter.Context, pass *analysis.Pass, checker *errcheck.Checker) []goanalysis.Issue { 74 pkg := &packages.Package{ 75 Fset: pass.Fset, 76 Syntax: pass.Files, 77 Types: pass.Pkg, 78 TypesInfo: pass.TypesInfo, 79 } 80 81 lintIssues := checker.CheckPackage(pkg).Unique() 82 if len(lintIssues.UncheckedErrors) == 0 { 83 return nil 84 } 85 86 issues := make([]goanalysis.Issue, len(lintIssues.UncheckedErrors)) 87 88 for i, err := range lintIssues.UncheckedErrors { 89 text := "Error return value is not checked" 90 91 if err.FuncName != "" { 92 code := err.SelectorName 93 if err.SelectorName == "" { 94 code = err.FuncName 95 } 96 97 text = fmt.Sprintf("Error return value of %s is not checked", formatCode(code, lintCtx.Cfg)) 98 } 99 100 issues[i] = goanalysis.NewIssue( 101 &result.Issue{ 102 FromLinter: errcheckName, 103 Text: text, 104 Pos: err.Pos, 105 }, 106 pass, 107 ) 108 } 109 110 return issues 111 } 112 113 // parseIgnoreConfig was taken from errcheck in order to keep the API identical. 114 // https://github.com/kisielk/errcheck/blob/1787c4bee836470bf45018cfbc783650db3c6501/main.go#L25-L60 115 func parseIgnoreConfig(s string) (map[string]*regexp.Regexp, error) { 116 if s == "" { 117 return nil, nil 118 } 119 120 cfg := map[string]*regexp.Regexp{} 121 122 for _, pair := range strings.Split(s, ",") { 123 colonIndex := strings.Index(pair, ":") 124 var pkg, re string 125 if colonIndex == -1 { 126 pkg = "" 127 re = pair 128 } else { 129 pkg = pair[:colonIndex] 130 re = pair[colonIndex+1:] 131 } 132 regex, err := regexp.Compile(re) 133 if err != nil { 134 return nil, err 135 } 136 cfg[pkg] = regex 137 } 138 139 return cfg, nil 140 } 141 142 func getChecker(errCfg *config.ErrcheckSettings) (*errcheck.Checker, error) { 143 ignoreConfig, err := parseIgnoreConfig(errCfg.Ignore) 144 if err != nil { 145 return nil, fmt.Errorf("failed to parse 'ignore' directive: %w", err) 146 } 147 148 checker := errcheck.Checker{ 149 Exclusions: errcheck.Exclusions{ 150 BlankAssignments: !errCfg.CheckAssignToBlank, 151 TypeAssertions: !errCfg.CheckTypeAssertions, 152 SymbolRegexpsByPackage: map[string]*regexp.Regexp{}, 153 }, 154 } 155 156 if !errCfg.DisableDefaultExclusions { 157 checker.Exclusions.Symbols = append(checker.Exclusions.Symbols, errcheck.DefaultExcludedSymbols...) 158 } 159 160 for pkg, re := range ignoreConfig { 161 checker.Exclusions.SymbolRegexpsByPackage[pkg] = re 162 } 163 164 if errCfg.Exclude != "" { 165 exclude, err := readExcludeFile(errCfg.Exclude) 166 if err != nil { 167 return nil, err 168 } 169 170 checker.Exclusions.Symbols = append(checker.Exclusions.Symbols, exclude...) 171 } 172 173 checker.Exclusions.Symbols = append(checker.Exclusions.Symbols, errCfg.ExcludeFunctions...) 174 175 return &checker, nil 176 } 177 178 func getFirstPathArg() string { 179 args := os.Args 180 181 // skip all args ([golangci-lint, run/linters]) before files/dirs list 182 for len(args) != 0 { 183 if args[0] == "run" { 184 args = args[1:] 185 break 186 } 187 188 args = args[1:] 189 } 190 191 // find first file/dir arg 192 firstArg := "./..." 193 for _, arg := range args { 194 if !strings.HasPrefix(arg, "-") { 195 firstArg = arg 196 break 197 } 198 } 199 200 return firstArg 201 } 202 203 func setupConfigFileSearch(name string) []string { 204 if strings.HasPrefix(name, "~") { 205 if u, err := user.Current(); err == nil { 206 name = strings.Replace(name, "~", u.HomeDir, 1) 207 } 208 } 209 210 if filepath.IsAbs(name) { 211 return []string{name} 212 } 213 214 firstArg := getFirstPathArg() 215 216 absStartPath, err := filepath.Abs(firstArg) 217 if err != nil { 218 absStartPath = filepath.Clean(firstArg) 219 } 220 221 // start from it 222 var curDir string 223 if fsutils.IsDir(absStartPath) { 224 curDir = absStartPath 225 } else { 226 curDir = filepath.Dir(absStartPath) 227 } 228 229 // find all dirs from it up to the root 230 configSearchPaths := []string{filepath.Join(".", name)} 231 for { 232 configSearchPaths = append(configSearchPaths, filepath.Join(curDir, name)) 233 newCurDir := filepath.Dir(curDir) 234 if curDir == newCurDir || newCurDir == "" { 235 break 236 } 237 curDir = newCurDir 238 } 239 240 return configSearchPaths 241 } 242 243 func readExcludeFile(name string) ([]string, error) { 244 var err error 245 var fh *os.File 246 247 for _, path := range setupConfigFileSearch(name) { 248 if fh, err = os.Open(path); err == nil { 249 break 250 } 251 } 252 253 if fh == nil { 254 return nil, fmt.Errorf("failed reading exclude file: %s: %w", name, err) 255 } 256 257 scanner := bufio.NewScanner(fh) 258 259 var excludes []string 260 for scanner.Scan() { 261 excludes = append(excludes, scanner.Text()) 262 } 263 264 if err := scanner.Err(); err != nil { 265 return nil, fmt.Errorf("failed scanning file: %s: %w", name, err) 266 } 267 268 return excludes, nil 269 }