diff --git a/Sources/SwiftDriver/Driver/Driver.swift b/Sources/SwiftDriver/Driver/Driver.swift index ac5c42900..c82eda6b9 100644 --- a/Sources/SwiftDriver/Driver/Driver.swift +++ b/Sources/SwiftDriver/Driver/Driver.swift @@ -419,6 +419,9 @@ public struct Driver { /// Path to the serialized diagnostics file. let serializedDiagnosticsFilePath: VirtualPath.Handle? + /// The format diagnostics are serialized in. + let serializedDiagnosticsFormat: FileType + /// Path to the serialized diagnostics file of the emit-module task. let emitModuleSerializedDiagnosticsFilePath: VirtualPath.Handle? @@ -1337,8 +1340,11 @@ public struct Driver { emitModuleSeparately: emitModuleSeparately, outputFileMap: self.outputFileMap, moduleName: moduleOutputInfo.name) + self.serializedDiagnosticsFormat = Self.serializedDiagnosticsFormat(&parsedOptions, + diagnosticEngine: diagnosticsEngine) self.serializedDiagnosticsFilePath = try Self.computeSupplementaryOutputPath( - &parsedOptions, type: .diagnostics, isOutputOptions: [.serializeDiagnostics], + &parsedOptions, type: self.serializedDiagnosticsFormat, + isOutputOptions: [.serializeDiagnostics, .serializeDiagnosticsEQ], outputPath: .serializeDiagnosticsPath, compilerOutputType: compilerOutputType, compilerMode: compilerMode, @@ -1346,7 +1352,8 @@ public struct Driver { outputFileMap: self.outputFileMap, moduleName: moduleOutputInfo.name) self.emitModuleSerializedDiagnosticsFilePath = try Self.computeSupplementaryOutputPath( - &parsedOptions, type: .emitModuleDiagnostics, isOutputOptions: [.serializeDiagnostics], + &parsedOptions, type: Self.emitModuleSerializedDiagnosticsFormat(for: self.serializedDiagnosticsFormat), + isOutputOptions: [.serializeDiagnostics, .serializeDiagnosticsEQ], outputPath: .emitModuleSerializeDiagnosticsPath, compilerOutputType: compilerOutputType, compilerMode: compilerMode, @@ -1354,7 +1361,8 @@ public struct Driver { outputFileMap: self.outputFileMap, moduleName: moduleOutputInfo.name) self.dependencyScanSerializedDiagnosticsPath = try Self.computeSupplementaryOutputPath( - &parsedOptions, type: .dependencyScanDiagnostics, isOutputOptions: [.serializeDiagnostics], + &parsedOptions, type: Self.dependencyScanSerializedDiagnosticsFormat(for: self.serializedDiagnosticsFormat), + isOutputOptions: [.serializeDiagnostics, .serializeDiagnosticsEQ], outputPath: .dependencyScanSerializeDiagnosticsPath, compilerOutputType: compilerOutputType, compilerMode: compilerMode, @@ -3986,6 +3994,42 @@ extension Driver { // Supplementary outputs. extension Driver { + /// The format diagnostics are serialized in. + static func serializedDiagnosticsFormat( + _ parsedOptions: inout ParsedOptions, + diagnosticEngine: DiagnosticsEngine + ) -> FileType { + // The last of the bare flag, which means 'dia', and '=' wins. + var format = FileType.diagnostics + for parsed in parsedOptions.arguments(for: .serializeDiagnostics, .serializeDiagnosticsEQ) { + guard parsed.option == .serializeDiagnosticsEQ else { + format = .diagnostics + continue + } + switch parsed.argument.asSingle { + case "sarif": + format = .sarifDiagnostics + case "dia": + format = .diagnostics + case let name: + diagnosticEngine.emit(.error_unsupported_argument(argument: name, option: .serializeDiagnosticsEQ)) + } + } + return format + } + + /// The format the emit-module job's diagnostics are serialized in, the same + /// as the rest of the compilation. + static func emitModuleSerializedDiagnosticsFormat(for format: FileType) -> FileType { + format == .sarifDiagnostics ? .emitModuleSARIFDiagnostics : .emitModuleDiagnostics + } + + /// The format the dependency scan's diagnostics are serialized in, the same + /// as the rest of the compilation. + static func dependencyScanSerializedDiagnosticsFormat(for format: FileType) -> FileType { + format == .sarifDiagnostics ? .dependencyScanSARIFDiagnostics : .dependencyScanDiagnostics + } + /// Determine the output path for a supplementary output. static func computeSupplementaryOutputPath( _ parsedOptions: inout ParsedOptions, @@ -4031,7 +4075,7 @@ extension Driver { // Emit-module serialized diagnostics are always specified as a single-output // file - if type == .emitModuleDiagnostics, + if type == .emitModuleDiagnostics || type == .emitModuleSARIFDiagnostics, let singleOutputPath = try outputFileMap?.existingOutputForSingleInput( outputType: type) { return singleOutputPath diff --git a/Sources/SwiftDriver/ExplicitModuleBuilds/ModuleDependencyScanning.swift b/Sources/SwiftDriver/ExplicitModuleBuilds/ModuleDependencyScanning.swift index dfe847c43..9e3ad512e 100644 --- a/Sources/SwiftDriver/ExplicitModuleBuilds/ModuleDependencyScanning.swift +++ b/Sources/SwiftDriver/ExplicitModuleBuilds/ModuleDependencyScanning.swift @@ -232,6 +232,10 @@ public extension Driver { if let depScanSerializedDiagnosticsPath = dependencyScanSerializedDiagnosticsPath { commandLine.appendFlag("-serialize-diagnostics-path") commandLine.appendPath(VirtualPath.lookup(depScanSerializedDiagnosticsPath)) + // The format goes with the path, as for compile jobs. + if serializedDiagnosticsFormat == .sarifDiagnostics { + commandLine.appendFlag("-serialize-diagnostics=sarif") + } } // loadedModuleTrace file. diff --git a/Sources/SwiftDriver/Jobs/CompileJob.swift b/Sources/SwiftDriver/Jobs/CompileJob.swift index 407af3046..eaee5cd60 100644 --- a/Sources/SwiftDriver/Jobs/CompileJob.swift +++ b/Sources/SwiftDriver/Jobs/CompileJob.swift @@ -100,6 +100,7 @@ extension Driver { .clangModuleMap, .jsonCompilerFeatures, .jsonTargetInfo, .jsonSwiftArtifacts, .indexUnitOutputPath, .modDepCache, .jsonAPIBaseline, .jsonABIBaseline, .swiftConstValues, .jsonAPIDescriptor, .moduleSummary, .moduleSemanticInfo, .dependencyScanDiagnostics, + .sarifDiagnostics, .emitModuleSARIFDiagnostics, .dependencyScanSARIFDiagnostics, .cachedDiagnostics, .jsonSupportedFeatures, nil: return false } @@ -551,12 +552,13 @@ extension FileType { case .swift, .dSYM, .autolink, .dependencies, .emitModuleDependencies, .swiftDocumentation, .pcm, .diagnostics, .emitModuleDiagnostics, + .sarifDiagnostics, .emitModuleSARIFDiagnostics, .objcHeader, .image, .swiftDeps, .moduleTrace, .tbd, .yamlOptimizationRecord, .bitstreamOptimizationRecord, .swiftInterface, .privateSwiftInterface, .packageSwiftInterface, .swiftSourceInfoFile, .clangModuleMap, .jsonSwiftArtifacts, .indexUnitOutputPath, .modDepCache, .jsonAPIBaseline, .jsonABIBaseline, .swiftConstValues, .jsonAPIDescriptor, .moduleSummary, .moduleSemanticInfo, - .cachedDiagnostics, .dependencyScanDiagnostics: + .cachedDiagnostics, .dependencyScanDiagnostics, .dependencyScanSARIFDiagnostics: fatalError("Output type can never be a primary output") } } diff --git a/Sources/SwiftDriver/Jobs/EmitModuleJob.swift b/Sources/SwiftDriver/Jobs/EmitModuleJob.swift index 170c95947..c9a2186b7 100644 --- a/Sources/SwiftDriver/Jobs/EmitModuleJob.swift +++ b/Sources/SwiftDriver/Jobs/EmitModuleJob.swift @@ -40,7 +40,13 @@ extension Driver { addSupplementalOutput(path: objcGeneratedHeaderPath, flag: "-emit-objc-header-path", type: .objcHeader) addSupplementalOutput(path: tbdPath, flag: "-emit-tbd-path", type: .tbd) addSupplementalOutput(path: moduleOutputPaths.apiDescriptorFilePath, flag: "-emit-api-descriptor-path", type: .jsonAPIDescriptor) - addSupplementalOutput(path: emitModuleSerializedDiagnosticsFilePath, flag: "-serialize-diagnostics-path", type: .emitModuleDiagnostics) + let emitModuleDiagnosticsFormat = Self.emitModuleSerializedDiagnosticsFormat(for: serializedDiagnosticsFormat) + addSupplementalOutput(path: emitModuleSerializedDiagnosticsFilePath, flag: "-serialize-diagnostics-path", + type: emitModuleDiagnosticsFormat) + // The format goes with the path, as for compile jobs. + if emitModuleSerializedDiagnosticsFilePath != nil && emitModuleDiagnosticsFormat == .emitModuleSARIFDiagnostics { + commandLine.appendFlag("-serialize-diagnostics=sarif") + } addSupplementalOutput(path: emitModuleDependenciesFilePath, flag: "-emit-dependencies-path", type: .emitModuleDependencies) // Skip files created by other jobs when emitting a module and building at the same time diff --git a/Sources/SwiftDriver/Jobs/FrontendJobHelpers.swift b/Sources/SwiftDriver/Jobs/FrontendJobHelpers.swift index 791075ea3..bcdeb0d70 100644 --- a/Sources/SwiftDriver/Jobs/FrontendJobHelpers.swift +++ b/Sources/SwiftDriver/Jobs/FrontendJobHelpers.swift @@ -935,7 +935,7 @@ extension Driver { flag: "-emit-reference-dependencies-path") try addOutputOfType( - outputType: .diagnostics, + outputType: serializedDiagnosticsFormat, finalOutputPath: serializedDiagnosticsFilePath, input: input, flag: "-serialize-diagnostics-path") @@ -1179,6 +1179,11 @@ extension Driver { } } + // The format flag enables serialization, so pass it only with a SARIF output. + if flaggedInputOutputPairs.contains(where: { $0.output.type == .sarifDiagnostics }) { + commandLine.appendFlag("-serialize-diagnostics=sarif") + } + return flaggedInputOutputPairs.map { $0.output } } diff --git a/Sources/SwiftDriver/Jobs/GeneratePCHJob.swift b/Sources/SwiftDriver/Jobs/GeneratePCHJob.swift index 1fafd4e0f..b5a70f73b 100644 --- a/Sources/SwiftDriver/Jobs/GeneratePCHJob.swift +++ b/Sources/SwiftDriver/Jobs/GeneratePCHJob.swift @@ -47,23 +47,32 @@ extension Driver { try addRuntimeLibraryFlags(commandLine: &commandLine) // TODO: Should this just be pch output with extension changed? - if parsedOptions.hasArgument(.serializeDiagnostics), let outputDirectory = parsedOptions.getLastArgument(.pchOutputDir)?.asSingle { + if parsedOptions.hasArgument(.serializeDiagnostics, .serializeDiagnosticsEQ), + let outputDirectory = parsedOptions.getLastArgument(.pchOutputDir)?.asSingle { commandLine.appendFlag(.serializeDiagnosticsPath) let path: VirtualPath - if let outputPath = try outputFileMap?.existingOutput(inputFile: input.fileHandle, outputType: .diagnostics) { + if let outputPath = try outputFileMap?.existingOutput(inputFile: input.fileHandle, + outputType: serializedDiagnosticsFormat) { path = VirtualPath.lookup(outputPath) } else if let modulePath = parsedOptions.getLastArgument(.emitModulePath) { // TODO: does this hash need to be persistent? let code = UInt(bitPattern: modulePath.asSingle.hashValue) let outputName = input.file.basenameWithoutExt + "-" + String(code, radix: 36) - path = try VirtualPath(path: outputDirectory).appending(component: outputName.appendingFileTypeExtension(.diagnostics)) + path = try VirtualPath(path: outputDirectory).appending( + component: outputName.appendingFileTypeExtension(serializedDiagnosticsFormat)) } else { path = try VirtualPath.createUniqueTemporaryFile( - RelativePath(validating: input.file.basenameWithoutExt.appendingFileTypeExtension(.diagnostics))) + RelativePath( + validating: input.file.basenameWithoutExt.appendingFileTypeExtension(serializedDiagnosticsFormat))) } commandLine.appendPath(path) - outputs.append(.init(file: path.intern(), type: .diagnostics)) + outputs.append(.init(file: path.intern(), type: serializedDiagnosticsFormat)) + + // The format goes with the path, as for compile jobs. + if serializedDiagnosticsFormat == .sarifDiagnostics { + commandLine.appendFlag("-serialize-diagnostics=sarif") + } } // New compute and add inputs and outputs. diff --git a/Sources/SwiftDriver/Utilities/FileType.swift b/Sources/SwiftDriver/Utilities/FileType.swift index 89b889238..bdf3ef412 100644 --- a/Sources/SwiftDriver/Utilities/FileType.swift +++ b/Sources/SwiftDriver/Utilities/FileType.swift @@ -86,12 +86,21 @@ public enum FileType: String, Hashable, CaseIterable, Codable { /// Clang/Swift serialized diagnostics case diagnostics = "dia" + /// Diagnostics serialized to SARIF + case sarifDiagnostics = "sarif" + /// Serialized diagnostics produced by module-generation case emitModuleDiagnostics = "emit-module.dia" + /// Diagnostics produced by module-generation, serialized to SARIF + case emitModuleSARIFDiagnostics = "emit-module.sarif" + /// Serialized diagnostics produced by dependency scanning case dependencyScanDiagnostics = "dependency-scan.dia" + /// Diagnostics produced by dependency scanning, serialized to SARIF + case dependencyScanSARIFDiagnostics = "dependency-scan.sarif" + /// Serialized diagnostics produced by module-generation case emitModuleDependencies = "emit-module.d" @@ -256,12 +265,21 @@ extension FileType: CustomStringConvertible { case .diagnostics: return "diagnostics" + case .sarifDiagnostics: + return "sarif-diagnostics" + case .emitModuleDiagnostics: return "emit-module-diagnostics" + case .emitModuleSARIFDiagnostics: + return "emit-module-sarif-diagnostics" + case .dependencyScanDiagnostics: return "dependency-scan-diagnostics" + case .dependencyScanSARIFDiagnostics: + return "dependency-scan-sarif-diagnostics" + case .jsonAPIBaseline: return "api-baseline-json" @@ -300,6 +318,7 @@ extension FileType { .importedModules, .indexData, .remap, .dSYM, .autolink, .dependencies, .emitModuleDependencies, .swiftDocumentation, .pcm, .diagnostics, .emitModuleDiagnostics, .dependencyScanDiagnostics, .objcHeader, .image, .swiftDeps, .moduleTrace, + .sarifDiagnostics, .emitModuleSARIFDiagnostics, .dependencyScanSARIFDiagnostics, .tbd, .yamlOptimizationRecord, .bitstreamOptimizationRecord, .swiftInterface, .privateSwiftInterface, .packageSwiftInterface, .swiftSourceInfoFile, .jsonDependencies, .clangModuleMap, .jsonTargetInfo, .jsonCompilerFeatures, @@ -428,10 +447,16 @@ extension FileType { return "bitstream-opt-record" case .diagnostics: return "diagnostics" + case .sarifDiagnostics: + return "sarif-diagnostics" case .emitModuleDiagnostics: return "emit-module-diagnostics" + case .emitModuleSARIFDiagnostics: + return "emit-module-sarif-diagnostics" case .dependencyScanDiagnostics: return "dependency-scan-diagnostics" + case .dependencyScanSARIFDiagnostics: + return "dependency-scan-sarif-diagnostics" case .indexUnitOutputPath: return "index-unit-output-path" case .jsonAPIBaseline: @@ -463,7 +488,8 @@ extension FileType { .jsonDependencies, .clangModuleMap, .jsonCompilerFeatures, .jsonTargetInfo, .jsonSwiftArtifacts, .jsonAPIBaseline, .jsonABIBaseline, .swiftConstValues, .jsonAPIDescriptor, .moduleSummary, .moduleSemanticInfo, .cachedDiagnostics, - .raw_llvmIr, .jsonSupportedFeatures: + .raw_llvmIr, .jsonSupportedFeatures, .sarifDiagnostics, .emitModuleSARIFDiagnostics, + .dependencyScanSARIFDiagnostics: return true case .image, .object, .dSYM, .pch, .sib, .raw_sib, .swiftModule, .swiftDocumentation, .swiftSourceInfoFile, .llvmBitcode, .diagnostics, @@ -483,6 +509,7 @@ extension FileType { .autolink, .swiftModule, .swiftDocumentation, .swiftInterface, .privateSwiftInterface, .packageSwiftInterface, .swiftSourceInfoFile, .raw_sil, .raw_sib, .diagnostics, .emitModuleDiagnostics, .objcHeader, .swiftDeps, .remap, + .sarifDiagnostics, .emitModuleSARIFDiagnostics, .dependencyScanSARIFDiagnostics, .importedModules, .tbd, .moduleTrace, .indexData, .yamlOptimizationRecord, .modDepCache, .bitstreamOptimizationRecord, .pcm, .pch, .jsonDependencies, .clangModuleMap, .jsonCompilerFeatures, .jsonTargetInfo, .jsonSwiftArtifacts, @@ -498,7 +525,8 @@ extension FileType { switch self { case .swift, .ast, .indexData, .indexUnitOutputPath, .jsonCompilerFeatures, .jsonTargetInfo, .jsonSupportedFeatures: return false - case .sil, .sib, .image, .object, .dSYM, .dependencies, .autolink, .swiftModule, .swiftDocumentation, .swiftInterface, .privateSwiftInterface, .packageSwiftInterface, .swiftSourceInfoFile, .swiftConstValues, .assembly, .raw_sil, .raw_sib, .llvmIR, .llvmBitcode, .diagnostics, .emitModuleDiagnostics, .emitModuleDependencies, .objcHeader, .swiftDeps, .modDepCache, .remap, .importedModules, .tbd, .jsonDependencies, .jsonSwiftArtifacts, .moduleTrace, .yamlOptimizationRecord, .bitstreamOptimizationRecord, .pcm, .pch, .clangModuleMap, .jsonAPIBaseline, .jsonABIBaseline, .jsonAPIDescriptor, .moduleSummary, .moduleSemanticInfo, .cachedDiagnostics, .raw_llvmIr, .dependencyScanDiagnostics: + case .sil, .sib, .image, .object, .dSYM, .dependencies, .autolink, .swiftModule, .swiftDocumentation, .swiftInterface, .privateSwiftInterface, .packageSwiftInterface, .swiftSourceInfoFile, .swiftConstValues, .assembly, .raw_sil, .raw_sib, .llvmIR, .llvmBitcode, .diagnostics, .emitModuleDiagnostics, .emitModuleDependencies, .objcHeader, .swiftDeps, .modDepCache, .remap, .importedModules, .tbd, .jsonDependencies, .jsonSwiftArtifacts, .moduleTrace, .yamlOptimizationRecord, .bitstreamOptimizationRecord, .pcm, .pch, .clangModuleMap, .jsonAPIBaseline, .jsonABIBaseline, .jsonAPIDescriptor, .moduleSummary, .moduleSemanticInfo, .cachedDiagnostics, .raw_llvmIr, .dependencyScanDiagnostics, + .sarifDiagnostics, .emitModuleSARIFDiagnostics, .dependencyScanSARIFDiagnostics: return true } } @@ -507,6 +535,7 @@ extension FileType { var supportCaching: Bool { switch self { case .diagnostics, .emitModuleDiagnostics, // diagnostics are cached using cached diagnostics. + .sarifDiagnostics, .emitModuleSARIFDiagnostics, // Those are by-product from swift-driver and not considered outputs need caching. .jsonSwiftArtifacts, .remap, .indexUnitOutputPath, .modDepCache, // the remaining should not be an output from a caching swift job. @@ -515,6 +544,7 @@ extension FileType { return false case .assembly, .llvmIR, .llvmBitcode, .object, .sil, .sib, .ast, .dependencies, .emitModuleDependencies, .swiftModule, .dependencyScanDiagnostics, + .dependencyScanSARIFDiagnostics, .swiftDocumentation, .swiftInterface, .privateSwiftInterface, .packageSwiftInterface, .swiftSourceInfoFile, .raw_sil, .raw_sib, .objcHeader, .swiftDeps, .tbd, .moduleTrace, .indexData, .yamlOptimizationRecord, diff --git a/Sources/SwiftDriverExecution/MultiJobExecutor.swift b/Sources/SwiftDriverExecution/MultiJobExecutor.swift index 0774be9fa..03d616208 100644 --- a/Sources/SwiftDriverExecution/MultiJobExecutor.swift +++ b/Sources/SwiftDriverExecution/MultiJobExecutor.swift @@ -707,7 +707,7 @@ fileprivate extension Job { /// Don't leave incorrect compiler outputs lying around, but don't remove diagnostics! func removeOutputsOfFailedCompilation(from fileSystem: TSCBasic.FileSystem) { guard kind.isCompile else {return} - for output in outputs where output.type != .diagnostics { + for output in outputs where output.type != .diagnostics && output.type != .sarifDiagnostics { guard let absolutePath = output.file.absolutePath else { continue } try? fileSystem.removeFileTree(absolutePath) } diff --git a/Tests/SwiftDriverTests/JobExecutorTests.swift b/Tests/SwiftDriverTests/JobExecutorTests.swift index e5839c222..a8c0ba214 100644 --- a/Tests/SwiftDriverTests/JobExecutorTests.swift +++ b/Tests/SwiftDriverTests/JobExecutorTests.swift @@ -241,6 +241,46 @@ extension DarwinToolchain { } } + /// A failed compile job loses its outputs, except for its diagnostics, which + /// explain the failure. + @Test(.skipHostOS(.win32, comment: "Uses /bin/sh"), + arguments: [FileType.diagnostics, .sarifDiagnostics]) + func failedCompilationKeepsDiagnostics(diagnosticsType: FileType) throws { + try withTemporaryDirectory { path in + let object = path.appending(component: "main.o") + let diagnostics = path.appending(component: "main.\(diagnosticsType.rawValue)") + + // Stands in for a frontend that writes its outputs and then fails. + let compile = Job( + moduleName: "main", + kind: .compile, + tool: ResolvedTool(path: try AbsolutePath(validating: "/bin/sh"), supportsResponseFiles: false), + commandLine: [ + "-c", "touch \"$0\" \"$1\"; exit 1", + .path(.absolute(object)), .path(.absolute(diagnostics)), + ], + inputs: [], + primaryInputs: [], + outputs: [ + .init(file: VirtualPath.absolute(object).intern(), type: .object), + .init(file: VirtualPath.absolute(diagnostics).intern(), type: diagnosticsType), + ] + ) + let executor = MultiJobExecutor( + workload: .all([compile]), + resolver: try ArgsResolver(fileSystem: localFileSystem), + executorDelegate: JobCollectingDelegate(), + diagnosticsEngine: DiagnosticsEngine() + ) + #expect(throws: (any Error).self) { + try executor.execute(env: ProcessEnv.block, fileSystem: localFileSystem) + } + + #expect(!localFileSystem.exists(object)) + #expect(localFileSystem.exists(diagnostics)) + } + } + /// Ensure the executor is capable of forwarding its standard input to the compile job that requires it. @Test(.requireHostOS(.macosx)) func inputForwarding() async throws { let hostTriple = try TestDriver(args: ["swiftc", "test.swift"]).hostTriple diff --git a/Tests/SwiftDriverTests/SARIFDiagnosticsTests.swift b/Tests/SwiftDriverTests/SARIFDiagnosticsTests.swift new file mode 100644 index 000000000..8b3945e41 --- /dev/null +++ b/Tests/SwiftDriverTests/SARIFDiagnosticsTests.swift @@ -0,0 +1,551 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift open source project +// +// Copyright (c) 2026 Apple Inc. and the Swift project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of Swift project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +import Foundation +@_spi(Testing) import SwiftDriver +import SwiftOptions +import TSCBasic +import TestUtilities +import Testing + +@Suite struct SARIFDiagnosticsTests { + /// Each primary gets its own SARIF log, and the format travels with the path. + @Test func sarifFormatSelectsSARIFOutputs() async throws { + var driver = try TestDriver(args: [ + "swiftc", "-typecheck", "foo.swift", "bar.swift", + "-serialize-diagnostics=sarif", + ]) + let plannedJobs = try await driver.planBuild().removingAutolinkExtractJobs() + let compileJobs = plannedJobs.filter { $0.kind == .compile } + #expect(compileJobs.count == 2) + + for job in compileJobs { + #expect(job.outputs.contains { $0.type == .sarifDiagnostics }) + #expect(!job.outputs.contains { $0.type == .diagnostics }) + #expect(job.commandLine.contains(.flag("-serialize-diagnostics=sarif"))) + #expect(job.commandLine.contains(.flag("-serialize-diagnostics-path"))) + } + } + + /// Without a format, diagnostics stay in the binary format and no format flag + /// is passed to any job. + @Test func bareFlagKeepsBinaryFormatAndPassesNoFormat() async throws { + var driver = try TestDriver(args: [ + "swiftc", "-typecheck", "foo.swift", "bar.swift", "-serialize-diagnostics", + ]) + let plannedJobs = try await driver.planBuild().removingAutolinkExtractJobs() + let compileJobs = plannedJobs.filter { $0.kind == .compile } + #expect(compileJobs.count == 2) + + for job in compileJobs { + #expect(job.outputs.contains { $0.type == .diagnostics }) + #expect(!job.outputs.contains { $0.type == .sarifDiagnostics }) + #expect(!job.commandLine.contains(.flag("-serialize-diagnostics=sarif"))) + #expect(!job.commandLine.contains(.flag("-serialize-diagnostics=dia"))) + } + } + + /// The format flag reaches only jobs that have a SARIF output. It turns + /// serialization on by itself, so a job without one would otherwise start + /// writing a log the driver knows nothing about. + @Test func formatDoesNotReachJobsWithoutSARIFOutputs() async throws { + var driver = try TestDriver(args: [ + "swiftc", "-emit-library", "-emit-module", "-module-name", "Test", + "foo.swift", "bar.swift", "-serialize-diagnostics=sarif", + ]) + let plannedJobs = try await driver.planBuild().removingAutolinkExtractJobs() + #expect(plannedJobs.contains { $0.kind == .link }) + + for job in plannedJobs where job.kind != .compile && job.kind != .emitModule { + #expect(!job.commandLine.contains(.flag("-serialize-diagnostics=sarif")), "\(job.kind)") + #expect(!job.outputs.contains { $0.type == .sarifDiagnostics }, "\(job.kind)") + #expect(!job.outputs.contains { $0.type == .emitModuleSARIFDiagnostics }, "\(job.kind)") + } + } + + /// The emit-module job's diagnostics follow the requested format, like the + /// compile jobs'. Without this a SARIF build would drop them, where a binary + /// build keeps them. + @Test func emitModuleJobFollowsRequestedFormat() async throws { + for (format, expectedType) in [ + ("-serialize-diagnostics=sarif", FileType.emitModuleSARIFDiagnostics), + ("-serialize-diagnostics", .emitModuleDiagnostics), + ("-serialize-diagnostics=dia", .emitModuleDiagnostics), + ] { + var driver = try TestDriver(args: [ + "swiftc", "-emit-module", "-module-name", "Test", "foo.swift", "bar.swift", format, + ]) + let plannedJobs = try await driver.planBuild().removingAutolinkExtractJobs() + let emitModuleJob = try #require(plannedJobs.first { $0.kind == .emitModule }, "\(format)") + let diagnostics = emitModuleJob.outputs.filter { + $0.type == .emitModuleDiagnostics || $0.type == .emitModuleSARIFDiagnostics + } + #expect(diagnostics.map(\.type) == [expectedType], "\(format)") + #expect(diagnostics.first?.file.name.hasSuffix(".\(expectedType.rawValue)") == true, "\(format)") + #expect( + emitModuleJob.commandLine.contains(.flag("-serialize-diagnostics=sarif")) + == (expectedType == .emitModuleSARIFDiagnostics), "\(format)") + } + } + + /// The emit-module job's SARIF log takes its path from the output file map's + /// 'emit-module-sarif-diagnostics' entry, which is a single output for the + /// whole module. + @Test func emitModuleUsesOutputFileMapEntry() async throws { + try await withTemporaryDirectory { path in + let ofmPath = path.appending(component: "ofm.json") + try localFileSystem.writeFileContents(ofmPath) { + $0.send( + """ + { + "": { + "emit-module-sarif-diagnostics": "/out/module-from-map.emit-module.sarif", + "emit-module-diagnostics": "/out/module-from-map.emit-module.dia" + } + } + """ + ) + } + var driver = try TestDriver(args: [ + "swiftc", "-emit-module", "-module-name", "Test", "foo.swift", "bar.swift", + "-serialize-diagnostics=sarif", "-output-file-map", ofmPath.pathString, + ]) + let plannedJobs = try await driver.planBuild().removingAutolinkExtractJobs() + let emitModuleJob = try #require(plannedJobs.first { $0.kind == .emitModule }) + let diagnostics = emitModuleJob.outputs.filter { $0.type == .emitModuleSARIFDiagnostics } + #expect(diagnostics.map(\.file) == [.absolute(try .init(validating: "/out/module-from-map.emit-module.sarif"))]) + } + } + + /// An explicit emit-module diagnostics path is used for the SARIF log. + @Test func emitModuleExplicitPathHoldsSARIFLog() async throws { + var driver = try TestDriver(args: [ + "swiftc", "-emit-module", "-module-name", "Test", "foo.swift", "bar.swift", + "-serialize-diagnostics=sarif", + "-emit-module-serialize-diagnostics-path", "/out/explicit.sarif", + ]) + let plannedJobs = try await driver.planBuild().removingAutolinkExtractJobs() + let emitModuleJob = try #require(plannedJobs.first { $0.kind == .emitModule }) + let diagnostics = emitModuleJob.outputs.filter { $0.type == .emitModuleSARIFDiagnostics } + #expect(diagnostics.map(\.file) == [.absolute(try .init(validating: "/out/explicit.sarif"))]) + #expect(emitModuleJob.commandLine.contains(.flag("-serialize-diagnostics=sarif"))) + } + + /// Paths passed in a supplementary output file map still come with the + /// format, which the map does not carry. + @Test func formatAccompaniesSupplementaryOutputFileMap() async throws { + var driver = try TestDriver(args: [ + "swiftc", "-c", "foo.swift", "bar.swift", + "-serialize-diagnostics=sarif", "-driver-filelist-threshold=0", + ]) + let plannedJobs = try await driver.planBuild().removingAutolinkExtractJobs() + let compileJobs = plannedJobs.filter { $0.kind == .compile } + #expect(compileJobs.count == 2) + + for job in compileJobs { + #expect(job.commandLine.contains(.flag("-supplementary-output-file-map"))) + #expect(!job.commandLine.contains(.flag("-serialize-diagnostics-path"))) + #expect(job.outputs.contains { $0.type == .sarifDiagnostics }) + #expect(job.commandLine.contains(.flag("-serialize-diagnostics=sarif"))) + } + } + + /// A PCH job serializes its diagnostics in the requested format, like every + /// other job. Diagnostics raised inside the bridging header are reported only + /// by the job that builds the PCH, so a binary PCH log would hide them from a + /// client reading SARIF. + @Test func pchFollowsRequestedFormat() async throws { + var driver = try TestDriver(args: [ + "swiftc", "-typecheck", "foo.swift", + "-import-objc-header", "header.h", "-pch-output-dir", "/tmp/pch", + "-emit-module-path", "/tmp/main.swiftmodule", + "-serialize-diagnostics=sarif", + ]) + let plannedJobs = try await driver.planBuild().removingAutolinkExtractJobs() + + let pchJobs = plannedJobs.filter { $0.kind == .generatePCH } + #expect(pchJobs.count == 1) + for job in pchJobs { + let diagnostics = job.outputs.filter { $0.type == .sarifDiagnostics } + #expect(diagnostics.count == 1) + let log = try #require(diagnostics.first) + #expect(log.file.parentDirectory == .absolute(try .init(validating: "/tmp/pch"))) + #expect(log.file.basename.hasPrefix("header-")) + #expect(diagnostics.first?.file.extension == "sarif") + #expect(!job.outputs.contains { $0.type == .diagnostics }) + #expect(job.commandLine.contains(.flag("-serialize-diagnostics=sarif"))) + } + + // The compile job serializes to SARIF too. + let compileJobs = plannedJobs.filter { $0.kind == .compile } + #expect(compileJobs.count == 1) + for job in compileJobs { + #expect(job.outputs.contains { $0.type == .sarifDiagnostics }) + #expect(job.commandLine.contains(.flag("-serialize-diagnostics=sarif"))) + } + } + + /// Without a format, a PCH job keeps the binary format. + @Test func pchKeepsBinaryFormatByDefault() async throws { + var driver = try TestDriver(args: [ + "swiftc", "-typecheck", "foo.swift", + "-import-objc-header", "header.h", "-pch-output-dir", "/tmp/pch", + "-serialize-diagnostics", + ]) + let plannedJobs = try await driver.planBuild().removingAutolinkExtractJobs() + + let pchJobs = plannedJobs.filter { $0.kind == .generatePCH } + #expect(pchJobs.count == 1) + for job in pchJobs { + #expect(job.outputs.contains { $0.type == .diagnostics }) + #expect(!job.outputs.contains { $0.type == .sarifDiagnostics }) + #expect(!job.commandLine.contains(.flag("-serialize-diagnostics=sarif"))) + } + } + + /// An unknown format is an error wherever it appears. + @Test func unknownFormatIsRejected() throws { + for formats in [ + ["-serialize-diagnostics=bogus"], + ["-serialize-diagnostics=sarif", "-serialize-diagnostics=bogus"], + ["-serialize-diagnostics=bogus", "-serialize-diagnostics=sarif"], + ["-serialize-diagnostics=bogus", "-serialize-diagnostics"], + ] { + let diagnosticEngine = DiagnosticsEngine() + _ = try TestDriver( + args: ["swiftc", "-typecheck", "foo.swift"] + formats, + diagnosticsEngine: diagnosticEngine) + #expect(diagnosticEngine.hasErrors, "\(formats)") + #expect( + diagnosticEngine.diagnostics.first?.message.data.description + == "unsupported argument 'bogus' to option '-serialize-diagnostics='", "\(formats)") + } + } + + /// Asking for the binary format by name is the same as not naming one. + @Test func explicitDiaFormatMatchesDefault() async throws { + var driver = try TestDriver(args: [ + "swiftc", "-typecheck", "foo.swift", "-serialize-diagnostics=dia", + ]) + let plannedJobs = try await driver.planBuild().removingAutolinkExtractJobs() + let compileJobs = plannedJobs.filter { $0.kind == .compile } + #expect(compileJobs.count == 1) + + for job in compileJobs { + #expect(job.outputs.contains { $0.type == .diagnostics }) + #expect(!job.outputs.contains { $0.type == .sarifDiagnostics }) + #expect(!job.commandLine.contains(.flag("-serialize-diagnostics=sarif"))) + #expect(!job.commandLine.contains(.flag("-serialize-diagnostics=dia"))) + } + } + + /// The last of the bare flag, which means 'dia', and a format wins. + @Test func lastFormatArgumentWins() async throws { + for (args, expected) in [ + (["-serialize-diagnostics=sarif", "-serialize-diagnostics=dia"], FileType.diagnostics), + (["-serialize-diagnostics=dia", "-serialize-diagnostics=sarif"], .sarifDiagnostics), + (["-serialize-diagnostics", "-serialize-diagnostics=sarif"], .sarifDiagnostics), + (["-serialize-diagnostics=sarif", "-serialize-diagnostics"], .diagnostics), + ] { + var driver = try TestDriver(args: ["swiftc", "-typecheck", "foo.swift"] + args) + let plannedJobs = try await driver.planBuild().removingAutolinkExtractJobs() + let compileJobs = plannedJobs.filter { $0.kind == .compile } + #expect(compileJobs.count == 1) + + for job in compileJobs { + #expect(job.outputs.contains { $0.type == expected }, "\(args)") + #expect( + job.commandLine.contains(.flag("-serialize-diagnostics=sarif")) + == (expected == .sarifDiagnostics), "\(args)") + } + } + } + + /// An output file map's 'sarif-diagnostics' entry names the SARIF log, and + /// its 'diagnostics' entry names the binary one; each is used only for its + /// own format. + @Test func outputFileMapNamesEachFormat() async throws { + try await withTemporaryDirectory { path in + let foo = path.appending(component: "foo.swift") + let ofmPath = path.appending(component: "ofm.json") + try localFileSystem.writeFileContents(ofmPath) { + $0.send( + """ + { + "\(foo.nativePathString(escaped: true))": { + "sarif-diagnostics": "/out/foo-from-map.sarif", + "diagnostics": "/out/foo-from-map.dia" + } + } + """ + ) + } + + for (format, expectedType, expectedPath) in [ + ("-serialize-diagnostics=sarif", FileType.sarifDiagnostics, "/out/foo-from-map.sarif"), + ("-serialize-diagnostics", .diagnostics, "/out/foo-from-map.dia"), + ] { + var driver = try TestDriver(args: [ + "swiftc", "-typecheck", foo.pathString, format, + "-output-file-map", ofmPath.pathString, + ]) + let plannedJobs = try await driver.planBuild().removingAutolinkExtractJobs() + let compileJob = try #require(plannedJobs.first { $0.kind == .compile }) + let diagnostics = compileJob.outputs.filter { $0.type == expectedType } + #expect(diagnostics.map(\.file) == [.absolute(try .init(validating: expectedPath))], "\(format)") + #expect( + compileJob.commandLine.contains( + .path(.absolute(try .init(validating: expectedPath)))), "\(format)") + } + } + } + + /// In whole-module mode the single compile job takes its log path from the + /// output file map's module-wide entry for the requested format. + @Test func wholeModuleUsesModuleEntryForEachFormat() async throws { + try await withTemporaryDirectory { path in + let ofmPath = path.appending(component: "ofm.json") + try localFileSystem.writeFileContents(ofmPath) { + $0.send( + """ + { + "": { + "sarif-diagnostics": "/out/module-from-map.sarif", + "diagnostics": "/out/module-from-map.dia" + } + } + """ + ) + } + + for (format, expectedType, expectedPath) in [ + ("-serialize-diagnostics=sarif", FileType.sarifDiagnostics, "/out/module-from-map.sarif"), + ("-serialize-diagnostics", .diagnostics, "/out/module-from-map.dia"), + ] { + var driver = try TestDriver(args: [ + "swiftc", "-typecheck", "-wmo", "-module-name", "Test", "foo.swift", "bar.swift", + format, "-output-file-map", ofmPath.pathString, + ]) + let plannedJobs = try await driver.planBuild().removingAutolinkExtractJobs() + let compileJobs = plannedJobs.filter { $0.kind == .compile } + #expect(compileJobs.count == 1, "\(format)") + let compileJob = try #require(compileJobs.first, "\(format)") + let diagnostics = compileJob.outputs.filter { + $0.type == .diagnostics || $0.type == .sarifDiagnostics + } + #expect(diagnostics.map(\.type) == [expectedType], "\(format)") + #expect(diagnostics.map(\.file) == [.absolute(try .init(validating: expectedPath))], "\(format)") + #expect( + compileJob.commandLine.contains(.flag("-serialize-diagnostics=sarif")) + == (expectedType == .sarifDiagnostics), "\(format)") + } + } + } + + /// A PCH job also takes its SARIF log path from the output file map. + @Test func pchUsesOutputFileMapEntry() async throws { + try await withTemporaryDirectory { path in + let header = path.appending(component: "header.h") + let ofmPath = path.appending(component: "ofm.json") + try localFileSystem.writeFileContents(ofmPath) { + $0.send( + """ + { + "\(header.nativePathString(escaped: true))": { + "sarif-diagnostics": "/out/header-from-map.sarif", + "diagnostics": "/out/header-from-map.dia" + } + } + """ + ) + } + var driver = try TestDriver(args: [ + "swiftc", "-typecheck", "foo.swift", + "-import-objc-header", header.pathString, "-pch-output-dir", "/tmp/pch", + "-output-file-map", ofmPath.pathString, + "-serialize-diagnostics=sarif", + ]) + let plannedJobs = try await driver.planBuild().removingAutolinkExtractJobs() + let pchJob = try #require(plannedJobs.first { $0.kind == .generatePCH }) + let diagnostics = pchJob.outputs.filter { $0.type == .sarifDiagnostics } + #expect(diagnostics.map(\.file) == [.absolute(try .init(validating: "/out/header-from-map.sarif"))]) + #expect(pchJob.commandLine.contains(.flag("-serialize-diagnostics=sarif"))) + } + } + + /// Without a module path to derive a name from, a persistent PCH's SARIF log + /// is a temporary file. + @Test func pchWithoutModulePathUsesTemporarySARIFFile() async throws { + var driver = try TestDriver(args: [ + "swiftc", "-typecheck", "foo.swift", + "-import-objc-header", "header.h", "-pch-output-dir", "/tmp/pch", + "-serialize-diagnostics=sarif", + ]) + let plannedJobs = try await driver.planBuild().removingAutolinkExtractJobs() + let pchJob = try #require(plannedJobs.first { $0.kind == .generatePCH }) + let diagnostics = pchJob.outputs.filter { $0.type == .sarifDiagnostics } + #expect(diagnostics.count == 1) + #expect(diagnostics.first?.file.isTemporary == true) + #expect(diagnostics.first?.file.extension == "sarif") + #expect(pchJob.commandLine.contains(.flag("-serialize-diagnostics=sarif"))) + } + + /// A SARIF log is named after the primary output it accompanies. + @Test func sarifLogSitsBesideObjectFile() async throws { + var driver = try TestDriver(args: [ + "swiftc", "-c", "foo.swift", "bar.swift", "-serialize-diagnostics=sarif", + ]) + let plannedJobs = try await driver.planBuild().removingAutolinkExtractJobs() + let compileJobs = plannedJobs.filter { $0.kind == .compile } + #expect(compileJobs.count == 2) + + for job in compileJobs { + let object = try #require(job.outputs.first { $0.type == .object }) + let diagnostics = try #require(job.outputs.first { $0.type == .sarifDiagnostics }) + #expect(!diagnostics.file.isTemporary) + #expect(diagnostics.file.basenameWithoutExt == object.file.basenameWithoutExt) + #expect(diagnostics.file.extension == "sarif") + } + } + + /// A batch job with several primaries carries one SARIF log per primary and + /// passes the format once. + @Test func batchModeJobCarriesFormat() async throws { + var driver = try TestDriver(args: [ + "swiftc", "-c", "foo.swift", "bar.swift", "-enable-batch-mode", + "-driver-batch-count", "1", "-serialize-diagnostics=sarif", + ]) + let plannedJobs = try await driver.planBuild().removingAutolinkExtractJobs() + let compileJobs = plannedJobs.filter { $0.kind == .compile } + #expect(compileJobs.count == 1) + + for job in compileJobs { + #expect(job.outputs.filter { $0.type == .sarifDiagnostics }.count == 2) + #expect( + job.commandLine.filter { $0 == .flag("-serialize-diagnostics=sarif") }.count == 1) + } + } + + /// swiftc chooses log paths itself, so it rejects an explicit one either way. + @Test func explicitDiagnosticsPathIsRejected() throws { + for format in ["-serialize-diagnostics=sarif", "-serialize-diagnostics"] { + #expect(throws: OptionParseError.self, "\(format)") { + _ = try TestDriver(args: [ + "swiftc", "-typecheck", "foo.swift", format, + "-serialize-diagnostics-path", "/tmp/explicit.out", + ]) + } + } + } + + /// The dependency scan's diagnostics follow the requested format. An import + /// that cannot be resolved is reported by the scan, before any compile job + /// runs, so a SARIF build would otherwise have no log of it at all. + @Test func dependencyScanFollowsRequestedFormat() throws { + for (format, expectedExtension) in [ + ("-serialize-diagnostics=sarif", FileType.dependencyScanSARIFDiagnostics.rawValue), + ("-serialize-diagnostics", FileType.dependencyScanDiagnostics.rawValue), + ("-serialize-diagnostics=dia", FileType.dependencyScanDiagnostics.rawValue), + ] { + var driver = try TestDriver(args: [ + "swiftc", "-explicit-module-build", "-module-name", "Test", "foo.swift", format, + ]) + let resolver = try ArgsResolver(fileSystem: localFileSystem) + let scannerCommand = try driver.dependencyScannerInvocationCommand().1.map { + try resolver.resolve($0) + } + let pathIndex = try #require( + scannerCommand.firstIndex(of: "-serialize-diagnostics-path"), "\(format)") + #expect(scannerCommand[pathIndex + 1].hasSuffix("Test.\(expectedExtension)"), "\(format)") + #expect( + scannerCommand.contains("-serialize-diagnostics=sarif") + == (expectedExtension == FileType.dependencyScanSARIFDiagnostics.rawValue), + "\(format)") + } + } + + /// An explicit dependency-scan diagnostics path is used for the SARIF log. + @Test func dependencyScanExplicitPathHoldsSARIFLog() throws { + var driver = try TestDriver(args: [ + "swiftc", "-explicit-module-build", "-module-name", "Test", "foo.swift", + "-serialize-diagnostics=sarif", + "-dependency-scan-serialize-diagnostics-path", "/out/scan.sarif", + ]) + let resolver = try ArgsResolver(fileSystem: localFileSystem) + let scannerCommand = try driver.dependencyScannerInvocationCommand().1.map { + try resolver.resolve($0) + } + #expect(scannerCommand.contains( + subsequence: [ + "-serialize-diagnostics-path", try AbsolutePath(validating: "/out/scan.sarif").pathString, + ])) + #expect(scannerCommand.contains("-serialize-diagnostics=sarif")) + } + + /// In whole-module mode the dependency scan takes its log path from the + /// output file map's module-wide entry for the requested format. + @Test func dependencyScanUsesModuleEntryForEachFormat() throws { + try withTemporaryDirectory { path in + let ofmPath = path.appending(component: "ofm.json") + try localFileSystem.writeFileContents(ofmPath) { + $0.send( + """ + { + "": { + "dependency-scan-sarif-diagnostics": "/out/scan-from-map.sarif", + "dependency-scan-diagnostics": "/out/scan-from-map.dia" + } + } + """ + ) + } + + for (format, expectedPath) in [ + ("-serialize-diagnostics=sarif", "/out/scan-from-map.sarif"), + ("-serialize-diagnostics", "/out/scan-from-map.dia"), + ] { + var driver = try TestDriver(args: [ + "swiftc", "-explicit-module-build", "-wmo", "-module-name", "Test", "foo.swift", + format, "-output-file-map", ofmPath.pathString, + ]) + let resolver = try ArgsResolver(fileSystem: localFileSystem) + let scannerCommand = try driver.dependencyScannerInvocationCommand().1.map { + try resolver.resolve($0) + } + #expect( + scannerCommand.contains( + subsequence: [ + "-serialize-diagnostics-path", try AbsolutePath(validating: expectedPath).pathString, + ]), + "\(format)") + #expect( + scannerCommand.contains("-serialize-diagnostics=sarif") + == expectedPath.hasSuffix(".sarif"), "\(format)") + } + } + } + + /// The new file type's public properties. + @Test func sarifDiagnosticsFileType() { + #expect(FileType.sarifDiagnostics.rawValue == "sarif") + #expect(FileType.sarifDiagnostics.description == "sarif-diagnostics") + #expect(!FileType.sarifDiagnostics.isPartOfSwiftCompilation) + #expect(FileType.emitModuleSARIFDiagnostics.rawValue == "emit-module.sarif") + #expect(FileType.emitModuleSARIFDiagnostics.description == "emit-module-sarif-diagnostics") + #expect(!FileType.emitModuleSARIFDiagnostics.isPartOfSwiftCompilation) + #expect(FileType.dependencyScanSARIFDiagnostics.rawValue == "dependency-scan.sarif") + #expect(FileType.dependencyScanSARIFDiagnostics.description == "dependency-scan-sarif-diagnostics") + #expect(!FileType.dependencyScanSARIFDiagnostics.isPartOfSwiftCompilation) + } +}