Skip to content

Commit ecd4fdb

Browse files
Dependencies: validate Swift imports against MODULE_DEPENDENCIES and provide fix-its (rdar://150314567)
1 parent b14c68f commit ecd4fdb

File tree

13 files changed

+431
-28
lines changed

13 files changed

+431
-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: 183 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,183 @@
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+
26+
public init(name: String, accessLevel: AccessLevel) {
27+
self.name = name
28+
self.accessLevel = accessLevel
29+
}
30+
31+
public init(entry: String) throws {
32+
let components = entry.split(separator: " ")
33+
guard (1...2).contains(components.count) else {
34+
throw StubError.error("expected 1 or 2 space-separated components in: \(entry)")
35+
}
36+
37+
let accessLevel: AccessLevel
38+
if components.count > 1 {
39+
if let a = AccessLevel(rawValue: String(components[0])) {
40+
accessLevel = a
41+
}
42+
else {
43+
throw StubError.error("unexpected access modifier '\(components[0])', expected one of: \(AccessLevel.allCases.map { $0.rawValue }.joined(separator: ", "))")
44+
}
45+
}
46+
else {
47+
accessLevel = .Private
48+
}
49+
50+
self.name = String(components.last!)
51+
self.accessLevel = accessLevel
52+
}
53+
54+
public var asBuildSettingEntry: String {
55+
"\(accessLevel == .Private ? "" : "\(accessLevel.rawValue) ")\(name)"
56+
}
57+
58+
public var asBuildSettingEntryQuotedIfNeeded: String {
59+
let e = asBuildSettingEntry
60+
return e.contains(" ") ? "\"\(e)\"" : e
61+
}
62+
}
63+
64+
public struct ModuleDependenciesContext: Sendable, SerializableCodable {
65+
var validate: BooleanWarningLevel
66+
var moduleDependencies: [ModuleDependency]
67+
var fixItContext: FixItContext?
68+
69+
init(validate: BooleanWarningLevel, moduleDependencies: [ModuleDependency], fixItContext: FixItContext? = nil) {
70+
self.validate = validate
71+
self.moduleDependencies = moduleDependencies
72+
self.fixItContext = fixItContext
73+
}
74+
75+
public init?(settings: Settings) {
76+
let validate = settings.globalScope.evaluate(BuiltinMacros.VALIDATE_MODULE_DEPENDENCIES)
77+
guard validate != .no else { return nil }
78+
let fixItContext = ModuleDependenciesContext.FixItContext(settings: settings)
79+
self.init(validate: validate, moduleDependencies: settings.moduleDependencies, fixItContext: fixItContext)
80+
}
81+
82+
/// Nil `imports` means the current toolchain doesn't have the features to gather imports. This is temporarily required to support running against older toolchains.
83+
public func makeDiagnostics(imports: [(ModuleDependency, importLocations: [Diagnostic.Location])]?) -> [Diagnostic] {
84+
guard validate != .no else { return [] }
85+
guard let imports else {
86+
return [Diagnostic(
87+
behavior: .error,
88+
location: .unknown,
89+
data: DiagnosticData("The current toolchain does not support \(BuiltinMacros.VALIDATE_MODULE_DEPENDENCIES.name)"))]
90+
}
91+
92+
let missingDeps = imports.filter {
93+
// 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
94+
if $0.importLocations.isEmpty { return false }
95+
96+
// 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
97+
if moduleDependencies.contains($0.0) { return false }
98+
return true
99+
}
100+
101+
guard !missingDeps.isEmpty else { return [] }
102+
103+
let behavior: Diagnostic.Behavior = validate == .yesError ? .error : .warning
104+
105+
let fixIt = fixItContext?.makeFixIt(newModules: missingDeps.map { $0.0 })
106+
let fixIts = fixIt.map { [$0] } ?? []
107+
108+
let importDiags: [Diagnostic] = missingDeps
109+
.flatMap { dep in
110+
dep.1.map {
111+
return Diagnostic(
112+
behavior: behavior,
113+
location: $0,
114+
data: DiagnosticData("Missing entry in \(BuiltinMacros.MODULE_DEPENDENCIES.name): \(dep.0.asBuildSettingEntryQuotedIfNeeded)"),
115+
fixIts: fixIts)
116+
}
117+
}
118+
119+
let message = "Missing entries in \(BuiltinMacros.MODULE_DEPENDENCIES.name): \(missingDeps.map { $0.0.asBuildSettingEntryQuotedIfNeeded }.sorted().joined(separator: " "))"
120+
121+
let location: Diagnostic.Location = fixIt.map {
122+
Diagnostic.Location.path($0.sourceRange.path, line: $0.sourceRange.endLine, column: $0.sourceRange.endColumn)
123+
} ?? Diagnostic.Location.buildSetting(BuiltinMacros.MODULE_DEPENDENCIES)
124+
125+
return [Diagnostic(
126+
behavior: behavior,
127+
location: location,
128+
data: DiagnosticData(message),
129+
fixIts: fixIts,
130+
childDiagnostics: importDiags)]
131+
}
132+
133+
struct FixItContext: Sendable, SerializableCodable {
134+
var sourceRange: Diagnostic.SourceRange
135+
var modificationStyle: ModificationStyle
136+
137+
init(sourceRange: Diagnostic.SourceRange, modificationStyle: ModificationStyle) {
138+
self.sourceRange = sourceRange
139+
self.modificationStyle = modificationStyle
140+
}
141+
142+
init?(settings: Settings) {
143+
guard let target = settings.target else { return nil }
144+
let thisTargetCondition = MacroCondition(parameter: BuiltinMacros.targetNameCondition, valuePattern: target.name)
145+
146+
if let assignment = (settings.globalScope.table.lookupMacro(BuiltinMacros.MODULE_DEPENDENCIES)?.sequence.first {
147+
$0.location != nil && ($0.conditions?.conditions == [thisTargetCondition] || ($0.conditions?.conditions.isEmpty ?? true))
148+
}),
149+
let location = assignment.location
150+
{
151+
self.init(sourceRange: .init(path: location.path, startLine: location.endLine, startColumn: location.endColumn, endLine: location.endLine, endColumn: location.endColumn), modificationStyle: .appendToExistingAssignment)
152+
}
153+
else if let path = settings.constructionComponents.targetXcconfigPath {
154+
self.init(sourceRange: .init(path: path, startLine: 0, startColumn: 0, endLine: 0, endColumn: 0), modificationStyle: .insertNewAssignment(targetNameCondition: nil))
155+
}
156+
else if let path = settings.constructionComponents.projectXcconfigPath {
157+
self.init(sourceRange: .init(path: path, startLine: 0, startColumn: 0, endLine: 0, endColumn: 0), modificationStyle: .insertNewAssignment(targetNameCondition: target.name))
158+
}
159+
else {
160+
return nil
161+
}
162+
}
163+
164+
enum ModificationStyle: Sendable, SerializableCodable, Hashable {
165+
case appendToExistingAssignment
166+
case insertNewAssignment(targetNameCondition: String?)
167+
}
168+
169+
func makeFixIt(newModules: [ModuleDependency]) -> Diagnostic.FixIt {
170+
let stringValue = newModules.map { $0.asBuildSettingEntryQuotedIfNeeded }.sorted().joined(separator: " ")
171+
let newText: String
172+
switch modificationStyle {
173+
case .appendToExistingAssignment:
174+
newText = " \(stringValue)"
175+
case .insertNewAssignment(let targetNameCondition):
176+
let targetCondition = targetNameCondition.map { "[target=\($0)]" } ?? ""
177+
newText = "\n\(BuiltinMacros.MODULE_DEPENDENCIES.name)\(targetCondition) = $(inherited) \(stringValue)\n"
178+
}
179+
180+
return Diagnostic.FixIt(sourceRange: sourceRange, newText: newText)
181+
}
182+
}
183+
}

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
@@ -2631,7 +2631,7 @@ public extension BuiltinMacros {
26312631
}
26322632

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

26372637
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.
@@ -5335,23 +5347,3 @@ extension MacroEvaluationScope {
53355347
}
53365348
}
53375349
}
5338-
5339-
extension Settings {
5340-
public struct ModuleDependencyInfo {
5341-
let name: String
5342-
let isPublic: Bool
5343-
}
5344-
5345-
public var moduleDependencies: [ModuleDependencyInfo] {
5346-
self.globalScope.evaluate(BuiltinMacros.MODULE_DEPENDENCIES).compactMap {
5347-
let components = $0.components(separatedBy: " ")
5348-
guard let name = components.last else {
5349-
return nil
5350-
}
5351-
return ModuleDependencyInfo(
5352-
name: name,
5353-
isPublic: components.count > 1 && components.first == "public"
5354-
)
5355-
}
5356-
}
5357-
}

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)