Skip to content

Commit f158dbd

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 f158dbd

File tree

5 files changed

+98
-1
lines changed

5 files changed

+98
-1
lines changed

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: 54 additions & 1 deletion
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,14 +59,17 @@ struct IndentationWidthRule: OptInRule {
4659
var violations: [StyleViolation] = []
4760
var previousLineIndentations: [Indentation] = []
4861

62+
let conditionContinuationLines = multilineConditionLines(in: file)
63+
4964
for line in file.lines {
5065
if ignoreCompilerDirective(line: line, in: file) { continue }
5166

5267
// Skip line if it's a whitespace-only line
5368
let indentationCharacterCount = line.content.countOfLeadingCharacters(in: CharacterSet(charactersIn: " \t"))
5469
if line.content.count == indentationCharacterCount { continue }
5570

56-
if ignoreComment(line: line, in: file) || ignoreMultilineStrings(line: line, in: file) { continue }
71+
if ignoreComment(line: line, in: file) || ignoreMultilineStrings(line: line, in: file)
72+
|| conditionContinuationLines.contains(line.index) { continue }
5773

5874
// Get space and tab count in prefix
5975
let prefix = String(line.content.prefix(indentationCharacterCount))
@@ -141,6 +157,14 @@ struct IndentationWidthRule: OptInRule {
141157
return violations
142158
}
143159

160+
private func multilineConditionLines(in file: SwiftLintFile) -> Set<Int> {
161+
if configuration.includeMultilineConditions {
162+
return []
163+
}
164+
let visitor = MultilineConditionLineVisitor(locationConverter: file.locationConverter)
165+
return visitor.walk(tree: file.syntaxTree, handler: \.continuationLines)
166+
}
167+
144168
private func ignoreCompilerDirective(line: Line, in file: SwiftLintFile) -> Bool {
145169
if configuration.includeCompilerDirectives {
146170
return false
@@ -203,3 +227,32 @@ struct IndentationWidthRule: OptInRule {
203227
)
204228
}
205229
}
230+
231+
private final class MultilineConditionLineVisitor: SyntaxVisitor {
232+
private let locationConverter: SourceLocationConverter
233+
private(set) var continuationLines = Set<Int>()
234+
235+
init(locationConverter: SourceLocationConverter) {
236+
self.locationConverter = locationConverter
237+
super.init(viewMode: .sourceAccurate)
238+
}
239+
240+
override func visitPost(_ node: GuardStmtSyntax) {
241+
collectContinuationLines(keyword: node.guardKeyword, conditions: node.conditions)
242+
}
243+
244+
override func visitPost(_ node: IfExprSyntax) {
245+
collectContinuationLines(keyword: node.ifKeyword, conditions: node.conditions)
246+
}
247+
248+
override func visitPost(_ node: WhileStmtSyntax) {
249+
collectContinuationLines(keyword: node.whileKeyword, conditions: node.conditions)
250+
}
251+
252+
private func collectContinuationLines(keyword: TokenSyntax, conditions: ConditionElementListSyntax) {
253+
let keywordLine = locationConverter.location(for: keyword.positionAfterSkippingLeadingTrivia).line
254+
let conditionsEndLine = locationConverter.location(for: conditions.endPositionBeforeTrailingTrivia).line
255+
guard keywordLine < conditionsEndLine else { return }
256+
continuationLines.formUnion((keywordLine + 1)...conditionsEndLine)
257+
}
258+
}

Tests/BuiltInRulesTests/IndentationWidthRuleTests.swift

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

271+
func testIncludeMultilineConditions() {
272+
let guardExample = """
273+
guard let x = foo(),
274+
let y = bar() else {
275+
return
276+
}
277+
"""
278+
let ifExample = """
279+
if let x = foo(),
280+
let y = bar() {
281+
doSomething()
282+
}
283+
"""
284+
285+
assertNoViolation(in: guardExample)
286+
assertNoViolation(in: ifExample)
287+
assertNoViolation(in: "while let x = foo(),\n let y = bar() {\n doSomething()\n}")
288+
assert1Violation(in: guardExample, includeMultilineConditions: true)
289+
assert1Violation(in: ifExample, includeMultilineConditions: true)
290+
assertNoViolation(in: "guard let x = foo() else {\n return\n}")
291+
assertNoViolation(
292+
in: "guard\n let x = foo(),\n let y = bar()\nelse {\n return\n}",
293+
includeMultilineConditions: true
294+
)
295+
}
296+
271297
// MARK: Helpers
272298
private func countViolations(
273299
in example: Example,
274300
indentationWidth: Int? = nil,
275301
includeComments: Bool = true,
276302
includeCompilerDirectives: Bool = true,
277303
includeMultilineStrings: Bool = true,
304+
includeMultilineConditions: Bool = false,
278305
file: StaticString = #filePath,
279306
line: UInt = #line
280307
) -> Int {
@@ -285,6 +312,7 @@ final class IndentationWidthRuleTests: SwiftLintTestCase {
285312
configDict["include_comments"] = includeComments
286313
configDict["include_compiler_directives"] = includeCompilerDirectives
287314
configDict["include_multiline_strings"] = includeMultilineStrings
315+
configDict["include_multiline_conditions"] = includeMultilineConditions
288316

289317
guard let config = makeConfig(configDict, IndentationWidthRule.identifier) else {
290318
XCTFail("Unable to create rule configuration.", file: (file), line: line)
@@ -301,6 +329,7 @@ final class IndentationWidthRuleTests: SwiftLintTestCase {
301329
includeComments: Bool = true,
302330
includeCompilerDirectives: Bool = true,
303331
includeMultilineStrings: Bool = true,
332+
includeMultilineConditions: Bool = false,
304333
file: StaticString = #filePath,
305334
line: UInt = #line
306335
) {
@@ -311,6 +340,7 @@ final class IndentationWidthRuleTests: SwiftLintTestCase {
311340
includeComments: includeComments,
312341
includeCompilerDirectives: includeCompilerDirectives,
313342
includeMultilineStrings: includeMultilineStrings,
343+
includeMultilineConditions: includeMultilineConditions,
314344
file: file,
315345
line: line
316346
),
@@ -326,6 +356,7 @@ final class IndentationWidthRuleTests: SwiftLintTestCase {
326356
includeComments: Bool = true,
327357
includeCompilerDirectives: Bool = true,
328358
includeMultilineStrings: Bool = true,
359+
includeMultilineConditions: Bool = false,
329360
file: StaticString = #filePath,
330361
line: UInt = #line
331362
) {
@@ -336,6 +367,7 @@ final class IndentationWidthRuleTests: SwiftLintTestCase {
336367
includeComments: includeComments,
337368
includeCompilerDirectives: includeCompilerDirectives,
338369
includeMultilineStrings: includeMultilineStrings,
370+
includeMultilineConditions: includeMultilineConditions,
339371
file: file,
340372
line: line
341373
)
@@ -347,6 +379,7 @@ final class IndentationWidthRuleTests: SwiftLintTestCase {
347379
includeComments: Bool = true,
348380
includeCompilerDirectives: Bool = true,
349381
includeMultilineStrings: Bool = true,
382+
includeMultilineConditions: Bool = false,
350383
file: StaticString = #filePath,
351384
line: UInt = #line
352385
) {
@@ -357,6 +390,7 @@ final class IndentationWidthRuleTests: SwiftLintTestCase {
357390
includeComments: includeComments,
358391
includeCompilerDirectives: includeCompilerDirectives,
359392
includeMultilineStrings: includeMultilineStrings,
393+
includeMultilineConditions: includeMultilineConditions,
360394
file: file,
361395
line: line
362396
)

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)