Skip to content
Closed
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
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,11 @@

### Bug Fixes

* Fix `unused_enumerated` false positives when `offset` and `element` are used in
separate trailing closures chained after `.enumerated()`.
[leno23](https://github.com/leno23)
[#5600](https://github.com/realm/SwiftLint/issues/5600)

* Avoid false positives in `prefer_self_in_static_references` for generic
constraints and generic parameter bounds such as `where A: P` and `<A: P>`
in classes and extensions.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,11 @@ struct UnusedEnumeratedRule: Rule {
print(i)
}
""", excludeFromDocumentation: true),
Example("""
rules.enumerated()
.first { $0.element.isValid() == false }
.flatMap { messages[$0.offset] }
"""),
],
triggeringExamples: [
Example("for (↓_, foo) in bar.enumerated() { }"),
Expand Down Expand Up @@ -110,6 +115,7 @@ private extension UnusedEnumeratedRule {
private struct Closure {
let enumeratedPosition: AbsolutePosition
let usedEnumeratedResultMembers: (zero: Bool, one: Bool)
let enclosingCall: FunctionCallExprSyntax
var zeroPosition: AbsolutePosition?
var onePosition: AbsolutePosition?
}
Expand All @@ -118,6 +124,7 @@ private extension UnusedEnumeratedRule {
let id: SyntaxIdentifier
let enumeratedPosition: AbsolutePosition
let usedEnumeratedResultMembers: (zero: Bool, one: Bool)
let enclosingCall: FunctionCallExprSyntax
}

final class Visitor: ViolationsSyntaxVisitor<ConfigurationType> {
Expand Down Expand Up @@ -180,7 +187,8 @@ private extension UnusedEnumeratedRule {
pendingClosure = PendingClosure(
id: trailingClosure.id,
enumeratedPosition: enumeratedPosition,
usedEnumeratedResultMembers: ExprSyntax(parentCall).usedEnumeratedResultMembers
usedEnumeratedResultMembers: ExprSyntax(parentCall).usedEnumeratedResultMembers,
enclosingCall: parentCall
)
}

Expand All @@ -191,7 +199,8 @@ private extension UnusedEnumeratedRule {
if let pendingClosure, pendingClosure.id == node.id {
closures.push(Closure(
enumeratedPosition: pendingClosure.enumeratedPosition,
usedEnumeratedResultMembers: pendingClosure.usedEnumeratedResultMembers
usedEnumeratedResultMembers: pendingClosure.usedEnumeratedResultMembers,
enclosingCall: pendingClosure.enclosingCall
))
self.pendingClosure = nil
} else {
Expand All @@ -200,13 +209,18 @@ private extension UnusedEnumeratedRule {
return .visitChildren
}

override func visitPost(_: ClosureExprSyntax) {
override func visitPost(_ node: ClosureExprSyntax) {
guard let closure = closures.pop().flatMap(\.self) else { return }

let chainedUsage = closure.enclosingCall.enumeratedMembersUsedInChainedClosures(
excluding: node.id
)
let zeroPosition = closure.zeroPosition
?? (closure.usedEnumeratedResultMembers.zero ? closure.enumeratedPosition : nil)
?? (closure.usedEnumeratedResultMembers.zero || chainedUsage.zero
? closure.enumeratedPosition : nil)
let onePosition = closure.onePosition
?? (closure.usedEnumeratedResultMembers.one ? closure.enumeratedPosition : nil)
?? (closure.usedEnumeratedResultMembers.one || chainedUsage.one
? closure.enumeratedPosition : nil)
guard (zeroPosition != nil) != (onePosition != nil) else { return }

addViolation(
Expand Down Expand Up @@ -341,6 +355,74 @@ private extension ClosureShorthandParameterSyntax {
}
}

private extension FunctionCallExprSyntax {
func enumeratedMembersUsedInChainedClosures(
excluding closureId: SyntaxIdentifier
) -> (zero: Bool, one: Bool) {
var usesOffset = false
var usesElement = false
var current: FunctionCallExprSyntax? = self

while let call = current,
let outerCall = call.outerFunctionCallWithTrailingClosure,
let trailingClosure = outerCall.trailingClosure,
trailingClosure.id != closureId {
let usage = trailingClosure.enumeratedTupleMembersUsed()
usesOffset = usesOffset || usage.zero
usesElement = usesElement || usage.one
current = outerCall
}

return (usesOffset, usesElement)
}

var outerFunctionCallWithTrailingClosure: FunctionCallExprSyntax? {
guard let memberAccess = parent?.as(MemberAccessExprSyntax.self),
let outerCall = memberAccess.parent?.as(FunctionCallExprSyntax.self),
outerCall.trailingClosure != nil else {
return nil
}
return outerCall
}
}

private extension ClosureExprSyntax {
func enumeratedTupleMembersUsed() -> (zero: Bool, one: Bool) {
EnumeratedTupleUsageScanner()
.walk(tree: Syntax(self), handler: \.usage)
}
}

private final class EnumeratedTupleUsageScanner: SyntaxVisitor {
private(set) var usage = (zero: false, one: false)

init() {
super.init(viewMode: .sourceAccurate)
}

override func visitPost(_ node: DeclReferenceExprSyntax) {
guard node.baseName.text == "$0" || node.baseName.text == "$1" else {
return
}

if node.baseName.text == "$0" {
let member = node.parent?.as(MemberAccessExprSyntax.self)?.declName.baseName.text
if member == "element" || member == "1" {
usage.one = true
} else if member == "offset" || member == "0" {
usage.zero = true
} else if node.isUnpacked {
usage.zero = true
usage.one = true
} else {
usage.zero = true
}
} else {
usage.one = true
}
}
}

private extension DeclReferenceExprSyntax {
var isUnpacked: Bool {
if let initializer = parent?.as(InitializerClauseSyntax.self),
Expand Down