Swift Macro 工具包

您知道 -0xF_ep-0_2 在 Swift 中是一个有效的浮点字面量吗? 您可能不知道(它等于 -63.5),而且作为宏的作者,您甚至不必关心! 在众多功能中,Macro 工具包可以保护您免受极端情况的影响,以便用户可以以任何奇怪(但正确)的方式使用您的宏。

您不需要深入了解 Swift 的语法即可制作出强大的宏,您只需要一个想法即可。

支持 Swift Macro 工具包

如果您觉得 Swift Macro 工具包有用,请考虑成为赞助者来支持我。 我将大部分空闲时间用于开源项目,每一笔赞助都帮助我花更多时间为社区制作高质量的工具和库。

为什么要使用它?

亲自看看;

获取浮点字面量的值

-0xF_ep-0_2 看起来像是您想要为其实现解析的浮点字面量类型吗? 不像; 但您不必这样做。

使用 Macro 工具包

return literal.value
不使用 Macro 工具包(值得一看)
let string = _syntax.floatingDigits.text

let isHexadecimal: Bool
let stringWithoutPrefix: String
switch string.prefix(2) {
    case "0x":
        isHexadecimal = true
        stringWithoutPrefix = String(string.dropFirst(2))
    default:
        isHexadecimal = false
        stringWithoutPrefix = string
}

let exponentSeparator: Character = isHexadecimal ? "p" : "e"
let parts = stringWithoutPrefix.lowercased().split(separator: exponentSeparator)
guard parts.count <= 2 else {
    fatalError("Float literal cannot contain more than one exponent separator")
}

let exponentValue: Int
if parts.count == 2 {
    // The exponent part is always decimal
    let exponentPart = parts[1]
    let exponentPartWithoutUnderscores = exponentPart.replacingOccurrences(of: "_", with: "")
    guard
        exponentPart.first != "_",
        !exponentPart.starts(with: "-_"),
        let exponent = Int(exponentPartWithoutUnderscores)
    else {
        fatalError("Float literal has invalid exponent part: \(string)")
    }
    exponentValue = exponent
} else {
    exponentValue = 0
}

let partsBeforeExponent = parts[0].split(separator: ".")
guard partsBeforeExponent.count <= 2 else {
    fatalError("Float literal cannot contain more than one decimal point: \(string)")
}

// The integer part can contain underscores anywhere except for the first character (which must be a digit).
let radix = isHexadecimal ? 16 : 10
let integerPart = partsBeforeExponent[0]
let integerPartWithoutUnderscores = integerPart.replacingOccurrences(of: "_", with: "")
guard
    integerPart.first != "_",
    let integerPartValue = Int(integerPartWithoutUnderscores, radix: radix).map(Double.init)
else {
    fatalError("Float literal has invalid integer part: \(string)")
}

let fractionalPartValue: Double
if partsBeforeExponent.count == 2 {
    // The fractional part can contain underscores anywhere except for the first character (which must be a digit).
    let fractionalPart = partsBeforeExponent[1]
    let fractionalPartWithoutUnderscores = fractionalPart.replacingOccurrences(of: "_", with: "")
    guard
        fractionalPart.first != "_",
        let fractionalPartDigitsValue = Int(fractionalPartWithoutUnderscores, radix: radix)
    else {
        fatalError("Float literal has invalid fractional part: \(string)")
    }

    fractionalPartValue = Double(fractionalPartDigitsValue) / pow(Double(radix), Double(fractionalPart.count - 1))
} else {
    fractionalPartValue = 0
}

let base: Double = isHexadecimal ? 2 : 10
let multiplier = pow(base, Double(exponentValue))
let sign: Double = _negationSyntax == nil ? 1 : -1

return (integerPartValue + fractionalPartValue) * multiplier * sign

类型解构

使用 Macro 工具包

guard case let .simple("Result", (successType, failureType))? = destructure(returnType) else {
    throw MacroError("Invalid return type")
}

不使用 Macro 工具包

guard
    let simpleReturnType = returnType.as(SimpleTypeIdentifierSyntax.self),
    simpleReturnType.name.description == "Result",
    let genericArguments = (simpleReturnType.genericArgumentClause?.arguments).map(Array.init),
    genericArguments.count == 2
else {
    throw MacroError("Invalid return type")
}
let successType = genericArguments[0]
let failureType = genericArguments[1]

类型规范化

Swift 有许多不同的方式来表达单个类型。 举几个这样的例子; () == VoidInt? == Optional<Int>[Int] == Array<Int>。 Swift Macro 工具包致力于向您隐藏这些细节,因此您不必处理所有极端情况。

使用 Macro 工具包

function.returnsVoid

不使用 Macro 工具包

func returnsVoid(_ function: FunctionDeclSyntax) -> Bool {
    // Function can either have no return type annotation, `()`, `Void`, or a nested single
    // element tuple with a Void-like inner type (e.g. `((((()))))` or `(((((Void)))))`)
    func isVoid(_ type: TypeSyntax) -> Bool {
        if type.description == "Void" || type.description == "()" {
            return true
        }

        guard let tuple = type.as(TupleTypeSyntax.self) else {
            return false
        }

        if let element = tuple.elements.first, tuple.elements.count == 1 {
            let isUnlabeled = element.name == nil && element.secondName == nil
            return isUnlabeled && isVoid(TypeSyntax(element.type))
        }
        return false
    }

    guard let returnType = function.output?.returnType else {
        return false
    }
    return isVoid(returnType)
}

获取字符串字面量的值

如果您想以正确的方式获取字符串字面量的值(不带插值),可能会非常繁琐。 您必须自己评估所有转义序列(unicode 序列尤其令人讨厌,例如 \u{2020})。 然后,如果用户想要使用原始字符串字面量(例如 #"This isn't a newline \n"#),事情会变得更加难以处理。 不过不用担心,Swift Macro 工具包已为您考虑周全。

使用 Macro 工具包

return literal.value
不使用 Macro 工具包
let segments = _syntax.segments.compactMap { (segment) -> String? in
    guard case let .stringSegment(segment) = segment else {
        return nil
    }
    return segment.content.text
}
guard segments.count == _syntax.segments.count else {
    return nil
}

let map: [Character: Character] = [
    "\\": "\\",
    "n": "\n",
    "r": "\r",
    "t": "\t",
    "0": "\0",
    "\"": "\"",
    "'": "'"
]
let hexadecimalCharacters = "0123456789abcdefABCDEF"

// The length of the `\###...` sequence that starts an escape sequence (zero hashes if not a raw string)
let escapeSequenceDelimiterLength = (_syntax.openDelimiter?.text.count ?? 0) + 1
// Evaluate backslash escape sequences within each segment before joining them together
let transformedSegments = segments.map { segment in
    var characters: [Character] = []
    var inEscapeSequence = false
    var iterator = segment.makeIterator()
    var escapeSequenceDelimiterPosition = 0 // Tracks the current position in the delimiter if parsing one
    while let c = iterator.next() {
        if inEscapeSequence {
            if let replacement = map[c] {
                characters.append(replacement)
            } else if c == "u" {
                var count = 0
                var digits: [Character] = []
                var iteratorCopy = iterator

                guard iterator.next() == "{" else {
                    fatalError("Expected '{' in unicode scalar escape sequence")
                }

                var foundClosingBrace = false
                while let c = iterator.next() {
                    if c == "}" {
                        foundClosingBrace = true
                        break
                    }

                    guard hexadecimalCharacters.contains(c) else {
                        iterator = iteratorCopy
                        break
                    }
                    iteratorCopy = iterator

                    digits.append(c)
                    count += 1
                }

                guard foundClosingBrace else {
                    fatalError("Expected '}' in unicode scalar escape sequence")
                }

                if !(1...8).contains(count) {
                    fatalError("Invalid unicode character escape sequence (must be 1 to 8 digits)")
                }

                guard
                    let value = UInt32(digits.map(String.init).joined(separator: ""), radix: 16),
                    let scalar = Unicode.Scalar(value)
                else {
                    fatalError("Invalid unicode scalar hexadecimal value literal")
                }

                characters.append(Character(scalar))
            }
            inEscapeSequence = false
        } else if c == "\\" && escapeSequenceDelimiterPosition == 0 {
            escapeSequenceDelimiterPosition += 1
        } else if !inEscapeSequence && c == "#" && escapeSequenceDelimiterPosition != 0 {
            escapeSequenceDelimiterPosition += 1
        } else {
            if escapeSequenceDelimiterPosition != 0 {
                characters.append("\\")
                for _ in 0..<(escapeSequenceDelimiterPosition - 1) {
                    characters.append("#")
                }
                escapeSequenceDelimiterPosition = 0
            }
            characters.append(c)
        }
        if escapeSequenceDelimiterPosition == escapeSequenceDelimiterLength {
            inEscapeSequence = true
            escapeSequenceDelimiterPosition = 0
        }
    }
    return characters.map(String.init).joined(separator: "")
}

return transformedSegments.joined(separator: "")

诊断创建

使用 Macro 工具包

let diagnostic = DiagnosticBuilder(for: function._syntax.funcKeyword)
    .message("can only add a completion-handler variant to an 'async' function")
    .messageID(domain: "AddCompletionHandlerMacro", id: "MissingAsync")
    .suggestReplacement(
        "add 'async'",
        old: function._syntax.signature,
        new: newSignature
    )
    .build()

不使用 Macro 工具包

let diagnostic = Diagnostic(
    node: Syntax(funcDecl.funcKeyword),
    message: SimpleDiagnosticMessage(
        message: "can only add a completion-handler variant to an 'async' function",
        diagnosticID: MessageID(domain: "AddCompletionHandlerMacro", id: "MissingAsync"),
        severity: .error
    ),
    fixIts: [
        FixIt(
            message: SimpleDiagnosticMessage(
                message: "add 'async'",
                diagnosticID: MessageID(domain: "AddCompletionHandlerMacro", id: "MissingAsync"),
                severity: .error
            ),
            changes: [
                FixIt.Change.replace(
                    oldNode: Syntax(funcDecl.signature),
                    newNode: Syntax(newSignature)
                )
            ]
        ),
    ]
)