Compare commits

...

2 Commits

Author SHA1 Message Date
Marcelo Fabri ecc30764d8 Remove unused declaration 2022-11-06 19:15:15 -08:00
Marcelo Fabri 4ec35845ad Rewrite `nimble_operator` rule with SwiftSyntax 2022-11-06 19:13:20 -08:00
2 changed files with 124 additions and 126 deletions

View File

@ -132,6 +132,7 @@
- `multiline_arguments_brackets`
- `multiline_parameters`
- `multiple_closures_with_trailing_closure`
- `nimble_operator`
- `no_extension_access_modifier`
- `no_fallthrough_only`
- `no_space_in_method_call`

View File

@ -1,7 +1,6 @@
import Foundation
import SourceKittenFramework
import SwiftSyntax
public struct NimbleOperatorRule: ConfigurationProviderRule, OptInRule, CorrectableRule {
public struct NimbleOperatorRule: ConfigurationProviderRule, SwiftSyntaxCorrectableRule, OptInRule {
public var configuration = SeverityConfiguration(.warning)
public init() {}
@ -53,7 +52,7 @@ public struct NimbleOperatorRule: ConfigurationProviderRule, OptInRule, Correcta
Example("↓expect(\"Hi!\").to(equal(\"Hi!\"))\n"): Example("expect(\"Hi!\") == \"Hi!\"\n"),
Example("↓expect(12).toNot(equal(10))\n"): Example("expect(12) != 10\n"),
Example("↓expect(value1).to(equal(value2))\n"): Example("expect(value1) == value2\n"),
Example("↓expect( value1 ).to(equal( value2.foo))\n"): Example("expect(value1) == value2.foo\n"),
Example("↓expect( value1 ).to(equal( value2.foo))\n"): Example("expect( value1 ) == value2.foo\n"),
Example("↓expect(value1).to(equal(10))\n"): Example("expect(value1) == 10\n"),
Example("↓expect(10).to(beGreaterThan(8))\n"): Example("expect(10) > 8\n"),
Example("↓expect(10).to(beGreaterThanOrEqualTo(10))\n"): Example("expect(10) >= 10\n"),
@ -70,149 +69,147 @@ public struct NimbleOperatorRule: ConfigurationProviderRule, OptInRule, Correcta
]
)
fileprivate typealias MatcherFunction = String
public func makeVisitor(file: SwiftLintFile) -> ViolationsSyntaxVisitor {
Visitor(viewMode: .sourceAccurate)
}
fileprivate enum Arity {
case nullary(analogueValue: String)
case withArguments
public func makeRewriter(file: SwiftLintFile) -> ViolationsSyntaxRewriter? {
Rewriter(
locationConverter: file.locationConverter,
disabledRegions: disabledRegions(file: file)
)
}
}
var hasArguments: Bool {
guard case .withArguments = self else {
return false
private extension NimbleOperatorRule {
final class Visitor: ViolationsSyntaxVisitor {
override func visitPost(_ node: FunctionCallExprSyntax) {
guard predicateDescription(for: node) != nil else {
return
}
return true
violations.append(node.positionAfterSkippingLeadingTrivia)
}
}
fileprivate typealias PredicateDescription = (to: String?, toNot: String?, arity: Arity)
final class Rewriter: SyntaxRewriter, ViolationsSyntaxRewriter {
private(set) var correctionPositions: [AbsolutePosition] = []
let locationConverter: SourceLocationConverter
let disabledRegions: [SourceRange]
private let predicatesMapping: [MatcherFunction: PredicateDescription] = [
init(locationConverter: SourceLocationConverter, disabledRegions: [SourceRange]) {
self.locationConverter = locationConverter
self.disabledRegions = disabledRegions
}
override func visit(_ node: FunctionCallExprSyntax) -> ExprSyntax {
guard let expectation = node.expectation(),
let predicate = predicatesMapping[expectation.operatorExpr.identifier.text],
let operatorExpr = expectation.operatorExpr(for: predicate),
let expectedValueExpr = expectation.expectedValueExpr(for: predicate),
!node.isContainedIn(regions: disabledRegions, locationConverter: locationConverter) else {
return super.visit(node)
}
correctionPositions.append(node.positionAfterSkippingLeadingTrivia)
let elements = ExprListSyntax([
expectation.baseExpr.withTrailingTrivia(.space),
operatorExpr.withTrailingTrivia(.space),
expectedValueExpr.withTrailingTrivia(node.trailingTrivia ?? .zero)
])
return super.visit(SequenceExprSyntax(elements: elements))
}
}
typealias MatcherFunction = String
static let predicatesMapping: [MatcherFunction: PredicateDescription] = [
"equal": (to: "==", toNot: "!=", .withArguments),
"beIdenticalTo": (to: "===", toNot: "!==", .withArguments),
"beGreaterThan": (to: ">", toNot: nil, .withArguments),
"beGreaterThanOrEqualTo": (to: ">=", toNot: nil, .withArguments),
"beLessThan": (to: "<", toNot: nil, .withArguments),
"beLessThanOrEqualTo": (to: "<=", toNot: nil, .withArguments),
"beTrue": (to: "==", toNot: "!=", .nullary(analogueValue: "true")),
"beFalse": (to: "==", toNot: "!=", .nullary(analogueValue: "false")),
"beNil": (to: "==", toNot: "!=", .nullary(analogueValue: "nil"))
"beTrue": (to: "==", toNot: "!=", .nullary(analogueValue: BooleanLiteralExprSyntax(booleanLiteral: true))),
"beFalse": (to: "==", toNot: "!=", .nullary(analogueValue: BooleanLiteralExprSyntax(booleanLiteral: false))),
"beNil": (to: "==", toNot: "!=", .nullary(analogueValue: NilLiteralExprSyntax(nilKeyword: .nilKeyword())))
]
public func validate(file: SwiftLintFile) -> [StyleViolation] {
let matches = violationMatchesRanges(in: file)
return matches.map {
StyleViolation(ruleDescription: Self.description,
severity: configuration.severity,
location: Location(file: file, characterOffset: $0.location))
}
}
private func violationMatchesRanges(in file: SwiftLintFile) -> [NSRange] {
let contents = file.stringView
return rawRegexResults(in: file).filter { range in
guard let byteRange = contents.NSRangeToByteRange(start: range.location, length: range.length) else {
return false
}
let containsCall = file.structureDictionary.structures(forByteOffset: byteRange.upperBound - 1)
.contains(where: { dict -> Bool in
return dict.expressionKind == .call && (dict.name ?? "").starts(with: "expect")
})
return containsCall
}
}
private func rawRegexResults(in file: SwiftLintFile) -> [NSRange] {
let operandPattern = "(.(?!expect\\())+?"
let operatorsPattern = "(" + predicatesMapping.map { name, predicateDescription in
let argumentsPattern = predicateDescription.arity.hasArguments
? operandPattern
: ""
return "\(name)\\(\(argumentsPattern)\\)"
}.joined(separator: "|") + ")"
let pattern = "expect\\(\(operandPattern)\\)\\.to(Not)?\\(\(operatorsPattern)\\)"
let excludingKinds = SyntaxKind.commentKinds
return file.match(pattern: pattern)
.filter { _, kinds in
excludingKinds.isDisjoint(with: kinds) && kinds.first == .identifier
}
.map { $0.0 }
}
public func correct(file: SwiftLintFile) -> [Correction] {
let matches = violationMatchesRanges(in: file)
.filter { file.ruleEnabled(violatingRanges: [$0], for: self).isNotEmpty }
guard matches.isNotEmpty else { return [] }
let description = Self.description
var corrections: [Correction] = []
var contents = file.contents
for range in matches.sorted(by: { $0.location > $1.location }) {
for (functionName, operatorCorrections) in predicatesMapping {
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
}
static func predicateDescription(for node: FunctionCallExprSyntax) -> PredicateDescription? {
guard let expectation = node.expectation() else {
return nil
}
file.write(contents)
return corrections
return Self.predicatesMapping[expectation.operatorExpr.identifier.text]
}
}
private extension String {
/// Returns corrected string if the correction is possible, otherwise returns nil.
///
/// - parameter name: The function name to replace.
/// - parameter predicateDescription: The Nimble operators to replace functions with.
/// - parameter range: The range in which replacements should be applied.
///
/// - returns: The corrected string if the correction is possible, otherwise returns nil.
func replace(function name: NimbleOperatorRule.MatcherFunction,
with predicateDescription: NimbleOperatorRule.PredicateDescription,
in range: NSRange) -> String? {
let anything = "\\s*(.*?)\\s*"
let toPattern = ("expect\\(\(anything)\\)\\.to\\(\(name)\\(\(anything)\\)\\)", predicateDescription.to)
let toNotPattern = ("expect\\(\(anything)\\)\\.toNot\\(\(name)\\(\(anything)\\)\\)", predicateDescription.toNot)
for case let (pattern, operatorString?) in [toPattern, toNotPattern] {
let expression = regex(pattern)
guard expression.matches(in: self, options: [], range: range).isNotEmpty else {
continue
}
let valueReplacementPattern: String
switch predicateDescription.arity {
case .nullary(let analogueValue):
valueReplacementPattern = analogueValue
case .withArguments:
valueReplacementPattern = "$2"
}
let replacementPattern = "expect($1) \(operatorString) \(valueReplacementPattern)"
return expression.stringByReplacingMatches(in: self,
options: [],
range: range,
withTemplate: replacementPattern)
private extension FunctionCallExprSyntax {
func expectation() -> Expectation? {
guard trailingClosure == nil,
argumentList.count == 1,
let memberExpr = calledExpression.as(MemberAccessExprSyntax.self),
let kind = Expectation.Kind(rawValue: memberExpr.name.text),
let baseExpr = memberExpr.base?.as(FunctionCallExprSyntax.self),
baseExpr.calledExpression.as(IdentifierExprSyntax.self)?.identifier.text == "expect",
let predicateExpr = argumentList.first?.expression.as(FunctionCallExprSyntax.self),
let operatorExpr = predicateExpr.calledExpression.as(IdentifierExprSyntax.self) else {
return nil
}
return nil
let expected = predicateExpr.argumentList.first?.expression
return Expectation(kind: kind, baseExpr: baseExpr, operatorExpr: operatorExpr, expected: expected)
}
}
private typealias PredicateDescription = (to: String, toNot: String?, arity: Arity)
private enum Arity {
case nullary(analogueValue: ExprSyntaxProtocol)
case withArguments
}
private struct Expectation {
let kind: Kind
let baseExpr: FunctionCallExprSyntax
let operatorExpr: IdentifierExprSyntax
let expected: ExprSyntax?
enum Kind {
case positive
case negative
init?(rawValue: String) {
switch rawValue {
case "to":
self = .positive
case "toNot", "notTo":
self = .negative
default:
return nil
}
}
}
func expectedValueExpr(for predicate: PredicateDescription) -> ExprSyntaxProtocol? {
switch predicate.arity {
case .withArguments:
return expected
case .nullary(let analogueValue):
return analogueValue
}
}
func operatorExpr(for predicate: PredicateDescription) -> BinaryOperatorExprSyntax? {
let operatorStr: String? = {
switch kind {
case .negative:
return predicate.toNot
case .positive:
return predicate.to
}
}()
return operatorStr.map(BinaryOperatorExprSyntax.init(text:))
}
}