diff --git a/MODULE.bazel b/MODULE.bazel index ebf76a0..61bb2b6 100644 --- a/MODULE.bazel +++ b/MODULE.bazel @@ -8,7 +8,7 @@ bazel_dep(name = "bazel_skylib", version = "1.4.1") bazel_dep(name = "aspect_bazel_lib", version = "1.34.0", dev_dependency = True) bazel_dep(name = "bazel_skylib_gazelle_plugin", version = "1.4.1", dev_dependency = True) -bazel_dep(name = "buildifier_prebuilt", version = "6.1.0", dev_dependency = True) +bazel_dep(name = "buildifier_prebuilt", version = "6.4.0", dev_dependency = True) bazel_dep(name = "gazelle", version = "0.33.0", dev_dependency = True, repo_name = "bazel_gazelle") bazel_dep(name = "platforms", version = "0.0.6", dev_dependency = True) bazel_dep(name = "rules_testing", version = "0.4.0", dev_dependency = True) diff --git a/examples/MODULE.bazel b/examples/MODULE.bazel index 56620a5..10e8125 100644 --- a/examples/MODULE.bazel +++ b/examples/MODULE.bazel @@ -6,6 +6,7 @@ local_path_override( path = "..", ) +bazel_dep(name = "bazel_skylib", version = "1.6.1") bazel_dep(name = "contrib_rules_jvm", version = "0.27.0") bazel_dep(name = "platforms", version = "0.0.10") bazel_dep(name = "rules_cc", version = "0.0.9") diff --git a/examples/args_location_expansion/BUILD.bazel b/examples/args_location_expansion/BUILD.bazel new file mode 100644 index 0000000..7bd8415 --- /dev/null +++ b/examples/args_location_expansion/BUILD.bazel @@ -0,0 +1,58 @@ +load("//args_location_expansion/rules:defs.bzl", "write_settings_rule") +load(":doubly_transitioned_test.bzl", "doubly_transitioned_test") + +write_settings_rule( + name = "single_rule", + tags = ["manual"], +) + +alias( + name = "alias_target", + actual = "//args_location_expansion/pkg:aliased_rule", + tags = ["manual"], +) + +doubly_transitioned_test( + name = "doubly_transitioned_test", + args = [ + "$(rlocationpath :source_file.txt)", + "$(rlocationpath :single_rule)", + "$(rlocationpaths //args_location_expansion/pkg:multiple_rules)", + "$(rlocationpath alias_target)", + "$(rlocationpath :alias_target)", + "$(rlocationpath @bazel_tools//tools/bash/runfiles)", + "_main/$(rootpath :single_rule)", + "_main/$(location :single_rule)", + ] + select({ + "@platforms//os:linux": ["$(rlocationpath //args_location_expansion/pkg:linux_only_rule)"], + "//conditions:default": ["$(rlocationpath //args_location_expansion/pkg:generic_rule)"], + }) + select({ + "@platforms//os:windows": [], + "//conditions:default": ["$(rlocationpath //args_location_expansion/pkg:special_09!%-@^_\"#$&'(*-+,;<=>?[]{|}~/._characters_rule)"], + }), + binary = ":test_bin", + data = [ + ":alias_target", + ":single_rule", + ":source_file.txt", + "//args_location_expansion/pkg:multiple_rules", + "@bazel_tools//tools/bash/runfiles", + ] + select({ + "@platforms//os:linux": ["//args_location_expansion/pkg:linux_only_rule"], + "//conditions:default": ["//args_location_expansion/pkg:generic_rule"], + }) + select({ + "@platforms//os:windows": [], + "//conditions:default": ["//args_location_expansion/pkg:special_09!%-@^_\"#$&'(*-+,;<=>?[]{|}~/._characters_rule"], + }), + env = select({ + "@platforms//os:windows": {"NUM_RUNFILES": "10"}, + "//conditions:default": {"NUM_RUNFILES": "11"}, + }), +) + +sh_binary( + name = "test_bin", + srcs = ["test_bin.sh"], + data = ["@bazel_tools//tools/bash/runfiles"], + visibility = ["//visibility:public"], +) diff --git a/examples/args_location_expansion/doubly_transitioned_test.bzl b/examples/args_location_expansion/doubly_transitioned_test.bzl new file mode 100644 index 0000000..4a9d374 --- /dev/null +++ b/examples/args_location_expansion/doubly_transitioned_test.bzl @@ -0,0 +1,6 @@ +load("@with_cfg.bzl", "with_cfg") +load("//args_location_expansion/rules:defs.bzl", "transitioned_test") + +_builder = with_cfg(transitioned_test) +_builder.set(Label("//args_location_expansion/rules:with_cfg_setting"), "with_cfg") +doubly_transitioned_test, _doubly_transitioned_test_ = _builder.build() diff --git a/examples/args_location_expansion/pkg/BUILD.bazel b/examples/args_location_expansion/pkg/BUILD.bazel new file mode 100644 index 0000000..16bdafb --- /dev/null +++ b/examples/args_location_expansion/pkg/BUILD.bazel @@ -0,0 +1,46 @@ +load("//args_location_expansion/rules:defs.bzl", "write_settings_rule") + +write_settings_rule( + name = "some_rule", + tags = ["manual"], +) + +write_settings_rule( + name = "other_rule", + tags = ["manual"], +) + +filegroup( + name = "multiple_rules", + srcs = [ + ":other_rule", + ":some_rule", + ], + tags = ["manual"], + visibility = ["//visibility:public"], +) + +write_settings_rule( + name = "aliased_rule", + tags = ["manual"], + visibility = ["//visibility:public"], +) + +write_settings_rule( + # Omits ')' as it doesn't seem to be supported with location expansion. + name = "special_09!%-@^_\"#$&'(*-+,;<=>?[]{|}~/._characters_rule", + tags = ["manual"], + visibility = ["//visibility:public"], +) + +write_settings_rule( + name = "linux_only_rule", + tags = ["manual"], + visibility = ["//visibility:public"], +) + +write_settings_rule( + name = "generic_rule", + tags = ["manual"], + visibility = ["//visibility:public"], +) diff --git a/examples/args_location_expansion/rules/BUILD.bazel b/examples/args_location_expansion/rules/BUILD.bazel new file mode 100644 index 0000000..d928598 --- /dev/null +++ b/examples/args_location_expansion/rules/BUILD.bazel @@ -0,0 +1,13 @@ +load("@bazel_skylib//rules:common_settings.bzl", "string_setting") + +string_setting( + name = "rule_setting", + build_setting_default = "unset", + visibility = ["//visibility:public"], +) + +string_setting( + name = "with_cfg_setting", + build_setting_default = "unset", + visibility = ["//visibility:public"], +) diff --git a/examples/args_location_expansion/rules/defs.bzl b/examples/args_location_expansion/rules/defs.bzl new file mode 100644 index 0000000..034b104 --- /dev/null +++ b/examples/args_location_expansion/rules/defs.bzl @@ -0,0 +1,71 @@ +load("@bazel_skylib//rules:common_settings.bzl", "BuildSettingInfo") + +def _write_settings_rule_impl(ctx): + # type: (ctx) -> None + rule_setting = ctx.attr._rule_setting[BuildSettingInfo].value + if rule_setting == "unset": + fail("rule_setting is unset") + with_cfg_setting = ctx.attr._with_cfg_setting[BuildSettingInfo].value + if with_cfg_setting == "unset": + fail("with_cfg_setting is unset") + + out = ctx.actions.declare_file(ctx.label.name + "_" + rule_setting[0] + "_" + with_cfg_setting[0]) + ctx.actions.write(out, "name:{},rule_setting:{},with_cfg_setting:{}\n".format(out.basename, rule_setting, with_cfg_setting)) + return [DefaultInfo(files = depset([out]))] + +write_settings_rule = rule( + implementation = _write_settings_rule_impl, + attrs = { + "_rule_setting": attr.label(default = ":rule_setting"), + "_with_cfg_setting": attr.label(default = ":with_cfg_setting"), + }, +) + +def _data_transition_impl(_settings, _attr): + return { + "//args_location_expansion/rules:rule_setting": "rule", + } + +_data_transition = transition( + implementation = _data_transition_impl, + inputs = ["//args_location_expansion/rules:rule_setting"], + outputs = ["//args_location_expansion/rules:rule_setting"], +) + +def _transitioned_test_impl(ctx): + # type: (ctx) -> None + executable = ctx.actions.declare_file(ctx.label.name) + ctx.actions.symlink(output = executable, target_file = ctx.executable.binary) + runfiles = ctx.runfiles() + runfiles = runfiles.merge(ctx.attr.binary[DefaultInfo].default_runfiles) + for target in ctx.attr.data: + runfiles = runfiles.merge(ctx.runfiles(transitive_files = target[DefaultInfo].files)) + runfiles = runfiles.merge(target[DefaultInfo].data_runfiles) + return [ + DefaultInfo( + executable = executable, + runfiles = runfiles, + ), + RunEnvironmentInfo( + environment = ctx.attr.env, + ), + ] + +transitioned_test = rule( + implementation = _transitioned_test_impl, + attrs = { + "binary": attr.label( + cfg = "target", + executable = True, + ), + "data": attr.label_list( + cfg = _data_transition, + allow_files = True, + ), + "env": attr.string_dict(), + "_allowlist_function_transition": attr.label( + default = "@bazel_tools//tools/allowlists/function_transition_allowlist", + ), + }, + test = True, +) diff --git a/examples/args_location_expansion/source_file.txt b/examples/args_location_expansion/source_file.txt new file mode 100644 index 0000000..a9c2200 --- /dev/null +++ b/examples/args_location_expansion/source_file.txt @@ -0,0 +1 @@ +name:source_file.txt,rule_setting:rule,with_cfg_setting:with_cfg diff --git a/examples/args_location_expansion/test_bin.sh b/examples/args_location_expansion/test_bin.sh new file mode 100755 index 0000000..b64b353 --- /dev/null +++ b/examples/args_location_expansion/test_bin.sh @@ -0,0 +1,31 @@ +#!/usr/bin/env bash + +# --- begin runfiles.bash initialization v3 --- +# Copy-pasted from the Bazel Bash runfiles library v3. +set -uo pipefail; set +e; f=bazel_tools/tools/bash/runfiles/runfiles.bash +# shellcheck disable=SC1090 +source "${RUNFILES_DIR:-/dev/null}/$f" 2>/dev/null || \ + source "$(grep -sm1 "^$f " "${RUNFILES_MANIFEST_FILE:-/dev/null}" | cut -f2- -d' ')" 2>/dev/null || \ + source "$0.runfiles/$f" 2>/dev/null || \ + source "$(grep -sm1 "^$f " "$0.runfiles_manifest" | cut -f2- -d' ')" 2>/dev/null || \ + source "$(grep -sm1 "^$f " "$0.exe.runfiles_manifest" | cut -f2- -d' ')" 2>/dev/null || \ + { echo>&2 "ERROR: cannot find $f"; exit 1; }; f=; set -e +# --- end runfiles.bash initialization v3 --- + +function check_runfile() { + real_path=$(rlocation "$1" || { >&2 echo "$1 not found"; exit 1; }) + basename=$(basename "$1") + want="name:$basename,rule_setting:rule,with_cfg_setting:with_cfg" + if [[ $1 != bazel_tools/* && "$(cat "$real_path")" != "$want" ]]; then + echo "Runfile content mismatch in $1: got '$(cat "$real_path")', want '$want'" + exit 1 + fi +} + +if [ "$#" -ne "$NUM_RUNFILES" ]; then + echo "Unexpected number of arguments: $#, want $NUM_RUNFILES" + exit 1 +fi +for arg in "$@"; do + check_runfile "$arg" +done diff --git a/with_cfg/private/BUILD.bazel b/with_cfg/private/BUILD.bazel index 5e07905..c556d34 100644 --- a/with_cfg/private/BUILD.bazel +++ b/with_cfg/private/BUILD.bazel @@ -20,6 +20,15 @@ config_setting( }, ) +bzl_library( + name = "args", + srcs = ["args.bzl"], + visibility = ["//with_cfg:__subpackages__"], + deps = [ + ":rewrite", + ], +) + bzl_library( name = "builder", srcs = ["builder.bzl"], @@ -136,6 +145,7 @@ bzl_library( srcs = ["wrapper.bzl"], visibility = ["//with_cfg:__subpackages__"], deps = [ + ":args", ":rewrite", ":select", ":setting", diff --git a/with_cfg/private/args.bzl b/with_cfg/private/args.bzl new file mode 100644 index 0000000..3ea456d --- /dev/null +++ b/with_cfg/private/args.bzl @@ -0,0 +1,164 @@ +""" +Logic for setting "args" on a transitioned rule with location expansion. + +Compared to "env" and "env_inherit", which are backed by a provider, "args" +handling in Bazel is still based on "magic" handling in the core and thus much +more difficult to generically forward. This results in the following +complexities handled by the functions in this file: + +1. The way locations expand in "args" depends on the configuration of the + underlying target, which can be affected by transitions both by with_cfg.bzl + and the rule itself. We thus need to obtain the expanded locations from the + rule context instead of e.g. rewriting labels to helper targets during the + loading phase, which would miss the effect of transitions applied by the rule + itself. + We use a non-recursing aspect on the original rule to collect the files + corresponding to labels mentioned in location expansion in "args" and provide + them to the frontend rule via output groups named after the user-provided + label strings. As a consequence, we also have to set the "args" attribute on + the underlying target so that the aspect can see it - aspect attributes are + limited to integers and string enums. +2. "args" can only be set at load time via the magic attribute and the only way + to get analysis time information into it is via the hard-coded location + expansion applied to it in + https://github.com/bazelbuild/bazel/blob/9425e365ebf921d4286fcf159b429e38f6b0a48f/src/main/java/com/google/devtools/build/lib/analysis/RunfilesSupport.java#L525 + We create a helper `filegroup` target for each unique label string mentioned + in a location expansion expression in "args", add it to the (otherwise + unused) `data` attribute of the frontend and rewrite the label strings to + instead point to the `filegroup` targets. +3. `native.package_relative_label` is not available in the analysis phase and + rule or aspect implementation logic can't see the labels of alias targets, + so the only way to reliably match user-provided labels to the files they + represent is to query `ctx.expand_location("$(execpaths ...)")` and manually + collect the files with the given paths. +""" + +load(":rewrite.bzl", "rewrite_locations_in_attr", "rewrite_locations_in_single_value") + +visibility("private") + +def rewrite_args(name, args, make_filegroup): + # type: (string, list[string], Any) -> tuple[list[string], list[Label]] + seen_labels = {} + filegroup_labels = [] + + def rewrite_label(label_string): + # type: (string) -> string + escaped_label = _escape_label(label_string) + filegroup_name = name + "__args__" + escaped_label + if label_string not in seen_labels: + seen_labels[label_string] = None + make_filegroup( + name = filegroup_name, + srcs = [":" + name], + output_group = _OUTPUT_GROUPS_PREFIX + label_string, + ) + filegroup_labels.append(native.package_relative_label(filegroup_name)) + return ":" + filegroup_name + + return rewrite_locations_in_attr(args, rewrite_label), filegroup_labels + +def _args_aspect_impl(target, ctx): + # type: (Target, ctx) -> list[Provider] + if not hasattr(ctx.rule.attr, "args"): + return [] + + execpaths_expansion_to_files = {} + targets = {} + + # https://github.com/bazelbuild/bazel/blob/af8deb85cbc627a507605e67aa71e829f7db630f/src/main/java/com/google/devtools/build/lib/analysis/LocationExpander.java#L446-L516 + for attr in _LOCATION_EXPANSION_ARGS: + deps = getattr(ctx.rule.attr, attr, []) + if type(deps) != type([]): + continue + for target in deps: + default_info = target[DefaultInfo] + if default_info.files_to_run and default_info.files_to_run.executable: + files = depset([default_info.files_to_run.executable]) + else: + files = default_info.files + + # https://github.com/bazelbuild/bazel/blob/af8deb85cbc627a507605e67aa71e829f7db630f/src/main/java/com/google/devtools/build/lib/analysis/LocationExpander.java#L338-L348 + sorted_paths = sorted([_callable_path_string(f) for f in files.to_list()]) + execpaths_expansion = " ".join([_shell_escape(p) for p in sorted_paths]) + execpaths_expansion_to_files[execpaths_expansion] = files + targets[target] = None + + labels = {} + + def collect_label(label): + labels[label] = None + return label + + for arg in ctx.rule.attr.args: + rewrite_locations_in_single_value(arg, collect_label) + + labels_to_files = {} + for label in labels.keys(): + execpaths_expansion = ctx.expand_location("$(execpaths {})".format(label), targets = targets.keys()) + labels_to_files[label] = execpaths_expansion_to_files[execpaths_expansion] + + return [ + OutputGroupInfo(**{_OUTPUT_GROUPS_PREFIX + label: f for label, f in labels_to_files.items()}), + ] + +args_aspect = aspect( + implementation = _args_aspect_impl, +) + +_LOCATION_EXPANSION_ARGS = [ + # keep sorted + "data", + "deps", + "implementation_deps", + "srcs", + "tools", +] + +_OUTPUT_GROUPS_PREFIX = "_with_cfg.bzl_args__" + +def _callable_path_string(file): + # type: (File) -> str + # https://github.com/bazelbuild/bazel/blob/af8deb85cbc627a507605e67aa71e829f7db630f/src/main/java/com/google/devtools/build/lib/vfs/PathFragment.java#L625-L631 + path = file.path + if not path: + return "." + if not "/" in path: + return "./" + path + return path + +# Based on +# https://github.com/bazelbuild/bazel/blob/af8deb85cbc627a507605e67aa71e829f7db630f/src/main/starlark/builtins_bzl/common/java/java_helper.bzl#L365-L386 +# Modified to handle ~ in the same way as +# https://github.com/bazelbuild/bazel/blob/30f6c8238f39c4a396b3cb56a98c1a2e79d10bb9/src/main/java/com/google/devtools/build/lib/util/ShellEscaper.java#L106-L108 +def _shell_escape(s): # type: (string) -> string + """Shell-escape a string + + Quotes a word so that it can be used, without further quoting, as an argument + (or part of an argument) in a shell command. + + Args: + s: (str) the string to escape + + Returns: + (str) the shell-escaped string + """ + if not s: + # Empty string is a special case: needs to be quoted to ensure that it + # gets treated as a separate argument. + return "''" + must_escape = s.startswith("~") + for c in s.elems(): + # We do this positively so as to be sure we don't inadvertently forget + # any unsafe characters. + if not c.isalnum() and c not in "@%-_+:,./~": + must_escape = True + break + if must_escape: + return "'" + s.replace("'", "'\\''") + "'" + else: + return s + +def _escape_label(label_string): # type: (string) -> string + # https://github.com/bazelbuild/bazel/blob/af8deb85cbc627a507605e67aa71e829f7db630f/src/main/java/com/google/devtools/build/lib/actions/Actions.java#L375-L381 + return label_string.replace("_", "_U").replace("/", "_S").replace("\\", "_B").replace(":", "_C").replace("@", "_A") diff --git a/with_cfg/private/frontend.bzl b/with_cfg/private/frontend.bzl index 0ba98b8..c5fb36f 100644 --- a/with_cfg/private/frontend.bzl +++ b/with_cfg/private/frontend.bzl @@ -69,6 +69,7 @@ def _clean_run_environment_info(run_environment_info): ) _frontend_attrs = { + "data": attr.label_list(allow_files = True), "env": attr.string_dict(), "env_inherit": attr.string_list(), # This attribute name is internal only, so it can only help to choose a diff --git a/with_cfg/private/rewrite.bzl b/with_cfg/private/rewrite.bzl index e7a9806..ff73e5f 100644 --- a/with_cfg/private/rewrite.bzl +++ b/with_cfg/private/rewrite.bzl @@ -22,7 +22,7 @@ def _rewrite_locations_in_value(value, label_rewriter): return value if is_dict(value): return { - _rewrite_locations_in_single_value(k, label_rewriter): _rewrite_locations_in_list_or_single_value(v, label_rewriter) + rewrite_locations_in_single_value(k, label_rewriter): _rewrite_locations_in_list_or_single_value(v, label_rewriter) for k, v in value.items() } return _rewrite_locations_in_list_or_single_value(value, label_rewriter) @@ -31,13 +31,13 @@ def _rewrite_locations_in_list_or_single_value(value, label_rewriter): if not value: return value if is_list(value): - return [_rewrite_locations_in_single_value(v, label_rewriter) for v in value] - return _rewrite_locations_in_single_value(value, label_rewriter) + return [rewrite_locations_in_single_value(v, label_rewriter) for v in value] + return rewrite_locations_in_single_value(value, label_rewriter) # Based on: # https://github.com/bazelbuild/bazel/blob/9bf8f396db5c8b204c61b34638ca15ece0328fc0/src/main/starlark/builtins_bzl/common/cc/cc_helper.bzl#L777C1-L830C27 # SPDX: Apache-2.0 -def _rewrite_locations_in_single_value(expression, label_rewriter): +def rewrite_locations_in_single_value(expression, label_rewriter): if not is_string(expression): return expression if "$(" not in expression: diff --git a/with_cfg/private/transitioning_alias.bzl b/with_cfg/private/transitioning_alias.bzl index d1979de..9edc4cc 100644 --- a/with_cfg/private/transitioning_alias.bzl +++ b/with_cfg/private/transitioning_alias.bzl @@ -1,3 +1,4 @@ +load(":args.bzl", "args_aspect") load(":providers.bzl", "FrontendInfo", "OriginalSettingsInfo") load(":setting.bzl", "get_attr_type", "validate_and_get_attr_name") @@ -30,6 +31,7 @@ def make_transitioning_alias(*, providers, transition, values, original_settings "exports": attr.label( allow_files = True, cfg = transition, + aspects = [args_aspect], ), "_allowlist_function_transition": attr.label( default = "@bazel_tools//tools/allowlists/function_transition_allowlist", diff --git a/with_cfg/private/wrapper.bzl b/with_cfg/private/wrapper.bzl index 33a10b2..73fa461 100644 --- a/with_cfg/private/wrapper.bzl +++ b/with_cfg/private/wrapper.bzl @@ -1,3 +1,4 @@ +load(":args.bzl", "rewrite_args") load(":rewrite.bzl", "make_label_rewriter", "rewrite_locations_in_attr") load(":select.bzl", "map_attr") load(":setting.bzl", "validate_and_get_attr_name") @@ -40,22 +41,13 @@ _COMMON_ATTRS = [ "toolchains", ] -# Attributes common to all executable and test rules. -# These attributes are applied to the original target and the frontend if the original target is -# executable. -# env and env_inherit are covered by the forwarded RunEnvironmentInfo instead. -_EXECUTABLE_ATTRS = [ - # keep sorted - "args", -] - # Attributes common to all test rules. # These attributes are applied to the original target and the frontend if the original target is a # test. +# args is treated specially. # env and env_inherit are covered by the forwarded RunEnvironmentInfo instead. _TEST_ATTRS = [ # keep sorted - "args", "flaky", "local", "shard_count", @@ -73,6 +65,14 @@ def _wrapper( values, original_settings_label, attrs_to_reset): + dirname, separator, basename = name.rpartition("/") + original_name = "{dirname}{separator}{basename}_/{basename}".format( + dirname = dirname, + separator = separator, + basename = basename, + ) + alias_name = name + "_with_cfg" + tags = kwargs.pop("tags", None) if not tags: tags_with_manual = ["manual"] @@ -101,12 +101,6 @@ def _wrapper( for attr in _TEST_ATTRS if attr in kwargs } - elif rule_info.executable: - extra_attrs = { - attr: kwargs.pop(attr) - for attr in _EXECUTABLE_ATTRS - if attr in kwargs - } else: extra_attrs = {} @@ -118,13 +112,22 @@ def _wrapper( if "env_inherit" in kwargs: extra_attrs["env_inherit"] = kwargs.pop("env_inherit") - dirname, separator, basename = name.rpartition("/") - original_name = "{dirname}{separator}{basename}_/{basename}".format( - dirname = dirname, - separator = separator, - basename = basename, - ) - alias_name = name + "_with_cfg" + frontend_attrs = {} + if "args" in kwargs: + # Leave the args attribute in place on the original rule so that they can be read by the + # args_aspect attached to the exports attribute of the transitioning_alias. + frontend_attrs["args"], frontend_attrs["data"] = rewrite_args( + alias_name, + kwargs["args"], + lambda *, name, srcs, output_group: native.filegroup( + name = name, + srcs = srcs, + output_group = output_group, + testonly = common_attrs.get("testonly", False), + tags = ["manual"], + visibility = ["//visibility:private"], + ), + ) processed_kwargs = _process_attrs_for_reset( attrs = kwargs, @@ -164,7 +167,7 @@ def _wrapper( exports = ":" + alias_name, tags = tags, visibility = visibility, - **(common_attrs | extra_attrs) + **(common_attrs | extra_attrs | frontend_attrs) ) for implicit_target in rule_info.implicit_targets: