From d3b9518fa9cf58bc276027a8c1f1ffdddf0baf69 Mon Sep 17 00:00:00 2001 From: Alex Hoppen Date: Sun, 6 Aug 2023 21:53:27 -0700 Subject: [PATCH] Add declarations to make construction of diagnostics in macros easier Essentially, this makes three improvement: 1. Add types to allow the creation of error/warning/Fix-It messages from string literals. These types have a static diagnostic ID but I think that should be sufficient for most macros. 2. Add a convenience initializer to `Diagnostic` to create it with a single Fix-It, eliminating the need to create an array literal containing all Fix-Its 3. Add a static method on `FixIt` to create a Fix-It with a single `replace` change. This simplifies the creation of a simple diagnostic from ```swift let diagnosticDomain = "AddCompletionHandlerMacro" struct CanOnlyBeAppliedDoAsyncFunctionErrorMessage: DiagnosticMessage { let message: String var diagnosticID: SwiftDiagnostics.MessageID { .init(domain: diagnosticDomain, id: "\(Self.self)") } var severity: SwiftDiagnostics.DiagnosticSeverity { .error } init(_ funcDecl: FunctionDeclSyntax) { message = "can only add a \(funcDecl) completion-handler variant to an 'async' function" } } enum AddCompletionHandlerFixItMessage: FixItMessage { case addAsync var fixItID: SwiftDiagnostics.MessageID { .init(domain: diagnosticDomain, id: "\(Self.self)") } var message: String { switch self { case .addAsync: return "add 'async'" } } } let diagnostic = Diagnostic( node: Syntax(funcDecl.funcKeyword), message: CanOnlyBeAppliedDoAsyncFunctionErrorMessage(funcDecl), fixIt: FixIt( message: AddCompletionHandlerFixItMessage.addAsync, changes: [ .replace(oldNode: Syntax(funcDecl.signature), newNode: Syntax(newSignature)) ] ) ) ``` to ```swift let diagnostic = Diagnostic( node: funcDecl.funcKeyword, message: MacroExpansionErrorMessage("can only add a \(node) completion-handler variant to an 'async' function"), fixIt: FixIt.replace( message: MacroExpansionFixItMessage("add 'async'") oldNode: funcDecl.signature, newNode: newSignature ) ) ``` --- Sources/SwiftDiagnostics/CMakeLists.txt | 1 + Sources/SwiftDiagnostics/Convenience.swift | 50 ++++++++++++++ Sources/SwiftDiagnostics/Diagnostic.swift | 6 +- .../SwiftSyntaxMacroExpansion/CMakeLists.txt | 1 + .../MacroExpansionDiagnosticMessages.swift | 68 +++++++++++++++++++ .../MacroSystem.swift | 2 +- .../MacroSystemTests.swift | 45 +++--------- 7 files changed, 134 insertions(+), 39 deletions(-) create mode 100644 Sources/SwiftDiagnostics/Convenience.swift create mode 100644 Sources/SwiftSyntaxMacroExpansion/MacroExpansionDiagnosticMessages.swift diff --git a/Sources/SwiftDiagnostics/CMakeLists.txt b/Sources/SwiftDiagnostics/CMakeLists.txt index c15c2ea8e75..05281ea0bf3 100644 --- a/Sources/SwiftDiagnostics/CMakeLists.txt +++ b/Sources/SwiftDiagnostics/CMakeLists.txt @@ -7,6 +7,7 @@ # See http://swift.org/CONTRIBUTORS.txt for Swift project authors add_swift_host_library(SwiftDiagnostics + Convenience.swift Diagnostic.swift DiagnosticsFormatter.swift FixIt.swift diff --git a/Sources/SwiftDiagnostics/Convenience.swift b/Sources/SwiftDiagnostics/Convenience.swift new file mode 100644 index 00000000000..ea42df6b6ba --- /dev/null +++ b/Sources/SwiftDiagnostics/Convenience.swift @@ -0,0 +1,50 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2014 - 2023 Apple Inc. and the Swift project authors +// Licensed under Apache License v2.0 with Runtime Library Exception +// +// See https://swift.org/LICENSE.txt for license information +// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors +// +//===----------------------------------------------------------------------===// + +import SwiftSyntax + +extension Diagnostic { + /// Construct a new diagnostic that has exactly one Fix-It. + public init( + node: some SyntaxProtocol, + position: AbsolutePosition? = nil, + message: DiagnosticMessage, + highlights: [Syntax]? = nil, + notes: [Note] = [], + fixIt: FixIt + ) { + self.init( + node: node, + position: position, + message: message, + highlights: highlights, + notes: notes, + fixIts: [fixIt] + ) + } +} + +extension FixIt { + /// A Fix-It that replaces `oldNode` by `newNode`. + public static func replace( + message: FixItMessage, + oldNode: some SyntaxProtocol, + newNode: some SyntaxProtocol + ) -> Self { + return FixIt( + message: message, + changes: [ + .replace(oldNode: Syntax(oldNode), newNode: Syntax(newNode)) + ] + ) + } +} diff --git a/Sources/SwiftDiagnostics/Diagnostic.swift b/Sources/SwiftDiagnostics/Diagnostic.swift index 249d59c55e7..939bfea6d57 100644 --- a/Sources/SwiftDiagnostics/Diagnostic.swift +++ b/Sources/SwiftDiagnostics/Diagnostic.swift @@ -36,17 +36,17 @@ public struct Diagnostic: CustomDebugStringConvertible { /// If `highlights` is `nil` then `node` will be highlighted. This is a /// reasonable default for almost all diagnostics. public init( - node: Syntax, + node: some SyntaxProtocol, position: AbsolutePosition? = nil, message: DiagnosticMessage, highlights: [Syntax]? = nil, notes: [Note] = [], fixIts: [FixIt] = [] ) { - self.node = node + self.node = Syntax(node) self.position = position ?? node.positionAfterSkippingLeadingTrivia self.diagMessage = message - self.highlights = highlights ?? [node] + self.highlights = highlights ?? [Syntax(node)] self.notes = notes self.fixIts = fixIts } diff --git a/Sources/SwiftSyntaxMacroExpansion/CMakeLists.txt b/Sources/SwiftSyntaxMacroExpansion/CMakeLists.txt index 1ed29c312ac..ad311fa5eee 100644 --- a/Sources/SwiftSyntaxMacroExpansion/CMakeLists.txt +++ b/Sources/SwiftSyntaxMacroExpansion/CMakeLists.txt @@ -3,6 +3,7 @@ add_swift_host_library(SwiftSyntaxMacroExpansion FunctionParameterUtils.swift IndentationUtils.swift MacroExpansion.swift + MacroExpansionDiagnosticMessages.swift MacroReplacement.swift MacroSystem.swift ) diff --git a/Sources/SwiftSyntaxMacroExpansion/MacroExpansionDiagnosticMessages.swift b/Sources/SwiftSyntaxMacroExpansion/MacroExpansionDiagnosticMessages.swift new file mode 100644 index 00000000000..ecb0391bbee --- /dev/null +++ b/Sources/SwiftSyntaxMacroExpansion/MacroExpansionDiagnosticMessages.swift @@ -0,0 +1,68 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2014 - 2023 Apple Inc. and the Swift project authors +// Licensed under Apache License v2.0 with Runtime Library Exception +// +// See https://swift.org/LICENSE.txt for license information +// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors +// +//===----------------------------------------------------------------------===// + +import SwiftDiagnostics + +/// An error during macro expansion that is described by its message. +/// +/// This type allows macro authors to quickly generate error messages based on a +/// string. For any non-trivial error messages, it is encouraged to define a +/// custom type that conforms to `DiagnosticMessage`. +public struct MacroExpansionErrorMessage: Error, DiagnosticMessage { + public let message: String + + public var severity: SwiftDiagnostics.DiagnosticSeverity { .error } + + public var diagnosticID: SwiftDiagnostics.MessageID { + .init(domain: diagnosticDomain, id: "\(Self.self)") + } + + public init(_ message: String) { + self.message = message + } +} + +/// An warning during macro expansion that is described by its message. +/// +/// This type allows macro authors to quickly generate warning messages based on +/// a string. For any non-trivial warning messages, it is encouraged to define a +/// custom type that conforms to `DiagnosticMessage`. +public struct MacroExpansionWarningMessage: DiagnosticMessage { + public let message: String + + public var severity: SwiftDiagnostics.DiagnosticSeverity { .warning } + + public var diagnosticID: SwiftDiagnostics.MessageID { + .init(domain: diagnosticDomain, id: "\(Self.self)") + } + + public init(_ message: String) { + self.message = message + } +} + +/// The message of a Fix-It that is specified by a string literal +/// +/// This type allows macro authors to quickly generate Fix-It messages based on +/// a string. For any non-trivial Fix-It messages, it is encouraged to define a +/// custom type that conforms to `FixItMessage`. +public struct MacroExpansionFixItMessage: FixItMessage { + public var message: String + + public var fixItID: SwiftDiagnostics.MessageID { + .init(domain: diagnosticDomain, id: "\(Self.self)") + } + + public init(_ message: String) { + self.message = message + } +} diff --git a/Sources/SwiftSyntaxMacroExpansion/MacroSystem.swift b/Sources/SwiftSyntaxMacroExpansion/MacroSystem.swift index f9e65bebe07..d672494c131 100644 --- a/Sources/SwiftSyntaxMacroExpansion/MacroSystem.swift +++ b/Sources/SwiftSyntaxMacroExpansion/MacroSystem.swift @@ -420,7 +420,7 @@ private class AttributeRemover: SyntaxRewriter { } } -private let diagnosticDomain: String = "SwiftSyntaxMacroExpansion" +let diagnosticDomain: String = "SwiftSyntaxMacroExpansion" private enum MacroApplicationError: DiagnosticMessage, Error { case accessorMacroOnVariableWithMultipleBindings diff --git a/Tests/SwiftSyntaxMacroExpansionTest/MacroSystemTests.swift b/Tests/SwiftSyntaxMacroExpansionTest/MacroSystemTests.swift index f0822d58ac8..8c64139f3d7 100644 --- a/Tests/SwiftSyntaxMacroExpansionTest/MacroSystemTests.swift +++ b/Tests/SwiftSyntaxMacroExpansionTest/MacroSystemTests.swift @@ -20,17 +20,6 @@ import SwiftSyntaxMacroExpansion import SwiftSyntaxMacrosTestSupport import XCTest -enum CustomError: Error, CustomStringConvertible { - case message(String) - - var description: String { - switch self { - case .message(let text): - return text - } - } -} - // MARK: Example macros public struct StringifyMacro: ExpressionMacro { public static func expansion( @@ -38,7 +27,7 @@ public struct StringifyMacro: ExpressionMacro { in context: some MacroExpansionContext ) throws -> ExprSyntax { guard let argument = macro.argumentList.first?.expression else { - throw CustomError.message("missing argument") + throw MacroExpansionErrorMessage("missing argument") } return "(\(argument), \(StringLiteralExprSyntax(content: argument.description)))" @@ -110,7 +99,7 @@ public struct ColumnMacro: ExpressionMacro { ) throws -> ExprSyntax { guard let sourceLoc: AbstractSourceLocation = context.location(of: macro) else { - throw CustomError.message("can't find location for macro") + throw MacroExpansionErrorMessage("can't find location for macro") } return sourceLoc.column.with(\.leadingTrivia, macro.leadingTrivia) } @@ -123,7 +112,7 @@ public struct FileIDMacro: ExpressionMacro { ) throws -> ExprSyntax { guard let sourceLoc: AbstractSourceLocation = context.location(of: macro) else { - throw CustomError.message("can't find location for macro") + throw MacroExpansionErrorMessage("can't find location for macro") } return sourceLoc.file.with(\.leadingTrivia, macro.leadingTrivia) } @@ -147,16 +136,6 @@ struct CheckContextIndependenceMacro: ExpressionMacro { } } -struct SimpleDiagnosticMessage: DiagnosticMessage { - let message: String - let diagnosticID: MessageID - let severity: DiagnosticSeverity -} - -extension SimpleDiagnosticMessage: FixItMessage { - var fixItID: MessageID { diagnosticID } -} - public struct ErrorMacro: DeclarationMacro { public static func expansion( of node: some FreestandingMacroExpansionSyntax, @@ -168,17 +147,13 @@ public struct ErrorMacro: DeclarationMacro { stringLiteral.segments.count == 1, case let .stringSegment(messageString) = stringLiteral.segments[0] else { - throw CustomError.message("#error macro requires a string literal") + throw MacroExpansionErrorMessage("#error macro requires a string literal") } context.diagnose( Diagnostic( node: Syntax(node), - message: SimpleDiagnosticMessage( - message: messageString.content.description, - diagnosticID: MessageID(domain: "test", id: "error"), - severity: .error - ) + message: MacroExpansionErrorMessage(messageString.content.description) ) ) @@ -197,7 +172,7 @@ struct DefineBitwidthNumberedStructsMacro: DeclarationMacro { stringLiteral.segments.count == 1, case let .stringSegment(prefix) = stringLiteral.segments[0] else { - throw CustomError.message( + throw MacroExpansionErrorMessage( "#bitwidthNumberedStructs macro requires a string literal" ) } @@ -304,12 +279,12 @@ public struct AddCompletionHandler: PeerMacro { // Only on functions at the moment. We could handle initializers as well // with a bit of work. guard let funcDecl = declaration.as(FunctionDeclSyntax.self) else { - throw CustomError.message("@addCompletionHandler only works on functions") + throw MacroExpansionErrorMessage("@addCompletionHandler only works on functions") } // This only makes sense for async functions. if funcDecl.signature.effectSpecifiers?.asyncSpecifier == nil { - throw CustomError.message( + throw MacroExpansionErrorMessage( "@addCompletionHandler requires an async function" ) } @@ -559,14 +534,14 @@ public struct UnwrapMacro: CodeItemMacro { in context: some MacroExpansionContext ) throws -> [CodeBlockItemSyntax] { guard !node.argumentList.isEmpty else { - throw CustomError.message("'#unwrap' requires arguments") + throw MacroExpansionErrorMessage("'#unwrap' requires arguments") } let errorThrower = node.trailingClosure let identifiers = try node.argumentList.map { argument in guard let tupleElement = argument.as(LabeledExprSyntax.self), let declReferenceExpr = tupleElement.expression.as(DeclReferenceExprSyntax.self) else { - throw CustomError.message("Arguments must be identifiers") + throw MacroExpansionErrorMessage("Arguments must be identifiers") } return declReferenceExpr.baseName }