mirror of
https://github.com/realm/SwiftLint.git
synced 2026-05-07 20:12:49 +00:00
156 lines
6.5 KiB
Swift
156 lines
6.5 KiB
Swift
import SwiftSyntax
|
|
|
|
@SwiftSyntaxRule(correctable: true)
|
|
struct FunctionNameWhitespaceRule: Rule {
|
|
var configuration = FunctionNameWhitespaceConfiguration()
|
|
|
|
static let description = RuleDescription(
|
|
identifier: "function_name_whitespace",
|
|
name: "Function Name Whitespace",
|
|
description: "There should be consistent whitespace before and after function names and generic parameters.",
|
|
kind: .style,
|
|
nonTriggeringExamples: FunctionNameWhitespaceRuleExamples.nonTriggeringExamples,
|
|
triggeringExamples: FunctionNameWhitespaceRuleExamples.triggeringExamples,
|
|
corrections: FunctionNameWhitespaceRuleExamples.corrections,
|
|
deprecatedAliases: ["operator_whitespace"]
|
|
)
|
|
}
|
|
|
|
private extension FunctionNameWhitespaceRule {
|
|
final class Visitor: ViolationsSyntaxVisitor<ConfigurationType> {
|
|
override func visitPost(_ node: FunctionDeclSyntax) {
|
|
validateFuncKeywordSpacing(for: node)
|
|
correctSingleCommentTrivia(
|
|
after: node.name,
|
|
reason: configuration.genericSpacing.beforeGenericViolationReason
|
|
)
|
|
validateFunctionNameTrailingTrivia(node: node)
|
|
if let genericParameterClause = node.genericParameterClause {
|
|
correctSingleCommentTrivia(
|
|
after: genericParameterClause,
|
|
reason: configuration.genericSpacing.afterGenericViolationReason
|
|
)
|
|
validateGenericTrailingTrivia(node: genericParameterClause)
|
|
}
|
|
}
|
|
|
|
private func validateFunctionNameTrailingTrivia(node: FunctionDeclSyntax) {
|
|
let nameTrailingTrivia = node.name.trailingTrivia
|
|
let replacement: String? =
|
|
if node.isOperatorDeclaration {
|
|
nameTrailingTrivia.isNotSingleSpaceWithoutComments ? " " : nil
|
|
} else {
|
|
switch configuration.genericSpacing {
|
|
case .noSpace where nameTrailingTrivia.isNotEmptyWithoutComments: ""
|
|
case .leadingSpace where nameTrailingTrivia.isNotSingleSpaceWithoutComments: " "
|
|
case .trailingSpace where nameTrailingTrivia.isNotEmptyWithoutComments: ""
|
|
case .leadingTrailingSpace where nameTrailingTrivia.isNotSingleSpaceWithoutComments: " "
|
|
default: nil
|
|
}
|
|
}
|
|
|
|
guard let replacement else { return }
|
|
violations.append(
|
|
.init(
|
|
position: node.name.endPositionBeforeTrailingTrivia,
|
|
reason: node.isOperatorDeclaration
|
|
? "Operators should be surrounded by a single whitespace when defining them"
|
|
: configuration.genericSpacing.beforeGenericViolationReason,
|
|
correction: .init(
|
|
start: node.name.endPositionBeforeTrailingTrivia,
|
|
end: node.name.endPosition,
|
|
replacement: replacement
|
|
)
|
|
)
|
|
)
|
|
}
|
|
|
|
private func validateFuncKeywordSpacing(for node: FunctionDeclSyntax) {
|
|
guard node.funcKeyword.trailingTrivia.isNotSingleSpaceWithoutComments else { return }
|
|
violations.append(
|
|
.init(
|
|
position: node.funcKeyword.endPositionBeforeTrailingTrivia,
|
|
reason: node.isOperatorDeclaration
|
|
? "Operators should be surrounded by a single whitespace when defining them"
|
|
: "Too many spaces between 'func' and function name",
|
|
correction: .init(
|
|
start: node.funcKeyword.endPositionBeforeTrailingTrivia,
|
|
end: node.name.positionAfterSkippingLeadingTrivia,
|
|
replacement: " "
|
|
)
|
|
)
|
|
)
|
|
}
|
|
|
|
private func validateGenericTrailingTrivia(node: GenericParameterClauseSyntax) {
|
|
let genericTrailingTrivia = node.rightAngle.trailingTrivia
|
|
let replacement: String? = switch configuration.genericSpacing {
|
|
case .noSpace where genericTrailingTrivia.isNotEmptyWithoutComments: ""
|
|
case .leadingSpace where genericTrailingTrivia.isNotEmptyWithoutComments: ""
|
|
case .trailingSpace where genericTrailingTrivia.isNotSingleSpaceWithoutComments: " "
|
|
case .leadingTrailingSpace where genericTrailingTrivia.isNotSingleSpaceWithoutComments: " "
|
|
default: nil
|
|
}
|
|
guard let replacement else { return }
|
|
violations.append(
|
|
.init(
|
|
position: node.endPositionBeforeTrailingTrivia,
|
|
reason: configuration.genericSpacing.afterGenericViolationReason,
|
|
correction: .init(
|
|
start: node.endPositionBeforeTrailingTrivia,
|
|
end: node.endPosition,
|
|
replacement: replacement
|
|
)
|
|
)
|
|
)
|
|
}
|
|
|
|
private func correctSingleCommentTrivia(after node: some SyntaxProtocol, reason: String) {
|
|
let trivia = node.trailingTrivia
|
|
guard trivia.containsComments else { return }
|
|
guard let comment = trivia.singleComment else { return }
|
|
let expectedTrivia = Trivia.surroundedBySpaces(comment: comment)
|
|
guard trivia != expectedTrivia else { return }
|
|
|
|
violations.append(
|
|
.init(
|
|
position: node.endPositionBeforeTrailingTrivia,
|
|
reason: reason,
|
|
correction: .init(
|
|
start: node.endPositionBeforeTrailingTrivia,
|
|
end: node.endPosition,
|
|
replacement: " \(comment) "
|
|
)
|
|
)
|
|
)
|
|
}
|
|
}
|
|
}
|
|
|
|
private extension FunctionDeclSyntax {
|
|
var isOperatorDeclaration: Bool {
|
|
switch name.tokenKind {
|
|
case .binaryOperator: true
|
|
default: false
|
|
}
|
|
}
|
|
}
|
|
|
|
private extension Trivia {
|
|
var singleComment: TriviaPiece? {
|
|
filter(\.isComment).onlyElement
|
|
}
|
|
|
|
static func surroundedBySpaces(comment: TriviaPiece) -> Trivia {
|
|
Trivia(pieces: [.spaces(1), comment, .spaces(1)])
|
|
}
|
|
|
|
var isNotEmptyWithoutComments: Bool {
|
|
isNotEmpty && !containsComments
|
|
}
|
|
|
|
var isNotSingleSpaceWithoutComments: Bool {
|
|
!isSingleSpace && !containsComments
|
|
}
|
|
}
|