├── .gitignore ├── LICENSE ├── Package.resolved ├── Package.swift ├── README.md ├── Sources ├── SwiftAST │ ├── Errors.swift │ ├── ParseTool.swift │ ├── SwiftASTTool.swift │ ├── SwiftBuildTool.swift │ ├── XcodebuildTool.swift │ └── main.swift └── SwiftASTCore │ ├── AST.swift │ ├── ASTLexer.swift │ ├── ASTNode.swift │ ├── ASTParser.swift │ ├── ASTPrinter.swift │ ├── ASTToken.swift │ ├── ASTTokenizer.swift │ ├── Errors.swift │ ├── ProcessManager.swift │ ├── Replacement.swift │ ├── SourceFile.swift │ └── SwiftAST.swift └── Tests ├── LinuxMain.swift └── SwiftASTTests ├── ParserTests.swift ├── TestEnvironments.swift └── XCTestManifests.swift /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | /.build 3 | /Packages 4 | /*.xcodeproj 5 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /Package.resolved: -------------------------------------------------------------------------------- 1 | { 2 | "object": { 3 | "pins": [ 4 | { 5 | "package": "SwiftPM", 6 | "repositoryURL": "https://github.com/apple/swift-package-manager.git", 7 | "state": { 8 | "branch": null, 9 | "revision": "235aacc514cb81a6881364b0fedcb3dd083228f3", 10 | "version": "0.3.0" 11 | } 12 | } 13 | ] 14 | }, 15 | "version": 1 16 | } 17 | -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version:5.0 2 | // The swift-tools-version declares the minimum version of Swift required to build this package. 3 | 4 | import PackageDescription 5 | 6 | let package = Package( 7 | name: "SwiftAST", 8 | products: [ 9 | .executable(name: "swift-ast", targets: ["SwiftAST"]), 10 | .library(name: "SwiftASTCore", type: .dynamic, targets: ["SwiftASTCore"]), 11 | ], 12 | dependencies: [ 13 | .package(url: "https://github.com/apple/swift-package-manager.git", from: "0.3.0"), 14 | ], 15 | targets: [ 16 | .target( 17 | name: "SwiftAST", 18 | dependencies: ["SwiftASTCore", "Utility"]), 19 | .target( 20 | name: "SwiftASTCore", 21 | dependencies: ["Utility"] 22 | ), 23 | .testTarget( 24 | name: "SwiftASTTests", 25 | dependencies: ["SwiftASTCore"]), 26 | ] 27 | ) 28 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # SwiftAST 2 | Experimental project for parsing an output that `swift -dump-ast` produces 3 | -------------------------------------------------------------------------------- /Sources/SwiftAST/Errors.swift: -------------------------------------------------------------------------------- 1 | //////////////////////////////////////////////////////////////////////////// 2 | // 3 | // Copyright 2019 Kishikawa Katsumi. 4 | // 5 | // Licensed under the Apache License, Version 2.0 (the "License"); 6 | // you may not use this file except in compliance with the License. 7 | // You may obtain a copy of the License at 8 | // 9 | // http://www.apache.org/licenses/LICENSE-2.0 10 | // 11 | // Unless required by applicable law or agreed to in writing, software 12 | // distributed under the License is distributed on an "AS IS" BASIS, 13 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | // See the License for the specific language governing permissions and 15 | // limitations under the License. 16 | // 17 | //////////////////////////////////////////////////////////////////////////// 18 | 19 | import Foundation 20 | import Basic 21 | import Utility 22 | 23 | func handle(error: Any) { 24 | switch error { 25 | case let anyError as AnyError: 26 | handle(error: anyError.underlyingError) 27 | default: 28 | handle(error) 29 | } 30 | } 31 | 32 | private func handle(_ error: Any) { 33 | switch error { 34 | case ArgumentParserError.expectedArguments(let parser, _): 35 | print(error: error) 36 | parser.printUsage(on: stderrStream) 37 | default: 38 | print(error: error) 39 | } 40 | } 41 | 42 | private func print(error: Any) { 43 | let writer = InteractiveWriter.stderr 44 | writer.write("error: ", inColor: .red, bold: true) 45 | writer.write("\(error)") 46 | writer.write("\n") 47 | } 48 | 49 | final class InteractiveWriter { 50 | static let stderr = InteractiveWriter(stream: stderrStream) 51 | 52 | let term: TerminalController? 53 | let stream: OutputByteStream 54 | 55 | init(stream: OutputByteStream) { 56 | self.term = (stream as? LocalFileOutputByteStream).flatMap(TerminalController.init(stream:)) 57 | self.stream = stream 58 | } 59 | 60 | func write(_ string: String, inColor color: TerminalController.Color = .noColor, bold: Bool = false) { 61 | if let term = term { 62 | term.write(string, inColor: color, bold: bold) 63 | } else { 64 | stream <<< string 65 | stream.flush() 66 | } 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /Sources/SwiftAST/ParseTool.swift: -------------------------------------------------------------------------------- 1 | //////////////////////////////////////////////////////////////////////////// 2 | // 3 | // Copyright 2019 Kishikawa Katsumi. 4 | // 5 | // Licensed under the Apache License, Version 2.0 (the "License"); 6 | // you may not use this file except in compliance with the License. 7 | // You may obtain a copy of the License at 8 | // 9 | // http://www.apache.org/licenses/LICENSE-2.0 10 | // 11 | // Unless required by applicable law or agreed to in writing, software 12 | // distributed under the License is distributed on an "AS IS" BASIS, 13 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | // See the License for the specific language governing permissions and 15 | // limitations under the License. 16 | // 17 | //////////////////////////////////////////////////////////////////////////// 18 | 19 | import Foundation 20 | import Basic 21 | import SwiftASTCore 22 | 23 | struct ParseTool { 24 | func run(source: String, buildCommand: [String], verbose: Bool = false) throws { 25 | if buildCommand.contains("xcodebuild") { 26 | try XcodebuildTool().run(source: source, arguments: buildCommand) 27 | } else if buildCommand.contains("swift") { 28 | try SwiftBuildTool().run(source: source, arguments: buildCommand) 29 | } else { 30 | throw ParserError.buildCommandNotSupported(buildCommand.joined(separator: " ")) 31 | } 32 | } 33 | } 34 | 35 | private enum ParserError: Error { 36 | case buildCommandNotSupported(String) 37 | } 38 | 39 | extension ParserError: CustomStringConvertible { 40 | var description: String { 41 | switch self { 42 | case .buildCommandNotSupported(let command): 43 | return "build command '\(command)' not supported" 44 | } 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /Sources/SwiftAST/SwiftASTTool.swift: -------------------------------------------------------------------------------- 1 | //////////////////////////////////////////////////////////////////////////// 2 | // 3 | // Copyright 2019 Kishikawa Katsumi. 4 | // 5 | // Licensed under the Apache License, Version 2.0 (the "License"); 6 | // you may not use this file except in compliance with the License. 7 | // You may obtain a copy of the License at 8 | // 9 | // http://www.apache.org/licenses/LICENSE-2.0 10 | // 11 | // Unless required by applicable law or agreed to in writing, software 12 | // distributed under the License is distributed on an "AS IS" BASIS, 13 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | // See the License for the specific language governing permissions and 15 | // limitations under the License. 16 | // 17 | //////////////////////////////////////////////////////////////////////////// 18 | 19 | import Foundation 20 | import Utility 21 | import POSIX 22 | 23 | struct SwiftASTTool { 24 | let parser: ArgumentParser 25 | let options: Options 26 | 27 | init(arguments: [String]) { 28 | parser = ArgumentParser(commandName: "swift-ast", usage: "[options] subcommand [options]", overview: "") 29 | 30 | let binder = ArgumentBinder() 31 | binder.bind(parser: parser) { $0.subcommand = $1 } 32 | binder.bind(option: parser.add(option: "--version", kind: Bool.self)) { (options, _) in options.subcommand = "version" } 33 | binder.bind(option: parser.add(option: "--verbose", kind: Bool.self, usage: "")) { $0.verbose = $1 } 34 | 35 | let parse = parser.add(subparser: "parse", overview: "") 36 | binder.bind(positional: parse.add(positional: "source", kind: String.self), to: { $0.source = $1 }) 37 | binder.bindArray(option: parse.add(option: "-buildCommand", kind: [String].self, strategy: .remaining, usage: "")) { $0.buildCommand = $1 } 38 | 39 | do { 40 | let result = try parser.parse(arguments) 41 | var options = Options() 42 | try binder.fill(parseResult: result, into: &options) 43 | self.options = options 44 | } catch { 45 | handle(error: error) 46 | POSIX.exit(1) 47 | } 48 | } 49 | } 50 | 51 | struct Options { 52 | var verbose = false 53 | var subcommand = "" 54 | 55 | var source = "" 56 | var buildCommand = [String]() 57 | } 58 | -------------------------------------------------------------------------------- /Sources/SwiftAST/SwiftBuildTool.swift: -------------------------------------------------------------------------------- 1 | //////////////////////////////////////////////////////////////////////////// 2 | // 3 | // Copyright 2019 Kishikawa Katsumi. 4 | // 5 | // Licensed under the Apache License, Version 2.0 (the "License"); 6 | // you may not use this file except in compliance with the License. 7 | // You may obtain a copy of the License at 8 | // 9 | // http://www.apache.org/licenses/LICENSE-2.0 10 | // 11 | // Unless required by applicable law or agreed to in writing, software 12 | // distributed under the License is distributed on an "AS IS" BASIS, 13 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | // See the License for the specific language governing permissions and 15 | // limitations under the License. 16 | // 17 | //////////////////////////////////////////////////////////////////////////// 18 | 19 | import Foundation 20 | import Basic 21 | import SwiftASTCore 22 | 23 | struct SwiftBuildTool { 24 | func run(source: String, arguments: [String], verbose: Bool = false) throws { 25 | let targetSource = URL(fileURLWithPath: source) 26 | let options = try SwiftBuildOptions(arguments) 27 | 28 | print("Reading project settings...") 29 | let swiftPackage = SwiftPackage(packagePath: options.packagePath, buildPath: options.buildPath) 30 | let packageDescription = try swiftPackage.describe(verbose: verbose) 31 | 32 | let dependency = try swiftPackage.showDependencies(verbose: verbose) 33 | let swiftOptions = constructSwiftOptions(swiftTestOptions: options, dependency: dependency) 34 | 35 | print("Build project target and dependencies...") 36 | let swiftBuild = SwiftBuild() 37 | try swiftBuild.build(arguments: options.rawOptions, verbose: verbose) 38 | 39 | print("Parsing...") 40 | for terget in packageDescription.targets { 41 | let path = URL(fileURLWithPath: terget.path) 42 | let sources = terget.sources.map { path.appendingPathComponent($0) } 43 | 44 | for source in sources { 45 | guard source == targetSource else { 46 | continue 47 | } 48 | if Basic.localFileSystem.exists(AbsolutePath(source.path)) && Basic.localFileSystem.isFile(AbsolutePath(source.path)) { 49 | let dependencies = sources.filter { $0 != source } 50 | let processor = SwiftAST(buildOptions: swiftOptions + ["-module-name", terget.name], dependencies: dependencies) 51 | let root = try processor.processFile(input: source, verbose: verbose) 52 | ASTPrinter(sourceFile: source).print(root) 53 | } 54 | } 55 | } 56 | } 57 | 58 | private func constructSwiftOptions(swiftTestOptions: SwiftBuildOptions, dependency: Dependency) -> [String] { 59 | let configuration = swiftTestOptions.configuration ?? "debug" 60 | 61 | let buildPath: URL 62 | if let buildPathOption = swiftTestOptions.buildPath { 63 | buildPath = URL(fileURLWithPath: buildPathOption) 64 | } else { 65 | if let packagePath = swiftTestOptions.packagePath { 66 | buildPath = URL(fileURLWithPath: packagePath).appendingPathComponent(".build") 67 | } else { 68 | buildPath = URL(fileURLWithPath: "./.build") 69 | } 70 | } 71 | let buildDirectory = buildPath.appendingPathComponent(configuration).path 72 | 73 | let fileSystem = Basic.localFileSystem 74 | var modulemapPaths = [String]() 75 | func findModules(_ dependencies: [Dependency]) { 76 | for dependency in dependencies { 77 | let modulemapPath = URL(fileURLWithPath: dependency.path).appendingPathComponent("module.modulemap").path 78 | if fileSystem.exists(AbsolutePath(modulemapPath)) && fileSystem.isFile(AbsolutePath(modulemapPath)) { 79 | modulemapPaths.append(modulemapPath) 80 | } 81 | findModules(dependency.dependencies) 82 | } 83 | } 84 | findModules(dependency.dependencies) 85 | 86 | var buildOptions = [String]() 87 | #if os(macOS) 88 | let sdk = try! SDK.macosx.path() 89 | buildOptions = ["-sdk", sdk, "-F", sdk + "/../../../Developer/Library/Frameworks"] 90 | let targetTriple = "x86_64-apple-macosx10.10" 91 | #else 92 | let targetTriple = "x86_64-unknown-linux" 93 | #endif 94 | buildOptions += ["-target", targetTriple, "-F", buildDirectory, "-I", buildDirectory] 95 | buildOptions += modulemapPaths.flatMap { ["-Xcc", "-fmodule-map-file=\($0)"] } 96 | 97 | return buildOptions 98 | } 99 | } 100 | 101 | struct SwiftBuildOptions { 102 | var configuration: String? 103 | var buildPath: String? 104 | var packagePath: String? 105 | var rawOptions: [String] 106 | 107 | var buildOptions: [String] { 108 | var options = [String]() 109 | if let configuration = configuration { 110 | options.append(contentsOf: ["--configuration", configuration]) 111 | } 112 | if let buildPath = buildPath { 113 | options.append(contentsOf: ["--build-path", buildPath]) 114 | } 115 | if let packagePath = packagePath { 116 | options.append(contentsOf: ["--package-path", packagePath]) 117 | } 118 | return options 119 | } 120 | 121 | init(_ arguments: [String]) throws { 122 | var options: [String] 123 | if let first = arguments.first, first == "swift" { 124 | options = Array(arguments.dropFirst()) 125 | } else { 126 | options = arguments 127 | } 128 | if let subcommand = options.first, !subcommand.hasPrefix("-") { 129 | guard subcommand == "build" || subcommand == "test" else { 130 | throw SwiftTestError.subcommandNotSupported(subcommand) 131 | } 132 | options = Array(options.dropFirst()) 133 | } 134 | rawOptions = options 135 | 136 | var iterator = options.makeIterator() 137 | while let option = iterator.next() { 138 | switch option { 139 | case "--configuration", "-c": 140 | configuration = iterator.next() 141 | case "--build-path": 142 | buildPath = iterator.next() 143 | case "--package-path": 144 | packagePath = iterator.next() 145 | default: 146 | break 147 | } 148 | } 149 | } 150 | } 151 | 152 | private class SwiftTool { 153 | #if os(macOS) 154 | let exec = ["/usr/bin/xcrun", "swift"] 155 | #else 156 | let exec = ["swift"] 157 | #endif 158 | 159 | let toolName: String 160 | let redirectOutput: Bool 161 | 162 | init(toolName: String, redirectOutput: Bool = true) { 163 | self.toolName = toolName 164 | self.redirectOutput = redirectOutput 165 | } 166 | 167 | var options: [String] { 168 | return [] 169 | } 170 | 171 | func run(_ arguments: [String], verbose: Bool = false) throws -> String { 172 | let process = Process(arguments: exec + [toolName] + options + arguments, redirectOutput: redirectOutput, verbose: verbose) 173 | try! process.launch() 174 | ProcessManager.default.add(process: process) 175 | let result = try! process.waitUntilExit() 176 | let output = try! result.utf8Output() 177 | switch result.exitStatus { 178 | case .terminated(let code) where code == 0: 179 | return output 180 | default: 181 | let errorOutput = try result.utf8stderrOutput() 182 | let command = process.arguments.map { $0.shellEscaped() }.joined(separator: " ") 183 | throw SwiftASTError.executingSubprocessFailed(command: command, output: errorOutput) 184 | } 185 | } 186 | } 187 | 188 | private class SwiftPackage: SwiftTool { 189 | let packagePath: String? 190 | let buildPath: String? 191 | 192 | init(packagePath: String?, buildPath: String?) { 193 | self.buildPath = buildPath 194 | self.packagePath = packagePath 195 | super.init(toolName: "package") 196 | } 197 | 198 | override var options: [String] { 199 | var options = [String]() 200 | if let packagePath = packagePath { 201 | options.append(contentsOf: ["--package-path", packagePath]) 202 | } 203 | if let buildPath = buildPath { 204 | options.append(contentsOf: ["--build-path", buildPath]) 205 | } 206 | return options 207 | } 208 | 209 | func describe(verbose: Bool = false) throws -> PackageDescription { 210 | let output = cleansingOutput(try run(["describe", "--type", "json"], verbose: verbose)) 211 | return try JSONDecoder().decode(PackageDescription.self, from: output.data(using: .utf8)!) 212 | } 213 | 214 | func showDependencies(verbose: Bool = false) throws -> Dependency { 215 | let output = cleansingOutput(try run(["show-dependencies", "--format", "json"], verbose: verbose)) 216 | return try! JSONDecoder().decode(Dependency.self, from: output.data(using: .utf8)!) 217 | } 218 | 219 | private func cleansingOutput(_ output: String) -> String { 220 | let index = output.firstIndex { $0 == "{" } 221 | if let index = index { 222 | return String(output[index...]) 223 | } 224 | return output 225 | } 226 | } 227 | 228 | private class SwiftBuild: SwiftTool { 229 | init() { 230 | super.init(toolName: "build", redirectOutput: false) 231 | } 232 | 233 | func build(arguments: [String], verbose: Bool = false) throws { 234 | _ = try run(arguments, verbose: verbose) 235 | } 236 | } 237 | 238 | private struct PackageDescription: Decodable { 239 | let name: String 240 | let path: String 241 | let targets: [Target] 242 | } 243 | 244 | private struct Target: Decodable { 245 | let c99name: String 246 | let moduleType: String 247 | let name: String 248 | let path: String 249 | let sources: [String] 250 | let type: String 251 | 252 | enum CodingKeys: String, CodingKey { 253 | case c99name 254 | case moduleType = "module_type" 255 | case name 256 | case path 257 | case sources 258 | case type 259 | } 260 | } 261 | 262 | private struct Dependency: Decodable { 263 | let name: String 264 | let path: String 265 | let url: String 266 | let version: String 267 | let dependencies: [Dependency] 268 | } 269 | 270 | private enum SwiftTestError: Error { 271 | case subcommandNotSupported(String) 272 | } 273 | 274 | extension SwiftTestError: CustomStringConvertible { 275 | var description: String { 276 | switch self { 277 | case .subcommandNotSupported(let subcommand): 278 | return "'swift \(subcommand)' is not supported. 'swift test' is only supported" 279 | } 280 | } 281 | } 282 | -------------------------------------------------------------------------------- /Sources/SwiftAST/XcodebuildTool.swift: -------------------------------------------------------------------------------- 1 | //////////////////////////////////////////////////////////////////////////// 2 | // 3 | // Copyright 2019 Kishikawa Katsumi. 4 | // 5 | // Licensed under the Apache License, Version 2.0 (the "License"); 6 | // you may not use this file except in compliance with the License. 7 | // You may obtain a copy of the License at 8 | // 9 | // http://www.apache.org/licenses/LICENSE-2.0 10 | // 11 | // Unless required by applicable law or agreed to in writing, software 12 | // distributed under the License is distributed on an "AS IS" BASIS, 13 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | // See the License for the specific language governing permissions and 15 | // limitations under the License. 16 | // 17 | //////////////////////////////////////////////////////////////////////////// 18 | 19 | import Foundation 20 | import Basic 21 | import SwiftASTCore 22 | 23 | struct XcodebuildTool { 24 | func run(source: String, arguments: [String], verbose: Bool = false) throws { 25 | let targetSource = URL(fileURLWithPath: source) 26 | let xcodebuildOptions = try XcodebuildOptions(arguments) 27 | 28 | print("Reading project settings...") 29 | let xcodebuild = Xcodebuild() 30 | let rawBuildSettings = try xcodebuild.showBuildSettings(arguments: xcodebuildOptions.rawOptions, verbose: verbose) 31 | 32 | let buildSettings = BuildSettings.parse(rawBuildSettings) 33 | guard let targetBuildSettings = buildSettings.values.first(where: { $0.sources().first { $0.absoluteURL == targetSource.absoluteURL } != nil }) else { 34 | throw XcodebuildError.sourceFileNotFound(targetSource) 35 | } 36 | 37 | print("Building dependencies...") 38 | let log = try xcodebuild.build(arguments: xcodebuildOptions.options, verbose: verbose) 39 | var swiftOptions = constructSwiftOptions(xcodebuildLog: log) 40 | if let productType = targetBuildSettings.settings["PRODUCT_TYPE"], productType.hasPrefix("com.apple.product-type.framework") || productType.hasPrefix("com.apple.product-type.library") { 41 | swiftOptions += ["-parse-as-library"] 42 | } 43 | 44 | let bridgingHeaderPath: URL? 45 | if let bridgingHeader = targetBuildSettings.settings["SWIFT_OBJC_BRIDGING_HEADER"] { 46 | if bridgingHeader.hasPrefix("/") { 47 | bridgingHeaderPath = URL(fileURLWithPath: bridgingHeader) 48 | } else { 49 | bridgingHeaderPath = URL(fileURLWithPath: targetBuildSettings.settings["SRCROOT"]!).appendingPathComponent(bridgingHeader) 50 | } 51 | } else { 52 | bridgingHeaderPath = nil 53 | } 54 | 55 | let fileSystem = Basic.localFileSystem 56 | 57 | print("Parsing...") 58 | let sources = targetBuildSettings.sources().filter { $0.pathExtension == "swift" } 59 | if fileSystem.exists(AbsolutePath(targetSource.path)) && fileSystem.isFile(AbsolutePath(targetSource.path)) { 60 | let dependencies = sources.filter { $0.absoluteURL != targetSource.absoluteURL } 61 | let processor = SwiftAST(buildOptions: swiftOptions, dependencies: dependencies, bridgingHeader: bridgingHeaderPath) 62 | let root = try processor.processFile(input: targetSource, verbose: verbose) 63 | ASTPrinter(sourceFile: targetSource).print(root) 64 | } 65 | } 66 | 67 | private func constructSwiftOptions(xcodebuildLog log: String) -> [String] { 68 | var options = [String]() 69 | let regex = try! NSRegularExpression(pattern: "^.+\\/swiftc\\s", options: [.caseInsensitive, .anchorsMatchLines]) 70 | log.enumerateLines { (line, stop) in 71 | if let _ = regex.firstMatch(in: line, range: NSRange(line.startIndex..., in: line)) { 72 | let compileOptions = line.split(separator: " ") 73 | var iterator = compileOptions.makeIterator() 74 | while let option = iterator.next() { 75 | switch option { 76 | case "-sdk": 77 | let sdk = String(iterator.next()!) 78 | options.append(String(option)) 79 | options.append(sdk) 80 | options.append("-F") 81 | options.append(sdk + "/../../../Developer/Library/Frameworks") 82 | case "-target": 83 | options.append(String(option)) 84 | options.append(String(iterator.next()!)) 85 | case "-F": 86 | options.append(String(option)) 87 | options.append(String(iterator.next()!)) 88 | case "-I": 89 | options.append(String(option)) 90 | options.append(String(iterator.next()!)) 91 | default: 92 | break 93 | } 94 | } 95 | stop = true 96 | } 97 | } 98 | return options 99 | } 100 | 101 | private func restoreOriginalSourceFiles(from backupFiles: [String: TemporaryFile]) { 102 | for (original, copy) in backupFiles { 103 | do { 104 | try FileManager.default.removeItem(atPath: original) 105 | try FileManager.default.copyItem(atPath: copy.path.asString, toPath: original) 106 | } catch { 107 | print(error.localizedDescription) 108 | } 109 | } 110 | } 111 | } 112 | 113 | struct XcodebuildOptions { 114 | var buildActions: [String] 115 | var options: [String] 116 | var rawOptions: [String] 117 | 118 | init(_ arguments: [String]) throws { 119 | let options: [String] 120 | if let first = arguments.first, first == "xcodebuild" { 121 | options = Array(arguments.dropFirst()) 122 | } else { 123 | options = arguments 124 | } 125 | rawOptions = options 126 | 127 | var iterator = options.enumerated().makeIterator() 128 | var buildActions = [(Int, String)]() 129 | var testOnlyOptions = [(Int, String)]() 130 | while let (index, option) = iterator.next() { 131 | switch option { 132 | case "-project", "-target", "-workspace", "-scheme", "-xcconfig", "-toolchain", "-find-executable", 133 | "-find-library", "-resultBundlePath", "-derivedDataPath", "-archivePath", "-exportOptionsPlist": 134 | _ = iterator.next() 135 | case "-enableCodeCoverage", "-testLanguage", "-testRegion": 136 | testOnlyOptions.append((index, option)) 137 | if let argument = iterator.next() { 138 | testOnlyOptions.append(argument) 139 | } else { 140 | throw XcodebuildError.invalidArgument(option) 141 | } 142 | case "build", "build-for-testing", "analyze", "archive", "test", "test-without-building", "install-src", "install", "clean": 143 | buildActions.append((index, option)) 144 | case "-xctestrun": 145 | throw XcodebuildError.optionNotSupported(option) 146 | default: 147 | break 148 | } 149 | } 150 | 151 | if buildActions.isEmpty { 152 | self.buildActions = ["build"] 153 | } else { 154 | self.buildActions = buildActions.map { $0.1 } 155 | } 156 | if self.buildActions.filter({ $0 == "build" || $0 == "test" || $0 == "build-for-testing" }).isEmpty { 157 | throw XcodebuildError.buildActionNotSupported 158 | } 159 | 160 | let indicesToBeRemoved = buildActions.map { $0.0 } + testOnlyOptions.map{ $0.0 } 161 | self.options = options.enumerated().filter { !indicesToBeRemoved.contains($0.offset) }.map { $0.element } 162 | } 163 | } 164 | 165 | private struct Xcodebuild { 166 | let exec = ["/usr/bin/xcrun", "xcodebuild"] 167 | 168 | func build(arguments: [String], verbose: Bool = false) throws -> String { 169 | let process = Process(arguments: exec + ["clean", "build"] + arguments, verbose: verbose) 170 | try! process.launch() 171 | ProcessManager.default.add(process: process) 172 | let result = try! process.waitUntilExit() 173 | let output = try! result.utf8Output() 174 | switch result.exitStatus { 175 | case .terminated(let code) where code == 0: 176 | return output 177 | default: 178 | let errorOutput = try result.utf8stderrOutput() 179 | let command = process.arguments.map { $0.shellEscaped() }.joined(separator: " ") 180 | throw SwiftASTError.executingSubprocessFailed(command: command, output: errorOutput) 181 | } 182 | } 183 | 184 | func showBuildSettings(arguments: [String], verbose: Bool = false) throws -> String { 185 | let process = Process(arguments: exec + arguments + ["-showBuildSettings"], verbose: verbose) 186 | try! process.launch() 187 | ProcessManager.default.add(process: process) 188 | let result = try! process.waitUntilExit() 189 | let output = try! result.utf8Output() 190 | switch result.exitStatus { 191 | case .terminated(let code) where code == 0: 192 | return output 193 | default: 194 | let errorOutput = try result.utf8stderrOutput() 195 | let command = process.arguments.map { $0.shellEscaped() }.joined(separator: " ") 196 | throw SwiftASTError.executingSubprocessFailed(command: command, output: errorOutput) 197 | } 198 | } 199 | 200 | func invoke(arguments: [String], verbose: Bool = false) throws { 201 | let process = Process(arguments: exec + arguments, redirectOutput: false, verbose: verbose) 202 | try! process.launch() 203 | ProcessManager.default.add(process: process) 204 | let result = try! process.waitUntilExit() 205 | switch result.exitStatus { 206 | case .terminated(let code) where code == 0: 207 | return 208 | default: 209 | let errorOutput = try result.utf8stderrOutput() 210 | let command = process.arguments.map { $0.shellEscaped() }.joined(separator: " ") 211 | throw SwiftASTError.executingSubprocessFailed(command: command, output: errorOutput) 212 | } 213 | } 214 | } 215 | 216 | private struct BuildSettings { 217 | let action: String 218 | let target: String 219 | let settings: [String: String] 220 | 221 | static func parse(_ rawBuildSettings: String) -> [String: BuildSettings] { 222 | var buildSettings = [String: BuildSettings]() 223 | 224 | var action: String? 225 | var target: String? 226 | var settings = [String: String]() 227 | 228 | let regex = try! NSRegularExpression(pattern: "^Build settings for action (\\S+) and target \\\"?([^\":]+)\\\"?:$", options: [.caseInsensitive, .anchorsMatchLines]) 229 | rawBuildSettings.enumerateLines { (line, stop) in 230 | if let result = regex.firstMatch(in: line, range: NSRange(line.startIndex..., in: line)) { 231 | if let target = target, let action = action { 232 | buildSettings[target] = BuildSettings(action: action, target: target, settings: settings) 233 | } 234 | action = String(line[Range(result.range(at: 1), in: line)!]) 235 | target = String(line[Range(result.range(at: 2), in: line)!]) 236 | return 237 | } 238 | 239 | let components = line.split(separator: "=").map { $0.trimmingCharacters(in: .whitespacesAndNewlines) } 240 | if components.count == 2 { 241 | settings[components[0]] = components[1] 242 | } 243 | } 244 | if let target = target, let action = action { 245 | buildSettings[target] = BuildSettings(action: action, target: target, settings: settings) 246 | } 247 | 248 | return buildSettings 249 | } 250 | 251 | fileprivate func sources() -> [URL] { 252 | var sources = [URL]() 253 | 254 | let projectFilePath = settings["PROJECT_FILE_PATH"]! 255 | let pbxprojData = try! Data(contentsOf: URL(fileURLWithPath: projectFilePath).appendingPathComponent("project.pbxproj")) 256 | let pbxproj = try? PropertyListSerialization.propertyList(from: pbxprojData, options: [], format: nil) 257 | if let pbxproj = pbxproj as? [String: Any] { 258 | if let rootObjectID = pbxproj["rootObject"] as? String, let objects = pbxproj["objects"] as? [String: Any], let rootObject = objects[rootObjectID] as? [String: Any] { 259 | if let targetIDs = rootObject["targets"] as? [String] { 260 | for targetID in targetIDs { 261 | if let targetObject = objects[targetID] as? [String: Any], let name = targetObject["name"] as? String, let productType = targetObject["productType"] as? String { 262 | if name == target && productType == settings["PRODUCT_TYPE"] { 263 | if let buildPhaseIDs = targetObject["buildPhases"] as? [String] { 264 | for buildPhaseID in buildPhaseIDs { 265 | if let buildPhase = objects[buildPhaseID] as? [String: Any], let isa = buildPhase["isa"] as? String, isa == "PBXSourcesBuildPhase", let fileIDs = buildPhase["files"] as? [String] { 266 | for fileID in fileIDs { 267 | if let fileObject = objects[fileID] as? [String: Any], let isa = fileObject["isa"] as? String, isa == "PBXBuildFile", let fileRefID = fileObject["fileRef"] as? String, let fileRefObject = objects[fileRefID] as? [String: Any] { 268 | if let isa = fileRefObject["isa"] as? String, isa == "PBXFileReference", let path = fileRefObject["path"] as? String { 269 | let groupPaths = parentGroupPaths(objects: objects, targetGroupID: fileRefID) 270 | var sourceRoot = URL(fileURLWithPath: settings["SRCROOT"]!) 271 | for groupPath in groupPaths where !groupPath.isEmpty { 272 | sourceRoot.appendPathComponent(groupPath) 273 | } 274 | sourceRoot.appendPathComponent(path) 275 | sources.append(sourceRoot) 276 | } 277 | } 278 | } 279 | } 280 | } 281 | } 282 | } 283 | } 284 | } 285 | } 286 | } 287 | } 288 | 289 | return sources 290 | } 291 | 292 | private func parentGroupPaths(objects: [String: Any], targetGroupID: String) -> [String] { 293 | var paths = [String]() 294 | for groupID in objects.keys { 295 | if let group = objects[groupID] as? [String: Any], let isa = group["isa"] as? String, isa == "PBXGroup" { 296 | if let children = group["children"] as? [String], children.contains(targetGroupID), let path = group["path"] as? String { 297 | paths.append(path) 298 | return parentGroupPaths(objects: objects, targetGroupID: groupID) + paths 299 | } 300 | } 301 | } 302 | return paths 303 | } 304 | } 305 | 306 | private enum XcodebuildError: Error { 307 | case buildActionNotSupported 308 | case optionNotSupported(String) 309 | case invalidArgument(String) 310 | case sourceFileNotFound(URL) 311 | } 312 | 313 | extension XcodebuildError: CustomStringConvertible { 314 | var description: String { 315 | switch self { 316 | case .buildActionNotSupported: 317 | return "xcodebuild action can only be specified as 'test' or 'build-for-testing'" 318 | case .optionNotSupported(let option): 319 | return "xcodebuild option '\(option)' not supported" 320 | case .invalidArgument(let option): 321 | return "xcodebuild option '\(option)' requires an argument" 322 | case .sourceFileNotFound(let url): 323 | return "source file (\(url.path) not found)" 324 | } 325 | } 326 | } 327 | -------------------------------------------------------------------------------- /Sources/SwiftAST/main.swift: -------------------------------------------------------------------------------- 1 | //////////////////////////////////////////////////////////////////////////// 2 | // 3 | // Copyright 2019 Kishikawa Katsumi. 4 | // 5 | // Licensed under the Apache License, Version 2.0 (the "License"); 6 | // you may not use this file except in compliance with the License. 7 | // You may obtain a copy of the License at 8 | // 9 | // http://www.apache.org/licenses/LICENSE-2.0 10 | // 11 | // Unless required by applicable law or agreed to in writing, software 12 | // distributed under the License is distributed on an "AS IS" BASIS, 13 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | // See the License for the specific language governing permissions and 15 | // limitations under the License. 16 | // 17 | //////////////////////////////////////////////////////////////////////////// 18 | 19 | import Foundation 20 | import Basic 21 | import SwiftASTCore 22 | 23 | do { 24 | let tool = SwiftASTTool(arguments: Array(CommandLine.arguments.dropFirst())) 25 | let options = tool.options 26 | 27 | switch options.subcommand { 28 | case "parse": 29 | let command = ParseTool() 30 | try command.run(source: options.source, buildCommand: options.buildCommand, verbose: options.verbose) 31 | case "version": 32 | print("0.1.0") 33 | default: 34 | tool.parser.printUsage(on: stdoutStream) 35 | } 36 | } catch { 37 | print("\(error)") 38 | } 39 | -------------------------------------------------------------------------------- /Sources/SwiftASTCore/AST.swift: -------------------------------------------------------------------------------- 1 | //////////////////////////////////////////////////////////////////////////// 2 | // 3 | // Copyright 2019 Kishikawa Katsumi. 4 | // 5 | // Licensed under the Apache License, Version 2.0 (the "License"); 6 | // you may not use this file except in compliance with the License. 7 | // You may obtain a copy of the License at 8 | // 9 | // http://www.apache.org/licenses/LICENSE-2.0 10 | // 11 | // Unless required by applicable law or agreed to in writing, software 12 | // distributed under the License is distributed on an "AS IS" BASIS, 13 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | // See the License for the specific language governing permissions and 15 | // limitations under the License. 16 | // 17 | //////////////////////////////////////////////////////////////////////////// 18 | 19 | import Foundation 20 | 21 | public struct AST { 22 | public let declarations: [Declaration] 23 | } 24 | 25 | public enum Statement { 26 | case expression(Expression) 27 | case declaration(Declaration) 28 | } 29 | 30 | public struct Expression { 31 | public let identifier = UUID() 32 | public let rawValue: String 33 | // FIXME: Remove optionals 34 | public let type: String! 35 | public let rawLocation: String! 36 | public let rawRange: String! 37 | public let location: SourceLocation! 38 | public let range: SourceRange! 39 | public let decl: String? 40 | public let value: String? 41 | public let throwsModifier: String? 42 | public let argumentLabels: String? 43 | public let isImplicit: Bool 44 | public var expressions = [Expression]() 45 | } 46 | 47 | extension Expression: Hashable { 48 | public func hash(into hasher: inout Hasher) { 49 | hasher.combine(identifier) 50 | } 51 | 52 | public static func ==(lhs: Expression, rhs: Expression) -> Bool { 53 | return lhs.identifier == rhs.identifier 54 | } 55 | } 56 | 57 | public enum Declaration { 58 | case `topLevelCode`(TopLevelCodeDeclaration) 59 | case `import`(ImportDeclaration) 60 | case `struct`(StructDeclaration) 61 | case `class`(ClassDeclaration) 62 | case `enum`(EnumDeclaration) 63 | case `extension`(ExtensionDeclaration) 64 | case variable(VariableDeclaration) 65 | case function(FunctionDeclaration) 66 | } 67 | 68 | public struct TopLevelCodeDeclaration { 69 | public let statements: [Statement] 70 | } 71 | 72 | public struct ImportDeclaration { 73 | public let importKind: String? 74 | public let importPath: String 75 | } 76 | 77 | public struct StructDeclaration { 78 | public let accessLevel: String 79 | public let name: String 80 | public let typeInheritance: String? 81 | public let members: [StructMember] 82 | } 83 | 84 | public enum StructMember { 85 | case declaration(Declaration) 86 | } 87 | 88 | public struct ClassDeclaration { 89 | public let accessLevel: String 90 | public let name: String 91 | public let typeInheritance: String? 92 | public let members: [ClassMember] 93 | } 94 | 95 | public enum ClassMember { 96 | case declaration(Declaration) 97 | } 98 | 99 | public struct EnumDeclaration { 100 | public let accessLevel: String 101 | public let name: String 102 | public let typeInheritance: String? 103 | public let members: [EnumMember] 104 | } 105 | 106 | public enum EnumMember { 107 | case declaration(Declaration) 108 | } 109 | 110 | public struct ExtensionDeclaration { 111 | public let accessLevel: String 112 | public let name: String 113 | public let typeInheritance: String? 114 | public let members: [ExtensionMember] 115 | } 116 | 117 | public enum ExtensionMember { 118 | case declaration(Declaration) 119 | } 120 | 121 | public struct VariableDeclaration { 122 | public let accessLevel: String 123 | public let isLet: Bool 124 | public let name: String 125 | public let type: String 126 | public let isImmutable: Bool 127 | } 128 | 129 | public struct FunctionDeclaration { 130 | public let accessLevel: String 131 | public let name: String 132 | public let parameters: [Parameter] 133 | public let body: [Statement] 134 | } 135 | 136 | public struct Parameter { 137 | public let externalName: String? 138 | public let localName: String 139 | public let type: String 140 | } 141 | 142 | public struct FunctionResult { 143 | public let type: String 144 | } 145 | 146 | public struct SourceRange { 147 | public let start: SourceLocation 148 | public let end: SourceLocation 149 | } 150 | 151 | extension SourceRange: Hashable { 152 | public static let zero = SourceRange(start: .zero, end: .zero) 153 | 154 | public func hash(into hasher: inout Hasher) { 155 | hasher.combine(start) 156 | hasher.combine(end) 157 | } 158 | 159 | public static func ==(lhs: SourceRange, rhs: SourceRange) -> Bool { 160 | return lhs.start == rhs.start && lhs.end == rhs.end 161 | } 162 | } 163 | 164 | extension SourceRange: CustomStringConvertible { 165 | public var description: String { 166 | return "\(start)-\(end)" 167 | } 168 | } 169 | 170 | public struct SourceLocation { 171 | public let line: Int 172 | public let column: Int 173 | public static let zero = SourceLocation(line: 0, column: 0) 174 | } 175 | 176 | extension SourceLocation: Hashable { 177 | public func hash(into hasher: inout Hasher) { 178 | hasher.combine(line) 179 | hasher.combine(column) 180 | } 181 | 182 | public static func ==(lhs: SourceLocation, rhs: SourceLocation) -> Bool { 183 | return lhs.line == rhs.line && lhs.column == rhs.column 184 | } 185 | } 186 | 187 | extension SourceLocation: Comparable { 188 | public static func <(lhs: SourceLocation, rhs: SourceLocation) -> Bool { 189 | if lhs.line != rhs.line { 190 | return lhs.line < rhs.line 191 | } 192 | return lhs.column < rhs.column 193 | } 194 | } 195 | 196 | extension SourceLocation: CustomStringConvertible { 197 | public var description: String { 198 | return "\(line):\(column)" 199 | } 200 | } 201 | -------------------------------------------------------------------------------- /Sources/SwiftASTCore/ASTLexer.swift: -------------------------------------------------------------------------------- 1 | //////////////////////////////////////////////////////////////////////////// 2 | // 3 | // Copyright 2019 Kishikawa Katsumi. 4 | // 5 | // Licensed under the Apache License, Version 2.0 (the "License"); 6 | // you may not use this file except in compliance with the License. 7 | // You may obtain a copy of the License at 8 | // 9 | // http://www.apache.org/licenses/LICENSE-2.0 10 | // 11 | // Unless required by applicable law or agreed to in writing, software 12 | // distributed under the License is distributed on an "AS IS" BASIS, 13 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | // See the License for the specific language governing permissions and 15 | // limitations under the License. 16 | // 17 | //////////////////////////////////////////////////////////////////////////// 18 | 19 | import Foundation 20 | 21 | class ASTLexer { 22 | class State { 23 | let tokens: [ASTToken] 24 | 25 | var current = ASTNode<[ASTToken]>([]) 26 | let root = ASTNode<[ASTToken]>([]) 27 | 28 | init(tokens: [ASTToken]) { 29 | self.tokens = tokens 30 | } 31 | } 32 | 33 | func lex(tokens: [ASTToken]) -> ASTNode<[ASTToken]> { 34 | let state = State(tokens: tokens) 35 | var stack = [(Int, ASTNode<[ASTToken]>)]() 36 | for token in state.tokens { 37 | switch token.type { 38 | case .token, .symbol, .string: 39 | if stack.isEmpty { 40 | if state.root.value.isEmpty && state.root.children.count == 0 { 41 | state.current = state.root 42 | } 43 | } 44 | state.current.value.append(token) 45 | case .indent(let count): 46 | if let top = stack.last?.0 { 47 | if count <= top { 48 | while let top = stack.last?.0, count <= top { 49 | state.current = stack.removeLast().1 50 | } 51 | } 52 | stack.append((count, state.current)) 53 | 54 | let current = ASTNode<[ASTToken]>([]) 55 | state.current.append(current) 56 | state.current = current 57 | } else { 58 | stack.append((count, state.current)) 59 | 60 | let current = ASTNode<[ASTToken]>([]) 61 | state.root.append(current) 62 | state.current = current 63 | } 64 | } 65 | } 66 | return state.root 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /Sources/SwiftASTCore/ASTNode.swift: -------------------------------------------------------------------------------- 1 | //////////////////////////////////////////////////////////////////////////// 2 | // 3 | // Copyright 2019 Kishikawa Katsumi. 4 | // 5 | // Licensed under the Apache License, Version 2.0 (the "License"); 6 | // you may not use this file except in compliance with the License. 7 | // You may obtain a copy of the License at 8 | // 9 | // http://www.apache.org/licenses/LICENSE-2.0 10 | // 11 | // Unless required by applicable law or agreed to in writing, software 12 | // distributed under the License is distributed on an "AS IS" BASIS, 13 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | // See the License for the specific language governing permissions and 15 | // limitations under the License. 16 | // 17 | //////////////////////////////////////////////////////////////////////////// 18 | 19 | import Foundation 20 | 21 | class ASTNode { 22 | var value: T 23 | var children = [ASTNode]() 24 | weak var parent: ASTNode? 25 | 26 | init(_ value: T) { 27 | self.value = value 28 | } 29 | 30 | func append(_ child: ASTNode) { 31 | children.append(child) 32 | child.parent = self 33 | } 34 | } 35 | 36 | extension ASTNode: CustomStringConvertible { 37 | var description: String { 38 | return recursiveDescription(self, level: 0) 39 | } 40 | 41 | private func recursiveDescription(_ node: ASTNode, level: Int) -> String { 42 | var description = "\(String(repeating: " ", count: level))\(node.value)\n" 43 | for child in node.children { 44 | description += recursiveDescription(child, level: level + 1) 45 | } 46 | return description 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /Sources/SwiftASTCore/ASTParser.swift: -------------------------------------------------------------------------------- 1 | //////////////////////////////////////////////////////////////////////////// 2 | // 3 | // Copyright 2019 Kishikawa Katsumi. 4 | // 5 | // Licensed under the Apache License, Version 2.0 (the "License"); 6 | // you may not use this file except in compliance with the License. 7 | // You may obtain a copy of the License at 8 | // 9 | // http://www.apache.org/licenses/LICENSE-2.0 10 | // 11 | // Unless required by applicable law or agreed to in writing, software 12 | // distributed under the License is distributed on an "AS IS" BASIS, 13 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | // See the License for the specific language governing permissions and 15 | // limitations under the License. 16 | // 17 | //////////////////////////////////////////////////////////////////////////// 18 | 19 | import Foundation 20 | 21 | class ASTParser { 22 | class State { 23 | let root: ASTNode<[ASTToken]> 24 | 25 | init(root: ASTNode<[ASTToken]>) { 26 | self.root = root 27 | } 28 | } 29 | 30 | func parse(root: ASTNode<[ASTToken]>) -> AST { 31 | let state = State(root: root) 32 | return parseSourceFileNode(node: state.root) 33 | } 34 | 35 | private func parseSourceFileNode(node sourceFileNode: ASTNode<[ASTToken]>) -> AST { 36 | var declarations = [Declaration]() 37 | for node in sourceFileNode.children { 38 | for token in node.value { 39 | switch (token.type, token.value) { 40 | case (.token, "top_level_code_decl"): 41 | declarations.append(.topLevelCode(parseTopLevelCodeDeclarationNode(node: node))) 42 | case (.token, "import_decl"): 43 | declarations.append(.import(parseImportDeclarationNode(node: node))) 44 | case (.token, "struct_decl"): 45 | declarations.append(.struct(parseStructDeclarationNode(node: node))) 46 | case (.token, "class_decl"): 47 | declarations.append(.class(parseClassDeclarationNode(node: node))) 48 | case (.token, "enum_decl"): 49 | declarations.append(.enum(parseEnumDeclarationNode(node: node))) 50 | case (.token, "extension_decl"): 51 | declarations.append(.extension(parseExtensionDeclarationNode(node: node))) 52 | case (.token, "func_decl"): 53 | declarations.append(.function(parseFunctionDeclarationNode(node: node))) 54 | default: 55 | break 56 | } 57 | } 58 | } 59 | return AST(declarations: declarations) 60 | } 61 | 62 | private func parseTopLevelCodeDeclarationNode(node: ASTNode<[ASTToken]>) -> TopLevelCodeDeclaration { 63 | var statements = [Statement]() 64 | for node in node.children { 65 | for token in node.value { 66 | switch (token.type, token.value) { 67 | case (.token, "brace_stmt"): 68 | statements.append(.expression(parseExpressionNode(node: node))) 69 | default: 70 | break 71 | } 72 | } 73 | } 74 | return TopLevelCodeDeclaration(statements: statements) 75 | } 76 | 77 | private func parseImportDeclarationNode(node: ASTNode<[ASTToken]>) -> ImportDeclaration { 78 | let tokens = node.value 79 | let attributes = parseKeyValueAttributes(tokens: tokens) 80 | let importKind = attributes["kind"] 81 | let importPath = parseSymbol(tokens: tokens)! 82 | return ImportDeclaration(importKind: importKind, importPath: importPath) 83 | } 84 | 85 | private func parseStructDeclarationNode(node: ASTNode<[ASTToken]>) -> StructDeclaration { 86 | let tokens = node.value 87 | let name = parseString(tokens: tokens)! 88 | let accessLevel = parseAccessLevel(tokens: tokens) 89 | let typeInheritance = parseInherits(tokens: tokens) 90 | let members: [StructMember] = 91 | filter(node: node, matches: "func_decl") 92 | .map { .declaration(.function(parseFunctionDeclarationNode(node: $0))) } + 93 | filter(node: node, matches: "var_decl") 94 | .map { .declaration(.variable(parseVariableDeclarationNode(node: $0))) } 95 | return StructDeclaration(accessLevel: accessLevel, name: name, typeInheritance: typeInheritance, members: members) 96 | } 97 | 98 | private func parseClassDeclarationNode(node: ASTNode<[ASTToken]>) -> ClassDeclaration { 99 | let tokens = node.value 100 | let name = parseString(tokens: tokens)! 101 | let accessLevel = parseAccessLevel(tokens: tokens) 102 | let typeInheritance = parseInherits(tokens: tokens) 103 | let members: [ClassMember] = 104 | filter(node: node, matches: "func_decl") 105 | .map { .declaration(.function(parseFunctionDeclarationNode(node: $0))) } 106 | return ClassDeclaration(accessLevel: accessLevel, name: name, typeInheritance: typeInheritance, members: members) 107 | } 108 | 109 | private func parseEnumDeclarationNode(node: ASTNode<[ASTToken]>) -> EnumDeclaration { 110 | let tokens = node.value 111 | let name = parseString(tokens: tokens)! 112 | let accessLevel = parseAccessLevel(tokens: tokens) 113 | let typeInheritance = parseInherits(tokens: tokens) 114 | let members: [EnumMember] = 115 | filter(node: node, matches: "func_decl") 116 | .map { .declaration(.function(parseFunctionDeclarationNode(node: $0))) } 117 | return EnumDeclaration(accessLevel: accessLevel, name: name, typeInheritance: typeInheritance, members: members) 118 | } 119 | 120 | private func parseExtensionDeclarationNode(node: ASTNode<[ASTToken]>) -> ExtensionDeclaration { 121 | let tokens = node.value 122 | let name = tokens[2].value 123 | let accessLevel = parseAccessLevel(tokens: tokens) 124 | let typeInheritance = parseInherits(tokens: tokens) 125 | let members: [ExtensionMember] = 126 | filter(node: node, matches: "func_decl") 127 | .map { .declaration(.function(parseFunctionDeclarationNode(node: $0))) } 128 | return ExtensionDeclaration(accessLevel: accessLevel, name: name, typeInheritance: typeInheritance, members: members) 129 | } 130 | 131 | private func parseVariableDeclarationNode(node: ASTNode<[ASTToken]>) -> VariableDeclaration { 132 | let tokens = node.value 133 | let attributes = parseKeyValueAttributes(tokens: tokens) 134 | 135 | let name: String! = parseString(tokens: tokens) ?? parseSymbol(tokens: tokens) 136 | let accessLevel = parseAccessLevel(tokens: tokens) 137 | let type = attributes["type"]! 138 | 139 | return VariableDeclaration(accessLevel: accessLevel, isLet: isLet(tokens: tokens), name: name, type: type, isImmutable: isImmutable(tokens: tokens)) 140 | } 141 | 142 | private func parseFunctionDeclarationNode(node: ASTNode<[ASTToken]>) -> FunctionDeclaration { 143 | let tokens = node.value 144 | 145 | let name = parseString(tokens: tokens) ?? parseSymbol(tokens: tokens) 146 | let accessLevel = parseAccessLevel(tokens: tokens) 147 | 148 | var parameters = [Parameter]() 149 | var body = [Statement]() 150 | for node in node.children { 151 | for token in node.value { 152 | switch (token.type, token.value) { 153 | case (.token, "parameter_list"): 154 | parameters.append(contentsOf: parseParameterListNode(node: node)) 155 | case (.token, "brace_stmt"): 156 | body.append(.expression(parseExpressionNode(node: node))) 157 | default: 158 | break 159 | } 160 | } 161 | } 162 | 163 | return FunctionDeclaration(accessLevel: accessLevel, name: name!, parameters: parameters, body: body) 164 | } 165 | 166 | private func filter(node: ASTNode<[ASTToken]>, matches value: String) -> [ASTNode<[ASTToken]>] { 167 | var nodes = [ASTNode<[ASTToken]>]() 168 | for node in node.children { 169 | let tokens = node.value 170 | if isImplicit(tokens: tokens) { 171 | continue 172 | } 173 | for token in tokens { 174 | switch (token.type, token.value) { 175 | case (.token, value): 176 | nodes.append(node) 177 | default: 178 | break 179 | } 180 | } 181 | } 182 | return nodes 183 | } 184 | 185 | private func parseExpressionNode(node: ASTNode<[ASTToken]>) -> Expression { 186 | let tokens = node.value 187 | let attributes = parseKeyValueAttributes(tokens: tokens) 188 | 189 | let rawValue = tokens[1].value 190 | let type = attributes["type"] 191 | let rawLocation = attributes["location"] 192 | var location: SourceLocation? 193 | if let rawLocation = rawLocation { 194 | location = parseLocation(rawLocation) 195 | } 196 | let rawRange = attributes["range"] 197 | var sourceRange: SourceRange? 198 | if let rawRange = rawRange { 199 | sourceRange = parseRange(rawRange) 200 | } 201 | let decl = attributes["decl"] 202 | let value = attributes["value"] 203 | let argumentLabels = attributes["arg_labels"] 204 | var throwsModifier: String? 205 | for token in tokens { 206 | switch (token.type, token.value) { 207 | case (.token, "nothrow"): 208 | throwsModifier = "nothrow" 209 | case (.token, "throws"): 210 | throwsModifier = "throws" 211 | case (.token, "rethrows"): 212 | throwsModifier = "rethrows" 213 | default: 214 | break 215 | } 216 | } 217 | let implicit = isImplicit(tokens: tokens) 218 | var expression = Expression(rawValue: rawValue, type: type, rawLocation: rawLocation, rawRange: rawRange, 219 | location: location, range: sourceRange, decl: decl, value: value, throwsModifier: throwsModifier, 220 | argumentLabels: argumentLabels, isImplicit: implicit, expressions: []) 221 | 222 | for node in node.children { 223 | let tokens = node.value 224 | if tokens.count > 1 { 225 | expression.expressions.append(parseExpressionNode(node: node)) 226 | } 227 | } 228 | return expression 229 | } 230 | 231 | private func parseLocation(_ locationAttribute: String) -> SourceLocation { 232 | let info = locationAttribute.split(separator: ":") 233 | let line = Int(info[1])! - 1 234 | let column = Int(info[2])! 235 | return SourceLocation(line: line, column: column) 236 | } 237 | 238 | private func parseRange(_ rangeAttribute: String) -> SourceRange { 239 | let info = rangeAttribute 240 | .replacingOccurrences(of: "[", with: "") 241 | .replacingOccurrences(of: "", with: "") 242 | .replacingOccurrences(of: " - line", with: "") 243 | .split(separator: ":") 244 | let start = SourceLocation(line: Int(info[1])! - 1, column: Int(info[2])! - 1) 245 | let end = SourceLocation(line: Int(info[3])! - 1, column: Int(info[4])!) 246 | return SourceRange(start: start, end: end) 247 | } 248 | 249 | private func parseParameterListNode(node: ASTNode<[ASTToken]>) -> [Parameter] { 250 | return node.children.map { parseParameter(tokens: $0.value) } 251 | } 252 | 253 | private func parseParameter(tokens: [ASTToken]) -> Parameter { 254 | let attributes = parseKeyValueAttributes(tokens: tokens) 255 | let externalName: String? = attributes["apiName"] 256 | var localName: String! 257 | for token in tokens { 258 | if case .string = token.type { 259 | localName = token.value 260 | } 261 | } 262 | let type = attributes["type"]! 263 | return Parameter(externalName: externalName, localName: localName, type: type) 264 | } 265 | 266 | private func parseKeyValueAttributes(tokens: [ASTToken]) -> [String: String] { 267 | var attributes = [String: String]() 268 | for (index, token) in tokens.enumerated() { 269 | switch (token.type, token.value) { 270 | case (.token, "="): 271 | attributes[tokens[index - 1].value] = tokens[index + 1].value 272 | default: 273 | break 274 | } 275 | } 276 | return attributes 277 | } 278 | 279 | private func parseString(tokens: [ASTToken]) -> String? { 280 | for token in tokens { 281 | switch token.type { 282 | case .string: 283 | return token.value 284 | default: 285 | break 286 | } 287 | } 288 | return nil 289 | } 290 | 291 | private func parseSymbol(tokens: [ASTToken]) -> String? { 292 | for token in tokens { 293 | switch token.type { 294 | case .symbol: 295 | return token.value 296 | default: 297 | break 298 | } 299 | } 300 | return nil 301 | } 302 | 303 | private func parseAccessLevel(tokens: [ASTToken]) -> String { 304 | let attributes = parseKeyValueAttributes(tokens: tokens) 305 | return attributes["access"] ?? "internal" 306 | } 307 | 308 | private func parseInherits(tokens: [ASTToken]) -> String? { 309 | for (index, token) in tokens.enumerated() { 310 | switch (token.type, token.value) { 311 | case (.token, ":"): 312 | if case .token = tokens[index - 1].type, tokens[index - 1].value == "inherits" { 313 | return tokens[index + 1.. Bool { 323 | return tokens.contains { 324 | if case .token = $0.type, $0.value == "let" { 325 | return true 326 | } 327 | return false 328 | } 329 | } 330 | 331 | private func isImmutable(tokens: [ASTToken]) -> Bool { 332 | return tokens.contains { 333 | if case .token = $0.type, $0.value == "immutable" { 334 | return true 335 | } 336 | return false 337 | } 338 | } 339 | 340 | private func isImplicit(tokens: [ASTToken]) -> Bool { 341 | return tokens.contains { 342 | if case .token = $0.type, $0.value == "implicit" { 343 | return true 344 | } 345 | return false 346 | } 347 | } 348 | } 349 | -------------------------------------------------------------------------------- /Sources/SwiftASTCore/ASTPrinter.swift: -------------------------------------------------------------------------------- 1 | //////////////////////////////////////////////////////////////////////////// 2 | // 3 | // Copyright 2019 Kishikawa Katsumi. 4 | // 5 | // Licensed under the Apache License, Version 2.0 (the "License"); 6 | // you may not use this file except in compliance with the License. 7 | // You may obtain a copy of the License at 8 | // 9 | // http://www.apache.org/licenses/LICENSE-2.0 10 | // 11 | // Unless required by applicable law or agreed to in writing, software 12 | // distributed under the License is distributed on an "AS IS" BASIS, 13 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | // See the License for the specific language governing permissions and 15 | // limitations under the License. 16 | // 17 | //////////////////////////////////////////////////////////////////////////// 18 | 19 | import Foundation 20 | import Basic 21 | 22 | public struct ASTPrinter { 23 | let sourceFile: SourceFile 24 | private let fileSystem = Basic.localFileSystem 25 | 26 | public init(sourceFile: URL) { 27 | let contents = try! fileSystem.readFileContents(AbsolutePath(sourceFile.path)) 28 | let sourceText = contents.asReadableString 29 | 30 | var lineNumber = 1 31 | var offset = 0 32 | var sourceLines = [SourceLine]() 33 | for line in sourceText.split(separator: "\n", omittingEmptySubsequences: false) { 34 | sourceLines.append(SourceLine(text: String(line).utf8, lineNumber: lineNumber, offset: offset)) 35 | lineNumber += 1 36 | offset += line.utf8.count + 1 // characters + newline 37 | } 38 | self.sourceFile = SourceFile(sourceText: sourceText.utf8, sourceLines: sourceLines) 39 | } 40 | 41 | public func print(_ root: AST) { 42 | for declaration in root.declarations { 43 | print(declaration) 44 | } 45 | } 46 | 47 | public func print(_ declaration: Declaration) { 48 | switch declaration { 49 | case .topLevelCode(let declaration): 50 | print(declaration) 51 | case .import(let declaration): 52 | print(declaration) 53 | case .struct(let declaration): 54 | print(declaration) 55 | case .class(let declaration): 56 | print(declaration) 57 | case .enum(let declaration): 58 | print(declaration) 59 | case .extension(let declaration): 60 | print(declaration) 61 | case .variable(let declaration): 62 | print(declaration) 63 | case .function(let declaration): 64 | print(declaration) 65 | } 66 | } 67 | 68 | public func print(_ expression: Expression) { 69 | if let range = expression.range { 70 | let source = sourceFile[range] 71 | Swift.print("[\(expression.rawValue) (\(range))] \(source)") 72 | } 73 | 74 | for expression in expression.expressions { 75 | print(expression) 76 | } 77 | } 78 | 79 | public func print(_ statement: Statement) { 80 | switch statement { 81 | case .expression(let expression): 82 | print(expression) 83 | case .declaration(let declaration): 84 | print(declaration) 85 | } 86 | } 87 | 88 | public func print(_ declaration: TopLevelCodeDeclaration) { 89 | for statement in declaration.statements { 90 | switch statement { 91 | case .expression(let expression): 92 | print(expression) 93 | case .declaration(let declaration): 94 | print(declaration) 95 | } 96 | } 97 | } 98 | 99 | public func print(_ declaration: ImportDeclaration) { 100 | if let importKind = declaration.importKind { 101 | Swift.print("import \(importKind) \(declaration.importPath)") 102 | } else { 103 | Swift.print("import \(declaration.importPath)") 104 | } 105 | } 106 | 107 | public func print(_ declaration: StructDeclaration) { 108 | if let typeInheritance = declaration.typeInheritance { 109 | Swift.print("\(declaration.accessLevel) struct \(declaration.name): \(typeInheritance) {") 110 | } else { 111 | Swift.print("\(declaration.accessLevel) struct \(declaration.name) {") 112 | } 113 | for member in declaration.members { 114 | switch member { 115 | case .declaration(let declaration): 116 | print(declaration) 117 | } 118 | } 119 | Swift.print("}") 120 | } 121 | 122 | public func print(_ declaration: ClassDeclaration) { 123 | if let typeInheritance = declaration.typeInheritance { 124 | Swift.print("\(declaration.accessLevel) class \(declaration.name): \(typeInheritance) {") 125 | } else { 126 | Swift.print("\(declaration.accessLevel) class \(declaration.name) {") 127 | } 128 | for member in declaration.members { 129 | switch member { 130 | case .declaration(let declaration): 131 | print(declaration) 132 | } 133 | } 134 | Swift.print("}") 135 | } 136 | 137 | public func print(_ declaration: EnumDeclaration) { 138 | if let typeInheritance = declaration.typeInheritance { 139 | Swift.print("\(declaration.accessLevel) enum \(declaration.name): \(typeInheritance) {") 140 | } else { 141 | Swift.print("\(declaration.accessLevel) enum \(declaration.name) {") 142 | } 143 | for member in declaration.members { 144 | switch member { 145 | case .declaration(let declaration): 146 | print(declaration) 147 | } 148 | } 149 | Swift.print("}") 150 | } 151 | 152 | public func print(_ declaration: ExtensionDeclaration) { 153 | if let typeInheritance = declaration.typeInheritance { 154 | Swift.print("\(declaration.accessLevel) extension \(declaration.name): \(typeInheritance) {") 155 | } else { 156 | Swift.print("\(declaration.accessLevel) extension \(declaration.name) {") 157 | } 158 | for member in declaration.members { 159 | switch member { 160 | case .declaration(let declaration): 161 | print(declaration) 162 | } 163 | } 164 | Swift.print("}") 165 | } 166 | 167 | public func print(_ declaration: VariableDeclaration) { 168 | Swift.print("\(declaration.accessLevel) \(declaration.isLet ? "let" : "\(declaration.isImmutable ? "immutable " : " ")var") \(declaration.name): \(declaration.type) \(declaration.isImmutable ? "{ get }" : "{ get set }")") 169 | } 170 | 171 | public func print(_ declaration: FunctionDeclaration) { 172 | var parmeters = "" 173 | for parameter in declaration.parameters { 174 | if let externalName = parameter.externalName { 175 | parmeters += "\(externalName) \(parameter.localName)" 176 | } else { 177 | parmeters += "\(parameter.localName)" 178 | } 179 | parmeters += ": \(parameter.type), " 180 | } 181 | Swift.print("\(declaration.accessLevel) func \(declaration.name)(\(parmeters)) {") 182 | 183 | for statement in declaration.body { 184 | print(statement) 185 | } 186 | 187 | Swift.print("}") 188 | } 189 | } 190 | -------------------------------------------------------------------------------- /Sources/SwiftASTCore/ASTToken.swift: -------------------------------------------------------------------------------- 1 | //////////////////////////////////////////////////////////////////////////// 2 | // 3 | // Copyright 2019 Kishikawa Katsumi. 4 | // 5 | // Licensed under the Apache License, Version 2.0 (the "License"); 6 | // you may not use this file except in compliance with the License. 7 | // You may obtain a copy of the License at 8 | // 9 | // http://www.apache.org/licenses/LICENSE-2.0 10 | // 11 | // Unless required by applicable law or agreed to in writing, software 12 | // distributed under the License is distributed on an "AS IS" BASIS, 13 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | // See the License for the specific language governing permissions and 15 | // limitations under the License. 16 | // 17 | //////////////////////////////////////////////////////////////////////////// 18 | 19 | import Foundation 20 | 21 | class ASTToken { 22 | enum TokenType { 23 | case token 24 | case symbol 25 | case string 26 | case indent(Int) 27 | } 28 | 29 | var type: TokenType 30 | var value: String 31 | 32 | init(type: TokenType, value: String) { 33 | self.type = type 34 | self.value = value 35 | } 36 | } 37 | 38 | extension ASTToken: CustomStringConvertible { 39 | var description: String { 40 | switch type { 41 | case .indent(let count): 42 | return String(repeating: "_", count: count) 43 | case .token: 44 | return "\(value)" 45 | case .symbol: 46 | return "'\(value)'" 47 | case .string: 48 | return "\"\(value)\"" 49 | } 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /Sources/SwiftASTCore/ASTTokenizer.swift: -------------------------------------------------------------------------------- 1 | //////////////////////////////////////////////////////////////////////////// 2 | // 3 | // Copyright 2019 Kishikawa Katsumi. 4 | // 5 | // Licensed under the Apache License, Version 2.0 (the "License"); 6 | // you may not use this file except in compliance with the License. 7 | // You may obtain a copy of the License at 8 | // 9 | // http://www.apache.org/licenses/LICENSE-2.0 10 | // 11 | // Unless required by applicable law or agreed to in writing, software 12 | // distributed under the License is distributed on an "AS IS" BASIS, 13 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | // See the License for the specific language governing permissions and 15 | // limitations under the License. 16 | // 17 | //////////////////////////////////////////////////////////////////////////// 18 | 19 | import Foundation 20 | 21 | class ASTTokenizer { 22 | class State { 23 | enum Mode { 24 | case plain 25 | case token 26 | case value 27 | case path 28 | case array 29 | case symbol 30 | case string 31 | case stringEscape 32 | case newline 33 | case indent 34 | } 35 | 36 | var mode = Mode.plain 37 | var tokens = [ASTToken]() 38 | var storage = "" 39 | var input: String 40 | 41 | init(input: String) { 42 | self.input = input 43 | } 44 | } 45 | 46 | func tokenize(source: String) -> [ASTToken] { 47 | var lines = [String]() 48 | for line in source.split(separator: "\n", omittingEmptySubsequences: false) { 49 | let trimmed = line.trimmingCharacters(in: .whitespaces) 50 | if trimmed.hasPrefix("(inherited_conformance") || trimmed.hasPrefix("(normal_conformance") || trimmed.hasPrefix("(abstract_conformance") || 51 | trimmed.hasPrefix("(specialized_conformance") || trimmed.hasPrefix("(assoc_type") || 52 | trimmed.hasPrefix("(value req") || !trimmed.hasPrefix("(") { 53 | continue 54 | } 55 | lines.append(String(line)) 56 | } 57 | 58 | let state = State(input: lines.joined(separator: "\n")) 59 | for character in state.input { 60 | switch state.mode { 61 | case .plain: 62 | switch character { 63 | case "'": 64 | state.mode = .symbol 65 | case "\"": 66 | state.mode = .string 67 | case "\n": 68 | state.mode = .newline 69 | state.storage = "" 70 | case "(", ")", ":": 71 | state.tokens.append(ASTToken(type: .token, value: String(character))) 72 | case " ": 73 | break 74 | default: 75 | state.mode = .token 76 | state.storage = String(character) 77 | } 78 | case .token: 79 | switch character { 80 | case "'": 81 | state.tokens.append(ASTToken(type: .token, value: state.storage)) 82 | state.mode = .symbol 83 | state.storage = "" 84 | case "\"": 85 | state.tokens.append(ASTToken(type: .token, value: state.storage)) 86 | state.mode = .string 87 | state.storage = "" 88 | case " ": 89 | state.tokens.append(ASTToken(type: .token, value: state.storage)) 90 | state.mode = .plain 91 | state.storage = "" 92 | case "\n": 93 | state.tokens.append(ASTToken(type: .token, value: state.storage)) 94 | state.mode = .newline 95 | state.storage = "" 96 | case "=": 97 | state.tokens.append(ASTToken(type: .token, value: state.storage)) 98 | state.tokens.append(ASTToken(type: .token, value: String(character))) 99 | if state.storage == "location" { 100 | state.mode = .path 101 | } else { 102 | state.mode = .value 103 | } 104 | state.storage = "" 105 | case "(", ")", ":": 106 | state.tokens.append(ASTToken(type: .token, value: state.storage)) 107 | state.tokens.append(ASTToken(type: .token, value: String(character))) 108 | state.mode = .plain 109 | state.storage = "" 110 | default: 111 | state.storage += String(character) 112 | } 113 | case .value: 114 | switch character { 115 | case "[": 116 | state.mode = .array 117 | case " ": 118 | state.tokens.append(ASTToken(type: .token, value: state.storage)) 119 | state.mode = .plain 120 | state.storage = "" 121 | case "'": 122 | state.mode = .symbol 123 | case "\"": 124 | state.mode = .string 125 | case "\n": 126 | state.tokens.append(ASTToken(type: .token, value: state.storage)) 127 | state.mode = .newline 128 | state.storage = "" 129 | default: 130 | state.storage += String(character) 131 | } 132 | case .path: 133 | switch character { 134 | case "[": 135 | state.mode = .array 136 | case " ": 137 | if state.storage.contains(":") { 138 | state.tokens.append(ASTToken(type: .token, value: state.storage)) 139 | state.mode = .plain 140 | state.storage = "" 141 | } else { 142 | state.storage += String(character) 143 | } 144 | case "'": 145 | state.mode = .symbol 146 | case "\"": 147 | state.mode = .string 148 | case "\n": 149 | state.tokens.append(ASTToken(type: .token, value: state.storage)) 150 | state.mode = .newline 151 | state.storage = "" 152 | default: 153 | state.storage += String(character) 154 | } 155 | case .array: 156 | switch character { 157 | case "]": 158 | state.tokens.append(ASTToken(type: .token, value: state.storage)) 159 | state.mode = .plain 160 | state.storage = "" 161 | default: 162 | state.storage += String(character) 163 | } 164 | case .symbol: 165 | switch character { 166 | case "'": 167 | state.tokens.append(ASTToken(type: .symbol, value: state.storage)) 168 | state.mode = .plain 169 | state.storage = "" 170 | default: 171 | state.storage += String(character) 172 | } 173 | case .string: 174 | switch character { 175 | case "\"": 176 | state.tokens.append(ASTToken(type: .string, value: state.storage)) 177 | state.mode = .plain 178 | state.storage = "" 179 | case "\\": 180 | state.mode = .stringEscape 181 | default: 182 | state.storage += String(character) 183 | } 184 | case .stringEscape: 185 | switch character { 186 | case "\"", "\\", "'", "t", "n", "r", "0": 187 | state.mode = .string 188 | state.storage += "\\" + String(character) 189 | default: 190 | fatalError("unexpected '\(character)' in string escape") 191 | } 192 | case .newline: 193 | switch character { 194 | case " ": 195 | state.mode = .indent 196 | state.storage = String(character) 197 | case "(", ")", ":": 198 | state.tokens.append(ASTToken(type: .token, value: String(character))) 199 | state.mode = .plain 200 | case "\n": 201 | break 202 | default: 203 | state.mode = .token 204 | state.storage = String(character) 205 | } 206 | case .indent: 207 | switch character { 208 | case " ": 209 | state.storage += " " 210 | case "\n": 211 | state.mode = .newline 212 | state.storage = "" 213 | case "(", ")", ":": 214 | state.tokens.append(ASTToken(type: .indent(state.storage.count), value: state.storage)) 215 | state.tokens.append(ASTToken(type: .token, value: String(character))) 216 | state.mode = .plain 217 | default: 218 | state.tokens.append(ASTToken(type: .indent(state.storage.count), value: state.storage)) 219 | state.mode = .token 220 | state.storage = String(character) 221 | } 222 | } 223 | } 224 | if !state.storage.isEmpty { 225 | switch state.mode { 226 | case .token, .value, .path: 227 | state.tokens.append(ASTToken(type: .token, value: state.storage)) 228 | case .symbol: 229 | state.tokens.append(ASTToken(type: .symbol, value: state.storage)) 230 | case .string: 231 | state.tokens.append(ASTToken(type: .string, value: state.storage)) 232 | default: 233 | break 234 | } 235 | } 236 | return state.tokens 237 | } 238 | } 239 | -------------------------------------------------------------------------------- /Sources/SwiftASTCore/Errors.swift: -------------------------------------------------------------------------------- 1 | //////////////////////////////////////////////////////////////////////////// 2 | // 3 | // Copyright 2019 Kishikawa Katsumi. 4 | // 5 | // Licensed under the Apache License, Version 2.0 (the "License"); 6 | // you may not use this file except in compliance with the License. 7 | // You may obtain a copy of the License at 8 | // 9 | // http://www.apache.org/licenses/LICENSE-2.0 10 | // 11 | // Unless required by applicable law or agreed to in writing, software 12 | // distributed under the License is distributed on an "AS IS" BASIS, 13 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | // See the License for the specific language governing permissions and 15 | // limitations under the License. 16 | // 17 | //////////////////////////////////////////////////////////////////////////// 18 | 19 | import Foundation 20 | 21 | public enum SwiftASTError: Error { 22 | case executingSubprocessFailed(command: String, output: String) 23 | } 24 | 25 | extension SwiftASTError: CustomStringConvertible { 26 | public var description: String { 27 | switch self { 28 | case .executingSubprocessFailed(let command, let output): 29 | var message = "failed to run the following command: '\(command)'" 30 | if !output.isEmpty { message += " output: \(output)" } 31 | return message 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /Sources/SwiftASTCore/ProcessManager.swift: -------------------------------------------------------------------------------- 1 | //////////////////////////////////////////////////////////////////////////// 2 | // 3 | // Copyright 2019 Kishikawa Katsumi. 4 | // 5 | // Licensed under the Apache License, Version 2.0 (the "License"); 6 | // you may not use this file except in compliance with the License. 7 | // You may obtain a copy of the License at 8 | // 9 | // http://www.apache.org/licenses/LICENSE-2.0 10 | // 11 | // Unless required by applicable law or agreed to in writing, software 12 | // distributed under the License is distributed on an "AS IS" BASIS, 13 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | // See the License for the specific language governing permissions and 15 | // limitations under the License. 16 | // 17 | //////////////////////////////////////////////////////////////////////////// 18 | 19 | import Foundation 20 | import Basic 21 | import Utility 22 | 23 | public final class ProcessManager { 24 | public static let `default` = ProcessManager() 25 | 26 | let processSet: ProcessSet 27 | let interruptHandler: InterruptHandler 28 | 29 | private init() { 30 | let processSet = ProcessSet() 31 | interruptHandler = try! InterruptHandler { 32 | processSet.terminate() 33 | var action = sigaction() 34 | #if os(macOS) 35 | action.__sigaction_u.__sa_handler = SIG_DFL 36 | #else 37 | action.__sigaction_handler = unsafeBitCast(SIG_DFL, to: sigaction.__Unnamed_union___sigaction_handler.self) 38 | #endif 39 | sigaction(SIGINT, &action, nil) 40 | kill(getpid(), SIGINT) 41 | } 42 | self.processSet = processSet 43 | } 44 | 45 | public func add(process: Basic.Process) { 46 | try! processSet.add(process) 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /Sources/SwiftASTCore/Replacement.swift: -------------------------------------------------------------------------------- 1 | //////////////////////////////////////////////////////////////////////////// 2 | // 3 | // Copyright 2019 Kishikawa Katsumi. 4 | // 5 | // Licensed under the Apache License, Version 2.0 (the "License"); 6 | // you may not use this file except in compliance with the License. 7 | // You may obtain a copy of the License at 8 | // 9 | // http://www.apache.org/licenses/LICENSE-2.0 10 | // 11 | // Unless required by applicable law or agreed to in writing, software 12 | // distributed under the License is distributed on an "AS IS" BASIS, 13 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | // See the License for the specific language governing permissions and 15 | // limitations under the License. 16 | // 17 | //////////////////////////////////////////////////////////////////////////// 18 | 19 | import Foundation 20 | 21 | struct Replacement { 22 | let sourceRange: SourceRange 23 | let sourceText: String 24 | } 25 | -------------------------------------------------------------------------------- /Sources/SwiftASTCore/SourceFile.swift: -------------------------------------------------------------------------------- 1 | //////////////////////////////////////////////////////////////////////////// 2 | // 3 | // Copyright 2019 Kishikawa Katsumi. 4 | // 5 | // Licensed under the Apache License, Version 2.0 (the "License"); 6 | // you may not use this file except in compliance with the License. 7 | // You may obtain a copy of the License at 8 | // 9 | // http://www.apache.org/licenses/LICENSE-2.0 10 | // 11 | // Unless required by applicable law or agreed to in writing, software 12 | // distributed under the License is distributed on an "AS IS" BASIS, 13 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | // See the License for the specific language governing permissions and 15 | // limitations under the License. 16 | // 17 | //////////////////////////////////////////////////////////////////////////// 18 | 19 | import Foundation 20 | 21 | struct SourceFile { 22 | let sourceText: String.UTF8View 23 | let sourceLines: [SourceLine] 24 | 25 | subscript(range: SourceRange) -> String { 26 | let start = range.start 27 | let end = range.end 28 | let startIndex: String.Index 29 | if start.line > 0 { 30 | startIndex = sourceText.index(sourceText.startIndex, offsetBy: sourceLines[start.line].offset + start.column) 31 | } else { 32 | startIndex = sourceText.index(sourceText.startIndex, offsetBy: start.column) 33 | } 34 | let endIndex: String.Index 35 | if end.line > 0 { 36 | endIndex = sourceText.index(sourceText.startIndex, offsetBy: sourceLines[end.line].offset + end.column) 37 | } else { 38 | endIndex = sourceText.index(sourceText.startIndex, offsetBy: end.column) 39 | } 40 | return String(String(sourceText)[startIndex.. AST { 36 | return try process(sourceFile: input, verbose: verbose) 37 | } 38 | 39 | private func process(sourceFile: URL, verbose: Bool = false) throws -> AST { 40 | let arguments = buildArguments(source: sourceFile) 41 | let rawAST = try dumpAST(arguments: arguments) 42 | let tokens = tokenize(rawAST: rawAST) 43 | let node = lex(tokens: tokens) 44 | let root = parse(node: node) 45 | return root 46 | } 47 | 48 | private func buildArguments(source: URL) -> [String] { 49 | #if os(macOS) 50 | let exec = ["/usr/bin/xcrun", "swift"] 51 | #else 52 | let exec = ["swift"] 53 | #endif 54 | let arguments = exec + [ 55 | "-frontend", 56 | "-suppress-warnings", 57 | "-dump-ast" 58 | ] 59 | let importObjcHeaderOption: [String] 60 | if let bridgingHeader = bridgingHeader { 61 | importObjcHeaderOption = ["-import-objc-header", bridgingHeader.path] 62 | } else { 63 | importObjcHeaderOption = [] 64 | } 65 | return arguments + buildOptions + importObjcHeaderOption + ["-primary-file", source.path] + dependencies.map { $0.path } 66 | } 67 | 68 | private func dumpAST(arguments: [String]) throws -> String { 69 | let process = Process(arguments: arguments) 70 | try! process.launch() 71 | ProcessManager.default.add(process: process) 72 | let result = try process.waitUntilExit() 73 | let output = try result.utf8Output() 74 | switch result.exitStatus { 75 | case .terminated(let code) where code == 0: 76 | return output 77 | default: 78 | let command = process.arguments.map { $0.shellEscaped() }.joined(separator: " ") 79 | let errorOutput = try result.utf8stderrOutput().split(separator: "\n").prefix(1).joined(separator: "\n") 80 | throw SwiftASTError.executingSubprocessFailed(command: command, output: errorOutput) 81 | } 82 | } 83 | 84 | private func tokenize(rawAST: String) -> [ASTToken] { 85 | let tokenizer = ASTTokenizer() 86 | return tokenizer.tokenize(source: rawAST) 87 | } 88 | 89 | private func lex(tokens: [ASTToken]) -> ASTNode<[ASTToken]> { 90 | let lexer = ASTLexer() 91 | return lexer.lex(tokens: tokens) 92 | } 93 | 94 | private func parse(node: ASTNode<[ASTToken]>) -> AST { 95 | let parser = ASTParser() 96 | return parser.parse(root: node) 97 | } 98 | } 99 | 100 | public enum SDK { 101 | case macosx 102 | case iphoneos 103 | case iphonesimulator 104 | case watchos 105 | case watchsimulator 106 | case appletvos 107 | case appletvsimulator 108 | 109 | public var name: String { 110 | switch self { 111 | case .macosx: 112 | return "macosx" 113 | case .iphoneos: 114 | return "iphoneos" 115 | case .iphonesimulator: 116 | return "iphonesimulator" 117 | case .watchos: 118 | return "watchos" 119 | case .watchsimulator: 120 | return "watchsimulator" 121 | case .appletvos: 122 | return "appletvos" 123 | case .appletvsimulator: 124 | return "appletvsimulator" 125 | } 126 | } 127 | 128 | public var os: String { 129 | switch self { 130 | case .macosx: 131 | return "macosx" 132 | case .iphoneos, .iphonesimulator: 133 | return "ios" 134 | case .watchos, .watchsimulator: 135 | return "watchos" 136 | case .appletvos, .appletvsimulator: 137 | return "tvos" 138 | } 139 | } 140 | 141 | public func path() throws -> String { 142 | let process = Process(arguments: ["/usr/bin/xcrun", "--sdk", name, "--show-sdk-path"]) 143 | try! process.launch() 144 | ProcessManager.default.add(process: process) 145 | let result = try! process.waitUntilExit() 146 | let output = try! result.utf8Output() 147 | switch result.exitStatus { 148 | case .terminated(let code) where code == 0: 149 | return output.trimmingCharacters(in: .whitespacesAndNewlines) 150 | default: 151 | throw SwiftASTError.executingSubprocessFailed(command: process.arguments.joined(separator: " "), output: try result.utf8stderrOutput()) 152 | } 153 | } 154 | 155 | public func version() throws -> String { 156 | let process = Process(arguments: ["/usr/bin/xcrun", "--sdk", name, "--show-sdk-version"]) 157 | try! process.launch() 158 | ProcessManager.default.add(process: process) 159 | let result = try! process.waitUntilExit() 160 | let output = try! result.utf8Output() 161 | switch result.exitStatus { 162 | case .terminated(let code) where code == 0: 163 | return output.trimmingCharacters(in: .whitespacesAndNewlines) 164 | default: 165 | throw SwiftASTError.executingSubprocessFailed(command: process.arguments.joined(separator: " "), output: try result.utf8stderrOutput()) 166 | } 167 | } 168 | } 169 | -------------------------------------------------------------------------------- /Tests/LinuxMain.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | 3 | import SwiftASTTests 4 | 5 | var tests = [XCTestCaseEntry]() 6 | tests += SwiftASTTests.allTests() 7 | XCTMain(tests) 8 | -------------------------------------------------------------------------------- /Tests/SwiftASTTests/ParserTests.swift: -------------------------------------------------------------------------------- 1 | //////////////////////////////////////////////////////////////////////////// 2 | // 3 | // Copyright 2017 Kishikawa Katsumi. 4 | // 5 | // Licensed under the Apache License, Version 2.0 (the "License"); 6 | // you may not use this file except in compliance with the License. 7 | // You may obtain a copy of the License at 8 | // 9 | // http://www.apache.org/licenses/LICENSE-2.0 10 | // 11 | // Unless required by applicable law or agreed to in writing, software 12 | // distributed under the License is distributed on an "AS IS" BASIS, 13 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | // See the License for the specific language governing permissions and 15 | // limitations under the License. 16 | // 17 | //////////////////////////////////////////////////////////////////////////// 18 | 19 | import XCTest 20 | import Basic 21 | @testable import SwiftASTCore 22 | 23 | class ParserTests: XCTestCase { 24 | func testParser() { 25 | let env = TestEnvironments() 26 | 27 | let source = """ 28 | import class Foundation.NSArray 29 | import class Foundation.NSDictionary 30 | import XCTest 31 | 32 | protocol Testable {} 33 | extension Testable { 34 | func testInt() { 35 | let i = 0 36 | let j = 1 37 | let k = 2 38 | XCTAssertEqual(i, j) 39 | } 40 | func testInt32() { 41 | let i = 0 42 | let j = 1 43 | let k = 2 44 | XCTAssertEqual(i, j) 45 | } 46 | func testInt64() { 47 | let i = 0 48 | let j = 1 49 | let k = 2 50 | XCTAssertEqual(i, j) 51 | } 52 | } 53 | 54 | private extension XCTestCase: Testable { 55 | let a = "a" 56 | let b = "b" 57 | let c = "c" 58 | 59 | private func testString() { 60 | let d = "d" 61 | let f = "f" 62 | XCTAssertEqual(a, b) 63 | XCTAssertEqual(c, d) 64 | } 65 | } 66 | 67 | private struct Helper: Testable { 68 | func generateData() -> [String] { 69 | return ["a", "b", "c"] 70 | } 71 | } 72 | 73 | public class Tests: XCTestCase, Testable { 74 | func test1() { 75 | let a = "a" 76 | let b = "b" 77 | let c = "c" 78 | let d = "d" 79 | XCTAssertEqual(a, b) 80 | XCTAssertEqual(c, d) 81 | } 82 | func test2() { 83 | let a = "a" 84 | let b = "b" 85 | XCTAssertEqual(a, b) 86 | } 87 | } 88 | 89 | func test() {} 90 | 91 | enum TestSuite { 92 | case target(String) 93 | case bundle 94 | 95 | func test() { 96 | let a = "a" 97 | let b = "b" 98 | let c = "c" 99 | let d = "d" 100 | XCTAssertEqual(a, b) 101 | XCTAssertEqual(c, d) 102 | } 103 | } 104 | 105 | """ 106 | 107 | try! source.write(toFile: env.sourceFilePath, atomically: true, encoding: .utf8) 108 | 109 | #if os(macOS) 110 | let exec = ["/usr/bin/xcrun", "swift"] 111 | #else 112 | let exec = ["swift"] 113 | #endif 114 | let arguments = exec + [ 115 | "-frontend", 116 | "-parse-as-library", 117 | "-dump-ast" 118 | ] + env.parseOptions + ["-primary-file", env.sourceFilePath] 119 | 120 | let process = Process(arguments: arguments) 121 | try! process.launch() 122 | ProcessManager.default.add(process: process) 123 | let result = try! process.waitUntilExit() 124 | let rawAST = try! result.utf8Output() 125 | print(rawAST) 126 | 127 | let tokenizer = ASTTokenizer() 128 | let tokens = tokenizer.tokenize(source: rawAST) 129 | 130 | let lexer = ASTLexer() 131 | let node = lexer.lex(tokens: tokens) 132 | 133 | let parser = ASTParser() 134 | let root = parser.parse(root: node) 135 | 136 | if case .import(let declaration) = root.declarations[0] { 137 | XCTAssertEqual(declaration.importKind, "class") 138 | XCTAssertEqual(declaration.importPath, "Foundation.NSArray") 139 | } else { 140 | XCTFail() 141 | } 142 | if case .import(let declaration) = root.declarations[1] { 143 | XCTAssertEqual(declaration.importKind, "class") 144 | XCTAssertEqual(declaration.importPath, "Foundation.NSDictionary") 145 | } else { 146 | XCTFail() 147 | } 148 | if case .import(let declaration) = root.declarations[2] { 149 | XCTAssertNil(declaration.importKind) 150 | XCTAssertEqual(declaration.importPath, "XCTest") 151 | } else { 152 | XCTFail() 153 | } 154 | 155 | if case .extension(let declaration) = root.declarations[3] { 156 | XCTAssertEqual(declaration.accessLevel, "internal") 157 | XCTAssertEqual(declaration.name, "Testable") 158 | XCTAssertNil(declaration.typeInheritance) 159 | 160 | XCTAssertEqual(declaration.members.count, 3) 161 | if case .declaration(let declaration) = declaration.members[0] { 162 | if case .function(let declaration) = declaration { 163 | XCTAssertEqual(declaration.accessLevel, "internal") 164 | XCTAssertEqual(declaration.name, "testInt()") 165 | } else { 166 | XCTFail() 167 | } 168 | } else { 169 | XCTFail() 170 | } 171 | if case .declaration(let declaration) = declaration.members[1] { 172 | if case .function(let declaration) = declaration { 173 | XCTAssertEqual(declaration.accessLevel, "internal") 174 | XCTAssertEqual(declaration.name, "testInt32()") 175 | } else { 176 | XCTFail() 177 | } 178 | } else { 179 | XCTFail() 180 | } 181 | if case .declaration(let declaration) = declaration.members[2] { 182 | if case .function(let declaration) = declaration { 183 | XCTAssertEqual(declaration.accessLevel, "internal") 184 | XCTAssertEqual(declaration.name, "testInt64()") 185 | } else { 186 | XCTFail() 187 | } 188 | } else { 189 | XCTFail() 190 | } 191 | } else { 192 | XCTFail() 193 | } 194 | 195 | if case .extension(let declaration) = root.declarations[4] { 196 | XCTAssertEqual(declaration.accessLevel, "internal") 197 | XCTAssertEqual(declaration.name, "XCTestCase") 198 | XCTAssertEqual(declaration.typeInheritance, "Testable") 199 | 200 | XCTAssertEqual(declaration.members.count, 1) 201 | if case .declaration(let declaration) = declaration.members[0] { 202 | if case .function(let declaration) = declaration { 203 | XCTAssertEqual(declaration.accessLevel, "private") 204 | XCTAssertEqual(declaration.name, "testString()") 205 | } else { 206 | XCTFail() 207 | } 208 | } else { 209 | XCTFail() 210 | } 211 | } else { 212 | XCTFail() 213 | } 214 | 215 | if case .struct(let declaration) = root.declarations[5] { 216 | XCTAssertEqual(declaration.accessLevel, "private") 217 | XCTAssertEqual(declaration.name, "Helper") 218 | XCTAssertEqual(declaration.typeInheritance, "Testable") 219 | 220 | XCTAssertEqual(declaration.members.count, 1) 221 | if case .declaration(let declaration) = declaration.members[0] { 222 | if case .function(let declaration) = declaration { 223 | XCTAssertEqual(declaration.accessLevel, "internal") 224 | XCTAssertEqual(declaration.name, "generateData()") 225 | } else { 226 | XCTFail() 227 | } 228 | } else { 229 | XCTFail() 230 | } 231 | } else { 232 | XCTFail() 233 | } 234 | 235 | if case .class(let declaration) = root.declarations[6] { 236 | XCTAssertEqual(declaration.accessLevel, "public") 237 | XCTAssertEqual(declaration.name, "Tests") 238 | XCTAssertEqual(declaration.typeInheritance, "XCTestCase, Testable") 239 | 240 | XCTAssertEqual(declaration.members.count, 2) 241 | if case .declaration(let declaration) = declaration.members[0] { 242 | if case .function(let declaration) = declaration { 243 | XCTAssertEqual(declaration.accessLevel, "internal") 244 | XCTAssertEqual(declaration.name, "test1()") 245 | } else { 246 | XCTFail() 247 | } 248 | } else { 249 | XCTFail() 250 | } 251 | if case .declaration(let declaration) = declaration.members[1] { 252 | if case .function(let declaration) = declaration { 253 | XCTAssertEqual(declaration.accessLevel, "internal") 254 | XCTAssertEqual(declaration.name, "test2()") 255 | } else { 256 | XCTFail() 257 | } 258 | } else { 259 | XCTFail() 260 | } 261 | } else { 262 | XCTFail() 263 | } 264 | 265 | if case .function(let declaration) = root.declarations[7] { 266 | XCTAssertEqual(declaration.accessLevel, "internal") 267 | XCTAssertEqual(declaration.name, "test()") 268 | } else { 269 | XCTFail() 270 | } 271 | 272 | if case .enum(let declaration) = root.declarations[8] { 273 | XCTAssertEqual(declaration.accessLevel, "internal") 274 | XCTAssertEqual(declaration.name, "TestSuite") 275 | XCTAssertNil(declaration.typeInheritance) 276 | 277 | XCTAssertEqual(declaration.members.count, 1) 278 | if case .declaration(let declaration) = declaration.members[0] { 279 | if case .function(let declaration) = declaration { 280 | XCTAssertEqual(declaration.accessLevel, "internal") 281 | XCTAssertEqual(declaration.name, "test()") 282 | } else { 283 | XCTFail() 284 | } 285 | } else { 286 | XCTFail() 287 | } 288 | } else { 289 | XCTFail() 290 | } 291 | } 292 | } 293 | -------------------------------------------------------------------------------- /Tests/SwiftASTTests/TestEnvironments.swift: -------------------------------------------------------------------------------- 1 | //////////////////////////////////////////////////////////////////////////// 2 | // 3 | // Copyright 2017 Kishikawa Katsumi. 4 | // 5 | // Licensed under the Apache License, Version 2.0 (the "License"); 6 | // you may not use this file except in compliance with the License. 7 | // You may obtain a copy of the License at 8 | // 9 | // http://www.apache.org/licenses/LICENSE-2.0 10 | // 11 | // Unless required by applicable law or agreed to in writing, software 12 | // distributed under the License is distributed on an "AS IS" BASIS, 13 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | // See the License for the specific language governing permissions and 15 | // limitations under the License. 16 | // 17 | //////////////////////////////////////////////////////////////////////////// 18 | 19 | import Foundation 20 | import Basic 21 | import SwiftASTCore 22 | 23 | class TestEnvironments { 24 | lazy var temporaryDirectory = { 25 | return try! TemporaryDirectory(prefix: "com.kishikawakatsumi.swift-power-assert", removeTreeOnDeinit: true) 26 | }() 27 | lazy var temporaryFile = { 28 | return try! TemporaryFile(dir: temporaryDirectory.path, prefix: "Tests", suffix: ".swift").path 29 | }() 30 | lazy var sourceFilePath = { 31 | return temporaryFile.asString 32 | }() 33 | lazy var utilitiesFilePath = { 34 | return temporaryDirectory.path.appending(component: "Utilities.swift").asString 35 | }() 36 | lazy var mainFilePath = { 37 | return temporaryDirectory.path.appending(component: "main.swift").asString 38 | }() 39 | lazy var executablePath = { 40 | return sourceFilePath + ".o" 41 | }() 42 | lazy var sdk = { 43 | return try! SDK.macosx.path() 44 | }() 45 | var targetTriple: String { 46 | #if os(macOS) 47 | return "x86_64-apple-macosx10.10" 48 | #else 49 | return "x86_64-unknown-linux" 50 | #endif 51 | } 52 | lazy var parseOptions: [String] = { 53 | let buildDirectory = temporaryDirectory.path.asString 54 | #if os(macOS) 55 | return [ 56 | "-sdk", 57 | sdk, 58 | "-target", 59 | targetTriple, 60 | "-F", 61 | sdk + "/../../../Developer/Library/Frameworks", 62 | "-F", 63 | buildDirectory, 64 | "-I", 65 | buildDirectory 66 | ] 67 | #else 68 | return [ 69 | "-target", 70 | targetTriple, 71 | "-F", 72 | buildDirectory, 73 | "-I", 74 | buildDirectory 75 | ] 76 | #endif 77 | }() 78 | lazy var execOptions: [String] = { 79 | #if os(macOS) 80 | return [ 81 | "/usr/bin/xcrun", 82 | "swiftc", 83 | "-O", 84 | "-whole-module-optimization", 85 | sourceFilePath, 86 | utilitiesFilePath, 87 | mainFilePath, 88 | "-o", 89 | executablePath, 90 | "-target", 91 | targetTriple, 92 | "-sdk", 93 | sdk, 94 | "-F", 95 | "\(sdk)/../../../Developer/Library/Frameworks", 96 | "-Xlinker", 97 | "-rpath", 98 | "-Xlinker", 99 | "\(sdk)/../../../Developer/Library/Frameworks", 100 | ] 101 | #else 102 | return [ 103 | "swiftc", 104 | "-O", 105 | "-whole-module-optimization", 106 | sourceFilePath, 107 | utilitiesFilePath, 108 | mainFilePath, 109 | "-o", 110 | executablePath, 111 | "-target", 112 | targetTriple, 113 | "-Xlinker", 114 | "-rpath", 115 | ] 116 | #endif 117 | }() 118 | } 119 | -------------------------------------------------------------------------------- /Tests/SwiftASTTests/XCTestManifests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | 3 | #if !canImport(ObjectiveC) 4 | public func allTests() -> [XCTestCaseEntry] { 5 | return [ 6 | testCase(SwiftASTTests.allTests), 7 | ] 8 | } 9 | #endif 10 | 11 | --------------------------------------------------------------------------------