mirror of
https://github.com/realm/SwiftLint.git
synced 2026-05-07 20:12:49 +00:00
248 lines
9.2 KiB
Swift
248 lines
9.2 KiB
Swift
import SwiftSyntax
|
|
|
|
@SwiftSyntaxRule(optIn: true)
|
|
struct AttributesRule: Rule {
|
|
var configuration = AttributesConfiguration()
|
|
|
|
static let description = RuleDescription(
|
|
identifier: "attributes",
|
|
name: "Attributes",
|
|
description: """
|
|
Attributes should be on their own lines in functions and types, but on the same line as variables and \
|
|
imports
|
|
""",
|
|
rationale: """
|
|
Erica Sadun says:
|
|
|
|
> My take on things after the poll and after talking directly with a number of \
|
|
developers is this: Placing attributes like `@objc`, `@testable`, `@available`, `@discardableResult` on \
|
|
their own lines before a member declaration has become a conventional Swift style.
|
|
|
|
> This approach limits declaration length. It allows a member to float below its attribute and supports \
|
|
flush-left access modifiers, so `internal`, `public`, etc appear in the leftmost column. Many developers \
|
|
mix-and-match styles for short Swift attributes like `@objc`
|
|
|
|
See https://ericasadun.com/2016/10/02/quick-style-survey/ for discussion.
|
|
|
|
SwiftLint's rule requires attributes to be on their own lines for functions and types, but on the same line \
|
|
for variables and imports.
|
|
""",
|
|
kind: .style,
|
|
nonTriggeringExamples: AttributesRuleExamples.nonTriggeringExamples,
|
|
triggeringExamples: AttributesRuleExamples.triggeringExamples
|
|
)
|
|
}
|
|
|
|
private extension AttributesRule {
|
|
final class Visitor: ViolationsSyntaxVisitor<ConfigurationType> {
|
|
override func visitPost(_ node: AttributeListSyntax) {
|
|
guard let helper = node.makeHelper(locationConverter: locationConverter) else {
|
|
return
|
|
}
|
|
|
|
let attributesAndPlacements = node.attributesAndPlacements(
|
|
configuration: configuration,
|
|
shouldBeOnSameLine: helper.shouldBeOnSameLine
|
|
)
|
|
|
|
let hasViolation = helper.hasViolation(
|
|
locationConverter: locationConverter,
|
|
attributesAndPlacements: attributesAndPlacements,
|
|
attributesWithArgumentsAlwaysOnNewLine: configuration.attributesWithArgumentsAlwaysOnNewLine
|
|
)
|
|
|
|
switch hasViolation {
|
|
case .argumentsAlwaysOnNewLineViolation:
|
|
let reason = """
|
|
Attributes with arguments or inside always_on_line_above must be on a new line \
|
|
instead of the same line
|
|
"""
|
|
|
|
violations.append(
|
|
ReasonedRuleViolation(
|
|
position: helper.violationPosition,
|
|
reason: reason,
|
|
severity: configuration.severityConfiguration.severity
|
|
)
|
|
)
|
|
return
|
|
case .violation:
|
|
violations.append(helper.violationPosition)
|
|
return
|
|
case .noViolation:
|
|
break
|
|
}
|
|
|
|
let linesForAttributes = attributesAndPlacements
|
|
.filter { $1 == .dedicatedLine }
|
|
.map { $0.0.endLine(locationConverter: locationConverter) }
|
|
|
|
if linesForAttributes.isEmpty {
|
|
return
|
|
}
|
|
if !linesForAttributes.contains(helper.keywordLine - 1) {
|
|
violations.append(helper.violationPosition)
|
|
return
|
|
}
|
|
|
|
let hasMultipleNewlines = node.children(viewMode: .sourceAccurate).enumerated().contains { index, element in
|
|
if index > 0, element.leadingTrivia.hasMultipleNewlines == true {
|
|
return true
|
|
}
|
|
return element.trailingTrivia.hasMultipleNewlines == true
|
|
}
|
|
|
|
if hasMultipleNewlines {
|
|
violations.append(helper.violationPosition)
|
|
return
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
private extension SyntaxProtocol {
|
|
func startLine(locationConverter: SourceLocationConverter) -> Int? {
|
|
locationConverter.location(for: positionAfterSkippingLeadingTrivia).line
|
|
}
|
|
|
|
func endLine(locationConverter: SourceLocationConverter) -> Int? {
|
|
locationConverter.location(for: endPositionBeforeTrailingTrivia).line
|
|
}
|
|
}
|
|
|
|
private extension Trivia {
|
|
var hasMultipleNewlines: Bool {
|
|
reduce(0, { $0 + $1.numberOfNewlines }) > 1
|
|
}
|
|
}
|
|
|
|
private extension TriviaPiece {
|
|
var numberOfNewlines: Int {
|
|
if case .newlines(let numberOfNewlines) = self {
|
|
return numberOfNewlines
|
|
}
|
|
return 0
|
|
}
|
|
}
|
|
|
|
private enum AttributePlacement {
|
|
case sameLineAsDeclaration
|
|
case dedicatedLine
|
|
}
|
|
|
|
private enum Violation {
|
|
case argumentsAlwaysOnNewLineViolation
|
|
case noViolation
|
|
case violation
|
|
}
|
|
|
|
private struct RuleHelper {
|
|
let violationPosition: AbsolutePosition
|
|
let keywordLine: Int
|
|
let shouldBeOnSameLine: Bool
|
|
|
|
func hasViolation(
|
|
locationConverter: SourceLocationConverter,
|
|
attributesAndPlacements: [(AttributeSyntax, AttributePlacement)],
|
|
attributesWithArgumentsAlwaysOnNewLine: Bool
|
|
) -> (Violation) {
|
|
var linesWithAttributes: Set<Int> = [keywordLine]
|
|
for (attribute, placement) in attributesAndPlacements {
|
|
guard let attributeStartLine = attribute.startLine(locationConverter: locationConverter) else {
|
|
continue
|
|
}
|
|
|
|
switch placement {
|
|
case .sameLineAsDeclaration:
|
|
if attributeStartLine != keywordLine {
|
|
return .violation
|
|
}
|
|
case .dedicatedLine:
|
|
let hasViolation = attributeStartLine == keywordLine ||
|
|
linesWithAttributes.contains(attributeStartLine)
|
|
linesWithAttributes.insert(attributeStartLine)
|
|
if hasViolation {
|
|
if attributesWithArgumentsAlwaysOnNewLine, shouldBeOnSameLine {
|
|
return .argumentsAlwaysOnNewLineViolation
|
|
}
|
|
return .violation
|
|
}
|
|
}
|
|
}
|
|
return .noViolation
|
|
}
|
|
}
|
|
|
|
private extension AttributeListSyntax {
|
|
func attributesAndPlacements(configuration: AttributesConfiguration, shouldBeOnSameLine: Bool)
|
|
-> [(AttributeSyntax, AttributePlacement)] {
|
|
self
|
|
.children(viewMode: .sourceAccurate)
|
|
.compactMap { $0.as(AttributeSyntax.self) }
|
|
.map { attribute in
|
|
let atPrefixedName = "@\(attribute.attributeNameText)"
|
|
if configuration.alwaysOnSameLine.contains(atPrefixedName) {
|
|
return (attribute, .sameLineAsDeclaration)
|
|
}
|
|
if configuration.alwaysOnNewLine.contains(atPrefixedName) {
|
|
return (attribute, .dedicatedLine)
|
|
}
|
|
if attribute.arguments != nil, configuration.attributesWithArgumentsAlwaysOnNewLine {
|
|
return (attribute, .dedicatedLine)
|
|
}
|
|
|
|
return shouldBeOnSameLine ? (attribute, .sameLineAsDeclaration) : (attribute, .dedicatedLine)
|
|
}
|
|
}
|
|
|
|
// swiftlint:disable:next cyclomatic_complexity
|
|
func makeHelper(locationConverter: SourceLocationConverter) -> RuleHelper? {
|
|
guard let parent else {
|
|
return nil
|
|
}
|
|
|
|
let keyword: any SyntaxProtocol
|
|
let shouldBeOnSameLine: Bool
|
|
if let funcKeyword = parent.as(FunctionDeclSyntax.self)?.funcKeyword {
|
|
keyword = funcKeyword
|
|
shouldBeOnSameLine = false
|
|
} else if let initKeyword = parent.as(InitializerDeclSyntax.self)?.initKeyword {
|
|
keyword = initKeyword
|
|
shouldBeOnSameLine = false
|
|
} else if let enumKeyword = parent.as(EnumDeclSyntax.self)?.enumKeyword {
|
|
keyword = enumKeyword
|
|
shouldBeOnSameLine = false
|
|
} else if let structKeyword = parent.as(StructDeclSyntax.self)?.structKeyword {
|
|
keyword = structKeyword
|
|
shouldBeOnSameLine = false
|
|
} else if let classKeyword = parent.as(ClassDeclSyntax.self)?.classKeyword {
|
|
keyword = classKeyword
|
|
shouldBeOnSameLine = false
|
|
} else if let extensionKeyword = parent.as(ExtensionDeclSyntax.self)?.extensionKeyword {
|
|
keyword = extensionKeyword
|
|
shouldBeOnSameLine = false
|
|
} else if let protocolKeyword = parent.as(ProtocolDeclSyntax.self)?.protocolKeyword {
|
|
keyword = protocolKeyword
|
|
shouldBeOnSameLine = false
|
|
} else if let importTok = parent.as(ImportDeclSyntax.self)?.importKeyword {
|
|
keyword = importTok
|
|
shouldBeOnSameLine = true
|
|
} else if let letOrVarKeyword = parent.as(VariableDeclSyntax.self)?.bindingSpecifier {
|
|
keyword = letOrVarKeyword
|
|
shouldBeOnSameLine = true
|
|
} else {
|
|
return nil
|
|
}
|
|
|
|
guard let keywordLine = keyword.startLine(locationConverter: locationConverter) else {
|
|
return nil
|
|
}
|
|
|
|
return RuleHelper(
|
|
violationPosition: keyword.positionAfterSkippingLeadingTrivia,
|
|
keywordLine: keywordLine,
|
|
shouldBeOnSameLine: shouldBeOnSameLine
|
|
)
|
|
}
|
|
}
|