mirror of
https://github.com/valitydev/thriftlint.git
synced 2024-11-06 00:05:20 +00:00
221 lines
6.1 KiB
Go
221 lines
6.1 KiB
Go
|
// Package linter lints Thrift files.
|
||
|
//
|
||
|
// Actual implementations of linter checks are in the subpackage "checks".
|
||
|
package thriftlint
|
||
|
|
||
|
import (
|
||
|
"io/ioutil"
|
||
|
"log"
|
||
|
"reflect"
|
||
|
"strings"
|
||
|
|
||
|
"github.com/alecthomas/go-thrift/parser"
|
||
|
// Imported to register checkers.
|
||
|
)
|
||
|
|
||
|
type logger interface {
|
||
|
Printf(format string, args ...interface{})
|
||
|
}
|
||
|
|
||
|
type Linter struct {
|
||
|
checkers Checks
|
||
|
includeDirs []string
|
||
|
log logger
|
||
|
}
|
||
|
|
||
|
type Option func(*Linter)
|
||
|
|
||
|
// WithIncludeDirs is an Option that sets the directories to use for searching when parsing Thrift includes.
|
||
|
func WithIncludeDirs(dirs ...string) Option {
|
||
|
return func(l *Linter) { l.includeDirs = dirs }
|
||
|
}
|
||
|
|
||
|
// WithLogger is an Option that sets the logger object used by the linter.
|
||
|
func WithLogger(logger logger) Option {
|
||
|
return func(l *Linter) { l.log = logger }
|
||
|
}
|
||
|
|
||
|
// Disable is an Option that disables the given checks.
|
||
|
func Disable(checks ...string) Option {
|
||
|
return func(l *Linter) {
|
||
|
l.checkers = l.checkers.CloneAndDisable(checks...)
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// New creates a new Linter.
|
||
|
func New(checks []Check, options ...Option) (*Linter, error) {
|
||
|
ids := []string{}
|
||
|
for _, check := range checks {
|
||
|
ids = append(ids, check.ID())
|
||
|
}
|
||
|
l := &Linter{
|
||
|
checkers: Checks(checks),
|
||
|
log: log.New(ioutil.Discard, "", 0),
|
||
|
}
|
||
|
for _, option := range options {
|
||
|
option(l)
|
||
|
}
|
||
|
l.log.Printf("Linting with: %s", strings.Join(ids, ", "))
|
||
|
return l, nil
|
||
|
}
|
||
|
|
||
|
// Lint the given files.
|
||
|
func (l *Linter) Lint(sources []string) (Messages, error) {
|
||
|
l.log.Printf("Parsing %d files", len(sources))
|
||
|
files, err := Parse(l.includeDirs, sources)
|
||
|
if err != nil {
|
||
|
return nil, err
|
||
|
}
|
||
|
messages := Messages{}
|
||
|
for _, file := range files {
|
||
|
l.log.Printf("Linting %s", file.Filename)
|
||
|
v := reflect.ValueOf(file)
|
||
|
enabledChecks := l.checkers.CloneAndDisable()
|
||
|
// Seed the "ancestors" with imports.
|
||
|
ancestors := []interface{}{file.Imports}
|
||
|
messages = append(messages, l.walk(file, ancestors, v, enabledChecks)...)
|
||
|
}
|
||
|
return messages, nil
|
||
|
}
|
||
|
|
||
|
// Apply checks to all Thrift objects in the file.
|
||
|
//
|
||
|
// parent is the last parent struct encountered.
|
||
|
// enabledChecks are updated recursively as (nolint[="check check,..."]) annotations are
|
||
|
// found in the AST.
|
||
|
func (l *Linter) walk(file *parser.Thrift, ancestors []interface{}, v reflect.Value,
|
||
|
enabledChecks Checks) (messages Messages) {
|
||
|
originalNode := v
|
||
|
v = reflect.Indirect(v)
|
||
|
switch v.Kind() {
|
||
|
case reflect.Struct:
|
||
|
// Update enabledChecks.
|
||
|
var annotations []*parser.Annotation
|
||
|
if annotationsField := v.FieldByName("Annotations"); annotationsField.IsValid() {
|
||
|
annotations = annotationsField.Interface().([]*parser.Annotation)
|
||
|
for _, a := range annotations {
|
||
|
if a.Name == "nolint" {
|
||
|
// Skip linting altogether if all checks are disabled.
|
||
|
if a.Value == "" {
|
||
|
return
|
||
|
}
|
||
|
enabledChecks = enabledChecks.CloneAndDisable(strings.Fields(a.Value)...)
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
ancestors = append(ancestors, originalNode.Interface())
|
||
|
for _, checker := range enabledChecks {
|
||
|
id := checker.ID()
|
||
|
for _, msg := range callChecker(checker.Checker(), ancestors) {
|
||
|
msg.File = file
|
||
|
msg.Checker = id
|
||
|
messages = append(messages, msg)
|
||
|
}
|
||
|
}
|
||
|
|
||
|
for i := 0; i < v.NumField(); i++ {
|
||
|
ft := v.Type().Field(i)
|
||
|
if ft.Name == "Pos" || (ft.Name == "Imports" && v.Type() == reflect.TypeOf(parser.Thrift{})) {
|
||
|
continue
|
||
|
}
|
||
|
messages = append(messages, l.walk(file, ancestors, v.Field(i), enabledChecks)...)
|
||
|
}
|
||
|
|
||
|
case reflect.Slice:
|
||
|
for i := 0; i < v.Len(); i++ {
|
||
|
messages = append(messages, l.walk(file, ancestors, v.Index(i), enabledChecks)...)
|
||
|
}
|
||
|
|
||
|
case reflect.Map:
|
||
|
for _, key := range v.MapKeys() {
|
||
|
messages = append(messages, l.walk(file, ancestors, v.MapIndex(key), enabledChecks)...)
|
||
|
}
|
||
|
}
|
||
|
return
|
||
|
}
|
||
|
|
||
|
// Apparently it's non-trivial to get the type of the empty interface...
|
||
|
var emptyInterfaceValue interface{}
|
||
|
var emptyInterfaceType = reflect.TypeOf(&emptyInterfaceValue).Elem()
|
||
|
|
||
|
// Call a checker function if its arguments end with the last element in ancestors, and all other
|
||
|
// arguments are present in ancestors, in order.
|
||
|
//
|
||
|
// For example, given ancestors = {*parser.Thrift, *parser.Struct, *parser.Field}
|
||
|
// the following functions would match:
|
||
|
//
|
||
|
// f(*parser.Thrift, *parser.Struct, *parser.Field)
|
||
|
// f(*parser.Struct, *parser.Field)
|
||
|
// f(*parser.Thrift, *parser.Field)
|
||
|
// f(*parser.Field)
|
||
|
//
|
||
|
// But these would not:
|
||
|
//
|
||
|
// f(*parser.Thrift)
|
||
|
// f(*parser.Struct)
|
||
|
// f(*parser.Field, *parser.Struct)
|
||
|
//
|
||
|
func callChecker(checker interface{}, ancestors []interface{}) Messages {
|
||
|
l := reflect.TypeOf(checker)
|
||
|
if l.Kind() != reflect.Func {
|
||
|
panic("checker must be a function but is " + l.String())
|
||
|
}
|
||
|
if l.NumOut() != 1 || l.Out(0) != reflect.TypeOf(Messages{}) {
|
||
|
panic("checkers must return exactly Messages")
|
||
|
}
|
||
|
|
||
|
args := []reflect.Value{}
|
||
|
switch {
|
||
|
// func(self interface{})
|
||
|
case l.NumIn() == 1 && l.In(0) == emptyInterfaceType:
|
||
|
args = append(args, reflect.ValueOf(ancestors[len(ancestors)-1]))
|
||
|
|
||
|
// func(parent, self interface{})
|
||
|
case l.NumIn() == 2 && l.In(0) == emptyInterfaceType && l.In(1) == emptyInterfaceType:
|
||
|
if len(ancestors) < 2 {
|
||
|
return nil
|
||
|
}
|
||
|
args = append(args,
|
||
|
reflect.ValueOf(ancestors[len(ancestors)-2]),
|
||
|
reflect.ValueOf(ancestors[len(ancestors)-1]),
|
||
|
)
|
||
|
|
||
|
default:
|
||
|
// Ensure last argument matches last ancestor.
|
||
|
if reflect.TypeOf(ancestors[len(ancestors)-1]) != l.In(l.NumIn()-1) {
|
||
|
return nil
|
||
|
}
|
||
|
|
||
|
ancestorsValues := []reflect.Value{}
|
||
|
for _, a := range ancestors {
|
||
|
ancestorsValues = append(ancestorsValues, reflect.ValueOf(a))
|
||
|
}
|
||
|
|
||
|
ancestorIndex := len(ancestorsValues) - 1
|
||
|
for parameterIndex := l.NumIn() - 1; ancestorIndex >= 0 && parameterIndex >= 0; parameterIndex-- {
|
||
|
for ancestorIndex >= 0 {
|
||
|
arg := ancestorsValues[ancestorIndex]
|
||
|
if arg.Type().ConvertibleTo(l.In(parameterIndex)) {
|
||
|
args = append(args, arg)
|
||
|
break
|
||
|
}
|
||
|
ancestorIndex--
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// Arguments did not match.
|
||
|
if len(args) != l.NumIn() {
|
||
|
return nil
|
||
|
}
|
||
|
|
||
|
// Reverse args to correct order.
|
||
|
for i, j := 0, len(args)-1; i < j; i, j = i+1, j-1 {
|
||
|
args[i], args[j] = args[j], args[i]
|
||
|
}
|
||
|
|
||
|
}
|
||
|
out := reflect.ValueOf(checker).Call(args)
|
||
|
return out[0].Interface().(Messages)
|
||
|
}
|