Skip to content

Commit

Permalink
add experimental go/analysis APIs support (#1140)
Browse files Browse the repository at this point in the history
* `checkers/analyzer` implements `go/analysis` analyzer object
* `cmd/gocritic-analysis` is a simple `singlechecker` runner

To test `go/analysis`-based binary, do the following:

```
$ go build -o gocritic ./cmd/gocritic-analysis
$ ./gocritic ./linting-targets
```

`gocritic-analysis` is experimental and temporary.
We will do a more thorough transition later.
This version allows us to test things out and adapt
the code base to `go/analysis` framework without hurry.
  • Loading branch information
quasilyte committed Oct 20, 2021
1 parent 63f9a6a commit 50a39c9
Show file tree
Hide file tree
Showing 3 changed files with 307 additions and 0 deletions.
69 changes: 69 additions & 0 deletions checkers/analyzer/analyzer.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
// Package analyzer implements `go/analysis` compatible interfaces.
package analyzer

import (
"github.com/go-critic/go-critic/framework/linter"
"golang.org/x/tools/go/analysis"
)

// Analyzer exports go-critic checkers as analysis-compatible object.
// The set of enabled checkers is controlled via the flags.
// Per-checker params are also passed via the flags.
var Analyzer = &analysis.Analyzer{
Name: "ruleguard",
Doc: "The most opinionated Go source code linter",
Run: runAnalyzer,
}

// DisableCache disables initialization optimization.
// This should only be useful for analyzer testing.
var DisableCache = false

var (
flagGoVersion string
flagEnable string
flagDisable string
flagEnableAll bool
flagDebugInit bool
)

var (
intParams = make(map[string]*int)
boolParams = make(map[string]*bool)
stringParams = make(map[string]*string)
)

var registeredCheckers = linter.GetCheckersInfo()

func init() {
Analyzer.Flags.BoolVar(&flagDebugInit, "debug-init", false,
`print gocritic initialization related debug info`)
Analyzer.Flags.BoolVar(&flagEnableAll, "enable-all", false,
`identical to -enable with all checkers listed. If true, -enable is ignored`)
Analyzer.Flags.StringVar(&flagEnable, "enable", "#diagnostic,#style,#security",
`comma-separated list of enabled checkers. Can include #tags`)
Analyzer.Flags.StringVar(&flagDisable, "disable", "<default>",
`comma-separated list of checkers to be disabled. Can include #tags`)
Analyzer.Flags.StringVar(&flagGoVersion, "go", "",
`select the Go version to target. Leave as string for the latest`)

for _, info := range registeredCheckers {
for pname, param := range info.Params {
key := checkerParamName(info, pname)
switch v := param.Value.(type) {
case int:
intParams[key] = Analyzer.Flags.Int(key, v, param.Usage)
case bool:
boolParams[key] = Analyzer.Flags.Bool(key, v, param.Usage)
case string:
stringParams[key] = Analyzer.Flags.String(key, v, param.Usage)
default:
panic("unreachable") // Checked in AddChecker
}
}
}
}

func checkerParamName(info *linter.CheckerInfo, pname string) string {
return "@" + info.Name + "." + pname
}
228 changes: 228 additions & 0 deletions checkers/analyzer/run.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,228 @@
package analyzer

import (
"fmt"
"log"
"path/filepath"
"strings"
"sync"

_ "github.com/go-critic/go-critic/checkers" // Register go-critic checkers
"github.com/go-critic/go-critic/framework/linter"
"golang.org/x/tools/go/analysis"
)

type gocritic struct {
infoList []*linter.CheckerInfo
goVersion linter.GoVersion
}

var (
globalGocriticMu sync.Mutex
globalGocritic *gocritic
globalInitErrorReported bool
)

func runAnalyzer(pass *analysis.Pass) (interface{}, error) {
critic, err := prepareGocritic()
if err != nil {
return nil, fmt.Errorf("init error: %w", err)
}

ctx := linter.NewContext(pass.Fset, pass.TypesSizes)
ctx.GoVersion = critic.goVersion
ctx.SetPackageInfo(pass.TypesInfo, pass.Pkg)

checkers, err := critic.createCheckers(ctx)
if err != nil {
return nil, err
}

for _, f := range pass.Files {
filename := filepath.Base(pass.Fset.Position(f.Pos()).Filename)
ctx.SetFileInfo(filename, f)
for _, c := range checkers {
warnings := c.Check(f)
for _, warning := range warnings {
diag := analysis.Diagnostic{
Pos: warning.Node.Pos(),
Message: fmt.Sprintf("%s: %s", c.Info.Name, warning.Text),
}
if warning.HasQuickFix() {
diag.SuggestedFixes = []analysis.SuggestedFix{
{
Message: "suggested replacement",
TextEdits: []analysis.TextEdit{
{
Pos: warning.Suggestion.From,
End: warning.Suggestion.To,
NewText: warning.Suggestion.Replacement,
},
},
},
}
}
pass.Report(diag)
}
}
}

return nil, nil
}

// prepareGocritic initializes a new gocririt object,
// but unlike newGocritic() it could use a cached version.
func prepareGocritic() (*gocritic, error) {
if DisableCache {
return newGocritic()
}

globalGocriticMu.Lock()
defer globalGocriticMu.Unlock()

// Don't report init error ever again if it was already reported.
if globalInitErrorReported {
return nil, nil
}

if globalGocritic != nil {
return globalGocritic, nil
}

critic, err := newGocritic()
if err != nil {
globalInitErrorReported = true
return nil, err
}
globalGocritic = critic
return critic, nil
}

func newGocritic() (*gocritic, error) {
critic := &gocritic{
infoList: filterCheckersList(registeredCheckers),
}

ver, err := linter.ParseGoVersion(flagGoVersion)
if err != nil {
return nil, err
}
critic.goVersion = ver

for _, info := range critic.infoList {
for pname, param := range info.Params {
key := checkerParamName(info, pname)
switch param.Value.(type) {
case int:
info.Params[pname].Value = *intParams[key]
case bool:
info.Params[pname].Value = *boolParams[key]
case string:
info.Params[pname].Value = *stringParams[key]
default:
panic("unreachable") // Checked in AddChecker
}
}
}

return critic, nil
}

func filterCheckersList(infoList []*linter.CheckerInfo) []*linter.CheckerInfo {
parseKeys := func(keys []string, byName, byTag map[string]bool) {
for _, key := range keys {
if strings.HasPrefix(key, "#") {
byTag[key[len("#"):]] = true
} else {
byName[key] = true
}
}
}
splitValues := func(s string) []string {
parts := strings.Split(s, ",")
for i := range parts {
parts[i] = strings.TrimSpace(parts[i])
}
return parts
}

disableArg := flagDisable
if disableArg == "<default>" {
if flagEnableAll {
disableArg = ""
} else {
disableArg = "#experimental,#opinionated,#performance"
}
}

enabledByName := make(map[string]bool)
enabledTags := make(map[string]bool)
parseKeys(splitValues(flagEnable), enabledByName, enabledTags)
disabledByName := make(map[string]bool)
disabledTags := make(map[string]bool)
parseKeys(splitValues(disableArg), disabledByName, disabledTags)

enabledByTag := func(info *linter.CheckerInfo) bool {
for _, tag := range info.Tags {
if enabledTags[tag] {
return true
}
}
return false
}
disabledByTag := func(info *linter.CheckerInfo) string {
for _, tag := range info.Tags {
if disabledTags[tag] {
return tag
}
}
return ""
}

var filtered []*linter.CheckerInfo

for _, info := range infoList {
enabled := flagEnableAll || enabledByName[info.Name] || enabledByTag(info)
notice := ""

switch {
case !enabled:
notice = "not enabled by name or tag (-enable)"
case disabledByName[info.Name]:
enabled = false
notice = "disabled by name (-disable)"
default:
if tag := disabledByTag(info); tag != "" {
enabled = false
notice = fmt.Sprintf("disabled by %q tag (-disable)", tag)
}
}

if flagDebugInit && !enabled {
log.Printf("\tdebug: %s: %s", info.Name, notice)
}
if !enabled {
continue
}
filtered = append(filtered, info)
}
if flagDebugInit {
for _, info := range filtered {
log.Printf("\tdebug: %s is enabled", info.Name)
}
}

return filtered
}

func (critic *gocritic) createCheckers(ctx *linter.Context) ([]*linter.Checker, error) {
checkers := make([]*linter.Checker, len(critic.infoList))
for i, info := range critic.infoList {
c, err := linter.NewChecker(ctx, info)
if err != nil {
return nil, fmt.Errorf("init %s: %w", info.Name, err)
}
checkers[i] = c
}
return checkers, nil
}
10 changes: 10 additions & 0 deletions cmd/gocritic-analysis/gocritic-analysis.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package main

import (
"github.com/go-critic/go-critic/checkers/analyzer"
"golang.org/x/tools/go/analysis/singlechecker"
)

func main() {
singlechecker.Main(analyzer.Analyzer)
}

0 comments on commit 50a39c9

Please sign in to comment.