1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144
|
//
// This source file is part of the Swift.org open source project
//
// Copyright (c) 2024 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 Swift project authors
//
import SwiftSyntax
import SwiftSyntaxMacros
import SwiftDiagnostics
extension MacroExpansionContext {
/// Get the type of the lexical context enclosing the given node.
///
/// - Parameters:
/// - node: The node whose lexical context should be examined.
///
/// - Returns: The type of the lexical context enclosing `node`, or `nil` if
/// the lexical context cannot be represented as a type.
///
/// If the lexical context includes functions, closures, or some other
/// non-type scope, the value of this property is `nil`.
var typeOfLexicalContext: TypeSyntax? {
var typeNames = [String]()
for lexicalContext in lexicalContext.reversed() {
guard let decl = lexicalContext.asProtocol((any DeclGroupSyntax).self) else {
return nil
}
typeNames.append(decl.type.trimmedDescription)
}
if typeNames.isEmpty {
return nil
}
return "\(raw: typeNames.joined(separator: "."))"
}
}
// MARK: -
extension MacroExpansionContext {
/// Create a unique name for a function that thunks another function.
///
/// - Parameters:
/// - functionDecl: The function to thunk.
/// - prefix: A prefix to apply to the thunked name before returning.
///
/// - Returns: A unique name to use for a thunk function that thunks
/// `functionDecl`.
func makeUniqueName(thunking functionDecl: FunctionDeclSyntax, withPrefix prefix: String = "") -> TokenSyntax {
// Find all the tokens of the function declaration including argument
// types, specifiers, etc. (but not any attributes nor the body of the
// function.) Use them as the base name we pass to makeUniqueName(). This
// ensures that we will end up with a unique identifier even if two
// functions in the same scope have the exact same identifier.
let identifierCharacters = functionDecl
.with(\.attributes, [])
.with(\.body, nil)
.tokens(viewMode: .fixedUp)
.map(\.textWithoutBackticks)
.joined()
// Strip out any characters in the function's signature that won't play well
// in a generated symbol name.
let identifier = String(
identifierCharacters.map { character in
if character.isLetter || character.isWholeNumber {
return character
}
return "_"
}
)
// If there is a non-ASCII character in the identifier, we might be
// stripping it out above because we are only looking for letters and
// digits. If so, add in a hash of the identifier to improve entropy and
// reduce the risk of a collision.
//
// For example, the following function names will produce identical unique
// names without this mutation:
//
// @Test(arguments: [0]) func A(🙃: Int) {}
// @Test(arguments: [0]) func A(🙂: Int) {}
//
// Note the check here is not the same as the one above: punctuation like
// "(" should be replaced, but should not cause a hash to be emitted since
// it does not contribute any entropy to the makeUniqueName() algorithm.
//
// The intent here is not to produce a cryptographically strong hash, but to
// disambiguate between superficially similar function names. A collision
// may still occur, but we only need it to be _unlikely_. CRC-32 is good
// enough for our purposes.
if !identifierCharacters.allSatisfy(\.isASCII) {
let crcValue = crc32(identifierCharacters.utf8)
let suffix = String(crcValue, radix: 16, uppercase: false)
return makeUniqueName("\(prefix)\(identifier)_\(suffix)")
}
return makeUniqueName("\(prefix)\(identifier)")
}
}
// MARK: -
extension MacroExpansionContext {
/// Emit a diagnostic message.
///
/// - Parameters:
/// - message: The diagnostic message to emit. The `node` and `position`
/// arguments to `Diagnostic.init()` are derived from the message's
/// `syntax` property.
func diagnose(_ message: DiagnosticMessage) {
diagnose(
Diagnostic(
node: message.syntax,
position: message.syntax.positionAfterSkippingLeadingTrivia,
message: message,
fixIts: message.fixIts
)
)
}
/// Emit a sequence of diagnostic messages.
///
/// - Parameters:
/// - messages: The diagnostic messages to emit.
func diagnose(_ messages: some Sequence<DiagnosticMessage>) {
for message in messages {
diagnose(message)
}
}
/// Emit a diagnostic message for debugging purposes during development of the
/// testing library.
///
/// - Parameters:
/// - message: The message to emit into the build log.
func debug(_ message: some Any, node: some SyntaxProtocol) {
diagnose(DiagnosticMessage(syntax: Syntax(node), message: String(describing: message), severity: .warning))
}
}
|