Files
SwiftLint/Source/SwiftLintFramework/Rules/ForWhereRule.swift
T
JP Simard b83e0991b9 Remove all file headers
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.
2018-05-04 13:42:02 -07:00

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
}
}