├── .github └── workflows │ └── test.yml ├── .gitignore ├── Docs └── v2-migration-guide.md ├── LICENSE ├── Package.resolved ├── Package.swift ├── Plugins └── CodegenPlugin │ ├── CodegenPlugin.swift │ └── EasyProcess.swift ├── README.md ├── Sources ├── SwiftTypeReader │ ├── Basic │ │ ├── AnyKey.swift │ │ ├── FileManagerEnumerateRelativePath.swift │ │ ├── GenericHashableStorage.swift │ │ ├── HashableBoxProtocol.swift │ │ ├── HashableFromIdentity.swift │ │ ├── MessageError.swift │ │ ├── OptionalEx.swift │ │ └── Printer.swift │ ├── Decl │ │ ├── AccessorDecl.swift │ │ ├── AnyDeclContextHashableBox.swift │ │ ├── AnyDeclHashableBox.swift │ │ ├── AssociatedTypeDecl.swift │ │ ├── Attribute.swift │ │ ├── CaseParamDecl.swift │ │ ├── ClassDecl.swift │ │ ├── Context.swift │ │ ├── Decl.swift │ │ ├── DeclContext.swift │ │ ├── DeclModifier.swift │ │ ├── DeclParentContextHolder.swift │ │ ├── EnumCaseElementDecl.swift │ │ ├── EnumDecl.swift │ │ ├── FuncDecl.swift │ │ ├── FuncParamDecl.swift │ │ ├── GenericContext.swift │ │ ├── GenericParamDecl.swift │ │ ├── GenericTypeDecl.swift │ │ ├── ImportDecl.swift │ │ ├── ImportedModule.swift │ │ ├── ImportedModulesRequest.swift │ │ ├── InitDecl.swift │ │ ├── InterfaceTypeRequest.swift │ │ ├── Module.swift │ │ ├── NominalTypeDecl.swift │ │ ├── ProtocolDecl.swift │ │ ├── SourceFile.swift │ │ ├── StandardLibraryBuilder.swift │ │ ├── StructDecl.swift │ │ ├── TypeAliasDecl.swift │ │ ├── TypeDecl.swift │ │ ├── ValueDecl.swift │ │ └── VarDecl.swift │ ├── Generics │ │ ├── GenericParamList.swift │ │ ├── GenericParamsRequest.swift │ │ ├── GenericSignature.swift │ │ ├── GenericSignatureRequest.swift │ │ ├── SubstitutionMap.swift │ │ ├── TypeContextSubstitutionMap.swift │ │ ├── TypeDeclContextSubstitutionMap.swift │ │ └── TypeSubst.swift │ ├── Lookup │ │ ├── LookupOptions.swift │ │ ├── TopLevelLookupRequest.swift │ │ ├── TypeResolveRequest.swift │ │ └── UnqualifiedLookupRequest.swift │ ├── Reader │ │ ├── AttributeReader.swift │ │ ├── ModifierReader.swift │ │ ├── Reader.swift │ │ └── TypeReprReader.swift │ ├── RequestEvaluator │ │ ├── CycleRequestError.swift │ │ ├── Request.swift │ │ └── RequestEvaluator.swift │ ├── Type │ │ ├── AnyTypeHashableBox.swift │ │ ├── ClassType.swift │ │ ├── DependentMemberType.swift │ │ ├── EnumType.swift │ │ ├── ErrorType.swift │ │ ├── FunctionType.swift │ │ ├── GenericParamType.swift │ │ ├── MetatypeType.swift │ │ ├── ModuleType.swift │ │ ├── NominalType.swift │ │ ├── ProtocolType.swift │ │ ├── SType.swift │ │ ├── StructType.swift │ │ ├── TypeAliasType.swift │ │ └── TypeToTypeReprRequest.swift │ ├── TypeRepr │ │ ├── AnyTypeReprHashableBox.swift │ │ ├── CompositionTypeRepr.swift │ │ ├── ErrorTypeRepr.swift │ │ ├── FunctionTypeRepr.swift │ │ ├── IdentTypeRepr.swift │ │ ├── MetatypeTypeRepr.swift │ │ ├── TupleTypeRepr.swift │ │ └── TypeRepr.swift │ └── TypeTransform │ │ └── TypeTransformer.swift └── codegen │ ├── Codegen.swift │ ├── Definitions.swift │ ├── MessageError.swift │ └── Renderers │ ├── BaseTypeRenderer.swift │ └── TypeTransformerRenderer.swift └── Tests └── SwiftTypeReaderTests ├── BasicReaderTests.swift ├── GenericsTests.swift ├── ReaderTestCaseBase.swift ├── RequestEvaluatorTests.swift ├── TypeResolveTests.swift └── Utils.swift /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: test 2 | 3 | on: 4 | push: 5 | branches: [ "main" ] 6 | pull_request: 7 | workflow_dispatch: 8 | 9 | jobs: 10 | test: 11 | runs-on: ubuntu-latest 12 | steps: 13 | - uses: actions/checkout@v4 14 | - run: swift test 15 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | /.build 3 | /Packages 4 | /*.xcodeproj 5 | xcuserdata/ 6 | DerivedData/ 7 | /.swiftpm 8 | -------------------------------------------------------------------------------- /Docs/v2-migration-guide.md: -------------------------------------------------------------------------------- 1 | # バージョン2への移行ガイド 2 | 3 | # Declの導入 4 | 5 | Declという概念を導入しました。 6 | これはソースコード上の宣言やデータ構造に対応付いたエンティティです。 7 | コード中でなにかのシンボルを書いた時、それが指し示す対象です。 8 | 9 | 従来は型を `SType` で表現していましたが、 10 | 型そのものを現す `SType` と、型を定義する宣言を現す `Decl` が分離されました。 11 | 12 | 例えばジェネリック型 `S` と、その具象化された `S` と `S` を考える事ができます。 13 | このとき、 `S` は共通の一つの `StructDecl` で表され、ジェネリック型パラメータ `T` はここに定義されます。 14 | そして、 `S` と `S` はそれぞれ異なる `StructType` で表され、ジェネリック型引数はそれぞれが保持します。 15 | 16 | ## VarDeclの導入 17 | 18 | Stored property は `VarDecl` で表現する事にしました。 19 | これはローカル変数やcomputed property、プロトコル要求に出現する様々な `var` と共通化された表現です。 20 | 21 | ## interface type と declared interface type 22 | 23 | 以下のコードを例に説明します。 24 | 25 | ```swift 26 | struct S { 27 | var a: Int 28 | } 29 | ``` 30 | 31 | interface typeは、その `Decl` を式として参照した時の型です。 32 | 例えば、 `S` のプロパティ `a` の interface type は `Int` です。 33 | 一方、`S` の interface type は `S` ではなく、そのメタタイプの `S.Type` です。 34 | 35 | declared interface typeは、`TypeDecl` が定義する型です。 36 | 例えば、 `S` の declared interface type は `S` です。 37 | 38 | # Typeツリーの見直し 39 | 40 | `SType` は `enum` から `protocol` に変更され、 41 | ほとんど全てのプロパティが除去されました。 42 | 型についてなにか操作を行いたい場合、 `any NominalType` や `StructType` に `as?` でダウンキャストするか、 43 | `asStruct` などのキャストプロパティを使ってください。 44 | 45 | 型の名前は `NominalType` から `NominalTypeDecl` を参照し、そこから `name` として取得できます。 46 | `SType` が名前すら持っていないのは、型には `(Int) -> Int` のような関数型など、名前を持たないものもあるからです。 47 | 48 | # TypeReprの導入 49 | 50 | 型を参照するコードは `TypeRepr` として表現されます。 51 | 従来は `TypeSpecifier` でしたが本家コンパイラに命名を寄せました。 52 | またこれもダウンキャストして扱うように変更されました。 53 | これもソースコード文法と対応して、`P & Q` や `(Int) -> Int` など、アイデンティティのドット区切りでは表現できない記述があるためです。 54 | 55 | 従来の形式は `IdentTypeRepr` が対応しています。 56 | 57 | `TypeRepr` は例えば `VarDecl` の型定義部分として保持されていて、 58 | `resolve` メソッドで `SType` に変換する事ができます。 59 | 60 | ## ErrorType 61 | 62 | 型の取得がなんらかの問題を生じた場合、 `ErrorType` になります。 63 | もし `TypeRepr` を解決しようとしたが定義が見つからなかった場合は、 `ErrorType.repr` で元の `TypeRepr` を参照できます。 64 | 従来は `UnresolvedType` としていましたが、これは本家コンパイラで、別の用途で命名されているので避けました。 65 | また、本家を参考に、定義不明以外の状況にも汎用化しました。 66 | 67 | # DeclContextの導入 68 | 69 | `DeclContext` は自身の子として `Decl` を保持するオブジェクトです。 70 | これにより `DeclContext` はツリー構造を構築します。 71 | `Decl` は自身の親 `DeclContext` を参照しています。 72 | 73 | `Module` や `StructDecl` は、メンバを持てるので、 `Decl` であると同時に `DeclContext` です。 74 | 75 | `Decl` ではあるが、 `DeclContext` ではないものとして、 76 | `var` を表す `VarDecl` や、ジェネリック型パラメータを表す `GenericParamDecl` があります。 77 | 78 | `Decl` ではないが、 `DeclContext` ではあるものとして、 79 | クロージャ式などがありますが、ここでは未実装です。 80 | 81 | ## DeclContext.find 82 | 83 | `DeclContext` のメンバを名前で探すメソッドとして `find` があり、便利です。 84 | テストケースをこれで書き換えているので参考にしてください。 85 | 86 | # Locationの廃止 87 | 88 | 従来の `Location` はあるエンティティがソースの意味構造において、 89 | どこに所属するものかを表現するパスデータでした。 90 | これは値型を多用したデータ構造において、アイデンティティを取り扱うために必要でした。 91 | また、ツリー構造を巡回するために、 92 | 親に登るために `Location` の末尾を切り捨てる操作などが必要でした。 93 | 94 | 新しい設計では、 `Decl` がクラスでアイデンティティを持ち、 95 | ツリー構造を形成しています。 96 | また、 `Decl` は自身の親への参照を持っています。 97 | よって、ある `Decl` はそれ自身がツリーにおける場所も表現しています。 98 | 99 | 場所の同一性判定も、単に `Decl` を同値比較する事で行なえます。 100 | 101 | 以上のように `Location` の必要性が解消したので廃止しました。 102 | 103 | # Import文の解釈の導入 104 | 105 | `import` 文はそれが書かれたファイルでのみ、外部モジュールのシンボルを読み込み可能にします。 106 | この挙動を実装しました。 107 | 108 | `import struct Lib.S` 形式も対応しました。 109 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 omochimetaru 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Package.resolved: -------------------------------------------------------------------------------- 1 | { 2 | "pins" : [ 3 | { 4 | "identity" : "codegenkit", 5 | "kind" : "remoteSourceControl", 6 | "location" : "https://github.com/omochi/CodegenKit.git", 7 | "state" : { 8 | "revision" : "ab5ad508aab30b1572caa6e73ac0206611db0b0d", 9 | "version" : "2.0.0" 10 | } 11 | }, 12 | { 13 | "identity" : "swift-argument-parser", 14 | "kind" : "remoteSourceControl", 15 | "location" : "https://github.com/apple/swift-argument-parser.git", 16 | "state" : { 17 | "revision" : "41982a3656a71c768319979febd796c6fd111d5c", 18 | "version" : "1.5.0" 19 | } 20 | }, 21 | { 22 | "identity" : "swift-cmark", 23 | "kind" : "remoteSourceControl", 24 | "location" : "https://github.com/apple/swift-cmark.git", 25 | "state" : { 26 | "revision" : "3bc2f3e25df0cecc5dc269f7ccae65d0f386f06a", 27 | "version" : "0.4.0" 28 | } 29 | }, 30 | { 31 | "identity" : "swift-collections", 32 | "kind" : "remoteSourceControl", 33 | "location" : "https://github.com/apple/swift-collections.git", 34 | "state" : { 35 | "revision" : "9bf03ff58ce34478e66aaee630e491823326fd06", 36 | "version" : "1.1.3" 37 | } 38 | }, 39 | { 40 | "identity" : "swift-format", 41 | "kind" : "remoteSourceControl", 42 | "location" : "https://github.com/swiftlang/swift-format.git", 43 | "state" : { 44 | "revision" : "65f9da9aad84adb7e2028eb32ca95164aa590e3b", 45 | "version" : "600.0.0" 46 | } 47 | }, 48 | { 49 | "identity" : "swift-markdown", 50 | "kind" : "remoteSourceControl", 51 | "location" : "https://github.com/apple/swift-markdown.git", 52 | "state" : { 53 | "revision" : "4aae40bf6fff5286e0e1672329d17824ce16e081", 54 | "version" : "0.4.0" 55 | } 56 | }, 57 | { 58 | "identity" : "swift-syntax", 59 | "kind" : "remoteSourceControl", 60 | "location" : "https://github.com/swiftlang/swift-syntax.git", 61 | "state" : { 62 | "revision" : "cb53fa1bd3219b0b23ded7dfdd3b2baff266fd25", 63 | "version" : "600.0.0" 64 | } 65 | } 66 | ], 67 | "version" : 2 68 | } 69 | -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version:5.7 2 | 3 | import PackageDescription 4 | 5 | let package = Package( 6 | name: "SwiftTypeReader", 7 | platforms: [.macOS(.v12)], 8 | products: [ 9 | .library( 10 | name: "SwiftTypeReader", 11 | targets: ["SwiftTypeReader"] 12 | ) 13 | ], 14 | dependencies: [ 15 | .package(url: "https://github.com/swiftlang/swift-syntax.git", "600.0.0"..<"999.0.0"), 16 | .package(url: "https://github.com/apple/swift-collections.git", from: "1.1.3"), 17 | .package(url: "https://github.com/omochi/CodegenKit.git", from: "2.0.0"), 18 | ], 19 | targets: [ 20 | .executableTarget( 21 | name: "codegen", 22 | dependencies: [ 23 | .product(name: "CodegenKit", package: "CodegenKit") 24 | ] 25 | ), 26 | .plugin( 27 | name: "CodegenPlugin", 28 | capability: .command( 29 | intent: .custom(verb: "codegen", description: "codegen"), 30 | permissions: [.writeToPackageDirectory(reason: "codegen")] 31 | ), 32 | dependencies: [ 33 | .target(name: "codegen") 34 | ] 35 | ), 36 | .target( 37 | name: "SwiftTypeReader", 38 | dependencies: [ 39 | .product(name: "SwiftParser", package: "swift-syntax"), 40 | .product(name: "Collections", package: "swift-collections"), 41 | ] 42 | ), 43 | .testTarget( 44 | name: "SwiftTypeReaderTests", 45 | dependencies: ["SwiftTypeReader"] 46 | ) 47 | ] 48 | ) 49 | -------------------------------------------------------------------------------- /Plugins/CodegenPlugin/CodegenPlugin.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import PackagePlugin 3 | 4 | @main 5 | struct CodegenPlugin: CommandPlugin { 6 | func performCommand(context: PluginContext, arguments: [String]) async throws { 7 | let codegen = try context.tool(named: "codegen") 8 | 9 | let sourcesDir = context.package.directory.appending(subpath: "Sources") 10 | 11 | let process = EasyProcess( 12 | path: URL(fileURLWithPath: codegen.path.string), 13 | args: [sourcesDir.string] 14 | ) 15 | try process.run() 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /Plugins/CodegenPlugin/EasyProcess.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | struct EasyProcess { 4 | init( 5 | path: URL, 6 | args: [String], 7 | outSink: ((Data) -> Void)? = nil, 8 | errorSink: ((Data) -> Void)? = nil 9 | ) { 10 | self.path = path 11 | self.args = args 12 | self.outSink = outSink ?? Self.defaultOutSink 13 | self.errorSink = errorSink ?? Self.defaultErrorSink 14 | } 15 | 16 | var path: URL 17 | var args: [String] 18 | var outSink: (Data) -> Void 19 | var errorSink: (Data) -> Void 20 | 21 | static func makeFileHandleSink(fileHandle: FileHandle) -> (Data) -> Void { 22 | return { (data) in 23 | try? fileHandle.write(contentsOf: data) 24 | } 25 | } 26 | 27 | static var defaultOutSink: (Data) -> Void { 28 | makeFileHandleSink(fileHandle: .standardOutput) 29 | } 30 | 31 | static var defaultErrorSink: (Data) -> Void { 32 | makeFileHandleSink(fileHandle: .standardError) 33 | } 34 | 35 | @discardableResult 36 | func run() throws -> Int32 { 37 | let queue = DispatchQueue(label: "EasyProcess.run") 38 | 39 | let p = Process() 40 | p.executableURL = path 41 | p.arguments = args 42 | 43 | let outPipe = Pipe() 44 | p.standardOutput = outPipe 45 | 46 | outPipe.fileHandleForReading.readabilityHandler = { (h) in 47 | queue.sync { 48 | let data = h.availableData 49 | if !data.isEmpty { 50 | outSink(data) 51 | } 52 | } 53 | } 54 | 55 | let errPipe = Pipe() 56 | p.standardError = errPipe 57 | 58 | errPipe.fileHandleForReading.readabilityHandler = { (h) in 59 | queue.sync { 60 | let data = h.availableData 61 | if !data.isEmpty { 62 | errorSink(data) 63 | } 64 | } 65 | } 66 | 67 | try p.run() 68 | 69 | p.waitUntilExit() 70 | 71 | outPipe.fileHandleForReading.readabilityHandler = nil 72 | errPipe.fileHandleForReading.readabilityHandler = nil 73 | 74 | try queue.sync { 75 | if let data = try outPipe.fileHandleForReading.readToEnd(), 76 | !data.isEmpty 77 | { 78 | outSink(data) 79 | } 80 | if let data = try errPipe.fileHandleForReading.readToEnd(), 81 | !data.isEmpty 82 | { 83 | errorSink(data) 84 | } 85 | } 86 | 87 | return p.terminationStatus 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # SwiftTypeReader 2 | 3 | You can gather type definitions from Swift source code. 4 | 5 | ## Example 6 | 7 | ```swift 8 | func testReadmeExample() throws { 9 | try withExtendedLifetime(Context()) { (context) in 10 | let module = context.getOrCreateModule(name: "main") 11 | let reader = Reader(context: context, module: module) 12 | 13 | let source = try reader.read( 14 | source: """ 15 | struct S { 16 | var a: Int? 17 | } 18 | """, 19 | file: URL(fileURLWithPath: "S.swift") 20 | ) 21 | _ = source 22 | 23 | let s = try XCTUnwrap(module.find(name: "S")?.asStruct) 24 | XCTAssertEqual(s.name, "S") 25 | 26 | XCTAssertEqual(s.storedProperties.count, 1) 27 | let a = try XCTUnwrap(s.find(name: "a")?.asVar) 28 | XCTAssertIdentical(a, s.storedProperties[safe: 0]) 29 | XCTAssertEqual(a.name, "a") 30 | 31 | let aType = try XCTUnwrap(a.interfaceType.asEnum) 32 | XCTAssertEqual(aType.name, "Optional") 33 | XCTAssertEqual(aType.genericArgs.count, 1) 34 | 35 | let aWrappedType = try XCTUnwrap(aType.genericArgs[safe: 0]?.asStruct) 36 | XCTAssertEqual(aWrappedType.name, "Int") 37 | } 38 | } 39 | ``` 40 | 41 | ## More documents 42 | 43 | - [Version 2 Guide](https://github.com/omochi/SwiftTypeReader/blob/main/Docs/v2-migration-guide.md) 44 | 45 | # Unsupported language features 46 | 47 | ## Function body 48 | 49 | It handles only signature. 50 | 51 | ## Generic signatures of function 52 | 53 | ## Variable without type annotation 54 | 55 | ```swift 56 | struct S { 57 | var a = 0 58 | } 59 | ``` 60 | 61 | It doesn't have type inference. 62 | 63 | # Vision 64 | 65 | This library focus to use for building other libraries below. 66 | 67 | - [CodableToTypeScript](https://github.com/omochi/CodableToTypeScript): Swift type transpiler for TypeScript by me. 68 | - [CallableKit](https://github.com/sidepelican/CallableKit): Swift RPC bridge for TypeScript by iceman. 69 | 70 | But It's useful in standalone for other purpose like meta programming for Swift. 71 | 72 | # Development 73 | 74 | ## Design consideration 75 | 76 | This library refer to the Swift compiler and [Slava's book](https://forums.swift.org/t/compiling-swift-generics-part-i/60898) to build architecture. 77 | It provides *decls*, *types*, and *type reprs*. 78 | 79 | ## Code generation 80 | 81 | ``` 82 | $ swift package codegen 83 | ``` 84 | -------------------------------------------------------------------------------- /Sources/SwiftTypeReader/Basic/AnyKey.swift: -------------------------------------------------------------------------------- 1 | /* 2 | Hashable erasure for heterogeneous dictionary key. 3 | This type mangles type id into hash value unlike AnyHashable doesn't. 4 | */ 5 | 6 | struct AnyKey: Hashable { 7 | public var value: any Hashable 8 | 9 | public init(_ value: T) { 10 | self.value = value 11 | } 12 | 13 | public static func ==(a: AnyKey, b: AnyKey) -> Bool { 14 | self.equals(a.value, b.value) 15 | } 16 | 17 | private static func equals(_ a: A, _ b: B) -> Bool 18 | where A: Equatable, B: Equatable 19 | { 20 | guard A.self == B.self, 21 | let b = b as? A else { return false } 22 | return a == b 23 | } 24 | 25 | public func hash(into hasher: inout Hasher) { 26 | Self.hash(value, into: &hasher) 27 | } 28 | 29 | private static func hash(_ value: T, into hasher: inout Hasher) where T: Hashable { 30 | hasher.combine(ObjectIdentifier(T.self)) 31 | hasher.combine(value) 32 | } 33 | } 34 | 35 | -------------------------------------------------------------------------------- /Sources/SwiftTypeReader/Basic/FileManagerEnumerateRelativePath.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | #if !os(WASI) 4 | extension FileManager { 5 | func enumerateRelative( 6 | path: URL, 7 | options: Set 8 | ) -> EnumerateRelativePath { 9 | return EnumerateRelativePath(fileManager: self, base: path, options: options) 10 | } 11 | } 12 | 13 | struct EnumerateRelativePath: Sequence { 14 | enum Option { 15 | case skipsHiddenFiles 16 | } 17 | 18 | typealias Element = URL 19 | 20 | var fileManager: FileManager 21 | var base: URL 22 | var options: Set