Files
SwiftLint/Source/SwiftLintFramework/Rules/Lint/ArrayInitRule.swift
T
JP Simard ea311bab23 [UnusedDeclarationRule] Speed up and detect more dead code (#3340)
By using SourceKit's `index` request to index the entire source file,
we can avoid having to make `cursor-info` requests for every candidate
token in the file, which scales linearly with the number of candiate
tokens.

For the Yams project, this approach improved the total SwiftLint run
time by 4.6x: 7.9 down from 36.8s.

The SourceKit index response doesn't have everything we need to identify
declarations, so we still need to make some `cursor-info` requests,
mostly to detect overrides: protocol conformances and parent class
overrides.

This approach ends up finding more unused declarations because the index
contains more declared USRs than can be found by calling `cursor-info`
on candidate tokens in a file.

---

Remove unused declaration in ArrayInitRule

---

Update package versions
2020-09-15 10:33:27 -07:00

211 lines
8.5 KiB
Swift

import Foundation
import SourceKittenFramework
public struct ArrayInitRule: ASTRule, ConfigurationProviderRule, OptInRule, AutomaticTestableRule {
public var configuration = SeverityConfiguration(.warning)
public init() {}
public static let description = RuleDescription(
identifier: "array_init",
name: "Array Init",
description: "Prefer using `Array(seq)` over `seq.map { $0 }` to convert a sequence into an Array.",
kind: .lint,
nonTriggeringExamples: [
Example("Array(foo)\n"),
Example("foo.map { $0.0 }\n"),
Example("foo.map { $1 }\n"),
Example("foo.map { $0() }\n"),
Example("foo.map { ((), $0) }\n"),
Example("foo.map { $0! }\n"),
Example("foo.map { $0! /* force unwrap */ }\n"),
Example("foo.something { RouteMapper.map($0) }\n"),
Example("foo.map { !$0 }\n"),
Example("foo.map { /* a comment */ !$0 }\n")
],
triggeringExamples: [
Example("↓foo.map({ $0 })\n"),
Example("↓foo.map { $0 }\n"),
Example("↓foo.map { return $0 }\n"),
Example("↓foo.map { elem in\n" +
" elem\n" +
"}\n"),
Example("↓foo.map { elem in\n" +
" return elem\n" +
"}\n"),
Example("↓foo.map { (elem: String) in\n" +
" elem\n" +
"}\n"),
Example("↓foo.map { elem -> String in\n" +
" elem\n" +
"}\n"),
Example("↓foo.map { $0 /* a comment */ }\n"),
Example("↓foo.map { /* a comment */ $0 }\n")
]
)
public func validate(file: SwiftLintFile, kind: SwiftExpressionKind,
dictionary: SourceKittenDictionary) -> [StyleViolation] {
guard kind == .call, let name = dictionary.name, name.hasSuffix(".map"),
let bodyOffset = dictionary.bodyOffset,
let bodyLength = dictionary.bodyLength,
let bodyRange = dictionary.bodyByteRange,
let nameOffset = dictionary.nameOffset,
let nameLength = dictionary.nameLength,
let offset = dictionary.offset else {
return []
}
let tokens = file.syntaxMap.tokens(inByteRange: bodyRange).filter { token in
guard let kind = token.kind else {
return false
}
return !SyntaxKind.commentKinds.contains(kind)
}
guard let firstToken = tokens.first,
case let nameEndPosition = nameOffset + nameLength,
isClosureParameter(firstToken: firstToken, nameEndPosition: nameEndPosition, file: file),
isShortParameterStyleViolation(file: file, tokens: tokens) ||
isParameterStyleViolation(file: file, dictionary: dictionary, tokens: tokens),
let lastToken = tokens.last,
case let bodyEndPosition = bodyOffset + bodyLength,
!containsTrailingContent(lastToken: lastToken, bodyEndPosition: bodyEndPosition, file: file),
!containsLeadingContent(tokens: tokens, bodyStartPosition: bodyOffset, file: file) else {
return []
}
return [
StyleViolation(ruleDescription: Self.description,
severity: configuration.severity,
location: Location(file: file, byteOffset: offset))
]
}
private func isClosureParameter(firstToken: SwiftLintSyntaxToken,
nameEndPosition: ByteCount,
file: SwiftLintFile) -> Bool {
let length = firstToken.offset - nameEndPosition
guard length > 0,
case let contents = file.stringView,
case let byteRange = ByteRange(location: nameEndPosition, length: length),
let nsRange = contents.byteRangeToNSRange(byteRange)
else {
return false
}
let pattern = regex("\\A\\s*\\(?\\s*\\{")
return pattern.firstMatch(in: file.contents, options: .anchored, range: nsRange) != nil
}
private func containsTrailingContent(lastToken: SwiftLintSyntaxToken,
bodyEndPosition: ByteCount,
file: SwiftLintFile) -> Bool {
let lastTokenEnd = lastToken.offset + lastToken.length
let remainingLength = bodyEndPosition - lastTokenEnd
let remainingRange = ByteRange(location: lastTokenEnd, length: remainingLength)
return containsContent(inByteRange: remainingRange, file: file)
}
private func containsLeadingContent(tokens: [SwiftLintSyntaxToken],
bodyStartPosition: ByteCount,
file: SwiftLintFile) -> Bool {
let inTokenPosition = tokens.firstIndex(where: { token in
token.kind == .keyword && file.contents(for: token) == "in"
})
let firstToken: SwiftLintSyntaxToken
let start: ByteCount
if let position = inTokenPosition {
let index = tokens.index(after: position)
firstToken = tokens[index]
let inToken = tokens[position]
start = inToken.offset + inToken.length
} else {
firstToken = tokens[0]
start = bodyStartPosition
}
let length = firstToken.offset - start
let remainingRange = ByteRange(location: start, length: length)
return containsContent(inByteRange: remainingRange, file: file)
}
private func containsContent(inByteRange byteRange: ByteRange, file: SwiftLintFile) -> Bool {
let stringView = file.stringView
let remainingTokens = file.syntaxMap.tokens(inByteRange: byteRange)
guard let nsRange = stringView.byteRangeToNSRange(byteRange) else {
return false
}
let ranges = NSMutableIndexSet(indexesIn: nsRange)
for tokenNSRange in remainingTokens.compactMap({ stringView.byteRangeToNSRange($0.range) }) {
ranges.remove(in: tokenNSRange)
}
var containsContent = false
ranges.enumerateRanges(options: []) { range, stop in
let substring = stringView.substring(with: range)
let processedSubstring = substring
.trimmingCharacters(in: CharacterSet(charactersIn: "{}"))
.trimmingCharacters(in: .whitespacesAndNewlines)
if !processedSubstring.isEmpty {
stop.pointee = true
containsContent = true
}
}
return containsContent
}
private func isShortParameterStyleViolation(file: SwiftLintFile, tokens: [SwiftLintSyntaxToken]) -> Bool {
let kinds = tokens.kinds
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: SwiftLintFile, dictionary: SourceKittenDictionary,
tokens: [SwiftLintSyntaxToken]) -> 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 = token.kind == .keyword
return !isKeyword || file.contents(for: token) != "in"
})
let kinds = tokens.kinds
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
}
}
}