mirror of
https://github.com/mgechev/revive.git
synced 2025-01-22 03:38:47 +02:00
feat: implement extensible model
This commit is contained in:
parent
f86d165f9c
commit
5b1d2b944c
2
.gitignore
vendored
2
.gitignore
vendored
@ -1 +1,3 @@
|
||||
golinter
|
||||
vendor
|
||||
|
||||
|
56
defaultrules/no-else-return-rule.go
Normal file
56
defaultrules/no-else-return-rule.go
Normal file
@ -0,0 +1,56 @@
|
||||
package defaultrules
|
||||
|
||||
import (
|
||||
"go/ast"
|
||||
"go/token"
|
||||
|
||||
"github.com/mgechev/golinter/file"
|
||||
"github.com/mgechev/golinter/rules"
|
||||
"github.com/mgechev/golinter/visitors"
|
||||
)
|
||||
|
||||
const ruleName = "no-else-return"
|
||||
|
||||
// LintElseRule lints given else constructs.
|
||||
type LintElseRule struct {
|
||||
rules.Rule
|
||||
}
|
||||
|
||||
// Apply applies the rule to given file.
|
||||
func (r *LintElseRule) Apply(file *file.File, arguments rules.RuleArguments) []rules.Failure {
|
||||
res := &lintElseVisitor{}
|
||||
visitors.Setup(res, rules.RuleConfig{Name: ruleName, Arguments: arguments}, file)
|
||||
res.Visit(file.GetAST())
|
||||
return res.GetFailures()
|
||||
}
|
||||
|
||||
type lintElseVisitor struct {
|
||||
visitors.RuleVisitor
|
||||
}
|
||||
|
||||
func (w *lintElseVisitor) VisitIfStmt(node *ast.IfStmt) {
|
||||
if node.Else == nil {
|
||||
return
|
||||
}
|
||||
if _, ok := node.Else.(*ast.BlockStmt); !ok {
|
||||
// only care about elses without conditions
|
||||
return
|
||||
}
|
||||
if len(node.Body.List) == 0 {
|
||||
return
|
||||
}
|
||||
// shortDecl := false // does the if statement have a ":=" initialization statement?
|
||||
if node.Init != nil {
|
||||
if as, ok := node.Init.(*ast.AssignStmt); ok && as.Tok == token.DEFINE {
|
||||
// shortDecl = true
|
||||
}
|
||||
}
|
||||
lastStmt := node.Body.List[len(node.Body.List)-1]
|
||||
if _, ok := lastStmt.(*ast.ReturnStmt); ok {
|
||||
w.AddFailure(rules.Failure{
|
||||
Failure: "if block ends with a return statement, so drop this else and outdent its block",
|
||||
Type: rules.FailureTypeWarning,
|
||||
Position: w.GetPosition(node.Else.Pos(), node.Else.End()),
|
||||
})
|
||||
}
|
||||
}
|
39
file/file.go
Normal file
39
file/file.go
Normal file
@ -0,0 +1,39 @@
|
||||
package file
|
||||
|
||||
import (
|
||||
"go/ast"
|
||||
"go/parser"
|
||||
"go/token"
|
||||
)
|
||||
|
||||
// File abstraction used for representing files.
|
||||
type File struct {
|
||||
Name string
|
||||
files *token.FileSet
|
||||
Content []byte
|
||||
ast *ast.File
|
||||
}
|
||||
|
||||
// New creates a new file
|
||||
func New(name string, content []byte, files *token.FileSet) (*File, error) {
|
||||
f, err := parser.ParseFile(files, name, content, parser.ParseComments)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &File{
|
||||
Name: name,
|
||||
Content: content,
|
||||
files: files,
|
||||
ast: f,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// ToPosition returns line and column for given position.
|
||||
func (f *File) ToPosition(pos token.Pos) token.Position {
|
||||
return f.files.Position(pos)
|
||||
}
|
||||
|
||||
// GetAST returns the AST of the file
|
||||
func (f *File) GetAST() *ast.File {
|
||||
return f.ast
|
||||
}
|
78
formatters/cli_formatter.go
Normal file
78
formatters/cli_formatter.go
Normal file
@ -0,0 +1,78 @@
|
||||
package formatters
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
|
||||
"github.com/mgechev/golinter/rules"
|
||||
"github.com/olekukonko/tablewriter"
|
||||
"github.com/ttacon/chalk"
|
||||
)
|
||||
|
||||
const (
|
||||
errorEmoji = ""
|
||||
warningEmoji = ""
|
||||
)
|
||||
|
||||
// CLIFormatter is an implementation of the Formatter interface
|
||||
// which formats the errors to JSON.
|
||||
type CLIFormatter struct {
|
||||
Metadata FormatterMetadata
|
||||
}
|
||||
|
||||
func formatFailure(failure rules.Failure) []string {
|
||||
fString := chalk.Blue.Color(failure.Failure)
|
||||
fTypeStr := string(failure.Type)
|
||||
fType := chalk.Red.Color(fTypeStr)
|
||||
lineColumn := failure.Position
|
||||
pos := chalk.Dim.TextStyle(fmt.Sprintf("(%d, %d)", lineColumn.Start.Line, lineColumn.Start.Column))
|
||||
if failure.Type == rules.FailureTypeWarning {
|
||||
fType = chalk.Yellow.Color(fTypeStr)
|
||||
}
|
||||
return []string{failure.GetFilename(), pos, fType, fString}
|
||||
}
|
||||
|
||||
// Format formats the failures gotten from the linter.
|
||||
func (f *CLIFormatter) Format(failures []rules.Failure) (string, error) {
|
||||
var result [][]string
|
||||
var totalErrors = 0
|
||||
for _, f := range failures {
|
||||
result = append(result, formatFailure(f))
|
||||
if f.Type == rules.FailureTypeError {
|
||||
totalErrors++
|
||||
}
|
||||
}
|
||||
total := len(failures)
|
||||
ps := "problems"
|
||||
if total == 1 {
|
||||
ps = "problem"
|
||||
}
|
||||
|
||||
fileReport := make(map[string][][]string)
|
||||
|
||||
for _, row := range result {
|
||||
if _, ok := fileReport[row[0]]; !ok {
|
||||
fileReport[row[0]] = [][]string{}
|
||||
}
|
||||
|
||||
fileReport[row[0]] = append(fileReport[row[0]], []string{row[1], row[2], row[3]})
|
||||
}
|
||||
|
||||
output := ""
|
||||
for filename, val := range fileReport {
|
||||
buf := new(bytes.Buffer)
|
||||
table := tablewriter.NewWriter(buf)
|
||||
table.SetBorder(false)
|
||||
table.SetColumnSeparator("")
|
||||
table.SetRowSeparator("")
|
||||
table.SetAutoWrapText(false)
|
||||
table.AppendBulk(val)
|
||||
table.Render()
|
||||
output += chalk.Dim.TextStyle(chalk.Underline.TextStyle(filename) + "\n")
|
||||
output += buf.String() + "\n"
|
||||
}
|
||||
|
||||
suffix := fmt.Sprintf("\n ✖ %d %s (%d errors) (%d warnings)", total, ps, totalErrors, total-totalErrors)
|
||||
|
||||
return output + suffix, nil
|
||||
}
|
@ -1,13 +1,15 @@
|
||||
package formatters
|
||||
|
||||
import "github.com/mgechev/golinter/visitors"
|
||||
import "github.com/mgechev/golinter/rules"
|
||||
|
||||
// FormatterMetadata configuration of a formatter
|
||||
type FormatterMetadata struct {
|
||||
Name string
|
||||
Description string
|
||||
Sample string
|
||||
}
|
||||
|
||||
// Formatter defines an interface for failure formatters
|
||||
type Formatter interface {
|
||||
Format([]visitors.Failure) string
|
||||
Format([]rules.Failure) string
|
||||
}
|
||||
|
22
formatters/json_formatter.go
Normal file
22
formatters/json_formatter.go
Normal file
@ -0,0 +1,22 @@
|
||||
package formatters
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
|
||||
"github.com/mgechev/golinter/rules"
|
||||
)
|
||||
|
||||
// JSONFormatter is an implementation of the Formatter interface
|
||||
// which formats the errors to JSON.
|
||||
type JSONFormatter struct {
|
||||
Metadata FormatterMetadata
|
||||
}
|
||||
|
||||
// Format formats the failures gotten from the linter.
|
||||
func (f *JSONFormatter) Format(failures []rules.Failure) (string, error) {
|
||||
result, error := json.Marshal(failures)
|
||||
if error != nil {
|
||||
return "", error
|
||||
}
|
||||
return string(result), nil
|
||||
}
|
@ -1,22 +0,0 @@
|
||||
package formatters
|
||||
|
||||
import "github.com/mgechev/golinter/visitors"
|
||||
import "encoding/json"
|
||||
|
||||
type JSONFormatter struct {
|
||||
Metadata FormatterMetadata
|
||||
}
|
||||
|
||||
// {
|
||||
// Name: "JSON Formatter",
|
||||
// Description: "This formatter produces JSON from the errors",
|
||||
// Sample: "[{ \"position\": 10, \"failure\": \"Forbidden semicolon\" }]"
|
||||
// }
|
||||
|
||||
func (f *JSONFormatter) Format(failures []visitors.Failure) (string, error) {
|
||||
result, error := json.Marshal(failures)
|
||||
if error != nil {
|
||||
return "", error
|
||||
}
|
||||
return string(result), nil
|
||||
}
|
10
glide.lock
generated
Normal file
10
glide.lock
generated
Normal file
@ -0,0 +1,10 @@
|
||||
hash: e7fa4d774614236f030e76f8adab9e29c97c0cfc9782e0cffb9c9df47be0e60e
|
||||
updated: 2017-08-27T19:03:09.172223776-07:00
|
||||
imports:
|
||||
- name: github.com/mattn/go-runewidth
|
||||
version: 9e777a8366cce605130a531d2cd6363d07ad7317
|
||||
- name: github.com/olekukonko/tablewriter
|
||||
version: be5337e7b39e64e5f91445ce7e721888dbab7387
|
||||
- name: github.com/ttacon/chalk
|
||||
version: 76b3c8b611dea8f83e49e9ce81fc2b189e0ef3d2
|
||||
testImports: []
|
7
glide.yaml
Normal file
7
glide.yaml
Normal file
@ -0,0 +1,7 @@
|
||||
package: github.com/mgechev/golinter
|
||||
import:
|
||||
- package: github.com/ttacon/chalk
|
||||
version: ~0.1.0
|
||||
- package: github.com/olekukonko/tablewriter
|
||||
- package: github.com/mattn/go-runewidth
|
||||
version: ~0.0.2
|
45
linter/linter.go
Normal file
45
linter/linter.go
Normal file
@ -0,0 +1,45 @@
|
||||
package linter
|
||||
|
||||
import (
|
||||
"go/token"
|
||||
|
||||
"github.com/mgechev/golinter/file"
|
||||
"github.com/mgechev/golinter/rules"
|
||||
)
|
||||
|
||||
// ReadFile defines an abstraction for reading files.
|
||||
type ReadFile func(path string) (result []byte, err error)
|
||||
|
||||
// Linter is used for lintign set of files.
|
||||
type Linter struct {
|
||||
reader ReadFile
|
||||
}
|
||||
|
||||
// New creates a new Linter
|
||||
func New(reader ReadFile) Linter {
|
||||
return Linter{reader: reader}
|
||||
}
|
||||
|
||||
// Lint lints a set of files with the specified rules.
|
||||
func (l *Linter) Lint(filenames []string, ruleSet []rules.Rule) ([]rules.Failure, error) {
|
||||
var fileSet token.FileSet
|
||||
var failures []rules.Failure
|
||||
for _, filename := range filenames {
|
||||
content, err := l.reader(filename)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
file, err := file.New(filename, content, &fileSet)
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for _, rule := range ruleSet {
|
||||
currentFailures := rule.Apply(file, []string{})
|
||||
failures = append(failures, currentFailures...)
|
||||
}
|
||||
}
|
||||
|
||||
return failures, nil
|
||||
}
|
62
main.go
62
main.go
@ -2,52 +2,42 @@ package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"go/ast"
|
||||
"go/parser"
|
||||
"go/token"
|
||||
|
||||
"github.com/mgechev/golinter/syntaxvisitor"
|
||||
"github.com/mgechev/golinter/defaultrules"
|
||||
"github.com/mgechev/golinter/formatters"
|
||||
"github.com/mgechev/golinter/linter"
|
||||
"github.com/mgechev/golinter/rules"
|
||||
)
|
||||
|
||||
type CustomLinter struct {
|
||||
syntaxvisitor.SyntaxVisitor
|
||||
}
|
||||
|
||||
func (w *CustomLinter) VisitIdent(node *ast.Ident) {
|
||||
fmt.Println("Child", node.Name)
|
||||
}
|
||||
|
||||
// This example demonstrates how to inspect the AST of a Go program.
|
||||
func ExampleInspect() {
|
||||
// src is the input for which we want to inspect the AST.
|
||||
func main() {
|
||||
src := `
|
||||
package p
|
||||
const c = 1.0
|
||||
var X = f(3.14)*2 + c
|
||||
|
||||
func Test() {
|
||||
if true {
|
||||
return 42;
|
||||
} else {
|
||||
return 23;
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
// Create the AST by parsing src.
|
||||
fset := token.NewFileSet() // positions are relative to fset
|
||||
f, err := parser.ParseFile(fset, "src.go", src, 0)
|
||||
linter := linter.New(func(file string) ([]byte, error) {
|
||||
return []byte(src), nil
|
||||
})
|
||||
var result []rules.Rule
|
||||
result = append(result, &defaultrules.LintElseRule{})
|
||||
|
||||
failures, err := linter.Lint([]string{"foo.go", "bar.go", "baz.go"}, result)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
var visitor CustomLinter
|
||||
visitor.SyntaxVisitor.Impl = &visitor
|
||||
visitor.Visit(f)
|
||||
var formatter formatters.CLIFormatter
|
||||
output, err := formatter.Format(failures)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
// output:
|
||||
// src.go:2:9: p
|
||||
// src.go:3:7: c
|
||||
// src.go:3:11: 1.0
|
||||
// src.go:4:5: X
|
||||
// src.go:4:9: f
|
||||
// src.go:4:11: 3.14
|
||||
// src.go:4:17: 2
|
||||
// src.go:4:21: c
|
||||
}
|
||||
|
||||
func main() {
|
||||
ExampleInspect()
|
||||
fmt.Println(output)
|
||||
}
|
||||
|
49
rules/rule.go
Normal file
49
rules/rule.go
Normal file
@ -0,0 +1,49 @@
|
||||
package rules
|
||||
|
||||
import (
|
||||
"go/token"
|
||||
|
||||
"github.com/mgechev/golinter/file"
|
||||
)
|
||||
|
||||
const (
|
||||
// FailureTypeWarning declares failures of type warning
|
||||
FailureTypeWarning = "warning"
|
||||
// FailureTypeError declares failures of type error.
|
||||
FailureTypeError = "error"
|
||||
)
|
||||
|
||||
// FailureType is the type for the failure types.
|
||||
type FailureType string
|
||||
|
||||
// Failure defines a struct for a linting failure.
|
||||
type Failure struct {
|
||||
Failure string
|
||||
Type FailureType
|
||||
Position FailurePosition
|
||||
file *file.File
|
||||
}
|
||||
|
||||
// GetFilename returns the filename.
|
||||
func (f *Failure) GetFilename() string {
|
||||
return f.Position.Start.Filename
|
||||
}
|
||||
|
||||
// FailurePosition returns the failure position
|
||||
type FailurePosition struct {
|
||||
Start token.Position
|
||||
End token.Position
|
||||
}
|
||||
|
||||
// RuleArguments is type used for the arguments of a rule.
|
||||
type RuleArguments []string
|
||||
|
||||
type RuleConfig struct {
|
||||
Name string
|
||||
Arguments RuleArguments
|
||||
}
|
||||
|
||||
// Rule defines an abstract rule.
|
||||
type Rule interface {
|
||||
Apply(file *file.File, args RuleArguments) []Failure
|
||||
}
|
37
visitors/rule_visitor.go
Normal file
37
visitors/rule_visitor.go
Normal file
@ -0,0 +1,37 @@
|
||||
package visitors
|
||||
|
||||
import (
|
||||
"go/token"
|
||||
|
||||
"github.com/mgechev/golinter/file"
|
||||
"github.com/mgechev/golinter/rules"
|
||||
)
|
||||
|
||||
// RuleVisitor defines a struct for a visitor.
|
||||
type RuleVisitor struct {
|
||||
SyntaxVisitor
|
||||
RuleName string
|
||||
RuleArguments rules.RuleArguments
|
||||
failures []rules.Failure
|
||||
File *file.File
|
||||
}
|
||||
|
||||
// AddFailure adds a failure to the ist of failures.
|
||||
func (w *RuleVisitor) AddFailure(failure rules.Failure) {
|
||||
w.failures = append(w.failures, failure)
|
||||
}
|
||||
|
||||
// GetFailures returns the list of failures.
|
||||
func (w *RuleVisitor) GetFailures() []rules.Failure {
|
||||
return w.failures
|
||||
}
|
||||
|
||||
// GetPosition returns position by given start and end token.Pos.
|
||||
func (w *RuleVisitor) GetPosition(start token.Pos, end token.Pos) rules.FailurePosition {
|
||||
s := w.File.ToPosition(start)
|
||||
e := w.File.ToPosition(end)
|
||||
return rules.FailurePosition{
|
||||
Start: s,
|
||||
End: e,
|
||||
}
|
||||
}
|
@ -1,35 +0,0 @@
|
||||
package visitors
|
||||
|
||||
import (
|
||||
"go/token"
|
||||
)
|
||||
|
||||
type RuleArguments []string
|
||||
|
||||
const DefaultLength = 1
|
||||
|
||||
type Failure struct {
|
||||
Failure string
|
||||
Position token.Pos
|
||||
}
|
||||
|
||||
type RuleVisitor struct {
|
||||
SyntaxVisitor
|
||||
ruleName string
|
||||
ruleArguments RuleArguments
|
||||
failures []Failure
|
||||
}
|
||||
|
||||
func New(ruleName string, ruleArguments RuleArguments) *RuleVisitor {
|
||||
result := RuleVisitor{ruleName: ruleName, ruleArguments: ruleArguments}
|
||||
result.failures = make([]Failure, DefaultLength)
|
||||
return &result
|
||||
}
|
||||
|
||||
func (w *RuleVisitor) AddFailure(failure Failure) {
|
||||
w.failures = append(w.failures, failure)
|
||||
}
|
||||
|
||||
func (w *RuleVisitor) GetFailures() []Failure {
|
||||
return w.failures
|
||||
}
|
27
visitors/setup.go
Normal file
27
visitors/setup.go
Normal file
@ -0,0 +1,27 @@
|
||||
package visitors
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"reflect"
|
||||
|
||||
"github.com/mgechev/golinter/file"
|
||||
"github.com/mgechev/golinter/rules"
|
||||
)
|
||||
|
||||
// Setup sets the proper pointers of given visitor.
|
||||
func Setup(v interface{}, conf rules.RuleConfig, file *file.File) error {
|
||||
val := reflect.ValueOf(v).Elem()
|
||||
field := val.FieldByName("RuleVisitor")
|
||||
if !field.IsValid() {
|
||||
return errors.New("invalid rule visitor")
|
||||
}
|
||||
field.Set(reflect.ValueOf(RuleVisitor{RuleName: conf.Name, RuleArguments: conf.Arguments, File: file}))
|
||||
|
||||
field = val.FieldByName("Impl")
|
||||
if !field.IsValid() {
|
||||
return errors.New("invalid rule visitor")
|
||||
}
|
||||
field.Set(reflect.ValueOf(v))
|
||||
|
||||
return nil
|
||||
}
|
@ -5,10 +5,13 @@ import (
|
||||
"go/ast"
|
||||
)
|
||||
|
||||
// SyntaxVisitor implements a visitor which knows how to handle the individual
|
||||
// Go lang syntax constructs.
|
||||
type SyntaxVisitor struct {
|
||||
Impl Visitor
|
||||
}
|
||||
|
||||
// Visit accepts an ast.Node and traverse its children.
|
||||
func (w *SyntaxVisitor) Visit(node ast.Node) {
|
||||
if node == nil {
|
||||
return
|
||||
@ -181,6 +184,7 @@ func (w *SyntaxVisitor) Visit(node ast.Node) {
|
||||
}
|
||||
}
|
||||
|
||||
// VisitUnaryExpr visits an unary expression.
|
||||
func (w *SyntaxVisitor) VisitUnaryExpr(node *ast.UnaryExpr) {
|
||||
w.Impl.Visit(node.X)
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user