diff --git a/Sources/SWBCore/Dependencies.swift b/Sources/SWBCore/Dependencies.swift index de31a838..2dd8946a 100644 --- a/Sources/SWBCore/Dependencies.swift +++ b/Sources/SWBCore/Dependencies.swift @@ -302,17 +302,18 @@ public struct HeaderDependenciesContext: Sendable, SerializableCodable { data: DiagnosticData("The current toolchain does not support \(BuiltinMacros.VALIDATE_HEADER_DEPENDENCIES.name)")) } - /// Compute missing module dependencies. - public func computeMissingAndUnusedDependencies(includes: [Path]) -> (missing: [HeaderDependency], unused: [HeaderDependency]) { + /// Compute missing header dependencies. + public func computeMissingAndUnusedDependencies(includes: [(Path, includeLocations: [Diagnostic.Location])]) -> (missing: [(HeaderDependency, includeLocations: [Diagnostic.Location])], unused: [HeaderDependency]) { let declaredNames = Set(declared.map { $0.name }) - let missing: [HeaderDependency] + let missing: [(HeaderDependency, includeLocations: [Diagnostic.Location])] if validate != .no { - missing = includes.filter { file in - return !declaredNames.contains(where: { file.ends(with: $0) }) - }.map { + missing = includes.filter { include in + return !declaredNames.contains(where: { include.0.ends(with: $0) }) + }.map { include in // TODO: What if the basename doesn't uniquely identify the header? - HeaderDependency(name: $0.basename, accessLevel: .Private, optional: false) + let dep = HeaderDependency(name: include.0.basename, accessLevel: .Private, optional: false) + return (dep, includeLocations: include.includeLocations) } } else { @@ -334,15 +335,26 @@ public struct HeaderDependenciesContext: Sendable, SerializableCodable { /// /// The compiler tracing information does not provide the include locations or whether they are public imports /// (which depends on whether the import is in an installed header file). - public func makeDiagnostics(missingDependencies: [HeaderDependency], unusedDependencies: [HeaderDependency]) -> [Diagnostic] { + public func makeDiagnostics(missingDependencies: [(HeaderDependency, includeLocations: [Diagnostic.Location])], unusedDependencies: [HeaderDependency]) -> [Diagnostic] { let missingDiagnostics: [Diagnostic] if !missingDependencies.isEmpty { let behavior: Diagnostic.Behavior = validate == .yesError ? .error : .warning - let fixIt = fixItContext?.makeFixIt(newHeaders: missingDependencies) + let fixIt = fixItContext?.makeFixIt(newHeaders: missingDependencies.map { $0.0 }) let fixIts = fixIt.map { [$0] } ?? [] - let message = "Missing entries in \(BuiltinMacros.HEADER_DEPENDENCIES.name): \(missingDependencies.map { $0.asBuildSettingEntryQuotedIfNeeded }.sorted().joined(separator: " "))" + let importDiags: [Diagnostic] = missingDependencies + .flatMap { dep in + dep.1.map { + return Diagnostic( + behavior: behavior, + location: $0, + data: DiagnosticData("Missing entry in \(BuiltinMacros.HEADER_DEPENDENCIES.name): \(dep.0.asBuildSettingEntryQuotedIfNeeded)"), + fixIts: fixIts) + } + } + + let message = "Missing entries in \(BuiltinMacros.HEADER_DEPENDENCIES.name): \(missingDependencies.map { $0.0.asBuildSettingEntryQuotedIfNeeded }.sorted().joined(separator: " "))" let location: Diagnostic.Location = fixIt.map { Diagnostic.Location.path($0.sourceRange.path, line: $0.sourceRange.endLine, column: $0.sourceRange.endColumn) @@ -352,7 +364,8 @@ public struct HeaderDependenciesContext: Sendable, SerializableCodable { behavior: behavior, location: location, data: DiagnosticData(message), - fixIts: fixIts)] + fixIts: fixIts, + childDiagnostics: importDiags)] } else { missingDiagnostics = [] diff --git a/Sources/SWBTaskExecution/TaskActions/ValidateDependenciesTaskAction.swift b/Sources/SWBTaskExecution/TaskActions/ValidateDependenciesTaskAction.swift index dc4f94b6..45325d9f 100644 --- a/Sources/SWBTaskExecution/TaskActions/ValidateDependenciesTaskAction.swift +++ b/Sources/SWBTaskExecution/TaskActions/ValidateDependenciesTaskAction.swift @@ -116,7 +116,7 @@ public final class ValidateDependenciesTaskAction: TaskAction { if unsupported { diagnostics.append(contentsOf: [headerContext.makeUnsupportedToolchainDiagnostic()]) } else { - let (missingDeps, unusedDeps) = headerContext.computeMissingAndUnusedDependencies(includes: allClangIncludes.map { $0.path }) + let (missingDeps, unusedDeps) = headerContext.computeMissingAndUnusedDependencies(includes: allClangIncludes.map { ($0.path, $0.includeLocations) }) outputDelegate.incrementCounter(.headerDependenciesMissing, by: missingDeps.count) outputDelegate.incrementCounter(.headerDependenciesUnused, by: unusedDeps.count) diff --git a/Tests/SWBBuildSystemTests/DependencyValidationTests.swift b/Tests/SWBBuildSystemTests/DependencyValidationTests.swift index 64a753ca..4c32a8d9 100644 --- a/Tests/SWBBuildSystemTests/DependencyValidationTests.swift +++ b/Tests/SWBBuildSystemTests/DependencyValidationTests.swift @@ -660,17 +660,18 @@ fileprivate struct DependencyValidationTests: CoreBasedTests { groupTree: TestGroup( "Sources", path: "Sources", children: [ - TestFile("CoreFoo.c") + TestFile("CoreFoo.c"), + TestFile("Project.xcconfig"), ]), buildConfigurations: [ TestBuildConfiguration( "Debug", + baseConfig: "Project.xcconfig", buildSettings: [ "PRODUCT_NAME": "$(TARGET_NAME)", "CLANG_ENABLE_MODULES": enableModules, "CLANG_ENABLE_EXPLICIT_MODULES": enableModules, "GENERATE_INFOPLIST_FILE": "YES", - "HEADER_DEPENDENCIES": "stdio.h", "VALIDATE_HEADER_DEPENDENCIES": "YES_ERROR", "SDKROOT": "$(HOST_PLATFORM)", "SUPPORTED_PLATFORMS": "$(HOST_PLATFORM)", @@ -691,9 +692,10 @@ fileprivate struct DependencyValidationTests: CoreBasedTests { let tester = try await BuildOperationTester(getCore(), testWorkspace, simulated: false) let SRCROOT = testWorkspace.sourceRoot.join("aProject") + let srcFile = SRCROOT.join("Sources/CoreFoo.c") // Write the source files. - try await tester.fs.writeFileContents(SRCROOT.join("Sources/CoreFoo.c")) { contents in + try await tester.fs.writeFileContents(srcFile) { contents in contents <<< """ #include #include @@ -702,9 +704,41 @@ fileprivate struct DependencyValidationTests: CoreBasedTests { """ } + let projectXCConfigPath = SRCROOT.join("Sources/Project.xcconfig") + try await tester.fs.writeFileContents(projectXCConfigPath) { stream in + stream <<< + """ + HEADER_DEPENDENCIES = stdio.h + """ + } + // Expect complaint about undeclared dependency try await tester.checkBuild(parameters: BuildParameters(configuration: "Debug"), runDestination: .host, persistent: true) { results in - results.checkError(.contains("Missing entries in HEADER_DEPENDENCIES: stdlib.h")) + results.checkError(.contains("Missing entries in HEADER_DEPENDENCIES: stdlib.h")) { + diag in + let expectedDiag = Diagnostic( + behavior: .error, + location: Diagnostic.Location.path(projectXCConfigPath, line: 1, column: 30), + data: DiagnosticData("Missing entries in HEADER_DEPENDENCIES: stdlib.h"), + fixIts: [ + Diagnostic.FixIt( + sourceRange: Diagnostic.SourceRange(path: projectXCConfigPath, startLine: 1, startColumn: 30, endLine: 1, endColumn: 30), + newText: " \\\n stdlib.h"), + ], + childDiagnostics: [ + Diagnostic( + behavior: .error, + location: Diagnostic.Location.path(srcFile, line: 2, column: 8), + data: DiagnosticData("Missing entry in HEADER_DEPENDENCIES: stdlib.h"), + fixIts: [Diagnostic.FixIt( + sourceRange: Diagnostic.SourceRange(path: projectXCConfigPath, startLine: 1, startColumn: 30, endLine: 1, endColumn: 30), + newText: " \\\n stdlib.h")], + ), + ] + ) + #expect(expectedDiag == diag) + return true + } } // Declaring dependencies resolves the problem