New Issue Checklist
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
New Issue Checklist
prefer_key_pathrule should not trigger on macros #5744 / Silenceprefer_key_pathrule on macro expansion expressions #5765 (see below), which this does not fully coverBug Description
prefer_key_path's autofix rewrites a property-access closure into a key path even when the call is nested inside a#require/#expectmacro argument. The rewritten key-path form fails to compile inside those macros, soswiftlint --fixproduces a non-building project.#5765("Silenceprefer_key_pathrule 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 — inPreferKeyPathRule.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 aFunctionCallExprSyntaxparent, not theMacroExpansionExprSyntax, so it is still flagged and still auto-corrected.Reproduction
swiftlint --fixrewrites it toitems.first(where: \.flag), after whichswift buildfails:(The key-path form is otherwise valid Swift — it compiles fine outside the macro.
#require/#expectdecompose 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_pathshould not rewrite closures inside a macro expansion — same rationale as #5744.)Expected behavior
prefer_key_pathshould not flag (or--fixrewrite) 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 aMacroExpansionExprSyntax/MacroExpansionDeclSyntaxrather than inspecting onlyparent.kind.Environment