diff --git a/CHANGELOG.md b/CHANGELOG.md index b0e76a4c15..a17e713aaf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -26,6 +26,13 @@ [JP Simard](https://github.com/jpsim) [#861](https://github.com/realm/SwiftLint/issues/861) +* Add `NimbleOperatorRule` opt-in rule that enforces using + [operator overloads](https://github.com/Quick/Nimble/#operator-overloads) + instead of free matcher functions when using + [Nimble](https://github.com/Quick/Nimble). + [Marcelo Fabri](https://github.com/marcelofabri) + [#881](https://github.com/realm/SwiftLint/issues/881) + ##### Bug Fixes diff --git a/Source/SwiftLintFramework/Models/MasterRuleList.swift b/Source/SwiftLintFramework/Models/MasterRuleList.swift index d4eb5eb51c..cb26f58e56 100644 --- a/Source/SwiftLintFramework/Models/MasterRuleList.swift +++ b/Source/SwiftLintFramework/Models/MasterRuleList.swift @@ -67,6 +67,7 @@ public let masterRuleList = RuleList(rules: MarkRule.self, MissingDocsRule.self, NestingRule.self, + NimbleOperatorRule.self, OpeningBraceRule.self, OperatorFunctionWhitespaceRule.self, OverriddenSuperCallRule.self, diff --git a/Source/SwiftLintFramework/Rules/NimbleOperatorRule.swift b/Source/SwiftLintFramework/Rules/NimbleOperatorRule.swift new file mode 100644 index 0000000000..d5494357b7 --- /dev/null +++ b/Source/SwiftLintFramework/Rules/NimbleOperatorRule.swift @@ -0,0 +1,63 @@ +// +// NimbleOperatorRule.swift +// SwiftLint +// +// Created by Marcelo Fabri on 20/11/16. +// Copyright © 2016 Realm. All rights reserved. +// + +import SourceKittenFramework + +public struct NimbleOperatorRule: ConfigurationProviderRule, OptInRule { + public var configuration = SeverityConfiguration(.Warning) + + public init() {} + + public static let description = RuleDescription( + identifier: "nimble_operator", + name: "Nimble Operator", + description: "Prefer Nimble operator overloads over free matcher functions.", + nonTriggeringExamples: [ + "expect(seagull.squawk) != \"Hi!\"\n", + "expect(\"Hi!\") == \"Hi!\"\n", + "expect(10) > 2\n", + "expect(10) >= 10\n", + "expect(10) < 11\n", + "expect(10) <= 10\n", + "expect(x) === x", + "expect(10) == 10", + "expect(object.asyncFunction()).toEventually(equal(1))\n", + "expect(actual).to(haveCount(expected))\n" + ], + triggeringExamples: [ + "↓expect(seagull.squawk).toNot(equal(\"Hi\"))\n", + "↓expect(12).toNot(equal(10))\n", + "↓expect(10).to(equal(10))\n", + "↓expect(10).to(beGreaterThan(8))\n", + "↓expect(10).to(beGreaterThanOrEqualTo(10))\n", + "↓expect(10).to(beLessThan(11))\n", + "↓expect(10).to(beLessThanOrEqualTo(10))\n", + "↓expect(x).to(beIdenticalTo(x))\n", + "expect(10) > 2\n ↓expect(10).to(beGreaterThan(2))\n", + ] + ) + + public func validateFile(file: File) -> [StyleViolation] { + let operators = ["equal", "beIdenticalTo", "beGreaterThan", + "beGreaterThanOrEqualTo", "beLessThan", "beLessThanOrEqualTo"] + let operatorsPattern = "(" + operators.joinWithSeparator("|") + ")" + let pattern = "expect\\((.(?!expect\\())+?\\)\\.to(Not)?\\(\(operatorsPattern)\\(" + let excludingKinds = SyntaxKind.commentKinds() + + let matches = file.matchPattern(pattern).filter { + // excluding comment kinds and making sure first token (`expect`) is an identifier + $0.1.filter(excludingKinds.contains).isEmpty && $0.1.first == .Identifier + } + + return matches.map { + StyleViolation(ruleDescription: self.dynamicType.description, + severity: configuration.severity, + location: Location(file: file, byteOffset: $0.0.location)) + } + } +} diff --git a/SwiftLint.xcodeproj/project.pbxproj b/SwiftLint.xcodeproj/project.pbxproj index 54bbb17a49..586d2a3d10 100644 --- a/SwiftLint.xcodeproj/project.pbxproj +++ b/SwiftLint.xcodeproj/project.pbxproj @@ -77,6 +77,7 @@ D44254271DB9C15C00492EA4 /* SyntacticSugarRule.swift in Sources */ = {isa = PBXBuildFile; fileRef = D44254251DB9C12300492EA4 /* SyntacticSugarRule.swift */; }; D44AD2761C0AA5350048F7B0 /* LegacyConstructorRule.swift in Sources */ = {isa = PBXBuildFile; fileRef = D44AD2741C0AA3730048F7B0 /* LegacyConstructorRule.swift */; }; D47A510E1DB29EEB00A4CC21 /* SwitchCaseOnNewlineRule.swift in Sources */ = {isa = PBXBuildFile; fileRef = D47A510D1DB29EEB00A4CC21 /* SwitchCaseOnNewlineRule.swift */; }; + D4DAE8BC1DE14E8F00B0AE7A /* NimbleOperatorRule.swift in Sources */ = {isa = PBXBuildFile; fileRef = D4DAE8BB1DE14E8F00B0AE7A /* NimbleOperatorRule.swift */; }; DAD3BE4A1D6ECD9500660239 /* PrivateOutletRuleConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = DAD3BE491D6ECD9500660239 /* PrivateOutletRuleConfiguration.swift */; }; E57B23C11B1D8BF000DEA512 /* ReturnArrowWhitespaceRule.swift in Sources */ = {isa = PBXBuildFile; fileRef = E57B23C01B1D8BF000DEA512 /* ReturnArrowWhitespaceRule.swift */; }; E802ED001C56A56000A35AE1 /* Benchmark.swift in Sources */ = {isa = PBXBuildFile; fileRef = E802ECFF1C56A56000A35AE1 /* Benchmark.swift */; }; @@ -276,6 +277,7 @@ D44254251DB9C12300492EA4 /* SyntacticSugarRule.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SyntacticSugarRule.swift; sourceTree = ""; }; D44AD2741C0AA3730048F7B0 /* LegacyConstructorRule.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LegacyConstructorRule.swift; sourceTree = ""; }; D47A510D1DB29EEB00A4CC21 /* SwitchCaseOnNewlineRule.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SwitchCaseOnNewlineRule.swift; sourceTree = ""; }; + D4DAE8BB1DE14E8F00B0AE7A /* NimbleOperatorRule.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NimbleOperatorRule.swift; sourceTree = ""; }; DAD3BE491D6ECD9500660239 /* PrivateOutletRuleConfiguration.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PrivateOutletRuleConfiguration.swift; sourceTree = ""; }; E57B23C01B1D8BF000DEA512 /* ReturnArrowWhitespaceRule.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ReturnArrowWhitespaceRule.swift; sourceTree = ""; }; E5A167C81B25A0B000CF2D03 /* OperatorFunctionWhitespaceRule.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OperatorFunctionWhitespaceRule.swift; sourceTree = ""; }; @@ -644,6 +646,7 @@ 856651A61D6B395F005E6B29 /* MarkRule.swift */, E849FF271BF9481A009AE999 /* MissingDocsRule.swift */, E88DEA951B099CF200A66CB0 /* NestingRule.swift */, + D4DAE8BB1DE14E8F00B0AE7A /* NimbleOperatorRule.swift */, 692B1EB11BD7E00F00EAABFF /* OpeningBraceRule.swift */, E5A167C81B25A0B000CF2D03 /* OperatorFunctionWhitespaceRule.swift */, 78F032441D7C877800BE709A /* OverriddenSuperCallRule.swift */, @@ -971,6 +974,7 @@ E88198581BEA956C00333A11 /* FunctionBodyLengthRule.swift in Sources */, E88DEA751B09852000A66CB0 /* File+SwiftLint.swift in Sources */, 3BCC04D11C4F56D3006073C3 /* SeverityLevelsConfiguration.swift in Sources */, + D4DAE8BC1DE14E8F00B0AE7A /* NimbleOperatorRule.swift in Sources */, 6CB514E91C760C6900FA02C4 /* Structure+SwiftLint.swift in Sources */, E86396C51BADAC15002C9E88 /* XcodeReporter.swift in Sources */, 094385011D5D2894009168CF /* WeakDelegateRule.swift in Sources */, diff --git a/Tests/SwiftLintFramework/RulesTests.swift b/Tests/SwiftLintFramework/RulesTests.swift index d783199b7f..06d133a3be 100644 --- a/Tests/SwiftLintFramework/RulesTests.swift +++ b/Tests/SwiftLintFramework/RulesTests.swift @@ -181,6 +181,10 @@ class RulesTests: XCTestCase { verifyRule(NestingRule.description) } + func testNimbleOperator() { + verifyRule(NimbleOperatorRule.description) + } + func testVerticalWhitespace() { verifyRule(VerticalWhitespaceRule.description) }