SwiftLint/Source/SwiftLintFramework/Rules/Style/LiteralExpressionEndIdentat...

272 lines
8.3 KiB
Swift

import Foundation
import SourceKittenFramework
struct LiteralExpressionEndIdentationRule: Rule, ConfigurationProviderRule, OptInRule {
var configuration = SeverityConfiguration(.warning)
init() {}
static let description = RuleDescription(
identifier: "literal_expression_end_indentation",
name: "Literal Expression End Indentation",
description: "Array and dictionary literal end should have the same indentation as the line that started it",
kind: .style,
nonTriggeringExamples: [
Example("""
[1, 2, 3]
"""),
Example("""
[1,
2
]
"""),
Example("""
[
1,
2
]
"""),
Example("""
[
1,
2]
"""),
Example("""
let x = [
1,
2
]
"""),
Example("""
[key: 2, key2: 3]
"""),
Example("""
[key: 1,
key2: 2
]
"""),
Example("""
[
key: 0,
key2: 20
]
""")
],
triggeringExamples: [
Example("""
let x = [
1,
2
↓]
"""),
Example("""
let x = [
1,
2
↓]
"""),
Example("""
let x = [
key: value
↓]
""")
],
corrections: [
Example("""
let x = [
key: value
↓ ]
"""): Example("""
let x = [
key: value
]
"""),
Example("""
let x = [
1,
2
↓]
"""): Example("""
let x = [
1,
2
]
"""),
Example("""
let x = [
1,
2
↓ ]
"""): Example("""
let x = [
1,
2
]
"""),
Example("""
let x = [
1,
2
↓ ] + [
3,
4
↓ ]
"""): Example("""
let x = [
1,
2
] + [
3,
4
]
""")
]
)
func validate(file: SwiftLintFile) -> [StyleViolation] {
return violations(in: file).map { violation in
return styleViolation(for: violation, in: file)
}
}
private func styleViolation(for violation: Violation, in file: SwiftLintFile) -> StyleViolation {
let reason = "\(Self.description.description); " +
"expected indentation of \(violation.indentationRanges.expected.length), " +
"got \(violation.indentationRanges.actual.length)"
return StyleViolation(ruleDescription: Self.description,
severity: configuration.severity,
location: Location(file: file, byteOffset: violation.endOffset),
reason: reason)
}
fileprivate static let notWhitespace = regex("[^\\s]")
}
extension LiteralExpressionEndIdentationRule: CorrectableRule {
func correct(file: SwiftLintFile) -> [Correction] {
let allViolations = violations(in: file).reversed().filter { violation in
guard let nsRange = file.stringView.byteRangeToNSRange(violation.range) else {
return false
}
return file.ruleEnabled(violatingRanges: [nsRange], for: self).isNotEmpty
}
guard allViolations.isNotEmpty else {
return []
}
var correctedContents = file.contents
var correctedLocations: [Int] = []
let actualLookup = actualViolationLookup(for: allViolations)
for violation in allViolations {
let expected = actualLookup(violation).indentationRanges.expected
let actual = violation.indentationRanges.actual
if correct(contents: &correctedContents, expected: expected, actual: actual) {
correctedLocations.append(actual.location)
}
}
var corrections = correctedLocations.map {
return Correction(ruleDescription: Self.description,
location: Location(file: file, characterOffset: $0))
}
file.write(correctedContents)
// Re-correct to catch cascading indentation from the first round.
corrections += correct(file: file)
return corrections
}
private func correct(contents: inout String, expected: NSRange, actual: NSRange) -> Bool {
guard let actualIndices = contents.nsrangeToIndexRange(actual) else {
return false
}
let correction = contents.substring(from: expected.location, length: expected.length)
contents = contents.replacingCharacters(in: actualIndices, with: correction)
return true
}
private func actualViolationLookup(for violations: [Violation]) -> (Violation) -> Violation {
let lookup = violations.reduce(into: [NSRange: Violation](), { result, violation in
result[violation.indentationRanges.actual] = violation
})
func actualViolation(for violation: Violation) -> Violation {
guard let actual = lookup[violation.indentationRanges.expected] else { return violation }
return actualViolation(for: actual)
}
return actualViolation
}
}
extension LiteralExpressionEndIdentationRule {
fileprivate struct Violation {
var indentationRanges: (expected: NSRange, actual: NSRange)
var endOffset: ByteCount
var range: ByteRange
}
fileprivate func violations(in file: SwiftLintFile) -> [Violation] {
return file.structureDictionary.traverseDepthFirst { subDict in
guard let kind = subDict.expressionKind else { return nil }
guard let violation = violation(in: file, of: kind, dictionary: subDict) else { return nil }
return [violation]
}
}
private func violation(in file: SwiftLintFile, of kind: SwiftExpressionKind,
dictionary: SourceKittenDictionary) -> Violation? {
guard kind == .dictionary || kind == .array else {
return nil
}
let elements = dictionary.elements.filter { $0.kind == "source.lang.swift.structure.elem.expr" }
let contents = file.stringView
guard elements.isNotEmpty,
let offset = dictionary.offset,
let length = dictionary.length,
let (startLine, _) = contents.lineAndCharacter(forByteOffset: offset),
let firstParamOffset = elements[0].offset,
let (firstParamLine, _) = contents.lineAndCharacter(forByteOffset: firstParamOffset),
startLine != firstParamLine,
let lastParamOffset = elements.last?.offset,
let (lastParamLine, _) = contents.lineAndCharacter(forByteOffset: lastParamOffset),
case let endOffset = offset + length - 1,
let (endLine, endPosition) = contents.lineAndCharacter(forByteOffset: endOffset),
lastParamLine != endLine
else {
return nil
}
let range = file.lines[startLine - 1].range
let regex = Self.notWhitespace
let actual = endPosition - 1
guard let match = regex.firstMatch(in: file.contents, options: [], range: range)?.range,
case let expected = match.location - range.location,
expected != actual
else {
return nil
}
var expectedRange = range
expectedRange.length = expected
var actualRange = file.lines[endLine - 1].range
actualRange.length = actual
return Violation(indentationRanges: (expected: expectedRange, actual: actualRange),
endOffset: endOffset,
range: ByteRange(location: offset, length: length))
}
}