lint.go 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577
  1. package lintcmd
  2. import (
  3. "crypto/sha256"
  4. "fmt"
  5. "go/build"
  6. "go/token"
  7. "io"
  8. "os"
  9. "os/signal"
  10. "path/filepath"
  11. "regexp"
  12. "strconv"
  13. "strings"
  14. "time"
  15. "unicode"
  16. "honnef.co/go/tools/analysis/lint"
  17. "honnef.co/go/tools/config"
  18. "honnef.co/go/tools/go/buildid"
  19. "honnef.co/go/tools/go/loader"
  20. "honnef.co/go/tools/lintcmd/cache"
  21. "honnef.co/go/tools/lintcmd/runner"
  22. "honnef.co/go/tools/unused"
  23. "golang.org/x/tools/go/analysis"
  24. "golang.org/x/tools/go/packages"
  25. )
  26. // A linter lints Go source code.
  27. type linter struct {
  28. Analyzers map[string]*lint.Analyzer
  29. Runner *runner.Runner
  30. }
  31. func computeSalt() ([]byte, error) {
  32. p, err := os.Executable()
  33. if err != nil {
  34. return nil, err
  35. }
  36. if id, err := buildid.ReadFile(p); err == nil {
  37. return []byte(id), nil
  38. } else {
  39. // For some reason we couldn't read the build id from the executable.
  40. // Fall back to hashing the entire executable.
  41. f, err := os.Open(p)
  42. if err != nil {
  43. return nil, err
  44. }
  45. defer f.Close()
  46. h := sha256.New()
  47. if _, err := io.Copy(h, f); err != nil {
  48. return nil, err
  49. }
  50. return h.Sum(nil), nil
  51. }
  52. }
  53. func newLinter(cfg config.Config) (*linter, error) {
  54. c, err := cache.Default()
  55. if err != nil {
  56. return nil, err
  57. }
  58. salt, err := computeSalt()
  59. if err != nil {
  60. return nil, fmt.Errorf("could not compute salt for cache: %s", err)
  61. }
  62. c.SetSalt(salt)
  63. r, err := runner.New(cfg, c)
  64. if err != nil {
  65. return nil, err
  66. }
  67. r.FallbackGoVersion = defaultGoVersion()
  68. return &linter{
  69. Runner: r,
  70. }, nil
  71. }
  72. type LintResult struct {
  73. CheckedFiles []string
  74. Diagnostics []diagnostic
  75. Warnings []string
  76. }
  77. func (l *linter) Lint(cfg *packages.Config, patterns []string) (LintResult, error) {
  78. var out LintResult
  79. as := make([]*analysis.Analyzer, 0, len(l.Analyzers))
  80. for _, a := range l.Analyzers {
  81. as = append(as, a.Analyzer)
  82. }
  83. results, err := l.Runner.Run(cfg, as, patterns)
  84. if err != nil {
  85. return out, err
  86. }
  87. if len(results) == 0 {
  88. // TODO(dh): emulate Go's behavior more closely once we have
  89. // access to go list's Match field.
  90. for _, pattern := range patterns {
  91. fmt.Fprintf(os.Stderr, "warning: %q matched no packages\n", pattern)
  92. }
  93. }
  94. analyzerNames := make([]string, 0, len(l.Analyzers))
  95. for name := range l.Analyzers {
  96. analyzerNames = append(analyzerNames, name)
  97. }
  98. used := map[unusedKey]bool{}
  99. var unuseds []unusedPair
  100. for _, res := range results {
  101. if len(res.Errors) > 0 && !res.Failed {
  102. panic("package has errors but isn't marked as failed")
  103. }
  104. if res.Failed {
  105. out.Diagnostics = append(out.Diagnostics, failed(res)...)
  106. } else {
  107. if res.Skipped {
  108. out.Warnings = append(out.Warnings, fmt.Sprintf("skipped package %s because it is too large", res.Package))
  109. continue
  110. }
  111. if !res.Initial {
  112. continue
  113. }
  114. out.CheckedFiles = append(out.CheckedFiles, res.Package.GoFiles...)
  115. allowedAnalyzers := filterAnalyzerNames(analyzerNames, res.Config.Checks)
  116. resd, err := res.Load()
  117. if err != nil {
  118. return out, err
  119. }
  120. ps := success(allowedAnalyzers, resd)
  121. filtered, err := filterIgnored(ps, resd, allowedAnalyzers)
  122. if err != nil {
  123. return out, err
  124. }
  125. // OPT move this code into the 'success' function.
  126. for i, diag := range filtered {
  127. a := l.Analyzers[diag.Category]
  128. // Some diag.Category don't map to analyzers, such as "staticcheck"
  129. if a != nil {
  130. filtered[i].MergeIf = a.Doc.MergeIf
  131. }
  132. }
  133. out.Diagnostics = append(out.Diagnostics, filtered...)
  134. for _, obj := range resd.Unused.Used {
  135. // FIXME(dh): pick the object whose filename does not include $GOROOT
  136. key := unusedKey{
  137. pkgPath: res.Package.PkgPath,
  138. base: filepath.Base(obj.Position.Filename),
  139. line: obj.Position.Line,
  140. name: obj.Name,
  141. }
  142. used[key] = true
  143. }
  144. if allowedAnalyzers["U1000"] {
  145. for _, obj := range resd.Unused.Unused {
  146. key := unusedKey{
  147. pkgPath: res.Package.PkgPath,
  148. base: filepath.Base(obj.Position.Filename),
  149. line: obj.Position.Line,
  150. name: obj.Name,
  151. }
  152. unuseds = append(unuseds, unusedPair{key, obj})
  153. if _, ok := used[key]; !ok {
  154. used[key] = false
  155. }
  156. }
  157. }
  158. }
  159. }
  160. for _, uo := range unuseds {
  161. if uo.obj.Kind == "type param" {
  162. // We don't currently flag unused type parameters on used objects, and flagging them on unused objects isn't
  163. // useful.
  164. continue
  165. }
  166. if used[uo.key] {
  167. continue
  168. }
  169. if uo.obj.InGenerated {
  170. continue
  171. }
  172. out.Diagnostics = append(out.Diagnostics, diagnostic{
  173. Diagnostic: runner.Diagnostic{
  174. Position: uo.obj.DisplayPosition,
  175. Message: fmt.Sprintf("%s %s is unused", uo.obj.Kind, uo.obj.Name),
  176. Category: "U1000",
  177. },
  178. MergeIf: lint.MergeIfAll,
  179. })
  180. }
  181. return out, nil
  182. }
  183. func filterIgnored(diagnostics []diagnostic, res runner.ResultData, allowedAnalyzers map[string]bool) ([]diagnostic, error) {
  184. couldHaveMatched := func(ig *lineIgnore) bool {
  185. for _, c := range ig.Checks {
  186. if c == "U1000" {
  187. // We never want to flag ignores for U1000,
  188. // because U1000 isn't local to a single
  189. // package. For example, an identifier may
  190. // only be used by tests, in which case an
  191. // ignore would only fire when not analyzing
  192. // tests. To avoid spurious "useless ignore"
  193. // warnings, just never flag U1000.
  194. return false
  195. }
  196. // Even though the runner always runs all analyzers, we
  197. // still only flag unmatched ignores for the set of
  198. // analyzers the user has expressed interest in. That way,
  199. // `staticcheck -checks=SA1000` won't complain about an
  200. // unmatched ignore for an unrelated check.
  201. if allowedAnalyzers[c] {
  202. return true
  203. }
  204. }
  205. return false
  206. }
  207. ignores, moreDiagnostics := parseDirectives(res.Directives)
  208. for _, ig := range ignores {
  209. for i := range diagnostics {
  210. diag := &diagnostics[i]
  211. if ig.Match(*diag) {
  212. diag.Severity = severityIgnored
  213. }
  214. }
  215. if ig, ok := ig.(*lineIgnore); ok && !ig.Matched && couldHaveMatched(ig) {
  216. diag := diagnostic{
  217. Diagnostic: runner.Diagnostic{
  218. Position: ig.Pos,
  219. Message: "this linter directive didn't match anything; should it be removed?",
  220. Category: "staticcheck",
  221. },
  222. }
  223. moreDiagnostics = append(moreDiagnostics, diag)
  224. }
  225. }
  226. return append(diagnostics, moreDiagnostics...), nil
  227. }
  228. type ignore interface {
  229. Match(diag diagnostic) bool
  230. }
  231. type lineIgnore struct {
  232. File string
  233. Line int
  234. Checks []string
  235. Matched bool
  236. Pos token.Position
  237. }
  238. func (li *lineIgnore) Match(p diagnostic) bool {
  239. pos := p.Position
  240. if pos.Filename != li.File || pos.Line != li.Line {
  241. return false
  242. }
  243. for _, c := range li.Checks {
  244. if m, _ := filepath.Match(c, p.Category); m {
  245. li.Matched = true
  246. return true
  247. }
  248. }
  249. return false
  250. }
  251. func (li *lineIgnore) String() string {
  252. matched := "not matched"
  253. if li.Matched {
  254. matched = "matched"
  255. }
  256. return fmt.Sprintf("%s:%d %s (%s)", li.File, li.Line, strings.Join(li.Checks, ", "), matched)
  257. }
  258. type fileIgnore struct {
  259. File string
  260. Checks []string
  261. }
  262. func (fi *fileIgnore) Match(p diagnostic) bool {
  263. if p.Position.Filename != fi.File {
  264. return false
  265. }
  266. for _, c := range fi.Checks {
  267. if m, _ := filepath.Match(c, p.Category); m {
  268. return true
  269. }
  270. }
  271. return false
  272. }
  273. type severity uint8
  274. const (
  275. severityError severity = iota
  276. severityWarning
  277. severityIgnored
  278. )
  279. func (s severity) String() string {
  280. switch s {
  281. case severityError:
  282. return "error"
  283. case severityWarning:
  284. return "warning"
  285. case severityIgnored:
  286. return "ignored"
  287. default:
  288. return fmt.Sprintf("Severity(%d)", s)
  289. }
  290. }
  291. // diagnostic represents a diagnostic in some source code.
  292. type diagnostic struct {
  293. runner.Diagnostic
  294. Severity severity
  295. MergeIf lint.MergeStrategy
  296. BuildName string
  297. }
  298. func (p diagnostic) equal(o diagnostic) bool {
  299. return p.Position == o.Position &&
  300. p.End == o.End &&
  301. p.Message == o.Message &&
  302. p.Category == o.Category &&
  303. p.Severity == o.Severity &&
  304. p.MergeIf == o.MergeIf &&
  305. p.BuildName == o.BuildName
  306. }
  307. func (p *diagnostic) String() string {
  308. if p.BuildName != "" {
  309. return fmt.Sprintf("%s [%s] (%s)", p.Message, p.BuildName, p.Category)
  310. } else {
  311. return fmt.Sprintf("%s (%s)", p.Message, p.Category)
  312. }
  313. }
  314. func failed(res runner.Result) []diagnostic {
  315. var diagnostics []diagnostic
  316. for _, e := range res.Errors {
  317. switch e := e.(type) {
  318. case packages.Error:
  319. msg := e.Msg
  320. if len(msg) != 0 && msg[0] == '\n' {
  321. // TODO(dh): See https://github.com/golang/go/issues/32363
  322. msg = msg[1:]
  323. }
  324. var posn token.Position
  325. if e.Pos == "" {
  326. // Under certain conditions (malformed package
  327. // declarations, multiple packages in the same
  328. // directory), go list emits an error on stderr
  329. // instead of JSON. Those errors do not have
  330. // associated position information in
  331. // go/packages.Error, even though the output on
  332. // stderr may contain it.
  333. if p, n, err := parsePos(msg); err == nil {
  334. if abs, err := filepath.Abs(p.Filename); err == nil {
  335. p.Filename = abs
  336. }
  337. posn = p
  338. msg = msg[n+2:]
  339. }
  340. } else {
  341. var err error
  342. posn, _, err = parsePos(e.Pos)
  343. if err != nil {
  344. panic(fmt.Sprintf("internal error: %s", e))
  345. }
  346. }
  347. diag := diagnostic{
  348. Diagnostic: runner.Diagnostic{
  349. Position: posn,
  350. Message: msg,
  351. Category: "compile",
  352. },
  353. Severity: severityError,
  354. }
  355. diagnostics = append(diagnostics, diag)
  356. case error:
  357. diag := diagnostic{
  358. Diagnostic: runner.Diagnostic{
  359. Position: token.Position{},
  360. Message: e.Error(),
  361. Category: "compile",
  362. },
  363. Severity: severityError,
  364. }
  365. diagnostics = append(diagnostics, diag)
  366. }
  367. }
  368. return diagnostics
  369. }
  370. type unusedKey struct {
  371. pkgPath string
  372. base string
  373. line int
  374. name string
  375. }
  376. type unusedPair struct {
  377. key unusedKey
  378. obj unused.SerializedObject
  379. }
  380. func success(allowedAnalyzers map[string]bool, res runner.ResultData) []diagnostic {
  381. diags := res.Diagnostics
  382. var diagnostics []diagnostic
  383. for _, diag := range diags {
  384. if !allowedAnalyzers[diag.Category] {
  385. continue
  386. }
  387. diagnostics = append(diagnostics, diagnostic{Diagnostic: diag})
  388. }
  389. return diagnostics
  390. }
  391. func defaultGoVersion() string {
  392. tags := build.Default.ReleaseTags
  393. v := tags[len(tags)-1][2:]
  394. return v
  395. }
  396. func filterAnalyzerNames(analyzers []string, checks []string) map[string]bool {
  397. allowedChecks := map[string]bool{}
  398. for _, check := range checks {
  399. b := true
  400. if len(check) > 1 && check[0] == '-' {
  401. b = false
  402. check = check[1:]
  403. }
  404. if check == "*" || check == "all" {
  405. // Match all
  406. for _, c := range analyzers {
  407. allowedChecks[c] = b
  408. }
  409. } else if strings.HasSuffix(check, "*") {
  410. // Glob
  411. prefix := check[:len(check)-1]
  412. isCat := strings.IndexFunc(prefix, func(r rune) bool { return unicode.IsNumber(r) }) == -1
  413. for _, a := range analyzers {
  414. idx := strings.IndexFunc(a, func(r rune) bool { return unicode.IsNumber(r) })
  415. if isCat {
  416. // Glob is S*, which should match S1000 but not SA1000
  417. cat := a[:idx]
  418. if prefix == cat {
  419. allowedChecks[a] = b
  420. }
  421. } else {
  422. // Glob is S1*
  423. if strings.HasPrefix(a, prefix) {
  424. allowedChecks[a] = b
  425. }
  426. }
  427. }
  428. } else {
  429. // Literal check name
  430. allowedChecks[check] = b
  431. }
  432. }
  433. return allowedChecks
  434. }
  435. var posRe = regexp.MustCompile(`^(.+?):(\d+)(?::(\d+)?)?`)
  436. func parsePos(pos string) (token.Position, int, error) {
  437. if pos == "-" || pos == "" {
  438. return token.Position{}, 0, nil
  439. }
  440. parts := posRe.FindStringSubmatch(pos)
  441. if parts == nil {
  442. return token.Position{}, 0, fmt.Errorf("internal error: malformed position %q", pos)
  443. }
  444. file := parts[1]
  445. line, _ := strconv.Atoi(parts[2])
  446. col, _ := strconv.Atoi(parts[3])
  447. return token.Position{
  448. Filename: file,
  449. Line: line,
  450. Column: col,
  451. }, len(parts[0]), nil
  452. }
  453. type options struct {
  454. Config config.Config
  455. BuildConfig BuildConfig
  456. LintTests bool
  457. GoVersion string
  458. PrintAnalyzerMeasurement func(analysis *analysis.Analyzer, pkg *loader.PackageSpec, d time.Duration)
  459. }
  460. func doLint(as []*lint.Analyzer, paths []string, opt *options) (LintResult, error) {
  461. if opt == nil {
  462. opt = &options{}
  463. }
  464. l, err := newLinter(opt.Config)
  465. if err != nil {
  466. return LintResult{}, err
  467. }
  468. analyzers := make(map[string]*lint.Analyzer, len(as))
  469. for _, a := range as {
  470. analyzers[a.Analyzer.Name] = a
  471. }
  472. l.Analyzers = analyzers
  473. l.Runner.GoVersion = opt.GoVersion
  474. l.Runner.Stats.PrintAnalyzerMeasurement = opt.PrintAnalyzerMeasurement
  475. cfg := &packages.Config{}
  476. if opt.LintTests {
  477. cfg.Tests = true
  478. }
  479. cfg.BuildFlags = opt.BuildConfig.Flags
  480. cfg.Env = append(os.Environ(), opt.BuildConfig.Envs...)
  481. printStats := func() {
  482. // Individual stats are read atomically, but overall there
  483. // is no synchronisation. For printing rough progress
  484. // information, this doesn't matter.
  485. switch l.Runner.Stats.State() {
  486. case runner.StateInitializing:
  487. fmt.Fprintln(os.Stderr, "Status: initializing")
  488. case runner.StateLoadPackageGraph:
  489. fmt.Fprintln(os.Stderr, "Status: loading package graph")
  490. case runner.StateBuildActionGraph:
  491. fmt.Fprintln(os.Stderr, "Status: building action graph")
  492. case runner.StateProcessing:
  493. fmt.Fprintf(os.Stderr, "Packages: %d/%d initial, %d/%d total; Workers: %d/%d\n",
  494. l.Runner.Stats.ProcessedInitialPackages(),
  495. l.Runner.Stats.InitialPackages(),
  496. l.Runner.Stats.ProcessedPackages(),
  497. l.Runner.Stats.TotalPackages(),
  498. l.Runner.ActiveWorkers(),
  499. l.Runner.TotalWorkers(),
  500. )
  501. case runner.StateFinalizing:
  502. fmt.Fprintln(os.Stderr, "Status: finalizing")
  503. }
  504. }
  505. if len(infoSignals) > 0 {
  506. ch := make(chan os.Signal, 1)
  507. signal.Notify(ch, infoSignals...)
  508. defer signal.Stop(ch)
  509. go func() {
  510. for range ch {
  511. printStats()
  512. }
  513. }()
  514. }
  515. res, err := l.Lint(cfg, paths)
  516. for i := range res.Diagnostics {
  517. res.Diagnostics[i].BuildName = opt.BuildConfig.Name
  518. }
  519. return res, err
  520. }