mirror of
https://github.com/realm/SwiftLint.git
synced 2026-06-06 20:18:40 +00:00
b83e0991b9
The MIT license doesn't require that all files be prepended with this licensing or copyright information. Realm confirmed that they're ok with this change. This will enable some companies to contribute to SwiftLint and the date & authorship information will remain accessible via git source control.
149 lines
5.5 KiB
Swift
149 lines
5.5 KiB
Swift
import Foundation
|
|
import SourceKittenFramework
|
|
|
|
public struct ForWhereRule: ASTRule, ConfigurationProviderRule {
|
|
public var configuration = SeverityConfiguration(.warning)
|
|
|
|
public init() {}
|
|
|
|
public static let description = RuleDescription(
|
|
identifier: "for_where",
|
|
name: "For Where",
|
|
description: "`where` clauses are preferred over a single `if` inside a `for`.",
|
|
kind: .idiomatic,
|
|
nonTriggeringExamples: [
|
|
"for user in users where user.id == 1 { }\n",
|
|
// if let
|
|
"for user in users {\n" +
|
|
" if let id = user.id { }\n" +
|
|
"}\n",
|
|
// if var
|
|
"for user in users {\n" +
|
|
" if var id = user.id { }\n" +
|
|
"}\n",
|
|
// if with else
|
|
"for user in users {\n" +
|
|
" if user.id == 1 { } else { }\n" +
|
|
"}\n",
|
|
// if with else if
|
|
"for user in users {\n" +
|
|
" if user.id == 1 {\n" +
|
|
"} else if user.id == 2 { }\n" +
|
|
"}\n",
|
|
// if is not the only expression inside for
|
|
"for user in users {\n" +
|
|
" if user.id == 1 { }\n" +
|
|
" print(user)\n" +
|
|
"}\n",
|
|
// if a variable is used
|
|
"for user in users {\n" +
|
|
" let id = user.id\n" +
|
|
" if id == 1 { }\n" +
|
|
"}\n",
|
|
// if something is after if
|
|
"for user in users {\n" +
|
|
" if user.id == 1 { }\n" +
|
|
" return true\n" +
|
|
"}\n",
|
|
// condition with multiple clauses
|
|
"for user in users {\n" +
|
|
" if user.id == 1 && user.age > 18 { }\n" +
|
|
"}\n",
|
|
// if case
|
|
"for (index, value) in array.enumerated() {\n" +
|
|
" if case .valueB(_) = value {\n" +
|
|
" return index\n" +
|
|
" }\n" +
|
|
"}\n"
|
|
],
|
|
triggeringExamples: [
|
|
"for user in users {\n" +
|
|
" ↓if user.id == 1 { return true }\n" +
|
|
"}\n"
|
|
]
|
|
)
|
|
|
|
private static let commentKinds = SyntaxKind.commentAndStringKinds
|
|
|
|
public func validate(file: File, kind: StatementKind,
|
|
dictionary: [String: SourceKitRepresentable]) -> [StyleViolation] {
|
|
|
|
guard kind == .forEach,
|
|
let subDictionary = forBody(dictionary: dictionary),
|
|
subDictionary.substructure.count == 1,
|
|
let bodyDictionary = subDictionary.substructure.first,
|
|
bodyDictionary.kind.flatMap(StatementKind.init) == .if,
|
|
isOnlyOneIf(dictionary: bodyDictionary),
|
|
isOnlyIfInsideFor(forDictionary: subDictionary, ifDictionary: bodyDictionary, file: file),
|
|
!isComplexCondition(dictionary: bodyDictionary, file: file),
|
|
let offset = bodyDictionary .offset else {
|
|
return []
|
|
}
|
|
|
|
return [
|
|
StyleViolation(ruleDescription: type(of: self).description,
|
|
severity: configuration.severity,
|
|
location: Location(file: file, byteOffset: offset))
|
|
]
|
|
}
|
|
|
|
private func forBody(dictionary: [String: SourceKitRepresentable]) -> [String: SourceKitRepresentable]? {
|
|
return dictionary.substructure.first(where: { subDict -> Bool in
|
|
subDict.kind.flatMap(StatementKind.init) == .brace
|
|
})
|
|
}
|
|
|
|
private func isOnlyOneIf(dictionary: [String: SourceKitRepresentable]) -> Bool {
|
|
let substructure = dictionary.substructure
|
|
guard substructure.count == 1 else {
|
|
return false
|
|
}
|
|
|
|
return dictionary.substructure.first?.kind.flatMap(StatementKind.init) == .brace
|
|
}
|
|
|
|
private func isOnlyIfInsideFor(forDictionary: [String: SourceKitRepresentable],
|
|
ifDictionary: [String: SourceKitRepresentable],
|
|
file: File) -> Bool {
|
|
guard let offset = forDictionary.offset,
|
|
let length = forDictionary.length,
|
|
let ifOffset = ifDictionary.offset,
|
|
let ifLength = ifDictionary.length else {
|
|
return false
|
|
}
|
|
|
|
let beforeIfRange = NSRange(location: offset, length: ifOffset - offset)
|
|
let ifFinalPosition = ifOffset + ifLength
|
|
let afterIfRange = NSRange(location: ifFinalPosition, length: offset + length - ifFinalPosition)
|
|
let allKinds = file.syntaxMap.kinds(inByteRange: beforeIfRange) +
|
|
file.syntaxMap.kinds(inByteRange: afterIfRange)
|
|
|
|
let doesntContainComments = !allKinds.contains { kind in
|
|
!ForWhereRule.commentKinds.contains(kind)
|
|
}
|
|
|
|
return doesntContainComments
|
|
}
|
|
|
|
private func isComplexCondition(dictionary: [String: SourceKitRepresentable], file: File) -> Bool {
|
|
let kind = "source.lang.swift.structure.elem.condition_expr"
|
|
let contents = file.contents.bridge()
|
|
return !dictionary.elements.filter { element in
|
|
guard element.kind == kind,
|
|
let offset = element.offset,
|
|
let length = element.length,
|
|
let range = contents.byteRangeToNSRange(start: offset, length: length) else {
|
|
return false
|
|
}
|
|
|
|
let containsKeyword = !file.match(pattern: "\\blet|var|case\\b", with: [.keyword], range: range).isEmpty
|
|
if containsKeyword {
|
|
return true
|
|
}
|
|
|
|
return !file.match(pattern: "\\|\\||&&", with: [], range: range).isEmpty
|
|
}.isEmpty
|
|
}
|
|
|
|
}
|