Skip to content

Commit fd10cfd

Browse files
authored
Add support for parsing external repos as well (#135)
1 parent 275544c commit fd10cfd

11 files changed

+120
-81
lines changed

Sources/SourceKitBazelBSP/RequestHandlers/BuildTargets/BazelTargetQuerier.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -143,6 +143,7 @@ final class BazelTargetQuerier {
143143
supportedDependencyRuleTypes: supportedDependencyRuleTypes,
144144
supportedTopLevelRuleTypes: supportedTopLevelRuleTypes,
145145
rootUri: config.rootUri,
146+
workspaceName: config.workspaceName,
146147
executionRoot: config.executionRoot,
147148
toolchainPath: config.devToolchainPath,
148149
)

Sources/SourceKitBazelBSP/RequestHandlers/BuildTargets/BazelTargetQuerierParser.swift

Lines changed: 72 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,6 @@ enum BazelTargetQuerierParserError: Error, LocalizedError {
3636
case unexpectedTargetType(Int)
3737
case noTopLevelTargets([TopLevelRuleType])
3838
case missingPathExtension(String)
39-
case unexpectedFileExtension(String)
4039

4140
var errorDescription: String? {
4241
switch self {
@@ -61,7 +60,6 @@ enum BazelTargetQuerierParserError: Error, LocalizedError {
6160
\(rules.map { $0.rawValue }.joined(separator: ", "))
6261
"""
6362
case .missingPathExtension(let path): return "Missing path extension for \(path)"
64-
case .unexpectedFileExtension(let pathExtension): return "Unexpected file extension: \(pathExtension)"
6563
}
6664
}
6765
}
@@ -74,6 +72,7 @@ protocol BazelTargetQuerierParser: AnyObject {
7472
supportedDependencyRuleTypes: [DependencyRuleType],
7573
supportedTopLevelRuleTypes: [TopLevelRuleType],
7674
rootUri: String,
75+
workspaceName: String,
7776
executionRoot: String,
7877
toolchainPath: String,
7978
) throws -> ProcessedCqueryResult
@@ -93,17 +92,12 @@ final class BazelTargetQuerierParserImpl: BazelTargetQuerierParser {
9392
supportedDependencyRuleTypes: [DependencyRuleType],
9493
supportedTopLevelRuleTypes: [TopLevelRuleType],
9594
rootUri: String,
95+
workspaceName: String,
9696
executionRoot: String,
9797
toolchainPath: String,
9898
) throws -> ProcessedCqueryResult {
9999
let cquery = try BazelProtobufBindings.parseCqueryResult(data: data)
100-
let targets = cquery.results
101-
.map { $0.target }
102-
.filter {
103-
// Ignore external labels.
104-
// FIXME: I guess _technically_ we could index those, but skipping for now.
105-
return !$0.rule.name.hasPrefix("@")
106-
}
100+
let targets = cquery.results.map { $0.target }
107101

108102
let supportedDependencyRuleTypesSet = Set(supportedDependencyRuleTypes)
109103
let testBundleRulesSet = Set(testBundleRules)
@@ -184,7 +178,7 @@ final class BazelTargetQuerierParserImpl: BazelTargetQuerierParser {
184178
let label = target.rule.name
185179
depLabelToUriMap[label] = (
186180
BuildTargetIdentifier(
187-
uri: try label.toTargetId(rootUri: rootUri)
181+
uri: try label.toTargetId(rootUri: rootUri, workspaceName: workspaceName, executionRoot: executionRoot)
188182
), label
189183
)
190184
}
@@ -219,7 +213,7 @@ final class BazelTargetQuerierParserImpl: BazelTargetQuerierParser {
219213
}
220214
depLabelToUriMap[label] = (
221215
BuildTargetIdentifier(
222-
uri: try realLabel.toTargetId(rootUri: rootUri)
216+
uri: try realLabel.toTargetId(rootUri: rootUri, workspaceName: workspaceName, executionRoot: executionRoot)
223217
), realLabel
224218
)
225219
}
@@ -233,9 +227,9 @@ final class BazelTargetQuerierParserImpl: BazelTargetQuerierParser {
233227
}
234228

235229
let rule = target.rule
236-
let idUri: URI = try rule.name.toTargetId(rootUri: rootUri)
230+
let idUri: URI = try rule.name.toTargetId(rootUri: rootUri, workspaceName: workspaceName, executionRoot: executionRoot)
237231
let id = BuildTargetIdentifier(uri: idUri)
238-
let baseDirectory: URI = try rule.name.toBaseDirectory(rootUri: rootUri)
232+
let baseDirectory: URI? = idUri.toBaseDirectory()
239233

240234
let sourcesItem = try processSrcsAttr(
241235
rule: rule,
@@ -377,17 +371,10 @@ final class BazelTargetQuerierParserImpl: BazelTargetQuerierParser {
377371
let hdrsAttribute = rule.attribute.first { $0.name == "hdrs" }?.stringListValue ?? []
378372
let srcs: [URI] = (srcsAttribute + hdrsAttribute).compactMap {
379373
guard let srcUri = srcToUriMap[$0] else {
380-
// If the file is not part of the original array provided to this function,
381-
// then this is likely a generated file.
382-
// FIXME: Generated files are handled by the `generated file` mmnemonic,
383-
// which we don't handle today. Ignoring them for now.
384-
logger.debug(
385-
"Skipping \($0, privacy: .public): Source does not exist, most likely a generated file."
386-
)
387374
return nil
388375
}
389376
return srcUri
390-
}.sorted(by: { $0.stringValue < $1.stringValue })
377+
}
391378
return SourcesItem(
392379
target: targetId,
393380
sources: try srcs.map {
@@ -408,12 +395,18 @@ final class BazelTargetQuerierParserImpl: BazelTargetQuerierParser {
408395
guard let pathExtension = src.fileURL?.pathExtension else {
409396
throw BazelTargetQuerierParserError.missingPathExtension(src.stringValue)
410397
}
411-
guard let extensionKind = SupportedExtension(rawValue: pathExtension) else {
412-
throw BazelTargetQuerierParserError.unexpectedFileExtension(pathExtension)
398+
let kind: SourceKitSourceItemKind
399+
let language: Language?
400+
401+
if let extensionKind = SupportedExtension(rawValue: pathExtension) {
402+
kind = extensionKind.kind
403+
language = extensionKind.language
404+
} else {
405+
logger.error("Unexpected file extension \(pathExtension) for \(src.stringValue). Will recover by setting `language` to `nil`.")
406+
kind = .source
407+
language = nil
413408
}
414409

415-
let kind: SourceKitSourceItemKind = extensionKind.kind
416-
let language: Language = extensionKind.language
417410
let copyDestinations = srcCopyDestinations(for: src, rootUri: rootUri, executionRoot: executionRoot)
418411

419412
return SourceItem(
@@ -468,16 +461,14 @@ final class BazelTargetQuerierParserImpl: BazelTargetQuerierParser {
468461
let attrName = isBuildTestRule ? "targets" : "deps"
469462
let thisRule = rule.name
470463
let depsAttribute = rule.attribute.first { $0.name == attrName }?.stringListValue ?? []
471-
return depsAttribute.compactMap { label in
464+
let implDeps = rule.attribute.first { $0.name == "implementation_deps" }?.stringListValue ?? []
465+
return (depsAttribute + implDeps).compactMap { label in
472466
guard let (depUri, depRealLabel) = depLabelToUriMap[label] else {
473-
logger.debug(
474-
"Skipping dependency \(label, privacy: .public): not considered a valid dependency"
475-
)
476467
return nil
477468
}
478469
dependencyGraph[thisRule, default: []].append(depRealLabel)
479470
return depUri
480-
}.sorted(by: { $0.uri.stringValue < $1.uri.stringValue })
471+
}
481472
}
482473

483474
private func traverseGraph(
@@ -625,51 +616,62 @@ extension Array {
625616
extension String {
626617
/// Converts a Bazel label into a URI and returns a unique target id.
627618
///
628-
/// file://<path-to-root>/<package-name>___<target-name>
619+
/// For local labels: file://<path-to-root>/<package-name>___<target-name>
620+
/// For external labels: file://<execution-root>/external/<repo-name>/<package-name>___<target-name>
629621
///
630-
fileprivate func toTargetId(rootUri: String) throws -> URI {
631-
let (packageName, targetName) = try splitTargetLabel()
632-
let path = "file://" + rootUri + "/" + packageName + "___" + targetName
622+
fileprivate func toTargetId(rootUri: String, workspaceName: String, executionRoot: String) throws -> URI {
623+
let (repoName, packageName, targetName) = try splitTargetLabel(workspaceName: workspaceName)
624+
let packagePath = packageName.isEmpty ? "" : "/" + packageName
625+
let path: String
626+
if repoName == workspaceName {
627+
path = "file://" + rootUri + packagePath + "/" + targetName
628+
} else {
629+
// External repo: use execution root + external path
630+
path = "file://" + executionRoot + "/external/" + repoName + packagePath + "/" + targetName
631+
}
633632
guard let uri = try? URI(string: path) else {
634633
throw BazelTargetQuerierParserError.convertUriFailed(path)
635634
}
636635
return uri
637636
}
638637

639-
/// Fetches the base directory of a target based on its id.
640-
///
641-
/// file://<path-to-root>/<package-name>
642-
///
643-
fileprivate func toBaseDirectory(rootUri: String) throws -> URI {
644-
let (packageName, _) = try splitTargetLabel()
645-
646-
let fileScheme = "file://" + rootUri + "/" + packageName
647-
648-
guard let uri = try? URI(string: fileScheme) else {
649-
throw BazelTargetQuerierParserError.convertUriFailed(fileScheme)
650-
}
651-
652-
return uri
653-
}
654-
655-
/// Splits a full Bazel label into a tuple of its package and target names.
656-
fileprivate func splitTargetLabel() throws -> (packageName: String, targetName: String) {
638+
/// Splits a full Bazel label into a tuple of its repo, package, and target names.
639+
/// For local labels (//package:target), the repo name is the provided workspace name.
640+
/// For external labels (@repo//package:target), the repo name is extracted.
641+
fileprivate func splitTargetLabel(workspaceName: String) throws -> (repoName: String, packageName: String, targetName: String) {
657642
let components = split(separator: ":")
658643

659644
guard components.count == 2 else {
660645
throw BazelTargetQuerierParserError.incorrectName(self)
661646
}
662647

663-
let packageName =
664-
if components[0].starts(with: "//") {
665-
String(components[0].dropFirst(2))
666-
} else {
667-
String(components[0])
668-
}
669-
648+
let repoAndPackage = components[0]
670649
let targetName = String(components[1])
671650

672-
return (packageName: packageName, targetName: targetName)
651+
let repoName: String
652+
let packageName: String
653+
654+
if repoAndPackage.hasPrefix("@//") {
655+
// Alias for the main repo.
656+
repoName = workspaceName
657+
packageName = String(repoAndPackage.dropFirst(3))
658+
} else if repoAndPackage.hasPrefix("//") {
659+
// Also the main repo.
660+
repoName = workspaceName
661+
packageName = String(repoAndPackage.dropFirst(2))
662+
} else if repoAndPackage.hasPrefix("@") && repoAndPackage.contains("//") {
663+
// External label
664+
let withoutAt = repoAndPackage.dropFirst()
665+
guard let slashIndex = withoutAt.firstIndex(of: "/") else {
666+
throw BazelTargetQuerierParserError.incorrectName(self)
667+
}
668+
repoName = String(withoutAt[..<slashIndex])
669+
packageName = String(withoutAt[slashIndex...].dropFirst(2))
670+
} else {
671+
throw BazelTargetQuerierParserError.incorrectName(self)
672+
}
673+
674+
return (repoName: repoName, packageName: packageName, targetName: targetName)
673675
}
674676

675677
// Converts a Bazel label to its "full" equivalent, if needed.
@@ -686,3 +688,13 @@ extension String {
686688
}
687689
}
688690
}
691+
692+
extension URI {
693+
/// Fetches the base directory of a target by dropping the last path component (target name) from the URI.
694+
fileprivate func toBaseDirectory() -> URI? {
695+
guard let url = fileURL else {
696+
return nil
697+
}
698+
return URI(url.deletingLastPathComponent())
699+
}
700+
}

Sources/SourceKitBazelBSP/RequestHandlers/InitializeHandler.swift

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,10 @@ final class InitializeHandler {
109109
)
110110
logger.debug("executionRoot: \(executionRoot, privacy: .public)")
111111

112+
// The workspace name is the last component of the execution root path.
113+
let workspaceName = URL(fileURLWithPath: executionRoot).lastPathComponent
114+
logger.debug("workspaceName: \(workspaceName, privacy: .public)")
115+
112116
// Collecting the rest of the env's details
113117
let devDir: String = try commandRunner.run("xcode-select --print-path")
114118
let xcodeVersion: String = try commandRunner.run(
@@ -125,6 +129,7 @@ final class InitializeHandler {
125129
return InitializedServerConfig(
126130
baseConfig: baseConfig,
127131
rootUri: rootUri,
132+
workspaceName: workspaceName,
128133
outputBase: outputBase,
129134
outputPath: outputPath,
130135
devDir: devDir,

Sources/SourceKitBazelBSP/RequestHandlers/SKOptions/BazelTargetCompilerArgsExtractor.swift

Lines changed: 20 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,6 @@ import struct os.OSAllocatedUnfairLock
2727
private let logger = makeFileLevelBSPLogger()
2828

2929
enum BazelTargetCompilerArgsExtractorError: Error, LocalizedError {
30-
case invalidCUri(String)
3130
case invalidTarget(String)
3231
case sdkRootNotFound(String)
3332
case targetNotFound(String)
@@ -39,7 +38,6 @@ enum BazelTargetCompilerArgsExtractorError: Error, LocalizedError {
3938

4039
var errorDescription: String? {
4140
switch self {
42-
case .invalidCUri(let uri): return "Unexpected C-type URI missing root URI prefix: \(uri)"
4341
case .invalidTarget(let target): return "Expected to receive a build_test target, but got: \(target)"
4442
case .sdkRootNotFound(let sdk): return "sdkRootPath not found for \(sdk). Is it installed?"
4543
case .targetNotFound(let target): return "Target \(target) not found in the aquery output."
@@ -77,8 +75,13 @@ final class BazelTargetCompilerArgsExtractor {
7775
private let config: InitializedServerConfig
7876
private var argsCache = [String: [String]]()
7977

78+
private let localFilePrefix: String
79+
private let externalFilePrefix: String
80+
8081
init(config: InitializedServerConfig) {
8182
self.config = config
83+
self.localFilePrefix = "file://" + config.rootUri + "/"
84+
self.externalFilePrefix = "file://" + config.outputBase + "/"
8285
}
8386

8487
func getParsingStrategy(for uri: URI, language: Language, targetUri: URI) throws -> ParsingStrategy {
@@ -93,11 +96,14 @@ final class BazelTargetCompilerArgsExtractor {
9396
}
9497
// Make the path relative, as this is what aquery will return
9598
let fullUri = uri.stringValue
96-
let prefixToCut = "file://" + config.rootUri + "/"
97-
guard fullUri.hasPrefix(prefixToCut) else {
98-
throw BazelTargetCompilerArgsExtractorError.invalidCUri(fullUri)
99+
let parsedFile: String
100+
if fullUri.hasPrefix(self.localFilePrefix) {
101+
parsedFile = String(fullUri.dropFirst(localFilePrefix.count))
102+
} else if fullUri.hasPrefix(self.externalFilePrefix) {
103+
parsedFile = String(fullUri.dropFirst(externalFilePrefix.count))
104+
} else {
105+
parsedFile = String(fullUri.dropFirst("file://".count))
99106
}
100-
let parsedFile = String(fullUri.dropFirst(prefixToCut.count))
101107
if let xflag = language.xflag {
102108
return .cImpl(parsedFile, xflag)
103109
}
@@ -190,7 +196,14 @@ final class BazelTargetCompilerArgsExtractor {
190196
fromAquery aquery: ProcessedAqueryResult,
191197
strategy: ParsingStrategy
192198
) throws -> Analysis_Action {
193-
let bazelTarget = platformInfo.label
199+
let bazelTarget: String = {
200+
let base = platformInfo.label
201+
guard base.hasPrefix("@") else {
202+
return base
203+
}
204+
// External labels show up as `@@` in the aquery.
205+
return "@\(base)"
206+
}()
194207
guard let target = aquery.targets[bazelTarget] else {
195208
throw BazelTargetCompilerArgsExtractorError.targetNotFound(bazelTarget)
196209
}

Sources/SourceKitBazelBSP/Server/InitializedServerConfig.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ import Foundation
2424
struct InitializedServerConfig: Equatable {
2525
let baseConfig: BaseServerConfig
2626
let rootUri: String
27+
let workspaceName: String
2728
let outputBase: String
2829
let outputPath: String
2930
let devDir: String

Tests/SourceKitBazelBSPTests/BazelTargetCompilerArgsExtractorTests.swift

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,7 @@ struct BazelTargetCompilerArgsExtractorTests {
6464
compileTopLevel: compileTopLevel
6565
),
6666
rootUri: mockRootUri,
67+
workspaceName: "_main",
6768
outputBase: mockOutputBase,
6869
outputPath: mockOutputPath,
6970
devDir: mockDevDir,
@@ -239,7 +240,7 @@ let expectedSwiftResult: [String] = [
239240
"-Xfrontend",
240241
"-const-gather-protocols-file",
241242
"-Xfrontend",
242-
"/private/var/tmp/_bazel_user/hash123/external/rules_swift+/swift/toolchains/config/const_protocols_to_gather.json",
243+
"/private/var/tmp/_bazel_user/hash123/execroot/__main__/external/rules_swift+/swift/toolchains/config/const_protocols_to_gather.json",
243244
"-DDEBUG",
244245
"-Onone",
245246
"-whole-module-optimization",

0 commit comments

Comments
 (0)