Skip to content

Add declarations to make construction of diagnostics in macros easier #2019

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Aug 7, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Sources/SwiftDiagnostics/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
50 changes: 50 additions & 0 deletions Sources/SwiftDiagnostics/Convenience.swift
Original file line number Diff line number Diff line change
@@ -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))
]
)
}
}
6 changes: 3 additions & 3 deletions Sources/SwiftDiagnostics/Diagnostic.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand Down
1 change: 1 addition & 0 deletions Sources/SwiftSyntaxMacroExpansion/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ add_swift_host_library(SwiftSyntaxMacroExpansion
FunctionParameterUtils.swift
IndentationUtils.swift
MacroExpansion.swift
MacroExpansionDiagnosticMessages.swift
MacroReplacement.swift
MacroSystem.swift
)
Expand Down
Original file line number Diff line number Diff line change
@@ -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
}
}
2 changes: 1 addition & 1 deletion Sources/SwiftSyntaxMacroExpansion/MacroSystem.swift
Original file line number Diff line number Diff line change
Expand Up @@ -420,7 +420,7 @@ private class AttributeRemover: SyntaxRewriter {
}
}

private let diagnosticDomain: String = "SwiftSyntaxMacroExpansion"
let diagnosticDomain: String = "SwiftSyntaxMacroExpansion"

private enum MacroApplicationError: DiagnosticMessage, Error {
case accessorMacroOnVariableWithMultipleBindings
Expand Down
45 changes: 10 additions & 35 deletions Tests/SwiftSyntaxMacroExpansionTest/MacroSystemTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -20,25 +20,14 @@ 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(
of macro: some FreestandingMacroExpansionSyntax,
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)))"
Expand Down Expand Up @@ -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)
}
Expand All @@ -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)
}
Expand All @@ -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,
Expand All @@ -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)
)
)

Expand All @@ -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"
)
}
Expand Down Expand Up @@ -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"
)
}
Expand Down Expand Up @@ -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
}
Expand Down