繼昨天的判斷目標:
@escaping
(() -> Void)
@convention(block)
)我們需要一個檢測方式去判斷,某種 type
符合 escaping
的格式
一樣是透過老方法,透過實作 visitor 檢查特定語法
但是在執行先做一些手腳
"typealias A = \(code)"
// example
typealias A = Int
typealias A = () -> Void
讓 type
在 parse 過程中順利些
public enum EscapingDetector {
public static func detectWithTypeAlias(code: String) -> Bool {
let target = "typealias A = \(code)"
return detect(code: target)
}
public static func detect(code: String) -> Bool {
let target: String = code
let source: SourceFileSyntax = Parser.parse(source: target)
let visitor = TypeEscapeVisitor(viewMode: .sourceAccurate)
visitor.walk(source)
return visitor.isEscape
}
}
internal final class TypeEscapeVisitor: SyntaxVisitor {
internal var isEscape: Bool = false
internal override func visit(_ node: TypeInitializerClauseSyntax) -> SyntaxVisitorContinueKind {
return self.find(type: node.value) ?? .skipChildren
}
internal final func find(type: TypeSyntax) -> SyntaxVisitorContinueKind? {
/// Swift.Optional<(Swift.Error) -> ()>
if let t = type.as(MemberTypeIdentifierSyntax.self) {
if let elements = t.genericArgumentClause?.arguments,
elements.count == 1,
t.baseType.description == "Swift",
t.name.description == "Optional" {
// <(Swift.Error) -> ()> -> ["(Swift.Error)", "()"]
// <Int> -> []
let parts = elements
.first?
.withoutTrivia()
.description
.parseSourkitFunctionTypeName() ?? []
if !parts.isEmpty {
self.isEscape = true
}
return .skipChildren
}
}
/// X?
if let wrapped: TypeSyntax = type.as(OptionalTypeSyntax.self)?.wrappedType {
return find(type: wrapped)
}
/// @escaping Closure
if let attrs: AttributeListSyntax = type.as(AttributedTypeSyntax.self)?.attributes {
self.walk(attrs) // -> visit(_ node: AttributeSyntax)
return .skipChildren
}
/// (Closure)
if
let elements: TupleTypeElementListSyntax = type.as(TupleTypeSyntax.self)?.elements,
let _ = elements.first?.type.as(FunctionTypeSyntax.self),
elements.count == 1 {
self.isEscape = true
return .skipChildren
}
return nil
}
internal override func visit(_ node: AttributeSyntax) -> SyntaxVisitorContinueKind {
if node.attributeName.text == "escaping" {
self.isEscape = true
}
return .skipChildren
}
}
最終再用 test 再次驗證我們的想法
final class EscapingDetectorTests: XCTestCase {
private let normal = "@escaping () -> Void"
private let parenthesis = "(() -> Void)"
private let parenthesisOption = "(() -> Void)?"
private let other = "Int"
func testNormal() throws {
let target = self.normal
XCTAssertTrue(EscapingDetector.detectWithTypeAlias(code: target))
}
func testParenthesis() throws {
let target = self.parenthesis
XCTAssertTrue(EscapingDetector.detectWithTypeAlias(code: target))
}
func testParenthesisOption() throws {
let target = self.parenthesisOption
XCTAssertTrue(EscapingDetector.detectWithTypeAlias(code: target))
}
func testOther() throws {
let target = self.other
XCTAssertFalse(EscapingDetector.detectWithTypeAlias(code: target))
}
func testNew() throws {
let target = "let action: @escaping () -> Void"
XCTAssertTrue(EscapingDetector.detect(code: target))
}
func testNew2() throws {
let target = "Swift.Optional<(Swift.Error) -> ()>"
XCTAssertTrue(EscapingDetector.detectWithTypeAlias(code: target))
}
func testNew2_2() throws {
let target = "((Swift.Error) -> ())?"
XCTAssertTrue(EscapingDetector.detectWithTypeAlias(code: target))
}
}