Files
SwiftLint/Source/SwiftLintFramework/Rules/Style/AttributesRule.swift
T
JP Simard 4e3c750086 Rewrite attributes with SwiftSyntax (#4359)
Not 100% the same as it previously was, but I think all the changes
reported by OSSCheck are improvements or neutral.
2022-10-13 00:40:24 -04:00

221 lines
7.9 KiB
Swift

import SwiftSyntax
public struct AttributesRule: SwiftSyntaxRule, OptInRule, ConfigurationProviderRule {
public var configuration = AttributesConfiguration()
public init() {}
public 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.
""",
kind: .style,
nonTriggeringExamples: AttributesRuleExamples.nonTriggeringExamples,
triggeringExamples: AttributesRuleExamples.triggeringExamples
)
public func makeVisitor(file: SwiftLintFile) -> ViolationsSyntaxVisitor? {
Visitor(
locationConverter: file.locationConverter,
configuration: configuration
)
}
}
private extension AttributesRule {
final class Visitor: SyntaxVisitor, ViolationsSyntaxVisitor {
private(set) var violationPositions: [AbsolutePosition] = []
private let locationConverter: SourceLocationConverter
private let configuration: AttributesConfiguration
init(locationConverter: SourceLocationConverter, configuration: AttributesConfiguration) {
self.locationConverter = locationConverter
self.configuration = configuration
super.init(viewMode: .sourceAccurate)
}
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
)
if hasViolation {
violationPositions.append(helper.violationPosition)
return
}
let linesForAttributes = attributesAndPlacements
.filter { $1 == .dedicatedLine }
.map { $0.0.endLine(locationConverter: locationConverter) }
if linesForAttributes.isEmpty {
return
} else if !linesForAttributes.contains(helper.keywordLine - 1) {
violationPositions.append(helper.violationPosition)
return
}
let hasMultipleNewlines = node.children(viewMode: .sourceAccurate).enumerated().contains { index, element in
if index > 0 && element.leadingTrivia?.hasMultipleNewlines == true {
return true
} else {
return element.trailingTrivia?.hasMultipleNewlines == true
}
}
if hasMultipleNewlines {
violationPositions.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
} else {
return 0
}
}
}
private enum AttributePlacement {
case sameLineAsDeclaration
case dedicatedLine
}
private struct RuleHelper {
let violationPosition: AbsolutePosition
let keywordLine: Int
let shouldBeOnSameLine: Bool
func hasViolation(
locationConverter: SourceLocationConverter,
attributesAndPlacements: [(AttributeSyntax, AttributePlacement)]
) -> Bool {
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 true
}
case .dedicatedLine:
let hasViolation = attributeStartLine == keywordLine ||
linesWithAttributes.contains(attributeStartLine)
linesWithAttributes.insert(attributeStartLine)
if hasViolation {
return true
}
}
}
return false
}
}
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.attributeName.text)"
if configuration.alwaysOnSameLine.contains(atPrefixedName) {
return (attribute, .sameLineAsDeclaration)
} else if configuration.alwaysOnNewLine.contains(atPrefixedName) {
return (attribute, .dedicatedLine)
} else if attribute.argument != nil {
return (attribute, .dedicatedLine)
}
return shouldBeOnSameLine ? (attribute, .sameLineAsDeclaration) : (attribute, .dedicatedLine)
}
}
// swiftlint:disable:next cyclomatic_complexity
func makeHelper(locationConverter: SourceLocationConverter) -> RuleHelper? {
guard let parent = 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)?.importTok {
keyword = importTok
shouldBeOnSameLine = true
} else if let letOrVarKeyword = parent.as(VariableDeclSyntax.self)?.letOrVarKeyword {
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
)
}
}