Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add new indentation_width rule #2765

Merged
merged 17 commits into from
Jan 6, 2020
Merged
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 @@ -30,6 +30,11 @@
to any declarations.
[Marcelo Fabri](https://github.com/marcelofabri)
[#2989](https://github.com/realm/SwiftLint/issues/2989)

* Add new indentation opt-in rule (`indentation_width`) checking for
super-basic additive indentation pattern.
[Frederick Pietschmann](https://github.com/fredpi)
[#227](https://github.com/realm/SwiftLint/issues/227)

#### Bug Fixes

Expand Down
78 changes: 78 additions & 0 deletions Rules.md
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@
* [Implicit Getter](#implicit-getter)
* [Implicit Return](#implicit-return)
* [Implicitly Unwrapped Optional](#implicitly-unwrapped-optional)
* [Indentation Width](#indentation-width)
* [Inert Defer](#inert-defer)
* [Is Disjoint](#is-disjoint)
* [Joined Default Parameter](#joined-default-parameter)
Expand Down Expand Up @@ -10638,6 +10639,83 @@ func foo(int: Int!) {}



## Indentation Width

Identifier | Enabled by default | Supports autocorrection | Kind | Analyzer | Minimum Swift Compiler Version
--- | --- | --- | --- | --- | ---
`indentation_width` | Disabled | No | style | No | 3.0.0

Indent code using either one tab or the configured amount of spaces, unindent to match previous indentations. Don't indent the first line.

### Examples

<details>
<summary>Non Triggering Examples</summary>

```swift
firstLine
secondLine
```

```swift
firstLine
secondLine
```

```swift
firstLine
secondLine
thirdLine

fourthLine
```

```swift
firstLine
secondLine
thirdLine
//test
fourthLine
```

```swift
firstLine
secondLine
thirdLine
fourthLine
```

</details>
<details>
<summary>Triggering Examples</summary>

```swift
firstLine
```

```swift
firstLine
secondLine
```

```swift
firstLine
secondLine

fourthLine
```

```swift
firstLine
secondLine
thirdLine
fourthLine
```

</details>



## Inert Defer

Identifier | Enabled by default | Supports autocorrection | Kind | Analyzer | Minimum Swift Compiler Version
Expand Down
1 change: 1 addition & 0 deletions Source/SwiftLintFramework/Models/MasterRuleList.swift
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@ public let masterRuleList = RuleList(rules: [
ImplicitGetterRule.self,
ImplicitReturnRule.self,
ImplicitlyUnwrappedOptionalRule.self,
IndentationWidthRule.self,
InertDeferRule.self,
IsDisjointRule.self,
JoinedDefaultParameterRule.self,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
public struct IndentationWidthConfiguration: RuleConfiguration, Equatable {
public var consoleDescription: String {
return "severity: \(severityConfiguration.consoleDescription), "
+ "indentation_width: \(indentationWidth)"
+ "include_comments: \(includeComments)"
}

public private(set) var severityConfiguration: SeverityConfiguration
public private(set) var indentationWidth: Int
public private(set) var includeComments: Bool

public init(
severity: ViolationSeverity,
indentationWidth: Int,
includeComments: Bool
) {
self.severityConfiguration = SeverityConfiguration(severity)
self.indentationWidth = indentationWidth
self.includeComments = includeComments
}

public mutating func apply(configuration: Any) throws {
guard let configurationDict = configuration as? [String: Any] else {
throw ConfigurationError.unknownConfiguration
}

if let config = configurationDict["severity"] {
try severityConfiguration.apply(configuration: config)
}

if let indentationWidth = configurationDict["indentation_width"] as? Int, indentationWidth >= 1 {
self.indentationWidth = indentationWidth
}

if let includeComments = configurationDict["include_comments"] as? Bool {
self.includeComments = includeComments
}
}
}
167 changes: 167 additions & 0 deletions Source/SwiftLintFramework/Rules/Style/IndentationWidthRule.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,167 @@
import Foundation
import SourceKittenFramework

public struct IndentationWidthRule: ConfigurationProviderRule, OptInRule {
// MARK: - Subtypes
private enum Indentation: Equatable {
case tabs(Int)
case spaces(Int)

func spacesEquivalent(indentationWidth: Int) -> Int {
switch self {
case let .tabs(tabs): return tabs * indentationWidth
case let .spaces(spaces): return spaces
}
}
}

// MARK: - Properties
public var configuration = IndentationWidthConfiguration(
severity: .warning,
indentationWidth: 4,
includeComments: true
)
public static let description = RuleDescription(
identifier: "indentation_width",
name: "Indentation Width",
description: "Indent code using either one tab or the configured amount of spaces, " +
"unindent to match previous indentations. Don't indent the first line.",
kind: .style,
nonTriggeringExamples: [
"firstLine\nsecondLine",
"firstLine\n secondLine",
"firstLine\n\tsecondLine\n\t\tthirdLine\n\n\t\tfourthLine",
"firstLine\n\tsecondLine\n\t\tthirdLine\n//test\n\t\tfourthLine",
"firstLine\n secondLine\n thirdLine\nfourthLine"
],
triggeringExamples: [
" firstLine",
"firstLine\n secondLine",
"firstLine\n\tsecondLine\n\n\t\t\tfourthLine",
"firstLine\n secondLine\n thirdLine\n fourthLine"
]
)

// MARK: - Initializers
public init() {}

// MARK: - Methods: Validation
public func validate(file: SwiftLintFile) -> [StyleViolation] { // swiftlint:disable:this function_body_length
var violations: [StyleViolation] = []
var previousLineIndentations: [Indentation] = []

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

if !configuration.includeComments {
// Skip line if it's part of a comment
let syntaxKindsInLine = Set(file.syntaxMap.tokens(inByteRange: line.byteRange).kinds)
if !syntaxKindsInLine.isEmpty && SyntaxKind.commentKinds.isSuperset(of: syntaxKindsInLine) { continue }
}

// Get space and tab count in prefix
let prefix = String(line.content.prefix(indentationCharacterCount))
let tabCount = prefix.filter { $0 == "\t" }.count
let spaceCount = prefix.filter { $0 == " " }.count

// Determine indentation
let indentation: Indentation
if tabCount != 0 && spaceCount != 0 {
// Catch mixed indentation
violations.append(
StyleViolation(
ruleDescription: IndentationWidthRule.description,
severity: .warning,
location: Location(file: file, characterOffset: line.range.location),
reason: "Code should be indented with tabs or " +
"\(configuration.indentationWidth) spaces, but not both in the same line."
)
)

// Model this line's indentation using spaces (although it's tabs & spaces) to let parsing continue
indentation = .spaces(spaceCount + tabCount * configuration.indentationWidth)
} else if tabCount != 0 {
indentation = .tabs(tabCount)
} else {
indentation = .spaces(spaceCount)
}

// Catch indented first line
guard !previousLineIndentations.isEmpty else {
previousLineIndentations = [indentation]

if indentation != .spaces(0) {
// There's an indentation although this is the first line!
violations.append(
StyleViolation(
ruleDescription: IndentationWidthRule.description,
severity: .warning,
location: Location(file: file, characterOffset: line.range.location),
reason: "The first line shall not be indented."
)
)
}

continue
}

let linesValidationResult = previousLineIndentations.map {
validate(indentation: indentation, comparingTo: $0)
}

// Catch wrong indentation or wrong unindentation
if !linesValidationResult.contains(true) {
let isIndentation = previousLineIndentations.last.map {
indentation.spacesEquivalent(indentationWidth: configuration.indentationWidth) >=
$0.spacesEquivalent(indentationWidth: configuration.indentationWidth)
} ?? true

let indentWidth = configuration.indentationWidth
violations.append(
StyleViolation(
ruleDescription: IndentationWidthRule.description,
severity: .warning,
location: Location(file: file, characterOffset: line.range.location),
reason: isIndentation ?
"Code should be indented using one tab or \(indentWidth) spaces." :
"Code should be unindented by multiples of one tab or multiples of \(indentWidth) spaces."
)
)
}

if linesValidationResult.first == true {
// Reset previousLineIndentations to this line only
// if this line's indentation matches the last valid line's indentation (first in the array)
previousLineIndentations = [indentation]
} else {
// We not only store this line's indentation, but also keep what was stored before.
// Therefore, the next line can be indented either according to the last valid line
// or any of the succeeding, failing lines.
// This mechanism avoids duplicate warnings.
previousLineIndentations.append(indentation)
}
}

return violations
}

/// Validates whether the indentation of a specific line is valid
/// based on the indentation of the previous line.
///
/// Returns a Bool determining the validity of the indentation.
private func validate(indentation: Indentation, comparingTo lastIndentation: Indentation) -> Bool {
let currentSpaceEquivalent = indentation.spacesEquivalent(indentationWidth: configuration.indentationWidth)
let lastSpaceEquivalent = lastIndentation.spacesEquivalent(indentationWidth: configuration.indentationWidth)

return (
// Allow indent by indentationWidth
currentSpaceEquivalent == lastSpaceEquivalent + configuration.indentationWidth ||
(
(lastSpaceEquivalent - currentSpaceEquivalent) >= 0 &&
(lastSpaceEquivalent - currentSpaceEquivalent) % configuration.indentationWidth == 0
) // Allow unindent if it stays in the grid
)
}
}
14 changes: 13 additions & 1 deletion SwiftLint.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -243,6 +243,9 @@
C946FECB1EAE67EE007DD778 /* LetVarWhitespaceRule.swift in Sources */ = {isa = PBXBuildFile; fileRef = C946FEC91EAE5E20007DD778 /* LetVarWhitespaceRule.swift */; };
C9802F2F1E0C8AEE008AB27F /* TrailingCommaRuleTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = C9802F2E1E0C8AEE008AB27F /* TrailingCommaRuleTests.swift */; };
CC26ED07204DEB510013BBBC /* RuleIdentifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = CC26ED05204DE86E0013BBBC /* RuleIdentifier.swift */; };
CC6D28592292F0380052B682 /* IndentationWidthRule.swift in Sources */ = {isa = PBXBuildFile; fileRef = CC6D28572292EF460052B682 /* IndentationWidthRule.swift */; };
CC6D285B2292F0600052B682 /* IndentationWidthConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = CC6D285A2292F0600052B682 /* IndentationWidthConfiguration.swift */; };
CC8C6D2322935F5200A55D1A /* IndentationWidthRuleTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = CC8C6D2122935F4E00A55D1A /* IndentationWidthRuleTests.swift */; };
CCD8B87920559D1E00B75847 /* DisableAllTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = CCD8B87720559C4A00B75847 /* DisableAllTests.swift */; };
CE8178ED1EAC039D0063186E /* UnusedOptionalBindingConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE8178EB1EAC02CD0063186E /* UnusedOptionalBindingConfiguration.swift */; };
D0AAAB5019FB0960007B24B3 /* SwiftLintFramework.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = D0D1216D19E87B05005E4BAA /* SwiftLintFramework.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
Expand Down Expand Up @@ -742,6 +745,9 @@
C946FEC91EAE5E20007DD778 /* LetVarWhitespaceRule.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LetVarWhitespaceRule.swift; sourceTree = "<group>"; };
C9802F2E1E0C8AEE008AB27F /* TrailingCommaRuleTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TrailingCommaRuleTests.swift; sourceTree = "<group>"; };
CC26ED05204DE86E0013BBBC /* RuleIdentifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RuleIdentifier.swift; sourceTree = "<group>"; };
CC6D28572292EF460052B682 /* IndentationWidthRule.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IndentationWidthRule.swift; sourceTree = "<group>"; usesTabs = 0; };
CC6D285A2292F0600052B682 /* IndentationWidthConfiguration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IndentationWidthConfiguration.swift; sourceTree = "<group>"; };
CC8C6D2122935F4E00A55D1A /* IndentationWidthRuleTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IndentationWidthRuleTests.swift; sourceTree = "<group>"; };
CCD8B87720559C4A00B75847 /* DisableAllTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DisableAllTests.swift; sourceTree = "<group>"; };
CE8178EB1EAC02CD0063186E /* UnusedOptionalBindingConfiguration.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UnusedOptionalBindingConfiguration.swift; sourceTree = "<group>"; };
D0D1211B19E87861005E4BAA /* main.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = main.swift; sourceTree = "<group>"; usesTabs = 0; };
Expand Down Expand Up @@ -1052,6 +1058,7 @@
8B01E4FB20A4183C00C9233E /* FunctionParameterCountConfiguration.swift */,
47ACC8971E7DC74E0088EEB2 /* ImplicitlyUnwrappedOptionalConfiguration.swift */,
B1FD3AB8238877F200CE121E /* ImplicitReturnConfiguration.swift */,
CC6D285A2292F0600052B682 /* IndentationWidthConfiguration.swift */,
3B034B6C1E0BE544005D49A9 /* LineLengthConfiguration.swift */,
F90DBD7E2092E669002CC310 /* MissingDocsRuleConfiguration.swift */,
188B3FF3207D61230073C2D6 /* ModifierOrderConfiguration.swift */,
Expand Down Expand Up @@ -1215,10 +1222,10 @@
1E82D5581D7775C7009553D7 /* ClosureSpacingRule.swift */,
756B585C2138ECD300D1A4E9 /* CollectionAlignmentRule.swift */,
E88DEA831B0990F500A66CB0 /* ColonRule.swift */,
D4D0B8F32211428D0053A116 /* ColonRuleExamples.swift */,
D47EF4811F69E34D0012C4CA /* ColonRule+Dictionary.swift */,
D47EF47F1F69E3100012C4CA /* ColonRule+FunctionCall.swift */,
D47EF4831F69E3D60012C4CA /* ColonRule+Type.swift */,
D4D0B8F32211428D0053A116 /* ColonRuleExamples.swift */,
695BE9CE1BDFD92B0071E985 /* CommaRule.swift */,
93E0C3CD1D67BD7F007FA25D /* ConditionalReturnsOnNewlineRule.swift */,
65454F451B14D73800319A6C /* ControlStatementRule.swift */,
Expand All @@ -1235,6 +1242,7 @@
D43DB1071DC573DA00281215 /* ImplicitGetterRule.swift */,
D4470D561EB69225008A1B2E /* ImplicitReturnRule.swift */,
B1FD3ABE238BC6D400CE121E /* ImplicitReturnRuleExamples.swift */,
CC6D28572292EF460052B682 /* IndentationWidthRule.swift */,
E88DEA7D1B098F2A00A66CB0 /* LeadingWhitespaceRule.swift */,
C946FEC91EAE5E20007DD778 /* LetVarWhitespaceRule.swift */,
D4EA77C91F81FACC00C315FB /* LiteralExpressionEndIdentationRule.swift */,
Expand Down Expand Up @@ -1537,6 +1545,7 @@
47ACC89B1E7DCFA00088EEB2 /* ImplicitlyUnwrappedOptionalRuleTests.swift */,
B1FD3ABA238BC5BC00CE121E /* ImplicitReturnConfigurationTests.swift */,
B1FD3ABC238BC5FB00CE121E /* ImplicitReturnRuleTests.swift */,
CC8C6D2122935F4E00A55D1A /* IndentationWidthRuleTests.swift */,
E832F10C1B17E725003F265F /* IntegrationTests.swift */,
3B63D46C1E1F05160057BE35 /* LineLengthConfigurationTests.swift */,
3B63D46E1E1F09DF0057BE35 /* LineLengthRuleTests.swift */,
Expand Down Expand Up @@ -2103,6 +2112,7 @@
D44254271DB9C15C00492EA4 /* SyntacticSugarRule.swift in Sources */,
D4EA77C81F817FD200C315FB /* UnneededBreakInSwitchRule.swift in Sources */,
D4D383852145F550000235BD /* StaticOperatorRule.swift in Sources */,
CC6D285B2292F0600052B682 /* IndentationWidthConfiguration.swift in Sources */,
006204DC1E1E492F00FFFBE1 /* VerticalWhitespaceConfiguration.swift in Sources */,
E88198441BEA93D200333A11 /* ColonRule.swift in Sources */,
623675B21F962FC4009BE6F3 /* QuickDiscouragedPendingTestRuleExamples.swift in Sources */,
Expand Down Expand Up @@ -2296,6 +2306,7 @@
D4DABFD71E2C23B1009617B6 /* NotificationCenterDetachmentRule.swift in Sources */,
3BA79C9B1C4767910057E705 /* NSRange+SwiftLint.swift in Sources */,
D4D5A5FF1E1F3A1C00D15E0C /* ShorthandOperatorRule.swift in Sources */,
CC6D28592292F0380052B682 /* IndentationWidthRule.swift in Sources */,
C3DE5DAC1E7DF9CA00761483 /* FatalErrorMessageRule.swift in Sources */,
A3184D56215BCEFF00621EA2 /* LegacyRandomRule.swift in Sources */,
626C16E21F948EBC00BB7475 /* QuickDiscouragedFocusedTestRuleExamples.swift in Sources */,
Expand Down Expand Up @@ -2338,6 +2349,7 @@
E832F10D1B17E725003F265F /* IntegrationTests.swift in Sources */,
D4C27C001E12DFF500DF713E /* LinterCacheTests.swift in Sources */,
D45255C81F0932F8003C9B56 /* RuleDescription+Examples.swift in Sources */,
CC8C6D2322935F5200A55D1A /* IndentationWidthRuleTests.swift in Sources */,
E81ADD721ED5ED9D000CD451 /* RegionTests.swift in Sources */,
D4998DE91DF194F20006E05D /* FileHeaderRuleTests.swift in Sources */,
750BBD0B214180AF007EC437 /* CollectionAlignmentRuleTests.swift in Sources */,
Expand Down
Loading