mirror of
https://github.com/realm/SwiftLint.git
synced 2026-06-06 20:18:40 +00:00
96 lines
3.9 KiB
Swift
96 lines
3.9 KiB
Swift
//
|
|
// FirstWhereRule.swift
|
|
// SwiftLint
|
|
//
|
|
// Created by Marcelo Fabri on 12/20/16.
|
|
// Copyright © 2016 Realm. All rights reserved.
|
|
//
|
|
|
|
import Foundation
|
|
import SourceKittenFramework
|
|
|
|
public struct FirstWhereRule: OptInRule, ConfigurationProviderRule {
|
|
public var configuration = SeverityConfiguration(.warning)
|
|
|
|
public init() {}
|
|
|
|
public static let description = RuleDescription(
|
|
identifier: "first_where",
|
|
name: "First Where",
|
|
description: "Prefer using `.first(where:)` over `.filter { }.first` in collections.",
|
|
nonTriggeringExamples: [
|
|
"kinds.filter(excludingKinds.contains).isEmpty && kinds.first == .identifier\n",
|
|
"myList.first(where: { $0 % 2 == 0 })\n",
|
|
"matchPattern(pattern).filter { $0.first == .identifier }\n"
|
|
],
|
|
triggeringExamples: [
|
|
"↓myList.filter { $0 % 2 == 0 }.first\n",
|
|
"↓myList.filter({ $0 % 2 == 0 }).first\n",
|
|
"↓myList.map { $0 + 1 }.filter({ $0 % 2 == 0 }).first\n",
|
|
"↓myList.map { $0 + 1 }.filter({ $0 % 2 == 0 }).first?.something()\n",
|
|
"↓myList.filter(someFunction).first\n"
|
|
]
|
|
)
|
|
|
|
public func validateFile(_ file: File) -> [StyleViolation] {
|
|
let pattern = "[\\s\\}\\)]*\\.first"
|
|
let firstRanges = file.matchPattern(pattern, withSyntaxKinds: [.identifier])
|
|
let contents = file.contents.bridge()
|
|
let structure = file.structure
|
|
|
|
let violatingLocations: [Int] = firstRanges.flatMap {
|
|
guard let bodyByteRange = contents.NSRangeToByteRange(start: $0.location,
|
|
length: $0.length),
|
|
case let firstLocation = $0.location + $0.length - 1,
|
|
let firstByteRange = contents.NSRangeToByteRange(start: firstLocation,
|
|
length: 1) else {
|
|
return nil
|
|
}
|
|
|
|
return methodCallFor(bodyByteRange.location - 1,
|
|
excludingOffset: firstByteRange.location,
|
|
dictionary: structure.dictionary, predicate: { dictionary in
|
|
guard let name = dictionary["key.name"] as? String else {
|
|
return false
|
|
}
|
|
|
|
return name.hasSuffix(".filter")
|
|
})
|
|
}
|
|
|
|
return violatingLocations.map {
|
|
StyleViolation(ruleDescription: type(of: self).description,
|
|
severity: configuration.severity,
|
|
location: Location(file: file, byteOffset: $0))
|
|
}
|
|
}
|
|
|
|
private func methodCallFor(_ byteOffset: Int,
|
|
excludingOffset: Int,
|
|
dictionary: [String: SourceKitRepresentable],
|
|
predicate: ([String: SourceKitRepresentable]) -> Bool) -> Int? {
|
|
|
|
if let kindString = (dictionary["key.kind"] as? String),
|
|
SwiftExpressionKind(rawValue: kindString) == .call,
|
|
let bodyOffset = (dictionary["key.bodyoffset"] as? Int64).flatMap({ Int($0) }),
|
|
let bodyLength = (dictionary["key.bodylength"] as? Int64).flatMap({ Int($0) }),
|
|
let offset = (dictionary["key.offset"] as? Int64).flatMap({ Int($0) }) {
|
|
let byteRange = NSRange(location: bodyOffset, length: bodyLength)
|
|
|
|
if NSLocationInRange(byteOffset, byteRange) &&
|
|
!NSLocationInRange(excludingOffset, byteRange) && predicate(dictionary) {
|
|
return offset
|
|
}
|
|
}
|
|
|
|
for dictionary in dictionary.substructure {
|
|
if let offset = methodCallFor(byteOffset, excludingOffset: excludingOffset,
|
|
dictionary: dictionary, predicate: predicate) {
|
|
return offset
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
}
|