mirror of
https://github.com/valitydev/thriftlint.git
synced 2024-11-06 00:05:20 +00:00
Initial export of Thrift linter.
This commit is contained in:
commit
8f1bd315f6
24
.gitignore
vendored
Normal file
24
.gitignore
vendored
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
# Compiled Object files, Static and Dynamic libs (Shared Objects)
|
||||||
|
*.o
|
||||||
|
*.a
|
||||||
|
*.so
|
||||||
|
|
||||||
|
# Folders
|
||||||
|
_obj
|
||||||
|
_test
|
||||||
|
|
||||||
|
# Architecture specific extensions/prefixes
|
||||||
|
*.[568vq]
|
||||||
|
[568vq].out
|
||||||
|
|
||||||
|
*.cgo1.go
|
||||||
|
*.cgo2.c
|
||||||
|
_cgo_defun.c
|
||||||
|
_cgo_gotypes.go
|
||||||
|
_cgo_export.*
|
||||||
|
|
||||||
|
_testmain.go
|
||||||
|
|
||||||
|
*.exe
|
||||||
|
*.test
|
||||||
|
*.prof
|
21
LICENSE
Normal file
21
LICENSE
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
MIT License
|
||||||
|
|
||||||
|
Copyright (c) 2017 Compass
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
58
README.md
Normal file
58
README.md
Normal file
@ -0,0 +1,58 @@
|
|||||||
|
# An extensible linter for Thrift [![](https://godoc.org/github.com/UrbanCompass/thriftlint?status.svg)](http://godoc.org/github.com/UrbanCompass/thriftlint)
|
||||||
|
|
||||||
|
This is an extensible linter for [Thrift](https://thrift.apache.org/). It
|
||||||
|
includes a set of common lint checks, but allows for both customisation of those
|
||||||
|
checks, and creation of new ones by implementing the
|
||||||
|
[Check](https://godoc.org/github.com/UrbanCompass/thriftlint#Check) interface.
|
||||||
|
|
||||||
|
For an example of how to build your own linter utility, please refer to the
|
||||||
|
[thrift-lint source](https://github.com/UrbanCompass/thriftlint/tree/master/cmd/thrift-lint).
|
||||||
|
|
||||||
|
## Example checker
|
||||||
|
|
||||||
|
Here is an example of a checker utilising the `MakeCheck()` convenience
|
||||||
|
function to ensure that fields are present in the file in the same order as
|
||||||
|
their field numbers:
|
||||||
|
|
||||||
|
```go
|
||||||
|
func CheckStructFieldOrder() thriftlint.Check {
|
||||||
|
return thriftlint.MakeCheck("field.order", func(s *parser.Struct) (messages thriftlint.Messages) {
|
||||||
|
fields := sortedFields(s.Fields)
|
||||||
|
sort.Sort(fields)
|
||||||
|
for i := 0; i < len(fields)-1; i++ {
|
||||||
|
a := fields[i]
|
||||||
|
b := fields[i+1]
|
||||||
|
if a.Pos.Line > b.Pos.Line {
|
||||||
|
messages.Warning(fields[i], "field %d and %d are out of order", a.ID, b.ID)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return
|
||||||
|
})
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## thrift-lint tool
|
||||||
|
|
||||||
|
A binary is included that can be used to perform basic linting with the builtin checks:
|
||||||
|
|
||||||
|
```
|
||||||
|
$ go get github.com/UrbanCompass/thriftlint/cmd/thrift-lint
|
||||||
|
$ thrift-lint --help
|
||||||
|
usage: thrift-lint [<flags>] <sources>...
|
||||||
|
|
||||||
|
A linter for Thrift.
|
||||||
|
|
||||||
|
For details, please refer to https://github.com/UrbanCompass/thriftlint
|
||||||
|
|
||||||
|
Flags:
|
||||||
|
--help Show context-sensitive help (also try --help-long
|
||||||
|
and --help-man).
|
||||||
|
-I, --include=DIR ... Include directories to search.
|
||||||
|
--debug Enable debug logging.
|
||||||
|
--disable=LINTER ... Linters to disable.
|
||||||
|
--list List linter checks.
|
||||||
|
--errors Only show errors.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
<sources> Thrift sources to lint.
|
||||||
|
```
|
28
annotations.go
Normal file
28
annotations.go
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
package thriftlint
|
||||||
|
|
||||||
|
import (
|
||||||
|
"reflect"
|
||||||
|
|
||||||
|
"github.com/alecthomas/go-thrift/parser"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Annotation returns the annotation value associated with "key" from the .Annotations field of a
|
||||||
|
// go-thrift AST node.
|
||||||
|
//
|
||||||
|
// This will panic if node is not a struct with an Annotations field of the correct type.
|
||||||
|
func Annotation(node interface{}, key, dflt string) string {
|
||||||
|
annotations := reflect.Indirect(reflect.ValueOf(node)).
|
||||||
|
FieldByName("Annotations").
|
||||||
|
Interface().([]*parser.Annotation)
|
||||||
|
for _, annotation := range annotations {
|
||||||
|
if annotation.Name == key {
|
||||||
|
return annotation.Value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return dflt
|
||||||
|
}
|
||||||
|
|
||||||
|
// AnnotationExists checks if an annotation is present at all.
|
||||||
|
func AnnotationExists(node interface{}, key string) bool {
|
||||||
|
return Annotation(node, key, "\000") != "\000"
|
||||||
|
}
|
130
api.go
Normal file
130
api.go
Normal file
@ -0,0 +1,130 @@
|
|||||||
|
package thriftlint
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/alecthomas/go-thrift/parser"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Severity of a linter message.
|
||||||
|
type Severity int
|
||||||
|
|
||||||
|
// Message severities.
|
||||||
|
const (
|
||||||
|
Warning Severity = iota
|
||||||
|
Error
|
||||||
|
)
|
||||||
|
|
||||||
|
func (s Severity) String() string {
|
||||||
|
if s == Warning {
|
||||||
|
return "warning"
|
||||||
|
}
|
||||||
|
return "error"
|
||||||
|
}
|
||||||
|
|
||||||
|
// Message represents a single linter message.
|
||||||
|
type Message struct {
|
||||||
|
// File that resulted in the message.
|
||||||
|
File *parser.Thrift
|
||||||
|
// ID of the Checker that generated this message.
|
||||||
|
Checker string
|
||||||
|
Severity Severity
|
||||||
|
Object interface{}
|
||||||
|
Message string
|
||||||
|
}
|
||||||
|
|
||||||
|
// Messages is the set of messages each check should return.
|
||||||
|
//
|
||||||
|
// Typically it will be used like so:
|
||||||
|
//
|
||||||
|
// func MyCheck(...) (messages Messages) {
|
||||||
|
// messages.Warning(t, "some warning")
|
||||||
|
// }
|
||||||
|
type Messages []*Message
|
||||||
|
|
||||||
|
// Warning adds a warning-level message to the Messages.
|
||||||
|
func (w *Messages) Warning(object interface{}, msg string, args ...interface{}) Messages {
|
||||||
|
message := &Message{Severity: Warning, Object: object, Message: fmt.Sprintf(msg, args...)}
|
||||||
|
*w = append(*w, message)
|
||||||
|
return *w
|
||||||
|
}
|
||||||
|
|
||||||
|
// Warning adds an error-level message to the Messages.
|
||||||
|
func (w *Messages) Error(object interface{}, msg string, args ...interface{}) Messages {
|
||||||
|
message := &Message{Severity: Error, Object: object, Message: fmt.Sprintf(msg, args...)}
|
||||||
|
*w = append(*w, message)
|
||||||
|
return *w
|
||||||
|
}
|
||||||
|
|
||||||
|
// Checks is a convenience wrapper around a slice of Checks.
|
||||||
|
type Checks []Check
|
||||||
|
|
||||||
|
// CloneAndDisable returns a copy of this Checks slice with all checks matching prefix disabled.
|
||||||
|
func (c Checks) CloneAndDisable(prefixes ...string) Checks {
|
||||||
|
out := Checks{}
|
||||||
|
skip:
|
||||||
|
for _, check := range c {
|
||||||
|
id := check.ID()
|
||||||
|
for _, prefix := range prefixes {
|
||||||
|
if prefix == id || strings.HasPrefix(id, prefix+".") {
|
||||||
|
continue skip
|
||||||
|
}
|
||||||
|
}
|
||||||
|
out = append(out, check)
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
// Has returns true if the Checks slice contains any checks matching prefix.
|
||||||
|
func (c Checks) Has(prefix string) bool {
|
||||||
|
for _, check := range c {
|
||||||
|
id := check.ID()
|
||||||
|
if prefix == id || strings.HasPrefix(id, prefix+".") {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check implementations are used by the linter to check AST nodes.
|
||||||
|
type Check interface {
|
||||||
|
// ID of the Check. Must be unique across all checks.
|
||||||
|
//
|
||||||
|
// IDs may be hierarchical, separated by a period. eg. "enum", "enum.values"
|
||||||
|
ID() string
|
||||||
|
// Checker returns the checking function.
|
||||||
|
//
|
||||||
|
// The checking function has the signature "func(...) Messages", where "..." is a sequence of
|
||||||
|
// Thrift AST types that are matched against the current node's ancestors as the linter walks
|
||||||
|
// the AST of each file. "..." may also be "interface{}" in which case the checker function
|
||||||
|
// will be called for each node in the AST.
|
||||||
|
//
|
||||||
|
// For example, the function:
|
||||||
|
//
|
||||||
|
// func (s *parser.Struct, f *parser.Field) (messages Messages)
|
||||||
|
//
|
||||||
|
// Will match all each struct field, but not union fields.
|
||||||
|
Checker() interface{}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MakeCheck creates a stateless Check type from an ID and a checker function.
|
||||||
|
func MakeCheck(id string, checker interface{}) Check {
|
||||||
|
return &statelessCheck{
|
||||||
|
id: id,
|
||||||
|
checker: checker,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type statelessCheck struct {
|
||||||
|
id string
|
||||||
|
checker interface{}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *statelessCheck) ID() string {
|
||||||
|
return s.id
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *statelessCheck) Checker() interface{} {
|
||||||
|
return s.checker
|
||||||
|
}
|
24
api_test.go
Normal file
24
api_test.go
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
package thriftlint
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestChecks(t *testing.T) {
|
||||||
|
checks := Checks{
|
||||||
|
MakeCheck("alpha", nil),
|
||||||
|
MakeCheck("alpha.beta", nil),
|
||||||
|
MakeCheck("beta.gamma", nil),
|
||||||
|
MakeCheck("beta.zeta", nil),
|
||||||
|
}
|
||||||
|
|
||||||
|
actual := checks.CloneAndDisable("alpha")
|
||||||
|
expected := Checks{checks[2], checks[3]}
|
||||||
|
require.Equal(t, expected, actual)
|
||||||
|
|
||||||
|
actual = checks.CloneAndDisable("alpha.beta")
|
||||||
|
expected = Checks{checks[0], checks[2], checks[3]}
|
||||||
|
require.Equal(t, expected, actual)
|
||||||
|
}
|
122
checks/annotations.go
Normal file
122
checks/annotations.go
Normal file
@ -0,0 +1,122 @@
|
|||||||
|
package checks
|
||||||
|
|
||||||
|
import (
|
||||||
|
"reflect"
|
||||||
|
"regexp"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/alecthomas/go-thrift/parser"
|
||||||
|
|
||||||
|
"github.com/UrbanCompass/thriftlint"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Accepts urls in the form /(<component>|:<var>)+[/]
|
||||||
|
const urlRegex = "(?:/(?:(?:[a-z0-9_]+)|(:[a-zA-Z_][a-zA-Z0-9_]+)))+/?"
|
||||||
|
|
||||||
|
// var (
|
||||||
|
// annotationRegistry = map[reflect.Type]map[string]string{
|
||||||
|
// thriftlint.ServiceType: {
|
||||||
|
// "api.url": urlRegex,
|
||||||
|
// "api.proxy": urlRegex,
|
||||||
|
// "api.test": "^$",
|
||||||
|
// },
|
||||||
|
// thriftlint.MethodType: {
|
||||||
|
// "api.url": urlRegex,
|
||||||
|
// "api.method": "GET|POST|DELETE|PUT",
|
||||||
|
// "api.roles": ".*",
|
||||||
|
// },
|
||||||
|
// // TODO(cameron): Generate the pattern here from api.InjectorMap once it's submitted.
|
||||||
|
// thriftlint.FieldType: {
|
||||||
|
// "api.inject": "^$",
|
||||||
|
// "api.path": "^$",
|
||||||
|
// },
|
||||||
|
// }
|
||||||
|
// )
|
||||||
|
|
||||||
|
type AnnotationPattern struct {
|
||||||
|
//AST nodes this annotation pattern should apply to.
|
||||||
|
Nodes []reflect.Type
|
||||||
|
Annotation string
|
||||||
|
Regex string
|
||||||
|
}
|
||||||
|
|
||||||
|
type annotationsCheck struct {
|
||||||
|
patterns map[reflect.Type]map[string]string
|
||||||
|
checks thriftlint.Checks
|
||||||
|
}
|
||||||
|
|
||||||
|
// CheckAnnotations validates Thrift annotations against regular expressions.
|
||||||
|
//
|
||||||
|
// All supported annotations must be represented.
|
||||||
|
//
|
||||||
|
// NOTE: This should be the last check added in order to correctly validate the allowed values
|
||||||
|
// for "nolint".
|
||||||
|
func CheckAnnotations(patterns []*AnnotationPattern, checks thriftlint.Checks) thriftlint.Check {
|
||||||
|
patternsLUT := map[reflect.Type]map[string]string{}
|
||||||
|
for _, pattern := range patterns {
|
||||||
|
for _, node := range pattern.Nodes {
|
||||||
|
mapping, ok := patternsLUT[node]
|
||||||
|
if !ok {
|
||||||
|
mapping = map[string]string{}
|
||||||
|
patternsLUT[node] = mapping
|
||||||
|
}
|
||||||
|
mapping[pattern.Annotation] = pattern.Regex
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return &annotationsCheck{
|
||||||
|
patterns: patternsLUT,
|
||||||
|
checks: checks,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *annotationsCheck) ID() string {
|
||||||
|
return "annotations"
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *annotationsCheck) Checker() interface{} {
|
||||||
|
return c.checker
|
||||||
|
}
|
||||||
|
|
||||||
|
// annotationsCheck verifies that annotations match basic regular expressions.
|
||||||
|
//
|
||||||
|
// It does not do any semantic checking.
|
||||||
|
func (c *annotationsCheck) checker(self interface{}) (messages thriftlint.Messages) {
|
||||||
|
v := reflect.Indirect(reflect.ValueOf(self))
|
||||||
|
var annotations []*parser.Annotation
|
||||||
|
if annotationsField := v.FieldByName("Annotations"); annotationsField.IsValid() {
|
||||||
|
annotations = annotationsField.Interface().([]*parser.Annotation)
|
||||||
|
}
|
||||||
|
// Validate type-specific annotations.
|
||||||
|
if checks, ok := c.patterns[v.Type()]; ok {
|
||||||
|
for _, annotation := range annotations {
|
||||||
|
if pattern, ok := checks[annotation.Name]; ok {
|
||||||
|
re := regexp.MustCompile("^(?:" + pattern + ")$")
|
||||||
|
if !re.MatchString(annotation.Value) {
|
||||||
|
messages.Warning(annotation, "invalid value %q for annotation %q (should match %q)",
|
||||||
|
annotation.Value, annotation.Name, pattern)
|
||||||
|
}
|
||||||
|
} else if annotation.Name != "nolint" {
|
||||||
|
messages.Warning(annotation, "unsupported annotation %q", annotation.Name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
for _, annotation := range annotations {
|
||||||
|
if annotation.Name != "nolint" {
|
||||||
|
messages.Warning(annotation, "unsupported annotation %q", annotation.Name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate `nolint` annotation contains only valid checks to be disabled.
|
||||||
|
for _, annotation := range annotations {
|
||||||
|
if annotation.Name == "nolint" && annotation.Value != "" {
|
||||||
|
lints := strings.Split(annotation.Value, ",")
|
||||||
|
for _, l := range lints {
|
||||||
|
if !c.checks.Has(l) {
|
||||||
|
messages.Warning(annotation, "%q is not a known linter check", l)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
17
checks/defaults.go
Normal file
17
checks/defaults.go
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
package checks
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/UrbanCompass/thriftlint"
|
||||||
|
|
||||||
|
"github.com/alecthomas/go-thrift/parser"
|
||||||
|
)
|
||||||
|
|
||||||
|
// CheckDefaultValues checks that default values are not provided.
|
||||||
|
func CheckDefaultValues() thriftlint.Check {
|
||||||
|
return thriftlint.MakeCheck("defaults", func(field *parser.Field) (messages thriftlint.Messages) {
|
||||||
|
if field.Default != nil {
|
||||||
|
messages.Warning(field, "default values are not allowed")
|
||||||
|
}
|
||||||
|
return
|
||||||
|
})
|
||||||
|
}
|
2
checks/doc.go
Normal file
2
checks/doc.go
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
// Package checks contains default checks included with the Thrift linter.
|
||||||
|
package checks
|
28
checks/enums.go
Normal file
28
checks/enums.go
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
package checks
|
||||||
|
|
||||||
|
import (
|
||||||
|
"sort"
|
||||||
|
|
||||||
|
"github.com/UrbanCompass/thriftlint"
|
||||||
|
|
||||||
|
"github.com/alecthomas/go-thrift/parser"
|
||||||
|
)
|
||||||
|
|
||||||
|
// CheckEnumSequence checks that enums start with 0 and increment sequentially.
|
||||||
|
func CheckEnumSequence() thriftlint.Check {
|
||||||
|
return thriftlint.MakeCheck("enum", func(e *parser.Enum) (messages thriftlint.Messages) {
|
||||||
|
values := []int{}
|
||||||
|
for _, v := range e.Values {
|
||||||
|
values = append(values, v.Value)
|
||||||
|
}
|
||||||
|
sort.Sort(sort.IntSlice(values))
|
||||||
|
for i := 0; i < len(values); i++ {
|
||||||
|
if values[i] != i {
|
||||||
|
messages.Warning(e,
|
||||||
|
"enum values for %s do not start at 0 and increase monotonically", e.Name)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return
|
||||||
|
})
|
||||||
|
}
|
31
checks/fields.go
Normal file
31
checks/fields.go
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
package checks
|
||||||
|
|
||||||
|
import (
|
||||||
|
"sort"
|
||||||
|
|
||||||
|
"github.com/alecthomas/go-thrift/parser"
|
||||||
|
|
||||||
|
"github.com/UrbanCompass/thriftlint"
|
||||||
|
)
|
||||||
|
|
||||||
|
type sortedFields []*parser.Field
|
||||||
|
|
||||||
|
func (s sortedFields) Len() int { return len(s) }
|
||||||
|
func (s sortedFields) Swap(i, j int) { s[i], s[j] = s[j], s[i] }
|
||||||
|
func (s sortedFields) Less(i, j int) bool { return s[i].ID < s[j].ID }
|
||||||
|
|
||||||
|
// CheckStructFieldOrder ensures that struct field IDs are present in-order in the file.
|
||||||
|
func CheckStructFieldOrder() thriftlint.Check {
|
||||||
|
return thriftlint.MakeCheck("field.order", func(s *parser.Struct) (messages thriftlint.Messages) {
|
||||||
|
fields := sortedFields(s.Fields)
|
||||||
|
sort.Sort(fields)
|
||||||
|
for i := 0; i < len(fields)-1; i++ {
|
||||||
|
a := fields[i]
|
||||||
|
b := fields[i+1]
|
||||||
|
if a.Pos.Line > b.Pos.Line {
|
||||||
|
messages.Warning(fields[i], "field %d and %d are out of order", a.ID, b.ID)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return
|
||||||
|
})
|
||||||
|
}
|
38
checks/indentation.go
Normal file
38
checks/indentation.go
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
package checks
|
||||||
|
|
||||||
|
import (
|
||||||
|
"reflect"
|
||||||
|
|
||||||
|
"github.com/UrbanCompass/thriftlint"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
// Map of (parent, self) AST node types to the expected indentation for that type in that
|
||||||
|
// context.
|
||||||
|
expectedIndentation = map[string]int{
|
||||||
|
indentationContext(thriftlint.ThriftType, thriftlint.ServiceType): 1,
|
||||||
|
indentationContext(thriftlint.ServiceType, thriftlint.MethodType): 3,
|
||||||
|
indentationContext(thriftlint.ThriftType, thriftlint.EnumType): 1,
|
||||||
|
indentationContext(thriftlint.EnumType, thriftlint.EnumValueType): 3,
|
||||||
|
indentationContext(thriftlint.ThriftType, thriftlint.StructType): 8,
|
||||||
|
indentationContext(thriftlint.StructType, thriftlint.FieldType): 3,
|
||||||
|
indentationContext(thriftlint.ThriftType, thriftlint.TypedefType): 1,
|
||||||
|
indentationContext(thriftlint.ThriftType, thriftlint.ConstantType): 1,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
func indentationContext(parent, self reflect.Type) string {
|
||||||
|
return parent.Name() + ":" + self.Name()
|
||||||
|
}
|
||||||
|
|
||||||
|
// CheckIndentation checks indentation is a multiple of 2.
|
||||||
|
func CheckIndentation() thriftlint.Check {
|
||||||
|
return thriftlint.MakeCheck("indentation", func(parent, self interface{}) (messages thriftlint.Messages) {
|
||||||
|
context := indentationContext(reflect.TypeOf(parent), reflect.TypeOf(self))
|
||||||
|
pos := thriftlint.Pos(self)
|
||||||
|
if expected, ok := expectedIndentation[context]; ok && expected != pos.Col {
|
||||||
|
messages.Warning(self, "should be indented to column %d not %d", expected, pos.Col)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
})
|
||||||
|
}
|
30
checks/maps.go
Normal file
30
checks/maps.go
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
package checks
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/UrbanCompass/thriftlint"
|
||||||
|
"github.com/alecthomas/go-thrift/parser"
|
||||||
|
)
|
||||||
|
|
||||||
|
// CheckMapKeys verifies that map keys are valid types.
|
||||||
|
func CheckMapKeys() thriftlint.Check {
|
||||||
|
return thriftlint.MakeCheck("map", checkMapKeys)
|
||||||
|
}
|
||||||
|
|
||||||
|
func checkMapKeys(file *parser.Thrift, t *parser.Type) (messages thriftlint.Messages) {
|
||||||
|
if t.Name == "map" {
|
||||||
|
kn := t.KeyType.Name
|
||||||
|
if kn != "string" && kn != "i16" && kn != "i32" && kn != "i64" && kn != "double" {
|
||||||
|
// Not an integral type, check if it's an enum and allow it if so.
|
||||||
|
resolved := thriftlint.Resolve(kn, file)
|
||||||
|
isEnum := false
|
||||||
|
if resolved != nil {
|
||||||
|
_, isEnum = resolved.(*parser.Enum)
|
||||||
|
}
|
||||||
|
if !isEnum {
|
||||||
|
messages.Error(t, "map keys must be string, enum, integer or double, not %q", kn)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return checkMapKeys(file, t.ValueType)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
72
checks/naming.go
Normal file
72
checks/naming.go
Normal file
@ -0,0 +1,72 @@
|
|||||||
|
package checks
|
||||||
|
|
||||||
|
import (
|
||||||
|
"reflect"
|
||||||
|
"regexp"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/UrbanCompass/thriftlint"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
upperCamelCaseRegex = `^[_A-Z][a-z]*([A-Z][0-9a-z]*)*$`
|
||||||
|
lowerCamelCaseRegex = `^[_a-z]+([A-Z0-9a-z]*)*$`
|
||||||
|
upperSnakeCaseRegex = `^[A-Z_]+([A-Z0-9]+_?)*$`
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
// CheckNamesDefaults is a map of Thrift AST node type to a regular expression for
|
||||||
|
// validating names of that type.
|
||||||
|
CheckNamesDefaults = map[reflect.Type]string{
|
||||||
|
thriftlint.ServiceType: upperCamelCaseRegex,
|
||||||
|
thriftlint.EnumType: upperCamelCaseRegex,
|
||||||
|
thriftlint.StructType: upperCamelCaseRegex,
|
||||||
|
thriftlint.EnumValueType: upperSnakeCaseRegex,
|
||||||
|
thriftlint.FieldType: lowerCamelCaseRegex,
|
||||||
|
thriftlint.MethodType: lowerCamelCaseRegex,
|
||||||
|
thriftlint.ConstantType: upperSnakeCaseRegex,
|
||||||
|
}
|
||||||
|
|
||||||
|
// CheckNamesDefaultBlacklist is names that should never be used for symbols.
|
||||||
|
CheckNamesDefaultBlacklist = map[string]bool{
|
||||||
|
"class": true,
|
||||||
|
"int": true,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
// CheckNames checks Thrift symbols comply with a set of regular expressions.
|
||||||
|
//
|
||||||
|
// If mathces or blacklist are nil, global defaults will be used.
|
||||||
|
func CheckNames(matches map[reflect.Type]string, blacklist map[string]bool) thriftlint.Check {
|
||||||
|
if matches == nil {
|
||||||
|
matches = CheckNamesDefaults
|
||||||
|
}
|
||||||
|
if blacklist == nil {
|
||||||
|
blacklist = CheckNamesDefaultBlacklist
|
||||||
|
}
|
||||||
|
regexes := map[reflect.Type]*regexp.Regexp{}
|
||||||
|
for t, p := range matches {
|
||||||
|
regexes[t] = regexp.MustCompile(p)
|
||||||
|
}
|
||||||
|
return thriftlint.MakeCheck("naming", func(v interface{}) (messages thriftlint.Messages) {
|
||||||
|
rv := reflect.Indirect(reflect.ValueOf(v))
|
||||||
|
nameField := rv.FieldByName("Name")
|
||||||
|
if !nameField.IsValid() {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
name := nameField.Interface().(string)
|
||||||
|
// Special-case DEPRECATED_ fields.
|
||||||
|
checker, ok := regexes[rv.Type()]
|
||||||
|
if !ok || strings.HasPrefix(name, "DEPRECATED_") {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if blacklist[name] {
|
||||||
|
messages.Warning(v, "%q is a disallowed name", name)
|
||||||
|
}
|
||||||
|
if ok := checker.MatchString(name); !ok {
|
||||||
|
messages.Warning(v, "name of %s %q should match %q", strings.ToLower(rv.Type().Name()),
|
||||||
|
name, checker.String())
|
||||||
|
}
|
||||||
|
return
|
||||||
|
})
|
||||||
|
}
|
18
checks/optional.go
Normal file
18
checks/optional.go
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
package checks
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/UrbanCompass/thriftlint"
|
||||||
|
|
||||||
|
"github.com/alecthomas/go-thrift/parser"
|
||||||
|
)
|
||||||
|
|
||||||
|
// CheckOptional ensures that all Thrift fields are optional, as is generally accepted best
|
||||||
|
// practice for Thrift.
|
||||||
|
func CheckOptional() thriftlint.Check {
|
||||||
|
return thriftlint.MakeCheck("optional", func(s *parser.Struct, f *parser.Field) (messages thriftlint.Messages) {
|
||||||
|
if f.Type.Name != "list" && f.Type.Name != "set" && f.Type.Name != "map" && !f.Optional {
|
||||||
|
messages.Warning(f, "%s must be optional", f.Name)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
})
|
||||||
|
}
|
19
checks/types.go
Normal file
19
checks/types.go
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
package checks
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/alecthomas/go-thrift/parser"
|
||||||
|
|
||||||
|
"github.com/UrbanCompass/thriftlint"
|
||||||
|
)
|
||||||
|
|
||||||
|
// CheckTypeReferences checks that types referenced in Thrift files are actually imported
|
||||||
|
// and exist.
|
||||||
|
func CheckTypeReferences() thriftlint.Check {
|
||||||
|
return thriftlint.MakeCheck("types", func(file *parser.Thrift, t *parser.Type) (messages thriftlint.Messages) {
|
||||||
|
if !thriftlint.BuiltinThriftTypes[t.Name] && !thriftlint.BuiltinThriftCollections[t.Name] &&
|
||||||
|
thriftlint.Resolve(t.Name, file) == nil {
|
||||||
|
messages.Error(t, "unknown type %q", t.Name)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
})
|
||||||
|
}
|
71
cmd/thrift-lint/main.go
Normal file
71
cmd/thrift-lint/main.go
Normal file
@ -0,0 +1,71 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"os"
|
||||||
|
|
||||||
|
"gopkg.in/alecthomas/kingpin.v3-unstable"
|
||||||
|
|
||||||
|
"github.com/UrbanCompass/thriftlint"
|
||||||
|
"github.com/UrbanCompass/thriftlint/checks"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
includeDirsFlag = kingpin.Flag("include", "Include directories to search.").Short('I').PlaceHolder("DIR").ExistingDirs()
|
||||||
|
debugFlag = kingpin.Flag("debug", "Enable debug logging.").Bool()
|
||||||
|
disableFlag = kingpin.Flag("disable", "Linters to disable.").PlaceHolder("LINTER").Strings()
|
||||||
|
listFlag = kingpin.Flag("list", "List linter checks.").Bool()
|
||||||
|
errorFlag = kingpin.Flag("errors", "Only show errors.").Bool()
|
||||||
|
sourcesArgs = kingpin.Arg("sources", "Thrift sources to lint.").Required().ExistingFiles()
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
kingpin.CommandLine.Help = `A linter for Thrift.
|
||||||
|
|
||||||
|
For details, please refer to https://github.com/UrbanCompass/thriftlint
|
||||||
|
`
|
||||||
|
kingpin.Parse()
|
||||||
|
checkers := thriftlint.Checks{
|
||||||
|
checks.CheckIndentation(),
|
||||||
|
checks.CheckNames(nil, nil),
|
||||||
|
checks.CheckOptional(),
|
||||||
|
checks.CheckDefaultValues(),
|
||||||
|
checks.CheckEnumSequence(),
|
||||||
|
checks.CheckMapKeys(),
|
||||||
|
checks.CheckTypeReferences(),
|
||||||
|
checks.CheckStructFieldOrder(),
|
||||||
|
}
|
||||||
|
checkers = append(checkers, checks.CheckAnnotations(nil, checkers))
|
||||||
|
|
||||||
|
if *listFlag {
|
||||||
|
for _, linter := range checkers {
|
||||||
|
fmt.Printf("%s\n", linter.ID())
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
options := []thriftlint.Option{
|
||||||
|
thriftlint.WithIncludeDirs(*includeDirsFlag...),
|
||||||
|
thriftlint.Disable(*disableFlag...),
|
||||||
|
}
|
||||||
|
if *debugFlag {
|
||||||
|
logger := log.New(os.Stdout, "debug: ", 0)
|
||||||
|
options = append(options, thriftlint.WithLogger(logger))
|
||||||
|
}
|
||||||
|
linter, err := thriftlint.New(checkers, options...)
|
||||||
|
kingpin.FatalIfError(err, "")
|
||||||
|
messages, err := linter.Lint(*sourcesArgs)
|
||||||
|
kingpin.FatalIfError(err, "")
|
||||||
|
status := 0
|
||||||
|
for _, msg := range messages {
|
||||||
|
if *errorFlag && msg.Severity != thriftlint.Error {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
pos := thriftlint.Pos(msg.Object)
|
||||||
|
fmt.Fprintf(os.Stderr, "%s:%d:%d:%s: %s (%s)\n", msg.File.Filename, pos.Line, pos.Col,
|
||||||
|
msg.Severity, msg.Message, msg.Checker)
|
||||||
|
status |= 1 << uint(msg.Severity)
|
||||||
|
}
|
||||||
|
os.Exit(status)
|
||||||
|
}
|
46
context.go
Normal file
46
context.go
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
package thriftlint
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/alecthomas/go-thrift/parser"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Resolve a symbol within a file to its type.
|
||||||
|
func Resolve(symbol string, file *parser.Thrift) interface{} {
|
||||||
|
parts := strings.SplitN(symbol, ".", 2)
|
||||||
|
name := symbol
|
||||||
|
target := file
|
||||||
|
if len(parts) == 2 {
|
||||||
|
target = file.Imports[parts[0]]
|
||||||
|
if target == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
name = parts[1]
|
||||||
|
if target == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if t, ok := target.Constants[name]; ok {
|
||||||
|
return t
|
||||||
|
}
|
||||||
|
if t, ok := target.Enums[name]; ok {
|
||||||
|
return t
|
||||||
|
}
|
||||||
|
if t, ok := target.Exceptions[name]; ok {
|
||||||
|
return t
|
||||||
|
}
|
||||||
|
if t, ok := target.Services[name]; ok {
|
||||||
|
return t
|
||||||
|
}
|
||||||
|
if t, ok := target.Structs[name]; ok {
|
||||||
|
return t
|
||||||
|
}
|
||||||
|
if t, ok := target.Typedefs[name]; ok {
|
||||||
|
return t
|
||||||
|
}
|
||||||
|
if t, ok := target.Unions[name]; ok {
|
||||||
|
return t
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
67
context_test.go
Normal file
67
context_test.go
Normal file
@ -0,0 +1,67 @@
|
|||||||
|
package thriftlint
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/alecthomas/go-thrift/parser"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestResolve(t *testing.T) {
|
||||||
|
parsed, err := parser.Parse("test.thrift", []byte(`
|
||||||
|
struct Struct {};
|
||||||
|
const string CONST = "";
|
||||||
|
enum Enum { CASE = 1; };
|
||||||
|
exception Exception {};
|
||||||
|
service Service {};
|
||||||
|
typedef Service Typedef;
|
||||||
|
union Union {};
|
||||||
|
`))
|
||||||
|
require.NoError(t, err)
|
||||||
|
ast := parsed.(*parser.Thrift)
|
||||||
|
ast.Imports = map[string]*parser.Thrift{
|
||||||
|
"pkg": ast,
|
||||||
|
}
|
||||||
|
|
||||||
|
actual := Resolve("Struct", ast)
|
||||||
|
require.NotNil(t, actual)
|
||||||
|
require.Equal(t, ast.Structs["Struct"], actual)
|
||||||
|
actual = Resolve("pkg.Struct", ast)
|
||||||
|
require.NotNil(t, actual)
|
||||||
|
require.Equal(t, ast.Structs["Struct"], actual)
|
||||||
|
|
||||||
|
actual = Resolve("CONST", ast)
|
||||||
|
require.NotNil(t, actual)
|
||||||
|
require.Equal(t, ast.Constants["CONST"], actual)
|
||||||
|
actual = Resolve("pkg.CONST", ast)
|
||||||
|
require.NotNil(t, actual)
|
||||||
|
require.Equal(t, ast.Constants["CONST"], actual)
|
||||||
|
|
||||||
|
actual = Resolve("Enum", ast)
|
||||||
|
require.NotNil(t, actual)
|
||||||
|
require.Equal(t, ast.Enums["Enum"], actual)
|
||||||
|
actual = Resolve("pkg.Enum", ast)
|
||||||
|
require.NotNil(t, actual)
|
||||||
|
require.Equal(t, ast.Enums["Enum"], actual)
|
||||||
|
|
||||||
|
actual = Resolve("Service", ast)
|
||||||
|
require.NotNil(t, actual)
|
||||||
|
require.Equal(t, ast.Services["Service"], actual)
|
||||||
|
actual = Resolve("pkg.Service", ast)
|
||||||
|
require.NotNil(t, actual)
|
||||||
|
require.Equal(t, ast.Services["Service"], actual)
|
||||||
|
|
||||||
|
actual = Resolve("Typedef", ast)
|
||||||
|
require.NotNil(t, actual)
|
||||||
|
require.Equal(t, ast.Typedefs["Typedef"], actual)
|
||||||
|
actual = Resolve("pkg.Typedef", ast)
|
||||||
|
require.NotNil(t, actual)
|
||||||
|
require.Equal(t, ast.Typedefs["Typedef"], actual)
|
||||||
|
|
||||||
|
actual = Resolve("Union", ast)
|
||||||
|
require.NotNil(t, actual)
|
||||||
|
require.Equal(t, ast.Unions["Union"], actual)
|
||||||
|
actual = Resolve("pkg.Union", ast)
|
||||||
|
require.NotNil(t, actual)
|
||||||
|
require.Equal(t, ast.Unions["Union"], actual)
|
||||||
|
}
|
7
doc.go
Normal file
7
doc.go
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
// Package thriftlint is an extensible Linter for Thrift files, written in Go.
|
||||||
|
//
|
||||||
|
// New() takes a set of checks and options and creates a linter. The linter checks can then
|
||||||
|
// be applied to a set of files with .Lint(files...).
|
||||||
|
//
|
||||||
|
// See the README for details.
|
||||||
|
package thriftlint
|
220
linter.go
Normal file
220
linter.go
Normal file
@ -0,0 +1,220 @@
|
|||||||
|
// 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)
|
||||||
|
}
|
43
linter_test.go
Normal file
43
linter_test.go
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
package thriftlint
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/alecthomas/go-thrift/parser"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestCallCheckerValidation(t *testing.T) {
|
||||||
|
failf := func(*parser.Thrift) {}
|
||||||
|
require.Panics(t, func() { callChecker(failf, []interface{}{}) })
|
||||||
|
okf := func(*parser.Thrift) Messages { return nil }
|
||||||
|
require.NotPanics(t, func() { callChecker(okf, []interface{}{&parser.Thrift{}}) })
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCallChecker(t *testing.T) {
|
||||||
|
okfuncs := []interface{}{
|
||||||
|
func(*parser.Thrift, *parser.Struct, *parser.Field) Messages {
|
||||||
|
return Messages{}
|
||||||
|
},
|
||||||
|
func(*parser.Struct, *parser.Field) Messages { return Messages{} },
|
||||||
|
func(*parser.Thrift, *parser.Field) Messages { return Messages{} },
|
||||||
|
func(*parser.Field) Messages { return Messages{} },
|
||||||
|
func(self interface{}) Messages { return Messages{} },
|
||||||
|
func(parent, self interface{}) Messages { return Messages{} },
|
||||||
|
}
|
||||||
|
ancestors := []interface{}{&parser.Thrift{}, &parser.Struct{}, &parser.Field{}}
|
||||||
|
for _, okf := range okfuncs {
|
||||||
|
out := callChecker(okf, ancestors)
|
||||||
|
require.NotNil(t, out)
|
||||||
|
}
|
||||||
|
|
||||||
|
badfuncs := []interface{}{
|
||||||
|
func(*parser.Thrift) Messages { return Messages{} },
|
||||||
|
func(*parser.Struct) Messages { return Messages{} },
|
||||||
|
func(*parser.Field, *parser.Struct) Messages { return Messages{} },
|
||||||
|
}
|
||||||
|
for _, badf := range badfuncs {
|
||||||
|
out := callChecker(badf, ancestors)
|
||||||
|
require.Nil(t, out)
|
||||||
|
}
|
||||||
|
}
|
73
parse.go
Normal file
73
parse.go
Normal file
@ -0,0 +1,73 @@
|
|||||||
|
package thriftlint
|
||||||
|
|
||||||
|
import (
|
||||||
|
"io"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
|
||||||
|
"github.com/alecthomas/go-thrift/parser"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Parse a set of .thrift source files into their corresponding ASTs.
|
||||||
|
func Parse(includeDirs []string, sources []string) (map[string]*parser.Thrift, error) {
|
||||||
|
p := parser.New()
|
||||||
|
p.Filesystem = &includeFilesystem{IncludeDirs: includeDirs}
|
||||||
|
|
||||||
|
var files map[string]*parser.Thrift
|
||||||
|
for _, path := range sources {
|
||||||
|
path, err := filepath.Abs(path)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
files, _, err = p.ParseFile(path)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for _, file := range files {
|
||||||
|
file.Imports = map[string]*parser.Thrift{}
|
||||||
|
for symbol, path := range file.Includes {
|
||||||
|
file.Imports[symbol] = files[path]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return files, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// A go-thrift/parser.Filesystem implementation that searches include dirs when attempting to open
|
||||||
|
// sources.
|
||||||
|
type includeFilesystem struct {
|
||||||
|
IncludeDirs []string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (i *includeFilesystem) Open(filename string) (io.ReadCloser, error) {
|
||||||
|
if filepath.IsAbs(filename) {
|
||||||
|
return os.Open(filename)
|
||||||
|
}
|
||||||
|
for _, d := range i.IncludeDirs {
|
||||||
|
path := filepath.Join(d, filename)
|
||||||
|
r, err := os.Open(path)
|
||||||
|
if os.IsNotExist(err) {
|
||||||
|
continue
|
||||||
|
} else if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return r, nil
|
||||||
|
}
|
||||||
|
return nil, os.ErrNotExist
|
||||||
|
}
|
||||||
|
|
||||||
|
func (i *includeFilesystem) Abs(dir, path string) (string, error) {
|
||||||
|
if filepath.IsAbs(path) {
|
||||||
|
return path, nil
|
||||||
|
}
|
||||||
|
for _, d := range i.IncludeDirs {
|
||||||
|
p, err := filepath.Abs(filepath.Join(d, path))
|
||||||
|
if err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if _, err := os.Stat(p); err == nil {
|
||||||
|
return filepath.Abs(p)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return filepath.Abs(filepath.Join(dir, path))
|
||||||
|
}
|
269
symbol.go
Normal file
269
symbol.go
Normal file
@ -0,0 +1,269 @@
|
|||||||
|
package thriftlint
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"fmt"
|
||||||
|
"go/doc"
|
||||||
|
"reflect"
|
||||||
|
"strings"
|
||||||
|
"unicode"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
// BuiltinThriftTypes is a map of the basic builtin Thrift types. Useful in templates.
|
||||||
|
BuiltinThriftTypes = map[string]bool{
|
||||||
|
"bool": true,
|
||||||
|
"byte": true,
|
||||||
|
"i16": true,
|
||||||
|
"i32": true,
|
||||||
|
"i64": true,
|
||||||
|
"double": true,
|
||||||
|
"string": true,
|
||||||
|
}
|
||||||
|
|
||||||
|
// BuiltinThriftCollections is the set of builtin collection types in Thrift.
|
||||||
|
BuiltinThriftCollections = map[string]bool{
|
||||||
|
"map": true,
|
||||||
|
"list": true,
|
||||||
|
"set": true,
|
||||||
|
"binary": true,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
// Scanner over a sequence of runes.
|
||||||
|
type scanner struct {
|
||||||
|
runes []rune
|
||||||
|
cursor int
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *scanner) peek() rune {
|
||||||
|
if s.cursor >= len(s.runes) {
|
||||||
|
return -1
|
||||||
|
}
|
||||||
|
return s.runes[s.cursor]
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *scanner) next() rune {
|
||||||
|
r := s.peek()
|
||||||
|
if r != -1 {
|
||||||
|
s.cursor++
|
||||||
|
}
|
||||||
|
return r
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *scanner) reverse() {
|
||||||
|
s.cursor--
|
||||||
|
}
|
||||||
|
|
||||||
|
func consumeLower(scan *scanner) string {
|
||||||
|
out := ""
|
||||||
|
for unicode.IsLower(scan.peek()) || unicode.IsNumber(scan.peek()) {
|
||||||
|
out += string(scan.next())
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
// ([A-Z]+)(?:[A-Z][a-z]|$)
|
||||||
|
func consumeMostUpper(scan *scanner) string {
|
||||||
|
out := ""
|
||||||
|
for unicode.IsUpper(scan.peek()) || unicode.IsNumber(scan.peek()) {
|
||||||
|
r := scan.next()
|
||||||
|
if unicode.IsLower(scan.peek()) && !commonInitialisms[out+string(r)] {
|
||||||
|
scan.reverse()
|
||||||
|
break
|
||||||
|
}
|
||||||
|
out += string(r)
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
func title(s string) string {
|
||||||
|
return strings.ToUpper(s[0:1]) + strings.ToLower(s[1:])
|
||||||
|
}
|
||||||
|
|
||||||
|
// From https://github.com/golang/lint/blob/master/lint.go
|
||||||
|
var commonInitialisms = map[string]bool{
|
||||||
|
"API": true,
|
||||||
|
"ASCII": true,
|
||||||
|
"CPU": true,
|
||||||
|
"CSS": true,
|
||||||
|
"DB": true,
|
||||||
|
"DNS": true,
|
||||||
|
"EOF": true,
|
||||||
|
"GUID": true,
|
||||||
|
"HTML": true,
|
||||||
|
"HTTP": true,
|
||||||
|
"HTTPS": true,
|
||||||
|
"ID": true,
|
||||||
|
"IP": true,
|
||||||
|
"JSON": true,
|
||||||
|
"LHS": true,
|
||||||
|
"MD5": true,
|
||||||
|
"MLS": true,
|
||||||
|
"OK": true,
|
||||||
|
"QPS": true,
|
||||||
|
"RAM": true,
|
||||||
|
"RHS": true,
|
||||||
|
"RPC": true,
|
||||||
|
"SHA": true,
|
||||||
|
"SLA": true,
|
||||||
|
"SMTP": true,
|
||||||
|
"SQL": true,
|
||||||
|
"SSH": true,
|
||||||
|
"TCP": true,
|
||||||
|
"TLS": true,
|
||||||
|
"TTL": true,
|
||||||
|
"UDP": true,
|
||||||
|
"UI": true,
|
||||||
|
"UID": true,
|
||||||
|
"URI": true,
|
||||||
|
"URL": true,
|
||||||
|
"UTC": true,
|
||||||
|
"UTF8": true,
|
||||||
|
"UUID": true,
|
||||||
|
"VM": true,
|
||||||
|
"XML": true,
|
||||||
|
"XSRF": true,
|
||||||
|
"XSS": true,
|
||||||
|
}
|
||||||
|
|
||||||
|
func Comment(v interface{}) []string {
|
||||||
|
comment := reflect.Indirect(reflect.ValueOf(v)).FieldByName("Comment").Interface().(string)
|
||||||
|
if comment == "" {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
w := bytes.NewBuffer(nil)
|
||||||
|
doc.ToText(w, comment, "", "", 80)
|
||||||
|
return strings.Split(strings.TrimSpace(w.String()), "\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
func IsInitialism(s string) bool {
|
||||||
|
return commonInitialisms[strings.ToUpper(s)]
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpperCamelCase converts a symbol to CamelCase
|
||||||
|
func UpperCamelCase(s string) string {
|
||||||
|
parts := []string{}
|
||||||
|
for _, part := range SplitSymbol(s) {
|
||||||
|
if part == "" {
|
||||||
|
parts = append(parts, "_")
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if part == "s" && len(parts) > 0 {
|
||||||
|
parts[len(parts)-1] += part
|
||||||
|
} else {
|
||||||
|
if commonInitialisms[strings.ToUpper(part)] {
|
||||||
|
part = strings.ToUpper(part)
|
||||||
|
} else {
|
||||||
|
part = title(part)
|
||||||
|
}
|
||||||
|
parts = append(parts, part)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return strings.Join(parts, "")
|
||||||
|
}
|
||||||
|
|
||||||
|
// LowerCamelCase converts a symbol to lowerCamelCase
|
||||||
|
func LowerCamelCase(s string) string {
|
||||||
|
first := true
|
||||||
|
parts := []string{}
|
||||||
|
for _, part := range SplitSymbol(s) {
|
||||||
|
if part == "" {
|
||||||
|
parts = append(parts, "_")
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if first {
|
||||||
|
parts = append(parts, strings.ToLower(part))
|
||||||
|
first = false
|
||||||
|
} else {
|
||||||
|
// Merge trailing s
|
||||||
|
if part == "s" && len(parts) > 0 {
|
||||||
|
parts[len(parts)-1] += part
|
||||||
|
} else {
|
||||||
|
if commonInitialisms[strings.ToUpper(part)] {
|
||||||
|
part = strings.ToUpper(part)
|
||||||
|
} else {
|
||||||
|
part = title(part)
|
||||||
|
}
|
||||||
|
parts = append(parts, part)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return strings.Join(parts, "")
|
||||||
|
}
|
||||||
|
|
||||||
|
// LowerSnakeCase converts a symbol to snake_case
|
||||||
|
func LowerSnakeCase(s string) string {
|
||||||
|
parts := []string{}
|
||||||
|
for _, part := range SplitSymbol(s) {
|
||||||
|
if part == "" {
|
||||||
|
parts = append(parts, "_")
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
parts = append(parts, strings.ToLower(part))
|
||||||
|
}
|
||||||
|
return strings.Join(parts, "_")
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpperSnakeCase converts a symbol to UPPER_SNAKE_CASE
|
||||||
|
func UpperSnakeCase(s string) string {
|
||||||
|
parts := []string{}
|
||||||
|
for _, part := range SplitSymbol(s) {
|
||||||
|
if part == "" {
|
||||||
|
parts = append(parts, "_")
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
parts = append(parts, strings.ToUpper(part))
|
||||||
|
}
|
||||||
|
return strings.Join(parts, "_")
|
||||||
|
}
|
||||||
|
|
||||||
|
// SplitSymbol splits an arbitrary symbol into parts. It accepts symbols in snake case and camel
|
||||||
|
// case, and correctly supports all-caps substrings.
|
||||||
|
//
|
||||||
|
// eg. "some_snake_case_symbol" would become ["some", "snake", "case", "symbol"]
|
||||||
|
// and "someCamelCaseSymbol" would become ["some", "Camel", "Case", "Symbol"]
|
||||||
|
func SplitSymbol(s string) []string {
|
||||||
|
// This is painful. See TestSplitSymbol for examples of what this does.
|
||||||
|
out := []string{}
|
||||||
|
scan := &scanner{runes: []rune(s)}
|
||||||
|
for scan.peek() != -1 {
|
||||||
|
part := ""
|
||||||
|
r := scan.peek()
|
||||||
|
switch {
|
||||||
|
case unicode.IsLower(r):
|
||||||
|
part = consumeLower(scan)
|
||||||
|
case unicode.IsUpper(r):
|
||||||
|
scan.next()
|
||||||
|
// [A-Z][a-z]+
|
||||||
|
if unicode.IsLower(scan.peek()) {
|
||||||
|
part += string(r)
|
||||||
|
part += consumeLower(scan)
|
||||||
|
} else {
|
||||||
|
scan.reverse()
|
||||||
|
part += consumeMostUpper(scan)
|
||||||
|
}
|
||||||
|
case unicode.IsNumber(r):
|
||||||
|
for unicode.IsNumber(scan.peek()) {
|
||||||
|
part += string(scan.next())
|
||||||
|
}
|
||||||
|
case r == '_':
|
||||||
|
scan.next()
|
||||||
|
if len(out) == 0 {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
default:
|
||||||
|
panic(fmt.Sprintf("unsupported character %q in %q", r, s))
|
||||||
|
}
|
||||||
|
out = append(out, part)
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extract the suffix from a . separated string (ie. namespace).
|
||||||
|
// Useful for getting the package reference from a files namespace.
|
||||||
|
func DotSuffix(pkg string) string {
|
||||||
|
parts := strings.Split(pkg, ".")
|
||||||
|
return parts[len(parts)-1]
|
||||||
|
}
|
67
symbol_test.go
Normal file
67
symbol_test.go
Normal file
@ -0,0 +1,67 @@
|
|||||||
|
package thriftlint
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/alecthomas/go-thrift/parser"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestSplitSymbol(t *testing.T) {
|
||||||
|
actual := SplitSymbol("someSnakeCaseAPI")
|
||||||
|
require.Equal(t, []string{"some", "Snake", "Case", "API"}, actual)
|
||||||
|
actual = SplitSymbol("someSnakeCase")
|
||||||
|
require.Equal(t, []string{"some", "Snake", "Case"}, actual)
|
||||||
|
actual = SplitSymbol("SomeCamelCase")
|
||||||
|
require.Equal(t, []string{"Some", "Camel", "Case"}, actual)
|
||||||
|
actual = SplitSymbol("SomeCamelCaseAPI")
|
||||||
|
require.Equal(t, []string{"Some", "Camel", "Case", "API"}, actual)
|
||||||
|
actual = SplitSymbol("some_underscore_case")
|
||||||
|
require.Equal(t, []string{"some", "underscore", "case"}, actual)
|
||||||
|
actual = SplitSymbol("SOME_UNDERSCORE_CASE")
|
||||||
|
require.Equal(t, []string{"SOME", "UNDERSCORE", "CASE"}, actual)
|
||||||
|
actual = SplitSymbol("ListingDBService")
|
||||||
|
require.Equal(t, []string{"Listing", "DB", "Service"}, actual)
|
||||||
|
actual = SplitSymbol("some1")
|
||||||
|
require.Equal(t, []string{"some1"}, actual)
|
||||||
|
actual = SplitSymbol("_id")
|
||||||
|
require.Equal(t, []string{"", "id"}, actual)
|
||||||
|
actual = SplitSymbol("listingIdSHAs")
|
||||||
|
require.Equal(t, []string{"listing", "Id", "SHA", "s"}, actual)
|
||||||
|
actual = SplitSymbol("listingIdSHAsToAdd")
|
||||||
|
require.Equal(t, []string{"listing", "Id", "SHA", "s", "To", "Add"}, actual)
|
||||||
|
actual = SplitSymbol("APIv3ProtocolTestService")
|
||||||
|
require.Equal(t, []string{"API", "v3", "Protocol", "Test", "Service"}, actual)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestUpperCamelCase(t *testing.T) {
|
||||||
|
actual := UpperCamelCase("listingIdSHAs")
|
||||||
|
require.Equal(t, "ListingIDSHAs", actual)
|
||||||
|
actual = UpperCamelCase("listingIdSHAsToAdd")
|
||||||
|
require.Equal(t, "ListingIDSHAsToAdd", actual)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestLowerCamelCase(t *testing.T) {
|
||||||
|
actual := LowerCamelCase("listingIdSHAs")
|
||||||
|
require.Equal(t, "listingIDSHAs", actual)
|
||||||
|
actual = LowerCamelCase("listingIdSHAsToAdd")
|
||||||
|
require.Equal(t, "listingIDSHAsToAdd", actual)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestComment(t *testing.T) {
|
||||||
|
enum := &parser.Enum{
|
||||||
|
Comment: strings.Repeat("hello ", 30),
|
||||||
|
}
|
||||||
|
actual := Comment(enum)
|
||||||
|
expected := []string{
|
||||||
|
"hello hello hello hello hello hello hello hello hello hello hello hello hello",
|
||||||
|
"hello hello hello hello hello hello hello hello hello hello hello hello hello",
|
||||||
|
"hello hello hello hello",
|
||||||
|
}
|
||||||
|
require.Equal(t, expected, actual)
|
||||||
|
enum = &parser.Enum{}
|
||||||
|
actual = Comment(enum)
|
||||||
|
require.Nil(t, actual)
|
||||||
|
}
|
35
types.go
Normal file
35
types.go
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
package thriftlint
|
||||||
|
|
||||||
|
import (
|
||||||
|
"reflect"
|
||||||
|
|
||||||
|
"github.com/alecthomas/go-thrift/parser"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Types and their supported annotations.
|
||||||
|
var (
|
||||||
|
TypeType = reflect.TypeOf(parser.Type{})
|
||||||
|
|
||||||
|
ThriftType = reflect.TypeOf(parser.Thrift{})
|
||||||
|
|
||||||
|
ServiceType = reflect.TypeOf(parser.Service{})
|
||||||
|
MethodType = reflect.TypeOf(parser.Method{})
|
||||||
|
|
||||||
|
EnumType = reflect.TypeOf(parser.Enum{})
|
||||||
|
EnumValueType = reflect.TypeOf(parser.EnumValue{})
|
||||||
|
|
||||||
|
StructType = reflect.TypeOf(parser.Struct{})
|
||||||
|
FieldType = reflect.TypeOf(parser.Field{})
|
||||||
|
|
||||||
|
ConstantType = reflect.TypeOf(parser.Constant{})
|
||||||
|
TypedefType = reflect.TypeOf(parser.Typedef{})
|
||||||
|
)
|
||||||
|
|
||||||
|
// Attempt to extra positional information from a struct.
|
||||||
|
func Pos(v interface{}) parser.Pos {
|
||||||
|
rv := reflect.Indirect(reflect.ValueOf(v))
|
||||||
|
if f := rv.FieldByName("Pos"); f.IsValid() {
|
||||||
|
return f.Interface().(parser.Pos)
|
||||||
|
}
|
||||||
|
return parser.Pos{}
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user