Skip to content

Commit

Permalink
Introduction of Yoda condition checking.
Browse files Browse the repository at this point in the history
This PR aims to implement [realm#1924][1].
[1]: realm#1924
  • Loading branch information
Daniel Metzing committed Dec 18, 2017
1 parent a02d4e1 commit 72a4c8e
Show file tree
Hide file tree
Showing 7 changed files with 229 additions and 1 deletion.
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,10 @@
* Invalidate cache when Swift patch version changes.
[Norio Nomura](https://github.com/norio-nomura)

* Add `yoda_condition` opt-in rule which warns to avoid Yoda conditions.
[Daniel Metzing](https://github.com/dirtydanee)
[#1924](https://github.com/realm/SwiftLint/issues/1924)

##### Bug Fixes

* None.
Expand Down
78 changes: 78 additions & 0 deletions Rules.md
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,7 @@
* [Void Return](#void-return)
* [Weak Delegate](#weak-delegate)
* [XCTFail Message](#xctfail-message)
* [Yoda condition rule](#yoda-condition-rule)
--------

## Array Init
Expand Down Expand Up @@ -16145,3 +16146,80 @@ func testFoo() {
```

</details>



## Yoda condition rule

Identifier | Enabled by default | Supports autocorrection | Kind
--- | --- | --- | ---
`yoda_condition` | Disabled | No | lint

The variable should be placed on the left, the constant on the right of a comparison operator.

### Examples

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

```swift
if foo == 42 {}

```

```swift
if foo <= 42.42 {}

```

```swift
guard foo >= 42 else { return }

```

```swift
guard foo != "str str" else { return }
```

```swift
while foo < 10 { }

```

```swift
while foo > 1 { }

```

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

```swift
if 42 == foo {}

```

```swift
if 42.42 >= foo {}

```

```swift
guard 42 <= foo else { return }

```

```swift
guard "str str" != foo else { return }
```

```swift
while 10 > foo { }
```

```swift
while 1 < foo { }
```

</details>
3 changes: 2 additions & 1 deletion Source/SwiftLintFramework/Models/MasterRuleList.swift
Original file line number Diff line number Diff line change
Expand Up @@ -124,5 +124,6 @@ public let masterRuleList = RuleList(rules: [
VerticalWhitespaceRule.self,
VoidReturnRule.self,
WeakDelegateRule.self,
XCTFailMessageRule.self
XCTFailMessageRule.self,
YodaConditionRule.self
])
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
//
// YodaConditionConfiguration.swift
// SwiftLint
//
// Created by Daniel.Metzing on 02/12/17.
// Copyright © 2017 Realm. All rights reserved.
//

public struct YodaConditionConfiguration: RuleConfiguration, Equatable {

private(set) var severityConfiguration = SeverityConfiguration(.warning)

public var consoleDescription: String {
return severityConfiguration.consoleDescription
}

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

if let severityString = configuration["severity"] as? String {
try severityConfiguration.apply(configuration: severityString)
}
}

public static func == (lhs: YodaConditionConfiguration,
rhs: YodaConditionConfiguration) -> Bool {
return lhs.severityConfiguration == rhs.severityConfiguration
}
}
102 changes: 102 additions & 0 deletions Source/SwiftLintFramework/Rules/YodaConditionRule.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
//
// YodaConditionRule.swift
// SwiftLint
//
// Created by Daniel.Metzing on 20/11/17.
// Copyright © 2017 Realm. All rights reserved.
//

import Foundation
import SourceKittenFramework

public struct YodaConditionRule: ASTRule, OptInRule, ConfigurationProviderRule {

public var configuration = YodaConditionConfiguration()

public init() {}

private static let pattern = "(?<!" + // Starting negative lookbehind
"(" + // First capturing group
"\\+|-|\\*|\\/" + // One of basic operators
")" + // Ending negative lookbehind
")" + // End first capturing group
"\\s+" + // Starting with whitespace
"(" + // Second capturing group
"(?:\\\"[\\\"\\w\\ ]+\")" + // Multiple words between quotes
"|" + // OR
"(?:\\d+" + // Number of digits
"(?:\\.\\d*)?)" + // Optionally followed by a dot and any number digits
")" + // End second capturing group
"\\s+" + // Followed by whitespace
"(" + // Third capturing group
"==|!=|>|<|>=|<=" + // One of comparison operators
")" + // End third capturing group
"\\s+" + // Followed by whitespace
"(" + // Fourth capturing group
"\\w+" + // Number of words
")" // End fourth capturing group
private static let regularExpression = regex(pattern)
private let observedStatements: Set <StatementKind> = [.if, .guard, .while]

public static let description = RuleDescription(
identifier: "yoda_condition",
name: "Yoda condition rule",
description: "The variable should be placed on the left, the constant on the right of a comparison operator.",
kind: .lint,
nonTriggeringExamples: [
"if foo == 42 {}\n",
"if foo <= 42.42 {}\n",
"guard foo >= 42 else { return }\n",
"guard foo != \"str str\" else { return }",
"while foo < 10 { }\n",
"while foo > 1 { }\n",
"while foo + 1 == 2"
],
triggeringExamples: [
"↓if 42 == foo {}\n",
"↓if 42.42 >= foo {}\n",
"↓guard 42 <= foo else { return }\n",
"↓guard \"str str\" != foo else { return }",
"↓while 10 > foo { }",
"↓while 1 < foo { }"
])

public func validate(file: File,
kind: StatementKind,
dictionary: [String: SourceKitRepresentable]) -> [StyleViolation] {

guard observedStatements.contains(kind),
let offset = dictionary.offset,
let length = dictionary.length
else {
return []
}

var matches = [NSTextCheckingResult]()
for line in file.lines where line.byteRange.contains(offset) {
matches = YodaConditionRule.regularExpression.matches(in: line.content,
options: NSRegularExpression.MatchingOptions(),
range: NSRange(location: 0,
length: line.content.utf16.count))
}

return matches.map { _ -> StyleViolation in
return StyleViolation(ruleDescription: type(of: self).description,
severity: .warning,
location: Location(file: file,
characterOffset: startOffset(of: offset,
with: length,
in: file)),
reason: configuration.consoleDescription)
}
}

private func startOffset(of offset: Int, with length: Int, in file: File) -> Int {
let range = file.contents.bridge().byteRangeToNSRange(start: offset, length: length)
guard let startOffset = range?.location else {
return offset
}

return startOffset
}
}
8 changes: 8 additions & 0 deletions SwiftLint.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@
02FD8AEF1BFC18D60014BFFB /* ExtendedNSStringTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02FD8AEE1BFC18D60014BFFB /* ExtendedNSStringTests.swift */; };
094385011D5D2894009168CF /* WeakDelegateRule.swift in Sources */ = {isa = PBXBuildFile; fileRef = 094384FF1D5D2382009168CF /* WeakDelegateRule.swift */; };
094385041D5D4F7C009168CF /* PrivateOutletRule.swift in Sources */ = {isa = PBXBuildFile; fileRef = 094385021D5D4F78009168CF /* PrivateOutletRule.swift */; };
1803C8B71FD30EF90007141A /* YodaConditionConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1803C8B51FD30DEC0007141A /* YodaConditionConfiguration.swift */; };
187290721FC37CA50016BEA2 /* YodaConditionRule.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1872906F1FC37A9B0016BEA2 /* YodaConditionRule.swift */; };
1E18574B1EADBA51004F89F7 /* NoExtensionAccessModifierRule.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1E18574A1EADBA51004F89F7 /* NoExtensionAccessModifierRule.swift */; };
1E3C2D711EE36C6F00C8386D /* PrivateOverFilePrivateRule.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1E3C2D701EE36C6F00C8386D /* PrivateOverFilePrivateRule.swift */; };
1E82D5591D7775C7009553D7 /* ClosureSpacingRule.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1E82D5581D7775C7009553D7 /* ClosureSpacingRule.swift */; };
Expand Down Expand Up @@ -354,6 +356,8 @@
02FD8AEE1BFC18D60014BFFB /* ExtendedNSStringTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ExtendedNSStringTests.swift; sourceTree = "<group>"; };
094384FF1D5D2382009168CF /* WeakDelegateRule.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = WeakDelegateRule.swift; sourceTree = "<group>"; };
094385021D5D4F78009168CF /* PrivateOutletRule.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PrivateOutletRule.swift; sourceTree = "<group>"; };
1803C8B51FD30DEC0007141A /* YodaConditionConfiguration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = YodaConditionConfiguration.swift; sourceTree = "<group>"; };
1872906F1FC37A9B0016BEA2 /* YodaConditionRule.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = YodaConditionRule.swift; sourceTree = "<group>"; };
1E18574A1EADBA51004F89F7 /* NoExtensionAccessModifierRule.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NoExtensionAccessModifierRule.swift; sourceTree = "<group>"; };
1E3C2D701EE36C6F00C8386D /* PrivateOverFilePrivateRule.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PrivateOverFilePrivateRule.swift; sourceTree = "<group>"; };
1E82D5581D7775C7009553D7 /* ClosureSpacingRule.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ClosureSpacingRule.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -733,6 +737,7 @@
BF48D2D61CBCCA5F0080BDAE /* TrailingWhitespaceConfiguration.swift */,
CE8178EB1EAC02CD0063186E /* UnusedOptionalBindingConfiguration.swift */,
006204DA1E1E48F900FFFBE1 /* VerticalWhitespaceConfiguration.swift */,
1803C8B51FD30DEC0007141A /* YodaConditionConfiguration.swift */,
);
path = RuleConfigurations;
sourceTree = "<group>";
Expand Down Expand Up @@ -1121,6 +1126,7 @@
D47079AE1DFE520000027086 /* VoidReturnRule.swift */,
094384FF1D5D2382009168CF /* WeakDelegateRule.swift */,
626D02961F31CBCC0054788D /* XCTFailMessageRule.swift */,
1872906F1FC37A9B0016BEA2 /* YodaConditionRule.swift */,
);
path = Rules;
sourceTree = "<group>";
Expand Down Expand Up @@ -1505,6 +1511,7 @@
E847F0A91BFBBABD00EA9363 /* EmptyCountRule.swift in Sources */,
D46252541DF63FB200BE2CA1 /* NumberSeparatorRule.swift in Sources */,
E315B83C1DFA4BC500621B44 /* DynamicInlineRule.swift in Sources */,
1803C8B71FD30EF90007141A /* YodaConditionConfiguration.swift in Sources */,
1E18574B1EADBA51004F89F7 /* NoExtensionAccessModifierRule.swift in Sources */,
D42D2B381E09CC0D00CD7A2E /* FirstWhereRule.swift in Sources */,
D4B0226F1E0C75F9007E5297 /* VerticalParameterAlignmentRule.swift in Sources */,
Expand Down Expand Up @@ -1600,6 +1607,7 @@
D40FE89D1F867BFF006433E2 /* OverrideInExtensionRule.swift in Sources */,
D41E7E0B1DF9DABB0065259A /* RedundantStringEnumValueRule.swift in Sources */,
E88DEA711B09847500A66CB0 /* ViolationSeverity.swift in Sources */,
187290721FC37CA50016BEA2 /* YodaConditionRule.swift in Sources */,
1E3C2D711EE36C6F00C8386D /* PrivateOverFilePrivateRule.swift in Sources */,
B2902A0C1D66815600BFCCF7 /* PrivateUnitTestRule.swift in Sources */,
D47A51101DB2DD4800A4CC21 /* AttributesRule.swift in Sources */,
Expand Down
4 changes: 4 additions & 0 deletions Tests/SwiftLintFrameworkTests/RulesTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -439,4 +439,8 @@ class RulesTests: XCTestCase {
func testXCTFailMessage() {
verifyRule(XCTFailMessageRule.description)
}

func testYodaConditionRule() {
verifyRule(YodaConditionRule.description)
}
}

0 comments on commit 72a4c8e

Please sign in to comment.