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 unused_control_flow_label rule #2545

Merged
merged 5 commits into from
Jan 6, 2019
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 @@ -27,6 +27,11 @@
`.last(where: { /* ... */ })` is more efficient.
[Marcelo Fabri](https://github.com/marcelofabri)

* Add `unused_control_flow_label` rule to validate that control flow labels are
used.
[Marcelo Fabri](https://github.com/marcelofabri)
[#2227](https://github.com/realm/SwiftLint/issues/2227)

#### Bug Fixes

* Fix false positives on `first_where` rule when calling `filter` without a
Expand Down
91 changes: 91 additions & 0 deletions Rules.md
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,7 @@
* [Unneeded Parentheses in Closure Argument](#unneeded-parentheses-in-closure-argument)
* [Untyped Error in Catch](#untyped-error-in-catch)
* [Unused Closure Parameter](#unused-closure-parameter)
* [Unused Control Flow Label](#unused-control-flow-label)
* [Unused Enumerated](#unused-enumerated)
* [Unused Import](#unused-import)
* [Unused Optional Binding](#unused-optional-binding)
Expand Down Expand Up @@ -21539,6 +21540,96 @@ func foo () {



## Unused Control Flow Label

Identifier | Enabled by default | Supports autocorrection | Kind | Analyzer | Minimum Swift Compiler Version
--- | --- | --- | --- | --- | ---
`unused_control_flow_label` | Enabled | Yes | lint | No | 3.0.0

Unused control flow label should be removed.

### Examples

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

```swift
loop: while true { break loop }
```

```swift
loop: while true { continue loop }
```

```swift
loop:
while true { break loop }
```

```swift
while true { break }
```

```swift
loop: for x in array { break loop }
```

```swift
label: switch number {
case 1: print("1")
case 2: print("2")
default: break label
}
```

```swift
loop: repeat {
if x == 10 {
break loop
}
} while true
```

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

```swift
↓loop: while true { break }
```

```swift
↓loop: while true { break loop1 }
```

```swift
↓loop: while true { break outerLoop }
```

```swift
↓loop: for x in array { break }
```

```swift
↓label: switch number {
case 1: print("1")
case 2: print("2")
default: break
}
```

```swift
↓loop: repeat {
if x == 10 {
break
}
} while true
```

</details>



## Unused Enumerated

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 @@ -146,6 +146,7 @@ public let masterRuleList = RuleList(rules: [
UnneededParenthesesInClosureArgumentRule.self,
UntypedErrorInCatchRule.self,
UnusedClosureParameterRule.self,
UnusedControlFlowLabelRule.self,
UnusedEnumeratedRule.self,
UnusedImportRule.self,
UnusedOptionalBindingRule.self,
Expand Down
173 changes: 173 additions & 0 deletions Source/SwiftLintFramework/Rules/Lint/UnusedControlFlowLabelRule.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,173 @@
import Foundation
import SourceKittenFramework

public struct UnusedControlFlowLabelRule: ASTRule, ConfigurationProviderRule, AutomaticTestableRule, CorrectableRule {
public var configuration = SeverityConfiguration(.warning)

public init() {}

public static let description = RuleDescription(
identifier: "unused_control_flow_label",
name: "Unused Control Flow Label",
description: "Unused control flow label should be removed.",
kind: .lint,
nonTriggeringExamples: [
"loop: while true { break loop }",
"loop: while true { continue loop }",
"loop:\n while true { break loop }",
"while true { break }",
"loop: for x in array { break loop }",
"""
label: switch number {
case 1: print("1")
case 2: print("2")
default: break label
}
""",
"""
loop: repeat {
if x == 10 {
break loop
}
} while true
"""
],
triggeringExamples: [
"↓loop: while true { break }",
"↓loop: while true { break loop1 }",
"↓loop: while true { break outerLoop }",
"↓loop: for x in array { break }",
"""
↓label: switch number {
case 1: print("1")
case 2: print("2")
default: break
}
""",
"""
↓loop: repeat {
if x == 10 {
break
}
} while true
"""
],
corrections: [
"↓loop: while true { break }": "while true { break }",
"↓loop: while true { break loop1 }": "while true { break loop1 }",
"↓loop: while true { break outerLoop }": "while true { break outerLoop }",
"↓loop: for x in array { break }": "for x in array { break }",
"""
↓label: switch number {
case 1: print("1")
case 2: print("2")
default: break
}
""": """
switch number {
case 1: print("1")
case 2: print("2")
default: break
}
""",
"""
↓loop: repeat {
if x == 10 {
break
}
} while true
""": """
repeat {
if x == 10 {
break
}
} while true
"""
]
)

private static let kinds: Set<StatementKind> = [.if, .for, .forEach, .while, .repeatWhile, .switch]

public func validate(file: File, kind: StatementKind,
dictionary: [String: SourceKitRepresentable]) -> [StyleViolation] {
return self.violationRanges(in: file, kind: kind, dictionary: dictionary).map { range in
StyleViolation(ruleDescription: type(of: self).description,
severity: configuration.severity,
location: Location(file: file, characterOffset: range.location))
}
}

public func correct(file: File) -> [Correction] {
let violatingRanges = file.ruleEnabled(violatingRanges: violationRanges(in: file), for: self)
guard !violatingRanges.isEmpty else { return [] }

let description = type(of: self).description
var corrections = [Correction]()
var contents = file.contents
for range in violatingRanges {
var rangeToRemove = range
let contentsNSString = contents.bridge()
if let byteRange = contentsNSString.NSRangeToByteRange(start: range.location, length: range.length),
let nextToken = file.syntaxMap.tokens.first(where: { $0.offset > byteRange.location }),
let nextTokenLocation = contentsNSString.byteRangeToNSRange(start: nextToken.offset, length: 0) {
rangeToRemove.length = nextTokenLocation.location - range.location
}

contents = contentsNSString.replacingCharacters(in: rangeToRemove, with: "")
let location = Location(file: file, characterOffset: range.location)
corrections.append(Correction(ruleDescription: description, location: location))
}

file.write(contents)
return corrections
}

private func violationRanges(in file: File, kind: StatementKind,
dictionary: [String: SourceKitRepresentable]) -> [NSRange] {
guard type(of: self).kinds.contains(kind),
let offset = dictionary.offset, let length = dictionary.length,
case let byteRange = NSRange(location: offset, length: length),
case let tokens = file.syntaxMap.tokens(inByteRange: byteRange),
let firstToken = tokens.first,
SyntaxKind(rawValue: firstToken.type) == .identifier,
case let contents = file.contents.bridge(),
let tokenContent = contents.substring(with: firstToken),
let range = contents.byteRangeToNSRange(start: offset, length: length) else {
return []
}

let pattern = "(?:break|continue)\\s+\(tokenContent)\\b"
guard file.match(pattern: pattern, with: [.keyword, .identifier], range: range).isEmpty,
let violationRange = contents.byteRangeToNSRange(start: firstToken.offset,
length: firstToken.length) else {
return []
}

return [violationRange]
}

private func violationRanges(in file: File, dictionary: [String: SourceKitRepresentable]) -> [NSRange] {
let ranges = dictionary.substructure.flatMap { subDict -> [NSRange] in
var ranges = violationRanges(in: file, dictionary: subDict)
if let kind = subDict.kind.flatMap(StatementKind.init(rawValue:)) {
ranges += violationRanges(in: file, kind: kind, dictionary: subDict)
}

return ranges
}

return ranges.unique
}

private func violationRanges(in file: File) -> [NSRange] {
return violationRanges(in: file, dictionary: file.structure.dictionary).sorted { lhs, rhs in
lhs.location > rhs.location
}
}
}

private extension NSString {
func substring(with token: SyntaxToken) -> String? {
return substringWithByteRange(start: token.offset, length: token.length)
}
}
4 changes: 4 additions & 0 deletions SwiftLint.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -291,6 +291,7 @@
D4D1B9BB1EAC2C910028BE6A /* AccessControlLevel.swift in Sources */ = {isa = PBXBuildFile; fileRef = D4D1B9B91EAC2C870028BE6A /* AccessControlLevel.swift */; };
D4D383852145F550000235BD /* StaticOperatorRule.swift in Sources */ = {isa = PBXBuildFile; fileRef = D4D383842145F550000235BD /* StaticOperatorRule.swift */; };
D4D5A5FF1E1F3A1C00D15E0C /* ShorthandOperatorRule.swift in Sources */ = {isa = PBXBuildFile; fileRef = D4D5A5FE1E1F3A1C00D15E0C /* ShorthandOperatorRule.swift */; };
D4D7320D21E15ED4001C07D9 /* UnusedControlFlowLabelRule.swift in Sources */ = {isa = PBXBuildFile; fileRef = D4D7320C21E15ED4001C07D9 /* UnusedControlFlowLabelRule.swift */; };
D4DA1DF41E17511D0037413D /* CompilerProtocolInitRule.swift in Sources */ = {isa = PBXBuildFile; fileRef = D4DA1DF31E17511D0037413D /* CompilerProtocolInitRule.swift */; };
D4DA1DFA1E18D6200037413D /* LargeTupleRule.swift in Sources */ = {isa = PBXBuildFile; fileRef = D4DA1DF91E18D6200037413D /* LargeTupleRule.swift */; };
D4DA1DFC1E19CD300037413D /* GenerateDocsCommand.swift in Sources */ = {isa = PBXBuildFile; fileRef = D4DA1DFB1E19CD300037413D /* GenerateDocsCommand.swift */; };
Expand Down Expand Up @@ -745,6 +746,7 @@
D4D1B9B91EAC2C870028BE6A /* AccessControlLevel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AccessControlLevel.swift; sourceTree = "<group>"; };
D4D383842145F550000235BD /* StaticOperatorRule.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StaticOperatorRule.swift; sourceTree = "<group>"; };
D4D5A5FE1E1F3A1C00D15E0C /* ShorthandOperatorRule.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ShorthandOperatorRule.swift; sourceTree = "<group>"; };
D4D7320C21E15ED4001C07D9 /* UnusedControlFlowLabelRule.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UnusedControlFlowLabelRule.swift; sourceTree = "<group>"; };
D4DA1DF31E17511D0037413D /* CompilerProtocolInitRule.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CompilerProtocolInitRule.swift; sourceTree = "<group>"; };
D4DA1DF91E18D6200037413D /* LargeTupleRule.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LargeTupleRule.swift; sourceTree = "<group>"; };
D4DA1DFB1E19CD300037413D /* GenerateDocsCommand.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GenerateDocsCommand.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -1033,6 +1035,7 @@
D40E041B1F46E3B30043BC4E /* SuperfluousDisableCommandRule.swift */,
E88DEA811B0990A700A66CB0 /* TodoRule.swift */,
D40AD0891E032F9700F48C30 /* UnusedClosureParameterRule.swift */,
D4D7320C21E15ED4001C07D9 /* UnusedControlFlowLabelRule.swift */,
8F715B82213B528B00427BD9 /* UnusedImportRule.swift */,
8F6B3153213CDCD100858E44 /* UnusedPrivateDeclarationRule.swift */,
D442541E1DB87C3D00492EA4 /* ValidIBInspectableRule.swift */,
Expand Down Expand Up @@ -1920,6 +1923,7 @@
62DADC481FFF0423002B6319 /* PrefixedTopLevelConstantRule.swift in Sources */,
D4130D991E16CC1300242361 /* TypeNameRuleExamples.swift in Sources */,
24E17F721B14BB3F008195BE /* File+Cache.swift in Sources */,
D4D7320D21E15ED4001C07D9 /* UnusedControlFlowLabelRule.swift in Sources */,
6C1D763221A4E69600DEF783 /* Request+DisableSourceKit.swift in Sources */,
47ACC8981E7DC74E0088EEB2 /* ImplicitlyUnwrappedOptionalConfiguration.swift in Sources */,
787CDE39208E7D41005F3D2F /* SwitchCaseAlignmentConfiguration.swift in Sources */,
Expand Down
7 changes: 7 additions & 0 deletions Tests/LinuxMain.swift
Original file line number Diff line number Diff line change
Expand Up @@ -1253,6 +1253,12 @@ extension UnusedClosureParameterRuleTests {
]
}

extension UnusedControlFlowLabelRuleTests {
static var allTests: [(String, (UnusedControlFlowLabelRuleTests) -> () throws -> Void)] = [
("testWithDefaultConfiguration", testWithDefaultConfiguration)
]
}

extension UnusedEnumeratedRuleTests {
static var allTests: [(String, (UnusedEnumeratedRuleTests) -> () throws -> Void)] = [
("testWithDefaultConfiguration", testWithDefaultConfiguration)
Expand Down Expand Up @@ -1548,6 +1554,7 @@ XCTMain([
testCase(UnneededParenthesesInClosureArgumentRuleTests.allTests),
testCase(UntypedErrorInCatchRuleTests.allTests),
testCase(UnusedClosureParameterRuleTests.allTests),
testCase(UnusedControlFlowLabelRuleTests.allTests),
testCase(UnusedEnumeratedRuleTests.allTests),
testCase(UnusedImportRuleTests.allTests),
testCase(UnusedOptionalBindingRuleTests.allTests),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -648,6 +648,12 @@ class UnusedClosureParameterRuleTests: XCTestCase {
}
}

class UnusedControlFlowLabelRuleTests: XCTestCase {
func testWithDefaultConfiguration() {
verifyRule(UnusedControlFlowLabelRule.description)
}
}

class UnusedEnumeratedRuleTests: XCTestCase {
func testWithDefaultConfiguration() {
verifyRule(UnusedEnumeratedRule.description)
Expand Down