Files
2025-10-08 08:12:05 +02:00

183 lines
7.4 KiB
Swift

import Foundation
import SwiftSyntax
@SwiftSyntaxRule(explicitRewriter: true)
struct TrailingCommaRule: Rule {
var configuration = TrailingCommaConfiguration()
private static let triggeringExamples: [Example] = [
Example("let foo = [1, 2, 3↓,]"),
Example("let foo = [1, 2, 3↓, ]"),
Example("let foo = [1, 2, 3 ↓,]"),
Example("let foo = [1: 2, 2: 3↓, ]"),
Example("struct Bar {\n let foo = [1: 2, 2: 3↓, ]\n}"),
Example("let foo = [1, 2, 3↓,] + [4, 5, 6↓,]"),
Example("let example = [ 1,\n2↓,\n // 3,\n]"),
Example("let foo = [\"אבג\", \"αβγ\", \"🇺🇸\"↓,]"),
Example("class C {\n #if true\n func f() {\n let foo = [1, 2, 3↓,]\n }\n #endif\n}"),
Example("foo([1: \"\\(error)\"↓,])"),
]
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]"),
Example("let foo = []"),
Example("let foo = [:]"),
Example("let foo = [1: 2, 2: 3]"),
Example("let foo = [Void]()"),
Example("let example = [ 1,\n 2\n // 3,\n]"),
Example("foo([1: \"\\(error)\"])"),
Example("let foo = [Int]()"),
],
triggeringExamples: Self.triggeringExamples,
corrections: Self.corrections
)
}
private extension TrailingCommaRule {
final class Visitor: ViolationsSyntaxVisitor<ConfigurationType> {
override func visitPost(_ node: DictionaryElementListSyntax) {
guard let lastElement = node.last else {
return
}
switch (lastElement.trailingComma, configuration.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, configuration.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 = configuration.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: ViolationsSyntaxRewriter<ConfigurationType> {
override func visit(_ node: DictionaryElementListSyntax) -> DictionaryElementListSyntax {
guard let lastElement = node.last, let index = node.index(of: lastElement) else {
return super.visit(node)
}
switch (lastElement.trailingComma, configuration.mandatoryComma) {
case (let commaToken?, false):
numberOfCorrections += 1
let newTrailingTrivia = (lastElement.value.trailingTrivia)
.appending(trivia: commaToken.leadingTrivia)
.appending(trivia: commaToken.trailingTrivia)
let newNode = node
.with(
\.[index],
lastElement
.with(\.trailingComma, nil)
.with(\.trailingTrivia, newTrailingTrivia)
)
return super.visit(newNode)
case (nil, true) where !locationConverter.isSingleLine(node: node):
numberOfCorrections += 1
let newNode = node
.with(
\.[index],
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, let index = node.index(of: lastElement) else {
return super.visit(node)
}
switch (lastElement.trailingComma, configuration.mandatoryComma) {
case (let commaToken?, false):
numberOfCorrections += 1
let newNode = node
.with(
\.[index],
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):
numberOfCorrections += 1
let newNode = node
.with(
\.[index],
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: some 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
}
}