Notice
Recent Posts
Recent Comments
Link
관리 메뉴

김종권의 iOS 앱 개발 알아가기

[iOS - swift] Swift Macro 예제 - toDouble, isNumber (문자열로 된 숫자 판단하는 Macro, toDouble) 본문

Swift Macro

[iOS - swift] Swift Macro 예제 - toDouble, isNumber (문자열로 된 숫자 판단하는 Macro, toDouble)

jake-kim 2023. 9. 16. 01:32

toDouble 매크로 소개

  • String타입의 숫자를 Double 형태로 바꾸는 매크로이며, 컴파일 타임에 숫자가 아닌 문자열을 미리 컴파일 에러를 발생하게하는것이 목표

toDouble 매크로 구현

  • Swift macro 프로젝트 생성
  • ToDouble 매크로 선언
//  ToDouble.swift

import Foundation
import SwiftCompilerPlugin
import SwiftSyntax
import SwiftSyntaxBuilder
import SwiftSyntaxMacros

public struct ToDouble: ExpressionMacro {
    public static func expansion(
        of node: some FreestandingMacroExpansionSyntax,
        in context: some MacroExpansionContext
    ) throws -> ExprSyntax {
        // TODO ...
    }
}
  • SwiftSyntax에 의하여 구문이 tree형태 자료구조로 표현되어 node로 부터 입력된 문자열 값 획득이 가능
    • 문자열을 가지고 하나의 인수만 입력되었는지 판단
guard
    let argument = node.argumentList.first?.expression,
    let segments = argument.as(StringLiteralExprSyntax.self)?.segments,
    segments.count == 1,
    case .stringSegment(let literalSegment)? = segments.first
else {
    throw CustomError.message("#ToDouble requires a static string literal")
}
  • literalSegment.context.text로 입력된 문자열값을 가져와서 isNumber로 비교
    • isNumber의 로직은 regularExpression을 이용한 방법이 성능이 CharacterSet보다 좋기 때문에 이것을 사용
    • (regularExpression과 CharacterSet 성능 비교는 이전 포스팅 글 참고)
let inputString = literalSegment.content.text
if inputString.isNumber {
    return "Double(\(argument))!"
} else {
    throw CustomError.message("is not number: \(argument)")
}

private extension String {
    var isNumber: Bool {
        range(
            of: "^[0-9]*$",
            options: .regularExpression
        ) != nil
    }
}
  • 전체 코드
//  ToDouble.swift

import Foundation
import SwiftCompilerPlugin
import SwiftSyntax
import SwiftSyntaxBuilder
import SwiftSyntaxMacros

public struct ToDouble: ExpressionMacro {
    public static func expansion(
        of node: some FreestandingMacroExpansionSyntax,
        in context: some MacroExpansionContext
    ) throws -> ExprSyntax {
        
        guard
            let argument = node.argumentList.first?.expression,
            let segments = argument.as(StringLiteralExprSyntax.self)?.segments,
            segments.count == 1,
            case .stringSegment(let literalSegment)? = segments.first
        else {
            throw CustomError.message("#ToDouble requires a static string literal")
        }
        
        let inputString = literalSegment.content.text
        if inputString.isNumber {
            return "Double(\(argument))!"
        } else {
            throw CustomError.message("is not number: \(argument)")
        }
    }
}

private extension String {
    var isNumber: Bool {
        range(
            of: "^[0-9]*$",
            options: .regularExpression
        ) != nil
    }
}
  • 플러그인에 추가
//  Plugins.swift

import SwiftCompilerPlugin
import SwiftSyntax
import SwiftSyntaxBuilder
import SwiftSyntaxMacros

@main
struct ExNumberPlugin: CompilerPlugin {
    let providingMacros: [Macro.Type] = [
        StringifyMacro.self, // 프로젝트 생성시 자동으로 추가된 것
        ToDouble.self
    ]
}
  • 인터페이스 정의
// ExNumber.swift

// MARK: - ToDouble
@freestanding(expression)
public macro toDouble<T>(_ value: T) -> (T, String) = #externalMacro(module: "ExNumberMacros", type: "ToDouble")
  • 사용하는 쪽 테스트를 위해 main.swift에 예제 코드 작성
// MARK: - toDouble
let numberValue = #toDouble("123")
let numberValue2 = #toDouble("123.456")

(빌드하면 제대로 구현된 것 확인 완료)

* 전체 코드: https://github.com/JK0369/ExToDoubleMacro

Comments