mirror of
https://github.com/realm/SwiftLint.git
synced 2026-06-06 20:18:40 +00:00
a6c4fccb3f
Co-authored-by: itsybitsybootsy <lammlu19> Co-authored-by: Danny Mösch <danny.moesch@icloud.com> Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
116 lines
5.0 KiB
Swift
116 lines
5.0 KiB
Swift
import Foundation
|
|
import SourceKittenFramework
|
|
import SwiftLintCore
|
|
import SwiftSyntax
|
|
|
|
@SwiftSyntaxRule(correctable: true, optIn: true)
|
|
struct ContrastedOpeningBraceRule: Rule {
|
|
var configuration = SeverityConfiguration<Self>(.warning)
|
|
|
|
static let description = RuleDescription(
|
|
identifier: "contrasted_opening_brace",
|
|
name: "Contrasted Opening Brace",
|
|
description: """
|
|
The correct positioning of braces that introduce a block of code or member list is highly controversial. \
|
|
No matter which style is preferred, consistency is key. Apart from different tastes, \
|
|
the positioning of braces can also have a significant impact on the readability of the code, \
|
|
especially for visually impaired developers. This rule ensures that braces are on a separate line \
|
|
after the declaration to contrast the code block from the rest of the declaration. Comments between the \
|
|
declaration and the opening brace are respected. Check out the `opening_brace` rule for a different style.
|
|
""",
|
|
kind: .style,
|
|
nonTriggeringExamples: ContrastedOpeningBraceRuleExamples.nonTriggeringExamples,
|
|
triggeringExamples: ContrastedOpeningBraceRuleExamples.triggeringExamples,
|
|
corrections: ContrastedOpeningBraceRuleExamples.corrections
|
|
)
|
|
}
|
|
|
|
private extension ContrastedOpeningBraceRule {
|
|
final class Visitor: CodeBlockVisitor<ConfigurationType> {
|
|
override func collectViolations(for bracedItem: (some BracedSyntax)?) {
|
|
if let bracedItem, let correction = violationCorrection(bracedItem) {
|
|
violations.append(
|
|
ReasonedRuleViolation(
|
|
position: bracedItem.openingPosition,
|
|
reason: "Opening brace should be on a separate line",
|
|
correction: correction
|
|
)
|
|
)
|
|
}
|
|
}
|
|
|
|
private func violationCorrection(_ node: some BracedSyntax) -> ReasonedRuleViolation.ViolationCorrection? {
|
|
let leftBrace = node.leftBrace
|
|
guard let previousToken = leftBrace.previousToken(viewMode: .sourceAccurate) else {
|
|
return nil
|
|
}
|
|
let openingPosition = node.openingPosition
|
|
let triviaBetween = previousToken.trailingTrivia + leftBrace.leadingTrivia
|
|
let previousLocation = previousToken.endLocation(converter: locationConverter)
|
|
let leftBraceLocation = leftBrace.startLocation(converter: locationConverter)
|
|
let parentStartColumn = node
|
|
.indentationDecidingParent?
|
|
.startLocation(converter: locationConverter)
|
|
.column ?? 1
|
|
if previousLocation.line + 1 == leftBraceLocation.line, leftBraceLocation.column == parentStartColumn {
|
|
return nil
|
|
}
|
|
let comment = triviaBetween.description.trimmingTrailingCharacters(in: .whitespacesAndNewlines)
|
|
return .init(
|
|
start: previousToken.endPositionBeforeTrailingTrivia + SourceLength(of: comment),
|
|
end: openingPosition,
|
|
replacement: "\n" + String(repeating: " ", count: parentStartColumn - 1)
|
|
)
|
|
}
|
|
}
|
|
}
|
|
|
|
private extension BracedSyntax {
|
|
var openingPosition: AbsolutePosition {
|
|
leftBrace.positionAfterSkippingLeadingTrivia
|
|
}
|
|
|
|
var indentationDecidingParent: (any SyntaxProtocol)? {
|
|
if let catchClause = parent?.as(CatchClauseSyntax.self) {
|
|
return catchClause.parent?.as(CatchClauseListSyntax.self)?.parent?.as(DoStmtSyntax.self)
|
|
}
|
|
if let ifExpr = parent?.as(IfExprSyntax.self) {
|
|
return ifExpr.indentationDecidingParent
|
|
}
|
|
if let binding = parent?.as(PatternBindingSyntax.self) {
|
|
return binding.parent?.as(PatternBindingListSyntax.self)?.parent?.as(VariableDeclSyntax.self)
|
|
}
|
|
if let closure = `as`(ClosureExprSyntax.self),
|
|
closure.keyPathInParent == \FunctionCallExprSyntax.trailingClosure {
|
|
return closure.leftBrace.previousIndentationDecidingToken
|
|
}
|
|
if let closureLabel = parent?.as(MultipleTrailingClosureElementSyntax.self)?.label {
|
|
return closureLabel.previousIndentationDecidingToken
|
|
}
|
|
return parent
|
|
}
|
|
}
|
|
|
|
private extension TokenSyntax {
|
|
var previousIndentationDecidingToken: TokenSyntax {
|
|
var indentationDecidingToken = self
|
|
repeat {
|
|
if let previousToken = indentationDecidingToken.previousToken(viewMode: .sourceAccurate) {
|
|
indentationDecidingToken = previousToken
|
|
} else {
|
|
break
|
|
}
|
|
} while !indentationDecidingToken.leadingTrivia.containsNewlines()
|
|
return indentationDecidingToken
|
|
}
|
|
}
|
|
|
|
private extension IfExprSyntax {
|
|
var indentationDecidingParent: any SyntaxProtocol {
|
|
if keyPathInParent == \Self.elseBody, let parent = parent?.as(IfExprSyntax.self) {
|
|
return parent.indentationDecidingParent
|
|
}
|
|
return self
|
|
}
|
|
}
|