From a50563c311f4e3eadbd65272e6272ebd628fe02f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tim=20Malmstr=C3=B6m?= Date: Thu, 25 Sep 2025 17:51:13 +0200 Subject: [PATCH] Rewriting macro analyzer - Including found call stack as motivation for decision (macro or not) - Allowing traversal of cached files before loading non cached files - Rewording to indicate whether function or call produces targets or not (by definition rule or macro) - Cleans up API (separating external and internal structures and state) --- WARNINGS.md | 8 - warn/BUILD.bazel | 3 + warn/docs/warnings.textproto | 8 +- warn/macro_analyzer.go | 302 +++++++++++++++++++++++++++++++++++ warn/macro_analyzer_test.go | 280 ++++++++++++++++++++++++++++++++ warn/multifile.go | 19 ++- warn/warn_bazel.go | 18 ++- warn/warn_bazel_test.go | 65 ++++++-- warn/warn_macro.go | 291 ++------------------------------- warn/warn_macro_test.go | 220 ++++++++++++++----------- 10 files changed, 809 insertions(+), 405 deletions(-) create mode 100644 warn/macro_analyzer.go create mode 100644 warn/macro_analyzer_test.go diff --git a/WARNINGS.md b/WARNINGS.md index 79b75315a..690237cb1 100644 --- a/WARNINGS.md +++ b/WARNINGS.md @@ -1216,14 +1216,6 @@ to [Symbolic Macros](https://bazel.build/extending/macros). + my_macro(name = "foo", env = "bar") ``` -The linter allows the following functions to be called with positional arguments: - - * `load()` - * `vardef()` - * `export_files()` - * `licenses()` - * `print()` - -------------------------------------------------------------------------------- ## `print()` is a debug function and shouldn't be submitted diff --git a/warn/BUILD.bazel b/warn/BUILD.bazel index 225b5fcfa..6408244e7 100644 --- a/warn/BUILD.bazel +++ b/warn/BUILD.bazel @@ -3,6 +3,7 @@ load("@io_bazel_rules_go//go:def.bzl", "go_library", "go_test") go_library( name = "warn", srcs = [ + "macro_analyzer.go", "multifile.go", "types.go", "warn.go", @@ -34,6 +35,7 @@ go_test( name = "warn_test", size = "small", srcs = [ + "macro_analyzer_test.go", "types_test.go", "warn_bazel_api_test.go", "warn_bazel_operation_test.go", @@ -53,6 +55,7 @@ go_test( "//build", "//tables", "//testutils", + "@com_github_google_go_cmp//cmp", ], ) diff --git a/warn/docs/warnings.textproto b/warn/docs/warnings.textproto index 9d3eb8e3b..16c390af1 100644 --- a/warn/docs/warnings.textproto +++ b/warn/docs/warnings.textproto @@ -884,13 +884,7 @@ warnings: { "```diff\n" "- my_macro(\"foo\", \"bar\")\n" "+ my_macro(name = \"foo\", env = \"bar\")\n" - "```\n\n" - "The linter allows the following functions to be called with positional arguments:\n\n" - " * `load()`\n" - " * `vardef()`\n" - " * `export_files()`\n" - " * `licenses()`\n" - " * `print()`" + "```" } warnings: { diff --git a/warn/macro_analyzer.go b/warn/macro_analyzer.go new file mode 100644 index 000000000..e95d07b22 --- /dev/null +++ b/warn/macro_analyzer.go @@ -0,0 +1,302 @@ +/* +Copyright 2025 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. +*/ + +// Analyzer to determine if a function is a macro (produces targets). + +package warn + +import ( + "fmt" + "strings" + + "github.com/bazelbuild/buildtools/build" + "github.com/bazelbuild/buildtools/labels" +) + +// MacroAnalyzer is an object that analyzes the directed graph of functions calling each other, +// determining whether a function produces targets or not. +type MacroAnalyzer struct { + fileReader *FileReader + cache map[symbolRef]*symbolAnalysisResult +} + +// NewMacroAnalyzer creates and initiates an instance of macroAnalyzer. +func NewMacroAnalyzer(fileReader *FileReader) MacroAnalyzer { + if fileReader == nil { + // If no file reader is provided, a default one is provided which fails on all reads. + // This can still be used if functions are preloaded via cache. + fileReader = NewFileReader(func(_ string) ([]byte, error) { + return nil, fmt.Errorf("tried to read file without file reader") + }) + } + return MacroAnalyzer{ + fileReader: fileReader, + cache: make(map[symbolRef]*symbolAnalysisResult), + } +} + +// MacroAnalyzerReport defines the results of analyzing a function using the MacroAnalyzer. +type MacroAnalyzerReport struct { + SelfDescription string + symbolAnalysis *symbolAnalysisResult +} + +// CanProduceTargets returns true if provided function has any call path which produces a target. +// A function which produces targets is by definition either a rule or a macro. +func (mar *MacroAnalyzerReport) CanProduceTargets() bool { + if mar.symbolAnalysis == nil { + return false + } + return mar.symbolAnalysis.canProduceTargets +} + +// PrintableCallStack returns a user-readable call stack, providing a path for how a function may +// produce targets. +func (mar *MacroAnalyzerReport) PrintableCallStack() string { + if mar.symbolAnalysis == nil { + return "" + } + return strings.Join(mar.symbolAnalysis.callStackFrames, "\n") +} + +// AnalyzeFn analyzes the provided def statement, and returns a report containing whether it produces a target (is a macro) or not. +func (ma *MacroAnalyzer) AnalyzeFn(f *build.File, def *build.DefStmt) (*MacroAnalyzerReport, error) { + ma.fileReader.AddFileToCache(f) + call := symbolCall{symbol: &symbolRef{pkg: f.Pkg, label: f.Label, name: def.Name}, line: exprLine(def)} + report, err := ma.analyzeSymbol(call) + if err != nil { + return nil, err + } + return &MacroAnalyzerReport{ + SelfDescription: call.asCallStackFrame(), + symbolAnalysis: report, + }, nil +} + +// AnalyzeFnCall analyzes a function call to see if it can produce a targets or not. +func (ma *MacroAnalyzer) AnalyzeFnCall(f *build.File, call *build.CallExpr) (*MacroAnalyzerReport, error) { + ma.fileReader.AddFileToCache(f) + if symbolName := callExprToString(call); symbolName != "" { + call := symbolCall{symbol: &symbolRef{pkg: f.Pkg, label: f.Label, name: symbolName}, line: exprLine(call)} + report, err := ma.analyzeSymbol(call) + if err != nil { + return nil, err + } + return &MacroAnalyzerReport{ + SelfDescription: call.asCallStackFrame(), + symbolAnalysis: report, + }, nil + } + return nil, fmt.Errorf("error checking call for being a macro at %s:%d", f.Path, exprLine(call)) +} + +// symbolAnalysisResult stores the result of analyzing a symbolRef. +type symbolAnalysisResult struct { + canProduceTargets bool + callStackFrames []string +} + +// symbolRef represents a symbol in a specific file. +type symbolRef struct { + pkg string + label string + name string +} + +// symbolCall represents a call (by line number) to a symbolRef. +type symbolCall struct { + line int + symbol *symbolRef +} + +func (sc *symbolCall) asCallStackFrame() string { + return fmt.Sprintf("%s:%s:%d %s", sc.symbol.pkg, sc.symbol.label, sc.line, sc.symbol.name) +} + +// traversalNode is an internal structure to keep track of symbol call hierarchies while traversing symbols. +type traversalNode struct { + parent *traversalNode + symbolCall *symbolCall +} + +// analyzeSymbol identifies a given symbol, and traverses its call stack to detect if any downstream calls can generate targets. +func (ma *MacroAnalyzer) analyzeSymbol(sc symbolCall) (*symbolAnalysisResult, error) { + queue := []*traversalNode{{symbolCall: &sc}} + visited := make(map[symbolRef]bool) + + var current *traversalNode + var nodeProducedTarget *traversalNode + + for len(queue) > 0 && nodeProducedTarget == nil { + current, queue = queue[0], queue[1:] + visited[*current.symbolCall.symbol] = true + + if producesTarget(current.symbolCall.symbol) { + nodeProducedTarget = current + } + calls, err := ma.expandSymbol(current.symbolCall.symbol) + if err != nil { + return nil, err + } + for _, call := range calls { + if _, isVisited := visited[*call.symbol]; isVisited { + continue + } + ref := &traversalNode{parent: current, symbolCall: &call} + // adding symbol to front/back of queue depending on whether the file is already loaded or not. + if ma.fileReader.IsCached(call.symbol.pkg, call.symbol.label) { + queue = append([]*traversalNode{ref}, queue...) + } else { + queue = append(queue, ref) + } + } + } + if nodeProducedTarget == nil { + // If no node produced a target, all visited nodes can be cached as non-macros. + for symbol := range visited { + ma.cache[symbol] = &symbolAnalysisResult{canProduceTargets: false} + } + } else { + // If a node produced a target, the call stack above the node can be cached as producing targets. + var callStackFrames []string + node := nodeProducedTarget + for node != nil { + ma.cache[*node.symbolCall.symbol] = &symbolAnalysisResult{canProduceTargets: true, callStackFrames: callStackFrames} + callStackFrames = append([]string{node.symbolCall.asCallStackFrame()}, callStackFrames...) + node = node.parent + } + } + return ma.cache[*sc.symbol], nil +} + +// exprLine returns the start line of an expression +func exprLine(expr build.Expr) int { + start, _ := expr.Span() + return start.Line +} + +// expandSymbol expands the provided symbol, returning a list of other symbols that it references. +// e.g. if the symbol is an alias, the aliased symbol is returned, or if the symbol is a function, the symbols it calls downstream are returned. +func (ma *MacroAnalyzer) expandSymbol(symbol *symbolRef) ([]symbolCall, error) { + f := ma.fileReader.GetFile(symbol.pkg, symbol.label) + if f == nil { + return nil, fmt.Errorf("unable to find file %s:%s", symbol.pkg, symbol.label) + } + + for _, stmt := range f.Stmt { + switch stmt := stmt.(type) { + case *build.AssignExpr: + if lhsIdent, ok := stmt.LHS.(*build.Ident); ok && lhsIdent.Name == symbol.name { + if rhsIdent, ok := stmt.RHS.(*build.Ident); ok { + return []symbolCall{{ + symbol: &symbolRef{pkg: f.Pkg, label: f.Label, name: rhsIdent.Name}, + line: exprLine(stmt), + }}, nil + } + if fnName := callExprToString(stmt.RHS); fnName != "" { + return []symbolCall{{ + symbol: &symbolRef{pkg: f.Pkg, label: f.Label, name: fnName}, + line: exprLine(stmt), + }}, nil + } + } + case *build.DefStmt: + if stmt.Name == symbol.name { + var calls []symbolCall + build.Walk(stmt, func(x build.Expr, _ []build.Expr) { + if fnName := callExprToString(x); fnName != "" { + calls = append(calls, symbolCall{ + symbol: &symbolRef{pkg: f.Pkg, label: f.Label, name: fnName}, + line: exprLine(x), + }) + } + }) + return calls, nil + } + case *build.LoadStmt: + label := labels.ParseRelative(stmt.Module.Value, f.Pkg) + if label.Repository != "" || label.Target == "" { + continue + } + for i, from := range stmt.From { + if stmt.To[i].Name == symbol.name { + return []symbolCall{{ + symbol: &symbolRef{pkg: label.Package, label: label.Target, name: from.Name}, + line: exprLine(stmt), + }}, nil + } + } + } + } + return nil, nil +} + +// callExprToString converts a callExpr to its "symbol name" +func callExprToString(expr build.Expr) string { + call, ok := expr.(*build.CallExpr) + if !ok { + return "" + } + + if fnIdent, ok := call.X.(*build.Ident); ok { + return fnIdent.Name + } + + // call of the format obj.fn(...), ignores call if anything other than ident.fn(). + if fn, ok := call.X.(*build.DotExpr); ok { + if obj, ok := fn.X.(*build.Ident); ok { + return fmt.Sprintf("%s.%s", obj.Name, fn.Name) + } + } + return "" +} + +// native functions which do not produce targets (https://bazel.build/rules/lib/toplevel/native). +var nativeRuleExceptions = map[string]bool{ + "native.existing_rule": true, + "native.existing_rules": true, + "native.exports_files": true, + "native.glob": true, + "native.module_name": true, + "native.module_version": true, + "native.package_default_visibility": true, + "native.package_group": true, + "native.package_name": true, + "native.package_relative_label": true, + "native.repo_name": true, + "native.repository_name": true, + "native.subpackages": true, +} + +// producesTargets returns true if the symbol name is a known generator of a target. +func producesTarget(s *symbolRef) bool { + // Calls to the macro() symbol produce a symbolic macro (https://bazel.build/extending/macros). + if s.name == "macro" { + return true + } + // Calls to the rule() symbol define a rule (https://bazel.build/extending/rules). + if s.name == "rule" { + return true + } + // Calls to native. invokes native rules (except defined list of native helper functions). + // https://bazel.build/rules/lib/toplevel/native + if strings.HasPrefix(s.name, "native.") { + if _, ok := nativeRuleExceptions[s.name]; !ok { + return true + } + } + return false +} diff --git a/warn/macro_analyzer_test.go b/warn/macro_analyzer_test.go new file mode 100644 index 000000000..a6c5314a9 --- /dev/null +++ b/warn/macro_analyzer_test.go @@ -0,0 +1,280 @@ +/* +Copyright 2025 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 warn + +import ( + "fmt" + "testing" + + "github.com/bazelbuild/buildtools/build" + "github.com/google/go-cmp/cmp" +) + +func mustFindDefStatement(f *build.File, name string) *build.DefStmt { + for _, stmt := range f.Stmt { + if def, ok := stmt.(*build.DefStmt); ok { + if def.Name == name { + return def + } + } + } + panic(fmt.Sprintf("unable to find def statement matching %q", name)) +} + +func mustFindCallExpression(f *build.File, name string) *build.CallExpr { + for _, stmt := range f.Stmt { + if call, ok := stmt.(*build.CallExpr); ok { + if fnIdent, ok := call.X.(*build.Ident); ok && fnIdent.Name == name { + return call + } + } + } + panic(fmt.Sprintf("unable to find call expression matching %q", name)) +} + +func fileReaderWithFiles(fileContents map[string]string) *FileReader { + return NewFileReader(func(filename string) ([]byte, error) { + return []byte(fileContents[filename]), nil + }) +} + +func TestAnalyzeFn(t *testing.T) { + defaultFilename := "BUILD" + defaultPackage := "//package/path" + defaultFilepath := fmt.Sprintf("%s/%s", defaultPackage, defaultFilename) + tests := []struct { + name string + fileContents map[string]string + wantCanProduceTargets bool + wantStackTrace string + }{ + { + name: "non_macro", + fileContents: map[string]string{ + defaultFilepath: ` +def other_function(): + pass + +def test_symbol(): + other_function() +`, + }, + wantCanProduceTargets: false, + wantStackTrace: "", + }, + { + name: "with_infinite_recursion", + fileContents: map[string]string{ + defaultFilepath: ` +def first_function(): + test_symbol() + +def test_symbol(): + first_function() +`, + }, + wantCanProduceTargets: false, + wantStackTrace: "", + }, + { + name: "macro_within_single_file", + fileContents: map[string]string{ + defaultFilepath: ` +macro_def = macro() + +def test_symbol(): + macro_def() +`, + }, + wantCanProduceTargets: true, + wantStackTrace: `//package/path:BUILD:5 macro_def +//package/path:BUILD:2 macro`, + }, + { + name: "macro_through_load_statement", + fileContents: map[string]string{ + "package/other_path/file.bzl": ` +imported_rule = rule() +`, + defaultFilepath: ` +load("//package/other_path:file.bzl", "imported_rule") + +def test_symbol(): + imported_rule() +`, + }, + wantCanProduceTargets: true, + wantStackTrace: `//package/path:BUILD:5 imported_rule +package/other_path:file.bzl:2 imported_rule +package/other_path:file.bzl:2 rule`, + }, + { + name: "with_load_statements_prioritizes_local_statements", + fileContents: map[string]string{ + "package/other_path/file.bzl": ` +imported_rule = rule() +`, + defaultFilepath: ` +load("//package/other_path:file.bzl", "imported_rule") + +def test_symbol(): + imported_rule() + native.cc_library() +`, + }, + wantCanProduceTargets: true, + wantStackTrace: `//package/path:BUILD:6 native.cc_library`, + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + ma := NewMacroAnalyzer(fileReaderWithFiles(tc.fileContents)) + f := ma.fileReader.GetFile(defaultPackage, defaultFilename) + macroDef := mustFindDefStatement(f, "test_symbol") + + report, err := ma.AnalyzeFn(f, macroDef) + + if err != nil { + t.Errorf("Got unexpected error %s", err) + } + if diff := cmp.Diff(tc.wantCanProduceTargets, report.CanProduceTargets()); diff != "" { + t.Errorf("AnalyzeFn.CanProduceTargets returned unexpected diff %s", diff) + } + if diff := cmp.Diff(tc.wantStackTrace, report.PrintableCallStack()); diff != "" { + t.Errorf("AnalyzeFn.PrintableCallStack returned unexpected diff %s", diff) + } + }) + } +} + +func TestAnalyzeFnCall(t *testing.T) { + defaultFilename := "BUILD" + defaultPackage := "//package/path" + defaultFilepath := fmt.Sprintf("%s/%s", defaultPackage, defaultFilename) + tests := []struct { + name string + fileContents map[string]string + wantCanProduceTargets bool + wantStackTrace string + }{ + { + name: "non_macro", + fileContents: map[string]string{ + defaultFilepath: ` +def test_symbol(): + pass + +test_symbol() +`, + }, + wantCanProduceTargets: false, + wantStackTrace: "", + }, + { + name: "with_infinite_recursion", + fileContents: map[string]string{ + defaultFilepath: ` +def test_symbol(): + second_function() + +def second_function(): + test_symbol() + +test_symbol() +`, + }, + wantCanProduceTargets: false, + wantStackTrace: "", + }, + { + name: "macro_within_single_file", + fileContents: map[string]string{ + defaultFilepath: ` +macro_def = macro() + +def test_symbol(): + macro_def() + +test_symbol() +`, + }, + wantCanProduceTargets: true, + wantStackTrace: `//package/path:BUILD:5 macro_def +//package/path:BUILD:2 macro`, + }, + { + name: "macro_through_load_statement", + fileContents: map[string]string{ + "package/other_path/file.bzl": ` +imported_rule = rule() +`, + defaultFilepath: ` +load("//package/other_path:file.bzl", "imported_rule") + +def test_symbol(): + imported_rule() + +test_symbol() +`, + }, + wantCanProduceTargets: true, + wantStackTrace: `//package/path:BUILD:5 imported_rule +package/other_path:file.bzl:2 imported_rule +package/other_path:file.bzl:2 rule`, + }, + { + name: "with_load_statements_prioritizes_local_statements", + fileContents: map[string]string{ + "package/other_path/file.bzl": ` +imported_rule = rule() +`, + defaultFilepath: ` +load("//package/other_path:file.bzl", "imported_rule") + +def test_symbol(): + imported_rule() + macro() + +test_symbol() +`, + }, + wantCanProduceTargets: true, + wantStackTrace: `//package/path:BUILD:6 macro`, + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + ma := NewMacroAnalyzer(fileReaderWithFiles(tc.fileContents)) + f := ma.fileReader.GetFile(defaultPackage, defaultFilename) + call := mustFindCallExpression(f, "test_symbol") + + report, err := ma.AnalyzeFnCall(f, call) + + if err != nil { + t.Errorf("Got unexpected error %s", err) + } + if diff := cmp.Diff(tc.wantCanProduceTargets, report.CanProduceTargets()); diff != "" { + t.Errorf("AnalyzeFn.CanProduceTargets returned unexpected diff %s", diff) + } + if diff := cmp.Diff(tc.wantStackTrace, report.PrintableCallStack()); diff != "" { + t.Errorf("AnalyzeFn.PrintableCallStack returned unexpected diff %s", diff) + } + }) + } +} diff --git a/warn/multifile.go b/warn/multifile.go index 73f5af4e5..174da4eaa 100644 --- a/warn/multifile.go +++ b/warn/multifile.go @@ -38,6 +38,23 @@ func NewFileReader(readFile func(string) ([]byte, error)) *FileReader { } } +// AddFileToCache adds the provided file to the filereader cache. +func (fr *FileReader) AddFileToCache(f *build.File) { + if f != nil { + fr.cache[f.Path] = f + } +} + +// IsCached returns true if the file is present in the cache. +func (fr *FileReader) IsCached(pkg, label string) bool { + filename := label + if pkg != "" { + filename = pkg + "/" + label + } + _, contains := fr.cache[filename] + return contains +} + // retrieveFile reads a Starlark file using only the readFile method // (without using the cache). func (fr *FileReader) retrieveFile(filename string) *build.File { @@ -72,6 +89,6 @@ func (fr *FileReader) GetFile(pkg, label string) *build.File { file.Pkg = pkg file.Label = label } - fr.cache[filename] = file + fr.AddFileToCache(file) return file } diff --git a/warn/warn_bazel.go b/warn/warn_bazel.go index be6000cd1..02f6fd8c5 100644 --- a/warn/warn_bazel.go +++ b/warn/warn_bazel.go @@ -175,26 +175,28 @@ func positionalArgumentsWarning(f *build.File, fileReader *FileReader) (findings if f.Type != build.TypeBuild { return nil } - macroAnalyzer := newMacroAnalyzer(fileReader) - macroAnalyzer.files[f.Pkg+":"+f.Label] = analyzeFile(f) + macroAnalyzer := NewMacroAnalyzer(fileReader) for _, expr := range f.Stmt { build.Walk(expr, func(x build.Expr, _ []build.Expr) { if fnCall, ok := x.(*build.CallExpr); ok { - fnIdent, ok := fnCall.X.(*build.Ident) - if !ok { + report, err := macroAnalyzer.AnalyzeFnCall(f, fnCall) + if err != nil { + // TODO: Analysis errors are simply ignored as buildifier does not currently handle errors. return } - - if macroAnalyzer.IsRuleOrMacro(function{pkg: f.Pkg, filename: f.Label, name: fnIdent.Name}).isRuleOrMacro { + if report.CanProduceTargets() { for _, arg := range fnCall.List { if _, ok := arg.(*build.AssignExpr); ok || arg == nil { continue } findings = append(findings, makeLinterFinding(fnCall, fmt.Sprintf( `All calls to rules or macros should pass arguments by keyword (arg_name=value) syntax. -Found call to rule or macro %q with positional arguments.`, - fnIdent.Name))) +Found call to rule or macro %q with positional arguments. +The function was considered a macro as it may produce targets via calls: +%s +%s`, + report.SelfDescription, report.SelfDescription, report.PrintableCallStack()))) return } } diff --git a/warn/warn_bazel_test.go b/warn/warn_bazel_test.go index 9201cea7d..ec089a823 100644 --- a/warn/warn_bazel_test.go +++ b/warn/warn_bazel_test.go @@ -124,14 +124,43 @@ my_rule("foo", "bar") my_function("foo", "bar") `, []string{ - `6: All calls to rules or macros should pass arguments by keyword (arg_name=value) syntax. -Found call to rule or macro "my_macro" with positional arguments.`, - `7: All calls to rules or macros should pass arguments by keyword (arg_name=value) syntax. -Found call to rule or macro "my_rule" with positional arguments.`, + `:6: All calls to rules or macros should pass arguments by keyword (arg_name=value) syntax. +Found call to rule or macro "test/package:BUILD:6 my_macro" with positional arguments. +The function was considered a macro as it may produce targets via calls: +test/package:BUILD:6 my_macro +test/package:BUILD:1 macro`, + `:7: All calls to rules or macros should pass arguments by keyword (arg_name=value) syntax. +Found call to rule or macro "test/package:BUILD:7 my_rule" with positional arguments. +The function was considered a macro as it may produce targets via calls: +test/package:BUILD:7 my_rule +test/package:BUILD:2 rule`, }, scopeBuild) } +func TestPositionalArgumentsWithNestedCalls(t *testing.T) { + checkFindings(t, "positional-args", ` +def macro3(foo, *args, **kwargs): + macro2() + +def macro2(foo, *, name): + macro1() + +def macro1(foo, name, bar): + native.java_library() + +macro3("foo") +`, + []string{`:10: All calls to rules or macros should pass arguments by keyword (arg_name=value) syntax. +Found call to rule or macro "test/package:BUILD:10 macro3" with positional arguments. +The function was considered a macro as it may produce targets via calls: +test/package:BUILD:10 macro3 +test/package:BUILD:2 macro2 +test/package:BUILD:5 macro1 +test/package:BUILD:8 native.java_library`}, + scopeBuild) +} + func TestPositionalArgumentsWarnsWhenCalledInNestedContexts(t *testing.T) { checkFindings(t, "positional-args", ` my_macro = macro() @@ -149,13 +178,25 @@ other_function(foo = my_function(x)) `, []string{ `6: All calls to rules or macros should pass arguments by keyword (arg_name=value) syntax. -Found call to rule or macro "my_macro" with positional arguments.`, - `7: All calls to rules or macros should pass arguments by keyword (arg_name=value) syntax. -Found call to rule or macro "my_rule" with positional arguments.`, - `10: All calls to rules or macros should pass arguments by keyword (arg_name=value) syntax. -Found call to rule or macro "my_macro" with positional arguments.`, - `11: All calls to rules or macros should pass arguments by keyword (arg_name=value) syntax. -Found call to rule or macro "my_rule" with positional arguments.`, +Found call to rule or macro "test/package:BUILD:6 my_macro" with positional arguments. +The function was considered a macro as it may produce targets via calls: +test/package:BUILD:6 my_macro +test/package:BUILD:1 macro`, + `:7: All calls to rules or macros should pass arguments by keyword (arg_name=value) syntax. +Found call to rule or macro "test/package:BUILD:7 my_rule" with positional arguments. +The function was considered a macro as it may produce targets via calls: +test/package:BUILD:7 my_rule +test/package:BUILD:2 rule`, + `:10: All calls to rules or macros should pass arguments by keyword (arg_name=value) syntax. +Found call to rule or macro "test/package:BUILD:10 my_macro" with positional arguments. +The function was considered a macro as it may produce targets via calls: +test/package:BUILD:10 my_macro +test/package:BUILD:1 macro`, + `:11: All calls to rules or macros should pass arguments by keyword (arg_name=value) syntax. +Found call to rule or macro "test/package:BUILD:11 my_rule" with positional arguments. +The function was considered a macro as it may produce targets via calls: +test/package:BUILD:11 my_rule +test/package:BUILD:2 rule`, }, scopeBuild) @@ -233,7 +274,7 @@ filegroup( ) some_rule( - arg1 = "normal/path/file.txt", + arg1 = "normal/path/file.txt", arg2 = "/external/repo/file.py", arg3 = ["file1.txt", "/external/another/file.cc"], )`, diff --git a/warn/warn_macro.go b/warn/warn_macro.go index f77c2f3f5..46befa7d1 100644 --- a/warn/warn_macro.go +++ b/warn/warn_macro.go @@ -23,31 +23,8 @@ import ( "strings" "github.com/bazelbuild/buildtools/build" - "github.com/bazelbuild/buildtools/labels" ) -// Internal constant that represents the native module -const nativeModule = "" - -// function represents a function identifier, which is a pair (module name, function name). -type function struct { - pkg string // package where the function is defined - filename string // name of a .bzl file relative to the package - name string // original name of the function -} - -func (f function) label() string { - return f.pkg + ":" + f.filename -} - -// funCall represents a call to another function. It contains information of the function itself as well as some -// information about the environment -type funCall struct { - function - nameAlias string // function name alias (it could be loaded with a different name or assigned to a new variable). - line int // line on which the function is being called -} - // acceptsNameArgument checks whether a function can accept a named argument called "name", // either directly or via **kwargs. func acceptsNameArgument(def *build.DefStmt) bool { @@ -59,251 +36,12 @@ func acceptsNameArgument(def *build.DefStmt) bool { return false } -// fileData represents information about rules and functions extracted from a file -type fileData struct { - loadedSymbols map[string]function // Symbols loaded from other files. - rulesOrMacros map[string]bool // all rules or macros defined in the file. - functions map[string]map[string]funCall // outer map: all functions defined in the file, inner map: all distinct function calls from the given function - aliases map[string]function // all top-level aliases (e.g. `foo = bar`). -} - -// resolvesExternal takes a local function definition and replaces it with an external one if it's been defined -// in another file and loaded -func resolveExternal(fn function, externalSymbols map[string]function) function { - if external, ok := externalSymbols[fn.name]; ok { - return external - } - return fn -} - -// exprLine returns the start line of an expression -func exprLine(expr build.Expr) int { - start, _ := expr.Span() - return start.Line -} - -// getFunCalls extracts information about functions that are being called from the given function -func getFunCalls(def *build.DefStmt, pkg, filename string, externalSymbols map[string]function) map[string]funCall { - funCalls := make(map[string]funCall) - build.Walk(def, func(expr build.Expr, stack []build.Expr) { - call, ok := expr.(*build.CallExpr) - if !ok { - return - } - if ident, ok := call.X.(*build.Ident); ok { - funCalls[ident.Name] = funCall{ - function: resolveExternal(function{pkg, filename, ident.Name}, externalSymbols), - nameAlias: ident.Name, - line: exprLine(call), - } - return - } - dot, ok := call.X.(*build.DotExpr) - if !ok { - return - } - if ident, ok := dot.X.(*build.Ident); !ok || ident.Name != "native" { - return - } - name := "native." + dot.Name - funCalls[name] = funCall{ - function: function{ - name: dot.Name, - filename: nativeModule, - }, - nameAlias: name, - line: exprLine(dot), - } - }) - return funCalls -} - -// analyzeFile extracts the information about rules and functions defined in the file -func analyzeFile(f *build.File) fileData { - if f == nil { - return fileData{} - } - - report := fileData{ - loadedSymbols: make(map[string]function), - rulesOrMacros: make(map[string]bool), - functions: make(map[string]map[string]funCall), - aliases: make(map[string]function), - } - - // Collect loaded symbols - for _, stmt := range f.Stmt { - load, ok := stmt.(*build.LoadStmt) - if !ok { - continue - } - label := labels.ParseRelative(load.Module.Value, f.Pkg) - if label.Repository != "" || label.Target == "" { - continue - } - for i, from := range load.From { - report.loadedSymbols[load.To[i].Name] = function{label.Package, label.Target, from.Name} - } - } - - for _, stmt := range f.Stmt { - switch stmt := stmt.(type) { - case *build.AssignExpr: - // Analyze aliases (`foo = bar`) or rule declarations (`foo = rule(...)`) - lhsIdent, ok := stmt.LHS.(*build.Ident) - if !ok { - continue - } - if rhsIdent, ok := stmt.RHS.(*build.Ident); ok { - report.aliases[lhsIdent.Name] = resolveExternal(function{f.Pkg, f.Label, rhsIdent.Name}, report.loadedSymbols) - continue - } - - call, ok := stmt.RHS.(*build.CallExpr) - if !ok { - continue - } - if ident, ok := call.X.(*build.Ident); ok { - if ident.Name == "rule" || ident.Name == "macro" { - report.rulesOrMacros[lhsIdent.Name] = true - continue - } - } - case *build.DefStmt: - report.functions[stmt.Name] = getFunCalls(stmt, f.Pkg, f.Label, report.loadedSymbols) - default: - continue - } - } - return report -} - -// functionReport represents the analysis result of a function -type functionReport struct { - isRuleOrMacro bool // whether the function is a macro (or a rule) - fc *funCall // a call to the rule or another macro -} - -// macroAnalyzer is an object that analyzes the directed graph of functions calling each other, -// loading other files lazily if necessary. -type macroAnalyzer struct { - fileReader *FileReader - files map[string]fileData - cache map[function]functionReport -} - -// getFileData retrieves a file using the fileReader object and extracts information about functions and rules -// defined in the file. -func (ma macroAnalyzer) getFileData(pkg, label string) fileData { - filename := pkg + ":" + label - if fd, ok := ma.files[filename]; ok { - return fd - } - if ma.fileReader == nil { - fd := fileData{} - ma.files[filename] = fd - return fd - } - f := ma.fileReader.GetFile(pkg, label) - fd := analyzeFile(f) - ma.files[filename] = fd - return fd -} - -// IsMacro is a public function that checks whether the given function is a macro -func (ma macroAnalyzer) IsRuleOrMacro(fn function) (report functionReport) { - // Check the cache first - if cached, ok := ma.cache[fn]; ok { - return cached - } - // Write a negative result to the cache before analyzing. This will prevent stack overflow crashes - // if the input data contains recursion. - ma.cache[fn] = report - defer func() { - // Update the cache with the actual result - ma.cache[fn] = report - }() - - // Check for native rules - if fn.filename == nativeModule { - switch fn.name { - case "glob", "existing_rule", "existing_rules", "package_name", - "repository_name", "exports_files": - // Not a rule - default: - report.isRuleOrMacro = true - } - return - } - - fileData := ma.getFileData(fn.pkg, fn.filename) - - // Check whether fn.name is an alias for another function - if alias, ok := fileData.aliases[fn.name]; ok { - if ma.IsRuleOrMacro(alias).isRuleOrMacro { - report.isRuleOrMacro = true - } - return - } - - // Check whether fn.name is a rule or macro - if fileData.rulesOrMacros[fn.name] { - report.isRuleOrMacro = true - return - } - - // Check whether fn.name is a loaded symbol from another file - if externalFn, ok := fileData.loadedSymbols[fn.name]; ok { - if ma.IsRuleOrMacro(externalFn).isRuleOrMacro { - report.isRuleOrMacro = true - return - } - } - - // Check whether fn.name is an ordinary function - funCalls, ok := fileData.functions[fn.name] - if !ok { - return - } - - // Prioritize function calls from already loaded files. If some of the function calls are from the same file - // (or another file that has been loaded already), check them first. - var knownFunCalls, newFunCalls []funCall - for _, fc := range funCalls { - if _, ok := ma.files[fc.function.pkg+":"+fc.function.filename]; ok || fc.function.filename == nativeModule { - knownFunCalls = append(knownFunCalls, fc) - } else { - newFunCalls = append(newFunCalls, fc) - } - } - - for _, fc := range append(knownFunCalls, newFunCalls...) { - if ma.IsRuleOrMacro(fc.function).isRuleOrMacro { - report.isRuleOrMacro = true - report.fc = &fc - return - } - } - - return -} - -// newMacroAnalyzer creates and initiates an instance of macroAnalyzer. -func newMacroAnalyzer(fileReader *FileReader) macroAnalyzer { - return macroAnalyzer{ - fileReader: fileReader, - files: make(map[string]fileData), - cache: make(map[function]functionReport), - } -} - func unnamedMacroWarning(f *build.File, fileReader *FileReader) []*LinterFinding { if f.Type != build.TypeBzl { return nil } - macroAnalyzer := newMacroAnalyzer(fileReader) - macroAnalyzer.files[f.Pkg+":"+f.Label] = analyzeFile(f) + macroAnalyzer := NewMacroAnalyzer(fileReader) findings := []*LinterFinding{} for _, stmt := range f.Stmt { @@ -316,24 +54,27 @@ func unnamedMacroWarning(f *build.File, fileReader *FileReader) []*LinterFinding continue } - report := macroAnalyzer.IsRuleOrMacro(function{f.Pkg, f.Label, def.Name}) - if !report.isRuleOrMacro { + report, err := macroAnalyzer.AnalyzeFn(f, def) + if err != nil { + // TODO: Analysis errors are simply ignored as buildifier does not currently handle errors. continue } - msg := fmt.Sprintf(`The macro %q should have a keyword argument called "name".`, def.Name) - if report.fc != nil { - // fc shouldn't be nil because that's the only node that can be found inside a function. - msg += fmt.Sprintf(` - -It is considered a macro because it calls a rule or another macro %q on line %d. + if !report.CanProduceTargets() { + continue + } + msg := fmt.Sprintf(`The macro %q should have a keyword argument called "name". +It is considered a macro as it may produce targets via calls: +%s By convention, every public macro needs a "name" argument (even if it doesn't use it). This is important for tooling and automation. - * If this function is a helper function that's not supposed to be used outside of this file, - please make it private (e.g. rename it to "_%s"). - * Otherwise, add a "name" argument. If possible, use that name when calling other macros/rules.`, report.fc.nameAlias, report.fc.line, def.Name) - } +* If this function is a helper function that's not supposed to be used outside of this file, + please make it private (e.g. rename it to "_%s"). +* Otherwise, add a "name" argument. If possible, use that name when calling other macros/rules.`, + def.Name, + report.PrintableCallStack(), + def.Name) finding := makeLinterFinding(def, msg) finding.End = def.ColonPos findings = append(findings, finding) diff --git a/warn/warn_macro_test.go b/warn/warn_macro_test.go index 13c6ce3ae..b2e856931 100644 --- a/warn/warn_macro_test.go +++ b/warn/warn_macro_test.go @@ -24,7 +24,7 @@ load(":foo.bzl", "foo") my_rule = rule() -def macro(x): +def a_macro(x): foo() my_rule(name = x) @@ -43,12 +43,13 @@ def another_macro(x): [native.cc_library() for i in x] `, []string{ - `5: The macro "macro" should have a keyword argument called "name". - -It is considered a macro because it calls a rule or another macro "my_rule" on line 7.`, - `19: The macro "another_macro" should have a keyword argument called "name". - -It is considered a macro because it calls a rule or another macro "native.cc_library" on line 21.`, + `:5: The macro "a_macro" should have a keyword argument called "name". +It is considered a macro as it may produce targets via calls: +test/package:test_file.bzl:7 my_rule +test/package:test_file.bzl:3 rule`, + `:19: The macro "another_macro" should have a keyword argument called "name". +It is considered a macro as it may produce targets via calls: +test/package:test_file.bzl:21 native.cc_library`, }, scopeBzl) @@ -70,32 +71,22 @@ def macro3(foo, *args, **kwargs): checkFindings(t, "unnamed-macro", ` my_rule = rule() -def macro(name): +def a_macro(name): my_rule(name = name) -alias = macro +alias = a_macro def bad_macro(): for x in y: alias(x) `, []string{ - `8: The macro "bad_macro" should have a keyword argument called "name". - -It is considered a macro because it calls a rule or another macro "alias" on line 10.`, - }, - scopeBzl) - - checkFindings(t, "unnamed-macro", ` -symbolic_macro = macro() - -def bad_macro(): - symbolic_macro(x) -`, - []string{ - `3: The macro "bad_macro" should have a keyword argument called "name". - -It is considered a macro because it calls a rule or another macro "symbolic_macro" on line 4.`, + `:8: The macro "bad_macro" should have a keyword argument called "name". +It is considered a macro as it may produce targets via calls: +test/package:test_file.bzl:10 alias +test/package:test_file.bzl:6 a_macro +test/package:test_file.bzl:4 my_rule +test/package:test_file.bzl:1 rule`, }, scopeBzl) @@ -115,18 +106,25 @@ def macro4(): my_rule() `, []string{ - `3: The macro "macro1" should have a keyword argument called "name". - -It is considered a macro because it calls a rule or another macro "my_rule" on line 4.`, - `6: The macro "macro2" should have a keyword argument called "name". - -It is considered a macro because it calls a rule or another macro "macro1" on line 7`, - `9: The macro "macro3" should have a keyword argument called "name". - -It is considered a macro because it calls a rule or another macro "macro2" on line 10.`, - `12: The macro "macro4" should have a keyword argument called "name". - -It is considered a macro because it calls a rule or another macro "my_rule" on line 13.`, + `:3: The macro "macro1" should have a keyword argument called "name". +It is considered a macro as it may produce targets via calls: +test/package:test_file.bzl:4 my_rule +test/package:test_file.bzl:1 rule`, + `:6: The macro "macro2" should have a keyword argument called "name". +It is considered a macro as it may produce targets via calls: +test/package:test_file.bzl:7 macro1 +test/package:test_file.bzl:4 my_rule +test/package:test_file.bzl:1 rule`, + `:9: The macro "macro3" should have a keyword argument called "name". +It is considered a macro as it may produce targets via calls: +test/package:test_file.bzl:10 macro2 +test/package:test_file.bzl:7 macro1 +test/package:test_file.bzl:4 my_rule +test/package:test_file.bzl:1 rule`, + `:12: The macro "macro4" should have a keyword argument called "name". +It is considered a macro as it may produce targets via calls: +test/package:test_file.bzl:13 my_rule +test/package:test_file.bzl:1 rule`, }, scopeBzl) } @@ -137,15 +135,15 @@ func TestUnnamedMacroRecursion(t *testing.T) { checkFindings(t, "unnamed-macro", ` my_rule = rule() -def macro(): - macro() +def a_macro(): + a_macro() `, []string{}, scopeBzl) checkFindings(t, "unnamed-macro", ` my_rule = rule() -def macro(): - macro() +def a_macro(): + a_macro() `, []string{}, scopeBzl) checkFindings(t, "unnamed-macro", ` @@ -172,12 +170,15 @@ def bar(): my_rule() `, []string{ - `3: The macro "foo" should have a keyword argument called "name". - -It is considered a macro because it calls a rule or another macro "bar" on line 4.`, - `6: The macro "bar" should have a keyword argument called "name". - -It is considered a macro because it calls a rule or another macro "my_rule" on line 8.`, + `:3: The macro "foo" should have a keyword argument called "name". +It is considered a macro as it may produce targets via calls: +test/package:test_file.bzl:4 bar +test/package:test_file.bzl:8 my_rule +test/package:test_file.bzl:1 rule`, + `:6: The macro "bar" should have a keyword argument called "name". +It is considered a macro as it may produce targets via calls: +test/package:test_file.bzl:8 my_rule +test/package:test_file.bzl:1 rule`, }, scopeBzl) } @@ -226,36 +227,48 @@ def not_macro(x): f() `, []string{ - `4: The macro "macro1" should have a keyword argument called "name". - -It is considered a macro because it calls a rule or another macro "abc" on line 5. + `:4: The macro "macro1" should have a keyword argument called "name". +It is considered a macro as it may produce targets via calls: +test/package:test_file.bzl:5 abc +test/package:subdir1/foo.bzl:1 bar +test/package:subdir1/foo.bzl:6 foo +test/package:subdir1/foo.bzl:3 native.foo_binary By convention, every public macro needs a "name" argument (even if it doesn't use it). This is important for tooling and automation. - * If this function is a helper function that's not supposed to be used outside of this file, - please make it private (e.g. rename it to "_macro1"). - * Otherwise, add a "name" argument. If possible, use that name when calling other macros/rules.`, - `7: The macro "macro2" should have a keyword argument called "name". - -It is considered a macro because it calls a rule or another macro "baz" on line 8. +* If this function is a helper function that's not supposed to be used outside of this file, + please make it private (e.g. rename it to "_macro1"). +* Otherwise, add a "name" argument. If possible, use that name when calling other macros/rules.`, + `:7: The macro "macro2" should have a keyword argument called "name". +It is considered a macro as it may produce targets via calls: +test/package:test_file.bzl:8 baz +test/package:subdir2/baz.bzl:2 baz +test/package:subdir2/baz.bzl:7 bar +test/package:subdir1/foo.bzl:2 bar +test/package:subdir1/foo.bzl:6 foo +test/package:subdir1/foo.bzl:3 native.foo_binary By convention, every public macro needs a "name" argument (even if it doesn't use it). This is important for tooling and automation. - * If this function is a helper function that's not supposed to be used outside of this file, - please make it private (e.g. rename it to "_macro2"). - * Otherwise, add a "name" argument. If possible, use that name when calling other macros/rules.`, - `10: The macro "macro3" should have a keyword argument called "name". - -It is considered a macro because it calls a rule or another macro "qux" on line 11. +* If this function is a helper function that's not supposed to be used outside of this file, + please make it private (e.g. rename it to "_macro2"). +* Otherwise, add a "name" argument. If possible, use that name when calling other macros/rules.`, + `:10: The macro "macro3" should have a keyword argument called "name". +It is considered a macro as it may produce targets via calls: +test/package:test_file.bzl:11 qux +test/package:subdir2/baz.bzl:2 qux +test/package:subdir2/baz.bzl:10 your_rule +test/package:subdir1/foo.bzl:2 my_rule +test/package:subdir1/foo.bzl:8 rule By convention, every public macro needs a "name" argument (even if it doesn't use it). This is important for tooling and automation. - * If this function is a helper function that's not supposed to be used outside of this file, - please make it private (e.g. rename it to "_macro3"). - * Otherwise, add a "name" argument. If possible, use that name when calling other macros/rules.`, +* If this function is a helper function that's not supposed to be used outside of this file, + please make it private (e.g. rename it to "_macro3"). +* Otherwise, add a "name" argument. If possible, use that name when calling other macros/rules.`, }, scopeBzl) } @@ -294,14 +307,18 @@ def qux(): load(":foo.bzl", "foo", "baz") load(":bar.bzl", quux = "qux") -def macro(): +def a_macro(): foo() baz() quux() `, []string{ - `4: The macro "macro" should have a keyword argument called "name". - -It is considered a macro because it calls a rule or another macro "quux" on line 7.`, + `:4: The macro "a_macro" should have a keyword argument called "name". +It is considered a macro as it may produce targets via calls: +test/package:test_file.bzl:7 quux +test/package:bar.bzl:2 qux +test/package:bar.bzl:10 quuux +test/package:foo.bzl:2 qux +test/package:foo.bzl:11 native.cc_library`, }, scopeBzl) } @@ -320,12 +337,16 @@ load(":foo.bzl", bar = "foo") my_rule = rule() -def macro(): +def a_macro(): bar() `, []string{ - `5: The macro "macro" should have a keyword argument called "name". - -It is considered a macro because it calls a rule or another macro "bar" on line 6.`, + `:5: The macro "a_macro" should have a keyword argument called "name". +It is considered a macro as it may produce targets via calls: +test/package:test_file.bzl:6 bar +test/package:foo.bzl:1 foo +test/package:foo.bzl:5 some_rule +test/package:test_file.bzl:2 my_rule +test/package:test_file.bzl:3 rule`, }, scopeBzl) } @@ -362,18 +383,23 @@ def macro4(): r = rule() `, []string{ - `6: The macro "macro1" should have a keyword argument called "name". - -It is considered a macro because it calls a rule or another macro "a" on line 7.`, - `9: The macro "macro2" should have a keyword argument called "name". - -It is considered a macro because it calls a rule or another macro "native.cc_library" on line 11.`, - `13: The macro "macro3" should have a keyword argument called "name". - -It is considered a macro because it calls a rule or another macro "a" on line 15.`, - `17: The macro "macro4" should have a keyword argument called "name". - -It is considered a macro because it calls a rule or another macro "r" on line 19.`, + `:6: The macro "macro1" should have a keyword argument called "name". +It is considered a macro as it may produce targets via calls: +test/package:test_file.bzl:7 a +:a.bzl:1 a +:a.bzl:1 rule`, + `:9: The macro "macro2" should have a keyword argument called "name". +It is considered a macro as it may produce targets via calls: +test/package:test_file.bzl:11 native.cc_library`, + `:13: The macro "macro3" should have a keyword argument called "name". +It is considered a macro as it may produce targets via calls: +test/package:test_file.bzl:15 a +:a.bzl:1 a +:a.bzl:1 rule`, + `:17: The macro "macro4" should have a keyword argument called "name". +It is considered a macro as it may produce targets via calls: +test/package:test_file.bzl:19 r +test/package:test_file.bzl:21 rule`, }, scopeBzl) if len(fileReaderRequests) == 1 && fileReaderRequests[0] == "a.bzl" { @@ -405,9 +431,13 @@ def macro1(): def macro2(name): baz() `, []string{ - `5: The macro "macro1" should have a keyword argument called "name". - -It is considered a macro because it calls a rule or another macro "baz" on line 6.`, + `:5: The macro "macro1" should have a keyword argument called "name". +It is considered a macro as it may produce targets via calls: +test/package:test_file.bzl:6 baz +test/package:test_file.bzl:3 bar +test/package:bar.bzl:1 bar +test/package:bar.bzl:4 my_rule +test/package:bar.bzl:2 rule`, }, scopeBzl) } @@ -418,13 +448,15 @@ my_rule = rule() def _not_macro(x): my_rule(name = x) -def macro(x): +def a_macro(x): _not_macro(x) `, []string{ - `6: The macro "macro" should have a keyword argument called "name". - -It is considered a macro because it calls a rule or another macro "_not_macro" on line 7.`, + `:6: The macro "a_macro" should have a keyword argument called "name". +It is considered a macro as it may produce targets via calls: +test/package:test_file.bzl:7 _not_macro +test/package:test_file.bzl:4 my_rule +test/package:test_file.bzl:1 rule`, }, scopeBzl) } @@ -433,7 +465,7 @@ func TestUnnamedMacroTypeAnnotation(t *testing.T) { checkFindings(t, "unnamed-macro", ` my_rule = rule() -def macro(name: string): +def a_macro(name: string): my_rule(name) `, []string{}, @@ -442,7 +474,7 @@ def macro(name: string): checkFindings(t, "unnamed-macro", ` my_rule = rule() -def macro(name: string = "default"): +def a_macro(name: string = "default"): my_rule(name) `, []string{},