Skip to content

Fix false positives for unused_enumerated with result member access#6544

Open
theamodhshetty wants to merge 2 commits intorealm:mainfrom
theamodhshetty:codex/fix-unused-enumerated-result-member
Open

Fix false positives for unused_enumerated with result member access#6544
theamodhshetty wants to merge 2 commits intorealm:mainfrom
theamodhshetty:codex/fix-unused-enumerated-result-member

Conversation

@theamodhshetty
Copy link
Copy Markdown
Contributor

Summary

  • avoid flagging .enumerated() when a higher-order call uses tuple members like ?.offset after the closure
  • keep tracking $0.element / $0.0 and seed the closure usage from result-member access on the surrounding call
  • add regression examples and the required changelog entry

Testing

  • swift test --filter UnusedEnumeratedRuleGeneratedTests
  • ./.build/debug/swiftlint lint Source/SwiftLintBuiltInRules/Rules/Idiomatic/UnusedEnumeratedRule.swift CHANGELOG.md --strict
  • ./.build/debug/swiftlint lint --only-rule unused_enumerated /tmp/unused_enumerated_ok.swift --quiet
  • ./.build/debug/swiftlint lint --only-rule unused_enumerated /tmp/unused_enumerated_bad.swift --quiet

Closes #5881

@SwiftLintBot
Copy link
Copy Markdown

SwiftLintBot commented Mar 17, 2026

1 Warning
⚠️ This PR may need tests.
19 Messages
📖 Building this branch resulted in a binary size of 27358.1 KiB vs 27357.74 KiB when built on main (0% larger).
📖 Linting Aerial with this PR took 0.22 s vs 0.17 s on main (29% slower).
📖 Linting Alamofire with this PR took 0.21 s vs 0.22 s on main (4% faster).
📖 Linting Brave with this PR took 0.8 s vs 0.81 s on main (1% faster).
📖 Linting DuckDuckGo with this PR took 3.35 s vs 3.36 s on main (0% faster).
📖 Linting Firefox with this PR took 1.51 s vs 1.51 s on main (0% slower).
📖 Linting Kickstarter with this PR took 0.95 s vs 0.94 s on main (1% slower).
📖 Linting Moya with this PR took 0.12 s vs 0.13 s on main (7% faster).
📖 Linting NetNewsWire with this PR took 0.35 s vs 0.34 s on main (2% slower).
📖 Linting Nimble with this PR took 0.2 s vs 0.18 s on main (11% slower).
📖 Linting PocketCasts with this PR took 0.91 s vs 0.92 s on main (1% faster).
📖 Linting Quick with this PR took 0.16 s vs 0.16 s on main (0% slower).
📖 Linting Realm with this PR took 0.42 s vs 0.42 s on main (0% slower).
📖 Linting Sourcery with this PR took 0.31 s vs 0.28 s on main (10% slower).
📖 Linting Swift with this PR took 0.51 s vs 0.5 s on main (2% slower).
📖 Linting SwiftLintPerformanceTests with this PR took 3.85 s vs 3.83 s on main (0% slower).
📖 Linting VLC with this PR took 0.25 s vs 0.24 s on main (4% slower).
📖 Linting Wire with this PR took 1.98 s vs 1.99 s on main (0% faster).
📖 Linting WordPress with this PR took 1.35 s vs 1.34 s on main (0% slower).

Generated by 🚫 Danger

@theamodhshetty
Copy link
Copy Markdown
Contributor Author

Pushed a small follow-up in eb3575a after the first CI pass.

The original version reset nextClosureId / lastEnumeratedPosition inside the same if let that introduced shorthand bindings with those names, which newer matrix legs were treating as the shadowed locals instead of the stored properties. This update just renames the local bindings so the property resets stay unambiguous across toolchains.

@rgoldberg
Copy link
Copy Markdown

Does anything else need to be done to merge this PR?

@theamodhshetty
Copy link
Copy Markdown
Contributor Author

I don’t think so from my side. The follow-up for the CI/toolchain issue is already in, and the checks are green now. If there’s anything else you want adjusted before merge, I’m happy to make it.

Copy link
Copy Markdown
Collaborator

@SimplyDanny SimplyDanny left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the contribution!

The idea looks good. I have a few remarks though ...

guard node.isEnumerated,
let parent = node.parent,
parent.as(MemberAccessExprSyntax.self)?.declName.baseName.text != "filter",
let trailingClosure = parent.parent?.as(FunctionCallExprSyntax.self)?.trailingClosure
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
let parentCall = parent?.parent?.as(FunctionCallExprSyntax.self)
let trailingClosure = parentCall.trailingClosure

Comment on lines +181 to +183
lastEnumeratedResultMemberUsage =
parent.parent?.as(FunctionCallExprSyntax.self)?
.usedEnumeratedResultMembers ?? (false, false)
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
lastEnumeratedResultMemberUsage =
parent.parent?.as(FunctionCallExprSyntax.self)?
.usedEnumeratedResultMembers ?? (false, false)
lastEnumeratedResultMemberUsage = parentCall.usedEnumeratedResultMembers ?? (false, false)

var current = parent

while let currentNode = current {
if let memberAccess = currentNode.as(MemberAccessExprSyntax.self) {
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If there isn't a member access whatsoever, this will always run up to the file's root. Can we find another condition to stop iteration prematurely?

case "element", "1":
return (false, true)
default:
return (false, false)
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
return (false, false)
break

) {
self.enumeratedPosition = enumeratedPosition
self.zeroPosition = usesZeroResultMember ? enumeratedPosition : nil
self.onePosition = usesOneResultMember ? enumeratedPosition : nil
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This logic looks a bit surprising on first sight. Why would zeroPosition become enumeratedPosition?

I think that actually, enumeratedPosition should always be non-optional, so that we only need to track whether "zero" or "one" was found. In turn, Closure must be Closure? in the stack.

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.

False positive for unused_enumerated

4 participants