prefer_key_path autofix produces uncompilable code for closures nested inside a #require/#expect call
Nobody has claimed this yet.
- Dominant language
- Swift
- Stars
- 19.7k
- Forks
- 2.3k
- Avg merge
- 1d 1h
- Merged PRs (30d)
- 11
Description
New Issue Checklist
- Updated SwiftLint to the latest version (0.63.2)
- Searched for existing issues — closest is #5744 / #5765 (see below), which this does not fully cover
Bug Description
prefer_key_path's autofix rewrites a property-access closure into a key path even when the call is nested inside a #require/#expect macro argument. The rewritten key-path form fails to compile inside those macros, so swiftlint --fix produces a non-building project.
#5765 ("Silence prefer_key_path rule on macro expansion expressions", fixing #5744) already handles closures that are a direct macro argument, e.g. #Predicate { $0.isCompleted }. But the silencing check only inspects the closure's immediate parent — in PreferKeyPathRule.swift, isInvalid(...) guards on ![.macroExpansionExpr, …].contains(parent.kind). A closure nested deeper — inside a function-call argument that is itself the macro's argument — has a FunctionCallExprSyntax parent, not the MacroExpansionExprSyntax, so it is still flagged and still auto-corrected.
Reproduction
import Testing
struct Item { let flag: Bool }
@Test func repro() throws {
let items = [Item(flag: true)]
let found = try #require(items.first(where: { $0.flag })) // flagged — but must not be rewritten
#expect(found.flag)
}
$ swiftlint lint --only-rule prefer_key_path
…:7:49: warning: Prefer Key Path Violation: Use a key path argument instead of a closure with property access (prefer_key_path)
swiftlint --fix rewrites it to items.first(where: \.flag), after which swift build fails:
macro expansion #require:2:3: error: call can throw, but it is not marked with 'try' and the error is not handled
note: call is to 'rethrows' function, but argument function can throw
(The key-path form is otherwise valid Swift — it compiles fine outside the macro. #require/#expect decompose the call via __checkFunctionCall, which passes the key path as an opaque closure parameter and loses its non-throwing guarantee. Reported separately at swiftlang/swift-testing#1723. Regardless of the swift-testing fix, prefer_key_path should not rewrite closures inside a macro expansion — same rationale as #5744.)
Expected behavior
prefer_key_path should not flag (or --fix rewrite) a closure that lies anywhere inside a macro-expansion argument — not only when the closure is the direct macro argument. The check could walk ancestors for a MacroExpansionExprSyntax / MacroExpansionDeclSyntax rather than inspecting only parent.kind.
Environment
- SwiftLint version: 0.63.2
- Xcode version: 26.4
- Swift: 6.2.4
- Installation method: installer
- Configuration:
only_rules:
- prefer_key_path
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Start in PreferKeyPathRule.swift, especially isInvalid(...), and reproduce the report with the supplied items.first(where:) example using swiftlint lint --only-rule prefer_key_path. The fix is done when closures nested anywhere inside a #require or #expect macro argument are neither flagged nor rewritten by swiftlint --fix.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- swift
- Domain
- tooling
- Issue type
- Bug
- Difficulty
- 3/5
- Estimated time
- 1-2 days
- Activity status
- Quiet
- Clarity
- Clearly specified
- Newbie friendliness
- 65/100