-
Notifications
You must be signed in to change notification settings - Fork 113
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
add experimental
go/analysis
APIs support (#1140)
* `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
Showing
3 changed files
with
307 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | ||
} |