diff --git a/Sources/SourceKitBazelBSP/RequestHandlers/BuildTargets/BazelTargetDiscoverer.swift b/Sources/SourceKitBazelBSP/RequestHandlers/BuildTargets/BazelTargetDiscoverer.swift index 5c94f666..bcbf2d09 100644 --- a/Sources/SourceKitBazelBSP/RequestHandlers/BuildTargets/BazelTargetDiscoverer.swift +++ b/Sources/SourceKitBazelBSP/RequestHandlers/BuildTargets/BazelTargetDiscoverer.swift @@ -1,11 +1,15 @@ import Foundation public enum BazelTargetDiscovererError: LocalizedError, Equatable { + case noRulesProvided case noTargetsDiscovered + case noLocationsProvided public var errorDescription: String? { switch self { + case .noRulesProvided: return "No rules provided!" case .noTargetsDiscovered: return "No targets discovered!" + case .noLocationsProvided: return "No locations provided!" } } } @@ -23,14 +27,22 @@ public enum BazelTargetDiscoverer { public static func discoverTargets( for rules: [TopLevelRuleType] = TopLevelRuleType.allCases, bazelWrapper: String = "bazel", + locations: [String] = [], commandRunner: CommandRunner? = nil ) throws -> [String] { - let commandRunner = commandRunner ?? ShellCommandRunner() + guard !rules.isEmpty else { + throw BazelTargetDiscovererError.noRulesProvided + } - let query = rules.map { - "kind(\($0.rawValue), ...)" + guard !locations.isEmpty else { + throw BazelTargetDiscovererError.noLocationsProvided } - .joined(separator: " + ") + + let commandRunner = commandRunner ?? ShellCommandRunner() + + let kindsQuery = "\"" + rules.map { $0.rawValue }.joined(separator: "|") + "\"" + let locationsQuery = locations.joined(separator: " union ") + let query = "kind(\(kindsQuery), \(locationsQuery))" let cmd = "\(bazelWrapper) query '\(query)' --output label" diff --git a/Sources/SourceKitBazelBSP/Server/BaseServerConfig.swift b/Sources/SourceKitBazelBSP/Server/BaseServerConfig.swift index e29c1cab..da3cbc89 100644 --- a/Sources/SourceKitBazelBSP/Server/BaseServerConfig.swift +++ b/Sources/SourceKitBazelBSP/Server/BaseServerConfig.swift @@ -49,7 +49,8 @@ package struct BaseServerConfig: Equatable { // We need to post-process the target list provided by the user // because the queries will always return the "full" label. // e.g: "//foo/bar" -> "//foo/bar:bar" - self.targets = targets.map { $0.toFullLabel() } + // We need to also de-dupe them if the user passed wildcards in Serve.swift. + self.targets = Set(targets.map { $0.toFullLabel() }).sorted() } } diff --git a/Sources/sourcekit-bazel-bsp/Commands/Serve.swift b/Sources/sourcekit-bazel-bsp/Commands/Serve.swift index 06e226e7..ae84c2cf 100644 --- a/Sources/sourcekit-bazel-bsp/Commands/Serve.swift +++ b/Sources/sourcekit-bazel-bsp/Commands/Serve.swift @@ -31,7 +31,7 @@ struct Serve: ParsableCommand { @Option( parsing: .singleValue, help: - "The *top level* Bazel application or test target that this should serve a BSP for. Can be specified multiple times. It's best to keep this list small if possible for performance reasons. If not specified, the server will try to discover top-level targets automatically." + "The *top level* Bazel application or test target that this should serve a BSP for. Can be specified multiple times. Wildcards are supported (e.g. //foo/...). It's best to keep this list small if possible for performance reasons. If not specified, the server will try to discover top-level targets automatically." ) var target: [String] = [] @@ -69,30 +69,33 @@ struct Serve: ParsableCommand { let targets: [String] if !target.isEmpty { - targets = target - } else { - // If the user provided no specific targets, try to discover them - // in the workspace. - logger.warning( - "No targets specified (--target)! Will now try to discover them. This can cause the BSP to perform poorly if we find too many targets. Prefer using --target explicitly if possible." - ) - do { - let rulesToUse: [TopLevelRuleType] - if !topLevelRuleToDiscover.isEmpty { - rulesToUse = topLevelRuleToDiscover + var expandedTargets = [String]() + var targetsToExpand = [String]() + for _target in target { + if _target.hasSuffix("...") { + targetsToExpand.append(_target) } else { - rulesToUse = TopLevelRuleType.allCases + expandedTargets.append(_target) } - targets = try BazelTargetDiscoverer.discoverTargets( - for: rulesToUse, - bazelWrapper: bazelWrapper - ) - } catch { - logger.error( - "Failed to initialize server: Could not discover targets. Please check your Bazel configuration or try specifying targets explicitly with `--target` instead. Failure: \(error)" + } + if !targetsToExpand.isEmpty { + expandedTargets.append( + contentsOf: try expandWildcardTargets( + bazelWrapper: bazelWrapper, + targets: targetsToExpand, + topLevelRuleToDiscover: topLevelRuleToDiscover + ) ) - throw error } + targets = expandedTargets + } else { + // If the user provided no specific targets, try to discover them + // in the workspace. + targets = try expandWildcardTargets( + bazelWrapper: bazelWrapper, + targets: ["..."], + topLevelRuleToDiscover: topLevelRuleToDiscover + ) } let config = BaseServerConfig( @@ -103,7 +106,39 @@ struct Serve: ParsableCommand { compileTopLevel: compileTopLevel, indexBuildBatchSize: indexBuildBatchSize ) + + logger.debug("Initializing BSP with targets: \(targets)") + let server = SourceKitBazelBSPServer(baseConfig: config) server.run() } + + private func expandWildcardTargets( + bazelWrapper: String, + targets: [String], + topLevelRuleToDiscover: [TopLevelRuleType] + ) throws -> [String] { + let targetsString = targets.joined(separator: ", ") + logger.warning( + "Will expand wildcard targets (\(targetsString, privacy: .public)). This can cause the BSP to perform poorly if we find too many targets. Prefer passing explicit targets via --targets if possible." + ) + let rulesToUse: [TopLevelRuleType] + if !topLevelRuleToDiscover.isEmpty { + rulesToUse = topLevelRuleToDiscover + } else { + rulesToUse = TopLevelRuleType.allCases + } + do { + return try BazelTargetDiscoverer.discoverTargets( + for: rulesToUse, + bazelWrapper: bazelWrapper, + locations: targets + ) + } catch { + logger.error( + "Failed to initialize server: Could not expand wildcard targets (\(targetsString, privacy: .public)). Please check your Bazel configuration or try specifying targets explicitly with `--target` instead. Failure: \(error, privacy: .public)" + ) + throw error + } + } } diff --git a/Tests/SourceKitBazelBSPTests/BazelTargetDiscovererTests.swift b/Tests/SourceKitBazelBSPTests/BazelTargetDiscovererTests.swift index 8de24f97..51842213 100644 --- a/Tests/SourceKitBazelBSPTests/BazelTargetDiscovererTests.swift +++ b/Tests/SourceKitBazelBSPTests/BazelTargetDiscovererTests.swift @@ -23,52 +23,77 @@ import Testing @testable import SourceKitBazelBSP struct BazelTargetDiscovererTests { - @Test("Discovers single rule type with multiple targets") + @Test func discoverSingleRuleTypeWithMultipleTargets() throws { let commandRunner = CommandRunnerFake() commandRunner.setResponse( - for: "fakeBazel query 'kind(ios_application, ...)' --output label", + for: "fakeBazel query 'kind(\"ios_application\", ...)' --output label", response: "//Example/HelloWorld:HelloWorld\n//Example/AnotherApp:AnotherApp\n" ) let targets = try BazelTargetDiscoverer.discoverTargets( for: [.iosApplication], bazelWrapper: "fakeBazel", - commandRunner: commandRunner + locations: ["..."], + commandRunner: commandRunner, ) #expect(targets == ["//Example/HelloWorld:HelloWorld", "//Example/AnotherApp:AnotherApp"]) #expect(commandRunner.commands.count == 1) - #expect(commandRunner.commands[0].command == "fakeBazel query 'kind(ios_application, ...)' --output label") + #expect(commandRunner.commands[0].command == "fakeBazel query 'kind(\"ios_application\", ...)' --output label") } - @Test("Discovers multiple rule types") + @Test func discoverMultipleRuleTypes() throws { let commandRunner = CommandRunnerFake() commandRunner.setResponse( - for: "fakeBazel query 'kind(ios_application, ...) + kind(ios_unit_test, ...)' --output label", + for: "fakeBazel query 'kind(\"ios_application|ios_unit_test\", ...)' --output label", response: "//Example/HelloWorld:HelloWorld\n//Example/HelloWorldTests:HelloWorldTests\n" ) let targets = try BazelTargetDiscoverer.discoverTargets( for: [.iosApplication, .iosUnitTest], bazelWrapper: "fakeBazel", - commandRunner: commandRunner + locations: ["..."], + commandRunner: commandRunner, ) #expect(targets == ["//Example/HelloWorld:HelloWorld", "//Example/HelloWorldTests:HelloWorldTests"]) #expect(commandRunner.commands.count == 1) #expect( commandRunner.commands[0].command - == "fakeBazel query 'kind(ios_application, ...) + kind(ios_unit_test, ...)' --output label" + == "fakeBazel query 'kind(\"ios_application|ios_unit_test\", ...)' --output label" ) } - @Test("Throws error when no targets found") + @Test + func discoverAtMultipleLocations() throws { + let commandRunner = CommandRunnerFake() + commandRunner.setResponse( + for: "fakeBazel query 'kind(\"ios_application\", //A/... union //B/...)' --output label", + response: "//Example/HelloWorld:HelloWorld\n//Example/AnotherApp:AnotherApp\n" + ) + + let targets = try BazelTargetDiscoverer.discoverTargets( + for: [.iosApplication], + bazelWrapper: "fakeBazel", + locations: ["//A/...", "//B/..."], + commandRunner: commandRunner, + ) + + #expect(targets == ["//Example/HelloWorld:HelloWorld", "//Example/AnotherApp:AnotherApp"]) + #expect(commandRunner.commands.count == 1) + #expect( + commandRunner.commands[0].command + == "fakeBazel query 'kind(\"ios_application\", //A/... union //B/...)' --output label" + ) + } + + @Test func throwsErrorWhenNoTargetsFound() throws { let commandRunner = CommandRunnerFake() commandRunner.setResponse( - for: "fakeBazel query 'kind(ios_application, ...)' --output label", + for: "fakeBazel query 'kind(\"ios_application\", ...)' --output label", response: "\n \n" ) @@ -76,7 +101,30 @@ struct BazelTargetDiscovererTests { try BazelTargetDiscoverer.discoverTargets( for: [.iosApplication], bazelWrapper: "fakeBazel", - commandRunner: commandRunner + locations: ["..."], + commandRunner: commandRunner, + ) + } + } + + @Test + func failsIfNoRulesProvided() throws { + #expect(throws: BazelTargetDiscovererError.noRulesProvided) { + try BazelTargetDiscoverer.discoverTargets( + for: [], + bazelWrapper: "fakeBazel", + locations: ["..."], + ) + } + } + + @Test + func failsIfNoLocationsProvided() throws { + #expect(throws: BazelTargetDiscovererError.noLocationsProvided) { + try BazelTargetDiscoverer.discoverTargets( + for: [.iosApplication], + bazelWrapper: "fakeBazel", + locations: [], ) } } diff --git a/rules/setup_sourcekit_bsp.bzl b/rules/setup_sourcekit_bsp.bzl index 47d71434..b0ae8ecb 100644 --- a/rules/setup_sourcekit_bsp.bzl +++ b/rules/setup_sourcekit_bsp.bzl @@ -77,7 +77,7 @@ setup_sourcekit_bsp = rule( ), # We avoid using label_list here to not trigger unnecessary bazel dependency graph checks. "targets": attr.string_list( - doc = "The *top level* Bazel applications or test targets that this should serve a BSP for. It's best to keep this list small if possible for performance reasons. If not specified, the server will try to discover top-level targets automatically", + doc = "The *top level* Bazel applications or test targets that this should serve a BSP for. Wildcards are supported (e.g. //foo/...). It's best to keep this list small if possible for performance reasons. If not specified, the server will try to discover top-level targets automatically", mandatory = True, ), "bazel_wrapper": attr.string(