This seems more or less to work:
extension TriviaPiece {
var isSwiftLintDirective: Bool {
switch self {
case let .lineComment(text):
return text.hasPrefix("// swiftlint:")
default:
return false
}
}
}
extension Trivia {
/// Splits the trivia into two parts: everything before the last SwiftLint directive,
/// and the directive itself + everything after.
func splitSwiftLintDirective() -> (leadingTrivia: Trivia, swiftLintDirective: Trivia?) {
guard let swiftLintIndex = pieces.lastIndex(where: { $0.isSwiftLintDirective }) else {
return (self, nil)
}
return (Trivia(pieces: pieces[..<swiftLintIndex]), Trivia(pieces: pieces[swiftLintIndex...]))
}
}
extension SyntaxProtocol {
mutating func addMainActor(_ attributes: WritableKeyPath<Self, AttributeListSyntax>) {
guard !self[keyPath: attributes].hasMainActor else {
return
}
let savedTrivia = leadingTrivia.splitSwiftLintDirective()
let indent: Trivia = {
if let last = savedTrivia.0.pieces.last,
case .spaces = last
{
return [.newlines(1), last]
} else {
return [.newlines(1)]
}
}()
leadingTrivia = []
self[keyPath: attributes].append(
.attribute(
AttributeSyntax(attributeName: IdentifierTypeSyntax(name: .identifier("MainActor")))
.with(self[keyPath: attributes].isEmpty ? \.trailingTrivia : \.leadingTrivia, indent)
)
)
if let swiftLintDirective = savedTrivia.swiftLintDirective {
self[keyPath: attributes].trailingTrivia = self[keyPath: attributes]
.trailingTrivia
.appending(swiftLintDirective)
}
leadingTrivia = savedTrivia.leadingTrivia
}
}
extension AttributeListSyntax.Element {
var isMainActor: Bool {
"MainActor" == self.as(AttributeSyntax.self)?
.attributeName
.as(IdentifierTypeSyntax.self)?
.name
.text
}
}
extension AttributeListSyntax {
var hasMainActor: Bool {
contains(where: \.isMainActor)
}
}
Probably still some edge cases to handle, but it's working pretty well for a lot of cases.