Skip to content

Fix prefer_key_path to catch closures nested in macro expansions - #6906

Open
jadhavgaurav wants to merge 1 commit into
realm:mainfrom
jadhavgaurav:fix-prefer-key-path-nested-macro
Open

Fix prefer_key_path to catch closures nested in macro expansions#6906
jadhavgaurav wants to merge 1 commit into
realm:mainfrom
jadhavgaurav:fix-prefer-key-path-nested-macro

Conversation

@jadhavgaurav

Copy link
Copy Markdown

What

prefer_key_path's isInvalid(restrictToStandardFunctions:) only checked a
closure's immediate parent for MacroExpansionExprSyntax. That correctly
silences a closure that is a direct macro argument, like #Predicate { $0.a },
but a closure nested one or more levels deeper inside a macro expansion's
arguments was still flagged and rewritten:

let found = try #require(items.first(where: { $0.flag }))

swiftlint --fix rewrote this to items.first(where: \.flag), which fails to
compile inside #require/#expect: those macros decompose the call via
__checkFunctionCall, and the key path form loses the non-throwing guarantee
the original closure had.

#5765 already silenced the direct-argument case (#Predicate { $0.a }), but
the check only inspected parent.kind, one level up from the closure.

Fix

Replace the single-level check with a full ancestor walk that looks for a
MacroExpansionExprSyntax or MacroExpansionDeclSyntax at any depth, in the
same style already used elsewhere in the rule set for ancestor walks (see
AccessibilityLabelForImageRule.isInsideLabelIconClosure()). This subsumes the
existing direct-argument case, so #Predicate { $0.a } keeps working exactly
as before, and additionally catches the deeper nesting from the issue.

Testing

Added closureNestedInMacroExpansionIsNotRewritten() to
PreferKeyPathRuleTests, covering:

  • the exact #require(f.first(where: { $0.a })) case from the issue,
  • #expect(...),
  • a closure nested two calls deep inside a macro argument,
  • a no-op correction assertion for the same case.

I could not run swift test on this machine (only Xcode Command Line Tools
are installed here, no full Xcode, so the swift-testing runtime the test
targets link against is unavailable, and SourceKittenFramework cannot dlopen
sourcekitdInProc). To still prove the regression test actually exercises the
fix, I added a small local executable target that links SwiftLintCore and
SwiftLintFramework directly (bypassing the sourcekitd-dependent CLI path,
since prefer_key_path is a SourceKitFreeRule) and ran the exact cases from
the new test through Linter/RuleStorage by hand:

  • with the fix applied: all three nested cases report 0 violations, the
    existing non-macro case (f.first(where: { $0.a })) still reports 1, and
    the direct-argument case (#Predicate { $0.a }) still reports 0.
  • with only Source/.../PreferKeyPathRule.swift reverted (via a saved patch,
    not git stash) and the test file left in place: the three nested cases
    each report 1 violation, i.e. the regression reproduces and the new test
    would fail. Reapplying the patch restores the passing state.

I also ran swift build (full product) and swift build --target BuiltInRulesTests successfully, and self-linted the two changed source files
with the locally built swiftlint binary (0 violations, a few SourceKit-only
rules skipped because of the same missing-sourcekitd limitation).

Fixes #6657

isInvalid(restrictToStandardFunctions:) only checked a closure's
immediate parent for MacroExpansionExprSyntax, so a closure nested one
level deeper, such as the where: argument of items.first(where:) inside
#require(...), was still rewritten to a key path. The key path form can
fail to compile inside #require/#expect, which decompose the call and
lose its non-throwing guarantee.

Replace the immediate-parent check with a full ancestor walk that looks
for a macro expansion at any depth, matching the style already used for
ancestor walks elsewhere in the rule set (see
AccessibilityLabelForImageRule.isInsideLabelIconClosure()). This also
subsumes the existing direct-argument case, so #Predicate { $0.a } keeps
working the same way.

Fixes realm#6657
@SwiftLintBot

Copy link
Copy Markdown
30 Messages
📖 Building this branch resulted in a binary size of 28467.95 KiB vs 28467.96 KiB when built on main (-1% smaller).
📖 Linting Aerial with this PR took 0.68 s vs 0.68 s on main (0% slower).
📖 Linting Alamofire with this PR took 1.0 s vs 0.98 s on main (2% slower).
📖 Linting Brave with this PR took 5.62 s vs 5.57 s on main (0% slower).
📖 Linting Brigade with this PR took 18.14 s vs 18.13 s on main (0% slower).
📖 Linting DuckDuckGo with this PR took 31.24 s vs 31.24 s on main (0% slower).
📖 Linting Firefox with this PR took 11.59 s vs 11.42 s on main (1% slower).
📖 Linting Kickstarter with this PR took 7.39 s vs 7.45 s on main (0% faster).
📖 Linting Moya with this PR took 0.38 s vs 0.38 s on main (0% slower).
📖 Linting NetNewsWire with this PR took 2.54 s vs 2.54 s on main (0% slower).
📖 Linting Nimble with this PR took 0.55 s vs 0.56 s on main (1% faster).
📖 Linting PocketCasts with this PR took 7.47 s vs 7.52 s on main (0% faster).
📖 Linting Quick with this PR took 0.36 s vs 0.37 s on main (2% faster).
📖 Linting Realm with this PR took 2.98 s vs 3.0 s on main (0% faster).
📖 Linting Sourcery with this PR took 1.66 s vs 1.67 s on main (0% faster).
📖 Linting Swift with this PR took 4.56 s vs 4.57 s on main (0% faster).
📖 Linting SwiftLintPerformanceTests with this PR took 0.19 s vs 0.19 s on main (0% slower).
📖 Linting VLC with this PR took 1.32 s vs 1.32 s on main (0% slower).
📖 Linting Wire with this PR took 16.38 s vs 16.43 s on main (0% faster).
📖 Linting WordPress with this PR took 10.66 s vs 10.7 s on main (0% faster).
📖 This PR fixed a violation in DuckDuckGo: /SharedPackages/Persistence/Tests/PersistenceTests/KeyedStoringTests.swift:765:46: Warning: Use a key path argument instead of a closure with property access (prefer_key_path)
📖 This PR fixed a violation in DuckDuckGo: /SharedPackages/Persistence/Tests/PersistenceTests/KeyedStoringTests.swift:772:46: Warning: Use a key path argument instead of a closure with property access (prefer_key_path)
📖 This PR fixed a violation in DuckDuckGo: /SharedPackages/Persistence/Tests/PersistenceTests/KeyedStoringTests.swift:778:46: Warning: Use a key path argument instead of a closure with property access (prefer_key_path)
📖 This PR fixed a violation in DuckDuckGo: /SharedPackages/Persistence/Tests/PersistenceTests/KeyedStoringTests.swift:851:46: Warning: Use a key path argument instead of a closure with property access (prefer_key_path)
📖 This PR fixed a violation in DuckDuckGo: /SharedPackages/Persistence/Tests/PersistenceTests/KeyedStoringTests.swift:859:46: Warning: Use a key path argument instead of a closure with property access (prefer_key_path)
📖 This PR fixed a violation in WordPress: /Tests/KeystoneTests/Tests/ViewRelated/Likes/LikesListViewModelTests.swift:27:37: Warning: Use a key path argument instead of a closure with property access (prefer_key_path)
📖 This PR fixed a violation in WordPress: /Tests/KeystoneTests/Tests/ViewRelated/Likes/LikesListViewModelTests.swift:143:37: Warning: Use a key path argument instead of a closure with property access (prefer_key_path)
📖 This PR fixed a violation in WordPress: /Tests/KeystoneTests/Tests/ViewRelated/Likes/LikesListViewModelTests.swift:148:37: Warning: Use a key path argument instead of a closure with property access (prefer_key_path)
📖 This PR fixed a violation in WordPress: /Tests/KeystoneTests/Tests/ViewRelated/Likes/LikesListViewModelTests.swift:171:37: Warning: Use a key path argument instead of a closure with property access (prefer_key_path)
📖 This PR fixed a violation in WordPress: /Modules/Tests/WordPressIntelligenceTests/PostExcerptGeneratorTests.swift:76:61: Warning: Use a key path argument instead of a closure with property access (prefer_key_path)

Generated by 🚫 Danger

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

prefer_key_path autofix produces uncompilable code for closures nested inside a #require/#expect call

2 participants