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) + } }