Skip to content

Commit 3de9897

Browse files
author
Pavlo Tanaiev
committed
Fix indentation_width false positives for multi-line conditions
Skip continuation lines of multi-line `guard`/`if`/`while` condition lists that are aligned to the keyword rather than following the `indentation_width` grid. Add `include_multiline_conditions` option (default: `false`) to control this behavior. Fixes #4961
1 parent cc3b22c commit 3de9897

5 files changed

Lines changed: 137 additions & 0 deletions

File tree

CHANGELOG.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,13 @@
3131
[SimplyDanny](https://github.com/SimplyDanny)
3232
[#6466](https://github.com/realm/SwiftLint/issues/6466)
3333

34+
* Fix false positives in `indentation_width` rule for continuation lines
35+
of multi-line `guard`/`if`/`while` conditions. A new option
36+
`include_multiline_conditions` (default: `false`) controls whether
37+
these lines are checked for indentation.
38+
[tanaev](https://github.com/tanaev)
39+
[#4961](https://github.com/realm/SwiftLint/issues/4961)
40+
3441
## 0.63.2: High-Speed Extraction
3542

3643
### Breaking

Source/SwiftLintBuiltInRules/Rules/RuleConfigurations/IndentationWidthConfiguration.swift

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,4 +22,6 @@ struct IndentationWidthConfiguration: SeverityBasedRuleConfiguration {
2222
private(set) var includeCompilerDirectives = true
2323
@ConfigurationElement(key: "include_multiline_strings")
2424
private(set) var includeMultilineStrings = true
25+
@ConfigurationElement(key: "include_multiline_conditions")
26+
private(set) var includeMultilineConditions = false
2527
}

Source/SwiftLintBuiltInRules/Rules/Style/IndentationWidthRule.swift

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import Foundation
22
import SourceKittenFramework
3+
import SwiftSyntax
34

45
@DisabledWithoutSourceKit
56
struct IndentationWidthRule: OptInRule {
@@ -31,6 +32,18 @@ struct IndentationWidthRule: OptInRule {
3132
Example("firstLine\n\tsecondLine\n\t\tthirdLine\n\n\t\tfourthLine"),
3233
Example("firstLine\n\tsecondLine\n\t\tthirdLine\n\t//test\n\t\tfourthLine"),
3334
Example("firstLine\n secondLine\n thirdLine\nfourthLine"),
35+
Example("""
36+
guard let x = foo(),
37+
let y = bar() else {
38+
return
39+
}
40+
"""),
41+
Example("""
42+
if let x = foo(),
43+
let y = bar() {
44+
doSomething()
45+
}
46+
"""),
3447
],
3548
triggeringExamples: [
3649
Example("↓ firstLine", testMultiByteOffsets: false, testDisableCommand: false),
@@ -46,6 +59,14 @@ struct IndentationWidthRule: OptInRule {
4659
var violations: [StyleViolation] = []
4760
var previousLineIndentations: [Indentation] = []
4861

62+
let multilineConditionLines: Set<Int>
63+
if !configuration.includeMultilineConditions {
64+
let visitor = MultilineConditionLineVisitor(locationConverter: file.locationConverter)
65+
multilineConditionLines = visitor.walk(tree: file.syntaxTree, handler: \.continuationLines)
66+
} else {
67+
multilineConditionLines = []
68+
}
69+
4970
for line in file.lines {
5071
if ignoreCompilerDirective(line: line, in: file) { continue }
5172

@@ -55,6 +76,8 @@ struct IndentationWidthRule: OptInRule {
5576

5677
if ignoreComment(line: line, in: file) || ignoreMultilineStrings(line: line, in: file) { continue }
5778

79+
if multilineConditionLines.contains(line.index) { continue }
80+
5881
// Get space and tab count in prefix
5982
let prefix = String(line.content.prefix(indentationCharacterCount))
6083
let tabCount = prefix.filter { $0 == "\t" }.count
@@ -203,3 +226,32 @@ struct IndentationWidthRule: OptInRule {
203226
)
204227
}
205228
}
229+
230+
private final class MultilineConditionLineVisitor: SyntaxVisitor {
231+
private let locationConverter: SourceLocationConverter
232+
private(set) var continuationLines = Set<Int>()
233+
234+
init(locationConverter: SourceLocationConverter) {
235+
self.locationConverter = locationConverter
236+
super.init(viewMode: .sourceAccurate)
237+
}
238+
239+
override func visitPost(_ node: GuardStmtSyntax) {
240+
collectContinuationLines(keyword: node.guardKeyword, conditions: node.conditions)
241+
}
242+
243+
override func visitPost(_ node: IfExprSyntax) {
244+
collectContinuationLines(keyword: node.ifKeyword, conditions: node.conditions)
245+
}
246+
247+
override func visitPost(_ node: WhileStmtSyntax) {
248+
collectContinuationLines(keyword: node.whileKeyword, conditions: node.conditions)
249+
}
250+
251+
private func collectContinuationLines(keyword: TokenSyntax, conditions: ConditionElementListSyntax) {
252+
let keywordLine = locationConverter.location(for: keyword.positionAfterSkippingLeadingTrivia).line
253+
let conditionsEndLine = locationConverter.location(for: conditions.endPositionBeforeTrailingTrivia).line
254+
guard keywordLine < conditionsEndLine else { return }
255+
continuationLines.formUnion((keywordLine + 1)...conditionsEndLine)
256+
}
257+
}

Tests/BuiltInRulesTests/IndentationWidthRuleTests.swift

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -268,13 +268,81 @@ final class IndentationWidthRuleTests: SwiftLintTestCase {
268268
assert1Violation(in: example4, includeMultilineStrings: true)
269269
}
270270

271+
func testIncludeMultilineConditions() {
272+
// Multi-line guard — no violation with default config (includeMultilineConditions: false)
273+
assertNoViolation(in: """
274+
guard let x = foo(),
275+
let y = bar() else {
276+
return
277+
}
278+
""")
279+
280+
// Multi-line if — no violation
281+
assertNoViolation(in: """
282+
if let x = foo(),
283+
let y = bar() {
284+
doSomething()
285+
}
286+
""")
287+
288+
// Multi-line while — no violation
289+
assertNoViolation(in: """
290+
while let x = foo(),
291+
let y = bar() {
292+
doSomething()
293+
}
294+
""")
295+
296+
// Multi-line guard with includeMultilineConditions: true — violations fire
297+
assert1Violation(in: """
298+
guard let x = foo(),
299+
let y = bar() else {
300+
return
301+
}
302+
""", includeMultilineConditions: true)
303+
304+
// Multi-line if with includeMultilineConditions: true — violations fire
305+
assert1Violation(in: """
306+
if let x = foo(),
307+
let y = bar() {
308+
doSomething()
309+
}
310+
""", includeMultilineConditions: true)
311+
312+
// Single-line guard still works normally
313+
assertNoViolation(in: """
314+
guard let x = foo() else {
315+
return
316+
}
317+
""")
318+
319+
// Conditions on separate lines with proper indentation — no violation either way
320+
assertNoViolation(in: """
321+
guard
322+
let x = foo(),
323+
let y = bar()
324+
else {
325+
return
326+
}
327+
""")
328+
assertNoViolation(in: """
329+
guard
330+
let x = foo(),
331+
let y = bar()
332+
else {
333+
return
334+
}
335+
""", includeMultilineConditions: true)
336+
}
337+
271338
// MARK: Helpers
272339
private func countViolations(
273340
in example: Example,
274341
indentationWidth: Int? = nil,
275342
includeComments: Bool = true,
276343
includeCompilerDirectives: Bool = true,
277344
includeMultilineStrings: Bool = true,
345+
includeMultilineConditions: Bool = false,
278346
file: StaticString = #filePath,
279347
line: UInt = #line
280348
) -> Int {
@@ -285,6 +353,7 @@ final class IndentationWidthRuleTests: SwiftLintTestCase {
285353
configDict["include_comments"] = includeComments
286354
configDict["include_compiler_directives"] = includeCompilerDirectives
287355
configDict["include_multiline_strings"] = includeMultilineStrings
356+
configDict["include_multiline_conditions"] = includeMultilineConditions
288357

289358
guard let config = makeConfig(configDict, IndentationWidthRule.identifier) else {
290359
XCTFail("Unable to create rule configuration.", file: (file), line: line)
@@ -301,6 +370,7 @@ final class IndentationWidthRuleTests: SwiftLintTestCase {
301370
includeComments: Bool = true,
302371
includeCompilerDirectives: Bool = true,
303372
includeMultilineStrings: Bool = true,
373+
includeMultilineConditions: Bool = false,
304374
file: StaticString = #filePath,
305375
line: UInt = #line
306376
) {
@@ -311,6 +381,7 @@ final class IndentationWidthRuleTests: SwiftLintTestCase {
311381
includeComments: includeComments,
312382
includeCompilerDirectives: includeCompilerDirectives,
313383
includeMultilineStrings: includeMultilineStrings,
384+
includeMultilineConditions: includeMultilineConditions,
314385
file: file,
315386
line: line
316387
),
@@ -326,6 +397,7 @@ final class IndentationWidthRuleTests: SwiftLintTestCase {
326397
includeComments: Bool = true,
327398
includeCompilerDirectives: Bool = true,
328399
includeMultilineStrings: Bool = true,
400+
includeMultilineConditions: Bool = false,
329401
file: StaticString = #filePath,
330402
line: UInt = #line
331403
) {
@@ -336,6 +408,7 @@ final class IndentationWidthRuleTests: SwiftLintTestCase {
336408
includeComments: includeComments,
337409
includeCompilerDirectives: includeCompilerDirectives,
338410
includeMultilineStrings: includeMultilineStrings,
411+
includeMultilineConditions: includeMultilineConditions,
339412
file: file,
340413
line: line
341414
)
@@ -347,6 +420,7 @@ final class IndentationWidthRuleTests: SwiftLintTestCase {
347420
includeComments: Bool = true,
348421
includeCompilerDirectives: Bool = true,
349422
includeMultilineStrings: Bool = true,
423+
includeMultilineConditions: Bool = false,
350424
file: StaticString = #filePath,
351425
line: UInt = #line
352426
) {
@@ -357,6 +431,7 @@ final class IndentationWidthRuleTests: SwiftLintTestCase {
357431
includeComments: includeComments,
358432
includeCompilerDirectives: includeCompilerDirectives,
359433
includeMultilineStrings: includeMultilineStrings,
434+
includeMultilineConditions: includeMultilineConditions,
360435
file: file,
361436
line: line
362437
)

Tests/IntegrationTests/Resources/default_rule_configurations.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -538,6 +538,7 @@ indentation_width:
538538
include_comments: true
539539
include_compiler_directives: true
540540
include_multiline_strings: true
541+
include_multiline_conditions: false
541542
meta:
542543
opt-in: true
543544
correctable: false

0 commit comments

Comments
 (0)