Skip to content

Commit 31d1e45

Browse files
committed
Adds Variable Shadowing Rule
1 parent ae7ba72 commit 31d1e45

5 files changed

Lines changed: 164 additions & 0 deletions

File tree

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,10 @@
5151
and FEFF formatting character (U+FEFF) in string literals, which can cause hard-to-debug issues.
5252
[kapitoshka438](https://github.com/kapitoshka438)
5353
[#6045](https://github.com/realm/SwiftLint/issues/6045)
54+
* Add `variable_shadowing` rule that flags when a variable declaration shadows
55+
an identifier from an outer scope.
56+
[nadeemnali](https://github.com/nadeemnali)
57+
[#6228](https://github.com/realm/SwiftLint/issues/6228)
5458

5559
### Bug Fixes
5660

Source/SwiftLintBuiltInRules/Models/BuiltInRules.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -240,6 +240,7 @@ public let builtInRules: [any Rule.Type] = [
240240
UnusedParameterRule.self,
241241
UnusedSetterValueRule.self,
242242
ValidIBInspectableRule.self,
243+
VariableShadowingRule.self,
243244
VerticalParameterAlignmentOnCallRule.self,
244245
VerticalParameterAlignmentRule.self,
245246
VerticalWhitespaceBetweenCasesRule.self,
Lines changed: 148 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,148 @@
1+
import SwiftLintCore
2+
import SwiftSyntax
3+
4+
@SwiftSyntaxRule
5+
struct VariableShadowingRule: Rule {
6+
var configuration = SeverityConfiguration<Self>(.warning)
7+
8+
static let description = RuleDescription(
9+
identifier: "variable_shadowing",
10+
name: "Variable Shadowing",
11+
description: "Prefer not to shadow variables declared in outer scopes",
12+
kind: .lint,
13+
nonTriggeringExamples: [
14+
Example("""
15+
var a: String?
16+
func test(a: String?) {
17+
print(a)
18+
}
19+
"""),
20+
Example("""
21+
var a: String = "hello"
22+
if let b = a {
23+
print(b)
24+
}
25+
"""),
26+
Example("""
27+
var a: String?
28+
func test() {
29+
if let b = a {
30+
print(b)
31+
}
32+
}
33+
"""),
34+
Example("""
35+
for i in 1...10 {
36+
print(i)
37+
}
38+
for j in 1...10 {
39+
print(j)
40+
}
41+
"""),
42+
Example("""
43+
func test() {
44+
var a: String = "hello"
45+
func nested() {
46+
var b: String = "world"
47+
print(a, b)
48+
}
49+
}
50+
"""),
51+
Example("""
52+
class Test {
53+
var a: String?
54+
func test(a: String?) {
55+
print(a)
56+
}
57+
}
58+
"""),
59+
],
60+
triggeringExamples: [
61+
Example("""
62+
var outer: String = "hello"
63+
func test() {
64+
let ↓outer = "world"
65+
print(outer)
66+
}
67+
"""),
68+
]
69+
)
70+
}
71+
72+
private extension VariableShadowingRule {
73+
final class Visitor: DeclaredIdentifiersTrackingVisitor<ConfigurationType> {
74+
override func visit(_ node: VariableDeclSyntax) -> SyntaxVisitorContinueKind {
75+
// Early exit for member blocks (class/struct properties)
76+
if node.parent?.is(MemberBlockItemSyntax.self) != true {
77+
// Check for shadowing BEFORE adding to scope
78+
node.bindings.forEach { binding in
79+
checkForShadowing(in: binding.pattern)
80+
}
81+
}
82+
return super.visit(node)
83+
}
84+
85+
override func visit(_ node: CodeBlockItemListSyntax) -> SyntaxVisitorContinueKind {
86+
guard let parent = node.parent else {
87+
return super.visit(node)
88+
}
89+
90+
// Call super first so the base visitor opens the child scope and collects identifiers
91+
let kind = super.visit(node)
92+
93+
// Check conditions if this is an if/while body AFTER identifiers from conditions were collected
94+
if let ifStmt = parent.as(IfExprSyntax.self) {
95+
checkForShadowingInConditions(ifStmt.conditions)
96+
} else if let whileStmt = parent.as(WhileStmtSyntax.self) {
97+
checkForShadowingInConditions(whileStmt.conditions)
98+
}
99+
100+
return kind
101+
}
102+
103+
override func visitPost(_ node: GuardStmtSyntax) {
104+
// Let the base visitor collect identifiers first (it does so in its visitPost)
105+
super.visitPost(node)
106+
// Check for shadowing in guard conditions after collection
107+
checkForShadowingInConditions(node.conditions)
108+
}
109+
110+
private func checkForShadowingInConditions(_ conditions: ConditionElementListSyntax) {
111+
for element in conditions {
112+
if let binding = element.condition.as(OptionalBindingConditionSyntax.self) {
113+
checkForShadowing(in: binding.pattern)
114+
}
115+
}
116+
}
117+
118+
private func checkForShadowing(in pattern: PatternSyntax) {
119+
if let identifier = pattern.as(IdentifierPatternSyntax.self) {
120+
let identifierText = identifier.identifier.text
121+
if isShadowingOuterScope(identifierText) {
122+
violations.append(identifier.identifier.positionAfterSkippingLeadingTrivia)
123+
}
124+
return
125+
}
126+
127+
if let tuple = pattern.as(TuplePatternSyntax.self) {
128+
tuple.elements.forEach { element in
129+
checkForShadowing(in: element.pattern)
130+
}
131+
return
132+
}
133+
134+
// Other pattern kinds are not relevant for shadowing checks here; no action needed.
135+
}
136+
137+
private func isShadowingOuterScope(_ identifier: String) -> Bool {
138+
guard !scope.isEmpty, scope.count > 1 else { return false }
139+
140+
// Use early exit and lazy evaluation for better performance
141+
for scopeDeclarations in scope.dropLast() where
142+
scopeDeclarations.lazy.contains(where: { $0.declares(id: identifier) }) {
143+
return true
144+
}
145+
return false
146+
}
147+
}
148+
}

Tests/GeneratedTests/GeneratedTests_10.swift

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,12 @@ final class ValidIBInspectableRuleGeneratedTests: SwiftLintTestCase {
8585
}
8686
}
8787

88+
final class VariableShadowingRuleGeneratedTests: SwiftLintTestCase {
89+
func testWithDefaultConfiguration() {
90+
verifyRule(VariableShadowingRule.description)
91+
}
92+
}
93+
8894
final class VerticalParameterAlignmentOnCallRuleGeneratedTests: SwiftLintTestCase {
8995
func testWithDefaultConfiguration() {
9096
verifyRule(VerticalParameterAlignmentOnCallRule.description)

Tests/IntegrationTests/Resources/default_rule_configurations.yml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1383,6 +1383,11 @@ valid_ibinspectable:
13831383
meta:
13841384
opt-in: false
13851385
correctable: false
1386+
variable_shadowing:
1387+
severity: warning
1388+
meta:
1389+
opt-in: false
1390+
correctable: false
13861391
vertical_parameter_alignment:
13871392
severity: warning
13881393
meta:

0 commit comments

Comments
 (0)