mirror of
https://github.com/realm/SwiftLint.git
synced 2026-06-06 20:18:40 +00:00
* [bazel] Remove custom SwiftSyntax BUILD file Something similar to this has been merged upstream instead now. This also renames the repo name to SwiftSyntax in preparation for it being in the BCR * [SwiftSyntax] Update to latest 509.0.0 tag https://github.com/apple/swift-syntax/releases/tag/509.0.0-swift-DEVELOPMENT-SNAPSHOT-2023-04-25-b
217 lines
9.1 KiB
Swift
217 lines
9.1 KiB
Swift
import SwiftSyntax
|
|
|
|
struct TrailingCommaRule: SwiftSyntaxCorrectableRule, ConfigurationProviderRule {
|
|
var configuration = TrailingCommaConfiguration()
|
|
|
|
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: node.count - 1,
|
|
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: node.count - 1,
|
|
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: node.count - 1,
|
|
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: node.count - 1,
|
|
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
|
|
}
|
|
}
|