diff --git a/Sources/SWBApplePlatform/CMakeLists.txt b/Sources/SWBApplePlatform/CMakeLists.txt index fee2e57a1..836ea7fa6 100644 --- a/Sources/SWBApplePlatform/CMakeLists.txt +++ b/Sources/SWBApplePlatform/CMakeLists.txt @@ -38,6 +38,7 @@ add_library(SWBApplePlatform AssetCatalogCompilerOutputParser.swift CMakeLists.txt DevelopmentAssetsTaskProducer.swift + EntityLinkerTool.swift ImageScaleFactorsInputFileGroupingStrategy.swift InterfaceBuilderCompilerOutputParser.swift LocalizationInputFileGroupingStrategy.swift @@ -46,6 +47,7 @@ add_library(SWBApplePlatform PluginDCLT.swift RealityAssetsTaskProducer.swift RegisterExecutionPolicyException.swift + SsafAnalyzerTool.swift StringCatalogCompilerOutputParser.swift StubBinaryTaskProducer.swift XCStringsInputFileGroupingStrategy.swift) @@ -70,6 +72,7 @@ SwiftBuild_Bundle(MODULE SWBApplePlatform FILES Specs/Embedded-Simulator.xcspec Specs/EmbeddedBinaryValidationUtility.xcspec Specs/EXUtil.xcspec + Specs/EntityLinker.xcspec Specs/GenerateAppPlaygroundAssetCatalog.xcspec Specs/GenerateTextureAtlas.xcspec Specs/IBCompiler.xcspec @@ -110,6 +113,7 @@ SwiftBuild_Bundle(MODULE SWBApplePlatform FILES Specs/SceneKitFileTypes.xcspec Specs/SceneKitTools.xcspec Specs/SpriteKitFileTypes.xcspec + Specs/SsafAnalyzer.xcspec Specs/StripSymbolsDarwin.xcspec Specs/TiffUtil.xcspec Specs/tvOSDevice.xcspec diff --git a/Sources/SWBApplePlatform/EntityLinkerTool.swift b/Sources/SWBApplePlatform/EntityLinkerTool.swift index a240f2d4c..b6bb9384d 100644 --- a/Sources/SWBApplePlatform/EntityLinkerTool.swift +++ b/Sources/SWBApplePlatform/EntityLinkerTool.swift @@ -29,14 +29,6 @@ public final class EntityLinkerToolSpec: GenericCommandLineToolSpec, SpecIdentif override public func discoveredCommandLineToolSpecInfo(_ producer: any CommandProducer, _ scope: MacroEvaluationScope, _ delegate: any CoreClientTargetDiagnosticProducingDelegate) async -> (any DiscoveredCommandLineToolSpecInfo)? { let toolPath = self.resolveExecutablePath(producer, Path("clang-ssaf-linker")) - - do { - return try await DiscoveredEntityLinkerToolSpecInfo.parseProjectNameAndSourceVersionStyleVersionInfo(producer, delegate, commandLine: [toolPath.str, "version"]) { versionInfo in - DiscoveredEntityLinkerToolSpecInfo(toolPath: toolPath, toolVersion: versionInfo.version) - } - } catch { - delegate.error(error) - return nil - } + return DiscoveredEntityLinkerToolSpecInfo(toolPath: toolPath, toolVersion: nil) } } diff --git a/Sources/SWBApplePlatform/Plugin.swift b/Sources/SWBApplePlatform/Plugin.swift index 8a27246a3..35790dca9 100644 --- a/Sources/SWBApplePlatform/Plugin.swift +++ b/Sources/SWBApplePlatform/Plugin.swift @@ -131,6 +131,8 @@ struct ApplePlatformSpecsExtension: SpecificationsExtension { [ AppExtensionPlistGeneratorSpec.self, AppIntentsMetadataCompilerSpec.self, + EntityLinkerToolSpec.self, + SsafAnalyzerToolSpec.self, AppIntentsSSUTrainingCompilerSpec.self, ExtensionPointExtractorSpec.self, ActoolCompilerSpec.self, diff --git a/Sources/SWBApplePlatform/Specs/SsafAnalyzer.xcspec b/Sources/SWBApplePlatform/Specs/SsafAnalyzer.xcspec new file mode 100644 index 000000000..9d94bed0e --- /dev/null +++ b/Sources/SWBApplePlatform/Specs/SsafAnalyzer.xcspec @@ -0,0 +1,30 @@ +//===----------------------------------------------------------------------===// +// +// 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 with Runtime Library Exception +// +// See http://swift.org/LICENSE.txt for license information +// See http://swift.org/CONTRIBUTORS.txt for the list of Swift project authors +// +//===----------------------------------------------------------------------===// + +( + { + Type = Compiler; + Identifier = com.apple.build-tools.clang-ssaf-analyzer; + ExecPath = "clang-ssaf-analyzer"; + Name = "SSAF Analyzer Tool"; + Description = "Analyzes linked entity summary files"; + CommandLine = "clang-ssaf-analyzer [special-args] [inputs] -o $(OutputPath)"; + RuleName = "AnalyzeSSAF $(OutputPath)"; + ExecDescription = "Analyzing Summary files"; + ProgressDescription = "Analyzing $(OutputPath)"; + InputFileTypes = (); + CommandOutputParser = XCGenericCommandOutputParser; + InputFileGroupings = (); + SynthesizeBuildRule = YES; + IsArchitectureNeutral = NO; + } +) diff --git a/Sources/SWBApplePlatform/SsafAnalyzerTool.swift b/Sources/SWBApplePlatform/SsafAnalyzerTool.swift new file mode 100644 index 000000000..bcb490b4a --- /dev/null +++ b/Sources/SWBApplePlatform/SsafAnalyzerTool.swift @@ -0,0 +1,34 @@ +//===----------------------------------------------------------------------===// +// +// 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 with Runtime Library Exception +// +// See http://swift.org/LICENSE.txt for license information +// See http://swift.org/CONTRIBUTORS.txt for the list of Swift project authors +// +//===----------------------------------------------------------------------===// + +import Foundation +public import SWBUtil +public import SWBCore +public import SWBMacro + + +public final class SsafAnalyzerToolSpec: GenericCommandLineToolSpec, SpecIdentifierType, @unchecked Sendable { + public static let identifier = "com.apple.build-tools.clang-ssaf-analyzer" + required public init(_ parser: SpecParser, _ basedOnSpec: Spec?) { + super.init(parser, basedOnSpec) + } + + public struct DiscoveredSsafAnalyzerToolSpecInfo: DiscoveredCommandLineToolSpecInfo { + public let toolPath: Path + public var toolVersion: Version? + } + + override public func discoveredCommandLineToolSpecInfo(_ producer: any CommandProducer, _ scope: MacroEvaluationScope, _ delegate: any CoreClientTargetDiagnosticProducingDelegate) async -> (any DiscoveredCommandLineToolSpecInfo)? { + let toolPath = self.resolveExecutablePath(producer, Path("clang-ssaf-analyzer")) + return DiscoveredSsafAnalyzerToolSpecInfo(toolPath: toolPath, toolVersion: nil) + } +} diff --git a/Sources/SWBCore/Settings/BuiltinMacros.swift b/Sources/SWBCore/Settings/BuiltinMacros.swift index 7f95842ba..3b2df2a85 100644 --- a/Sources/SWBCore/Settings/BuiltinMacros.swift +++ b/Sources/SWBCore/Settings/BuiltinMacros.swift @@ -1023,6 +1023,7 @@ public final class BuiltinMacros { public static let RUN_CLANG_STATIC_ANALYZER = BuiltinMacros.declareBooleanMacro("RUN_CLANG_STATIC_ANALYZER") public static let INVOKE_SSAF = BuiltinMacros.declareBooleanMacro("INVOKE_SSAF") public static let EXTRACT_SUMMARIES = BuiltinMacros.declareStringMacro("EXTRACT_SUMMARIES") + public static let STOP_AT_LU_SUMMARY_GENERATION = BuiltinMacros.declareStringListMacro("STOP_AT_LU_SUMMARY_GENERATION") public static let SWIFT_API_DIGESTER_MODE = BuiltinMacros.declareEnumMacro("SWIFT_API_DIGESTER_MODE") as EnumMacroDeclaration public static let RUN_SWIFT_ABI_CHECKER_TOOL = BuiltinMacros.declareBooleanMacro("RUN_SWIFT_ABI_CHECKER_TOOL") public static let RUN_SWIFT_ABI_CHECKER_TOOL_DRIVER = BuiltinMacros.declareBooleanMacro("RUN_SWIFT_ABI_CHECKER_TOOL_DRIVER") @@ -2242,6 +2243,7 @@ public final class BuiltinMacros { RUN_CLANG_STATIC_ANALYZER, INVOKE_SSAF, EXTRACT_SUMMARIES, + STOP_AT_LU_SUMMARY_GENERATION, RUN_DOCUMENTATION_COMPILER, SKIP_BUILDING_DOCUMENTATION, RUN_SYMBOL_GRAPH_EXTRACT, diff --git a/Sources/SWBCore/TaskGeneration.swift b/Sources/SWBCore/TaskGeneration.swift index bbd564dac..4c6d21865 100644 --- a/Sources/SWBCore/TaskGeneration.swift +++ b/Sources/SWBCore/TaskGeneration.swift @@ -150,6 +150,7 @@ public protocol CommandProducer: PlatformBuildContext, SpecLookupContext, Refere var clangStaticAnalyzerSpec: ClangCompilerSpec { get } var entityLinkerToolSpec: CommandLineToolSpec { get } + var ssafAnalyzerToolSpec: CommandLineToolSpec { get } /// The Clang modules verifier tool spec to use. var clangModuleVerifierSpec: ClangCompilerSpec { get } diff --git a/Sources/SWBTaskConstruction/TaskProducers/BuildPhaseTaskProducers/SourcesTaskProducer.swift b/Sources/SWBTaskConstruction/TaskProducers/BuildPhaseTaskProducers/SourcesTaskProducer.swift index fea73ee03..95a25057d 100644 --- a/Sources/SWBTaskConstruction/TaskProducers/BuildPhaseTaskProducers/SourcesTaskProducer.swift +++ b/Sources/SWBTaskConstruction/TaskProducers/BuildPhaseTaskProducers/SourcesTaskProducer.swift @@ -1004,6 +1004,16 @@ package final class SourcesTaskProducer: FilesBasedBuildPhaseTaskProducerBase, F let output = Path(binaryOutput.str + ".linked-summaries.json") await context.entityLinkerToolSpec.constructTasks(CommandBuildContext(producer: context, scope: scope, inputs: ssafInputs, output: output), delegate) } + await appendGeneratedTasks(&perArchTasks) { delegate in + let linkedSummariesInput = Path(binaryOutput.str + ".linked-summaries.json") + let analyzerOutput = Path(binaryOutput.str + ".ssaf-analysis.json") + let analysisName = scope.evaluate(BuiltinMacros.EXTRACT_SUMMARIES) + let stopAtAnalyses = Set(scope.evaluate(BuiltinMacros.STOP_AT_LU_SUMMARY_GENERATION)) + let specialArgs = analysisName.split(separator: ",") + .filter { !stopAtAnalyses.contains(String($0)) } + .flatMap { ["-a", "\($0)AnalysisResult"] } + await context.ssafAnalyzerToolSpec.constructTasks(CommandBuildContext(producer: context, scope: scope, inputs: [FileToBuild(context: context, absolutePath: linkedSummariesInput)], output: analyzerOutput), delegate, specialArgs: specialArgs) + } } // Handle linking prelinked objects. Presently we always do this if GENERATE_PRELINK_OBJECT_FILE even if there are no other tasks, since PRELINK_LIBS or PRELINK_FLAGS might be set to values which will cause a prelinked object file to be generated. diff --git a/Sources/SWBTaskConstruction/TaskProducers/TaskProducer.swift b/Sources/SWBTaskConstruction/TaskProducers/TaskProducer.swift index b7e9bb9d6..9dd811008 100644 --- a/Sources/SWBTaskConstruction/TaskProducers/TaskProducer.swift +++ b/Sources/SWBTaskConstruction/TaskProducers/TaskProducer.swift @@ -234,6 +234,7 @@ public class TaskProducerContext: StaleFileRemovalContext, BuildFileResolution public let clangPreprocessorSpec: ClangCompilerSpec public let clangStaticAnalyzerSpec: ClangCompilerSpec public let entityLinkerToolSpec: CommandLineToolSpec + public let ssafAnalyzerToolSpec: CommandLineToolSpec public let clangModuleVerifierSpec: ClangCompilerSpec private let _clangStatCacheSpec: Result var clangStatCacheSpec: ClangStatCacheSpec? { return specForResult(_clangStatCacheSpec) } @@ -358,6 +359,7 @@ public class TaskProducerContext: StaleFileRemovalContext, BuildFileResolution self.clangPreprocessorSpec = try! workspaceContext.core.specRegistry.getSpec(domain: domain, ofType: ClangPreprocessorSpec.self) self.clangStaticAnalyzerSpec = try! workspaceContext.core.specRegistry.getSpec(domain: domain, ofType: ClangStaticAnalyzerSpec.self) self.entityLinkerToolSpec = try! workspaceContext.core.specRegistry.getSpec("com.apple.build-tools.clang-ssaf-linker", domain: domain, ofType: CommandLineToolSpec.self) + self.ssafAnalyzerToolSpec = try! workspaceContext.core.specRegistry.getSpec("com.apple.build-tools.clang-ssaf-analyzer", domain: domain, ofType: CommandLineToolSpec.self) self.clangModuleVerifierSpec = try! workspaceContext.core.specRegistry.getSpec(domain: domain, ofType: ClangModuleVerifierSpec.self) self._clangStatCacheSpec = Result { try workspaceContext.core.specRegistry.getSpec("com.apple.compilers.clang-stat-cache", ofType: ClangStatCacheSpec.self) } self.codesignSpec = try! workspaceContext.core.specRegistry.getSpec("com.apple.build-tools.codesign", domain: domain, ofType: CodesignToolSpec.self) diff --git a/Sources/SWBTestSupport/DummyCommandProducer.swift b/Sources/SWBTestSupport/DummyCommandProducer.swift index 2b957eceb..99cc82e65 100644 --- a/Sources/SWBTestSupport/DummyCommandProducer.swift +++ b/Sources/SWBTestSupport/DummyCommandProducer.swift @@ -128,6 +128,7 @@ package struct MockCommandProducer: CommandProducer, Sendable { self.clangPreprocessorSpec = try getSpec(ofType: ClangPreprocessorSpec.self) self.clangStaticAnalyzerSpec = try getSpec(ofType: ClangStaticAnalyzerSpec.self) self.entityLinkerToolSpec = try getSpec("com.apple.build-tools.clang-ssaf-linker", ofType: CommandLineToolSpec.self) + self.ssafAnalyzerToolSpec = try getSpec("com.apple.build-tools.clang-ssaf-analyzer", ofType: CommandLineToolSpec.self) self.clangModuleVerifierSpec = try getSpec(ofType: ClangModuleVerifierSpec.self) self.diffSpec = try getSpec("com.apple.build-tools.diff", ofType: CommandLineToolSpec.self) self.stripSpec = try getSpec("com.apple.build-tools.strip", ofType: StripToolSpec.self) @@ -165,6 +166,7 @@ package struct MockCommandProducer: CommandProducer, Sendable { package let clangPreprocessorSpec: ClangCompilerSpec package let clangStaticAnalyzerSpec: ClangCompilerSpec package let entityLinkerToolSpec: CommandLineToolSpec + package let ssafAnalyzerToolSpec: CommandLineToolSpec package let clangModuleVerifierSpec: ClangCompilerSpec package let diffSpec: CommandLineToolSpec package let stripSpec: StripToolSpec diff --git a/Sources/SWBUniversalPlatform/Specs/Clang.xcspec b/Sources/SWBUniversalPlatform/Specs/Clang.xcspec index 64219c273..ca0ab715c 100644 --- a/Sources/SWBUniversalPlatform/Specs/Clang.xcspec +++ b/Sources/SWBUniversalPlatform/Specs/Clang.xcspec @@ -3482,6 +3482,13 @@ { Name = EXTRACT_SUMMARIES; Type = String; DefaultValue = ""; + Condition = "$(INVOKE_SSAF)"; + Category = "SAPolicy"; + }, + { Name = STOP_AT_LU_SUMMARY_GENERATION; + Type = StringList; + DefaultValue = ""; + Condition = "$(INVOKE_SSAF)"; Category = "SAPolicy"; }, // This entry exists for string expansion in 'RuleName'. diff --git a/Tests/SWBBuildSystemTests/SwiftBuildOperationTests.swift b/Tests/SWBBuildSystemTests/SwiftBuildOperationTests.swift index 60c16eeb8..5cbe71957 100644 --- a/Tests/SWBBuildSystemTests/SwiftBuildOperationTests.swift +++ b/Tests/SWBBuildSystemTests/SwiftBuildOperationTests.swift @@ -491,6 +491,112 @@ fileprivate struct SwiftBuildOperationTests: CoreBasedTests { } } + @Test(.requireSDKs(.host), .requireClangFeatures(.invokeSsaf)) + func invokeSsafCommandLineFlagsUnsafeBuffer() async throws { + func makeTestWorkspace(_ tmpDirPath: Path, invokeSSAF: String, extractSummaries: String = "") -> TestWorkspace { + TestWorkspace( + "Test", + sourceRoot: tmpDirPath.join("Test"), + projects: [ + TestProject( + "aProject", + groupTree: TestGroup("Sources", children: [TestFile("File1.cpp")]), + buildConfigurations: [TestBuildConfiguration( + "Debug", + buildSettings: [ + "PRODUCT_NAME": "$(TARGET_NAME)", + "INVOKE_SSAF": invokeSSAF, + "EXTRACT_SUMMARIES": extractSummaries, + // Uncomment to test with a local build of clang + // "CC": "/bin/clang", + "CODE_SIGNING_ALLOWED": "NO", + ])], + targets: [ + TestStandardTarget( + "Test", + type: .dynamicLibrary, + buildPhases: [TestSourcesBuildPhase(["File1.cpp"])]) + ]) + ]) + } + + // INVOKE_SSAF=YES: both flags are present and the summary file path is co-located with + // the object file, sharing the same basename but with a .json extension. + try await withTemporaryDirectory { tmpDirPath in + let tester = try await BuildOperationTester(getCore(), makeTestWorkspace(tmpDirPath, invokeSSAF: "YES", extractSummaries: "UnsafeBufferUsage"), simulated: false) + try await tester.fs.writeFileContents(tmpDirPath.join("Test/aProject/File1.cpp")) { + $0 <<< "inline int shared_inline(int *p) {\n" + $0 <<< " int * l = p;\n" + $0 <<< " return l[5];\n" + $0 <<< "}\n" + $0 <<< "\n" + $0 <<< "int f(int *x) {\n" + $0 <<< " return shared_inline(x);\n" + $0 <<< "}\n" + $0 <<< "int g(int *y) {\n" + $0 <<< " return shared_inline(y);\n" + $0 <<< "}\n" + } + try await tester.checkBuild(runDestination: .host) { results in + try results.checkTask(.matchRuleType("CompileC")) { task throws in + let objectPath = try #require(task.outputPaths.first { $0.str.hasSuffix(".o") }) + let expectedJsonPath = objectPath.dirname.join(objectPath.basenameWithoutSuffix + ".ssaf-tu.json").str + task.checkCommandLineContains(["--ssaf-extract-summaries=UnsafeBufferUsage"]) + task.checkCommandLineContains(["--ssaf-tu-summary-file=\(expectedJsonPath)"]) + + let jsonBytes = try tester.fs.read(Path(expectedJsonPath)) + #expect(!jsonBytes.isEmpty) + } + // The entity linker should receive File1.ssaf-tu.json as input and produce a .linked-summaries.json file. + try results.checkTask(.matchRuleType("LinkEntity")) { task throws in + #expect(task.inputPaths.contains(where: { $0.str.hasSuffix("File1.ssaf-tu.json") })) + let linkedSummaryPath = try #require(task.outputPaths.first { $0.str.hasSuffix(".linked-summaries.json") }) + #expect(tester.fs.exists(linkedSummaryPath)) + let linkedSummaryBytes = try tester.fs.read(linkedSummaryPath) + #expect(!linkedSummaryBytes.isEmpty) + } + // The analyzer should receive the .linked-summaries.json as input, pass -a UnsafeBufferReachableAnalysisResult, + // and produce a non-empty .ssaf-analysis.json file. + try results.checkTask(.matchRuleType("AnalyzeSSAF")) { task throws in + #expect(task.inputPaths.contains(where: { $0.str.hasSuffix(".linked-summaries.json") })) + task.checkCommandLineContains(["-a", "UnsafeBufferUsageAnalysisResult"]) + let analysisPath = try #require(task.outputPaths.first { $0.str.hasSuffix(".ssaf-analysis.json") }) + #expect(tester.fs.exists(analysisPath)) + let analysisBytes = try tester.fs.read(analysisPath) + #expect(!analysisBytes.isEmpty) + } + results.checkNoDiagnostics() + } + } + + // INVOKE_SSAF=NO: neither SSAF flag is present and no entity linker or analyzer task is created. + try await withTemporaryDirectory { tmpDirPath in + let tester = try await BuildOperationTester(getCore(), makeTestWorkspace(tmpDirPath, invokeSSAF: "NO"), simulated: false) + try await tester.fs.writeFileContents(tmpDirPath.join("Test/aProject/File1.cpp")) { + $0 <<< "inline int shared_inline(int *p) {\n" + $0 <<< " int * l = p;\n" + $0 <<< " return l[5];\n" + $0 <<< "}\n" + $0 <<< "\n" + $0 <<< "int f(int *x) {\n" + $0 <<< " return shared_inline(x);\n" + $0 <<< "}\n" + $0 <<< "int g(int *y) {\n" + $0 <<< " return shared_inline(y);\n" + $0 <<< "}\n" + } + try await tester.checkBuild(runDestination: .host) { results in + results.checkTask(.matchRuleType("CompileC")) { task in + task.checkCommandLineNoMatch([.prefix("--ssaf-extract-summaries=")]) + task.checkCommandLineNoMatch([.prefix("--ssaf-tu-summary-file=")]) + } + results.checkNoTask(.matchRuleType("LinkEntity")) + results.checkNoTask(.matchRuleType("AnalyzeSSAF")) + results.checkNoDiagnostics() + } + } + } + @Test(.requireSDKs(.host)) func avoidEmitModuleSourceInfo() async throws { try await withTemporaryDirectory { tmpDirPath async throws -> Void in diff --git a/Tests/SWBTaskConstructionTests/ClangTests.swift b/Tests/SWBTaskConstructionTests/ClangTests.swift index 6b711c6b7..90f85a897 100644 --- a/Tests/SWBTaskConstructionTests/ClangTests.swift +++ b/Tests/SWBTaskConstructionTests/ClangTests.swift @@ -530,4 +530,88 @@ fileprivate struct ClangTests: CoreBasedTests { } } } + + @Test(.requireSDKs(.host), .requireClangFeatures(.invokeSsaf)) + func invokeSsafOptionsMultiple() async throws { + func getTestProject(invokeSSAF: String, extractSummaries: String = "", stopAtLUSummaryGeneration: String = "") -> TestProject { + TestProject( + "aProject", + groupTree: TestGroup( + "SomeFiles", + children: [ + TestFile("File1.c"), + ]), + buildConfigurations: [ + TestBuildConfiguration( + "Debug", + buildSettings: [ + "PRODUCT_NAME": "$(TARGET_NAME)", + "INVOKE_SSAF": invokeSSAF, + "EXTRACT_SUMMARIES": extractSummaries, + "STOP_AT_LU_SUMMARY_GENERATION": stopAtLUSummaryGeneration, + // Uncomment to test with a local build of clang + // "CC": "/bin/clang", + ]), + ], + targets: [ + TestStandardTarget( + "Test", + type: .dynamicLibrary, + buildPhases: [ + TestSourcesBuildPhase(["File1.c"]), + ] + ), + ]) + } + + let core = try await getCore() + + // When INVOKE_SSAF is YES, the extract-summaries value and a .ssaf-tu.json summary file path are added. + // The summary file is co-located with the object file: same directory, same basename, .ssaf-tu.json extension. + do { + let tester = try TaskConstructionTester(core, getTestProject(invokeSSAF: "YES", extractSummaries: "CallGraph,UnsafeBufferUsage", stopAtLUSummaryGeneration: "CallGraph")) + await tester.checkBuild(runDestination: .host) { results in + results.checkTask(.matchRuleType("CompileC")) { task in + task.checkCommandLineContains(["--ssaf-extract-summaries=CallGraph,UnsafeBufferUsage"]) + if let objectPath = task.outputs.map({ $0.path }).first(where: { $0.str.hasSuffix(".o") }) { + let expectedJsonPath = objectPath.dirname.join(objectPath.basenameWithoutSuffix + ".ssaf-tu.json").str + task.checkCommandLineContains(["--ssaf-tu-summary-file=\(expectedJsonPath)"]) + } else { + Issue.record("No .o output found in CompileC task outputs") + } + } + // The entity linker task should receive the .ssaf-tu.json summary matching File1.c as input + // and produce a .linked-summaries.json output. + results.checkTask(.matchRuleType("LinkEntity")) { task in + let jsonInputs = task.inputs.filter { $0.path.fileExtension == "json" } + if let jsonInput = jsonInputs.first { + #expect(jsonInput.path.basenameWithoutSuffix == "File1.ssaf-tu") + } else { + Issue.record("Expected File1.ssaf-tu.json as input to the LinkEntity task") + } + #expect(task.outputs.map({ $0.path }).contains(where: { $0.str.hasSuffix(".linked-summaries.json") })) + } + results.checkTask(.matchRuleType("AnalyzeSSAF")) { task in + #expect(task.inputs.map({ $0.path }).contains(where: { $0.str.hasSuffix(".linked-summaries.json") })) + task.checkCommandLineContains(["-a", "UnsafeBufferUsageAnalysisResult"]) + #expect(task.outputs.map({ $0.path }).contains(where: { $0.str.hasSuffix(".ssaf-analysis.json") })) + } + results.checkNoDiagnostics() + } + } + + // When INVOKE_SSAF is NO, neither ssaf flag is present and no entity linker or analyzer task is created. + do { + let tester = try TaskConstructionTester(core, getTestProject(invokeSSAF: "NO")) + await tester.checkBuild(runDestination: .host) { results in + results.checkTask(.matchRuleType("CompileC")) { task in + task.checkCommandLineNoMatch([.prefix("--ssaf-extract-summaries=")]) + task.checkCommandLineNoMatch([.prefix("--ssaf-tu-summary-file=")]) + } + results.checkNoTask(.matchRuleType("LinkEntity")) + results.checkNoTask(.matchRuleType("AnalyzeSSAF")) + results.checkNoDiagnostics() + } + } + } }