Files
SwiftLint/Source/SwiftLintBuiltInRules/Rules/Style/OpeningBraceRule.swift
2025-10-08 08:12:05 +02:00

212 lines
8.1 KiB
Swift

import Foundation
import SourceKittenFramework
import SwiftLintCore
import SwiftSyntax
@SwiftSyntaxRule(correctable: true)
struct OpeningBraceRule: Rule {
var configuration = OpeningBraceConfiguration()
static let description = RuleDescription(
identifier: "opening_brace",
name: "Opening Brace Spacing",
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 preceded \
by a single space and on the same line as the declaration. Comments between the declaration and the \
opening brace are respected. Check out the `contrasted_opening_brace` rule for a different style.
""",
kind: .style,
nonTriggeringExamples: OpeningBraceRuleExamples.nonTriggeringExamples,
triggeringExamples: OpeningBraceRuleExamples.triggeringExamples,
corrections: OpeningBraceRuleExamples.corrections
)
}
private extension OpeningBraceRule {
final class Visitor: CodeBlockVisitor<ConfigurationType> {
// MARK: - Type Declarations
override func visitPost(_ node: ActorDeclSyntax) {
if configuration.ignoreMultilineTypeHeaders,
hasMultilinePredecessors(node.memberBlock, keyword: node.actorKeyword) {
return
}
super.visitPost(node)
}
override func visitPost(_ node: ClassDeclSyntax) {
if configuration.ignoreMultilineTypeHeaders,
hasMultilinePredecessors(node.memberBlock, keyword: node.classKeyword) {
return
}
super.visitPost(node)
}
override func visitPost(_ node: EnumDeclSyntax) {
if configuration.ignoreMultilineTypeHeaders,
hasMultilinePredecessors(node.memberBlock, keyword: node.enumKeyword) {
return
}
super.visitPost(node)
}
override func visitPost(_ node: ExtensionDeclSyntax) {
if configuration.ignoreMultilineTypeHeaders,
hasMultilinePredecessors(node.memberBlock, keyword: node.extensionKeyword) {
return
}
super.visitPost(node)
}
override func visitPost(_ node: ProtocolDeclSyntax) {
if configuration.ignoreMultilineTypeHeaders,
hasMultilinePredecessors(node.memberBlock, keyword: node.protocolKeyword) {
return
}
super.visitPost(node)
}
override func visitPost(_ node: StructDeclSyntax) {
if configuration.ignoreMultilineTypeHeaders,
hasMultilinePredecessors(node.memberBlock, keyword: node.structKeyword) {
return
}
super.visitPost(node)
}
// MARK: - Conditional Statements
override func visitPost(_ node: ForStmtSyntax) {
if configuration.ignoreMultilineStatementConditions,
hasMultilinePredecessors(node.body, keyword: node.forKeyword) {
return
}
super.visitPost(node)
}
override func visitPost(_ node: IfExprSyntax) {
if configuration.ignoreMultilineStatementConditions,
hasMultilinePredecessors(node.body, keyword: node.ifKeyword) {
return
}
super.visitPost(node)
}
override func visitPost(_ node: WhileStmtSyntax) {
if configuration.ignoreMultilineStatementConditions,
hasMultilinePredecessors(node.body, keyword: node.whileKeyword) {
return
}
super.visitPost(node)
}
// MARK: - Functions and Initializers
override func visitPost(_ node: FunctionDeclSyntax) {
if let body = node.body,
configuration.shouldIgnoreMultilineFunctionSignatures,
hasMultilinePredecessors(body, keyword: node.funcKeyword) {
return
}
super.visitPost(node)
}
override func visitPost(_ node: InitializerDeclSyntax) {
if let body = node.body,
configuration.shouldIgnoreMultilineFunctionSignatures,
hasMultilinePredecessors(body, keyword: node.initKeyword) {
return
}
super.visitPost(node)
}
// MARK: - Other Methods
/// Checks if a `BracedSyntax` has a multiline predecessor.
/// For type declarations, the predecessor is the header. For conditional statements,
/// it is the condition list, and for functions, it is the signature.
private func hasMultilinePredecessors(_ body: some BracedSyntax, keyword: TokenSyntax) -> Bool {
guard let endToken = body.previousToken(viewMode: .sourceAccurate) else {
return false
}
let startLocation = keyword.endLocation(converter: locationConverter)
let endLocation = endToken.endLocation(converter: locationConverter)
let braceLocation = body.leftBrace.endLocation(converter: locationConverter)
return startLocation.line != endLocation.line && endLocation.line != braceLocation.line
}
override func collectViolations(for bracedItem: (some BracedSyntax)?) {
if let bracedItem, let correction = violationCorrection(bracedItem) {
violations.append(
ReasonedRuleViolation(
position: bracedItem.openingPosition,
reason: """
Opening braces should be preceded by a single space and on the same line \
as the declaration
""",
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)
if previousLocation.line != leftBraceLocation.line {
let trailingCommentText = previousToken.trailingTrivia.description.trimmingCharacters(in: .whitespaces)
return .init(
start: previousToken.endPositionBeforeTrailingTrivia,
end: openingPosition.advanced(by: trailingCommentText.isNotEmpty ? 1 : 0),
replacement: trailingCommentText.isNotEmpty ? " { \(trailingCommentText)" : " "
)
}
if previousLocation.column + 1 == leftBraceLocation.column {
return nil
}
if triviaBetween.containsComments {
if triviaBetween.pieces.last == .spaces(1) {
return nil
}
let comment = triviaBetween.description.trimmingTrailingCharacters(in: .whitespaces)
return .init(
start: previousToken.endPositionBeforeTrailingTrivia + SourceLength(of: comment),
end: openingPosition,
replacement: " "
)
}
return .init(
start: previousToken.endPositionBeforeTrailingTrivia,
end: openingPosition,
replacement: " "
)
}
}
}
private extension BracedSyntax {
var openingPosition: AbsolutePosition {
leftBrace.positionAfterSkippingLeadingTrivia
}
}