Skip to content

Commit 3a267cb

Browse files
hvadehracopybara-github
authored andcommitted
Export rule class with 'name' if provided to 'rule()'
https://docs.google.com/document/d/12sC92mq7WasTvDWsm4U782oCegitfi1jbgOnMIGCe-A/edit PiperOrigin-RevId: 391720461
1 parent b3411eb commit 3a267cb

File tree

6 files changed

+171
-5
lines changed

6 files changed

+171
-5
lines changed

src/main/java/com/google/devtools/build/lib/analysis/starlark/StarlarkRuleClassFunctions.java

Lines changed: 36 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,8 @@
5454
import com.google.devtools.build.lib.cmdline.PackageIdentifier;
5555
import com.google.devtools.build.lib.events.Event;
5656
import com.google.devtools.build.lib.events.EventHandler;
57+
import com.google.devtools.build.lib.events.EventKind;
58+
import com.google.devtools.build.lib.events.StoredEventHandler;
5759
import com.google.devtools.build.lib.packages.AllowlistChecker;
5860
import com.google.devtools.build.lib.packages.Attribute;
5961
import com.google.devtools.build.lib.packages.Attribute.StarlarkComputedDefaultTemplate;
@@ -299,6 +301,7 @@ public StarlarkCallable rule(
299301
Object cfg,
300302
Object execGroups,
301303
Object compileOneFiletype,
304+
Object name,
302305
StarlarkThread thread)
303306
throws EvalException {
304307
BazelStarlarkContext bazelContext = BazelStarlarkContext.from(thread);
@@ -461,7 +464,36 @@ public StarlarkCallable rule(
461464
builder.setPreferredDependencyPredicate(FileType.of((String) compileOneFiletype));
462465
}
463466

464-
return new StarlarkRuleFunction(builder, type, attributes, thread.getCallerLocation());
467+
StarlarkRuleFunction starlarkRuleFunction =
468+
new StarlarkRuleFunction(builder, type, attributes, thread.getCallerLocation());
469+
// If a name= parameter is supplied (and we're currently initializing a .bzl module), export the
470+
// rule immediately under that name; otherwise the rule will be exported by the postAssignHook
471+
// set up in BzlLoadFunction.
472+
//
473+
// Because exporting can raise multiple errors, we need to accumulate them here into a single
474+
// EvalException. This is a code smell because any non-ERROR events will be lost, and any
475+
// location
476+
// information in the events will be overwritten by the location of this rule's definition.
477+
// However, this is currently fine because StarlarkRuleFunction#export only creates events that
478+
// are ERRORs and that have the rule definition as their location.
479+
// TODO(brandjon): Instead of accumulating events here, consider registering the rule in the
480+
// BazelStarlarkContext, and exporting such rules after module evaluation in
481+
// BzlLoadFunction#execAndExport.
482+
if (name != Starlark.NONE && bzlModule != null) {
483+
StoredEventHandler handler = new StoredEventHandler();
484+
starlarkRuleFunction.export(handler, bzlModule.label(), (String) name);
485+
if (handler.hasErrors()) {
486+
StringBuilder errors =
487+
handler.getEvents().stream()
488+
.filter(e -> e.getKind() == EventKind.ERROR)
489+
.reduce(
490+
new StringBuilder(),
491+
(sb, ev) -> sb.append("\n").append(ev.getMessage()),
492+
StringBuilder::append);
493+
throw Starlark.errorf("Errors in exporting %s: %s", name, errors.toString());
494+
}
495+
}
496+
return starlarkRuleFunction;
465497
}
466498

467499
/**
@@ -756,6 +788,9 @@ public Object call(StarlarkThread thread, Tuple args, Dict<String, Object> kwarg
756788
}
757789

758790
/** Export a RuleFunction from a Starlark file with a given name. */
791+
// To avoid losing event information in the case where the rule was defined with an explicit
792+
// name= arg, all events should be created using errorf(). See the comment in rule() above for
793+
// details.
759794
@Override
760795
public void export(EventHandler handler, Label starlarkLabel, String ruleClassName) {
761796
Preconditions.checkState(ruleClass == null && builder != null);

src/main/java/com/google/devtools/build/lib/starlarkbuildapi/StarlarkRuleFunctionsApi.java

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -352,6 +352,24 @@ public interface StarlarkRuleFunctionsApi<FileApiT extends FileApi> {
352352
doc =
353353
"Used by --compile_one_dependency: if multiple rules consume the specified file, "
354354
+ "should we choose this rule over others."),
355+
@Param(
356+
name = "name",
357+
named = true,
358+
defaultValue = "None",
359+
positional = false,
360+
doc =
361+
"The name of this rule, as understood by Bazel and reported in contexts such as"
362+
+ " logging, <code>native.existing_rule(...)[kind]</code>, and <code>bazel"
363+
+ " query</code>. Usually this is the same as the Starlark identifier that gets"
364+
+ " bound to this rule; for instance a rule called <code>foo_library</code>"
365+
+ " would typically be declared as <code>foo_library = rule(...)</code> and"
366+
+ " instantiated in a BUILD file as <code>foo_library(...)</code>.<p>If this"
367+
+ " parameter is omitted, the rule's name is set to the name of the first"
368+
+ " Starlark global variable to be bound to this rule within its declaring .bzl"
369+
+ " module. Thus, <code>foo_library = rule(...)</code> need not specify this"
370+
+ " parameter if the name is <code>foo_library</code>.<p>Specifying an explicit"
371+
+ " name for a rule does not change where you are allowed to instantiate the"
372+
+ " rule."),
355373
},
356374
useStarlarkThread = true)
357375
StarlarkCallable rule(
@@ -374,6 +392,7 @@ StarlarkCallable rule(
374392
Object cfg,
375393
Object execGroups,
376394
Object compileOneFiletype,
395+
Object name,
377396
StarlarkThread thread)
378397
throws EvalException;
379398

src/main/java/com/google/devtools/build/skydoc/SkydocMain.java

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -279,8 +279,12 @@ public Module eval(
279279
for (Entry<String, Object> envEntry : sortedBindings.entrySet()) {
280280
if (ruleFunctions.containsKey(envEntry.getValue())) {
281281
RuleInfo.Builder ruleInfoBuild = ruleFunctions.get(envEntry.getValue()).getRuleInfo();
282-
RuleInfo ruleInfo = ruleInfoBuild.setRuleName(envEntry.getKey()).build();
283-
ruleInfoMap.put(envEntry.getKey(), ruleInfo);
282+
// Use symbol name as the rule name only if not already set in the call to rule()
283+
if ("".equals(ruleInfoBuild.getRuleName())) {
284+
ruleInfoBuild.setRuleName(envEntry.getKey());
285+
}
286+
RuleInfo ruleInfo = ruleInfoBuild.build();
287+
ruleInfoMap.put(ruleInfo.getRuleName(), ruleInfo);
284288
}
285289
if (providerInfos.containsKey(envEntry.getValue())) {
286290
ProviderInfo.Builder providerInfoBuild =

src/main/java/com/google/devtools/build/skydoc/fakebuildapi/FakeStarlarkRuleFunctionsApi.java

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -137,6 +137,7 @@ public StarlarkCallable rule(
137137
Object cfg,
138138
Object execGroups,
139139
Object compileOneFiletype,
140+
Object name,
140141
StarlarkThread thread)
141142
throws EvalException {
142143
ImmutableMap.Builder<String, FakeDescriptor> attrsMapBuilder = ImmutableMap.builder();
@@ -154,9 +155,11 @@ public StarlarkCallable rule(
154155

155156
RuleDefinitionIdentifier functionIdentifier = new RuleDefinitionIdentifier();
156157

157-
// Only the Builder is passed to RuleInfoWrapper as the rule name is not yet available.
158+
// Only the Builder is passed to RuleInfoWrapper as the rule name may not be available yet.
158159
RuleInfo.Builder ruleInfo = RuleInfo.newBuilder().setDocString(doc).addAllAttribute(attrInfos);
159-
160+
if (name != Starlark.NONE) {
161+
ruleInfo.setRuleName((String) name);
162+
}
160163
Location loc = thread.getCallerLocation();
161164
ruleInfoList.add(new RuleInfoWrapper(functionIdentifier, loc, ruleInfo));
162165

src/test/java/com/google/devtools/build/lib/starlark/StarlarkRuleClassFunctionsTest.java

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1645,6 +1645,57 @@ public void testExportAliasedName() throws Exception {
16451645
assertThat(fooName).isEqualTo("d");
16461646
}
16471647

1648+
@Test
1649+
public void testExportWithSpecifiedName() throws Exception {
1650+
evalAndExport(
1651+
ev, //
1652+
"def _impl(ctx): pass",
1653+
"a = rule(implementation = _impl, name = 'r')",
1654+
"z = a");
1655+
1656+
String aName = ((StarlarkRuleFunction) ev.lookup("a")).getRuleClass().getName();
1657+
assertThat(aName).isEqualTo("r");
1658+
String zName = ((StarlarkRuleFunction) ev.lookup("z")).getRuleClass().getName();
1659+
assertThat(zName).isEqualTo("r");
1660+
}
1661+
1662+
@Test
1663+
public void testExportWithSpecifiedNameFailure() throws Exception {
1664+
ev.setFailFast(false);
1665+
1666+
evalAndExport(
1667+
ev, //
1668+
"def _impl(ctx): pass",
1669+
"rule(implementation = _impl, name = '1a')");
1670+
1671+
ev.assertContainsError("Invalid rule name: 1a");
1672+
}
1673+
1674+
@Test
1675+
public void testExportWithMultipleErrors() throws Exception {
1676+
ev.setFailFast(false);
1677+
1678+
evalAndExport(
1679+
ev,
1680+
"def _impl(ctx): pass",
1681+
"rule(",
1682+
" implementation = _impl,",
1683+
" attrs = {",
1684+
" 'name' : attr.string(),",
1685+
" 'tags' : attr.string_list(),",
1686+
" },",
1687+
" name = '1a',",
1688+
")");
1689+
1690+
ev.assertContainsError(
1691+
"Error in rule: Errors in exporting 1a: \n"
1692+
+ "cannot add attribute: There is already a built-in attribute 'name' which cannot be"
1693+
+ " overridden.\n"
1694+
+ "cannot add attribute: There is already a built-in attribute 'tags' which cannot be"
1695+
+ " overridden.\n"
1696+
+ "Invalid rule name: 1a");
1697+
}
1698+
16481699
@Test
16491700
public void testOutputToGenfiles() throws Exception {
16501701
evalAndExport(ev, "def impl(ctx): pass", "r1 = rule(impl, output_to_genfiles=True)");

src/test/java/com/google/devtools/build/skydoc/SkydocTest.java

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -220,6 +220,60 @@ public void testMultipleRuleNames() throws Exception {
220220
assertThat(ruleInfoMap.build().keySet()).containsExactly("rule_one", "rule_two");
221221
}
222222

223+
@Test
224+
public void testRuleExportedWithSpecifiedName() throws Exception {
225+
scratch.file(
226+
"/execroot/io_bazel/test/test.bzl",
227+
"def rule_impl(ctx):",
228+
" return []",
229+
"",
230+
"rule_one = rule(",
231+
" doc = 'Rule one',",
232+
" implementation = rule_impl,",
233+
" name = 'rule_one_exported_name',",
234+
")");
235+
236+
ImmutableMap.Builder<String, RuleInfo> ruleInfoMap = ImmutableMap.builder();
237+
238+
skydocMain.eval(
239+
StarlarkSemantics.DEFAULT,
240+
Label.parseAbsoluteUnchecked("//test:test.bzl"),
241+
ruleInfoMap,
242+
ImmutableMap.builder(),
243+
ImmutableMap.builder(),
244+
ImmutableMap.builder(),
245+
ImmutableMap.builder());
246+
247+
assertThat(ruleInfoMap.build().keySet()).containsExactly("rule_one_exported_name");
248+
}
249+
250+
@Test
251+
public void testUnassignedRuleNotDocumented() throws Exception {
252+
scratch.file(
253+
"/execroot/io_bazel/test/test.bzl",
254+
"def rule_impl(ctx):",
255+
" return []",
256+
"",
257+
"rule(",
258+
" doc = 'Undocumented rule',",
259+
" implementation = rule_impl,",
260+
" name = 'rule_exported_name',",
261+
")");
262+
263+
ImmutableMap.Builder<String, RuleInfo> ruleInfoMap = ImmutableMap.builder();
264+
265+
skydocMain.eval(
266+
StarlarkSemantics.DEFAULT,
267+
Label.parseAbsoluteUnchecked("//test:test.bzl"),
268+
ruleInfoMap,
269+
ImmutableMap.builder(),
270+
ImmutableMap.builder(),
271+
ImmutableMap.builder(),
272+
ImmutableMap.builder());
273+
274+
assertThat(ruleInfoMap.build().keySet()).isEmpty();
275+
}
276+
223277
@Test
224278
public void testRulesAcrossMultipleFiles() throws Exception {
225279
scratch.file("/execroot/io_bazel/lib/rule_impl.bzl", "def rule_impl(ctx):", " return []");

0 commit comments

Comments
 (0)