Files
SwiftLint/Source/SwiftLintCoreMacros/RuleConfigurationMacros.swift
T
2023-10-19 21:35:57 +02:00

159 lines
6.0 KiB
Swift

import SwiftDiagnostics
import SwiftSyntax
import SwiftSyntaxMacros
private let configurationElementName = "ConfigurationElement"
private let acceptableByConfigurationElementName = "AcceptableByConfigurationElement"
enum RuleConfigurationMacroError: String, DiagnosticMessage {
case notStruct = "Attribute can only be applied to structs"
case notEnum = "Attribute can only be applied to enums"
case noStringRawType = "Attribute can only be applied to enums with a 'String' raw type"
var message: String {
rawValue
}
var diagnosticID: MessageID {
MessageID(domain: "SwiftLint", id: "RuleConfigurationMacro.\(self)")
}
var severity: DiagnosticSeverity {
.error
}
func diagnose(at node: some SyntaxProtocol) -> Diagnostic {
Diagnostic(node: Syntax(node), message: self)
}
}
enum AutoApply: MemberMacro {
static func expansion(
of node: AttributeSyntax,
providingMembersOf declaration: some DeclGroupSyntax,
in context: some MacroExpansionContext
) throws -> [DeclSyntax] {
guard let configuration = declaration.as(StructDeclSyntax.self) else {
context.diagnose(RuleConfigurationMacroError.notStruct.diagnose(at: declaration))
return []
}
var annotatedVarDecls = configuration.memberBlock.members
.compactMap {
if let varDecl = $0.decl.as(VariableDeclSyntax.self),
let annotation = varDecl.configurationElementAnnotation {
return (varDecl, annotation)
}
return nil
}
let firstIndexWithoutKey = annotatedVarDecls
.partition { _, annotation in
if case let .argumentList(arguments) = annotation.arguments {
return arguments.contains { $0.label?.text == "key" } == true
}
return false
}
let elementNames = annotatedVarDecls.compactMap {
$0.0.bindings.first?.pattern.as(IdentifierPatternSyntax.self)?.identifier.text
}
let elementsWithoutKeyUpdate = elementNames[..<firstIndexWithoutKey]
.map {
"""
try \($0).apply(configuration, ruleID: Parent.identifier)
"""
}
let elementsWithKeyUpdate = elementNames[firstIndexWithoutKey...]
.map {
"""
try \($0).apply(configuration[$\($0).key], ruleID: Parent.identifier)
try $\($0).performAfterParseOperations()
"""
}
let configBinding = elementsWithKeyUpdate.isEmpty ? "_" : "configuration"
return [
"""
mutating func apply(configuration: Any) throws {
\(raw: elementsWithoutKeyUpdate.joined(separator: "\n"))
guard let \(raw: configBinding) = configuration as? [String: Any] else {
\(raw: elementsWithoutKeyUpdate.isEmpty
? "throw Issue.invalidConfiguration(ruleID: Parent.description.identifier)"
: "return")
}
\(raw: elementsWithKeyUpdate.joined(separator: "\n"))
if !supportedKeys.isSuperset(of: configuration.keys) {
let unknownKeys = Set(configuration.keys).subtracting(supportedKeys)
throw Issue.invalidConfigurationKeys(unknownKeys.sorted())
}
}
"""
]
}
}
enum MakeAcceptableByConfigurationElement: ExtensionMacro {
static func expansion(
of node: AttributeSyntax,
attachedTo declaration: some DeclGroupSyntax,
providingExtensionsOf type: some TypeSyntaxProtocol,
conformingTo protocols: [TypeSyntax],
in context: some MacroExpansionContext
) throws -> [ExtensionDeclSyntax] {
guard let enumDecl = declaration.as(EnumDeclSyntax.self) else {
context.diagnose(RuleConfigurationMacroError.notEnum.diagnose(at: declaration))
return []
}
guard enumDecl.hasStringRawType else {
context.diagnose(RuleConfigurationMacroError.noStringRawType.diagnose(at: declaration))
return []
}
let accessLevel = enumDecl.accessLevel
return [
try ExtensionDeclSyntax("""
extension \(type): \(raw: acceptableByConfigurationElementName) {
\(raw: accessLevel)func asOption() -> OptionType { .symbol(rawValue) }
\(raw: accessLevel)init(fromAny value: Any, context ruleID: String) throws {
if let value = value as? String, let newSelf = Self(rawValue: value) {
self = newSelf
} else {
throw Issue.unknownConfiguration(ruleID: ruleID)
}
}
}
""")
]
}
}
private extension VariableDeclSyntax {
var configurationElementAnnotation: AttributeSyntax? {
let attribute = attributes.first {
if let attr = $0.as(AttributeSyntax.self), let attrId = attr.attributeName.as(IdentifierTypeSyntax.self) {
return attrId.name.text == configurationElementName
}
return false
}
return if case let .attribute(unwrapped) = attribute { unwrapped } else { nil }
}
}
private extension EnumDeclSyntax {
var hasStringRawType: Bool {
if let inheritanceClause {
return inheritanceClause.inheritedTypes.contains {
$0.type.as(IdentifierTypeSyntax.self)?.name.text == "String"
}
}
return false
}
var accessLevel: String {
modifiers.compactMap {
switch $0.name.tokenKind {
case .keyword(.public): "public "
case .keyword(.package): "package "
case .keyword(.private): "private "
default: nil
}
}.first ?? ""
}
}