diff --git a/CHANGELOG.md b/CHANGELOG.md index 01a21b3a05..32e5836cf2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,11 +6,11 @@ ##### Enhancements -- None. +- Added a `--bazel-query` option to override the default Bazel top-level target query. ##### Bug Fixes -- None. +- Follow embedded bundle and plugin edges transitively in the generated Bazel scan rule so custom Bazel queries can avoid building incorrectly transitioned targets while still analyzing extension- and plugin-reachable code. ## 3.7.4 (2026-04-26) diff --git a/README.md b/README.md index 17d3ad87a8..b358b99645 100644 --- a/README.md +++ b/README.md @@ -458,7 +458,9 @@ By default, Periphery looks for the index store at `.build/debug/index/store`. T bazel run @periphery -- scan --bazel ``` -The `--bazel` option enables Bazel mode, which provides seamless integration with your project. It works by querying your project to identify all top-level targets, generating a hidden implementation of the [scan](https://github.com/peripheryapp/periphery/blob/master/bazel/rules.bzl) rule, and then invoking `bazel run`. You can filter the top-level targets with the `--bazel-filter ` option, where `` will be passed as the first argument to Bazel's [filter](https://bazel.build/query/language#filter) operator. The generated query can be seen in the console with the `--verbose` option. +The `--bazel` option enables Bazel mode, which provides seamless integration with your project. It works by querying your project to identify all top-level targets, generating a hidden implementation of the [scan](https://github.com/peripheryapp/periphery/blob/master/bazel/rules.bzl) rule, and then invoking `bazel run`. You can filter the default top-level target query with the `--bazel-filter ` option, where `` will be passed as the first argument to Bazel's [filter](https://bazel.build/query/language#filter) operator. You can also override the generated query entirely with `--bazel-query `, which is useful when you need to exclude targets such as ones tagged `manual`, or avoid building targets that use an incorrect transition when built directly. The generated query can be seen in the console with the `--verbose` option. + +Periphery's generated scan rule follows embedded bundle and plugin edges transitively, so you can root the scan in application targets while still analyzing code that is only reachable through extensions, app clips, watch applications, or Swift compiler plugins. > [!TIP] > By default, Periphery passes `--check_visibility=false` to `bazel run` to simplify integration, since the generated scan target references your project's targets which may not otherwise be visible. However, disabling visibility checking can invalidate Bazel's analysis cache, resulting in slower subsequent builds. diff --git a/Sources/Configuration/Configuration.swift b/Sources/Configuration/Configuration.swift index 88aa090874..4afc834584 100644 --- a/Sources/Configuration/Configuration.swift +++ b/Sources/Configuration/Configuration.swift @@ -146,6 +146,9 @@ public final class Configuration { @Setting(key: "bazel_filter", defaultValue: nil) public var bazelFilter: String? + @Setting(key: "bazel_query", defaultValue: nil) + public var bazelQuery: String? + @Setting(key: "bazel_index_store", defaultValue: nil) public var bazelIndexStore: FilePath? @@ -224,7 +227,7 @@ public final class Configuration { $disableUpdateCheck, $strict, $indexStorePath, $skipBuild, $skipSchemesValidation, $cleanBuild, $buildArguments, $xcodeListArguments, $relativeResults, $jsonPackageManifestPath, $retainCodableProperties, $retainEncodableProperties, $baseline, $writeBaseline, - $writeResults, $genericProjectConfig, $bazel, $bazelFilter, $bazelIndexStore, $bazelCheckVisibility, + $writeResults, $genericProjectConfig, $bazel, $bazelFilter, $bazelQuery, $bazelIndexStore, $bazelCheckVisibility, ] private func buildFilenameMatchers(with patterns: [String]) -> [FilenameMatcher] { diff --git a/Sources/Frontend/Commands/ScanCommand.swift b/Sources/Frontend/Commands/ScanCommand.swift index c6bb3a63e3..990db06824 100644 --- a/Sources/Frontend/Commands/ScanCommand.swift +++ b/Sources/Frontend/Commands/ScanCommand.swift @@ -156,6 +156,9 @@ struct ScanCommand: ParsableCommand { @Option(help: "Filter pattern applied to the Bazel top-level targets query") var bazelFilter: String? + @Option(help: "Query expression used for the Bazel top-level targets query. Overrides the default query and bypasses bazelFilter.") + var bazelQuery: String? + @Option(help: "Path to a global index store populated by Bazel. If provided, will be used instead of individual module stores.") var bazelIndexStore: FilePath? @@ -221,6 +224,7 @@ struct ScanCommand: ParsableCommand { configuration.apply(\.$genericProjectConfig, genericProjectConfig) configuration.apply(\.$bazel, bazel) configuration.apply(\.$bazelFilter, bazelFilter) + configuration.apply(\.$bazelQuery, bazelQuery) configuration.apply(\.$bazelIndexStore, bazelIndexStore) configuration.apply(\.$bazelCheckVisibility, bazelCheckVisibility) diff --git a/Sources/ProjectDrivers/BazelProjectDriver.swift b/Sources/ProjectDrivers/BazelProjectDriver.swift index 4157d5d0bc..531be94327 100644 --- a/Sources/ProjectDrivers/BazelProjectDriver.swift +++ b/Sources/ProjectDrivers/BazelProjectDriver.swift @@ -136,6 +136,10 @@ public final class BazelProjectDriver: ProjectDriver { } private var query: String { + if let bazelQuery = configuration.bazelQuery { + return bazelQuery + } + let kinds = Self.topLevelKinds.joined(separator: "|") let query = "filter('^//.*', kind('(\(kinds)) rule', deps(//...)))" diff --git a/bazel/internal/scan/scan.bzl b/bazel/internal/scan/scan.bzl index e46cce083d..da231fd9fe 100644 --- a/bazel/internal/scan/scan.bzl +++ b/bazel/internal/scan/scan.bzl @@ -19,6 +19,26 @@ PeripheryInfo = provider( }, ) +_TRANSITIVE_ATTRS = [ + "app_clips", + "deps", + "extension", + "extensions", + "plugins", + "swift_target", + "watch_application", +] + +def _periphery_info_providers(rule_attr): + providers = [] + for attr in _TRANSITIVE_ATTRS: + value = getattr(rule_attr, attr, None) + values = value if type(value) == "list" else ([value] if value else []) + for target in values: + if PeripheryInfo in target: + providers.append(target[PeripheryInfo]) + return providers + def _force_indexstore_impl(settings, _attr): return { "//command_line_option:features": settings["//command_line_option:features"] + [ @@ -90,12 +110,7 @@ def _scan_inputs_aspect_impl(target, ctx): elif ".xcmappingmodel" in resource.path: xcmappingmodels.append(resource) - deps = getattr(ctx.rule.attr, "deps", []) - providers = [dep[PeripheryInfo] for dep in deps] - swift_target = getattr(ctx.rule.attr, "swift_target", None) - - if swift_target: - providers.append(swift_target[PeripheryInfo]) + providers = _periphery_info_providers(ctx.rule.attr) swift_srcs_depset = depset( swift_srcs, @@ -206,5 +221,5 @@ def scan_impl(ctx): scan_inputs_aspect = aspect( _scan_inputs_aspect_impl, - attr_aspects = ["deps", "swift_target"], + attr_aspects = _TRANSITIVE_ATTRS, )