Skip to content

Commit 572505d

Browse files
Dependencies: validate Swift imports against MODULE_DEPENDENCIES and provide fix-its (rdar://150314567)
1 parent 30ec11c commit 572505d

File tree

13 files changed

+432
-28
lines changed

13 files changed

+432
-28
lines changed

Sources/SWBCore/CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ add_library(SWBCore
3434
ConfiguredTarget.swift
3535
Core.swift
3636
CustomTaskTypeDescription.swift
37+
Dependencies.swift
3738
DependencyInfoEditPayload.swift
3839
DependencyResolution.swift
3940
DiagnosticSupport.swift

Sources/SWBCore/Dependencies.swift

Lines changed: 184 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,184 @@
1+
//===----------------------------------------------------------------------===//
2+
//
3+
// This source file is part of the Swift open source project
4+
//
5+
// Copyright (c) 2025 Apple Inc. and the Swift project authors
6+
// Licensed under Apache License v2.0 with Runtime Library Exception
7+
//
8+
// See http://swift.org/LICENSE.txt for license information
9+
// See http://swift.org/CONTRIBUTORS.txt for the list of Swift project authors
10+
//
11+
//===----------------------------------------------------------------------===//
12+
13+
public import SWBUtil
14+
import SWBMacro
15+
16+
public struct ModuleDependency: Hashable, Sendable, SerializableCodable {
17+
public let name: String
18+
public let accessLevel: AccessLevel
19+
20+
public enum AccessLevel: String, Hashable, Sendable, CaseIterable, Codable, Serializable {
21+
case Private = "private"
22+
case Package = "package"
23+
case Public = "public"
24+
25+
public init(_ string: String) throws {
26+
guard let accessLevel = AccessLevel(rawValue: string) else {
27+
throw StubError.error("unexpected access modifier '\(string)', expected one of: \(AccessLevel.allCases.map { $0.rawValue }.joined(separator: ", "))")
28+
}
29+
30+
self = accessLevel
31+
}
32+
}
33+
34+
public init(name: String, accessLevel: AccessLevel) {
35+
self.name = name
36+
self.accessLevel = accessLevel
37+
}
38+
39+
public init(entry: String) throws {
40+
var it = entry.split(separator: " ").makeIterator()
41+
switch (it.next(), it.next(), it.next()) {
42+
case (let .some(name), nil, nil):
43+
self.name = String(name)
44+
self.accessLevel = .Private
45+
46+
case (let .some(accessLevel), let .some(name), nil):
47+
self.name = String(name)
48+
self.accessLevel = try AccessLevel(String(accessLevel))
49+
50+
default:
51+
throw StubError.error("expected 1 or 2 space-separated components in: \(entry)")
52+
}
53+
}
54+
55+
public var asBuildSettingEntry: String {
56+
"\(accessLevel == .Private ? "" : "\(accessLevel.rawValue) ")\(name)"
57+
}
58+
59+
public var asBuildSettingEntryQuotedIfNeeded: String {
60+
let e = asBuildSettingEntry
61+
return e.contains(" ") ? "\"\(e)\"" : e
62+
}
63+
}
64+
65+
public struct ModuleDependenciesContext: Sendable, SerializableCodable {
66+
var validate: BooleanWarningLevel
67+
var moduleDependencies: [ModuleDependency]
68+
var fixItContext: FixItContext?
69+
70+
init(validate: BooleanWarningLevel, moduleDependencies: [ModuleDependency], fixItContext: FixItContext? = nil) {
71+
self.validate = validate
72+
self.moduleDependencies = moduleDependencies
73+
self.fixItContext = fixItContext
74+
}
75+
76+
public init?(settings: Settings) {
77+
let validate = settings.globalScope.evaluate(BuiltinMacros.VALIDATE_MODULE_DEPENDENCIES)
78+
guard validate != .no else { return nil }
79+
let fixItContext = ModuleDependenciesContext.FixItContext(settings: settings)
80+
self.init(validate: validate, moduleDependencies: settings.moduleDependencies, fixItContext: fixItContext)
81+
}
82+
83+
/// Nil `imports` means the current toolchain doesn't have the features to gather imports. This is temporarily required to support running against older toolchains.
84+
public func makeDiagnostics(imports: [(ModuleDependency, importLocations: [Diagnostic.Location])]?) -> [Diagnostic] {
85+
guard validate != .no else { return [] }
86+
guard let imports else {
87+
return [Diagnostic(
88+
behavior: .error,
89+
location: .unknown,
90+
data: DiagnosticData("The current toolchain does not support \(BuiltinMacros.VALIDATE_MODULE_DEPENDENCIES.name)"))]
91+
}
92+
93+
let missingDeps = imports.filter {
94+
// ignore module deps without source locations, these are inserted by swift / swift-build and we should treat them as implementation details which we can track without needing the user to declare them
95+
if $0.importLocations.isEmpty { return false }
96+
97+
// TODO: if the difference is just the access modifier, we emit a new entry, but ultimately our fixit should update the existing entry or emit an error about a conflict
98+
if moduleDependencies.contains($0.0) { return false }
99+
return true
100+
}
101+
102+
guard !missingDeps.isEmpty else { return [] }
103+
104+
let behavior: Diagnostic.Behavior = validate == .yesError ? .error : .warning
105+
106+
let fixIt = fixItContext?.makeFixIt(newModules: missingDeps.map { $0.0 })
107+
let fixIts = fixIt.map { [$0] } ?? []
108+
109+
let importDiags: [Diagnostic] = missingDeps
110+
.flatMap { dep in
111+
dep.1.map {
112+
return Diagnostic(
113+
behavior: behavior,
114+
location: $0,
115+
data: DiagnosticData("Missing entry in \(BuiltinMacros.MODULE_DEPENDENCIES.name): \(dep.0.asBuildSettingEntryQuotedIfNeeded)"),
116+
fixIts: fixIts)
117+
}
118+
}
119+
120+
let message = "Missing entries in \(BuiltinMacros.MODULE_DEPENDENCIES.name): \(missingDeps.map { $0.0.asBuildSettingEntryQuotedIfNeeded }.sorted().joined(separator: " "))"
121+
122+
let location: Diagnostic.Location = fixIt.map {
123+
Diagnostic.Location.path($0.sourceRange.path, line: $0.sourceRange.endLine, column: $0.sourceRange.endColumn)
124+
} ?? Diagnostic.Location.buildSetting(BuiltinMacros.MODULE_DEPENDENCIES)
125+
126+
return [Diagnostic(
127+
behavior: behavior,
128+
location: location,
129+
data: DiagnosticData(message),
130+
fixIts: fixIts,
131+
childDiagnostics: importDiags)]
132+
}
133+
134+
struct FixItContext: Sendable, SerializableCodable {
135+
var sourceRange: Diagnostic.SourceRange
136+
var modificationStyle: ModificationStyle
137+
138+
init(sourceRange: Diagnostic.SourceRange, modificationStyle: ModificationStyle) {
139+
self.sourceRange = sourceRange
140+
self.modificationStyle = modificationStyle
141+
}
142+
143+
init?(settings: Settings) {
144+
guard let target = settings.target else { return nil }
145+
let thisTargetCondition = MacroCondition(parameter: BuiltinMacros.targetNameCondition, valuePattern: target.name)
146+
147+
if let assignment = (settings.globalScope.table.lookupMacro(BuiltinMacros.MODULE_DEPENDENCIES)?.sequence.first {
148+
$0.location != nil && ($0.conditions?.conditions == [thisTargetCondition] || ($0.conditions?.conditions.isEmpty ?? true))
149+
}),
150+
let location = assignment.location
151+
{
152+
self.init(sourceRange: .init(path: location.path, startLine: location.endLine, startColumn: location.endColumn, endLine: location.endLine, endColumn: location.endColumn), modificationStyle: .appendToExistingAssignment)
153+
}
154+
else if let path = settings.constructionComponents.targetXcconfigPath {
155+
self.init(sourceRange: .init(path: path, startLine: 0, startColumn: 0, endLine: 0, endColumn: 0), modificationStyle: .insertNewAssignment(targetNameCondition: nil))
156+
}
157+
else if let path = settings.constructionComponents.projectXcconfigPath {
158+
self.init(sourceRange: .init(path: path, startLine: 0, startColumn: 0, endLine: 0, endColumn: 0), modificationStyle: .insertNewAssignment(targetNameCondition: target.name))
159+
}
160+
else {
161+
return nil
162+
}
163+
}
164+
165+
enum ModificationStyle: Sendable, SerializableCodable, Hashable {
166+
case appendToExistingAssignment
167+
case insertNewAssignment(targetNameCondition: String?)
168+
}
169+
170+
func makeFixIt(newModules: [ModuleDependency]) -> Diagnostic.FixIt {
171+
let stringValue = newModules.map { $0.asBuildSettingEntryQuotedIfNeeded }.sorted().joined(separator: " ")
172+
let newText: String
173+
switch modificationStyle {
174+
case .appendToExistingAssignment:
175+
newText = " \(stringValue)"
176+
case .insertNewAssignment(let targetNameCondition):
177+
let targetCondition = targetNameCondition.map { "[target=\($0)]" } ?? ""
178+
newText = "\n\(BuiltinMacros.MODULE_DEPENDENCIES.name)\(targetCondition) = $(inherited) \(stringValue)\n"
179+
}
180+
181+
return Diagnostic.FixIt(sourceRange: sourceRange, newText: newText)
182+
}
183+
}
184+
}

Sources/SWBCore/LibSwiftDriver/LibSwiftDriver.swift

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -238,6 +238,24 @@ public final class SwiftModuleDependencyGraph: SwiftGlobalExplicitDependencyGrap
238238
return fileDependencies
239239
}
240240

241+
func mainModule(for key: String) async throws -> SwiftDriver.ModuleInfo? {
242+
let graph = try await registryQueue.sync {
243+
guard let driver = self.registry[key] else {
244+
throw StubError.error("Unable to find jobs for key \(key). Be sure to plan the build ahead of fetching results.")
245+
}
246+
return driver.intermoduleDependencyGraph
247+
}
248+
guard let graph else { return nil }
249+
return graph.mainModule
250+
}
251+
252+
/// Nil result means the current toolchain / libSwiftScan does not support importInfos
253+
public func mainModuleImportModuleDependencies(for key: String) async throws -> [(ModuleDependency, importLocations: [SWBUtil.Diagnostic.Location])]? {
254+
try await mainModule(for: key)?.importInfos?.map {
255+
(ModuleDependency($0), $0.sourceLocations.map { Diagnostic.Location($0) })
256+
}
257+
}
258+
241259
public func queryTransitiveDependencyModuleNames(for key: String) async throws -> [String] {
242260
let graph = try await registryQueue.sync {
243261
guard let driver = self.registry[key] else {
@@ -849,3 +867,29 @@ extension SWBUtil.Diagnostic.Behavior {
849867
}
850868
}
851869
}
870+
871+
extension SWBUtil.Diagnostic.Location {
872+
init(_ loc: ScannerDiagnosticSourceLocation) {
873+
self = .path(Path(loc.bufferIdentifier), line: loc.lineNumber, column: loc.columnNumber)
874+
}
875+
}
876+
877+
extension ModuleDependency.AccessLevel {
878+
init(_ accessLevel: ImportInfo.ImportAccessLevel) {
879+
switch accessLevel {
880+
case .Private, .FilePrivate, .Internal:
881+
self = .Private
882+
case .Package:
883+
self = .Package
884+
case .Public:
885+
self = .Public
886+
}
887+
}
888+
}
889+
890+
extension ModuleDependency {
891+
init(_ importInfo: ImportInfo) {
892+
self.name = importInfo.importIdentifier
893+
self.accessLevel = .init(importInfo.accessLevel)
894+
}
895+
}

Sources/SWBCore/LinkageDependencyResolver.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -375,7 +375,7 @@ actor LinkageDependencyResolver {
375375
buildRequestContext.getCachedSettings($0.parameters, target: $0.target).globalScope.evaluate(BuiltinMacros.PRODUCT_MODULE_NAME)
376376
})
377377

378-
for moduleDependencyName in configuredTargetSettings.moduleDependencies.map { $0.name } {
378+
for moduleDependencyName in (configuredTargetSettings.moduleDependencies.map { $0.name }) {
379379
if !moduleNamesOfExplicitDependencies.contains(moduleDependencyName), let implicitDependency = await implicitDependency(forModuleName: moduleDependencyName, from: configuredTarget, imposedParameters: imposedParameters, source: .moduleDependency(name: moduleDependencyName, buildSetting: BuiltinMacros.MODULE_DEPENDENCIES)) {
380380
await result.append(ResolvedTargetDependency(target: implicitDependency, reason: .implicitBuildSetting(settingName: BuiltinMacros.MODULE_DEPENDENCIES.name, options: [moduleDependencyName])))
381381
}

Sources/SWBCore/Settings/BuiltinMacros.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2637,7 +2637,7 @@ public extension BuiltinMacros {
26372637
}
26382638

26392639
/// Enumeration macro type for tri-state booleans, typically used for warnings which can be set to "No", "Yes", or "Yes (Error)".
2640-
public enum BooleanWarningLevel: String, Equatable, Hashable, Serializable, EnumerationMacroType, Encodable {
2640+
public enum BooleanWarningLevel: String, Equatable, Hashable, Serializable, EnumerationMacroType, Codable {
26412641
public static let defaultValue = BooleanWarningLevel.no
26422642

26432643
case yesError = "YES_ERROR"

Sources/SWBCore/Settings/Settings.swift

Lines changed: 12 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -750,6 +750,8 @@ public final class Settings: PlatformBuildContext, Sendable {
750750
targetBuildVersionPlatforms(in: globalScope)
751751
}
752752

753+
public let moduleDependencies: [ModuleDependency]
754+
753755
public static func supportsMacCatalyst(scope: MacroEvaluationScope, core: Core) -> Bool {
754756
@preconcurrency @PluginExtensionSystemActor func sdkVariantInfoExtensions() -> [any SDKVariantInfoExtensionPoint.ExtensionProtocol] {
755757
core.pluginManager.extensions(of: SDKVariantInfoExtensionPoint.self)
@@ -896,6 +898,7 @@ public final class Settings: PlatformBuildContext, Sendable {
896898
}
897899

898900
self.supportedBuildVersionPlatforms = effectiveSupportedPlatforms(sdkRegistry: sdkRegistry)
901+
self.moduleDependencies = builder.moduleDependencies
899902

900903
self.constructionComponents = builder.constructionComponents
901904
}
@@ -1281,6 +1284,8 @@ private class SettingsBuilder {
12811284
/// The bound signing settings, once added in computeSigningSettings().
12821285
var signingSettings: Settings.SigningSettings? = nil
12831286

1287+
var moduleDependencies: [ModuleDependency] = []
1288+
12841289

12851290
// Mutable state of the builder as we're building up the settings table.
12861291

@@ -1615,6 +1620,13 @@ private class SettingsBuilder {
16151620
}
16161621
}
16171622

1623+
do {
1624+
self.moduleDependencies = try createScope(sdkToUse: boundProperties.sdk).evaluate(BuiltinMacros.MODULE_DEPENDENCIES).map { try ModuleDependency(entry: $0) }
1625+
}
1626+
catch {
1627+
errors.append("Failed to parse \(BuiltinMacros.MODULE_DEPENDENCIES.name): \(error)")
1628+
}
1629+
16181630
// At this point settings construction is finished.
16191631

16201632
// Analyze the settings to generate any issues about them.
@@ -5349,23 +5361,3 @@ extension MacroEvaluationScope {
53495361
}
53505362
}
53515363
}
5352-
5353-
extension Settings {
5354-
public struct ModuleDependencyInfo {
5355-
let name: String
5356-
let isPublic: Bool
5357-
}
5358-
5359-
public var moduleDependencies: [ModuleDependencyInfo] {
5360-
self.globalScope.evaluate(BuiltinMacros.MODULE_DEPENDENCIES).compactMap {
5361-
let components = $0.components(separatedBy: " ")
5362-
guard let name = components.last else {
5363-
return nil
5364-
}
5365-
return ModuleDependencyInfo(
5366-
name: name,
5367-
isPublic: components.count > 1 && components.first == "public"
5368-
)
5369-
}
5370-
}
5371-
}

Sources/SWBCore/SpecImplementations/Tools/SwiftCompiler.swift

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -446,7 +446,9 @@ public struct SwiftTaskPayload: ParentTaskPayload {
446446
/// The preview build style in effect (dynamic replacement or XOJIT), if any.
447447
public let previewStyle: PreviewStyleMessagePayload?
448448

449-
init(moduleName: String, indexingPayload: SwiftIndexingPayload, previewPayload: SwiftPreviewPayload?, localizationPayload: SwiftLocalizationPayload?, numExpectedCompileSubtasks: Int, driverPayload: SwiftDriverPayload?, previewStyle: PreviewStyle?) {
449+
public let moduleDependenciesContext: ModuleDependenciesContext?
450+
451+
init(moduleName: String, indexingPayload: SwiftIndexingPayload, previewPayload: SwiftPreviewPayload?, localizationPayload: SwiftLocalizationPayload?, numExpectedCompileSubtasks: Int, driverPayload: SwiftDriverPayload?, previewStyle: PreviewStyle?, moduleDependenciesContext: ModuleDependenciesContext?) {
450452
self.moduleName = moduleName
451453
self.indexingPayload = indexingPayload
452454
self.previewPayload = previewPayload
@@ -461,29 +463,32 @@ public struct SwiftTaskPayload: ParentTaskPayload {
461463
case nil:
462464
self.previewStyle = nil
463465
}
466+
self.moduleDependenciesContext = moduleDependenciesContext
464467
}
465468

466469
public func serialize<T: Serializer>(to serializer: T) {
467-
serializer.serializeAggregate(7) {
470+
serializer.serializeAggregate(8) {
468471
serializer.serialize(moduleName)
469472
serializer.serialize(indexingPayload)
470473
serializer.serialize(previewPayload)
471474
serializer.serialize(localizationPayload)
472475
serializer.serialize(numExpectedCompileSubtasks)
473476
serializer.serialize(driverPayload)
474477
serializer.serialize(previewStyle)
478+
serializer.serialize(moduleDependenciesContext)
475479
}
476480
}
477481

478482
public init(from deserializer: any Deserializer) throws {
479-
try deserializer.beginAggregate(7)
483+
try deserializer.beginAggregate(8)
480484
self.moduleName = try deserializer.deserialize()
481485
self.indexingPayload = try deserializer.deserialize()
482486
self.previewPayload = try deserializer.deserialize()
483487
self.localizationPayload = try deserializer.deserialize()
484488
self.numExpectedCompileSubtasks = try deserializer.deserialize()
485489
self.driverPayload = try deserializer.deserialize()
486490
self.previewStyle = try deserializer.deserialize()
491+
self.moduleDependenciesContext = try deserializer.deserialize()
487492
}
488493
}
489494

@@ -2289,7 +2294,6 @@ public final class SwiftCompilerSpec : CompilerSpec, SpecIdentifierType, SwiftDi
22892294
]
22902295
}
22912296

2292-
22932297
// BUILT_PRODUCTS_DIR here is guaranteed to be absolute by `getCommonTargetTaskOverrides`.
22942298
let payload = SwiftTaskPayload(
22952299
moduleName: moduleName,
@@ -2306,7 +2310,9 @@ public final class SwiftCompilerSpec : CompilerSpec, SpecIdentifierType, SwiftDi
23062310
previewPayload: previewPayload,
23072311
localizationPayload: localizationPayload,
23082312
numExpectedCompileSubtasks: isUsingWholeModuleOptimization ? 1 : cbc.inputs.count,
2309-
driverPayload: await driverPayload(uniqueID: String(args.hashValue), scope: cbc.scope, delegate: delegate, compilationMode: compilationMode, isUsingWholeModuleOptimization: isUsingWholeModuleOptimization, args: args, tempDirPath: objectFileDir, explicitModulesTempDirPath: Path(cbc.scope.evaluate(BuiltinMacros.SWIFT_EXPLICIT_MODULES_OUTPUT_PATH)), variant: variant, arch: arch + compilationMode.moduleBaseNameSuffix, commandLine: ["builtin-SwiftDriver", "--"] + args, ruleInfo: ruleInfo(compilationMode.ruleNameIntegratedDriver, targetName), casOptions: casOptions, linkerResponseFilePath: moduleLinkerArgsPath), previewStyle: cbc.scope.previewStyle
2313+
driverPayload: await driverPayload(uniqueID: String(args.hashValue), scope: cbc.scope, delegate: delegate, compilationMode: compilationMode, isUsingWholeModuleOptimization: isUsingWholeModuleOptimization, args: args, tempDirPath: objectFileDir, explicitModulesTempDirPath: Path(cbc.scope.evaluate(BuiltinMacros.SWIFT_EXPLICIT_MODULES_OUTPUT_PATH)), variant: variant, arch: arch + compilationMode.moduleBaseNameSuffix, commandLine: ["builtin-SwiftDriver", "--"] + args, ruleInfo: ruleInfo(compilationMode.ruleNameIntegratedDriver, targetName), casOptions: casOptions, linkerResponseFilePath: moduleLinkerArgsPath),
2314+
previewStyle: cbc.scope.previewStyle,
2315+
moduleDependenciesContext: cbc.producer.moduleDependenciesContext
23102316
)
23112317

23122318
// Finally, assemble the input and output paths and create the Swift compiler command.

0 commit comments

Comments
 (0)