Skip to content

Commit 49075cb

Browse files
committed
Refactor diagnostic generation
Instead of emitting diagnostics separately per language, this emits them together by first computing missing dependencies for each language and then merging and deduplicating them before diagnostic generation. The code here will be forward compatible when we extend the Clang implementation to respect access levels and/or provide import locations.
1 parent 4e290b9 commit 49075cb

File tree

3 files changed

+72
-49
lines changed

3 files changed

+72
-49
lines changed

Sources/SWBCore/Dependencies.swift

Lines changed: 33 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ public struct ModuleDependency: Hashable, Sendable, SerializableCodable {
1717
public let name: String
1818
public let accessLevel: AccessLevel
1919

20-
public enum AccessLevel: String, Hashable, Sendable, CaseIterable, Codable, Serializable {
20+
public enum AccessLevel: String, Hashable, Sendable, CaseIterable, Codable, Serializable, Comparable {
2121
case Private = "private"
2222
case Package = "package"
2323
case Public = "public"
@@ -29,6 +29,18 @@ public struct ModuleDependency: Hashable, Sendable, SerializableCodable {
2929

3030
self = accessLevel
3131
}
32+
33+
// This allows easy merging of different access levels to always end up with the broadest one needed for a target.
34+
public static func < (lhs: Self, rhs: Self) -> Bool {
35+
switch lhs {
36+
case .Private:
37+
return true
38+
case .Public:
39+
return false
40+
case .Package:
41+
return rhs == .Public
42+
}
43+
}
3244
}
3345

3446
public init(name: String, accessLevel: AccessLevel) {
@@ -85,7 +97,7 @@ public struct ModuleDependenciesContext: Sendable, SerializableCodable {
8597
/// The compiler tracing information does not provide the import locations or whether they are public imports
8698
/// (which depends on whether the import is in an installed header file).
8799
/// If `files` is nil, the current toolchain does support the feature to trace imports.
88-
public func computeMissingDependencies(files: [Path]?) -> [ModuleDependency]? {
100+
public func computeMissingDependencies(files: [Path]?) -> [(ModuleDependency, importLocations: [Diagnostic.Location])]? {
89101
guard validate != .no else { return [] }
90102
guard let files else {
91103
return nil
@@ -108,67 +120,45 @@ public struct ModuleDependenciesContext: Sendable, SerializableCodable {
108120
ModuleDependency(name: $0, accessLevel: .Private)
109121
})
110122

111-
return Array(missingDeps)
123+
return missingDeps.map { ($0, []) }
112124
}
113125

114-
/// Make diagnostics for missing module dependencies from Clang imports.
115-
public func makeDiagnostics(missingDependencies: [ModuleDependency]?) -> [Diagnostic] {
116-
guard let missingDependencies else {
117-
return [Diagnostic(
118-
behavior: .warning,
119-
location: .unknown,
120-
data: DiagnosticData("The current toolchain does not support \(BuiltinMacros.VALIDATE_MODULE_DEPENDENCIES.name)"))]
121-
}
122-
123-
guard !missingDependencies.isEmpty else { return [] }
124-
125-
let behavior: Diagnostic.Behavior = validate == .yesError ? .error : .warning
126-
127-
let fixIt = fixItContext?.makeFixIt(newModules: Array(missingDependencies))
128-
let fixIts = fixIt.map { [$0] } ?? []
129-
130-
let message = "Missing entries in \(BuiltinMacros.MODULE_DEPENDENCIES.name): \(missingDependencies.map { $0.asBuildSettingEntryQuotedIfNeeded }.sorted().joined(separator: " "))"
131-
132-
let location: Diagnostic.Location = fixIt.map {
133-
Diagnostic.Location.path($0.sourceRange.path, line: $0.sourceRange.endLine, column: $0.sourceRange.endColumn)
134-
} ?? Diagnostic.Location.buildSetting(BuiltinMacros.MODULE_DEPENDENCIES)
135-
136-
return [Diagnostic(
137-
behavior: behavior,
138-
location: location,
139-
data: DiagnosticData(message),
140-
fixIts: fixIts)]
141-
}
142-
143-
/// Make diagnostics for missing module dependencies from Swift imports.
126+
/// Compute missing module dependencies from Swift imports.
144127
///
145128
/// If `imports` is nil, the current toolchain does not support the features to gather imports.
146-
public func makeDiagnostics(imports: [(ModuleDependency, importLocations: [Diagnostic.Location])]?) -> [Diagnostic] {
129+
public func computeMissingDependencies(imports: [(ModuleDependency, importLocations: [Diagnostic.Location])]?) -> [(ModuleDependency, importLocations: [Diagnostic.Location])]? {
147130
guard validate != .no else { return [] }
148131
guard let imports else {
149-
return [Diagnostic(
150-
behavior: .warning,
151-
location: .unknown,
152-
data: DiagnosticData("The current toolchain does not support \(BuiltinMacros.VALIDATE_MODULE_DEPENDENCIES.name)"))]
132+
return nil
153133
}
154134

155-
let missingDeps = imports.filter {
135+
return imports.filter {
156136
// 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
157137
if $0.importLocations.isEmpty { return false }
158138

159139
// 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
160140
if moduleDependencies.contains($0.0) { return false }
161141
return true
162142
}
143+
}
144+
145+
/// Make diagnostics for missing module dependencies.
146+
public func makeDiagnostics(missingDependencies: [(ModuleDependency, importLocations: [Diagnostic.Location])]?) -> [Diagnostic] {
147+
guard let missingDependencies else {
148+
return [Diagnostic(
149+
behavior: .warning,
150+
location: .unknown,
151+
data: DiagnosticData("The current toolchain does not support \(BuiltinMacros.VALIDATE_MODULE_DEPENDENCIES.name)"))]
152+
}
163153

164-
guard !missingDeps.isEmpty else { return [] }
154+
guard !missingDependencies.isEmpty else { return [] }
165155

166156
let behavior: Diagnostic.Behavior = validate == .yesError ? .error : .warning
167157

168-
let fixIt = fixItContext?.makeFixIt(newModules: missingDeps.map { $0.0 })
158+
let fixIt = fixItContext?.makeFixIt(newModules: missingDependencies.map { $0.0 })
169159
let fixIts = fixIt.map { [$0] } ?? []
170160

171-
let importDiags: [Diagnostic] = missingDeps
161+
let importDiags: [Diagnostic] = missingDependencies
172162
.flatMap { dep in
173163
dep.1.map {
174164
return Diagnostic(
@@ -179,7 +169,7 @@ public struct ModuleDependenciesContext: Sendable, SerializableCodable {
179169
}
180170
}
181171

182-
let message = "Missing entries in \(BuiltinMacros.MODULE_DEPENDENCIES.name): \(missingDeps.map { $0.0.asBuildSettingEntryQuotedIfNeeded }.sorted().joined(separator: " "))"
172+
let message = "Missing entries in \(BuiltinMacros.MODULE_DEPENDENCIES.name): \(missingDependencies.map { $0.0.asBuildSettingEntryQuotedIfNeeded }.sorted().joined(separator: " "))"
183173

184174
let location: Diagnostic.Location = fixIt.map {
185175
Diagnostic.Location.path($0.sourceRange.path, line: $0.sourceRange.endLine, column: $0.sourceRange.endColumn)

Sources/SWBTaskExecution/TaskActions/ValidateDependenciesTaskAction.swift

Lines changed: 22 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -63,14 +63,30 @@ public final class ValidateDependenciesTaskAction: TaskAction {
6363
if unsupported {
6464
diagnostics.append(contentsOf: context.makeDiagnostics(missingDependencies: nil))
6565
} else {
66-
// Filter missing C dependencies by known Swift dependencies to avoid duplicate diagnostics between the two.
67-
let swiftImports = allImports.map { $0.dependency.name }
68-
let missingDependencies = context.computeMissingDependencies(files: allFiles.map { Path($0) })?.filter {
69-
!swiftImports.contains($0.name)
66+
let clangMissingDeps = context.computeMissingDependencies(files: allFiles.map { Path($0) })
67+
let swiftMissingDeps = context.computeMissingDependencies(imports: allImports.map { ($0.dependency, $0.importLocations) })
68+
69+
// Update Swift dependencies with information from Clang dependencies on the same module.
70+
var clangMissingDepsByName = [String: (ModuleDependency, importLocations: [Diagnostic.Location])]()
71+
clangMissingDeps?.forEach {
72+
clangMissingDepsByName[$0.0.name] = $0
7073
}
74+
let updatedSwiftMissingDeps: [(ModuleDependency, importLocations: [Diagnostic.Location])] = swiftMissingDeps?.map {
75+
if let clangMissingDep = clangMissingDepsByName[$0.0.name] {
76+
return (
77+
ModuleDependency(name: $0.0.name, accessLevel: max($0.0.accessLevel, clangMissingDep.0.accessLevel)),
78+
$0.importLocations + clangMissingDep.importLocations
79+
)
80+
} else {
81+
return $0
82+
}
83+
} ?? []
84+
85+
// Filter missing C dependencies by known Swift dependencies to avoid duplicate diagnostics.
86+
let swiftImports = allImports.map { $0.dependency.name }
87+
let uniqueClangMissingDeps = clangMissingDeps?.filter { !swiftImports.contains($0.0.name) } ?? []
7188

72-
diagnostics.append(contentsOf: context.makeDiagnostics(missingDependencies: missingDependencies))
73-
diagnostics.append(contentsOf: context.makeDiagnostics(imports: allImports.map { ($0.dependency, $0.importLocations) }))
89+
diagnostics.append(contentsOf: context.makeDiagnostics(missingDependencies: uniqueClangMissingDeps + updatedSwiftMissingDeps))
7490
}
7591

7692
for diagnostic in diagnostics {

Tests/SWBBuildSystemTests/DependencyValidationTests.swift

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,23 @@ import SWBMacro
1919

2020
@Suite
2121
fileprivate struct DependencyValidationTests: CoreBasedTests {
22+
@Test
23+
func moduleDependencyAccessLevelComparable() async throws {
24+
#expect(ModuleDependency.AccessLevel.Private == .Private)
25+
#expect(ModuleDependency.AccessLevel.Private < .Package)
26+
#expect(ModuleDependency.AccessLevel.Private < .Public)
27+
28+
#expect(ModuleDependency.AccessLevel.Package == .Package)
29+
#expect(ModuleDependency.AccessLevel.Package < .Public)
30+
#expect(ModuleDependency.AccessLevel.Package > .Private)
31+
32+
#expect(ModuleDependency.AccessLevel.Public == .Public)
33+
#expect(ModuleDependency.AccessLevel.Public > .Private)
34+
#expect(ModuleDependency.AccessLevel.Public > .Package)
35+
36+
#expect(max(ModuleDependency.AccessLevel.Public, ModuleDependency.AccessLevel.Private) == .Public)
37+
}
38+
2239
@Test(.requireSDKs(.macOS))
2340
func dependencyValidation() async throws {
2441
try await testDependencyValidation(BuildParameters(configuration: "Debug"))

0 commit comments

Comments
 (0)