diff --git a/Sources/SwiftRefactor/SyntaxUtils.swift b/Sources/SwiftRefactor/SyntaxUtils.swift index 62a108c325c..d8ebdc3099e 100644 --- a/Sources/SwiftRefactor/SyntaxUtils.swift +++ b/Sources/SwiftRefactor/SyntaxUtils.swift @@ -31,13 +31,3 @@ extension Trivia { return Trivia(pieces: self.reversed().drop(while: \.isWhitespace).reversed()) } } - -extension TypeSyntax { - var isVoid: Bool { - switch self.as(TypeSyntaxEnum.self) { - case .identifierType(let identifierType) where identifierType.name.text == "Void": return true - case .tupleType(let tupleType) where tupleType.elements.isEmpty: return true - default: return false - } - } -} diff --git a/Sources/SwiftSyntax/Utils.swift b/Sources/SwiftSyntax/Utils.swift index 3be00281ca9..a2a507ee969 100644 --- a/Sources/SwiftSyntax/Utils.swift +++ b/Sources/SwiftSyntax/Utils.swift @@ -102,3 +102,65 @@ extension RawUnexpectedNodesSyntax { self.init(raw: raw) } } + +extension TypeSyntaxProtocol { + /// Check if this syntax matches any of the standard names for `Void`: + /// * Void + /// * Swift.Void + /// * () + public var isVoid: Bool { + if let identifierType = self.as(IdentifierTypeSyntax.self) { + return identifierType.name.text == "Void" + } + if let memberType = self.as(MemberTypeSyntax.self) { + return memberType.baseType.isSwiftCoreModule && memberType.name.text == "Void" + } + if let tupleType = self.as(TupleTypeSyntax.self) { + return tupleType.elements.isEmpty + } + return false + } + + var isSwiftCoreModule: Bool { + guard let identifierType = self.as(IdentifierTypeSyntax.self) else { + return false + } + return identifierType.name.text == "Swift" + } + + /// Check if this syntax could resolve to the type passed. Only supports types where the canonical type + /// can be named using only IdentifierTypeSyntax and MemberTypeSyntax. A non-exhaustive list of unsupported + /// types includes: + /// * array types + /// * function types + /// * optional types + /// * tuple types (including Void!) + /// The type syntax is allowed to use any level of qualified name for the type, e.g. Swift.Int.self + /// will match against both "Swift.Int" and "Int". + /// + /// - Parameter type: Type to check against. NB: if passing a type alias, the canonical type will be used. + /// - Returns: true if `self` spells out some suffix of the fully qualified name of `type`, otherwise false + public func canRepresentBasicType(type: Any.Type) -> Bool { + let qualifiedTypeName = String(reflecting: type) + var typeNames = qualifiedTypeName.split(separator: ".") + var currType: TypeSyntaxProtocol = self + + while !typeNames.isEmpty { + let typeName = typeNames.popLast()! + if let identifierType = currType.as(IdentifierTypeSyntax.self) { + // It doesn't matter whether this is the final element of typeNames, because we don't know + // surrounding context - the Foo.Bar.Baz type can be referred to as `Baz` inside Foo.Bar + return identifierType.name.text == typeName + } else if let memberType = currType.as(MemberTypeSyntax.self) { + if memberType.name.text != typeName { + return false + } + currType = memberType.baseType + } else { + return false + } + } + + return false + } +} diff --git a/Tests/SwiftSyntaxTest/UtilsTests.swift b/Tests/SwiftSyntaxTest/UtilsTests.swift new file mode 100644 index 00000000000..c90fc41bc08 --- /dev/null +++ b/Tests/SwiftSyntaxTest/UtilsTests.swift @@ -0,0 +1,76 @@ +//===----------------------------------------------------------------------===// +// +// 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 the list of Swift project authors +// +//===----------------------------------------------------------------------===// + +import SwiftSyntax +import XCTest + +class UtilsTests: XCTestCase { + + public func testIsVoid() { + XCTAssertTrue(TypeSyntax("Void").isVoid) + XCTAssertTrue(TypeSyntax("Swift.Void").isVoid) + XCTAssertTrue(TypeSyntax("()").isVoid) + + XCTAssertFalse(TypeSyntax("(Int, Int)").isVoid) + XCTAssertFalse(TypeSyntax("Swift").isVoid) + XCTAssertFalse(TypeSyntax("Swift.()").isVoid) + XCTAssertFalse(TypeSyntax("Int").isVoid) + XCTAssertFalse(TypeSyntax("(())").isVoid) + XCTAssertFalse(TypeSyntax("(Void)").isVoid) + } + + public func testIsInt() { + XCTAssertTrue(TypeSyntax("Int").canRepresentBasicType(type: Int.self)) + XCTAssertTrue(TypeSyntax("Swift.Int").canRepresentBasicType(type: Int.self)) + XCTAssertTrue(TypeSyntax("Int").canRepresentBasicType(type: Swift.Int.self)) + XCTAssertTrue(TypeSyntax("Swift.Int").canRepresentBasicType(type: Swift.Int.self)) + + // Only the canonical type syntax matches + XCTAssertFalse(TypeSyntax("CInt").canRepresentBasicType(type: Int.self)) + XCTAssertFalse(TypeSyntax("Swift.CInt").canRepresentBasicType(type: Int.self)) + XCTAssertFalse(TypeSyntax("CInt").canRepresentBasicType(type: Swift.Int.self)) + XCTAssertFalse(TypeSyntax("Swift.CInt").canRepresentBasicType(type: Swift.Int.self)) + } + + public func testIsCInt() { + // Match against the canonical type (platform dependent) + XCTAssertEqual(TypeSyntax("Swift.Int").canRepresentBasicType(type: Swift.CInt.self), CInt.self == Int.self) + XCTAssertEqual(TypeSyntax("Int").canRepresentBasicType(type: Swift.CInt.self), CInt.self == Int.self) + XCTAssertEqual(TypeSyntax("Int").canRepresentBasicType(type: CInt.self), CInt.self == Int.self) + XCTAssertEqual(TypeSyntax("Swift.Int").canRepresentBasicType(type: CInt.self), CInt.self == Int.self) + + XCTAssertFalse(TypeSyntax("Swift.CInt").canRepresentBasicType(type: Swift.CInt.self)) + XCTAssertFalse(TypeSyntax("CInt").canRepresentBasicType(type: Swift.CInt.self)) + XCTAssertFalse(TypeSyntax("CInt").canRepresentBasicType(type: CInt.self)) + XCTAssertFalse(TypeSyntax("Swift.CInt").canRepresentBasicType(type: CInt.self)) + } + + public func testIsArrayType() { + // Only plain name types are supported + XCTAssertFalse(TypeSyntax("[Int]").canRepresentBasicType(type: [Int].self)) + XCTAssertFalse(TypeSyntax("Int").canRepresentBasicType(type: [Int].self)) + } + + public func testIsOptionalType() { + // Only plain name types are supported + XCTAssertFalse(TypeSyntax("Int?").canRepresentBasicType(type: Int?.self)) + XCTAssertFalse(TypeSyntax("Optional").canRepresentBasicType(type: Int?.self)) + XCTAssertFalse(TypeSyntax("Int").canRepresentBasicType(type: [Int].self)) + } + + public func testIsTupleTypes() { + // Only plain name types are supported + XCTAssertFalse(TypeSyntax("()").canRepresentBasicType(type: Void.self)) + XCTAssertFalse(TypeSyntax("Void").canRepresentBasicType(type: Void.self)) + XCTAssertFalse(TypeSyntax("(Int, Int)").canRepresentBasicType(type: (Int, Int).self)) + } +} \ No newline at end of file