Skip to content

Commit fc8b6a0

Browse files
authored
Add support for passing wildcards to --targets (#97)
1 parent e3671f3 commit fc8b6a0

File tree

5 files changed

+134
-38
lines changed

5 files changed

+134
-38
lines changed

Sources/SourceKitBazelBSP/RequestHandlers/BuildTargets/BazelTargetDiscoverer.swift

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,15 @@
11
import Foundation
22

33
public enum BazelTargetDiscovererError: LocalizedError, Equatable {
4+
case noRulesProvided
45
case noTargetsDiscovered
6+
case noLocationsProvided
57

68
public var errorDescription: String? {
79
switch self {
10+
case .noRulesProvided: return "No rules provided!"
811
case .noTargetsDiscovered: return "No targets discovered!"
12+
case .noLocationsProvided: return "No locations provided!"
913
}
1014
}
1115
}
@@ -23,14 +27,22 @@ public enum BazelTargetDiscoverer {
2327
public static func discoverTargets(
2428
for rules: [TopLevelRuleType] = TopLevelRuleType.allCases,
2529
bazelWrapper: String = "bazel",
30+
locations: [String] = [],
2631
commandRunner: CommandRunner? = nil
2732
) throws -> [String] {
28-
let commandRunner = commandRunner ?? ShellCommandRunner()
33+
guard !rules.isEmpty else {
34+
throw BazelTargetDiscovererError.noRulesProvided
35+
}
2936

30-
let query = rules.map {
31-
"kind(\($0.rawValue), ...)"
37+
guard !locations.isEmpty else {
38+
throw BazelTargetDiscovererError.noLocationsProvided
3239
}
33-
.joined(separator: " + ")
40+
41+
let commandRunner = commandRunner ?? ShellCommandRunner()
42+
43+
let kindsQuery = "\"" + rules.map { $0.rawValue }.joined(separator: "|") + "\""
44+
let locationsQuery = locations.joined(separator: " union ")
45+
let query = "kind(\(kindsQuery), \(locationsQuery))"
3446

3547
let cmd = "\(bazelWrapper) query '\(query)' --output label"
3648

Sources/SourceKitBazelBSP/Server/BaseServerConfig.swift

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,8 @@ package struct BaseServerConfig: Equatable {
4949
// We need to post-process the target list provided by the user
5050
// because the queries will always return the "full" label.
5151
// e.g: "//foo/bar" -> "//foo/bar:bar"
52-
self.targets = targets.map { $0.toFullLabel() }
52+
// We need to also de-dupe them if the user passed wildcards in Serve.swift.
53+
self.targets = Set(targets.map { $0.toFullLabel() }).sorted()
5354
}
5455
}
5556

Sources/sourcekit-bazel-bsp/Commands/Serve.swift

Lines changed: 56 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ struct Serve: ParsableCommand {
3131
@Option(
3232
parsing: .singleValue,
3333
help:
34-
"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."
34+
"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."
3535
)
3636
var target: [String] = []
3737

@@ -69,30 +69,33 @@ struct Serve: ParsableCommand {
6969

7070
let targets: [String]
7171
if !target.isEmpty {
72-
targets = target
73-
} else {
74-
// If the user provided no specific targets, try to discover them
75-
// in the workspace.
76-
logger.warning(
77-
"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."
78-
)
79-
do {
80-
let rulesToUse: [TopLevelRuleType]
81-
if !topLevelRuleToDiscover.isEmpty {
82-
rulesToUse = topLevelRuleToDiscover
72+
var expandedTargets = [String]()
73+
var targetsToExpand = [String]()
74+
for _target in target {
75+
if _target.hasSuffix("...") {
76+
targetsToExpand.append(_target)
8377
} else {
84-
rulesToUse = TopLevelRuleType.allCases
78+
expandedTargets.append(_target)
8579
}
86-
targets = try BazelTargetDiscoverer.discoverTargets(
87-
for: rulesToUse,
88-
bazelWrapper: bazelWrapper
89-
)
90-
} catch {
91-
logger.error(
92-
"Failed to initialize server: Could not discover targets. Please check your Bazel configuration or try specifying targets explicitly with `--target` instead. Failure: \(error)"
80+
}
81+
if !targetsToExpand.isEmpty {
82+
expandedTargets.append(
83+
contentsOf: try expandWildcardTargets(
84+
bazelWrapper: bazelWrapper,
85+
targets: targetsToExpand,
86+
topLevelRuleToDiscover: topLevelRuleToDiscover
87+
)
9388
)
94-
throw error
9589
}
90+
targets = expandedTargets
91+
} else {
92+
// If the user provided no specific targets, try to discover them
93+
// in the workspace.
94+
targets = try expandWildcardTargets(
95+
bazelWrapper: bazelWrapper,
96+
targets: ["..."],
97+
topLevelRuleToDiscover: topLevelRuleToDiscover
98+
)
9699
}
97100

98101
let config = BaseServerConfig(
@@ -103,7 +106,39 @@ struct Serve: ParsableCommand {
103106
compileTopLevel: compileTopLevel,
104107
indexBuildBatchSize: indexBuildBatchSize
105108
)
109+
110+
logger.debug("Initializing BSP with targets: \(targets)")
111+
106112
let server = SourceKitBazelBSPServer(baseConfig: config)
107113
server.run()
108114
}
115+
116+
private func expandWildcardTargets(
117+
bazelWrapper: String,
118+
targets: [String],
119+
topLevelRuleToDiscover: [TopLevelRuleType]
120+
) throws -> [String] {
121+
let targetsString = targets.joined(separator: ", ")
122+
logger.warning(
123+
"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."
124+
)
125+
let rulesToUse: [TopLevelRuleType]
126+
if !topLevelRuleToDiscover.isEmpty {
127+
rulesToUse = topLevelRuleToDiscover
128+
} else {
129+
rulesToUse = TopLevelRuleType.allCases
130+
}
131+
do {
132+
return try BazelTargetDiscoverer.discoverTargets(
133+
for: rulesToUse,
134+
bazelWrapper: bazelWrapper,
135+
locations: targets
136+
)
137+
} catch {
138+
logger.error(
139+
"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)"
140+
)
141+
throw error
142+
}
143+
}
109144
}

Tests/SourceKitBazelBSPTests/BazelTargetDiscovererTests.swift

Lines changed: 59 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -23,60 +23,108 @@ import Testing
2323
@testable import SourceKitBazelBSP
2424

2525
struct BazelTargetDiscovererTests {
26-
@Test("Discovers single rule type with multiple targets")
26+
@Test
2727
func discoverSingleRuleTypeWithMultipleTargets() throws {
2828
let commandRunner = CommandRunnerFake()
2929
commandRunner.setResponse(
30-
for: "fakeBazel query 'kind(ios_application, ...)' --output label",
30+
for: "fakeBazel query 'kind(\"ios_application\", ...)' --output label",
3131
response: "//Example/HelloWorld:HelloWorld\n//Example/AnotherApp:AnotherApp\n"
3232
)
3333

3434
let targets = try BazelTargetDiscoverer.discoverTargets(
3535
for: [.iosApplication],
3636
bazelWrapper: "fakeBazel",
37-
commandRunner: commandRunner
37+
locations: ["..."],
38+
commandRunner: commandRunner,
3839
)
3940

4041
#expect(targets == ["//Example/HelloWorld:HelloWorld", "//Example/AnotherApp:AnotherApp"])
4142
#expect(commandRunner.commands.count == 1)
42-
#expect(commandRunner.commands[0].command == "fakeBazel query 'kind(ios_application, ...)' --output label")
43+
#expect(commandRunner.commands[0].command == "fakeBazel query 'kind(\"ios_application\", ...)' --output label")
4344
}
4445

45-
@Test("Discovers multiple rule types")
46+
@Test
4647
func discoverMultipleRuleTypes() throws {
4748
let commandRunner = CommandRunnerFake()
4849
commandRunner.setResponse(
49-
for: "fakeBazel query 'kind(ios_application, ...) + kind(ios_unit_test, ...)' --output label",
50+
for: "fakeBazel query 'kind(\"ios_application|ios_unit_test\", ...)' --output label",
5051
response: "//Example/HelloWorld:HelloWorld\n//Example/HelloWorldTests:HelloWorldTests\n"
5152
)
5253

5354
let targets = try BazelTargetDiscoverer.discoverTargets(
5455
for: [.iosApplication, .iosUnitTest],
5556
bazelWrapper: "fakeBazel",
56-
commandRunner: commandRunner
57+
locations: ["..."],
58+
commandRunner: commandRunner,
5759
)
5860

5961
#expect(targets == ["//Example/HelloWorld:HelloWorld", "//Example/HelloWorldTests:HelloWorldTests"])
6062
#expect(commandRunner.commands.count == 1)
6163
#expect(
6264
commandRunner.commands[0].command
63-
== "fakeBazel query 'kind(ios_application, ...) + kind(ios_unit_test, ...)' --output label"
65+
== "fakeBazel query 'kind(\"ios_application|ios_unit_test\", ...)' --output label"
6466
)
6567
}
6668

67-
@Test("Throws error when no targets found")
69+
@Test
70+
func discoverAtMultipleLocations() throws {
71+
let commandRunner = CommandRunnerFake()
72+
commandRunner.setResponse(
73+
for: "fakeBazel query 'kind(\"ios_application\", //A/... union //B/...)' --output label",
74+
response: "//Example/HelloWorld:HelloWorld\n//Example/AnotherApp:AnotherApp\n"
75+
)
76+
77+
let targets = try BazelTargetDiscoverer.discoverTargets(
78+
for: [.iosApplication],
79+
bazelWrapper: "fakeBazel",
80+
locations: ["//A/...", "//B/..."],
81+
commandRunner: commandRunner,
82+
)
83+
84+
#expect(targets == ["//Example/HelloWorld:HelloWorld", "//Example/AnotherApp:AnotherApp"])
85+
#expect(commandRunner.commands.count == 1)
86+
#expect(
87+
commandRunner.commands[0].command
88+
== "fakeBazel query 'kind(\"ios_application\", //A/... union //B/...)' --output label"
89+
)
90+
}
91+
92+
@Test
6893
func throwsErrorWhenNoTargetsFound() throws {
6994
let commandRunner = CommandRunnerFake()
7095
commandRunner.setResponse(
71-
for: "fakeBazel query 'kind(ios_application, ...)' --output label",
96+
for: "fakeBazel query 'kind(\"ios_application\", ...)' --output label",
7297
response: "\n \n"
7398
)
7499

75100
#expect(throws: BazelTargetDiscovererError.noTargetsDiscovered) {
76101
try BazelTargetDiscoverer.discoverTargets(
77102
for: [.iosApplication],
78103
bazelWrapper: "fakeBazel",
79-
commandRunner: commandRunner
104+
locations: ["..."],
105+
commandRunner: commandRunner,
106+
)
107+
}
108+
}
109+
110+
@Test
111+
func failsIfNoRulesProvided() throws {
112+
#expect(throws: BazelTargetDiscovererError.noRulesProvided) {
113+
try BazelTargetDiscoverer.discoverTargets(
114+
for: [],
115+
bazelWrapper: "fakeBazel",
116+
locations: ["..."],
117+
)
118+
}
119+
}
120+
121+
@Test
122+
func failsIfNoLocationsProvided() throws {
123+
#expect(throws: BazelTargetDiscovererError.noLocationsProvided) {
124+
try BazelTargetDiscoverer.discoverTargets(
125+
for: [.iosApplication],
126+
bazelWrapper: "fakeBazel",
127+
locations: [],
80128
)
81129
}
82130
}

rules/setup_sourcekit_bsp.bzl

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,7 @@ setup_sourcekit_bsp = rule(
7777
),
7878
# We avoid using label_list here to not trigger unnecessary bazel dependency graph checks.
7979
"targets": attr.string_list(
80-
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",
80+
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",
8181
mandatory = True,
8282
),
8383
"bazel_wrapper": attr.string(

0 commit comments

Comments
 (0)