Skip to content
Open
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
1 change: 1 addition & 0 deletions .swiftlint.yml
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ disabled_rules:
- trailing_closure
- type_contents_order
- vertical_whitespace_between_cases
- variable_shadowing

# Configurations
attributes:
Expand Down
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,11 @@
[kapitoshka438](https://github.com/kapitoshka438)
[#6045](https://github.com/realm/SwiftLint/issues/6045)

* Add `variable_shadowing` rule that flags when a variable declaration shadows
an identifier from an outer scope.
[nadeemnali](https://github.com/nadeemnali)
[#6228](https://github.com/realm/SwiftLint/issues/6228)

### Bug Fixes

* Add an `ignore_attributes` option to `implicit_optional_initialization` so
Expand Down
1 change: 1 addition & 0 deletions Source/SwiftLintBuiltInRules/Models/BuiltInRules.swift
Original file line number Diff line number Diff line change
Expand Up @@ -240,6 +240,7 @@ public let builtInRules: [any Rule.Type] = [
UnusedParameterRule.self,
UnusedSetterValueRule.self,
ValidIBInspectableRule.self,
VariableShadowingRule.self,
VerticalParameterAlignmentOnCallRule.self,
VerticalParameterAlignmentRule.self,
VerticalWhitespaceBetweenCasesRule.self,
Expand Down
313 changes: 313 additions & 0 deletions Source/SwiftLintBuiltInRules/Rules/Lint/VariableShadowingRule.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,313 @@
import SwiftLintCore
import SwiftSyntax

@SwiftSyntaxRule
struct VariableShadowingRule: Rule {
var configuration = VariableShadowingConfiguration()

static let description = RuleDescription(
identifier: "variable_shadowing",
name: "Variable Shadowing",
description: "Do not shadow variables declared in outer scopes",
kind: .lint,
nonTriggeringExamples: [
Example("""
var a: String?
func test(a: String?) {
print(a)
}
""", configuration: ["ignore_parameters": true]),
Example("""
var a: String = "hello"
if let b = a {
print(b)
}
"""),
Example("""
var a: String?
func test() {
if let b = a {
print(b)
}
}
"""),
Example("""
for i in 1...10 {
print(i)
}
for j in 1...10 {
print(j)
}
"""),
Example("""
func test() {
var a: String = "hello"
func nested() {
var b: String = "world"
print(a, b)
}
}
"""),
Example("""
class Test {
var a: String?
func test(a: String?) {
print(a)
}
}
"""),
Example("""
var outer: String = "hello"
if let inner = Optional(outer) {
print(inner)
}
"""),
Example("""
var a: String = "outer"
let (b, c) = ("first", "second")
print(a, b, c)
"""),
Example("""
class Test {
var property: String = "class property"
func test() {
var localVar = "local"
print(property, localVar)
}
}
"""),
Example("""
func outer() {
func inner() {
print("no shadowing")
}
}
"""),
Example("""
var result: String?
if let unwrappedResult = result {
print(unwrappedResult)
}
"""),
Example("""
var value: Int? = 10
guard let safeValue = value else {
return
}
print(safeValue)
"""),
Example("""
var data: [Int?] = [1, nil, 3]
for case let item? in data {
print(item)
}
"""),
],
triggeringExamples: [
Example("""
var outer: String = "hello"
func test() {
let ↓outer = "world"
print(outer)
}
"""),
Example("""
var x = 1
do {
let ↓x = 2
print(x)
}
"""),
Example("""
var counter = 0
func incrementCounter() {
var ↓counter = 1
counter += 1
}
"""),
Example("""
func outer() {
var value = 10
do {
let ↓value = 20
print(value)
}
}
"""),
Example("""
var globalName = "global"
func test() {
for item in [1, 2, 3] {
var ↓globalName = "local"
print(globalName)
}
}
"""),
Example("""
var foo = 1
do {
let ↓foo = 2
}
"""),
Example("""
var bar = 1
func test() {
let ↓bar = 2
}
"""),
Example("""
var a = 1
if let ↓a = Optional(2) {
let ↓a = 3
print(a)
}
"""),
Example("""
var i = 1
for ↓i in 1...3 {
let ↓i = 2
print(i)
}
"""),
Example("""
func test() {
var a = 1
do {
var ↓a = 2
print(a)
}
}
"""),
Example("""
func test() {
var a = 1
if true {
var ↓a = 2
print(a)
}
}
"""),
Example("""
func test() {
var a = 1
for _ in 0..<1 {
var ↓a = 2
print(a)
}
}
"""),
Example("""
func test() {
var a = 1
while true {
var ↓a = 2
break
}
}
"""),
Example("""
var a = 1
if let ↓a = Optional(2) {}
"""),
Example("""
var i = 1
for ↓i in 1...3 {}
"""),
Example("""
var a: String?
func test(↓a: String?) {
print(a)
}
""", configuration: ["ignore_parameters": 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.

There should be a few more example involving properties, local variables, parameters, functions, tuples, optional bindings, ...

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Examples added for all the use cases

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.

What about:

var a = 1
if let a = b {}
var i = 1
for i in list {}
struct S {
  var a = 1
  var b: Int {
    let a = 2
    return a
  }
}
var a = 1
func a(a: Int) {} // Should we trigger for the function name and/or the parameter (optionally)?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Thanks for highlighting, i have added additional triggering examples, expanding the triggering examples

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

var a = 1
func a(a: Int) {} // Should we trigger for the function name and/or the parameter (optionally)?

Triggering for parameters is optional, but the current behavior (not triggering) is standard and user-friendly. If you want to add an option to control this, I can add a configuration flag (e.g., ignoreParameters: true/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.

Thanks for highlighting, i have added additional triggering examples, expanding the triggering examples

Sure? I can't find them.

Triggering for parameters is optional, but the current behavior (not triggering) is standard and user-friendly. If you want to add an option to control this, I can add a configuration flag (e.g., ignoreParameters: true/false).

I'd prefer an option for that as I think some would like that. However, I leave it open to you to implement it or ignore it for now. In the latter case, this can be a follow-up.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Thanks for highlighting, i have added additional triggering examples, expanding the triggering examples

Sure? I can't find them.

Apologies, didn't push it yet.

Triggering for parameters is optional, but the current behavior (not triggering) is standard and user-friendly. If you want to add an option to control this, I can add a configuration flag (e.g., ignoreParameters: true/false).

I'd prefer an option for that as I think some would like that. However, I leave it open to you to implement it or ignore it for now. In the latter case, this can be a follow-up.

Yes, i can look into this, will tag you once all the changes are pushed

Copy link
Copy Markdown
Contributor Author

@nadeemnali nadeemnali Apr 8, 2026

Choose a reason for hiding this comment

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

Hi @SimplyDanny, I have updated the rules with configuration and also added relevent examples, so all changes are pushed now

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.

I'm still missing

struct S {
  var a = 1
  var b: Int {
    let a = 2
    return a
  }
}

And like if expressions, we can also have while let a = ... {}
and guard let a = ... else {}.

)
}

private extension VariableShadowingRule {
final class Visitor: DeclaredIdentifiersTrackingVisitor<VariableShadowingConfiguration> {
override func visit(_ node: VariableDeclSyntax) -> SyntaxVisitorContinueKind {
if node.parent?.is(MemberBlockItemSyntax.self) == false {
node.bindings.forEach { binding in
checkForShadowing(in: binding.pattern)
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.

No test case fails if checkForBindingShadowing is used here. Is checkForShadowing really necessary? I actually doubt it. If it is, we need an example.

}
}
return super.visit(node)
}

override func visit(_ node: FunctionDeclSyntax) -> SyntaxVisitorContinueKind {
if !configuration.ignoreParameters {
for param in node.signature.parameterClause.parameters {
let nameToken = param.secondName ?? param.firstName
if nameToken.text != "_", isShadowingAnyScope(nameToken.text) {
violations.append(nameToken.positionAfterSkippingLeadingTrivia)
}
}
}
return super.visit(node)
}

override func visit(_ node: ForStmtSyntax) -> SyntaxVisitorContinueKind {
checkForBindingShadowing(in: node.pattern)
return super.visit(node)
}

override func visit(_ node: IfExprSyntax) -> SyntaxVisitorContinueKind {
for condition in node.conditions {
if let optBinding = condition.condition.as(OptionalBindingConditionSyntax.self) {
checkForBindingShadowing(in: optBinding.pattern)
}
}
return super.visit(node)
}

// Used for VariableDecl: the new identifier is added to the *current* scope,
// so we only check ancestor scopes (dropLast).
private func checkForShadowing(in pattern: PatternSyntax) {
if let identifier = pattern.as(IdentifierPatternSyntax.self) {
let identifierText = identifier.identifier.text
if isShadowingOuterScope(identifierText) {
violations.append(identifier.identifier.positionAfterSkippingLeadingTrivia)
}
} else if let tuple = pattern.as(TuplePatternSyntax.self) {
tuple.elements.forEach { element in
checkForShadowing(in: element.pattern)
}
} else if let valueBinding = pattern.as(ValueBindingPatternSyntax.self) {
checkForShadowing(in: valueBinding.pattern)
}
}

// Used for if-let / for-loop bindings: the new identifier is added to a *child* scope,
// so we check all current scopes.
private func checkForBindingShadowing(in pattern: PatternSyntax) {
if let identifier = pattern.as(IdentifierPatternSyntax.self) {
let identifierText = identifier.identifier.text
if isShadowingAnyScope(identifierText) {
violations.append(identifier.identifier.positionAfterSkippingLeadingTrivia)
}
} else if let tuple = pattern.as(TuplePatternSyntax.self) {
tuple.elements.forEach { element in
checkForBindingShadowing(in: element.pattern)
}
} else if let valueBinding = pattern.as(ValueBindingPatternSyntax.self) {
checkForBindingShadowing(in: valueBinding.pattern)
}
}

private func isShadowingOuterScope(_ identifier: String) -> Bool {
guard scope.count > 1 else { return false }

for scopeDeclarations in scope.dropLast() where
scopeDeclarations.contains(where: { $0.declares(id: identifier) }) {
return true
}
return false
}

/// Checks all scope levels including the current one. Used for parameter checking
/// since parameters are declared into a child scope, not the current one.
private func isShadowingAnyScope(_ identifier: String) -> Bool {
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.

We might not need this anymore once #6589 is merged.

scope.contains { $0.contains { $0.declares(id: identifier) } }
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import SwiftLintCore

@AutoConfigParser
struct VariableShadowingConfiguration: SeverityBasedRuleConfiguration {
@ConfigurationElement(key: "severity")
private(set) var severityConfiguration = SeverityConfiguration<Parent>(.warning)
@ConfigurationElement(key: "ignore_parameters")
private(set) var ignoreParameters = true
}
15 changes: 15 additions & 0 deletions Tests/BuiltInRulesTests/VariableShadowingRuleTests.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
@testable import SwiftLintBuiltInRules
import TestHelpers
import XCTest

final class VariableShadowingRuleTests: SwiftLintTestCase {
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 test isn't needed. You already check the option in the rule's examples.

func testWithIgnoreParametersTrue() {
let configuration = ["ignore_parameters": true]
verifyRule(VariableShadowingRule.description, ruleConfiguration: configuration)
}

func testWithIgnoreParametersFalse() {
let configuration = ["ignore_parameters": false]
verifyRule(VariableShadowingRule.description, ruleConfiguration: configuration)
}
}
12 changes: 6 additions & 6 deletions Tests/GeneratedTests/GeneratedTests_10.swift
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,12 @@ final class ValidIBInspectableRuleGeneratedTests: SwiftLintTestCase {
}
}

final class VariableShadowingRuleGeneratedTests: SwiftLintTestCase {
func testWithDefaultConfiguration() {
verifyRule(VariableShadowingRule.description)
}
}

final class VerticalParameterAlignmentOnCallRuleGeneratedTests: SwiftLintTestCase {
func testWithDefaultConfiguration() {
verifyRule(VerticalParameterAlignmentOnCallRule.description)
Expand Down Expand Up @@ -150,9 +156,3 @@ final class XCTSpecificMatcherRuleGeneratedTests: SwiftLintTestCase {
verifyRule(XCTSpecificMatcherRule.description)
}
}

final class YodaConditionRuleGeneratedTests: SwiftLintTestCase {
func testWithDefaultConfiguration() {
verifyRule(YodaConditionRule.description)
}
}
Loading