Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down
1 change: 1 addition & 0 deletions Sources/BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
34 changes: 34 additions & 0 deletions Sources/SourceGraph/Mutators/AppIntentsRetainer.swift
Original file line number Diff line number Diff line change
@@ -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:<length><module_name>..."
/// 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) }
}
}
1 change: 1 addition & 0 deletions Sources/SourceGraph/SourceGraphMutatorRunner.swift
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ public final class SourceGraphMutatorRunner {
XCTestRetainer.self,
SwiftTestingRetainer.self,
SwiftUIRetainer.self,
AppIntentsRetainer.self,
StringInterpolationAppendInterpolationRetainer.self,
PropertyWrapperRetainer.self,
ResultBuilderRetainer.self,
Expand Down
3 changes: 3 additions & 0 deletions Tests/Fixtures/Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,9 @@ targets.append(contentsOf: [
),
.target(
name: "ObjcAnnotatedRetentionFixtures"
),
.target(
name: "AppIntentsRetentionFixtures"
)
])
#endif
Expand Down
Original file line number Diff line number Diff line change
@@ -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] {
[]
}
}
Original file line number Diff line number Diff line change
@@ -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"
]
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import AppIntents

struct SimpleIntent: AppIntent {
static var title: LocalizedStringResource = "Simple Intent"

func perform() async throws -> some IntentResult {
.result()
}
}
Original file line number Diff line number Diff line change
@@ -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"
)
}
}
31 changes: 31 additions & 0 deletions Tests/PeripheryTests/AppIntentsRetentionTest.swift
Original file line number Diff line number Diff line change
@@ -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
Loading