Skip to content

Commit

Permalink
Showing 6 changed files with 270 additions and 0 deletions.
10 changes: 10 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -152,6 +152,7 @@ Some rules support a feature that automatically fixed the problems.

| Official | ID | Purpose |
|----------|-----------------------------------|--------------------------------------------------------------------------|
| Yes | ENUM_FIELD_NAMES_PREFIX | Verifies that enum field names are prefixed with its ENUM_NAME_UPPER_SNAKE_CASE. |
| Yes | ENUM_FIELD_NAMES_UPPER_SNAKE_CASE | Verifies that all enum field names are CAPITALS_WITH_UNDERSCORES. |
| Yes | ENUM_FIELD_NAMES_ZERO_VALUE_END_WITH | Verifies that the zero value enum should have the suffix (e.g. "UNSPECIFIED", "INVALID"). The default is "UNSPECIFIED". You can configure the specific suffix with `.protolint.yaml`. |
| Yes | ENUM_NAMES_UPPER_CAMEL_CASE | Verifies that all enum names are CamelCase (with an initial capital). |
@@ -184,6 +185,15 @@ I recommend that you add `all_default: true` in `.protolint.yaml`, because all l
Here are some examples that show good style enabled by default.
`-` is a bad style, `+` is a good style:

__ENUM_FIELD_NAMES_PREFIX__

```diff
enum FooBar {
- UNSPECIFIED = 0;
+ FOO_BAR_UNSPECIFIED = 0;
}
```

__ENUM_FIELD_NAMES_UPPER_SNAKE_CASE__

```diff
67 changes: 67 additions & 0 deletions internal/addon/rules/enumFieldNamesPrefixRule.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
package rules

import (
"strings"

"github.com/yoheimuta/protolint/linter/strs"

"github.com/yoheimuta/go-protoparser/v4/parser"
"github.com/yoheimuta/protolint/linter/report"
"github.com/yoheimuta/protolint/linter/visitor"
)

// EnumFieldNamesPrefixRule verifies that enum field names are prefixed with its ENUM_NAME_UPPER_SNAKE_CASE.
// See https://developers.google.com/protocol-buffers/docs/style#enums.
type EnumFieldNamesPrefixRule struct {
}

// NewEnumFieldNamesPrefixRule creates a new EnumFieldNamesPrefixRule.
func NewEnumFieldNamesPrefixRule() EnumFieldNamesPrefixRule {
return EnumFieldNamesPrefixRule{}
}

// ID returns the ID of this rule.
func (r EnumFieldNamesPrefixRule) ID() string {
return "ENUM_FIELD_NAMES_PREFIX"
}

// Purpose returns the purpose of this rule.
func (r EnumFieldNamesPrefixRule) Purpose() string {
return `Verifies that enum field names are prefixed with its ENUM_NAME_UPPER_SNAKE_CASE.`
}

// IsOfficial decides whether or not this rule belongs to the official guide.
func (r EnumFieldNamesPrefixRule) IsOfficial() bool {
return true
}

// Apply applies the rule to the proto.
func (r EnumFieldNamesPrefixRule) Apply(proto *parser.Proto) ([]report.Failure, error) {
v := &enumFieldNamesPrefixVisitor{
BaseAddVisitor: visitor.NewBaseAddVisitor(r.ID()),
}
return visitor.RunVisitor(v, proto, r.ID())
}

type enumFieldNamesPrefixVisitor struct {
*visitor.BaseAddVisitor
enumName string
}

// VisitEnum checks the enum.
func (v *enumFieldNamesPrefixVisitor) VisitEnum(enum *parser.Enum) bool {
v.enumName = enum.EnumName
return true
}

// VisitEnumField checks the enum field.
func (v *enumFieldNamesPrefixVisitor) VisitEnumField(field *parser.EnumField) bool {
expectedPrefix, err := strs.ToUpperSnakeCaseFromCamelCase(v.enumName)
if err != nil {
expectedPrefix = strings.ToUpper(v.enumName)
}
if !strings.HasPrefix(field.Ident, expectedPrefix) {
v.AddFailuref(field.Meta.Pos, "EnumField name %q should have the prefix %q", field.Ident, expectedPrefix)
}
return false
}
125 changes: 125 additions & 0 deletions internal/addon/rules/enumFieldNamesPrefixRule_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
package rules_test

import (
"reflect"
"testing"

"github.com/yoheimuta/protolint/internal/addon/rules"

"github.com/yoheimuta/go-protoparser/v4/parser"
"github.com/yoheimuta/go-protoparser/v4/parser/meta"
"github.com/yoheimuta/protolint/linter/report"
)

func TestEnumFieldNamesPrefixRule_Apply(t *testing.T) {
tests := []struct {
name string
inputProto *parser.Proto
wantFailures []report.Failure
}{
{
name: "no failures for proto without enum fields",
inputProto: &parser.Proto{
ProtoBody: []parser.Visitee{
&parser.Enum{
EnumName: "FooBar",
},
},
},
},
{
name: "no failures for proto with valid enum field names",
inputProto: &parser.Proto{
ProtoBody: []parser.Visitee{
&parser.Service{},
&parser.Enum{
EnumName: "FooBar",
EnumBody: []parser.Visitee{
&parser.EnumField{
Ident: "FOO_BAR_UNSPECIFIED",
Number: "0",
},
&parser.EnumField{
Ident: "FOO_BAR_FIRST_VALUE",
Number: "1",
},
&parser.EnumField{
Ident: "FOO_BAR_SECOND_VALUE",
Number: "2",
},
},
},
},
},
},
{
name: "no failures for proto with valid enum field names even when its enum name is snake case",
inputProto: &parser.Proto{
ProtoBody: []parser.Visitee{
&parser.Service{},
&parser.Enum{
EnumName: "foo_bar",
EnumBody: []parser.Visitee{
&parser.EnumField{
Ident: "FOO_BAR_UNSPECIFIED",
Number: "0",
},
},
},
},
},
},
{
name: "failures for proto with invalid enum field names",
inputProto: &parser.Proto{
ProtoBody: []parser.Visitee{
&parser.Enum{
EnumName: "FooBar",
EnumBody: []parser.Visitee{
&parser.EnumField{
Ident: "BAR_UNSPECIFIED",
Number: "0",
Meta: meta.Meta{
Pos: meta.Position{
Filename: "example.proto",
Offset: 100,
Line: 5,
Column: 10,
},
},
},
},
},
},
},
wantFailures: []report.Failure{
report.Failuref(
meta.Position{
Filename: "example.proto",
Offset: 100,
Line: 5,
Column: 10,
},
"ENUM_FIELD_NAMES_PREFIX",
`EnumField name "BAR_UNSPECIFIED" should have the prefix "FOO_BAR"`,
),
},
},
}

for _, test := range tests {
test := test
t.Run(test.name, func(t *testing.T) {
rule := rules.NewEnumFieldNamesPrefixRule()

got, err := rule.Apply(test.inputProto)
if err != nil {
t.Errorf("got err %v, but want nil", err)
return
}
if !reflect.DeepEqual(got, test.wantFailures) {
t.Errorf("got %v, but want %v", got, test.wantFailures)
}
})
}
}
1 change: 1 addition & 0 deletions internal/cmd/subcmds/rules.go
Original file line number Diff line number Diff line change
@@ -72,6 +72,7 @@ func newAllInternalRules(
fixMode,
),

rules.NewEnumFieldNamesPrefixRule(),
rules.NewEnumFieldNamesUpperSnakeCaseRule(),
rules.NewEnumFieldNamesZeroValueEndWithRule(
enumFieldNamesZeroValueEndWith.Suffix,
12 changes: 12 additions & 0 deletions linter/strs/strs.go
Original file line number Diff line number Diff line change
@@ -2,6 +2,7 @@
package strs

import (
"fmt"
"strings"
"unicode/utf8"
)
@@ -91,6 +92,17 @@ func HasAnyUpperCase(s string) bool {
return false
}

// ToUpperSnakeCaseFromCamelCase converts s to UPPER_SNAKE_CASE from camelCase/CamelCase.
func ToUpperSnakeCaseFromCamelCase(s string) (string, error) {
ws := SplitCamelCaseWord(s)
if ws == nil {
return "", fmt.Errorf("s `%s` should be camelCase", s)
}
return strings.ToUpper(
strings.Join(ws, "_"),
), nil
}

// toSnake converts s to snake_case.
func toSnake(s string) string {
output := ""
55 changes: 55 additions & 0 deletions linter/strs/strs_test.go
Original file line number Diff line number Diff line change
@@ -185,6 +185,61 @@ func TestSplitCamelCaseWord(t *testing.T) {
}
}

func TestToUpperSnakeCaseFromCamelCase(t *testing.T) {
tests := []struct {
name string
input string
want string
wantExistErr bool
}{
{
name: "if s is empty, returns an error",
wantExistErr: true,
},
{
name: "if s is not camel_case, returns an error",
input: "not_camel",
wantExistErr: true,
},
{
name: "input consists of one word",
input: "Account",
want: "ACCOUNT",
},
{
name: "input consists of words with an initial capital",
input: "AccountStatus",
want: "ACCOUNT_STATUS",
},
{
name: "input consists of words without an initial capital",
input: "accountStatus",
want: "ACCOUNT_STATUS",
},
}

for _, test := range tests {
test := test
t.Run(test.name, func(t *testing.T) {
got, err := strs.ToUpperSnakeCaseFromCamelCase(test.input)
if test.wantExistErr {
if err == nil {
t.Errorf("got err nil, but want err")
}
return
}
if err != nil {
t.Errorf("got err %v, but want nil", err)
return
}

if !reflect.DeepEqual(got, test.want) {
t.Errorf("got %v, but want %v", got, test.want)
}
})
}
}

func TestSplitSnakeCaseWord(t *testing.T) {
tests := []struct {
name string

0 comments on commit d193089

Please sign in to comment.