Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
87 changes: 59 additions & 28 deletions internal/addon/rules/fieldNumbersOrderAscendingRule.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (

"github.com/yoheimuta/go-protoparser/v4/parser"
"github.com/yoheimuta/go-protoparser/v4/parser/meta"
"github.com/yoheimuta/protolint/linter/disablerule"
"github.com/yoheimuta/protolint/linter/report"
"github.com/yoheimuta/protolint/linter/rule"
"github.com/yoheimuta/protolint/linter/visitor"
Expand Down Expand Up @@ -41,12 +42,14 @@ func (r FieldNumbersOrderAscendingRule) IsOfficial() bool {
func (r FieldNumbersOrderAscendingRule) Apply(proto *parser.Proto) ([]report.Failure, error) {
v := &fieldNumbersOrderAscendingVisitor{
BaseAddVisitor: visitor.NewBaseAddVisitor(r.ID(), string(r.Severity())),
ruleID: r.ID(),
}
return visitor.RunVisitor(v, proto, r.ID())
}

type fieldNumbersOrderAscendingVisitor struct {
*visitor.BaseAddVisitor
ruleID string
}

// VisitMessage checks the message
Expand All @@ -57,35 +60,48 @@ func (v *fieldNumbersOrderAscendingVisitor) VisitMessage(message *parser.Message
hasError bool
)

// This rule walks the message body itself instead of relying on VisitField, so the
// disable comments attached to a single field never reach the visitor wrapper and have
// to be interpreted here. A disabled field keeps taking part in the ordering chain;
// only the report about it is suppressed.
// See https://github.com/yoheimuta/protolint/issues/558
interpreter := disablerule.NewInterpreter(v.ruleID)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Share disable state with the visitor wrapper

When a persistent disable is already active upon entering a message or enum, extendedDisableRuleVisitor skips this method, so this newly created interpreter never sees an enable attached to a field and checks cannot resume for later fields in that body. Conversely, if this local interpreter encounters a disable after an earlier ordering failure, return !hasError prevents the wrapper from traversing the children, so the disable is not carried to later declarations. Both cases violate the documented disable-until-enable-or-EOF behavior; the ordering logic needs to participate in the wrapper's single stateful traversal rather than maintaining an independent interpreter.

Useful? React with 👍 / 👎.


for _, element := range message.MessageBody {
field, ok := element.(*parser.Field)
if !ok {
continue
}

disabled := interpreter.Interpret(field.Comments, field.InlineComment)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Process directives on every body element

Because Interpret is called only after the element has been asserted to *parser.Field, persistent directives attached to an option, reserved declaration, nested declaration, or standalone comment are ignored while this method evaluates the fields. For example, option deprecated = true; // protolint:disable FIELD_NUMBERS_ORDER_ASCENDING followed by descending fields still produces a failure; the wrapper visits the option only after VisitMessage has already performed the ordering check. The enum loop has the same problem, so directives from every body element need to be consumed in source order.

Useful? React with 👍 / 👎.


number, err := strconv.Atoi(field.FieldNumber)
if err != nil {
v.AddFailuref(
field.Meta.Pos,
"field number '%s' is not a number",
field.FieldNumber,
)

hasError = true
if !disabled {
v.AddFailuref(
field.Meta.Pos,
"field number '%s' is not a number",
field.FieldNumber,
)

hasError = true
}
continue
}

if number <= 0 {
v.AddFailuref(
field.Meta.Pos,
"field number should be positive integer",
)

hasError = true
if !disabled {
v.AddFailuref(
field.Meta.Pos,
"field number should be positive integer",
)

hasError = true
}
continue
}

number, isError := v.isAscending(field.Meta.Pos, field.FieldName, number, lastName, lastNumber, false)
number, isError := v.isAscending(field.Meta.Pos, field.FieldName, number, lastName, lastNumber, false, disabled)
if isError {
hasError = true
}
Expand Down Expand Up @@ -118,35 +134,43 @@ func (v *fieldNumbersOrderAscendingVisitor) VisitEnum(enum *parser.Enum) bool {
}
}

interpreter := disablerule.NewInterpreter(v.ruleID)

for _, element := range enum.EnumBody {
field, ok := element.(*parser.EnumField)
if !ok {
continue
}

disabled := interpreter.Interpret(field.Comments, field.InlineComment)

number, err := strconv.Atoi(field.Number)
if err != nil {
v.AddFailuref(
field.Meta.Pos,
"field number '%s' is not a number",
field.Number,
)

hasError = true
if !disabled {
v.AddFailuref(
field.Meta.Pos,
"field number '%s' is not a number",
field.Number,
)

hasError = true
}
continue
}

if number < 0 {
v.AddFailuref(
field.Meta.Pos,
"field number should be positive integer",
)

hasError = true
if !disabled {
v.AddFailuref(
field.Meta.Pos,
"field number should be positive integer",
)

hasError = true
}
continue
}

number, isError := v.isAscending(field.Meta.Pos, field.Ident, number, lastIdent, lastNumber, allowAlias)
number, isError := v.isAscending(field.Meta.Pos, field.Ident, number, lastIdent, lastNumber, allowAlias, disabled)
if isError {
hasError = true
}
Expand All @@ -160,11 +184,15 @@ func (v *fieldNumbersOrderAscendingVisitor) VisitEnum(enum *parser.Enum) bool {

func (v *fieldNumbersOrderAscendingVisitor) isAscending(
pos meta.Position, fieldName string, number int, lastName string, lastNumber int, allowEqual bool,
disabled bool,
) (curNumber int, hasError bool) {
if number == lastNumber {
if allowEqual {
return number, false
}
if disabled {
return number, false
}
v.AddFailuref(
pos,
"fields %s and %s have the same number %d",
Expand All @@ -175,6 +203,9 @@ func (v *fieldNumbersOrderAscendingVisitor) isAscending(
}

if number < lastNumber {
if disabled {
return number, false
}
v.AddFailuref(
pos,
"field %s should be after %s (ascending order expected)",
Expand Down
148 changes: 148 additions & 0 deletions internal/addon/rules/fieldNumbersOrderAscendingRule_disable_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
package rules_test

import (
"reflect"
"testing"

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

// TestFieldNumbersOrderAscendingRule_Apply_disableComments checks that the per-line
// disable comments are honoured by this rule.
// See https://github.com/yoheimuta/protolint/issues/558
func TestFieldNumbersOrderAscendingRule_Apply_disableComments(t *testing.T) {
disableThis := func() *parser.Comment {
return &parser.Comment{Raw: "// protolint:disable:this FIELD_NUMBERS_ORDER_ASCENDING"}
}

tests := []struct {
name string
inputProto *parser.Proto
wantFailures []report.Failure
}{
{
name: "no failures for out-of-order fields disabled by an inline comment",
inputProto: &parser.Proto{
ProtoBody: []parser.Visitee{
&parser.Message{
MessageBody: []parser.Visitee{
&parser.Field{
FieldName: "a",
FieldNumber: "2",
InlineComment: disableThis(),
},
&parser.Field{
FieldName: "b",
FieldNumber: "1",
InlineComment: disableThis(),
},
},
},
},
},
},
{
name: "no failures for out-of-order enum fields disabled by an inline comment",
inputProto: &parser.Proto{
ProtoBody: []parser.Visitee{
&parser.Enum{
EnumBody: []parser.Visitee{
&parser.EnumField{
Ident: "A",
Number: "2",
InlineComment: disableThis(),
},
&parser.EnumField{
Ident: "B",
Number: "1",
InlineComment: disableThis(),
},
},
},
},
},
},
{
name: "a field following disabled ones is still checked against them",
inputProto: &parser.Proto{
ProtoBody: []parser.Visitee{
&parser.Message{
MessageBody: []parser.Visitee{
&parser.Field{
FieldName: "a",
FieldNumber: "2",
InlineComment: disableThis(),
},
&parser.Field{
FieldName: "b",
FieldNumber: "1",
InlineComment: disableThis(),
},
&parser.Field{
FieldName: "c",
FieldNumber: "1",
},
},
},
},
},
wantFailures: []report.Failure{
report.Failuref(
meta.Position{},
"FIELD_NUMBERS_ORDER_ASCENDING",
string(rule.SeverityError),
"fields %s and %s have the same number %d",
"b", "c", 1,
),
},
},
{
name: "a field without the comment is still checked",
inputProto: &parser.Proto{
ProtoBody: []parser.Visitee{
&parser.Message{
MessageBody: []parser.Visitee{
&parser.Field{
FieldName: "a",
FieldNumber: "2",
},
&parser.Field{
FieldName: "b",
FieldNumber: "1",
},
},
},
},
},
wantFailures: []report.Failure{
report.Failuref(
meta.Position{},
"FIELD_NUMBERS_ORDER_ASCENDING",
string(rule.SeverityError),
"field %s should be after %s (ascending order expected)",
"a", "b",
),
},
},
}

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

got, err := r.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)
}
})
}
}