forked from realm/SwiftLint
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathNimbleOperatorRule.swift
More file actions
157 lines (134 loc) · 6.54 KB
/
NimbleOperatorRule.swift
File metadata and controls
157 lines (134 loc) · 6.54 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
//
// NimbleOperatorRule.swift
// SwiftLint
//
// Created by Marcelo Fabri on 20/11/16.
// Copyright © 2016 Realm. All rights reserved.
//
import Foundation
import SourceKittenFramework
public struct NimbleOperatorRule: ConfigurationProviderRule, OptInRule, CorrectableRule {
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"
],
corrections: [
"↓expect(seagull.squawk).toNot(equal(\"Hi\"))\n": "expect(seagull.squawk) != \"Hi\"\n",
"↓expect(\"Hi!\").to(equal(\"Hi!\"))\n": "expect(\"Hi!\") == \"Hi!\"\n",
"↓expect(12).toNot(equal(10))\n": "expect(12) != 10\n",
"↓expect(value1).to(equal(value2))\n": "expect(value1) == value2\n",
"↓expect( value1 ).to(equal( value2.foo))\n": "expect(value1) == value2.foo\n",
"↓expect(value1).to(equal(10))\n": "expect(value1) == 10\n",
"↓expect(10).to(beGreaterThan(8))\n": "expect(10) > 8\n",
"↓expect(10).to(beGreaterThanOrEqualTo(10))\n": "expect(10) >= 10\n",
"↓expect(10).to(beLessThan(11))\n": "expect(10) < 11\n",
"↓expect(10).to(beLessThanOrEqualTo(10))\n": "expect(10) <= 10\n",
"↓expect(x).to(beIdenticalTo(x))\n": "expect(x) === x\n",
"expect(10) > 2\n ↓expect(10).to(beGreaterThan(2))\n": "expect(10) > 2\n expect(10) > 2\n"
]
)
fileprivate typealias Operators = (to: String?, toNot: String?)
fileprivate typealias MatcherFunction = String
fileprivate let operatorsMapping: [MatcherFunction: Operators] = [
"equal": (to: "==", toNot: "!="),
"beIdenticalTo": (to: "===", toNot: "!=="),
"beGreaterThan": (to: ">", toNot: nil),
"beGreaterThanOrEqualTo": (to: ">=", toNot: nil),
"beLessThan": (to: "<", toNot: nil),
"beLessThanOrEqualTo": (to: "<=", toNot: nil)
]
public func validate(file: File) -> [StyleViolation] {
let matches = violationMatchesRanges(in: file)
return matches.map {
StyleViolation(ruleDescription: type(of: self).description,
severity: configuration.severity,
location: Location(file: file, characterOffset: $0.location))
}
}
private func violationMatchesRanges(in file: File) -> [NSRange] {
let operatorNames = operatorsMapping.keys
let operatorsPattern = "(" + operatorNames.joined(separator: "|") + ")"
let variablePattern = "(.(?!expect\\())+?"
let pattern = "expect\\(\(variablePattern)\\)\\.to(Not)?\\(\(operatorsPattern)\\(\(variablePattern)\\)\\)"
let excludingKinds = SyntaxKind.commentKinds()
return file.match(pattern: pattern)
.filter { _, kinds in
kinds.filter(excludingKinds.contains).isEmpty && kinds.first == .identifier
}.map { $0.0 }
}
public func correct(file: File) -> [Correction] {
let matches = violationMatchesRanges(in: file)
.filter { !file.ruleEnabled(violatingRanges: [$0], for: self).isEmpty }
guard !matches.isEmpty else { return [] }
let description = type(of: self).description
var corrections: [Correction] = []
var contents = file.contents
for range in matches.sorted(by: { $0.location > $1.location }) {
for (functionName, operatorCorrections) in operatorsMapping {
guard let correctedString = contents.replace(function: functionName,
with: operatorCorrections,
in: range)
else {
continue
}
contents = correctedString
let correction = Correction(ruleDescription: description,
location: Location(file: file, characterOffset: range.location))
corrections.insert(correction, at: 0)
break
}
}
file.write(contents)
return corrections
}
}
extension String {
/// Returns corrected string if the correction is possible, otherwise returns nil.
fileprivate func replace(function name: NimbleOperatorRule.MatcherFunction,
with operators: NimbleOperatorRule.Operators,
in range: NSRange) -> String? {
let anything = "\\s*(.*?)\\s*"
let toPattern = ("expect\\(\(anything)\\)\\.to\\(\(name)\\(\(anything)\\)\\)", operators.to)
let toNotPattern = ("expect\\(\(anything)\\)\\.toNot\\(\(name)\\(\(anything)\\)\\)", operators.toNot)
var correctedString: String?
for (pattern, operatorString) in [toPattern, toNotPattern] {
guard let operatorString = operatorString else {
continue
}
let expression = regex(pattern)
if !expression.matches(in: self, options: [], range: range).isEmpty {
correctedString = expression.stringByReplacingMatches(in: self,
options: [],
range: range,
withTemplate: "expect($1) \(operatorString) $2")
break
}
}
return correctedString
}
}