1
0
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:
mgechev 2017-08-27 16:57:16 -07:00
parent f86d165f9c
commit 5b1d2b944c
16 changed files with 406 additions and 95 deletions

2
.gitignore vendored
View File

@ -1 +1,3 @@
golinter
vendor

View 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
View 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
}

View 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
}

View File

@ -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
}

View 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
}

View File

@ -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
View 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
View 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
View 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
View File

@ -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
View 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
View 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,
}
}

View File

@ -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
View 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
}

View File

@ -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)
}