Files
SwiftLint/Source/SwiftLintFramework/Rules/ArrayInitRule.swift
T
Marcelo Fabri 8f05f92cd1 Add array_init opt-in rule
Fixes #1271
2017-09-16 02:32:48 -03:00

135 lines
4.8 KiB
Swift

//
// ArrayInitRule.swift
// SwiftLint
//
// Created by Marcelo Fabri on 09/16/17.
// Copyright © 2017 Realm. All rights reserved.
//
import Foundation
import SourceKittenFramework
public struct ArrayInitRule: ASTRule, ConfigurationProviderRule, OptInRule {
public var configuration = SeverityConfiguration(.warning)
public init() {}
public static let description = RuleDescription(
identifier: "array_init",
name: "Array Init",
description: "Prefer using Array(seq) than seq.map { $0 } to convert a sequence into an Array.",
kind: .lint,
nonTriggeringExamples: [
"Array(foo)\n",
"foo.map { $0.0 }\n",
"foo.map { $1 }\n",
"let set = Set(array)\n"
],
triggeringExamples: [
"↓foo.map({ $0 })\n",
"↓foo.map { $0 } \n",
"↓foo.map { return $0 } \n",
"↓foo.map { elem in\n" +
" elem\n" +
"}\n",
"↓foo.map { elem in\n" +
" return elem\n" +
"}\n",
"↓foo.map { (elem: String) in\n" +
" elem\n" +
"}\n",
"↓foo.map { elem -> String in\n" +
" elem\n" +
"}\n"
]
)
public func validate(file: File, kind: SwiftExpressionKind,
dictionary: [String: SourceKitRepresentable]) -> [StyleViolation] {
guard kind == .call, let name = dictionary.name, name.hasSuffix(".map"),
let bodyOffset = dictionary.bodyOffset,
let bodyLength = dictionary.bodyLength,
let offset = dictionary.offset else {
return []
}
let range = NSRange(location: bodyOffset, length: bodyLength)
let tokens = file.syntaxMap.tokens(inByteRange: range).filter { token in
guard let kind = SyntaxKind(rawValue: token.type) else {
return false
}
return !SyntaxKind.commentKinds().contains(kind)
}
guard isShortParameterStyleViolation(file: file, tokens: tokens) ||
isParameterStyleViolation(file: file, dictionary: dictionary, tokens: tokens) else {
return []
}
return [
StyleViolation(ruleDescription: type(of: self).description,
severity: configuration.severity,
location: Location(file: file, byteOffset: offset))
]
}
private func isShortParameterStyleViolation(file: File, tokens: [SyntaxToken]) -> Bool {
let kinds = tokens.flatMap { SyntaxKind(rawValue: $0.type) }
switch kinds {
case [.identifier]:
let identifier = file.contents(for: tokens[0])
return identifier == "$0"
case [.keyword, .identifier]:
let keyword = file.contents(for: tokens[0])
let identifier = file.contents(for: tokens[1])
return keyword == "return" && identifier == "$0"
default:
return false
}
}
private func isParameterStyleViolation(file: File, dictionary: [String: SourceKitRepresentable],
tokens: [SyntaxToken]) -> Bool {
let parameters = dictionary.enclosedVarParameters
guard parameters.count == 1,
let offset = parameters[0].offset,
let length = parameters[0].length,
let parameterName = parameters[0].name else {
return false
}
let parameterEnd = offset + length
let tokens = Array(tokens.filter { $0.offset >= parameterEnd }.drop { token in
let isKeyword = SyntaxKind(rawValue: token.type) == .keyword
return !isKeyword || file.contents(for: token) != "in"
})
let kinds = tokens.flatMap { SyntaxKind(rawValue: $0.type) }
switch kinds {
case [.keyword, .identifier]:
let keyword = file.contents(for: tokens[0])
let identifier = file.contents(for: tokens[1])
return keyword == "in" && identifier == parameterName
case [.keyword, .keyword, .identifier]:
let firstKeyword = file.contents(for: tokens[0])
let secondKeyword = file.contents(for: tokens[1])
let identifier = file.contents(for: tokens[2])
return firstKeyword == "in" && secondKeyword == "return" && identifier == parameterName
default:
return false
}
}
}
private func ~= (array: [SyntaxKind], value: [SyntaxKind]) -> Bool {
return array == value
}
private extension File {
func contents(for token: SyntaxToken) -> String? {
return contents.bridge().substringWithByteRange(start: token.offset,
length: token.length)
}
}