diff --git a/CHANGELOG.md b/CHANGELOG.md
index 7abe058712d..262532bcd41 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -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.
diff --git a/Rules.md b/Rules.md
index 8121bff9ba2..fac62091e5e 100644
--- a/Rules.md
+++ b/Rules.md
@@ -117,6 +117,7 @@
* [Void Return](#void-return)
* [Weak Delegate](#weak-delegate)
* [XCTFail Message](#xctfail-message)
+* [Yoda condition rule](#yoda-condition-rule)
--------
## Array Init
@@ -16145,3 +16146,80 @@ func testFoo() {
```
+
+
+
+## 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
+
+
+Non Triggering Examples
+
+```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 { }
+
+```
+
+
+
+Triggering Examples
+
+```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 { }
+```
+
+
diff --git a/Source/SwiftLintFramework/Models/MasterRuleList.swift b/Source/SwiftLintFramework/Models/MasterRuleList.swift
index e0b05b2a089..f1833e26721 100644
--- a/Source/SwiftLintFramework/Models/MasterRuleList.swift
+++ b/Source/SwiftLintFramework/Models/MasterRuleList.swift
@@ -124,5 +124,6 @@ public let masterRuleList = RuleList(rules: [
VerticalWhitespaceRule.self,
VoidReturnRule.self,
WeakDelegateRule.self,
- XCTFailMessageRule.self
+ XCTFailMessageRule.self,
+ YodaConditionRule.self
])
diff --git a/Source/SwiftLintFramework/Rules/RuleConfigurations/YodaConditionConfiguration.swift b/Source/SwiftLintFramework/Rules/RuleConfigurations/YodaConditionConfiguration.swift
new file mode 100644
index 00000000000..f3badea12d4
--- /dev/null
+++ b/Source/SwiftLintFramework/Rules/RuleConfigurations/YodaConditionConfiguration.swift
@@ -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
+ }
+}
diff --git a/Source/SwiftLintFramework/Rules/YodaConditionRule.swift b/Source/SwiftLintFramework/Rules/YodaConditionRule.swift
new file mode 100644
index 00000000000..2e5f05d1be0
--- /dev/null
+++ b/Source/SwiftLintFramework/Rules/YodaConditionRule.swift
@@ -0,0 +1,96 @@
+//
+// 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 = "\\s+" + // Starting with whitespace
+ "(" + // First capturing group
+ "(?:\\\"[\\\"\\w\\ ]+\")" + // Multiple words between quotes
+ "|" + // OR
+ "(?:\\d+" + // Number of digits
+ "(?:\\.\\d*)?)" + // Optionally followed by a dot and any number digits
+ ")" + // End first capturing group
+ "\\s+" + // Followed by whitespace
+ "(" + // Second capturing group
+ "==|!=|>|<|>=|<=" + // One of comparison operators
+ ")" + // End second capturing group
+ "\\s+" + // Followed by whitespace
+ "(" + // Third capturing group
+ "\\w+" + // Number of words
+ ")" // End third capturing group
+ private static let regularExpression = regex(pattern)
+ private let observedStatements: Set = [.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"
+ ],
+ 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
+ }
+}
diff --git a/SwiftLint.xcodeproj/project.pbxproj b/SwiftLint.xcodeproj/project.pbxproj
index 4587d454966..a4267fb6d98 100644
--- a/SwiftLint.xcodeproj/project.pbxproj
+++ b/SwiftLint.xcodeproj/project.pbxproj
@@ -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 */; };
@@ -354,6 +356,8 @@
02FD8AEE1BFC18D60014BFFB /* ExtendedNSStringTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ExtendedNSStringTests.swift; sourceTree = ""; };
094384FF1D5D2382009168CF /* WeakDelegateRule.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = WeakDelegateRule.swift; sourceTree = ""; };
094385021D5D4F78009168CF /* PrivateOutletRule.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PrivateOutletRule.swift; sourceTree = ""; };
+ 1803C8B51FD30DEC0007141A /* YodaConditionConfiguration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = YodaConditionConfiguration.swift; sourceTree = ""; };
+ 1872906F1FC37A9B0016BEA2 /* YodaConditionRule.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = YodaConditionRule.swift; sourceTree = ""; };
1E18574A1EADBA51004F89F7 /* NoExtensionAccessModifierRule.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NoExtensionAccessModifierRule.swift; sourceTree = ""; };
1E3C2D701EE36C6F00C8386D /* PrivateOverFilePrivateRule.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PrivateOverFilePrivateRule.swift; sourceTree = ""; };
1E82D5581D7775C7009553D7 /* ClosureSpacingRule.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ClosureSpacingRule.swift; sourceTree = ""; };
@@ -733,6 +737,7 @@
BF48D2D61CBCCA5F0080BDAE /* TrailingWhitespaceConfiguration.swift */,
CE8178EB1EAC02CD0063186E /* UnusedOptionalBindingConfiguration.swift */,
006204DA1E1E48F900FFFBE1 /* VerticalWhitespaceConfiguration.swift */,
+ 1803C8B51FD30DEC0007141A /* YodaConditionConfiguration.swift */,
);
path = RuleConfigurations;
sourceTree = "";
@@ -1121,6 +1126,7 @@
D47079AE1DFE520000027086 /* VoidReturnRule.swift */,
094384FF1D5D2382009168CF /* WeakDelegateRule.swift */,
626D02961F31CBCC0054788D /* XCTFailMessageRule.swift */,
+ 1872906F1FC37A9B0016BEA2 /* YodaConditionRule.swift */,
);
path = Rules;
sourceTree = "";
@@ -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 */,
@@ -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 */,
diff --git a/Tests/SwiftLintFrameworkTests/RulesTests.swift b/Tests/SwiftLintFrameworkTests/RulesTests.swift
index 2a71944aa69..f258094e000 100644
--- a/Tests/SwiftLintFrameworkTests/RulesTests.swift
+++ b/Tests/SwiftLintFrameworkTests/RulesTests.swift
@@ -439,4 +439,8 @@ class RulesTests: XCTestCase {
func testXCTFailMessage() {
verifyRule(XCTFailMessageRule.description)
}
+
+ func testYodaConditionRule() {
+ verifyRule(YodaConditionRule.description)
+ }
}