diff --git a/CHANGELOG.md b/CHANGELOG.md index 35c6f0f4d..ed0b84ab8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,7 @@ ##### Bug Fixes - Fix indexing of xib/storyboard files in SPM projects. +- Fix types conforming to App Intents protocols being reported as unused. ## 3.3.0 (2025-12-13) diff --git a/Sources/BUILD.bazel b/Sources/BUILD.bazel index 79cfd10df..642d402cc 100644 --- a/Sources/BUILD.bazel +++ b/Sources/BUILD.bazel @@ -55,6 +55,7 @@ swift_library( "SourceGraph/Elements/SourceFile.swift", "SourceGraph/Mutators/AccessibilityCascader.swift", "SourceGraph/Mutators/AncestralReferenceEliminator.swift", + "SourceGraph/Mutators/AppIntentsRetainer.swift", "SourceGraph/Mutators/AssetReferenceRetainer.swift", "SourceGraph/Mutators/AssignOnlyPropertyReferenceEliminator.swift", "SourceGraph/Mutators/CodablePropertyRetainer.swift", diff --git a/Sources/SourceGraph/Mutators/AppIntentsRetainer.swift b/Sources/SourceGraph/Mutators/AppIntentsRetainer.swift new file mode 100644 index 000000000..07029a964 --- /dev/null +++ b/Sources/SourceGraph/Mutators/AppIntentsRetainer.swift @@ -0,0 +1,34 @@ +import Configuration +import Foundation +import Shared + +/// Retains types conforming to App Intents protocols. +/// +/// Types conforming to these protocols are discovered and invoked by the system at runtime, +/// so they should not be reported as unused. +final class AppIntentsRetainer: SourceGraphMutator { + private let graph: SourceGraph + + /// USR prefix for Swift symbols from the AppIntents module. + /// Swift USRs encode the module name with a length prefix: "s:..." + /// For AppIntents (10 characters), this becomes "s:10AppIntents". + private static let appIntentsModuleUsrPrefix = "s:10AppIntents" + + required init(graph: SourceGraph, configuration _: Configuration, swiftVersion _: SwiftVersion) { + self.graph = graph + } + + func mutate() { + graph + .declarations(ofKinds: [.class, .struct, .enum]) + .lazy + .filter { + $0.related.contains { + self.graph.isExternal($0) && + $0.kind == .protocol && + $0.usr.hasPrefix(Self.appIntentsModuleUsrPrefix) + } + } + .forEach { graph.markRetained($0) } + } +} diff --git a/Sources/SourceGraph/SourceGraphMutatorRunner.swift b/Sources/SourceGraph/SourceGraphMutatorRunner.swift index 93aaf963c..eac09e13c 100644 --- a/Sources/SourceGraph/SourceGraphMutatorRunner.swift +++ b/Sources/SourceGraph/SourceGraphMutatorRunner.swift @@ -38,6 +38,7 @@ public final class SourceGraphMutatorRunner { XCTestRetainer.self, SwiftTestingRetainer.self, SwiftUIRetainer.self, + AppIntentsRetainer.self, StringInterpolationAppendInterpolationRetainer.self, PropertyWrapperRetainer.self, ResultBuilderRetainer.self, diff --git a/Tests/Fixtures/Package.swift b/Tests/Fixtures/Package.swift index 3ca2ae225..ffeb439bf 100644 --- a/Tests/Fixtures/Package.swift +++ b/Tests/Fixtures/Package.swift @@ -45,6 +45,9 @@ targets.append(contentsOf: [ ), .target( name: "ObjcAnnotatedRetentionFixtures" + ), + .target( + name: "AppIntentsRetentionFixtures" ) ]) #endif diff --git a/Tests/Fixtures/Sources/AppIntentsRetentionFixtures/testRetainsAppEntity.swift b/Tests/Fixtures/Sources/AppIntentsRetentionFixtures/testRetainsAppEntity.swift new file mode 100644 index 000000000..af3a650ce --- /dev/null +++ b/Tests/Fixtures/Sources/AppIntentsRetentionFixtures/testRetainsAppEntity.swift @@ -0,0 +1,21 @@ +import AppIntents + +struct SimpleEntity: AppEntity { + static var typeDisplayRepresentation: TypeDisplayRepresentation = "Simple Entity" + static var defaultQuery = SimpleEntityQuery() + + var id: String + var displayRepresentation: DisplayRepresentation { + DisplayRepresentation(title: "\(id)") + } +} + +struct SimpleEntityQuery: EntityQuery { + func entities(for identifiers: [String]) async throws -> [SimpleEntity] { + [] + } + + func suggestedEntities() async throws -> [SimpleEntity] { + [] + } +} diff --git a/Tests/Fixtures/Sources/AppIntentsRetentionFixtures/testRetainsAppEnum.swift b/Tests/Fixtures/Sources/AppIntentsRetentionFixtures/testRetainsAppEnum.swift new file mode 100644 index 000000000..ae87e5ba9 --- /dev/null +++ b/Tests/Fixtures/Sources/AppIntentsRetentionFixtures/testRetainsAppEnum.swift @@ -0,0 +1,12 @@ +import AppIntents + +enum SimpleAppEnum: String, AppEnum { + case optionA + case optionB + + static var typeDisplayRepresentation: TypeDisplayRepresentation = "Simple Enum" + static var caseDisplayRepresentations: [SimpleAppEnum: DisplayRepresentation] = [ + .optionA: "Option A", + .optionB: "Option B" + ] +} diff --git a/Tests/Fixtures/Sources/AppIntentsRetentionFixtures/testRetainsAppIntent.swift b/Tests/Fixtures/Sources/AppIntentsRetentionFixtures/testRetainsAppIntent.swift new file mode 100644 index 000000000..a0bd8308f --- /dev/null +++ b/Tests/Fixtures/Sources/AppIntentsRetentionFixtures/testRetainsAppIntent.swift @@ -0,0 +1,9 @@ +import AppIntents + +struct SimpleIntent: AppIntent { + static var title: LocalizedStringResource = "Simple Intent" + + func perform() async throws -> some IntentResult { + .result() + } +} diff --git a/Tests/Fixtures/Sources/AppIntentsRetentionFixtures/testRetainsAppShortcutsProvider.swift b/Tests/Fixtures/Sources/AppIntentsRetentionFixtures/testRetainsAppShortcutsProvider.swift new file mode 100644 index 000000000..d2d787a24 --- /dev/null +++ b/Tests/Fixtures/Sources/AppIntentsRetentionFixtures/testRetainsAppShortcutsProvider.swift @@ -0,0 +1,20 @@ +import AppIntents + +struct ShortcutIntent: AppIntent { + static var title: LocalizedStringResource = "Shortcut Intent" + + func perform() async throws -> some IntentResult { + .result() + } +} + +struct SimpleShortcutsProvider: AppShortcutsProvider { + static var appShortcuts: [AppShortcut] { + AppShortcut( + intent: ShortcutIntent(), + phrases: ["Run shortcut"], + shortTitle: "Shortcut", + systemImageName: "star" + ) + } +} diff --git a/Tests/PeripheryTests/AppIntentsRetentionTest.swift b/Tests/PeripheryTests/AppIntentsRetentionTest.swift new file mode 100644 index 000000000..9ccc7d3d1 --- /dev/null +++ b/Tests/PeripheryTests/AppIntentsRetentionTest.swift @@ -0,0 +1,31 @@ +#if os(macOS) + @testable import TestShared + import XCTest + + final class AppIntentsRetentionTest: FixtureSourceGraphTestCase { + func testRetainsAppIntent() { + analyze { + assertReferenced(.struct("SimpleIntent")) + } + } + + func testRetainsAppEntity() { + analyze { + assertReferenced(.struct("SimpleEntity")) + assertReferenced(.struct("SimpleEntityQuery")) + } + } + + func testRetainsAppEnum() { + analyze { + assertReferenced(.enum("SimpleAppEnum")) + } + } + + func testRetainsAppShortcutsProvider() { + analyze { + assertReferenced(.struct("SimpleShortcutsProvider")) + } + } + } +#endif