SwiftLint/Source/SwiftLintFramework/Rules/Style/AttributesRule.swift

214 lines
7.3 KiB
Swift

import Foundation
import SourceKittenFramework
import SwiftSyntax
private enum AttributesRuleError: Error {
case unexpectedBlankLine
case moreThanOneAttributeInSameLine
}
public struct AttributesRule: Rule, OptInRule, ConfigurationProviderRule {
public var configuration = AttributesConfiguration()
private static let parametersPattern = "^\\s*\\(.+\\)"
private static let regularExpression = regex(parametersPattern, options: [])
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 validate(file: SwiftLintFile) -> [StyleViolation] {
guard let tree = file.syntaxTree else { return [] }
let visitor = AttributesRuleVisitor(file: file, configuration: configuration)
visitor.walk(tree)
return visitor.positions.map { position in
StyleViolation(ruleDescription: Self.description,
severity: configuration.severityConfiguration.severity,
location: Location(file: file, byteOffset: position))
}
}
}
private final class AttributesRuleVisitor: SyntaxVisitor {
var positions: [ByteCount] = []
private let file: SwiftLintFile
private let configuration: AttributesConfiguration
init(file: SwiftLintFile, configuration: AttributesConfiguration) {
self.file = file
self.configuration = configuration
super.init()
}
override func visitPost(_ node: ImportDeclSyntax) {
guard let attributes = node.attributes else {
return
}
let importOffset = ByteCount(node.importTok.positionAfterSkippingLeadingTrivia)
guard let (importLine, _) = file.stringView.lineAndCharacter(forByteOffset: importOffset) else {
return
}
for attr in attributes {
let attributeOffset = ByteCount(attr.positionAfterSkippingLeadingTrivia)
guard let (attributeLine, _) = file.stringView.lineAndCharacter(forByteOffset: attributeOffset),
attributeLine != importLine else {
continue
}
positions.append(importOffset)
return
}
}
override func visitPost(_ node: FunctionDeclSyntax) {
guard let attributes = node.attributes,
let position = validate(attributes: attributes, token: node.funcKeyword, fallbackValue: false) else {
return
}
positions.append(position)
}
override func visitPost(_ node: InitializerDeclSyntax) {
guard let attributes = node.attributes,
let position = validate(attributes: attributes, token: node.initKeyword, fallbackValue: false) else {
return
}
positions.append(position)
}
override func visitPost(_ node: VariableDeclSyntax) {
guard let attributes = node.attributes,
let position = validate(attributes: attributes, token: node.letOrVarKeyword, fallbackValue: true) else {
return
}
positions.append(position)
}
override func visitPost(_ node: ClassDeclSyntax) {
guard let attributes = node.attributes,
let position = validate(attributes: attributes, token: node.classOrActorKeyword, fallbackValue: false) else {
return
}
positions.append(position)
}
override func visitPost(_ node: StructDeclSyntax) {
guard let attributes = node.attributes,
let position = validate(attributes: attributes, token: node.structKeyword, fallbackValue: false) else {
return
}
positions.append(position)
}
override func visitPost(_ node: ProtocolDeclSyntax) {
guard let attributes = node.attributes,
let position = validate(attributes: attributes, token: node.protocolKeyword, fallbackValue: false) else {
return
}
positions.append(position)
}
override func visitPost(_ node: EnumDeclSyntax) {
guard let attributes = node.attributes,
let position = validate(attributes: attributes, token: node.enumKeyword, fallbackValue: false) else {
return
}
positions.append(position)
}
private func validate(attributes: AttributeListSyntax,
token: TokenSyntax,
fallbackValue: Bool) -> ByteCount? {
let tokenOffset = ByteCount(token.positionAfterSkippingLeadingTrivia)
guard let (tokenLine, _) = file.stringView.lineAndCharacter(forByteOffset: tokenOffset) else {
return nil
}
var lines: [(line: Int, sameLine: Bool)] = []
for attr in attributes {
let attributeOffset = ByteCount(attr.endPositionBeforeTrailingTrivia)
guard let (attributeLine, _) = file.stringView.lineAndCharacter(forByteOffset: attributeOffset) else {
continue
}
let attributeShouldBeOnSameLine: Bool = {
let attributeTokensBeforeParams = attr.withoutTrivia().tokens.split { $0.tokenKind == .leftParen }[0]
let attributeWithoutParams = attributeTokensBeforeParams.map(\.text).joined()
if configuration.alwaysOnNewLine.contains(attributeWithoutParams) {
return false
}
if configuration.alwaysOnSameLine.contains(attributeWithoutParams) {
return true
}
if attr.hasParameters {
return false
}
return fallbackValue
}()
let isLinePositionViolation: Bool
switch attributeShouldBeOnSameLine {
case true:
isLinePositionViolation = attributeLine != tokenLine
case false:
isLinePositionViolation = attributeLine == tokenLine
}
if isLinePositionViolation {
return tokenOffset
}
lines.append((attributeLine, attributeShouldBeOnSameLine))
}
// check if there're two or more attributes on the same line when one of them requires to be on its own line
let violatesLineExclusivity = zip(lines.dropFirst(), lines).contains { lhs, rhs in
return lhs.line == rhs.line && (!lhs.sameLine || !rhs.sameLine)
}
if violatesLineExclusivity {
return tokenOffset
}
lines.append((tokenLine, false))
// check if there's one of more blank lines between attributes or between attributes and the declaration
let containsBlankLine = zip(lines.dropFirst().map(\.line), lines.map(\.line)).map(-).contains { $0 > 1 }
if containsBlankLine {
return tokenOffset
}
return nil
}
}
private extension AttributeListSyntax.Element {
var hasParameters: Bool {
var kinds = tokens.map(\.tokenKind)
kinds.removeAll(where: { $0 == .atSign })
return kinds.count > 1
}
}