Skip to content
Draft
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
2 changes: 2 additions & 0 deletions build/BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ go_yacc(
go_library(
name = "build",
srcs = [
"comments.go",
"lex.go",
"parse.y.baz.go", # keep
"print.go",
Expand All @@ -33,6 +34,7 @@ go_test(
size = "small",
srcs = [
"checkfile_test.go",
"comments_test.go",
"lex_test.go",
"parse_test.go",
"print_test.go",
Expand Down
46 changes: 46 additions & 0 deletions build/comments.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
/*
Copyright 2026 Google LLC

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

https://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

package build

import (
"slices"
"strings"
)

// checkSelfOrInheritedComment checks comments relate to the expression, and returns true if
// the provided predicate matches any of the comments.
func checkSelfOrInheritedComment(expr Expr, predicate func(Comment) bool) bool {
for _, comment := range slices.Concat(
expr.Comment().Before,
expr.Comment().After,
expr.Comment().Suffix,
expr.Comment().Inherited) {
if predicate(comment) {
return true
}
}
return false
}

// HasCommentContaining does a case insensitive matching to see if an expression,
// or its parent expressions have a comment containing the provided prefix.
func HasCommentContaining(expr Expr, prefix string) bool {
return checkSelfOrInheritedComment(expr, func(comment Comment) bool {
trimmedComment := strings.Trim(comment.Token, " \t\n#")
return strings.Contains(strings.ToLower(trimmedComment), strings.ToLower(prefix))
})
}
193 changes: 193 additions & 0 deletions build/comments_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,193 @@
/*
Copyright 2026 Google LLC

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

https://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

package build

import (
"testing"
)

func callByName(name string) func(f *File) Expr {
return func(f *File) Expr {
var expr Expr
WalkInterruptable(f, func(x Expr, stk []Expr) error {
if call, ok := x.(*CallExpr); ok {
if ident, ok := call.X.(*Ident); ok && ident.Name == name {
expr = call
return &StopTraversalError{}
}
}
return nil
})
return expr
}
}

func assignExprByLHSName(name string) func(f *File) Expr {
return func(f *File) Expr {
var expr Expr
WalkInterruptable(f, func(x Expr, stk []Expr) error {
if assign, ok := x.(*AssignExpr); ok {
if ident, ok := assign.LHS.(*Ident); ok && ident.Name == name {
expr = assign
return &StopTraversalError{}
}
}
return nil
})
return expr
}
}

func TestHasCommentContaining(t *testing.T) {
var tests = []struct {
name string
buildFile string
selector func(*File) Expr
comment string
want bool
}{
{
name: "rule_call_with_comment",
buildFile: `
# has-comment
my_rule(
name = "my_target",
)`,
comment: "has-comment",
selector: callByName("my_rule"),
want: true,
},
{
name: "rule_call_with_trailing_comment",
buildFile: `
my_rule(
name = "my_target",
) # has-comment
`,
comment: "has-comment",
selector: callByName("my_rule"),
want: true,
},
{
name: "rule_call_with_inherited_comment",
buildFile: `
# has-comment
my_rule(
name = "my_target",
attr = my_func()
)
`,
comment: "has-comment",
selector: callByName("my_func"),
want: true,
},
{
name: "function_call_with_comment",
buildFile: `
my_rule(
name = "my_target",
# has-comment
attr = my_func()
)
`,
comment: "has-comment",
selector: callByName("my_func"),
want: true,
},
{
name: "function_call_with_trailing_comment",
buildFile: `
my_rule(
name = "my_target",
attr = my_func() # has-comment
)
`,
comment: "has-comment",
selector: callByName("my_func"),
want: true,
},
{
name: "rule_call_without_comment",
buildFile: `
my_rule(
name = "my_target",
)`,
comment: "has-comment",
selector: callByName("my_rule"),
want: false,
},
{
name: "sibling_call_has_comment",
buildFile: `
my_rule(
name = "my_target",
attr = my_func(),
attr2 = my_other_func() # has-comment
)`,
comment: "has-comment",
selector: callByName("my_func"),
want: false,
},
{
name: "assign_expr_with_trailing_comment",
buildFile: `
my_var = 1 # has-comment
`,
comment: "has-comment",
selector: assignExprByLHSName("my_var"),
want: true,
},
{
name: "assign_expr_with_inherited_comment",
buildFile: `
# has-comment
my_rule(
name = "my_target",
attr = my_func(func_arg = 1)
)
`,
comment: "has-comment",
selector: assignExprByLHSName("func_arg"),
want: true,
},
{
name: "assign_expr_without_comment",
buildFile: `
my_var = 1
`,
comment: "has-comment",
selector: assignExprByLHSName("my_var"),
want: false,
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
bld, err := Parse("BUILD", []byte(tc.buildFile))
if err != nil {
t.Error(err)
}
expr := tc.selector(bld)
if expr == nil {
t.Error("selector returned nil")
}
got := HasCommentContaining(expr, tc.comment)
if got != tc.want {
t.Errorf("HasCommentContaining(%q) = %v, want %v", tc.comment, got, tc.want)
}
})
}
}
20 changes: 20 additions & 0 deletions build/lex.go
Original file line number Diff line number Diff line change
Expand Up @@ -843,6 +843,7 @@ func (in *input) assignComments() {
in.order(in.file)
in.assignSuffixComments()
in.assignLineComments()
in.assignInheritedComments()
}

func (in *input) assignSuffixComments() {
Expand Down Expand Up @@ -903,3 +904,22 @@ func reverseComments(list []Comment) {
list[i], list[j] = list[j], list[i]
}
}

// assignInheritedComments assigns comments to the Inherited field of each expression.
func (in *input) assignInheritedComments() {
Walk(in.file, func(node Expr, stack []Expr) {
cs := node.Comment()
for _, stackExpr := range stack {
cs.Inherited = append(cs.Inherited, stackExpr.Comment().Before...)
cs.Inherited = append(cs.Inherited, stackExpr.Comment().After...)

eStart, eEnd := node.Span()
for _, suffixComment := range stackExpr.Comment().Suffix {
if suffixComment.Start.Line <= eStart.Line || suffixComment.Start.Line <= eEnd.Line {
// Suffix comments are inherited only for expressions which overlap with the comment line number.
cs.Inherited = append(cs.Inherited, suffixComment)
}
Comment on lines +918 to +921
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

medium

The condition suffixComment.Start.Line <= eStart.Line || suffixComment.Start.Line <= eEnd.Line is redundant and can be simplified to suffixComment.Start.Line <= eEnd.Line.

However, the existing comment on line 919 states that suffix comments are inherited for expressions that overlap with the comment line number. An overlap check would be eStart.Line <= suffixComment.Start.Line && suffixComment.Start.Line <= eEnd.Line.

This creates a discrepancy between the code's logic and its documentation. Please clarify the intended behavior. If the 'overlap' behavior (as described in the existing comment) was the intentional previous behavior, the code should be adjusted to implement that. If the suffixComment.Start.Line <= eEnd.Line behavior is the intended one, then the code can be simplified as noted, and the comment on line 919 should be updated to reflect this logic.

Please ensure the chosen behavior is consistent with previous intentional behavior.

References
  1. When modifying code, ensure that changes are consistent with previous behavior, especially if the previous behavior was intentional.

}
}
})
}
74 changes: 74 additions & 0 deletions build/parse_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -250,4 +250,78 @@ var parseTests = []struct {
},
},
},
{
in: `go_binary(name = "x") # comment`,
out: &File{
Path: "BUILD",
Type: TypeBuild,
Stmt: []Expr{
&CallExpr{
X: &Ident{
NamePos: Position{1, 1, 0},
Name: "go_binary",
Comments: Comments{
Inherited: []Comment{
{
Token: "# comment",
Start: Position{1, 23, 22},
},
},
},
},
ListStart: Position{1, 10, 9},
List: []Expr{
&AssignExpr{
LHS: &Ident{
NamePos: Position{1, 11, 10},
Name: "name",
Comments: Comments{
Inherited: []Comment{
{
Token: "# comment",
Start: Position{1, 23, 22},
},
},
},
},
OpPos: Position{1, 16, 15},
Op: "=",
RHS: &StringExpr{
Start: Position{1, 18, 17},
Value: "x",
End: Position{1, 21, 20},
Token: `"x"`,
Comments: Comments{
Inherited: []Comment{
{
Token: "# comment",
Start: Position{1, 23, 22},
},
},
},
},
Comments: Comments{
Inherited: []Comment{
{
Token: "# comment",
Start: Position{1, 23, 22},
},
},
},
},
},
End: End{Pos: Position{1, 21, 20}},
ForceMultiLine: false,
Comments: Comments{
Suffix: []Comment{
{
Token: "# comment",
Start: Position{1, 23, 22},
},
},
},
},
},
},
},
}
Loading