Skip to content

Commit db3a4e3

Browse files
authored
Handle cross-language duplications for missing dependencies (#696)
Instead of emitting diagnostics separately per language, this emits them together by first computing missing dependencies for each language and then merging/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 94ed881 commit db3a4e3

File tree

4 files changed

+163
-43
lines changed

4 files changed

+163
-43
lines changed

Sources/SWBCore/Dependencies.swift

Lines changed: 35 additions & 36 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) {
@@ -80,18 +92,15 @@ public struct ModuleDependenciesContext: Sendable, SerializableCodable {
8092
self.init(validate: validate, moduleDependencies: settings.moduleDependencies, fixItContext: fixItContext)
8193
}
8294

83-
/// Make diagnostics for missing module dependencies from Clang imports.
95+
/// Compute missing module dependencies from Clang imports.
8496
///
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 makeDiagnostics(files: [Path]?) -> [Diagnostic] {
100+
public func computeMissingDependencies(files: [Path]?) -> [(ModuleDependency, importLocations: [Diagnostic.Location])]? {
89101
guard validate != .no else { return [] }
90102
guard let files else {
91-
return [Diagnostic(
92-
behavior: .warning,
93-
location: .unknown,
94-
data: DiagnosticData("The current toolchain does not support \(BuiltinMacros.VALIDATE_MODULE_DEPENDENCIES.name)"))]
103+
return nil
95104
}
96105

97106
// The following is a provisional/incomplete mechanism for resolving a module dependency from a file path.
@@ -111,55 +120,45 @@ public struct ModuleDependenciesContext: Sendable, SerializableCodable {
111120
ModuleDependency(name: $0, accessLevel: .Private)
112121
})
113122

114-
guard !missingDeps.isEmpty else { return [] }
115-
116-
let behavior: Diagnostic.Behavior = validate == .yesError ? .error : .warning
117-
118-
let fixIt = fixItContext?.makeFixIt(newModules: Array(missingDeps))
119-
let fixIts = fixIt.map { [$0] } ?? []
120-
121-
let message = "Missing entries in \(BuiltinMacros.MODULE_DEPENDENCIES.name): \(missingDeps.map { $0.asBuildSettingEntryQuotedIfNeeded }.sorted().joined(separator: " "))"
122-
123-
let location: Diagnostic.Location = fixIt.map {
124-
Diagnostic.Location.path($0.sourceRange.path, line: $0.sourceRange.endLine, column: $0.sourceRange.endColumn)
125-
} ?? Diagnostic.Location.buildSetting(BuiltinMacros.MODULE_DEPENDENCIES)
126-
127-
return [Diagnostic(
128-
behavior: behavior,
129-
location: location,
130-
data: DiagnosticData(message),
131-
fixIts: fixIts)]
123+
return missingDeps.map { ($0, []) }
132124
}
133125

134-
/// Make diagnostics for missing module dependencies from Swift imports.
126+
/// Compute missing module dependencies from Swift imports.
135127
///
136128
/// If `imports` is nil, the current toolchain does not support the features to gather imports.
137-
public func makeDiagnostics(imports: [(ModuleDependency, importLocations: [Diagnostic.Location])]?) -> [Diagnostic] {
129+
public func computeMissingDependencies(imports: [(ModuleDependency, importLocations: [Diagnostic.Location])]?) -> [(ModuleDependency, importLocations: [Diagnostic.Location])]? {
138130
guard validate != .no else { return [] }
139131
guard let imports else {
140-
return [Diagnostic(
141-
behavior: .warning,
142-
location: .unknown,
143-
data: DiagnosticData("The current toolchain does not support \(BuiltinMacros.VALIDATE_MODULE_DEPENDENCIES.name)"))]
132+
return nil
144133
}
145134

146-
let missingDeps = imports.filter {
135+
return imports.filter {
147136
// 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
148137
if $0.importLocations.isEmpty { return false }
149138

150139
// 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
151140
if moduleDependencies.contains($0.0) { return false }
152141
return true
153142
}
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+
}
154153

155-
guard !missingDeps.isEmpty else { return [] }
154+
guard !missingDependencies.isEmpty else { return [] }
156155

157156
let behavior: Diagnostic.Behavior = validate == .yesError ? .error : .warning
158157

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

162-
let importDiags: [Diagnostic] = missingDeps
161+
let importDiags: [Diagnostic] = missingDependencies
163162
.flatMap { dep in
164163
dep.1.map {
165164
return Diagnostic(
@@ -170,7 +169,7 @@ public struct ModuleDependenciesContext: Sendable, SerializableCodable {
170169
}
171170
}
172171

173-
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: " "))"
174173

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

Sources/SWBTaskExecution/TaskActions/ValidateDependenciesTaskAction.swift

Lines changed: 29 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -36,8 +36,8 @@ public final class ValidateDependenciesTaskAction: TaskAction {
3636
}
3737

3838
do {
39-
var allFiles = Set<String>()
40-
var allImports = Set<DependencyValidationInfo.Import>()
39+
var allClangFiles = Set<String>()
40+
var allSwiftImports = Set<DependencyValidationInfo.Import>()
4141
var unsupported = false
4242

4343
for inputPath in task.inputPaths {
@@ -47,11 +47,11 @@ public final class ValidateDependenciesTaskAction: TaskAction {
4747
switch info.payload {
4848
case .clangDependencies(let files):
4949
files.forEach {
50-
allFiles.insert($0)
50+
allClangFiles.insert($0)
5151
}
5252
case .swiftDependencies(let imports):
5353
imports.forEach {
54-
allImports.insert($0)
54+
allSwiftImports.insert($0)
5555
}
5656
case .unsupported:
5757
unsupported = true
@@ -61,10 +61,32 @@ public final class ValidateDependenciesTaskAction: TaskAction {
6161
var diagnostics: [Diagnostic] = []
6262

6363
if unsupported {
64-
diagnostics.append(contentsOf: context.makeDiagnostics(files: nil))
64+
diagnostics.append(contentsOf: context.makeDiagnostics(missingDependencies: nil))
6565
} else {
66-
diagnostics.append(contentsOf: context.makeDiagnostics(files: allFiles.map { Path($0) }))
67-
diagnostics.append(contentsOf: context.makeDiagnostics(imports: allImports.map { ($0.dependency, $0.importLocations) }))
66+
let clangMissingDeps = context.computeMissingDependencies(files: allClangFiles.map { Path($0) })
67+
let swiftMissingDeps = context.computeMissingDependencies(imports: allSwiftImports.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
73+
}
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 = Set(allSwiftImports.map { $0.dependency.name })
87+
let uniqueClangMissingDeps = clangMissingDeps?.filter { !swiftImports.contains($0.0.name) } ?? []
88+
89+
diagnostics.append(contentsOf: context.makeDiagnostics(missingDependencies: uniqueClangMissingDeps + updatedSwiftMissingDeps))
6890
}
6991

7092
for diagnostic in diagnostics {

Sources/SWBTestSupport/CoreBasedTests.swift

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -155,6 +155,14 @@ extension CoreBasedTests {
155155
}
156156
}
157157

158+
/// The path to clang in the default toolchain.
159+
public var defaultClangPath: Path {
160+
get async throws {
161+
let clangInfo = try await clangInfo
162+
return clangInfo.toolPath
163+
}
164+
}
165+
158166
/// The path to the libClang.dylib compiler in the default toolchain.
159167
package var libClangPath: Path {
160168
get async throws {

Tests/SWBBuildSystemTests/DependencyValidationTests.swift

Lines changed: 91 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"))
@@ -529,4 +546,78 @@ fileprivate struct DependencyValidationTests: CoreBasedTests {
529546
}
530547
}
531548

549+
@Test(.requireSDKs(.host), .requireClangFeatures(.printHeadersDirectPerFile), .skipHostOS(.windows, "toolchain too old"), .skipHostOS(.linux, "toolchain too old"))
550+
func validateModuleDependenciesMixedSource() async throws {
551+
try await withTemporaryDirectory { tmpDir async throws -> Void in
552+
let testWorkspace = try await TestWorkspace(
553+
"Test",
554+
sourceRoot: tmpDir.join("Test"),
555+
projects: [
556+
TestProject(
557+
"aProject",
558+
groupTree: TestGroup(
559+
"Sources", path: "Sources",
560+
children: [
561+
TestFile("CoreFoo.m"),
562+
TestFile("Swift.swift"),
563+
]),
564+
buildConfigurations: [
565+
TestBuildConfiguration(
566+
"Debug",
567+
buildSettings: [
568+
"PRODUCT_NAME": "$(TARGET_NAME)",
569+
"CLANG_ENABLE_MODULES": "YES",
570+
"CLANG_ENABLE_EXPLICIT_MODULES": "YES",
571+
"SWIFT_ENABLE_EXPLICIT_MODULES": "YES",
572+
"SWIFT_UPCOMING_FEATURE_INTERNAL_IMPORTS_BY_DEFAULT": "YES",
573+
"SWIFT_VERSION": swiftVersion,
574+
"GENERATE_INFOPLIST_FILE": "YES",
575+
"VALIDATE_MODULE_DEPENDENCIES": "YES_ERROR",
576+
"SDKROOT": "$(HOST_PLATFORM)",
577+
"SUPPORTED_PLATFORMS": "$(HOST_PLATFORM)",
578+
"DSTROOT": tmpDir.join("dstroot").str,
579+
580+
// Temporarily override to use the latest toolchain in CI because we depend on swift and swift-driver changes which aren't in the baseline tools yet
581+
"TOOLCHAINS": "swift",
582+
// We still want to use the default clang since that is used to gate the test
583+
"CC": defaultClangPath.str,
584+
]
585+
)
586+
],
587+
targets: [
588+
TestStandardTarget(
589+
"CoreFoo", type: .framework,
590+
buildPhases: [
591+
TestSourcesBuildPhase(["CoreFoo.m", "Swift.swift"]),
592+
TestFrameworksBuildPhase()
593+
])
594+
])
595+
]
596+
)
597+
598+
let tester = try await BuildOperationTester(getCore(), testWorkspace, simulated: false)
599+
let SRCROOT = testWorkspace.sourceRoot.join("aProject")
600+
601+
try await tester.fs.writeFileContents(SRCROOT.join("Sources/Swift.swift")) { stream in
602+
stream <<< """
603+
import Foundation
604+
import AppKit
605+
"""
606+
}
607+
608+
try await tester.fs.writeFileContents(SRCROOT.join("Sources/CoreFoo.m")) { contents in
609+
contents <<< """
610+
#include <Foundation/Foundation.h>
611+
#include <Accelerate/Accelerate.h>
612+
613+
void f(void) { };
614+
"""
615+
}
616+
617+
try await tester.checkBuild(parameters: BuildParameters(configuration: "Debug"), runDestination: .host, persistent: true) { results in
618+
results.checkError(.contains("Missing entries in MODULE_DEPENDENCIES: Accelerate AppKit Foundation"))
619+
}
620+
}
621+
}
622+
532623
}

0 commit comments

Comments
 (0)