Skip to content

Commit 948c888

Browse files
committed
Retain types conforming to App Intents protocols
1 parent a08a869 commit 948c888

File tree

9 files changed

+132
-0
lines changed

9 files changed

+132
-0
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
##### Bug Fixes
1212

1313
- Fix indexing of xib/storyboard files in SPM projects.
14+
- Fix types conforming to App Intents protocols being reported as unused.
1415

1516
## 3.3.0 (2025-12-13)
1617

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
import Configuration
2+
import Foundation
3+
import Shared
4+
5+
/// Retains types conforming to App Intents protocols.
6+
///
7+
/// Types conforming to these protocols are discovered and invoked by the system at runtime,
8+
/// so they should not be reported as unused.
9+
final class AppIntentsRetainer: SourceGraphMutator {
10+
private let graph: SourceGraph
11+
12+
/// USR prefix for Swift symbols from the AppIntents module.
13+
/// Swift USRs encode the module name with a length prefix: "s:<length><module_name>..."
14+
/// For AppIntents (10 characters), this becomes "s:10AppIntents".
15+
private static let appIntentsModuleUsrPrefix = "s:10AppIntents"
16+
17+
required init(graph: SourceGraph, configuration _: Configuration, swiftVersion _: SwiftVersion) {
18+
self.graph = graph
19+
}
20+
21+
func mutate() {
22+
graph
23+
.declarations(ofKinds: [.class, .struct, .enum])
24+
.lazy
25+
.filter {
26+
$0.related.contains {
27+
self.graph.isExternal($0) &&
28+
$0.kind == .protocol &&
29+
$0.usr.hasPrefix(Self.appIntentsModuleUsrPrefix)
30+
}
31+
}
32+
.forEach { graph.markRetained($0) }
33+
}
34+
}

Sources/SourceGraph/SourceGraphMutatorRunner.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ public final class SourceGraphMutatorRunner {
3838
XCTestRetainer.self,
3939
SwiftTestingRetainer.self,
4040
SwiftUIRetainer.self,
41+
AppIntentsRetainer.self,
4142
StringInterpolationAppendInterpolationRetainer.self,
4243
PropertyWrapperRetainer.self,
4344
ResultBuilderRetainer.self,

Tests/Fixtures/Package.swift

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,9 @@ targets.append(contentsOf: [
4545
),
4646
.target(
4747
name: "ObjcAnnotatedRetentionFixtures"
48+
),
49+
.target(
50+
name: "AppIntentsRetentionFixtures"
4851
)
4952
])
5053
#endif
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
import AppIntents
2+
3+
struct SimpleEntity: AppEntity {
4+
static var typeDisplayRepresentation: TypeDisplayRepresentation = "Simple Entity"
5+
static var defaultQuery = SimpleEntityQuery()
6+
7+
var id: String
8+
var displayRepresentation: DisplayRepresentation {
9+
DisplayRepresentation(title: "\(id)")
10+
}
11+
}
12+
13+
struct SimpleEntityQuery: EntityQuery {
14+
func entities(for identifiers: [String]) async throws -> [SimpleEntity] {
15+
[]
16+
}
17+
18+
func suggestedEntities() async throws -> [SimpleEntity] {
19+
[]
20+
}
21+
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
import AppIntents
2+
3+
enum SimpleAppEnum: String, AppEnum {
4+
case optionA
5+
case optionB
6+
7+
static var typeDisplayRepresentation: TypeDisplayRepresentation = "Simple Enum"
8+
static var caseDisplayRepresentations: [SimpleAppEnum: DisplayRepresentation] = [
9+
.optionA: "Option A",
10+
.optionB: "Option B"
11+
]
12+
}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
import AppIntents
2+
3+
struct SimpleIntent: AppIntent {
4+
static var title: LocalizedStringResource = "Simple Intent"
5+
6+
func perform() async throws -> some IntentResult {
7+
.result()
8+
}
9+
}
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
import AppIntents
2+
3+
struct ShortcutIntent: AppIntent {
4+
static var title: LocalizedStringResource = "Shortcut Intent"
5+
6+
func perform() async throws -> some IntentResult {
7+
.result()
8+
}
9+
}
10+
11+
struct SimpleShortcutsProvider: AppShortcutsProvider {
12+
static var appShortcuts: [AppShortcut] {
13+
AppShortcut(
14+
intent: ShortcutIntent(),
15+
phrases: ["Run shortcut"],
16+
shortTitle: "Shortcut",
17+
systemImageName: "star"
18+
)
19+
}
20+
}
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
#if os(macOS)
2+
@testable import TestShared
3+
import XCTest
4+
5+
final class AppIntentsRetentionTest: FixtureSourceGraphTestCase {
6+
func testRetainsAppIntent() {
7+
analyze {
8+
assertReferenced(.struct("SimpleIntent"))
9+
}
10+
}
11+
12+
func testRetainsAppEntity() {
13+
analyze {
14+
assertReferenced(.struct("SimpleEntity"))
15+
assertReferenced(.struct("SimpleEntityQuery"))
16+
}
17+
}
18+
19+
func testRetainsAppEnum() {
20+
analyze {
21+
assertReferenced(.enum("SimpleAppEnum"))
22+
}
23+
}
24+
25+
func testRetainsAppShortcutsProvider() {
26+
analyze {
27+
assertReferenced(.struct("SimpleShortcutsProvider"))
28+
}
29+
}
30+
}
31+
#endif

0 commit comments

Comments
 (0)