SwiftLint/Source/SwiftLintFramework/Rules/Style/TrailingCommaRule.swift

219 lines
9.1 KiB
Swift

import SwiftSyntax
struct TrailingCommaRule: SwiftSyntaxCorrectableRule, ConfigurationProviderRule {
var configuration = TrailingCommaConfiguration()
init() {}
private static let triggeringExamples: [Example] = [
Example("let foo = [1, 2, 3↓,]\n"),
Example("let foo = [1, 2, 3↓, ]\n"),
Example("let foo = [1, 2, 3 ↓,]\n"),
Example("let foo = [1: 2, 2: 3↓, ]\n"),
Example("struct Bar {\n let foo = [1: 2, 2: 3↓, ]\n}\n"),
Example("let foo = [1, 2, 3↓,] + [4, 5, 6↓,]\n"),
Example("let example = [ 1,\n2↓,\n // 3,\n]"),
Example("let foo = [\"אבג\", \"αβγ\", \"🇺🇸\"↓,]\n"),
Example("class C {\n #if true\n func f() {\n let foo = [1, 2, 3↓,]\n }\n #endif\n}"),
Example("foo([1: \"\\(error)\"↓,])\n")
]
private static let corrections: [Example: Example] = {
let fixed = triggeringExamples.map { example -> Example in
let fixedString = example.code.replacingOccurrences(of: "↓,", with: "")
return example.with(code: fixedString)
}
var result: [Example: Example] = [:]
for (triggering, correction) in zip(triggeringExamples, fixed) {
result[triggering] = correction
}
return result
}()
static let description = RuleDescription(
identifier: "trailing_comma",
name: "Trailing Comma",
description: "Trailing commas in arrays and dictionaries should be avoided/enforced.",
kind: .style,
nonTriggeringExamples: [
Example("let foo = [1, 2, 3]\n"),
Example("let foo = []\n"),
Example("let foo = [:]\n"),
Example("let foo = [1: 2, 2: 3]\n"),
Example("let foo = [Void]()\n"),
Example("let example = [ 1,\n 2\n // 3,\n]"),
Example("foo([1: \"\\(error)\"])\n"),
Example("let foo = [Int]()\n")
],
triggeringExamples: Self.triggeringExamples,
corrections: Self.corrections
)
func makeVisitor(file: SwiftLintFile) -> ViolationsSyntaxVisitor {
Visitor(
mandatoryComma: configuration.mandatoryComma,
locationConverter: file.locationConverter
)
}
func makeRewriter(file: SwiftLintFile) -> ViolationsSyntaxRewriter? {
Rewriter(
mandatoryComma: configuration.mandatoryComma,
locationConverter: file.locationConverter,
disabledRegions: disabledRegions(file: file)
)
}
}
private extension TrailingCommaRule {
final class Visitor: ViolationsSyntaxVisitor {
private let mandatoryComma: Bool
private let locationConverter: SourceLocationConverter
init(mandatoryComma: Bool, locationConverter: SourceLocationConverter) {
self.mandatoryComma = mandatoryComma
self.locationConverter = locationConverter
super.init(viewMode: .sourceAccurate)
}
override func visitPost(_ node: DictionaryElementListSyntax) {
guard let lastElement = node.last else {
return
}
switch (lastElement.trailingComma, mandatoryComma) {
case (let commaToken?, false):
violations.append(violation(for: commaToken.positionAfterSkippingLeadingTrivia))
case (nil, true) where !locationConverter.isSingleLine(node: node):
violations.append(violation(for: lastElement.endPositionBeforeTrailingTrivia))
case (_, true), (nil, false):
break
}
}
override func visitPost(_ node: ArrayElementListSyntax) {
guard let lastElement = node.last else {
return
}
switch (lastElement.trailingComma, mandatoryComma) {
case (let commaToken?, false):
violations.append(violation(for: commaToken.positionAfterSkippingLeadingTrivia))
case (nil, true) where !locationConverter.isSingleLine(node: node):
violations.append(violation(for: lastElement.endPositionBeforeTrailingTrivia))
case (_, true), (nil, false):
break
}
}
private func violation(for position: AbsolutePosition) -> ReasonedRuleViolation {
let reason = mandatoryComma
? "Multi-line collection literals should have trailing commas"
: "Collection literals should not have trailing commas"
return ReasonedRuleViolation(position: position, reason: reason)
}
}
final class Rewriter: SyntaxRewriter, ViolationsSyntaxRewriter {
private(set) var correctionPositions: [AbsolutePosition] = []
private let mandatoryComma: Bool
private let locationConverter: SourceLocationConverter
private let disabledRegions: [SourceRange]
init(mandatoryComma: Bool, locationConverter: SourceLocationConverter, disabledRegions: [SourceRange]) {
self.mandatoryComma = mandatoryComma
self.locationConverter = locationConverter
self.disabledRegions = disabledRegions
}
override func visit(_ node: DictionaryElementListSyntax) -> DictionaryElementListSyntax {
guard let lastElement = node.last,
!lastElement.isContainedIn(regions: disabledRegions, locationConverter: locationConverter) else {
return super.visit(node)
}
switch (lastElement.trailingComma, mandatoryComma) {
case (let commaToken?, false):
correctionPositions.append(commaToken.positionAfterSkippingLeadingTrivia)
let newTrailingTrivia = (lastElement.valueExpression.trailingTrivia)
.appending(trivia: commaToken.leadingTrivia)
.appending(trivia: commaToken.trailingTrivia)
let newNode = node
.replacing(
childAt: lastElement.indexInParent,
with: lastElement
.with(\.trailingComma, nil)
.with(\.trailingTrivia, newTrailingTrivia)
)
return super.visit(newNode)
case (nil, true) where !locationConverter.isSingleLine(node: node):
correctionPositions.append(lastElement.endPositionBeforeTrailingTrivia)
let newNode = node
.replacing(
childAt: lastElement.indexInParent,
with: lastElement
.with(\.trailingTrivia, [])
.with(\.trailingComma, .commaToken())
.with(\.trailingTrivia, lastElement.trailingTrivia)
)
return super.visit(newNode)
case (_, true), (nil, false):
return super.visit(node)
}
}
override func visit(_ node: ArrayElementListSyntax) -> ArrayElementListSyntax {
guard let lastElement = node.last,
!lastElement.isContainedIn(regions: disabledRegions, locationConverter: locationConverter) else {
return super.visit(node)
}
switch (lastElement.trailingComma, mandatoryComma) {
case (let commaToken?, false):
correctionPositions.append(commaToken.positionAfterSkippingLeadingTrivia)
let newNode = node
.replacing(
childAt: lastElement.indexInParent,
with: lastElement
.with(\.trailingComma, nil)
.with(\.trailingTrivia,
(lastElement.expression.trailingTrivia)
.appending(trivia: commaToken.leadingTrivia)
.appending(trivia: commaToken.trailingTrivia)
)
)
return super.visit(newNode)
case (nil, true) where !locationConverter.isSingleLine(node: node):
correctionPositions.append(lastElement.endPositionBeforeTrailingTrivia)
let newNode = node.replacing(
childAt: lastElement.indexInParent,
with: lastElement
.with(\.expression, lastElement.expression.with(\.trailingTrivia, []))
.with(\.trailingComma, .commaToken())
.with(\.trailingTrivia, lastElement.expression.trailingTrivia)
)
return super.visit(newNode)
case (_, true), (nil, false):
return super.visit(node)
}
}
}
}
private extension SourceLocationConverter {
func isSingleLine(node: SyntaxProtocol) -> Bool {
location(for: node.positionAfterSkippingLeadingTrivia).line ==
location(for: node.endPositionBeforeTrailingTrivia).line
}
}
private extension Trivia {
func appending(trivia: Trivia) -> Trivia {
var result = self
for piece in trivia.pieces {
result = result.appending(piece)
}
return result
}
}