├── .gitignore ├── main-illustration.png ├── spm_resolve.command ├── spm_generate_xcodeproj.command ├── Sources └── Synopsis │ ├── Model │ ├── ParsingResult.swift │ ├── GoodFile.swift │ ├── CompiledStructure.swift │ ├── SynopsisError.swift │ ├── Accessibility.swift │ ├── Annotation.swift │ ├── Extensible.swift │ ├── XcodeMessage.swift │ ├── EnumCase.swift │ ├── MethodDescription.swift │ ├── Declaration.swift │ ├── ArgumentDescription.swift │ ├── ClassDescription.swift │ ├── StructDescription.swift │ ├── ProtocolDescription.swift │ ├── PropertyDescription.swift │ ├── TypeDescription.swift │ ├── EnumDescription.swift │ └── FunctionDescription.swift │ ├── Utility │ ├── ClassDescriptionParser.swift │ ├── StructDescriptionParser.swift │ ├── ProtocolDescriptionParser.swift │ ├── MethodDescriptionParser.swift │ ├── AnnotationParser.swift │ ├── SwiftDocKey.swift │ ├── EnumCaseParser.swift │ ├── ArgumentDescriptionParser.swift │ ├── ExtensibleParser.swift │ ├── EnumDescriptionParser.swift │ ├── PropertyDescriptionParser.swift │ ├── String.swift │ ├── FunctionDescriptionParser.swift │ ├── TypeParser.swift │ └── LexemeString.swift │ └── Synopsis.swift ├── Tests ├── LinuxMain.swift └── SynopsisTests │ ├── Versing │ ├── EnumDescriptionVersingTests.swift │ ├── ArgumentDescriptionVersingTests.swift │ ├── PropertyDescriptionVersingTests.swift │ ├── ClassDescriptionVersingTests.swift │ └── MethodDescriptionVersingTests.swift │ ├── SynopsisTestCase.swift │ └── Utility │ ├── EnumDescriptionParserTests.swift │ ├── MethodDescriptionParserTests.swift │ ├── StructDescriptionParserTests.swift │ ├── ProtocolDescriptionParserTests.swift │ └── ClassDescriptionParserTests.swift ├── Package.swift ├── LICENSE └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | /.build 3 | /Packages 4 | /*.xcodeproj 5 | -------------------------------------------------------------------------------- /main-illustration.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RedMadRobot/synopsis/HEAD/main-illustration.png -------------------------------------------------------------------------------- /spm_resolve.command: -------------------------------------------------------------------------------- 1 | DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" 2 | cd "$DIR" 3 | 4 | swift package resolve 5 | -------------------------------------------------------------------------------- /spm_generate_xcodeproj.command: -------------------------------------------------------------------------------- 1 | DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" 2 | cd "$DIR" 3 | 4 | swift package resolve 5 | swift package generate-xcodeproj 6 | 7 | open Synopsis.xcodeproj 8 | -------------------------------------------------------------------------------- /Sources/Synopsis/Model/ParsingResult.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Project «Synopsis» 3 | // Created by Jeorge Taflanidi 4 | // 5 | 6 | 7 | import Foundation 8 | 9 | 10 | struct ParsingResult { 11 | let errors: [SynopsisError] 12 | let models: [ParsedModel] 13 | } 14 | -------------------------------------------------------------------------------- /Sources/Synopsis/Model/GoodFile.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Project «Synopsis» 3 | // Created by Jeorge Taflanidi 4 | // 5 | 6 | 7 | import Foundation 8 | import SourceKittenFramework 9 | 10 | 11 | /** 12 | `SourceKittenFramework.File` abstraction assumes there could be no `path`, which is not suitable 13 | for `Synopsis`. 14 | */ 15 | struct GoodFile { 16 | let path: URL 17 | let file: File 18 | } 19 | -------------------------------------------------------------------------------- /Sources/Synopsis/Utility/ClassDescriptionParser.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Project «Synopsis» 3 | // Created by Jeorge Taflanidi 4 | // 5 | 6 | 7 | import Foundation 8 | import SourceKittenFramework 9 | 10 | 11 | class ClassDescriptionParser: ExtensibleParser { 12 | 13 | override func isRawExtensibleDescription(_ element: [String : AnyObject]) -> Bool { 14 | guard let kind: String = element.kind 15 | else { return false} 16 | return SwiftDeclarationKind.`class`.rawValue == kind 17 | } 18 | 19 | } 20 | -------------------------------------------------------------------------------- /Sources/Synopsis/Utility/StructDescriptionParser.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Project «Synopsis» 3 | // Created by Jeorge Taflanidi 4 | // 5 | 6 | 7 | import Foundation 8 | import SourceKittenFramework 9 | 10 | 11 | class StructDescriptionParser: ExtensibleParser { 12 | 13 | override func isRawExtensibleDescription(_ element: [String : AnyObject]) -> Bool { 14 | guard let kind: String = element.kind 15 | else { return false} 16 | return SwiftDeclarationKind.`struct`.rawValue == kind 17 | } 18 | 19 | } 20 | -------------------------------------------------------------------------------- /Sources/Synopsis/Utility/ProtocolDescriptionParser.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Project «Synopsis» 3 | // Created by Jeorge Taflanidi 4 | // 5 | 6 | 7 | import Foundation 8 | import SourceKittenFramework 9 | 10 | 11 | class ProtocolDescriptionParser: ExtensibleParser { 12 | 13 | override func isRawExtensibleDescription(_ element: [String : AnyObject]) -> Bool { 14 | guard let kind: String = element.kind 15 | else { return false} 16 | return SwiftDeclarationKind.`protocol`.rawValue == kind 17 | } 18 | 19 | } 20 | -------------------------------------------------------------------------------- /Tests/LinuxMain.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | @testable import SynopsisTests 3 | 4 | XCTMain([ 5 | testCase(ClassDescriptionParserTests.allTests), 6 | testCase(EnumDescriptionParserTests.allTests), 7 | testCase(MethodDescriptionParserTests.allTests), 8 | testCase(ProtocolDescriptionParserTests.allTests), 9 | testCase(StructDescriptionParserTests.allTests), 10 | testCase(ArgumentDescriptionVersingTests.allTests), 11 | testCase(ClassDescriptionVersingTests.allTests), 12 | testCase(EnumDescriptionVersingTests.allTests), 13 | testCase(MethodDescriptionVersingTests.allTests), 14 | testCase(PropertyDescriptionVersingTests.allTests), 15 | ]) 16 | -------------------------------------------------------------------------------- /Sources/Synopsis/Utility/MethodDescriptionParser.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Project «Synopsis» 3 | // Created by Jeorge Taflanidi 4 | // 5 | 6 | 7 | import Foundation 8 | import SourceKittenFramework 9 | 10 | 11 | class MethodDescriptionParser: FunctionDescriptionParser { 12 | override func isRawFunctionDescription(_ element: [String : AnyObject]) -> Bool { 13 | guard let kind: String = element.kind 14 | else { return false } 15 | return SwiftDeclarationKind.functionMethodInstance.rawValue == kind 16 | || SwiftDeclarationKind.functionMethodStatic.rawValue == kind 17 | || SwiftDeclarationKind.functionMethodClass.rawValue == kind 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version:4.0 2 | 3 | 4 | import PackageDescription 5 | 6 | 7 | let package = Package( 8 | name: "Synopsis", 9 | products: [ 10 | Product.library( 11 | name: "Synopsis", 12 | targets: ["Synopsis"] 13 | ), 14 | ], 15 | dependencies: [ 16 | Package.Dependency.package( 17 | url: "https://github.com/jpsim/SourceKitten", 18 | from: "0.18.0" 19 | ), 20 | ], 21 | targets: [ 22 | Target.target( 23 | name: "Synopsis", 24 | dependencies: ["SourceKittenFramework"] 25 | ), 26 | Target.testTarget( 27 | name: "SynopsisTests", 28 | dependencies: ["Synopsis"] 29 | ), 30 | ] 31 | ) 32 | -------------------------------------------------------------------------------- /Sources/Synopsis/Model/CompiledStructure.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Project «Synopsis» 3 | // Created by Jeorge Taflanidi 4 | // 5 | 6 | 7 | import Foundation 8 | import SourceKittenFramework 9 | 10 | 11 | struct CompiledStructure { 12 | let file: GoodFile 13 | let structure: SwiftDocs 14 | 15 | var topElements: [[String: AnyObject]] { 16 | return structure.docsDictionary.subsctructure 17 | } 18 | 19 | init(structure: SwiftDocs, file: GoodFile) { 20 | self.structure = structure 21 | self.file = file 22 | } 23 | 24 | init?(file: GoodFile) { 25 | if let docs: SwiftDocs = SwiftDocs(file: file.file, arguments: [CompilerArgument.buildSymbols, file.path.path]) { 26 | self.init(structure: docs, file: file) 27 | return 28 | } 29 | 30 | return nil 31 | } 32 | 33 | private enum CompilerArgument { 34 | static let buildSymbols: String = "-j4" 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /Sources/Synopsis/Model/SynopsisError.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Project «Synopsis» 3 | // Created by Jeorge Taflanidi 4 | // 5 | 6 | 7 | import Foundation 8 | 9 | 10 | /** 11 | Synopsis library makes a lot of work with files. 12 | 13 | This means I/O errors and content parsing errors. 14 | 15 | Thus, this structure was created. 16 | */ 17 | public struct SynopsisError { 18 | /** 19 | What happened? 20 | */ 21 | public let description: String 22 | 23 | /** 24 | Where happened? 25 | */ 26 | public let file: URL 27 | 28 | /** 29 | FFS make auto-public initializers @ Apple 30 | */ 31 | public init(description: String, file: URL) { 32 | self.description = description 33 | self.file = file 34 | } 35 | 36 | public static func errorReadingFile(file: URL) -> SynopsisError { 37 | return self.init(description: "Cannot read file", file: file) 38 | } 39 | 40 | public static func errorCompilingFile(file: URL) -> SynopsisError { 41 | return self.init(description: "Cannot parse file", file: file) 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Redmadrobot et al. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /Tests/SynopsisTests/Versing/EnumDescriptionVersingTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Project «Synopsis» 3 | // Created by Jeorge Taflanidi 4 | // 5 | 6 | 7 | import XCTest 8 | @testable import Synopsis 9 | 10 | 11 | class EnumDescriptionVersingTests: XCTestCase { 12 | 13 | func testVerse_fullyPacked_returnsAsExpected() { 14 | let enumDescription = EnumDescription.template( 15 | comment: "Docs", 16 | accessibility: Accessibility.`private`, 17 | name: "MyEnum", 18 | inheritedTypes: ["String"], 19 | cases: [ 20 | EnumCase.template(comment: "First", name: "firstName", defaultValue: "\"first_name\""), 21 | EnumCase.template(comment: "Second", name: "lastName", defaultValue: "\"last_name\""), 22 | ], 23 | properties: [], 24 | methods: [] 25 | ) 26 | 27 | let expectedVerse = """ 28 | /// Docs 29 | private enum MyEnum: String { 30 | /// First 31 | case firstName = "first_name" 32 | 33 | /// Second 34 | case lastName = "last_name" 35 | } 36 | 37 | """ 38 | 39 | XCTAssertEqual(enumDescription.verse, expectedVerse) 40 | } 41 | 42 | static var allTests = [ 43 | ("testVerse_fullyPacked_returnsAsExpected", testVerse_fullyPacked_returnsAsExpected) 44 | ] 45 | 46 | } 47 | -------------------------------------------------------------------------------- /Tests/SynopsisTests/SynopsisTestCase.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Project «Synopsis» 3 | // Created by Jeorge Taflanidi 4 | // 5 | 6 | 7 | import XCTest 8 | @testable import Synopsis 9 | 10 | 11 | class SynopsisTestCase: XCTestCase { 12 | 13 | func storeContents(_ contents: String, asFile filename: String) { 14 | let appSupportFolderURL: URL = FileManager.default.urls(for: FileManager.SearchPathDirectory.cachesDirectory, in: FileManager.SearchPathDomainMask.userDomainMask).first! 15 | let fileURL: URL = URL(string: filename, relativeTo: appSupportFolderURL)! 16 | 17 | try! contents.write(to: fileURL, atomically: true, encoding: String.Encoding.utf8) 18 | } 19 | 20 | func deleteFile(named name: String) { 21 | let appSupportFolderURL: URL = FileManager.default.urls(for: FileManager.SearchPathDirectory.cachesDirectory, in: FileManager.SearchPathDomainMask.userDomainMask).first! 22 | let fileURL: URL = URL(string: name, relativeTo: appSupportFolderURL)! 23 | 24 | do { 25 | try FileManager.default.removeItem(at: fileURL) 26 | } catch let error { 27 | print(error) 28 | } 29 | } 30 | 31 | func urlForFile(named name: String) -> URL { 32 | let appSupportFolderURL: URL = FileManager.default.urls(for: FileManager.SearchPathDirectory.cachesDirectory, in: FileManager.SearchPathDomainMask.userDomainMask).first! 33 | return URL(string: name, relativeTo: appSupportFolderURL)! 34 | } 35 | 36 | } 37 | -------------------------------------------------------------------------------- /Tests/SynopsisTests/Versing/ArgumentDescriptionVersingTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Project «Synopsis» 3 | // Created by Jeorge Taflanidi 4 | // 5 | 6 | 7 | import XCTest 8 | @testable import Synopsis 9 | 10 | 11 | class ArgumentDescriptionVersingTests: XCTestCase { 12 | 13 | func testVerse_intArgumentNoBodyName_returnsAsExpected() { 14 | let argumentDescription = ArgumentDescription.template( 15 | name: "count", 16 | bodyName: "count", 17 | type: TypeDescription.integer, 18 | defaultValue: nil, 19 | comment: nil 20 | ) 21 | 22 | let expectedVerse = """ 23 | count: Int 24 | """ 25 | 26 | XCTAssertEqual(argumentDescription.verse, expectedVerse) 27 | } 28 | 29 | func testVerse_hasDefaultValueAndComment_returnsWithExplicitType() { 30 | let argumentDescription = ArgumentDescription.template( 31 | name: "withDictionary", 32 | bodyName: "dictionary", 33 | type: TypeDescription.map(key: TypeDescription.string, value: TypeDescription.object(name: "AnyObject")), 34 | defaultValue: "[:]", 35 | comment: "raw arguments" 36 | ) 37 | 38 | let expectedVerse = """ 39 | withDictionary dictionary: [String: AnyObject] = [:] // raw arguments 40 | """ 41 | 42 | XCTAssertEqual(argumentDescription.verse, expectedVerse) 43 | } 44 | 45 | static var allTests = [ 46 | ("testVerse_intArgumentNoBodyName_returnsAsExpected", testVerse_intArgumentNoBodyName_returnsAsExpected), 47 | ("testVerse_hasDefaultValueAndComment_returnsWithExplicitType", testVerse_hasDefaultValueAndComment_returnsWithExplicitType), 48 | ] 49 | 50 | } 51 | 52 | -------------------------------------------------------------------------------- /Sources/Synopsis/Model/Accessibility.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Project «Synopsis» 3 | // Created by Jeorge Taflanidi 4 | // 5 | 6 | 7 | import Foundation 8 | 9 | 10 | /** 11 | Access mode for Swift statements. 12 | */ 13 | public enum Accessibility: CustomDebugStringConvertible { 14 | case `private` 15 | case `internal` 16 | case `public` 17 | case `open` 18 | 19 | static func deduce(forRawStructureElement element: [String: AnyObject]) -> Accessibility { 20 | let accessibilityString: String = element.accessibility 21 | switch accessibilityString { 22 | case "source.lang.swift.accessibility.private": return Accessibility.`private` 23 | case "source.lang.swift.accessibility.public": return Accessibility.`public` 24 | case "source.lang.swift.accessibility.open": return Accessibility.`open` 25 | default: return Accessibility.`internal` 26 | } 27 | } 28 | 29 | /** 30 | Write down own source code. 31 | */ 32 | public var verse: String { 33 | switch self { 34 | case Accessibility.`private`: return "private" 35 | case Accessibility.`public`: return "public" 36 | case Accessibility.`open`: return "open" 37 | default: return "" 38 | } 39 | } 40 | 41 | public var debugDescription: String { 42 | switch self { 43 | case Accessibility.`private`: return "ACCESSIBILITY: private" 44 | case Accessibility.`public`: return "ACCESSIBILITY: public" 45 | case Accessibility.`open`: return "ACCESSIBILITY: open" 46 | default: return "ACCESSIBILITY: internal" 47 | } 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /Sources/Synopsis/Utility/AnnotationParser.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Project «Synopsis» 3 | // Created by Jeorge Taflanidi 4 | // 5 | 6 | 7 | import Foundation 8 | 9 | 10 | /** 11 | Utility to parse annotations from comments of any kind. 12 | */ 13 | class AnnotationParser { 14 | 15 | /** 16 | Parse annotations from block and inline comments. 17 | 18 | ``` 19 | @annotation 20 | ``` 21 | 22 | ``` 23 | @annotation; @annotation 24 | ``` 25 | 26 | ``` 27 | @annotation 28 | @annotation 29 | ``` 30 | 31 | ``` 32 | @annotation value 33 | @annotation 34 | ``` 35 | 36 | ``` 37 | @annotation value 38 | ``` 39 | ``` 40 | @annotation value; 41 | ``` 42 | ``` 43 | @annotation value @annotation value 44 | ``` 45 | ``` 46 | @annotation value; @annotation value 47 | ``` 48 | ``` 49 | @annotation value; @annotation value; 50 | ``` 51 | */ 52 | func parse(comment string: String) -> [Annotation] { 53 | var s: String = string 54 | var annotaions: [Annotation] = [] 55 | 56 | while s.contains("@") { 57 | let annotationName: String = String(s.truncateUntil(word: "@", deleteWord: true).firstWord()) 58 | s = String(s.truncateUntil(word: "@" + annotationName, deleteWord: true)) 59 | 60 | let annotationValue: String? 61 | 62 | if s.hasPrefix("\n") 63 | || s.hasPrefix(" \n") 64 | || s.hasPrefix(";") 65 | || s.hasPrefix(";\n") 66 | || s.trimmingCharacters(in: CharacterSet.whitespacesAndNewlines).isEmpty { 67 | annotationValue = nil 68 | } else { 69 | annotationValue = String(s.truncateLeadingWhitespace().firstWord(sentenceDividers: ["\n", " ", ";"])) 70 | } 71 | 72 | annotaions.append(Annotation(name: annotationName, value: annotationValue, declaration: nil)) 73 | } 74 | 75 | return annotaions 76 | } 77 | 78 | } 79 | -------------------------------------------------------------------------------- /Sources/Synopsis/Model/Annotation.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Project «Synopsis» 3 | // Created by Jeorge Taflanidi 4 | // 5 | 6 | 7 | import Foundation 8 | 9 | 10 | /** 11 | Meta-information about classes, protocols, structures, properties, methods and method arguments located in the nearby 12 | documentation comments. 13 | */ 14 | public struct Annotation: Equatable, CustomDebugStringConvertible { 15 | 16 | /** 17 | Name of the annotation; doesn't include "@" symbol. 18 | */ 19 | public let name: String 20 | 21 | /** 22 | Value of the annotation; optional, contains first word after annotation name, if any. 23 | 24 | Inline annotations may be divided by semicolon, which may go immediately after annotation name 25 | in case annotation doesn't have any value. 26 | */ 27 | public let value: String? 28 | 29 | /** 30 | Annotation declaration. 31 | */ 32 | public let declaration: Declaration? // FIXME: Make mandatory 33 | 34 | /** 35 | Write down own source code. 36 | */ 37 | public var verse: String { 38 | return "@\(name)" + (nil != value ? " \(value!)" : "") 39 | } 40 | 41 | /** 42 | FFS make auto-public initializers @ Apple 43 | */ 44 | public init(name: String, value: String?, declaration: Declaration?) { 45 | self.name = name 46 | self.value = value 47 | self.declaration = declaration 48 | } 49 | 50 | public static func ==(left: Annotation, right: Annotation) -> Bool { 51 | return left.name == right.name 52 | && left.value == right.value 53 | && left.declaration == right.declaration 54 | } 55 | 56 | public var debugDescription: String { 57 | return "ANNOTATION: name = \(name)" + (nil != value ? "; value = \(value!)" : "") 58 | } 59 | 60 | } 61 | 62 | 63 | public extension Sequence where Iterator.Element == Annotation { 64 | 65 | public subscript(annotationName: String) -> Iterator.Element? { 66 | for item in self { 67 | if item.name == annotationName { 68 | return item 69 | } 70 | } 71 | return nil 72 | } 73 | 74 | public func contains(annotationName: String) -> Bool { 75 | return nil != self[annotationName] 76 | } 77 | 78 | } 79 | -------------------------------------------------------------------------------- /Sources/Synopsis/Model/Extensible.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Project «Synopsis» 3 | // Created by Jeorge Taflanidi 4 | // 5 | 6 | 7 | import Foundation 8 | 9 | 10 | /** 11 | Basically, protocols, structs and classes. 12 | */ 13 | public protocol Extensible: Equatable, CustomDebugStringConvertible { 14 | 15 | /** 16 | Documentation comment above the extensible. 17 | */ 18 | var comment: String? { get } 19 | 20 | /** 21 | Annotations. 22 | 23 | Annotations are located inside the comment. 24 | */ 25 | var annotations: [Annotation] { get } 26 | 27 | /** 28 | Declaration. 29 | */ 30 | var declaration: Declaration { get } 31 | 32 | /** 33 | Access visibility. 34 | */ 35 | var accessibility: Accessibility { get } 36 | 37 | /** 38 | Name. 39 | */ 40 | var name: String { get } 41 | 42 | /** 43 | Inherited types: parent class/classes, protocols etc. 44 | */ 45 | var inheritedTypes: [String] { get } 46 | 47 | /** 48 | List of properties. 49 | */ 50 | var properties: [PropertyDescription] { get } 51 | 52 | /** 53 | List of methods. 54 | */ 55 | var methods: [MethodDescription] { get } 56 | 57 | /** 58 | Write down own source code. 59 | */ 60 | var verse: String { get } 61 | 62 | init( 63 | comment: String?, 64 | annotations: [Annotation], 65 | declaration: Declaration, 66 | accessibility: Accessibility, 67 | name: String, 68 | inheritedTypes: [String], 69 | properties: [PropertyDescription], 70 | methods: [MethodDescription] 71 | ) 72 | 73 | } 74 | 75 | 76 | public func ==(left: E, right: E) -> Bool { 77 | return left.comment == right.comment 78 | && left.annotations == right.annotations 79 | && left.declaration == right.declaration 80 | && left.accessibility == right.accessibility 81 | && left.name == right.name 82 | && left.inheritedTypes == right.inheritedTypes 83 | && left.properties == right.properties 84 | && left.methods == right.methods 85 | } 86 | 87 | 88 | public extension Sequence where Iterator.Element: Extensible { 89 | 90 | public subscript(name: String) -> Iterator.Element? { 91 | for item in self { 92 | if item.name == name { 93 | return item 94 | } 95 | } 96 | return nil 97 | } 98 | 99 | public func contains(name: String) -> Bool { 100 | return nil != self[name] 101 | } 102 | 103 | } 104 | -------------------------------------------------------------------------------- /Sources/Synopsis/Model/XcodeMessage.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Project «Synopsis» 3 | // Created by Jeorge Taflanidi 4 | // 5 | 6 | 7 | import Foundation 8 | 9 | 10 | /** 11 | Error, warning or note. 12 | 13 | `XcodeMessage` instance is designed to be printed via `print()` method such that IDE will receieve this input 14 | and display errors and warnings inside of the source code editor as actual errors and warnings. 15 | */ 16 | public struct XcodeMessage: Error, CustomDebugStringConvertible, Equatable { 17 | 18 | /** 19 | Red errors, yellow warnings and "invisible" notes. 20 | */ 21 | public enum MessageType: String { 22 | case error = "error" 23 | case warning = "warning" 24 | case note = "note" 25 | } 26 | 27 | /** 28 | Source code file URL. 29 | */ 30 | public let file: URL 31 | 32 | /** 33 | Line number, where error/warning/note occured. 34 | */ 35 | public let lineNumber: Int 36 | 37 | /** 38 | Column, where error/warning/note occured. 39 | */ 40 | private let columnNumber: Int 41 | 42 | /** 43 | Message to be displayed. 44 | */ 45 | public let message: String 46 | 47 | /** 48 | Error, warning or note. 49 | */ 50 | public let type: MessageType 51 | 52 | /** 53 | FFS make auto-public initializers @ Apple 54 | */ 55 | public init( 56 | file: URL, 57 | lineNumber: Int, 58 | columnNumber: Int, 59 | message: String, 60 | type: MessageType 61 | ) { 62 | self.file = file 63 | self.lineNumber = lineNumber 64 | self.columnNumber = columnNumber 65 | self.message = message 66 | self.type = type 67 | } 68 | 69 | /** 70 | Initialize with `Declaration` instance. 71 | */ 72 | public init( 73 | declaration: Declaration, 74 | message: String, 75 | type: MessageType = .error 76 | ) { 77 | self.init( 78 | file: declaration.filePath, 79 | lineNumber: declaration.lineNumber, 80 | columnNumber: declaration.columnNumber, 81 | message: message, 82 | type: type 83 | ) 84 | } 85 | 86 | public var debugDescription: String { 87 | return "\(self.file.path):\(self.lineNumber):\(self.columnNumber): \(self.type.rawValue): \(self.message)\n" 88 | } 89 | 90 | public static func ==(left: XcodeMessage, right: XcodeMessage) -> Bool { 91 | return left.file == right.file 92 | && left.lineNumber == right.lineNumber 93 | && left.message == right.message 94 | && left.type == right.type 95 | } 96 | 97 | } 98 | -------------------------------------------------------------------------------- /Sources/Synopsis/Utility/SwiftDocKey.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Project «Synopsis» 3 | // Created by Jeorge Taflanidi 4 | // 5 | 6 | 7 | import Foundation 8 | import SourceKittenFramework 9 | 10 | 11 | /** 12 | ATTENTION! 13 | 14 | Most of the methods below are unsafe. Use on your own risk. 15 | */ 16 | extension Dictionary where Key == String { 17 | var accessibility: String { 18 | return self["key.accessibility"] as! String 19 | } 20 | 21 | var offset: Int { 22 | return self[SwiftDocKey.offset] as! Int 23 | } 24 | 25 | var length: Int { 26 | return self[SwiftDocKey.length] as! Int 27 | } 28 | 29 | var parsedDeclaration: String { 30 | if let value = self[SwiftDocKey.parsedDeclaration] as? String { 31 | return value 32 | } 33 | 34 | preconditionFailure(""" 35 | 36 | !!! ATTENTION !!! 37 | It looks like Swift compiler can't reach your source code file. 38 | This usually happens when provided path does not exist or when this path contains relative injections like ".": 39 | 40 | /Users/user/Projects/MyProject/./Sources 41 | ^~~~~~~~~ 42 | 43 | Swift compiler can't navigate through "." and ".." yet. Sorry about that. 44 | Also, Swift compiler uses absolute paths, so we concatenate your relative paths with current working directory. 45 | 46 | Please make sure your "-input" folder reads like "Sources/Classes" and not like "./Sources/Classes". 47 | (or provide absolute path, if applicable) 48 | !!! ATTENTION !!! 49 | 50 | """) 51 | } 52 | 53 | var subsctructure: [[String: AnyObject]] { 54 | return self[SwiftDocKey.substructure] as? [[String: AnyObject]] ?? [] 55 | } 56 | 57 | var comment: String? { 58 | return self[SwiftDocKey.documentationComment] as? String 59 | } 60 | 61 | var name: String { 62 | return self[SwiftDocKey.name] as! String 63 | } 64 | 65 | var inheritedTypes: [String] { 66 | let inheritedTypes: [[String: AnyObject]] = self[SwiftDocKey.inheritedtypes] as? [[String: AnyObject]] ?? [] 67 | return inheritedTypes.map { (inheritedType: [String: AnyObject]) -> String in 68 | return inheritedType[SwiftDocKey.name] as! String 69 | } 70 | } 71 | 72 | var typename: String { 73 | return self[SwiftDocKey.typeName] as! String 74 | } 75 | 76 | var kind: String? { 77 | return self[SwiftDocKey.kind] as? String 78 | } 79 | 80 | var bodyOffset: Int? { 81 | return self[SwiftDocKey.bodyOffset] as? Int 82 | } 83 | 84 | var bodyLength: Int? { 85 | return self[SwiftDocKey.bodyLength] as? Int 86 | } 87 | 88 | subscript(key: SwiftDocKey) -> Value? { 89 | return self[key.rawValue] 90 | } 91 | } 92 | 93 | -------------------------------------------------------------------------------- /Sources/Synopsis/Utility/EnumCaseParser.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Project «Synopsis» 3 | // Created by Jeorge Taflanidi 4 | // 5 | 6 | 7 | import Foundation 8 | import SourceKittenFramework 9 | 10 | 11 | class EnumCaseParser { 12 | 13 | func parse(rawStructureElements: [[String: AnyObject]], file: GoodFile) -> [EnumCase] { 14 | return rawStructureElements 15 | .filter { isRawEnumCaseDescription($0) } 16 | .flatMap { (rawEnumCaseDescription: [String: AnyObject]) -> [EnumCaseElement] in 17 | /** 18 | Each enum case may contain multiple options: 19 | 20 | enum MyEnum { 21 | case option1, option2 22 | } 23 | 24 | Our goal is to flat them out. 25 | */ 26 | let offset: Int = rawEnumCaseDescription.offset 27 | return rawEnumCaseDescription.subsctructure.map { EnumCaseElement(offset: offset, structure: $0) } 28 | } 29 | .map { (enumCaseElement: EnumCaseElement) -> EnumCase in 30 | let parsedDeclaration: String = enumCaseElement.structure.parsedDeclaration 31 | 32 | let comment: String? = enumCaseElement.structure.comment 33 | let annotations: [Annotation] = nil != comment ? AnnotationParser().parse(comment: comment!) : [] 34 | let name: String = enumCaseElement.structure.name 35 | let defaultValue: String? = getDefaultValue(fromParsedDeclaration: parsedDeclaration) 36 | 37 | let declaration: Declaration = 38 | Declaration( 39 | filePath: file.path, 40 | fileContents: file.file.contents, 41 | rawText: parsedDeclaration, 42 | offset: enumCaseElement.offset 43 | ) 44 | 45 | return EnumCase( 46 | comment: comment, 47 | annotations: annotations, 48 | name: name, 49 | defaultValue: defaultValue, 50 | declaration: declaration 51 | ) 52 | } 53 | } 54 | 55 | } 56 | 57 | 58 | private extension EnumCaseParser { 59 | 60 | struct EnumCaseElement { 61 | let offset: Int 62 | let structure: [String: AnyObject] 63 | } 64 | 65 | func isRawEnumCaseDescription(_ element: [String: AnyObject]) -> Bool { 66 | guard let kind: String = element.kind 67 | else { return false } 68 | return SwiftDeclarationKind.enumcase.rawValue == kind 69 | } 70 | 71 | func getDefaultValue(fromParsedDeclaration parsedDeclaration: String) -> String? { 72 | let lex = LexemeString(parsedDeclaration) 73 | 74 | for index in parsedDeclaration.indices { 75 | if "=" == parsedDeclaration[index] && lex.inSourceCodeRange(index) { 76 | let defaultValueStart: String.Index = parsedDeclaration.index(after: index) 77 | return parsedDeclaration[defaultValueStart...].trimmingCharacters(in: CharacterSet.whitespacesAndNewlines) 78 | } 79 | } 80 | 81 | return nil 82 | } 83 | 84 | } 85 | -------------------------------------------------------------------------------- /Sources/Synopsis/Model/EnumCase.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Project «Synopsis» 3 | // Created by Jeorge Taflanidi 4 | // 5 | 6 | 7 | import Foundation 8 | 9 | 10 | /** 11 | Case of enum. 12 | */ 13 | public struct EnumCase: Equatable, CustomDebugStringConvertible { 14 | 15 | /** 16 | Documentation comment. 17 | */ 18 | public let comment: String? 19 | 20 | /** 21 | Annotations. 22 | */ 23 | public let annotations: [Annotation] 24 | 25 | /** 26 | Case name. 27 | */ 28 | public let name: String 29 | 30 | /** 31 | Raw default value. 32 | */ 33 | public let defaultValue: String? 34 | 35 | /** 36 | Declaration line. 37 | */ 38 | public let declaration: Declaration 39 | 40 | /** 41 | Write down own source code. 42 | */ 43 | public var verse: String { 44 | let commentStr: String 45 | if let commentExpl: String = comment, !commentExpl.isEmpty { 46 | commentStr = commentExpl.prefixEachLine(with: "/// ") + "\n" 47 | } else { 48 | commentStr = "" 49 | } 50 | 51 | let defaultValueStr: String = nil != defaultValue ? " = \(defaultValue!)" : "" 52 | 53 | return """ 54 | \(commentStr)case \(name)\(defaultValueStr) 55 | """ 56 | } 57 | 58 | /** 59 | FFS make auto-public initializers @ Apple 60 | */ 61 | public init( 62 | comment: String?, 63 | annotations: [Annotation], 64 | name: String, 65 | defaultValue: String?, 66 | declaration: Declaration 67 | ) { 68 | self.comment = comment 69 | self.annotations = annotations 70 | self.name = name 71 | self.defaultValue = defaultValue 72 | self.declaration = declaration 73 | } 74 | 75 | /** 76 | Make a template enum case for later code generation. 77 | */ 78 | public static func template( 79 | comment: String?, 80 | name: String, 81 | defaultValue: String? 82 | ) -> EnumCase { 83 | return EnumCase( 84 | comment: comment, 85 | annotations: [], 86 | name: name, 87 | defaultValue: defaultValue, 88 | declaration: Declaration.mock 89 | ) 90 | } 91 | 92 | public static func ==(left: EnumCase, right: EnumCase) -> Bool { 93 | return left.comment == right.comment 94 | && left.annotations == right.annotations 95 | && left.name == right.name 96 | && left.defaultValue == right.defaultValue 97 | && left.declaration == right.declaration 98 | } 99 | 100 | public var debugDescription: String { 101 | return "ENUMCASE: name = \(name)" 102 | } 103 | 104 | } 105 | 106 | 107 | public extension Sequence where Iterator.Element == EnumCase { 108 | 109 | public subscript(name: String) -> Iterator.Element? { 110 | for item in self { 111 | if item.name == name { 112 | return item 113 | } 114 | } 115 | return nil 116 | } 117 | 118 | public func contains(name: String) -> Bool { 119 | return nil != self[name] 120 | } 121 | 122 | } 123 | -------------------------------------------------------------------------------- /Tests/SynopsisTests/Utility/EnumDescriptionParserTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Project «Synopsis» 3 | // Created by Jeorge Taflanidi 4 | // 5 | 6 | 7 | import XCTest 8 | @testable import Synopsis 9 | 10 | 11 | class EnumDescriptionParserTests: SynopsisTestCase { 12 | 13 | override func setUp() { 14 | super.setUp() 15 | storeContents(enumFile, asFile: "EnumFile.swift") 16 | } 17 | 18 | override func tearDown() { 19 | deleteFile(named: "EnumFile.swift") 20 | super.tearDown() 21 | } 22 | 23 | func testParse_enumFile_returnsEnum() { 24 | let inputFile: URL = urlForFile(named: "EnumFile.swift") 25 | let parser = EnumDescriptionParser() 26 | let result: ParsingResult = parser.parse(files: [inputFile]) 27 | 28 | XCTAssertEqual(result.models.count, 1) 29 | let actualEnumDescription: EnumDescription = result.models.first! 30 | 31 | XCTAssertEqual( 32 | actualEnumDescription, 33 | EnumDescription( 34 | comment: "EnumName docs\n\n@model Name", 35 | annotations: [ 36 | Annotation(name: "model", value: "Name", declaration: nil) 37 | ], 38 | declaration: Declaration( 39 | filePath: inputFile, 40 | rawText: "public enum EnumName: String, CodingKeys", 41 | offset: 44, 42 | lineNumber: 6, 43 | columnNumber: 8 44 | ), 45 | accessibility: Accessibility.`public`, 46 | name: "EnumName", 47 | inheritedTypes: ["String", "CodingKeys"], 48 | cases: [ 49 | EnumCase( 50 | comment: "First docs", 51 | annotations: [], 52 | name: "first", 53 | defaultValue: "\"1st\"", 54 | declaration: Declaration( 55 | filePath: inputFile, 56 | rawText: "case first = \"1st\"", 57 | offset: 103, 58 | lineNumber: 8, 59 | columnNumber: 5 60 | ) 61 | ), 62 | EnumCase( 63 | comment: "Second docs", 64 | annotations: [], 65 | name: "second", 66 | defaultValue: "\"2nd\"", 67 | declaration: Declaration( 68 | filePath: inputFile, 69 | rawText: "case second = \"2nd\"", 70 | offset: 147, 71 | lineNumber: 11, 72 | columnNumber: 5 73 | ) 74 | ), 75 | ], 76 | properties: [], 77 | methods: [] 78 | ) 79 | ) 80 | } 81 | 82 | static var allTests = [ 83 | ("testParse_enumFile_returnsEnum", testParse_enumFile_returnsEnum), 84 | ] 85 | 86 | } 87 | 88 | 89 | let enumFile = """ 90 | /** 91 | EnumName docs 92 | 93 | @model Name 94 | */ 95 | public enum EnumName: String, CodingKeys { 96 | /// First docs 97 | case first = "1st" 98 | 99 | /// Second docs 100 | case second = "2nd" 101 | } 102 | """ 103 | -------------------------------------------------------------------------------- /Sources/Synopsis/Model/MethodDescription.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Project «Synopsis» 3 | // Created by Jeorge Taflanidi 4 | // 5 | 6 | 7 | import Foundation 8 | 9 | 10 | /** 11 | A method. 12 | */ 13 | public class MethodDescription: FunctionDescription { 14 | 15 | /** 16 | Is it a simple method or an initializer? 17 | */ 18 | public var isInitializer: Bool { 19 | return name.hasPrefix("init(") 20 | } 21 | 22 | /** 23 | Write down own source code. 24 | */ 25 | public override var verse: String { 26 | let commentStr: String 27 | if let commentExpl: String = comment, !commentExpl.isEmpty { 28 | commentStr = commentExpl.prefixEachLine(with: "/// ") + "\n" 29 | } else { 30 | commentStr = "" 31 | } 32 | 33 | let openBraceIndex: String.Index = name.index(of: "(")! 34 | 35 | let accessibilityStr = accessibility.verse.isEmpty ? "" : "\(accessibility.verse) " 36 | let funcStr = isInitializer ? "" : "func " 37 | let nameStr = name[.. String in 67 | if arguments.last != argument { 68 | return result + argument.verseWithComa + "\n" 69 | } 70 | return result + argument.verse + "\n" 71 | } 72 | } 73 | 74 | return """ 75 | \(commentStr)\(accessibilityStr)\(kindStr)\(funcStr)\(nameStr)(\(argumentsStr.indent))\(returnTypeStr)\(bodyStr) 76 | """ 77 | } 78 | 79 | /** 80 | FFS make auto-public initializers @ Apple 81 | */ 82 | public required init( 83 | comment: String?, 84 | annotations: [Annotation], 85 | accessibility: Accessibility, 86 | name: String, 87 | arguments: [ArgumentDescription], 88 | returnType: TypeDescription?, 89 | declaration: Declaration, 90 | kind: Kind, 91 | body: String? 92 | ) { 93 | super.init( 94 | comment: comment, 95 | annotations: annotations, 96 | accessibility: accessibility, 97 | name: name, 98 | arguments: arguments, 99 | returnType: returnType, 100 | declaration: declaration, 101 | kind: kind, 102 | body: body 103 | ) 104 | } 105 | 106 | public override var debugDescription: String { 107 | return "METHOD: name = \(name)" + (nil != returnType ? "; return type = \(returnType!)" : "") 108 | } 109 | 110 | } 111 | -------------------------------------------------------------------------------- /Sources/Synopsis/Model/Declaration.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Project «Synopsis» 3 | // Created by Jeorge Taflanidi 4 | // 5 | 6 | 7 | import Foundation 8 | 9 | 10 | /** 11 | Source code element declaration. 12 | 13 | Includes absolute file path, line number, column number, offset and raw declaration text itself. 14 | */ 15 | public struct Declaration: Equatable { 16 | 17 | /** 18 | File, where statement is declared. 19 | */ 20 | public let filePath: URL 21 | 22 | /** 23 | Parsed condensed declaration. 24 | */ 25 | public let rawText: String 26 | 27 | /** 28 | How many characters to skip. 29 | */ 30 | public let offset: Int 31 | 32 | /** 33 | Calculated line number. 34 | */ 35 | public let lineNumber: Int 36 | 37 | /** 38 | Calculated column number. 39 | */ 40 | public let columnNumber: Int 41 | 42 | /** 43 | FFS make auto-public initializers @ Apple 44 | */ 45 | public init( 46 | filePath: URL, 47 | rawText: String, 48 | offset: Int, 49 | lineNumber: Int, 50 | columnNumber: Int 51 | ) { 52 | self.filePath = filePath 53 | self.rawText = rawText 54 | self.offset = offset 55 | self.lineNumber = lineNumber 56 | self.columnNumber = columnNumber 57 | } 58 | 59 | public init( 60 | filePath: URL, 61 | fileContents: String, 62 | rawText: String, 63 | offset: Int 64 | ) { 65 | let offsetIndex: String.Index = fileContents.index(fileContents.startIndex, offsetBy: offset) 66 | let textBeforeDeclaration: Substring = fileContents[.. Bool { 90 | return left.filePath == right.filePath 91 | && left.rawText == right.rawText 92 | && left.offset == right.offset 93 | && left.lineNumber == right.lineNumber 94 | && left.columnNumber == right.columnNumber 95 | } 96 | 97 | public static let mock = Declaration( 98 | filePath: URL(fileURLWithPath: Declaration.MockProperties.filePath), 99 | rawText: Declaration.MockProperties.rawText, 100 | offset: Declaration.MockProperties.offset, 101 | lineNumber: Declaration.MockProperties.lineNumber, 102 | columnNumber: Declaration.MockProperties.columnNumber 103 | ) 104 | 105 | private enum MockProperties { 106 | static let filePath = "" 107 | static let rawText = "" 108 | static let offset = -1 109 | static let lineNumber = -1 110 | static let columnNumber = -1 111 | } 112 | 113 | } 114 | -------------------------------------------------------------------------------- /Tests/SynopsisTests/Versing/PropertyDescriptionVersingTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Project «Synopsis» 3 | // Created by Jeorge Taflanidi 4 | // 5 | 6 | 7 | import XCTest 8 | @testable import Synopsis 9 | 10 | 11 | class PropertyDescriptionVersingTests: XCTestCase { 12 | 13 | func testVerse_simpleIntProperty_returnsAsExpected() { 14 | let propertyDescription = PropertyDescription.template( 15 | comment: nil, 16 | accessibility: Accessibility.`internal`, 17 | constant: true, 18 | name: "count", 19 | type: TypeDescription.integer, 20 | defaultValue: nil, 21 | kind: .instance, 22 | body: nil 23 | ) 24 | 25 | let expectedVerse = """ 26 | let count: Int 27 | """ 28 | 29 | XCTAssertEqual(propertyDescription.verse, expectedVerse) 30 | } 31 | 32 | func testVerse_publicStringPropertyWithBody_returnsAsExpected() { 33 | let propertyDescription = PropertyDescription( 34 | comment: nil, 35 | annotations: [], 36 | accessibility: Accessibility.`public`, 37 | constant: false, 38 | name: "name", 39 | type: TypeDescription.string, 40 | defaultValue: nil, 41 | declaration: Declaration.mock, 42 | kind: .instance, 43 | body: "return \"Travis\"" 44 | ) 45 | 46 | let expectedVerse = """ 47 | public var name: String { 48 | return "Travis" 49 | } 50 | """ 51 | 52 | XCTAssertEqual(propertyDescription.verse, expectedVerse) 53 | } 54 | 55 | func testVerse_privateStringPropertyWithDefaultValueAndComment_returnsWithExplicitType() { 56 | let propertyDescription = PropertyDescription.template( 57 | comment: "Entity name.\n\n Wild text.", 58 | accessibility: Accessibility.`private`, 59 | constant: false, 60 | name: "name", 61 | type: TypeDescription.string, 62 | defaultValue: "\"Jeff\"", 63 | kind: .instance, 64 | body: nil 65 | ) 66 | 67 | let expectedVerse = """ 68 | /// Entity name. 69 | ///\(" ") 70 | /// Wild text. 71 | private var name: String = "Jeff" 72 | """ 73 | 74 | XCTAssertEqual(propertyDescription.verse, expectedVerse) 75 | } 76 | 77 | func testVerse_publicStaticLet_returnsAsExpected() { 78 | let propertyDescirption = PropertyDescription.template( 79 | comment: nil, 80 | accessibility: Accessibility.`public`, 81 | constant: true, 82 | name: "pi", 83 | type: TypeDescription.doublePrecision, 84 | defaultValue: "3.1415926536", 85 | kind: PropertyDescription.Kind.static, 86 | body: nil 87 | ) 88 | 89 | let expextedVerse = """ 90 | public static let pi: Double = 3.1415926536 91 | """ 92 | 93 | XCTAssertEqual(propertyDescirption.verse, expextedVerse) 94 | } 95 | 96 | static var allTests = [ 97 | ("testVerse_simpleIntProperty_returnsAsExpected", testVerse_simpleIntProperty_returnsAsExpected), 98 | ("testVerse_publicStringPropertyWithBody_returnsAsExpected", testVerse_publicStringPropertyWithBody_returnsAsExpected), 99 | ("testVerse_privateStringPropertyWithDefaultValueAndComment_returnsWithExplicitType", testVerse_privateStringPropertyWithDefaultValueAndComment_returnsWithExplicitType), 100 | ("testVerse_publicStaticLet_returnsAsExpected", testVerse_publicStaticLet_returnsAsExpected), 101 | ] 102 | 103 | } 104 | -------------------------------------------------------------------------------- /Sources/Synopsis/Utility/ArgumentDescriptionParser.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Project «Synopsis» 3 | // Created by Jeorge Taflanidi 4 | // 5 | 6 | 7 | import Foundation 8 | 9 | 10 | /** 11 | Method arguments' parser. 12 | */ 13 | class ArgumentDescriptionParser { 14 | 15 | func parse(functionParsedDeclaration declaration: String) -> [ArgumentDescription] { 16 | let rawArguments: String = extractRawArguments(fromMethodDeclaration: declaration) 17 | return parse(rawArguments: rawArguments) 18 | } 19 | 20 | } 21 | 22 | 23 | private extension ArgumentDescriptionParser { 24 | 25 | func extractRawArguments(fromMethodDeclaration declaration: String) -> String { 26 | let openBraceIndex: String.Index = findOpenBrace(inMethodDeclaration: declaration) 27 | let argumentsStart = declaration.index(after: openBraceIndex) 28 | 29 | let lex = LexemeString(declaration) 30 | for index in declaration.indices { 31 | if ")" == declaration[index] && lex.inSourceCodeRange(index) { 32 | return String(declaration[argumentsStart.. String.Index { 40 | let lex = LexemeString(declaration) 41 | for index in declaration.indices { 42 | if "(" == declaration[index] && lex.inSourceCodeRange(index) { 43 | return index 44 | } 45 | } 46 | 47 | return declaration.startIndex 48 | } 49 | 50 | func parse(rawArguments arguments: String) -> [ArgumentDescription] { 51 | if arguments.isEmpty { return [] } 52 | 53 | var rawArgumentsWithoutComments: String = "" 54 | var inlineComments: [String] = [] 55 | 56 | let lex = LexemeString(arguments) 57 | for lexeme in lex.lexemes { 58 | if lexeme.isCommentKind() { 59 | inlineComments.append(String(arguments[lexeme.left...lexeme.right]).truncateInlineCommentOpening()) 60 | } else { 61 | rawArgumentsWithoutComments += String(arguments[lexeme.left...lexeme.right]) 62 | } 63 | } 64 | 65 | return parse(rawArgumentsWithoutComments: rawArgumentsWithoutComments, comments: inlineComments) 66 | } 67 | 68 | func parse(rawArgumentsWithoutComments: String, comments: [String]) -> [ArgumentDescription] { 69 | var commentsIterator = comments.makeIterator() 70 | return 71 | rawArgumentsWithoutComments 72 | .replacingOccurrences(of: "\n", with: "") 73 | .components(separatedBy: ",") 74 | .map { parse(argumentLine: $0, comment: commentsIterator.next()) } 75 | } 76 | 77 | func parse(argumentLine line: String, comment: String?) -> ArgumentDescription { 78 | let argumentName: String 79 | let externalArgumentName: String 80 | 81 | let annotations: [Annotation] = nil != comment ? AnnotationParser().parse(comment: comment!) : [] 82 | let argumentType: TypeDescription = TypeParser().deduceType(fromDeclaration: line) 83 | let defaultValue: String? = getDefaultValue(fromArgumentLineWithNoComments: line) 84 | 85 | // NOTE: method arguments always have explicit types 86 | let names = String(line.truncateAfter(word: ":", deleteWord: true).trimmingCharacters(in: CharacterSet.whitespaces)) 87 | 88 | externalArgumentName = String(names.firstWord()) 89 | if names.contains(" ") { 90 | argumentName = String(names.truncateUntil(word: " ", deleteWord: true)) 91 | } else { 92 | argumentName = externalArgumentName 93 | } 94 | 95 | return ArgumentDescription( 96 | name: externalArgumentName, 97 | bodyName: argumentName, 98 | type: argumentType, 99 | defaultValue: defaultValue, 100 | annotations: annotations, 101 | declaration: nil, 102 | comment: comment 103 | ) 104 | } 105 | 106 | func getDefaultValue(fromArgumentLineWithNoComments line: String) -> String? { 107 | guard let equalSignIndex: String.Index = line.index(of: "=") 108 | else { return nil } 109 | 110 | return String(line[equalSignIndex...].dropFirst().trimmingCharacters(in: CharacterSet.whitespacesAndNewlines)) 111 | } 112 | 113 | } 114 | -------------------------------------------------------------------------------- /Sources/Synopsis/Utility/ExtensibleParser.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Project «Synopsis» 3 | // Created by Jeorge Taflanidi 4 | // 5 | 6 | 7 | import Foundation 8 | import SourceKittenFramework 9 | 10 | 11 | class ExtensibleParser { 12 | 13 | func parse(files pathList: [URL]) -> ParsingResult { 14 | var ioErrors: [SynopsisError] = [] 15 | 16 | let files: [GoodFile] = pathList.flatMap { (fileURL: URL) -> GoodFile? in 17 | if let file = File(path: fileURL.path) { 18 | return GoodFile(path: fileURL, file: file) 19 | } 20 | 21 | ioErrors.append(SynopsisError.errorReadingFile(file: fileURL)) 22 | return nil 23 | } 24 | 25 | let result: ParsingResult = parse(files: files) 26 | return ParsingResult(errors: result.errors + ioErrors, models: result.models) 27 | } 28 | 29 | func parse(files: [GoodFile]) -> ParsingResult { 30 | var compilingErrors: [SynopsisError] = [] 31 | 32 | let compiledStructures: [CompiledStructure] = files.flatMap { (goodFile: GoodFile) -> CompiledStructure? in 33 | if let structure: CompiledStructure = CompiledStructure(file: goodFile) { 34 | return structure 35 | } 36 | 37 | compilingErrors.append(SynopsisError.errorCompilingFile(file: goodFile.path)) 38 | return nil 39 | } 40 | 41 | let result: [Model] = translate(compiledStructures) 42 | return ParsingResult(errors: compilingErrors, models: result) 43 | } 44 | 45 | func isRawExtensibleDescription(_ element: [String: AnyObject]) -> Bool { 46 | return false 47 | } 48 | 49 | } 50 | 51 | 52 | private extension ExtensibleParser { 53 | 54 | func translate(_ structures: [CompiledStructure]) -> [Model] { 55 | return structures.flatMap { translate($0) } 56 | } 57 | 58 | func translate(_ structure: CompiledStructure) -> [Model] { 59 | let topStructureElements = structure.topElements 60 | 61 | let extensibles: [Model] = 62 | topStructureElements 63 | .filter { isRawExtensibleDescription($0) } 64 | .map { (rawExtensibleDescription: [String: AnyObject]) -> Model in 65 | let declarationOffset: Int = rawExtensibleDescription.offset 66 | let declarationRawText: String = rawExtensibleDescription.parsedDeclaration 67 | let rawStructureElements = rawExtensibleDescription.subsctructure 68 | 69 | let comment: String? = rawExtensibleDescription.comment 70 | let annotations: [Annotation] = nil != comment ? AnnotationParser().parse(comment: comment!) : [] 71 | let accessibility: Accessibility = Accessibility.deduce(forRawStructureElement: rawExtensibleDescription) 72 | let name: String = rawExtensibleDescription.name 73 | let inheritedTypes: [String] = rawExtensibleDescription.inheritedTypes 74 | 75 | let declaration: Declaration = 76 | Declaration( 77 | filePath: structure.file.path, 78 | fileContents: structure.file.file.contents, 79 | rawText: declarationRawText, 80 | offset: declarationOffset 81 | ) 82 | 83 | let properties: [PropertyDescription] = 84 | PropertyDescriptionParser().parse(rawStructureElements: rawStructureElements, file: structure.file) 85 | 86 | let methods: [MethodDescription] = 87 | MethodDescriptionParser().parse(rawStructureElements: rawStructureElements, file: structure.file) 88 | 89 | return Model( 90 | comment: comment, 91 | annotations: annotations, 92 | declaration: declaration, 93 | accessibility: accessibility, 94 | name: name, 95 | inheritedTypes: inheritedTypes, 96 | properties: properties, 97 | methods: methods 98 | ) 99 | } 100 | 101 | return extensibles 102 | } 103 | 104 | } 105 | -------------------------------------------------------------------------------- /Sources/Synopsis/Model/ArgumentDescription.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Project «Synopsis» 3 | // Created by Jeorge Taflanidi 4 | // 5 | 6 | 7 | import Foundation 8 | 9 | 10 | /** 11 | Method argument description. 12 | */ 13 | public struct ArgumentDescription: Equatable, CustomDebugStringConvertible { 14 | 15 | /** 16 | Argument "external" name used in method calls. 17 | */ 18 | public let name: String 19 | 20 | /** 21 | Argument "internal" name used inside method body. 22 | */ 23 | public let bodyName: String 24 | 25 | /** 26 | Argument type. 27 | */ 28 | public let type: TypeDescription 29 | 30 | /** 31 | Default value, if any. 32 | */ 33 | public let defaultValue: String? 34 | 35 | /** 36 | Argument annotations; N.B.: arguments only have inline annotations. 37 | */ 38 | public let annotations: [Annotation] 39 | 40 | /** 41 | Argument declaration. 42 | */ 43 | public let declaration: Declaration? // FIXME: Make mandatory 44 | 45 | /** 46 | Inline comment. 47 | */ 48 | public let comment: String? 49 | 50 | /** 51 | Write down own source code. 52 | */ 53 | public var verse: String { 54 | let defaultValueStr: String = nil != defaultValue ? " = \(defaultValue!)" : "" 55 | 56 | if name == bodyName { 57 | return "\(name): \(type.verse)\(defaultValueStr)" + (nil != comment ? " // \(comment!)" : "") 58 | } else { 59 | return "\(name) \(bodyName): \(type.verse)\(defaultValueStr)" + (nil != comment ? " // \(comment!)" : "") 60 | } 61 | } 62 | 63 | /** 64 | Write down own source code like if it was one of multiple arguments. 65 | */ 66 | public var verseWithComa: String { 67 | let defaultValueStr: String = nil != defaultValue ? " = \(defaultValue!)" : "" 68 | 69 | if name == bodyName { 70 | return "\(name): \(type.verse)\(defaultValueStr)," + (nil != comment ? " // \(comment!)" : "") 71 | } else { 72 | return "\(name) \(bodyName): \(type.verse)\(defaultValueStr)," + (nil != comment ? " // \(comment!)" : "") 73 | } 74 | } 75 | 76 | /** 77 | FFS make auto-public initializers @ Apple 78 | */ 79 | public init( 80 | name: String, 81 | bodyName: String, 82 | type: TypeDescription, 83 | defaultValue: String?, 84 | annotations: [Annotation], 85 | declaration: Declaration?, 86 | comment: String? 87 | ) { 88 | self.name = name 89 | self.bodyName = bodyName 90 | self.type = type 91 | self.defaultValue = defaultValue 92 | self.annotations = annotations 93 | self.declaration = declaration 94 | self.comment = comment 95 | } 96 | 97 | /** 98 | Make a template for later code generation. 99 | */ 100 | public static func template( 101 | name: String, 102 | bodyName: String, 103 | type: TypeDescription, 104 | defaultValue: String?, 105 | comment: String? 106 | ) -> ArgumentDescription { 107 | return ArgumentDescription( 108 | name: name, 109 | bodyName: bodyName, 110 | type: type, 111 | defaultValue: defaultValue, 112 | annotations: [], 113 | declaration: Declaration.mock, 114 | comment: comment 115 | ) 116 | } 117 | 118 | public static func ==(left: ArgumentDescription, right: ArgumentDescription) -> Bool { 119 | return left.annotations == right.annotations 120 | && left.name == right.name 121 | && left.bodyName == right.bodyName 122 | && left.type == right.type 123 | && left.defaultValue == right.defaultValue 124 | && left.declaration == right.declaration 125 | && left.comment == right.comment 126 | } 127 | 128 | public var debugDescription: String { 129 | return "ARGUMENT: name = \(name); body name = \(bodyName); type = \(type)" 130 | } 131 | 132 | } 133 | 134 | 135 | public extension Sequence where Iterator.Element == ArgumentDescription { 136 | 137 | public subscript(argumentName: String) -> Iterator.Element? { 138 | for item in self { 139 | if item.name == argumentName { 140 | return item 141 | } 142 | } 143 | return nil 144 | } 145 | 146 | public func contains(argumentName: String) -> Bool { 147 | return nil != self[argumentName] 148 | } 149 | 150 | } 151 | -------------------------------------------------------------------------------- /Sources/Synopsis/Model/ClassDescription.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Project «Synopsis» 3 | // Created by Jeorge Taflanidi 4 | // 5 | 6 | 7 | import Foundation 8 | 9 | 10 | /** 11 | Class description. 12 | */ 13 | public struct ClassDescription: Extensible { 14 | 15 | /** 16 | Documentation comment above the class. 17 | */ 18 | public let comment: String? 19 | 20 | /** 21 | Class annotations. 22 | 23 | Class annotations are located inside the block comment above the class declaration. 24 | */ 25 | public let annotations: [Annotation] 26 | 27 | /** 28 | Class declaration line. 29 | */ 30 | public let declaration: Declaration 31 | 32 | /** 33 | Access visibility. 34 | */ 35 | public let accessibility: Accessibility 36 | 37 | /** 38 | Name of the class. 39 | */ 40 | public let name: String 41 | 42 | /** 43 | Inherited types: parent class/classes, protocols etc. 44 | */ 45 | public let inheritedTypes: [String] 46 | 47 | /** 48 | List of class properties. 49 | */ 50 | public let properties: [PropertyDescription] 51 | 52 | /** 53 | Class methods. 54 | */ 55 | public let methods: [MethodDescription] 56 | 57 | /** 58 | Write down own source code. 59 | */ 60 | public var verse: String { 61 | let commentStr: String 62 | if let commentExpl: String = comment, !commentExpl.isEmpty { 63 | commentStr = commentExpl.prefixEachLine(with: "/// ") + "\n" 64 | } else { 65 | commentStr = "" 66 | } 67 | 68 | let accessibilityStr: String = accessibility.verse.isEmpty ? "" : "\(accessibility.verse) " 69 | 70 | let inheritedTypesStr: String = inheritedTypes.isEmpty ? "" : ": " + inheritedTypes.joined(separator: ", ") 71 | 72 | let propertiesStr: String 73 | if properties.isEmpty { 74 | propertiesStr = "" 75 | } else { 76 | propertiesStr = properties.reduce("\n") { (result: String, property: PropertyDescription) -> String in 77 | if properties.last == property { 78 | return result + property.verse.indent + "\n" 79 | } 80 | return result + property.verse.indent + "\n\n" 81 | } 82 | } 83 | 84 | let methodsStr: String 85 | if methods.isEmpty { 86 | methodsStr = "" 87 | } else { 88 | methodsStr = methods.reduce("\n") { (result: String, method: MethodDescription) -> String in 89 | if methods.last == method { 90 | return result + method.verse.indent + "\n" 91 | } 92 | return result + method.verse.indent + "\n\n" 93 | } 94 | } 95 | 96 | return """ 97 | \(commentStr)\(accessibilityStr)class \(name)\(inheritedTypesStr) {\(propertiesStr)\(methodsStr)}\n 98 | """ 99 | } 100 | 101 | /** 102 | FFS make auto-public initializers @ Apple 103 | */ 104 | public init( 105 | comment: String?, 106 | annotations: [Annotation], 107 | declaration: Declaration, 108 | accessibility: Accessibility, 109 | name: String, 110 | inheritedTypes: [String], 111 | properties: [PropertyDescription], 112 | methods: [MethodDescription] 113 | ) { 114 | self.comment = comment 115 | self.annotations = annotations 116 | self.declaration = declaration 117 | self.accessibility = accessibility 118 | self.name = name 119 | self.inheritedTypes = inheritedTypes 120 | self.properties = properties 121 | self.methods = methods 122 | } 123 | 124 | public static func template( 125 | comment: String?, 126 | accessibility: Accessibility, 127 | name: String, 128 | inheritedTypes: [String], 129 | properties: [PropertyDescription], 130 | methods: [MethodDescription] 131 | ) -> ClassDescription { 132 | return ClassDescription( 133 | comment: comment, 134 | annotations: [], 135 | declaration: Declaration.mock, 136 | accessibility: accessibility, 137 | name: name, 138 | inheritedTypes: inheritedTypes, 139 | properties: properties, 140 | methods: methods 141 | ) 142 | } 143 | 144 | public var debugDescription: String { 145 | if inheritedTypes.isEmpty { 146 | return "CLASS: name = \(name)" 147 | } 148 | return "CLASS: name = \(name); inherited = \(inheritedTypes.joined(separator: ", "))" 149 | } 150 | 151 | } 152 | -------------------------------------------------------------------------------- /Sources/Synopsis/Model/StructDescription.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Project «Synopsis» 3 | // Created by Jeorge Taflanidi 4 | // 5 | 6 | 7 | import Foundation 8 | 9 | 10 | /** 11 | Struct description. 12 | */ 13 | public struct StructDescription: CustomDebugStringConvertible, Extensible { 14 | 15 | /** 16 | Documentation comment above the struct. 17 | */ 18 | public let comment: String? 19 | 20 | /** 21 | Struct annotations. 22 | 23 | Struct annotations are located inside the block comment above the struct declaration. 24 | */ 25 | public let annotations: [Annotation] 26 | 27 | /** 28 | Struct declaration line. 29 | */ 30 | public let declaration: Declaration 31 | 32 | /** 33 | Access visibility. 34 | */ 35 | public let accessibility: Accessibility 36 | 37 | /** 38 | Name of the struct. 39 | */ 40 | public let name: String 41 | 42 | /** 43 | Inherited protocols. 44 | */ 45 | public let inheritedTypes: [String] 46 | 47 | /** 48 | List of struct properties. 49 | */ 50 | public let properties: [PropertyDescription] 51 | 52 | /** 53 | Struct methods. 54 | */ 55 | public let methods: [MethodDescription] 56 | 57 | /** 58 | Write down own source code. 59 | */ 60 | public var verse: String { 61 | let commentStr: String 62 | if let commentExpl: String = comment, !commentExpl.isEmpty { 63 | commentStr = commentExpl.prefixEachLine(with: "/// ") + "\n" 64 | } else { 65 | commentStr = "" 66 | } 67 | 68 | let accessibilityStr: String = accessibility.verse.isEmpty ? "" : "\(accessibility.verse) " 69 | 70 | let inheritedTypesStr: String = inheritedTypes.isEmpty ? "" : ": " + inheritedTypes.joined(separator: ", ") 71 | 72 | let propertiesStr: String 73 | if properties.isEmpty { 74 | propertiesStr = "" 75 | } else { 76 | propertiesStr = properties.reduce("\n") { (result: String, property: PropertyDescription) -> String in 77 | if properties.last == property { 78 | return result + property.verse.indent + "\n" 79 | } 80 | return result + property.verse.indent + "\n\n" 81 | } 82 | } 83 | 84 | let methodsStr: String 85 | if methods.isEmpty { 86 | methodsStr = "" 87 | } else { 88 | methodsStr = methods.reduce("\n") { (result: String, method: MethodDescription) -> String in 89 | if methods.last == method { 90 | return result + method.verse.indent + "\n" 91 | } 92 | return result + method.verse.indent + "\n\n" 93 | } 94 | } 95 | 96 | return """ 97 | \(commentStr)\(accessibilityStr)struct \(name)\(inheritedTypesStr) {\(propertiesStr)\(methodsStr)}\n 98 | """ 99 | } 100 | 101 | /** 102 | FFS make auto-public initializers @ Apple 103 | */ 104 | public init( 105 | comment: String?, 106 | annotations: [Annotation], 107 | declaration: Declaration, 108 | accessibility: Accessibility, 109 | name: String, 110 | inheritedTypes: [String], 111 | properties: [PropertyDescription], 112 | methods: [MethodDescription] 113 | ) { 114 | self.comment = comment 115 | self.annotations = annotations 116 | self.declaration = declaration 117 | self.accessibility = accessibility 118 | self.name = name 119 | self.inheritedTypes = inheritedTypes 120 | self.properties = properties 121 | self.methods = methods 122 | } 123 | 124 | /** 125 | Make a template property for later code generation. 126 | */ 127 | public static func template( 128 | comment: String?, 129 | accessibility: Accessibility, 130 | name: String, 131 | inheritedTypes: [String], 132 | properties: [PropertyDescription], 133 | methods: [MethodDescription] 134 | ) -> StructDescription { 135 | return StructDescription( 136 | comment: comment, 137 | annotations: [], 138 | declaration: Declaration.mock, 139 | accessibility: accessibility, 140 | name: name, 141 | inheritedTypes: inheritedTypes, 142 | properties: properties, 143 | methods: methods 144 | ) 145 | } 146 | 147 | public var debugDescription: String { 148 | if inheritedTypes.isEmpty { 149 | return "STRUCT: name = \(name)" 150 | } 151 | return "STRUCT: name = \(name); inherited = \(inheritedTypes.joined(separator: ", "))" 152 | } 153 | 154 | } 155 | -------------------------------------------------------------------------------- /Sources/Synopsis/Model/ProtocolDescription.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Project «Synopsis» 3 | // Created by Jeorge Taflanidi 4 | // 5 | 6 | 7 | import Foundation 8 | 9 | 10 | /** 11 | Protocol description. 12 | */ 13 | public struct ProtocolDescription: CustomDebugStringConvertible, Extensible { 14 | 15 | /** 16 | Documentation comment above the protocol. 17 | */ 18 | public let comment: String? 19 | 20 | /** 21 | Protocol annotations. 22 | 23 | Protocol annotations are located inside the block comment above the protocol declaration. 24 | */ 25 | public let annotations: [Annotation] 26 | 27 | /** 28 | Protocol declaration line. 29 | */ 30 | public let declaration: Declaration 31 | 32 | /** 33 | Access visibility. 34 | */ 35 | public let accessibility: Accessibility 36 | 37 | /** 38 | Name of the protocol. 39 | */ 40 | public let name: String 41 | 42 | /** 43 | Inherited protocols. 44 | */ 45 | public let inheritedTypes: [String] 46 | 47 | /** 48 | List of protocol properties. 49 | */ 50 | public let properties: [PropertyDescription] 51 | 52 | /** 53 | Protocol methods. 54 | */ 55 | public let methods: [MethodDescription] 56 | 57 | /** 58 | Write down own source code. 59 | */ 60 | public var verse: String { 61 | let commentStr: String 62 | if let commentExpl: String = comment, !commentExpl.isEmpty { 63 | commentStr = commentExpl.prefixEachLine(with: "/// ") + "\n" 64 | } else { 65 | commentStr = "" 66 | } 67 | 68 | let accessibilityStr: String = accessibility.verse.isEmpty ? "" : "\(accessibility.verse) " 69 | 70 | let inheritedTypesStr: String = inheritedTypes.isEmpty ? "" : ": " + inheritedTypes.joined(separator: ", ") 71 | 72 | let propertiesStr: String 73 | if properties.isEmpty { 74 | propertiesStr = "" 75 | } else { 76 | propertiesStr = properties.reduce("\n") { (result: String, property: PropertyDescription) -> String in 77 | if properties.last == property { 78 | return result + property.verse.indent + "\n" 79 | } 80 | return result + property.verse.indent + "\n\n" 81 | } 82 | } 83 | 84 | let methodsStr: String 85 | if methods.isEmpty { 86 | methodsStr = "" 87 | } else { 88 | methodsStr = methods.reduce("\n") { (result: String, method: MethodDescription) -> String in 89 | if methods.last == method { 90 | return result + method.verse.indent + "\n" 91 | } 92 | return result + method.verse.indent + "\n\n" 93 | } 94 | } 95 | 96 | return """ 97 | \(commentStr)\(accessibilityStr)protocol \(name)\(inheritedTypesStr) {\(propertiesStr)\(methodsStr)}\n 98 | """ 99 | } 100 | 101 | /** 102 | FFS make auto-public initializers @ Apple 103 | */ 104 | public init( 105 | comment: String?, 106 | annotations: [Annotation], 107 | declaration: Declaration, 108 | accessibility: Accessibility, 109 | name: String, 110 | inheritedTypes: [String], 111 | properties: [PropertyDescription], 112 | methods: [MethodDescription] 113 | ) { 114 | self.comment = comment 115 | self.annotations = annotations 116 | self.declaration = declaration 117 | self.accessibility = accessibility 118 | self.name = name 119 | self.inheritedTypes = inheritedTypes 120 | self.properties = properties 121 | self.methods = methods 122 | } 123 | 124 | /** 125 | Make a template property for later code generation. 126 | */ 127 | public static func template( 128 | comment: String?, 129 | accessibility: Accessibility, 130 | name: String, 131 | inheritedTypes: [String], 132 | properties: [PropertyDescription], 133 | methods: [MethodDescription] 134 | ) -> ProtocolDescription { 135 | return ProtocolDescription( 136 | comment: comment, 137 | annotations: [], 138 | declaration: Declaration.mock, 139 | accessibility: accessibility, 140 | name: name, 141 | inheritedTypes: inheritedTypes, 142 | properties: properties, 143 | methods: methods 144 | ) 145 | } 146 | 147 | public var debugDescription: String { 148 | if inheritedTypes.isEmpty { 149 | return "PROTOCOL: name = \(name)" 150 | } 151 | return "PROTOCOL: name = \(name); inherited = \(inheritedTypes.joined(separator: ", "))" 152 | } 153 | 154 | } 155 | -------------------------------------------------------------------------------- /Sources/Synopsis/Utility/EnumDescriptionParser.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Project «Synopsis» 3 | // Created by Jeorge Taflanidi 4 | // 5 | 6 | 7 | import Foundation 8 | import SourceKittenFramework 9 | 10 | 11 | class EnumDescriptionParser { 12 | 13 | func parse(files pathList: [URL]) -> ParsingResult { 14 | var ioErrors: [SynopsisError] = [] 15 | 16 | let files: [GoodFile] = pathList.flatMap { (fileURL: URL) -> GoodFile? in 17 | if let file = File(path: fileURL.path) { 18 | return GoodFile(path: fileURL, file: file) 19 | } 20 | 21 | ioErrors.append(SynopsisError.errorReadingFile(file: fileURL)) 22 | return nil 23 | } 24 | 25 | let result: ParsingResult = parse(files: files) 26 | return ParsingResult(errors: result.errors + ioErrors, models: result.models) 27 | } 28 | 29 | func parse(files: [GoodFile]) -> ParsingResult { 30 | var compilingErrors: [SynopsisError] = [] 31 | 32 | let compiledStructures: [CompiledStructure] = files.flatMap { (goodFile: GoodFile) -> CompiledStructure? in 33 | if let structure: CompiledStructure = CompiledStructure(file: goodFile) { 34 | return structure 35 | } 36 | 37 | compilingErrors.append(SynopsisError.errorCompilingFile(file: goodFile.path)) 38 | return nil 39 | } 40 | 41 | let result: [EnumDescription] = translate(compiledStructures) 42 | return ParsingResult(errors: compilingErrors, models: result) 43 | } 44 | 45 | } 46 | 47 | 48 | private extension EnumDescriptionParser { 49 | 50 | func translate(_ structures: [CompiledStructure]) -> [EnumDescription] { 51 | return structures.flatMap { translate($0) } 52 | } 53 | 54 | func translate(_ structure: CompiledStructure) -> [EnumDescription] { 55 | let topStructureElements = structure.topElements 56 | 57 | let enums: [EnumDescription] = 58 | topStructureElements 59 | .filter { isRawEnumDescription($0) } 60 | .map { (rawEnumDescription: [String: AnyObject]) -> EnumDescription in 61 | let declarationOffset: Int = rawEnumDescription.offset 62 | let declarationRawText: String = rawEnumDescription.parsedDeclaration 63 | let rawStructureElements = rawEnumDescription.subsctructure 64 | 65 | let comment: String? = rawEnumDescription.comment 66 | let annotations: [Annotation] = nil != comment ? AnnotationParser().parse(comment: comment!) : [] 67 | let accessibility: Accessibility = Accessibility.deduce(forRawStructureElement: rawEnumDescription) 68 | let name: String = rawEnumDescription.name 69 | let inheritedTypes: [String] = rawEnumDescription.inheritedTypes 70 | 71 | let declaration: Declaration = 72 | Declaration( 73 | filePath: structure.file.path, 74 | fileContents: structure.file.file.contents, 75 | rawText: declarationRawText, 76 | offset: declarationOffset 77 | ) 78 | 79 | let cases: [EnumCase] = 80 | EnumCaseParser().parse(rawStructureElements: rawStructureElements, file: structure.file) 81 | 82 | let properties: [PropertyDescription] = 83 | PropertyDescriptionParser().parse(rawStructureElements: rawStructureElements, file: structure.file) 84 | 85 | let methods: [MethodDescription] = 86 | MethodDescriptionParser().parse(rawStructureElements: rawStructureElements, file: structure.file) 87 | 88 | return EnumDescription( 89 | comment: comment, 90 | annotations: annotations, 91 | declaration: declaration, 92 | accessibility: accessibility, 93 | name: name, 94 | inheritedTypes: inheritedTypes, 95 | cases: cases, 96 | properties: properties, 97 | methods: methods 98 | ) 99 | } 100 | 101 | return enums 102 | } 103 | 104 | func isRawEnumDescription(_ element: [String: AnyObject]) -> Bool { 105 | guard let kind: String = element.kind 106 | else { return false } 107 | return SwiftDeclarationKind.`enum`.rawValue == kind 108 | } 109 | 110 | } 111 | -------------------------------------------------------------------------------- /Sources/Synopsis/Utility/PropertyDescriptionParser.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Project «Synopsis» 3 | // Created by Jeorge Taflanidi 4 | // 5 | 6 | 7 | import Foundation 8 | import SourceKittenFramework 9 | 10 | 11 | class PropertyDescriptionParser { 12 | 13 | func parse(rawStructureElements: [[String: AnyObject]], file: GoodFile) -> [PropertyDescription] { 14 | return rawStructureElements 15 | .filter { isInstanceVariableDescription($0) } 16 | .map { (rawPropertyDescription: [String: AnyObject]) -> PropertyDescription in 17 | let parsedDeclaration: String = rawPropertyDescription.parsedDeclaration 18 | let declarationOffset: Int = rawPropertyDescription.offset 19 | 20 | let comment: String? = rawPropertyDescription.comment 21 | let annotations: [Annotation] = nil != comment ? AnnotationParser().parse(comment: comment!) : [] 22 | let accessibility: Accessibility = Accessibility.deduce(forRawStructureElement: rawPropertyDescription) 23 | let constant: Bool = parsedDeclaration.contains("let ") 24 | let name: String = rawPropertyDescription.name 25 | let type: TypeDescription = TypeParser().parse(rawDescription: rawPropertyDescription) 26 | let defaultValue: String? = getDefaultValue(fromParsedDeclaration: parsedDeclaration) 27 | let body: String? = getBody(rawPropertyDescription: rawPropertyDescription, file: file) 28 | 29 | let kind: PropertyDescription.Kind = getKind(rawPropertyDescription: rawPropertyDescription) 30 | 31 | let declaration: Declaration = 32 | Declaration( 33 | filePath: file.path, 34 | fileContents: file.file.contents, 35 | rawText: parsedDeclaration, 36 | offset: declarationOffset 37 | ) 38 | 39 | return PropertyDescription( 40 | comment: comment, 41 | annotations: annotations, 42 | accessibility: accessibility, 43 | constant: constant, 44 | name: name, 45 | type: type, 46 | defaultValue: defaultValue, 47 | declaration: declaration, 48 | kind: kind, 49 | body: body 50 | ) 51 | } 52 | } 53 | 54 | } 55 | 56 | 57 | private extension PropertyDescriptionParser { 58 | 59 | func isInstanceVariableDescription(_ element: [String: AnyObject]) -> Bool { 60 | guard let kind: String = element.kind 61 | else { return false } 62 | return SwiftDeclarationKind.varInstance.rawValue == kind 63 | || SwiftDeclarationKind.varClass.rawValue == kind 64 | || SwiftDeclarationKind.varStatic.rawValue == kind 65 | } 66 | 67 | func getDefaultValue(fromParsedDeclaration parsedDeclaration: String) -> String? { 68 | let lex = LexemeString(parsedDeclaration) 69 | 70 | for index in parsedDeclaration.indices { 71 | if "=" == parsedDeclaration[index] && lex.inSourceCodeRange(index) { 72 | let defaultValueStart: String.Index = parsedDeclaration.index(after: index) 73 | return parsedDeclaration[defaultValueStart...].trimmingCharacters(in: CharacterSet.whitespacesAndNewlines) 74 | } 75 | } 76 | 77 | return nil 78 | } 79 | 80 | func getBody(rawPropertyDescription: [String: AnyObject], file: GoodFile) -> String? { 81 | guard 82 | let bodyOffset: Int = rawPropertyDescription.bodyOffset, 83 | let bodyLength: Int = rawPropertyDescription.bodyLength 84 | else { 85 | return nil 86 | } 87 | 88 | let fileContents: String = file.file.contents 89 | 90 | let bodyStartIndex: String.Index = fileContents.index(fileContents.startIndex, offsetBy: bodyOffset) 91 | let bodyEndIndex: String.Index = fileContents.index(bodyStartIndex, offsetBy: bodyLength) 92 | 93 | return String(fileContents[bodyStartIndex.. PropertyDescription.Kind { 97 | guard let kind: String = rawPropertyDescription.kind 98 | else { return PropertyDescription.Kind.instance } 99 | 100 | switch kind { 101 | case SwiftDeclarationKind.varStatic.rawValue: return PropertyDescription.Kind.`static` 102 | case SwiftDeclarationKind.varClass.rawValue: return PropertyDescription.Kind.`class` 103 | 104 | default: return PropertyDescription.Kind.instance 105 | } 106 | } 107 | 108 | } 109 | -------------------------------------------------------------------------------- /Tests/SynopsisTests/Utility/MethodDescriptionParserTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Project «Synopsis» 3 | // Created by Jeorge Taflanidi 4 | // 5 | 6 | 7 | import XCTest 8 | @testable import Synopsis 9 | 10 | 11 | class MethodDescriptionParserTests: SynopsisTestCase { 12 | 13 | override func setUp() { 14 | super.setUp() 15 | storeContents(topLevelMethods, asFile: "TopLevelMethods.swift") 16 | storeContents(genericReturnType, asFile: "GenericReturnType.swift") 17 | storeContents(multilineMethod, asFile: "Multiline.swift") 18 | } 19 | 20 | override func tearDown() { 21 | deleteFile(named: "Multiline.swift") 22 | deleteFile(named: "GenericReturnType.swift") 23 | deleteFile(named: "TopLevelMethods.swift") 24 | super.tearDown() 25 | } 26 | 27 | func testParse_topLevelMethods_returnsAsExpected() { 28 | let inputFile: URL = urlForFile(named: "TopLevelMethods.swift") 29 | let parser = FunctionDescriptionParser() 30 | 31 | let result: ParsingResult = parser.parse(files: [inputFile]) 32 | 33 | XCTAssertEqual(result.models.count, 1) 34 | let topLevelFunction: FunctionDescription = result.models.first! 35 | 36 | XCTAssertEqual( 37 | topLevelFunction, 38 | FunctionDescription( 39 | comment: nil, 40 | annotations: [], 41 | accessibility: Accessibility.`internal`, 42 | name: "topLevelFunction()", 43 | arguments: [], 44 | returnType: TypeDescription.void, 45 | declaration: Declaration( 46 | filePath: inputFile, 47 | rawText: "func topLevelFunction()", 48 | offset: 0, 49 | lineNumber: 1, 50 | columnNumber: 1 51 | ), 52 | kind: .free, 53 | body: "\n" 54 | ) 55 | ) 56 | } 57 | 58 | func testParse_complexReturnType_returnsAsExpected() { 59 | let inputFile: URL = urlForFile(named: "GenericReturnType.swift") 60 | let parser = FunctionDescriptionParser() 61 | 62 | let result: ParsingResult = parser.parse(files: [inputFile]) 63 | 64 | XCTAssertEqual(result.models.count, 1) 65 | let topLevelFunction: FunctionDescription = result.models.first! 66 | 67 | XCTAssertEqual( 68 | topLevelFunction, 69 | FunctionDescription( 70 | comment: nil, 71 | annotations: [], 72 | accessibility: Accessibility.`internal`, 73 | name: "genericReturnType()", 74 | arguments: [], 75 | returnType: TypeDescription.generic(name: "ServiceCall", constraints: [TypeDescription.void]), 76 | declaration: Declaration( 77 | filePath: inputFile, 78 | rawText: "func genericReturnType() /* bla bla */ -> ServiceCall", 79 | offset: 0, 80 | lineNumber: 1, 81 | columnNumber: 1 82 | ), 83 | kind: .free, 84 | body: "\n" 85 | ) 86 | ) 87 | } 88 | 89 | func testParse_multilineMethod_returnsAsExpected() { 90 | let inputFile: URL = urlForFile(named: "Multiline.swift") 91 | let parser = FunctionDescriptionParser() 92 | 93 | let result: ParsingResult = parser.parse(files: [inputFile]) 94 | 95 | XCTAssertEqual(result.models.count, 1) 96 | let topLevelFunction: FunctionDescription = result.models.first! 97 | 98 | XCTAssertEqual( 99 | topLevelFunction, 100 | FunctionDescription( 101 | comment: nil, 102 | annotations: [], 103 | accessibility: Accessibility.`internal`, 104 | name: "some(other:)", 105 | arguments: [ 106 | ArgumentDescription( 107 | name: "other", 108 | bodyName: "other", 109 | type: TypeDescription.string, 110 | defaultValue: nil, 111 | annotations: [], 112 | declaration: nil, 113 | comment: nil 114 | ) 115 | ], 116 | returnType: TypeDescription.object(name: "Something"), 117 | declaration: Declaration( 118 | filePath: inputFile, 119 | rawText: "func some(\n other: String\n) -> Something" , 120 | offset: 0, 121 | lineNumber: 1, 122 | columnNumber: 1 123 | ), 124 | kind: .free, 125 | body: "\n print(other)\n return 1\n" 126 | ) 127 | ) 128 | } 129 | 130 | static var allTests = [ 131 | ("testParse_topLevelMethods_returnsAsExpected", testParse_topLevelMethods_returnsAsExpected), 132 | ("testParse_complexReturnType_returnsAsExpected", testParse_complexReturnType_returnsAsExpected), 133 | ("testParse_multilineMethod_returnsAsExpected", testParse_multilineMethod_returnsAsExpected), 134 | ] 135 | 136 | } 137 | 138 | 139 | let topLevelMethods = """ 140 | func topLevelFunction() { 141 | } 142 | """ 143 | 144 | 145 | let genericReturnType = """ 146 | func genericReturnType() /* bla bla */ -> ServiceCall { 147 | } 148 | """ 149 | 150 | 151 | let multilineMethod = """ 152 | func some( 153 | other: String 154 | ) -> Something { 155 | print(other) 156 | return 1 157 | } 158 | """ 159 | -------------------------------------------------------------------------------- /Sources/Synopsis/Model/PropertyDescription.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Project «Synopsis» 3 | // Created by Jeorge Taflanidi 4 | // 5 | 6 | 7 | import Foundation 8 | 9 | 10 | /** 11 | Property description. 12 | */ 13 | public struct PropertyDescription: Equatable, CustomDebugStringConvertible { 14 | 15 | /** 16 | Documentation comment. 17 | */ 18 | public let comment: String? 19 | 20 | /** 21 | Property annotations. 22 | */ 23 | public let annotations: [Annotation] 24 | 25 | /** 26 | Access visibility. 27 | */ 28 | public let accessibility: Accessibility 29 | 30 | /** 31 | Property is `let`. 32 | 33 | Otherwise `var`. 34 | */ 35 | public let constant: Bool 36 | 37 | /** 38 | Property name. 39 | */ 40 | public let name: String 41 | 42 | /** 43 | Property type. 44 | */ 45 | public let type: TypeDescription 46 | 47 | /** 48 | Raw default value. 49 | */ 50 | public let defaultValue: String? 51 | 52 | /** 53 | Property declaration line. 54 | */ 55 | public let declaration: Declaration 56 | 57 | /** 58 | Kind of a property. 59 | */ 60 | public let kind: Kind 61 | 62 | /** 63 | Getters, setters, didSetters, willSetters etc. 64 | */ 65 | public let body: String? 66 | 67 | /** 68 | Write down own source code. 69 | */ 70 | public var verse: String { 71 | let commentStr: String 72 | if let commentExpl: String = comment, !commentExpl.isEmpty { 73 | commentStr = commentExpl.prefixEachLine(with: "/// ") + "\n" 74 | } else { 75 | commentStr = "" 76 | } 77 | 78 | let accessibilityStr: String = accessibility.verse.isEmpty ? "" : "\(accessibility.verse) " 79 | let kindStr: String = kind.verse.isEmpty ? "" : "\(kind.verse) " 80 | let constantStr: String = constant ? "let" : "var" 81 | let bodyStr: String = nil != body ? " {\n\(body!.indent)\n}" : "" 82 | let defaultValueStr: String = nil != defaultValue ? " = \(defaultValue!)" : "" 83 | 84 | return """ 85 | \(commentStr)\(accessibilityStr)\(kindStr)\(constantStr) \(name): \(type.verse)\(defaultValueStr)\(bodyStr) 86 | """ 87 | } 88 | 89 | /** 90 | FFS make auto-public initializers @ Apple 91 | */ 92 | public init( 93 | comment: String?, 94 | annotations: [Annotation], 95 | accessibility: Accessibility, 96 | constant: Bool, 97 | name: String, 98 | type: TypeDescription, 99 | defaultValue: String?, 100 | declaration: Declaration, 101 | kind: Kind, 102 | body: String? 103 | ) { 104 | self.comment = comment 105 | self.annotations = annotations 106 | self.accessibility = accessibility 107 | self.constant = constant 108 | self.name = name 109 | self.type = type 110 | self.defaultValue = defaultValue 111 | self.declaration = declaration 112 | self.kind = kind 113 | self.body = body 114 | } 115 | 116 | /** 117 | Make a template property for later code generation. 118 | */ 119 | public static func template( 120 | comment: String?, 121 | accessibility: Accessibility, 122 | constant: Bool, 123 | name: String, 124 | type: TypeDescription, 125 | defaultValue: String?, 126 | kind: Kind, 127 | body: String? 128 | ) -> PropertyDescription { 129 | return PropertyDescription( 130 | comment: comment, 131 | annotations: [], 132 | accessibility: accessibility, 133 | constant: constant, 134 | name: name, 135 | type: type, 136 | defaultValue: defaultValue, 137 | declaration: Declaration.mock, 138 | kind: kind, 139 | body: body 140 | ) 141 | } 142 | 143 | public static func ==(left: PropertyDescription, right: PropertyDescription) -> Bool { 144 | return left.comment == right.comment 145 | && left.annotations == right.annotations 146 | && left.accessibility == right.accessibility 147 | && left.constant == right.constant 148 | && left.name == right.name 149 | && left.type == right.type 150 | && left.defaultValue == right.defaultValue 151 | && left.declaration == right.declaration 152 | && left.kind == right.kind 153 | && left.body == right.body 154 | } 155 | 156 | public var debugDescription: String { 157 | return "PROPERTY: name = \(name); type = \(type); constant = \(constant)" 158 | } 159 | 160 | public enum Kind { 161 | case `class` 162 | case `static` 163 | case instance 164 | 165 | public var verse: String { 166 | switch self { 167 | case .`class`: return "class" 168 | case .`static`: return "static" 169 | case .instance: return "" 170 | } 171 | } 172 | 173 | // TODO support global, local & parameter kinds 174 | } 175 | 176 | } 177 | 178 | 179 | public extension Sequence where Iterator.Element == PropertyDescription { 180 | 181 | public subscript(propertyName: String) -> Iterator.Element? { 182 | for item in self { 183 | if item.name == propertyName { 184 | return item 185 | } 186 | } 187 | return nil 188 | } 189 | 190 | public func contains(propertyName: String) -> Bool { 191 | return nil != self[propertyName] 192 | } 193 | 194 | } 195 | -------------------------------------------------------------------------------- /Tests/SynopsisTests/Utility/StructDescriptionParserTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Project «Synopsis» 3 | // Created by Jeorge Taflanidi 4 | // 5 | 6 | 7 | import XCTest 8 | @testable import Synopsis 9 | 10 | 11 | class StructDescriptionParserTests: SynopsisTestCase { 12 | 13 | override func setUp() { 14 | super.setUp() 15 | storeContents(basicStruct, asFile: "BasicStruct.swift") 16 | } 17 | 18 | override func tearDown() { 19 | deleteFile(named: "BasicStruct.swift") 20 | super.tearDown() 21 | } 22 | 23 | func testParse_basicFile_returnsAsExpected() { 24 | let inputFile: URL = urlForFile(named: "BasicStruct.swift") 25 | let parser = StructDescriptionParser() 26 | let result: ParsingResult = parser.parse(files: [inputFile]) 27 | 28 | let expectedAnnotations: [Annotation] = [] 29 | 30 | let expectedDeclaration: Declaration = Declaration( 31 | filePath: inputFile, 32 | rawText: "public struct Person: Codable", 33 | offset: 174, 34 | lineNumber: 21, 35 | columnNumber: 8 36 | ) 37 | 38 | let expectedProperties: [PropertyDescription] = [ 39 | PropertyDescription( 40 | comment: nil, 41 | annotations: [], 42 | accessibility: Accessibility.`public`, 43 | constant: true, 44 | name: "name", 45 | type: TypeDescription.string, 46 | defaultValue: nil, 47 | declaration: Declaration( 48 | filePath: inputFile, 49 | rawText: "public let name: String", 50 | offset: 210, 51 | lineNumber: 22, 52 | columnNumber: 12 53 | ), 54 | kind: .instance, 55 | body: nil 56 | ), 57 | PropertyDescription( 58 | comment: nil, 59 | annotations: [], 60 | accessibility: Accessibility.`public`, 61 | constant: true, 62 | name: "surname", 63 | type: TypeDescription.string, 64 | defaultValue: nil, 65 | declaration: Declaration( 66 | filePath: inputFile, 67 | rawText: "public let surname: String", 68 | offset: 241, 69 | lineNumber: 23, 70 | columnNumber: 12 71 | ), 72 | kind: .instance, 73 | body: nil 74 | ), 75 | ] 76 | 77 | let expectedMethods: [MethodDescription] = [ 78 | MethodDescription( 79 | comment: nil, 80 | annotations: [], 81 | accessibility: Accessibility.`public`, 82 | name: "init(name:surname:)", 83 | arguments: [ 84 | ArgumentDescription( 85 | name: "name", 86 | bodyName: "name", 87 | type: TypeDescription.string, 88 | defaultValue: "\"John\"", 89 | annotations: [], 90 | declaration: nil, 91 | comment: nil 92 | ), 93 | ArgumentDescription( 94 | name: "surname", 95 | bodyName: "surname", 96 | type: TypeDescription.string, 97 | defaultValue: "\"Appleseed\"", 98 | annotations: [], 99 | declaration: nil, 100 | comment: nil 101 | ), 102 | ], 103 | returnType: TypeDescription.object(name: "Person"), 104 | declaration: Declaration( 105 | filePath: inputFile, 106 | rawText: "public init(name: String = \"John\", surname: String = \"Appleseed\")", 107 | offset: 273, 108 | lineNumber: 25, 109 | columnNumber: 12 110 | ), 111 | kind: .instance, 112 | body: "\n self.name = name\n self.surname = surname\n " 113 | ), 114 | ] 115 | 116 | XCTAssertEqual(result.models.count, 1) 117 | let basicStructDescription: StructDescription = result.models.first! 118 | 119 | XCTAssertEqual(basicStructDescription.comment, "Struct docs") 120 | XCTAssertEqual(basicStructDescription.annotations, expectedAnnotations) 121 | XCTAssertEqual(basicStructDescription.declaration, expectedDeclaration) 122 | XCTAssertEqual(basicStructDescription.accessibility, Accessibility.`public`) 123 | XCTAssertEqual(basicStructDescription.name, "Person") 124 | XCTAssertEqual(basicStructDescription.inheritedTypes.first, "Codable") 125 | XCTAssertEqual(basicStructDescription.properties, expectedProperties) 126 | XCTAssertEqual(basicStructDescription.methods, expectedMethods) 127 | } 128 | 129 | static var allTests = [ 130 | ("testParse_basicFile_returnsAsExpected", testParse_basicFile_returnsAsExpected), 131 | ] 132 | } 133 | 134 | 135 | let basicStruct = """ 136 | // 137 | // Basic.swift 138 | // SynopsisTests 139 | // 140 | // Created by John Appleseed on 10.11.29H. 141 | // 142 | 143 | 144 | import Foundation 145 | 146 | 147 | /** 148 | Basic docs 149 | */ 150 | class Basic {} 151 | 152 | 153 | /** 154 | Struct docs 155 | */ 156 | public struct Person: Codable { 157 | public let name: String 158 | public let surname: String 159 | 160 | public init(name: String = "John", surname: String = "Appleseed") { 161 | self.name = name 162 | self.surname = surname 163 | } 164 | } 165 | """ 166 | -------------------------------------------------------------------------------- /Sources/Synopsis/Model/TypeDescription.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Project «Synopsis» 3 | // Created by Jeorge Taflanidi 4 | // 5 | 6 | 7 | import Foundation 8 | 9 | 10 | /** 11 | Type of class properties, methods, method arguments, variables etc. 12 | */ 13 | public indirect enum TypeDescription: Equatable, CustomStringConvertible { 14 | 15 | /** 16 | Boolean. 17 | */ 18 | case boolean 19 | 20 | /** 21 | Anything, which contains "Int" in it: Int, Int16, Int32, Int64, UInt etc. 22 | */ 23 | case integer 24 | 25 | /** 26 | Float. 27 | */ 28 | case floatingPoint 29 | 30 | /** 31 | Double. 32 | */ 33 | case doublePrecision 34 | 35 | /** 36 | String 37 | */ 38 | case string 39 | 40 | /** 41 | Date (formerly known as NSDate). 42 | */ 43 | case date 44 | 45 | /** 46 | Data (formerly known as NSData). 47 | */ 48 | case data 49 | 50 | /** 51 | Void. 52 | */ 53 | case void 54 | 55 | /** 56 | Anything optional; wraps actual type. 57 | */ 58 | case optional(wrapped: TypeDescription) 59 | 60 | /** 61 | Classes, structures, enums & protocol. Except for Date, Data and collections of any kind. 62 | */ 63 | case object(name: String) 64 | 65 | /** 66 | Array collection. 67 | */ 68 | case array(element: TypeDescription) 69 | 70 | /** 71 | Map/dictionary collection. 72 | */ 73 | case map(key: TypeDescription, value: TypeDescription) 74 | 75 | /** 76 | Generic type. 77 | 78 | Like `object`, contains type name and also contains type for item in corner brakets. 79 | */ 80 | case generic(name: String, constraints: [TypeDescription]) 81 | 82 | /** 83 | Write down own source code. 84 | 85 | ???: use .description 86 | */ 87 | public var verse: String { 88 | switch self { 89 | case .boolean: return "Bool" 90 | case .integer: return "Int" 91 | case .floatingPoint: return "Float" 92 | case .doublePrecision: return "Double" 93 | case .string: return "String" 94 | case .date: return "Date" 95 | case .data: return "Data" 96 | case .void: return "Void" 97 | case .optional(let wrapped): return "\(wrapped.verse)?" 98 | case .object(let name): return name 99 | case .array(let element): return "[\(element.verse)]" 100 | case .map(let key, let value): return "[\(key.verse): \(value.verse)]" 101 | case .generic(let name, let constraints): 102 | let constraintsString: String = constraints.map { "\($0.verse)" }.joined(separator: ", ") 103 | return "\(name)<\(constraintsString)>" 104 | } 105 | } 106 | 107 | public var description: String { 108 | switch self { 109 | case .boolean: return "Bool" 110 | case .integer: return "Int" 111 | case .floatingPoint: return "Float" 112 | case .doublePrecision: return "Double" 113 | case .string: return "String" 114 | case .date: return "Date" 115 | case .data: return "Data" 116 | case .void: return "Void" 117 | case let .optional(wrapped): return "\(wrapped)?" 118 | case let .object(name): return "\(name)" 119 | case let .array(item): return "[\(item)]" 120 | case let .map(key, value): return "[\(key): \(value)]" 121 | case let .generic(name, constraints): 122 | let constraintsString: String = constraints.map { "\($0)" }.joined(separator: ", ") 123 | return "\(name)<\(constraintsString)>" 124 | } 125 | } 126 | 127 | public static func ==(left: TypeDescription, right: TypeDescription) -> Bool { 128 | switch (left, right) { 129 | case (TypeDescription.boolean, TypeDescription.boolean): 130 | return true 131 | 132 | case (TypeDescription.integer, TypeDescription.integer): 133 | return true 134 | 135 | case (TypeDescription.floatingPoint, TypeDescription.floatingPoint): 136 | return true 137 | 138 | case (TypeDescription.doublePrecision, TypeDescription.doublePrecision): 139 | return true 140 | 141 | case (TypeDescription.date, TypeDescription.date): 142 | return true 143 | 144 | case (TypeDescription.data, TypeDescription.data): 145 | return true 146 | 147 | case (TypeDescription.string, TypeDescription.string): 148 | return true 149 | 150 | case (TypeDescription.void, TypeDescription.void): 151 | return true 152 | 153 | case (let TypeDescription.optional(wrappedLeft), let TypeDescription.optional(wrappedRight)): 154 | return wrappedLeft == wrappedRight 155 | 156 | case (let TypeDescription.object(name: leftName), let TypeDescription.object(name: rightName)): 157 | return leftName == rightName 158 | 159 | case (let TypeDescription.array(element: leftItem), let TypeDescription.array(element: rightItem)): 160 | return leftItem == rightItem 161 | 162 | case (let TypeDescription.map(key: leftKey, value: leftValue), let TypeDescription.map(key: rightKey, value: rightValue)): 163 | return leftKey == rightKey 164 | && leftValue == rightValue 165 | 166 | case (let TypeDescription.generic(name: leftName, constraints: leftConstraints), let TypeDescription.generic(name: rightName, constraints: rightConstraints)): 167 | return leftName == rightName 168 | && leftConstraints == rightConstraints 169 | 170 | default: 171 | return false 172 | } 173 | } 174 | 175 | } 176 | -------------------------------------------------------------------------------- /Sources/Synopsis/Model/EnumDescription.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Project «Synopsis» 3 | // Created by Jeorge Taflanidi 4 | // 5 | 6 | 7 | import Foundation 8 | 9 | 10 | /** 11 | Enum description. 12 | */ 13 | public struct EnumDescription: Equatable, CustomDebugStringConvertible { 14 | 15 | /** 16 | Documentation comment above the enum. 17 | */ 18 | public let comment: String? 19 | 20 | /** 21 | Enum annotations. 22 | 23 | Enum annotations are located inside the block comment above the enum declaration. 24 | */ 25 | public let annotations: [Annotation] 26 | 27 | /** 28 | Enum declaration line. 29 | */ 30 | public let declaration: Declaration 31 | 32 | /** 33 | Access visibility. 34 | */ 35 | public let accessibility: Accessibility 36 | 37 | /** 38 | Name of the enum. 39 | */ 40 | public let name: String 41 | 42 | /** 43 | Inherited protocols, classes, structs etc. 44 | */ 45 | public let inheritedTypes: [String] 46 | 47 | /** 48 | Cases. 49 | */ 50 | public let cases: [EnumCase] 51 | 52 | /** 53 | List of enum properties. 54 | */ 55 | public let properties: [PropertyDescription] 56 | 57 | /** 58 | Enum methods. 59 | */ 60 | public let methods: [MethodDescription] 61 | 62 | /** 63 | Write down own source code. 64 | */ 65 | public var verse: String { 66 | let commentStr: String 67 | if let commentExpl: String = comment, !commentExpl.isEmpty { 68 | commentStr = commentExpl.prefixEachLine(with: "/// ") + "\n" 69 | } else { 70 | commentStr = "" 71 | } 72 | 73 | let accessibilityStr: String = accessibility.verse.isEmpty ? "" : "\(accessibility.verse) " 74 | 75 | let inheritedTypesStr: String = inheritedTypes.isEmpty ? "" : ": " + inheritedTypes.joined(separator: ", ") 76 | 77 | let casesStr: String 78 | if cases.isEmpty { 79 | casesStr = "" 80 | } else { 81 | casesStr = cases.reduce("\n") { (result: String, enumCase: EnumCase) -> String in 82 | if cases.last == enumCase { 83 | return result + enumCase.verse.indent + "\n" 84 | } 85 | return result + enumCase.verse.indent + "\n\n" 86 | } 87 | } 88 | 89 | let propertiesStr: String 90 | if properties.isEmpty { 91 | propertiesStr = "" 92 | } else { 93 | propertiesStr = properties.reduce("\n") { (result: String, property: PropertyDescription) -> String in 94 | if properties.last == property { 95 | return result + property.verse.indent + "\n" 96 | } 97 | return result + property.verse.indent + "\n\n" 98 | } 99 | } 100 | 101 | let methodsStr: String 102 | if methods.isEmpty { 103 | methodsStr = "" 104 | } else { 105 | methodsStr = methods.reduce("\n") { (result: String, method: MethodDescription) -> String in 106 | if methods.last == method { 107 | return result + method.verse.indent + "\n" 108 | } 109 | return result + method.verse.indent + "\n\n" 110 | } 111 | } 112 | 113 | return """ 114 | \(commentStr)\(accessibilityStr)enum \(name)\(inheritedTypesStr) {\(casesStr)\(propertiesStr)\(methodsStr)}\n 115 | """ 116 | } 117 | 118 | /** 119 | FFS make auto-public initializers @ Apple 120 | */ 121 | public init( 122 | comment: String?, 123 | annotations: [Annotation], 124 | declaration: Declaration, 125 | accessibility: Accessibility, 126 | name: String, 127 | inheritedTypes: [String], 128 | cases: [EnumCase], 129 | properties: [PropertyDescription], 130 | methods: [MethodDescription] 131 | ) { 132 | self.comment = comment 133 | self.annotations = annotations 134 | self.declaration = declaration 135 | self.accessibility = accessibility 136 | self.name = name 137 | self.inheritedTypes = inheritedTypes 138 | self.cases = cases 139 | self.properties = properties 140 | self.methods = methods 141 | } 142 | 143 | /** 144 | Make a template enum for later code generation. 145 | */ 146 | public static func template( 147 | comment: String?, 148 | accessibility: Accessibility, 149 | name: String, 150 | inheritedTypes: [String], 151 | cases: [EnumCase], 152 | properties: [PropertyDescription], 153 | methods: [MethodDescription] 154 | ) -> EnumDescription { 155 | return EnumDescription( 156 | comment: comment, 157 | annotations: [], 158 | declaration: Declaration.mock, 159 | accessibility: accessibility, 160 | name: name, 161 | inheritedTypes: inheritedTypes, 162 | cases: cases, 163 | properties: properties, 164 | methods: methods 165 | ) 166 | } 167 | 168 | public static func ==(left: EnumDescription, right: EnumDescription) -> Bool { 169 | return left.comment == right.comment 170 | && left.annotations == right.annotations 171 | && left.declaration == right.declaration 172 | && left.accessibility == right.accessibility 173 | && left.name == right.name 174 | && left.inheritedTypes == right.inheritedTypes 175 | && left.cases == right.cases 176 | && left.properties == right.properties 177 | && left.methods == right.methods 178 | } 179 | 180 | public var debugDescription: String { 181 | if inheritedTypes.isEmpty { 182 | return "ENUM: name = \(name)" 183 | } 184 | return "ENUM: name = \(name); inherited = \(inheritedTypes.joined(separator: ", "))" 185 | } 186 | 187 | } 188 | -------------------------------------------------------------------------------- /Sources/Synopsis/Utility/String.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Project «Synopsis» 3 | // Created by Jeorge Taflanidi 4 | // 5 | 6 | 7 | import Foundation 8 | 9 | 10 | extension String { 11 | 12 | func detectInlineComment(startingAt index: String.Index) -> Bool { 13 | return self[index...].hasPrefix("//") 14 | } 15 | 16 | func detectInlineComment(endingAt index: String.Index) -> Bool { 17 | return self[index...].hasPrefix("\n") 18 | } 19 | 20 | func detectBlockComment(startingAt index: String.Index) -> Bool { 21 | return self[index...].hasPrefix("/*") 22 | } 23 | 24 | func detectBlockComment(endingAt index: String.Index) -> Bool { 25 | return self[index...].hasPrefix("*/") 26 | } 27 | 28 | func detectTextLiteral(startingAt index: String.Index) -> Bool { 29 | return self[index...].hasPrefix("\"\"\"\n") 30 | } 31 | 32 | func detectTextLiteral(endingAt index: String.Index) -> Bool { 33 | return self[index...].hasPrefix("\"\"\"") 34 | } 35 | 36 | func detectStringLiteral(startingAt index: String.Index) -> Bool { 37 | return self[index...].hasPrefix("\"") 38 | } 39 | 40 | func detectStringLiteral(endingAt index: String.Index) -> Bool { 41 | return detectStringLiteral(startingAt: index) 42 | } 43 | 44 | func truncateUntilExist(word: String) -> Substring { 45 | if let range = range(of: word) { 46 | return self[range.lowerBound...].dropFirst().truncateUntilExist(word: word) 47 | } 48 | 49 | return Substring(self) 50 | } 51 | 52 | func truncateLeadingWhitespace() -> Substring { 53 | if hasPrefix(" ") { 54 | return dropFirst().truncateLeadingWhitespace() 55 | } 56 | 57 | if hasPrefix("\n") { 58 | return dropFirst().truncateLeadingWhitespace() 59 | } 60 | 61 | return Substring(self) 62 | } 63 | 64 | func truncateUntil(word: String, deleteWord: Bool) -> Substring { 65 | guard let wordRange = range(of: word) 66 | else { 67 | return Substring(self) 68 | } 69 | 70 | return deleteWord ? self[wordRange.upperBound...] : self[wordRange.lowerBound...] 71 | } 72 | 73 | func truncateAfter(word: String, deleteWord: Bool) -> Substring { 74 | guard let wordRange: Range = range(of: word) 75 | else { 76 | return Substring(self) 77 | } 78 | 79 | return deleteWord ? self[.. Substring { 83 | for divider in sentenceDividers { 84 | if contains(divider) { 85 | return truncateAfter(word: divider, deleteWord: true).firstWord(sentenceDividers: sentenceDividers) 86 | } 87 | } 88 | 89 | return Substring(self) 90 | } 91 | 92 | func truncateInlineCommentOpening() -> String { 93 | if self.hasPrefix("//") { 94 | return String(self.dropFirst().dropFirst()).truncateInlineCommentOpening() 95 | } 96 | 97 | return self 98 | } 99 | 100 | var indent: String { 101 | return self 102 | .components(separatedBy: "\n") 103 | .map { $0.isEmpty ? $0 : " " + $0 } 104 | .joined(separator: "\n") 105 | } 106 | 107 | func prefixEachLine(with prefix: String) -> String { 108 | return self 109 | .components(separatedBy: "\n") 110 | .map { prefix + $0 } 111 | .joined(separator: "\n") 112 | } 113 | } 114 | 115 | 116 | extension Substring { 117 | 118 | func detectInlineComment(startingAt index: String.Index) -> Bool { 119 | return self[index...].hasPrefix("//") 120 | } 121 | 122 | func detectInlineComment(endingAt index: String.Index) -> Bool { 123 | return self[index...].hasPrefix("\n") 124 | } 125 | 126 | func detectBlockComment(startingAt index: String.Index) -> Bool { 127 | return self[index...].hasPrefix("/*") 128 | } 129 | 130 | func detectBlockComment(endingAt index: String.Index) -> Bool { 131 | return self[index...].hasPrefix("*/") 132 | } 133 | 134 | func detectTextLiteral(startingAt index: String.Index) -> Bool { 135 | return self[index...].hasPrefix("\"\"\"\n") 136 | } 137 | 138 | func detectTextLiteral(endingAt index: String.Index) -> Bool { 139 | return self[index...].hasPrefix("\"\"\"") 140 | } 141 | 142 | func detectStringLiteral(startingAt index: String.Index) -> Bool { 143 | return self[index...].hasPrefix("\"") 144 | } 145 | 146 | func detectStringLiteral(endingAt index: String.Index) -> Bool { 147 | return detectStringLiteral(startingAt: index) 148 | } 149 | 150 | func truncateUntilExist(word: String) -> Substring { 151 | if let range = self.range(of: word) { 152 | return self[range.lowerBound...].dropFirst().truncateUntilExist(word: word) 153 | } 154 | 155 | return self 156 | } 157 | 158 | func truncateAfter(word: String, deleteWord: Bool) -> Substring { 159 | guard let wordRange: Range = range(of: word) 160 | else { 161 | return self 162 | } 163 | 164 | return deleteWord ? self[.. Substring { 168 | if hasPrefix(" ") { 169 | return dropFirst().truncateLeadingWhitespace() 170 | } 171 | 172 | if hasPrefix("\n") { 173 | return dropFirst().truncateLeadingWhitespace() 174 | } 175 | 176 | return self 177 | } 178 | 179 | func firstWord(sentenceDividers: [String] = ["\n", " ", ".", ",", ";", ":"]) -> Substring { 180 | for divider in sentenceDividers { 181 | if contains(divider) { 182 | return truncateAfter(word: divider, deleteWord: true).firstWord(sentenceDividers: sentenceDividers) 183 | } 184 | } 185 | 186 | return self 187 | } 188 | 189 | } 190 | -------------------------------------------------------------------------------- /Tests/SynopsisTests/Versing/ClassDescriptionVersingTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Project «Synopsis» 3 | // Created by Jeorge Taflanidi 4 | // 5 | 6 | 7 | import XCTest 8 | @testable import Synopsis 9 | 10 | 11 | class ClassDescriptionVersingTests: XCTestCase { 12 | 13 | func testVerse_simpleEmpty_returnsSingleLine() { 14 | let classDescription = ClassDescription.template( 15 | comment: nil, 16 | accessibility: Accessibility.`internal`, 17 | name: "Entity", 18 | inheritedTypes: [], 19 | properties: [], 20 | methods: [] 21 | ) 22 | 23 | let expectedVerse = """ 24 | class Entity {} 25 | 26 | """ 27 | 28 | XCTAssertEqual(classDescription.verse, expectedVerse) 29 | } 30 | 31 | func testVerse_fullyPacked_returnsAsExpected() { 32 | let computedPropertyBody = """ 33 | get { 34 | return 0 35 | } 36 | 37 | set { 38 | print(newValue) 39 | } 40 | """ 41 | 42 | let properties = [ 43 | PropertyDescription.template( 44 | comment: "inferredString docs", 45 | accessibility: Accessibility.`internal`, 46 | constant: true, 47 | name: "inferredString", 48 | type: TypeDescription.string, 49 | defaultValue: "\"inferredStringValue\"", 50 | kind: .instance, 51 | body: nil 52 | ), 53 | PropertyDescription.template( 54 | comment: "basicInt docs", 55 | accessibility: Accessibility.`internal`, 56 | constant: false, 57 | name: "basicInt", 58 | type: TypeDescription.integer, 59 | defaultValue: nil, 60 | kind: .instance, 61 | body: computedPropertyBody 62 | ), 63 | PropertyDescription.template( 64 | comment: "complexType docs\n@ignore", 65 | accessibility: Accessibility.`private`, 66 | constant: true, 67 | name: "complexType", 68 | type: TypeDescription.array(element: TypeDescription.object(name: "Basic")), 69 | defaultValue: "[]", 70 | kind: .instance, 71 | body: nil 72 | ) 73 | ] 74 | 75 | let methods = [ 76 | try! MethodDescription.template( 77 | comment: "init docs", 78 | accessibility: Accessibility.`internal`, 79 | name: "init(basicInt:)", 80 | arguments: [ 81 | ArgumentDescription.template( 82 | name: "basicInt", 83 | bodyName: "basicInt", 84 | type: TypeDescription.integer, 85 | defaultValue: nil, 86 | comment: nil 87 | ) 88 | ], 89 | returnType: TypeDescription.object(name: "Basic"), 90 | kind: .instance, 91 | body: "self.basicInt = basicInt" 92 | ), 93 | try! MethodDescription.template( 94 | comment: "internalMethod docs", 95 | accessibility: Accessibility.`internal`, 96 | name: "internalMethod(parameter:asInteger:)", 97 | arguments: [ 98 | ArgumentDescription.template( 99 | name: "parameter", 100 | bodyName: "parameter", 101 | type: TypeDescription.optional(wrapped: TypeDescription.string), 102 | defaultValue: nil, 103 | comment: "@string" 104 | ), 105 | ArgumentDescription.template( 106 | name: "asInteger", 107 | bodyName: "integer", 108 | type: TypeDescription.integer, 109 | defaultValue: "0", 110 | comment: nil 111 | ) 112 | ], 113 | returnType: nil, 114 | kind: .instance, 115 | body: "print(parameter)\nprint(integer)" 116 | ), 117 | try! MethodDescription.template( 118 | comment: "privateMethod docs", 119 | accessibility: Accessibility.`private`, 120 | name: "privateMethod()", 121 | arguments: [], 122 | returnType: TypeDescription.object(name: "Never"), 123 | kind: .instance, 124 | body: "print(inferredString)" 125 | ) 126 | ] 127 | 128 | let comment = """ 129 | Basic docs 130 | 131 | @model 132 | @realm DBBasic 133 | """ 134 | 135 | let classDescription = ClassDescription.template( 136 | comment: comment, 137 | accessibility: Accessibility.`open`, 138 | name: "Basic", 139 | inheritedTypes: ["Entity", "Codable"], 140 | properties: properties, 141 | methods: methods 142 | ) 143 | 144 | let expectedVerse = """ 145 | /// Basic docs 146 | ///\(" ") 147 | /// @model 148 | /// @realm DBBasic 149 | open class Basic: Entity, Codable { 150 | /// inferredString docs 151 | let inferredString: String = "inferredStringValue" 152 | 153 | /// basicInt docs 154 | var basicInt: Int { 155 | get { 156 | return 0 157 | } 158 | 159 | set { 160 | print(newValue) 161 | } 162 | } 163 | 164 | /// complexType docs 165 | /// @ignore 166 | private let complexType: [Basic] = [] 167 | 168 | /// init docs 169 | init( 170 | basicInt: Int 171 | ) { 172 | self.basicInt = basicInt 173 | } 174 | 175 | /// internalMethod docs 176 | func internalMethod( 177 | parameter: String?, // @string 178 | asInteger integer: Int = 0 179 | ) { 180 | print(parameter) 181 | print(integer) 182 | } 183 | 184 | /// privateMethod docs 185 | private func privateMethod() -> Never { 186 | print(inferredString) 187 | } 188 | } 189 | 190 | """ 191 | 192 | XCTAssertEqual(classDescription.verse, expectedVerse) 193 | } 194 | 195 | static var allTests = [ 196 | ("testVerse_simpleEmpty_returnsSingleLine", testVerse_simpleEmpty_returnsSingleLine), 197 | ("testVerse_fullyPacked_returnsAsExpected", testVerse_fullyPacked_returnsAsExpected), 198 | ] 199 | 200 | } 201 | 202 | -------------------------------------------------------------------------------- /Tests/SynopsisTests/Versing/MethodDescriptionVersingTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Project «Synopsis» 3 | // Created by Jeorge Taflanidi 4 | // 5 | 6 | 7 | import XCTest 8 | @testable import Synopsis 9 | 10 | 11 | class MethodDescriptionVersingTests: XCTestCase { 12 | 13 | func testVerse_methodNameWithoutBrackets_throwsFunctionTemplateError() { 14 | do { 15 | let _ = try MethodDescription.template( 16 | comment: nil, 17 | accessibility: Accessibility.`internal`, 18 | name: "method", 19 | arguments: [], 20 | returnType: nil, 21 | kind: .instance, 22 | body: "" 23 | ) 24 | } catch let error as FunctionDescription.FunctionTemplateError { 25 | if case FunctionDescription.FunctionTemplateError.nameLacksRoundBrackets = error { 26 | XCTAssert(true) 27 | return 28 | } 29 | } catch { 30 | XCTAssert(false) 31 | } 32 | 33 | XCTAssert(false) 34 | } 35 | 36 | func testVerse_simpleMethodEmptyBody_returnsSingleLine() { 37 | let methodDescription = try! MethodDescription.template( 38 | comment: nil, 39 | accessibility: Accessibility.`internal`, 40 | name: "method()", 41 | arguments: [], 42 | returnType: nil, 43 | kind: .instance, 44 | body: "" 45 | ) 46 | 47 | let expectedVerse = """ 48 | func method() {} 49 | """ 50 | 51 | XCTAssertEqual(methodDescription.verse, expectedVerse) 52 | } 53 | 54 | func testVerse_simpleMethodWithBody_returnsMultiline() { 55 | let methodDescription = try! MethodDescription.template( 56 | comment: nil, 57 | accessibility: Accessibility.`internal`, 58 | name: "method()", 59 | arguments: [], 60 | returnType: nil, 61 | kind: .instance, 62 | body: "print(\"Test\")" 63 | ) 64 | 65 | let expectedVerse = """ 66 | func method() { 67 | print("Test") 68 | } 69 | """ 70 | 71 | XCTAssertEqual(methodDescription.verse, expectedVerse) 72 | } 73 | 74 | func testVerse_simpleInit_returnsAsExpected() { 75 | let methodDescription = try! MethodDescription.template( 76 | comment: nil, 77 | accessibility: Accessibility.`internal`, 78 | name: "init(name:)", 79 | arguments: [ 80 | ArgumentDescription.template( 81 | name: "name", 82 | bodyName: "name", 83 | type: TypeDescription.string, 84 | defaultValue: nil, 85 | comment: nil 86 | ) 87 | ], 88 | returnType: TypeDescription.object(name: "Entity"), 89 | kind: .instance, 90 | body: "self.name = name" 91 | ) 92 | 93 | let expectedVerse = """ 94 | init( 95 | name: String 96 | ) { 97 | self.name = name 98 | } 99 | """ 100 | 101 | XCTAssertEqual(methodDescription.verse, expectedVerse) 102 | } 103 | 104 | func testVerse_methodWithComments_returnsAsExpected() { 105 | let methodDescription = try! MethodDescription.template( 106 | comment: "Change name.\nObviously", 107 | accessibility: Accessibility.`public`, 108 | name: "change(personName:)", 109 | arguments: [ 110 | ArgumentDescription.template( 111 | name: "personName", 112 | bodyName: "name", 113 | type: TypeDescription.string, 114 | defaultValue: nil, 115 | comment: "@json" 116 | ) 117 | ], 118 | returnType: TypeDescription.object(name: "Person"), 119 | kind: .instance, 120 | body: "print(\"TBD\")" 121 | ) 122 | 123 | let expectedVerse = """ 124 | /// Change name. 125 | /// Obviously 126 | public func change( 127 | personName name: String // @json 128 | ) -> Person { 129 | print("TBD") 130 | } 131 | """ 132 | 133 | XCTAssertEqual(methodDescription.verse, expectedVerse) 134 | } 135 | 136 | func testVerse_bigComplexServiceCallProtocol_returnsWithoutBody() { 137 | let comment = """ 138 | Get Person by identifier. 139 | 140 | - parameter personId: Identifier 141 | - parameter fullPayload: Full long payload. 0 = short payload, else full. 142 | 143 | @get 144 | @url https://service.com/persons/{person_id} 145 | """ 146 | 147 | let methodDescription = try! MethodDescription.template( 148 | comment: comment, 149 | accessibility: Accessibility.`public`, 150 | name: "get(personId:fullPayload:)", 151 | arguments: [ 152 | ArgumentDescription.template( 153 | name: "personId", 154 | bodyName: "id", 155 | type: TypeDescription.integer, 156 | defaultValue: nil, 157 | comment: "@url person_id" 158 | ), 159 | ArgumentDescription.template( 160 | name: "fullPayload", 161 | bodyName: "fullPayload", 162 | type: TypeDescription.integer, 163 | defaultValue: "0", 164 | comment: "@query full_payload" 165 | ) 166 | ], 167 | returnType: TypeDescription.generic( 168 | name: "ServiceCall", 169 | constraints: [ 170 | TypeDescription.object(name: "Person") 171 | ] 172 | ), 173 | kind: .instance, 174 | body: nil 175 | ) 176 | 177 | let expectedVerse = """ 178 | /// Get Person by identifier. 179 | ///\(" ") 180 | /// - parameter personId: Identifier 181 | /// - parameter fullPayload: Full long payload. 0 = short payload, else full. 182 | ///\(" ") 183 | /// @get 184 | /// @url https://service.com/persons/{person_id} 185 | public func get( 186 | personId id: Int, // @url person_id 187 | fullPayload: Int = 0 // @query full_payload 188 | ) -> ServiceCall 189 | """ 190 | 191 | XCTAssertEqual(methodDescription.verse, expectedVerse) 192 | } 193 | 194 | static var allTests = [ 195 | ("testVerse_methodNameWithoutBrackets_throwsFunctionTemplateError", testVerse_methodNameWithoutBrackets_throwsFunctionTemplateError), 196 | ("testVerse_simpleMethodEmptyBody_returnsSingleLine", testVerse_simpleMethodEmptyBody_returnsSingleLine), 197 | ("testVerse_simpleMethodWithBody_returnsMultiline", testVerse_simpleMethodWithBody_returnsMultiline), 198 | ("testVerse_simpleInit_returnsAsExpected", testVerse_simpleInit_returnsAsExpected), 199 | ("testVerse_bigComplexServiceCallProtocol_returnsWithoutBody", testVerse_bigComplexServiceCallProtocol_returnsWithoutBody), 200 | ] 201 | 202 | } 203 | -------------------------------------------------------------------------------- /Sources/Synopsis/Model/FunctionDescription.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Project «Synopsis» 3 | // Created by Jeorge Taflanidi 4 | // 5 | 6 | 7 | import Foundation 8 | 9 | 10 | /** 11 | A function. 12 | */ 13 | public class FunctionDescription: Equatable, CustomDebugStringConvertible { 14 | 15 | /** 16 | Documentation comment. 17 | */ 18 | public let comment: String? 19 | 20 | /** 21 | Function annotations. 22 | 23 | Function annotations are located inside block comment above the declaration. 24 | */ 25 | public let annotations: [Annotation] 26 | 27 | /** 28 | Access visibility. 29 | */ 30 | public let accessibility: Accessibility 31 | 32 | /** 33 | Function name. 34 | 35 | Almost like signature, but without argument types. 36 | */ 37 | public let name: String 38 | 39 | /** 40 | Function arguments. 41 | */ 42 | public let arguments: [ArgumentDescription] 43 | 44 | /** 45 | Return type. 46 | */ 47 | public let returnType: TypeDescription? 48 | 49 | /** 50 | Function declaration line. 51 | */ 52 | public let declaration: Declaration 53 | 54 | /** 55 | Kind. 56 | */ 57 | public let kind: Kind 58 | 59 | /** 60 | Function body, if available. 61 | */ 62 | public let body: String? 63 | 64 | /** 65 | Write down own source code. 66 | */ 67 | public var verse: String { 68 | let commentStr: String 69 | if let commentExpl: String = comment, !commentExpl.isEmpty { 70 | commentStr = commentExpl.prefixEachLine(with: "/// ") + "\n" 71 | } else { 72 | commentStr = "" 73 | } 74 | 75 | let openBraceIndex: String.Index = name.index(of: "(")! 76 | 77 | let accessibilityStr = accessibility.verse.isEmpty ? "" : "\(accessibility.verse) " 78 | let funcStr = "func " 79 | let nameStr = name[.. String in 105 | if arguments.last != argument { 106 | return result + argument.verseWithComa + "\n" 107 | } 108 | return result + argument.verse + "\n" 109 | } 110 | } 111 | 112 | return """ 113 | \(commentStr)\(accessibilityStr)\(kindStr)\(funcStr)\(nameStr)(\(argumentsStr.indent))\(returnTypeStr)\(bodyStr) 114 | """ 115 | } 116 | 117 | /** 118 | FFS make auto-public initializers @ Apple 119 | */ 120 | public required init( 121 | comment: String?, 122 | annotations: [Annotation], 123 | accessibility: Accessibility, 124 | name: String, 125 | arguments: [ArgumentDescription], 126 | returnType: TypeDescription?, 127 | declaration: Declaration, 128 | kind: Kind, 129 | body: String? 130 | ) { 131 | self.comment = comment 132 | self.annotations = annotations 133 | self.accessibility = accessibility 134 | self.name = name 135 | self.arguments = arguments 136 | self.returnType = returnType 137 | self.declaration = declaration 138 | self.kind = kind 139 | self.body = body 140 | } 141 | 142 | /** 143 | Make a template for later code generation. 144 | 145 | - throws: FunctionTemplateError 146 | */ 147 | public class func template( 148 | comment: String?, 149 | accessibility: Accessibility, 150 | name: String, 151 | arguments: [ArgumentDescription], 152 | returnType: TypeDescription?, 153 | kind: Kind, 154 | body: String? 155 | ) throws -> Self { 156 | if !checkRoundBrackets(inName: name) { 157 | throw FunctionTemplateError.nameLacksRoundBrackets(name: name) 158 | } 159 | 160 | return self.init( 161 | comment: comment, 162 | annotations: [], 163 | accessibility: accessibility, 164 | name: name, 165 | arguments: arguments, 166 | returnType: returnType, 167 | declaration: Declaration.mock, 168 | kind: kind, 169 | body: body 170 | ) 171 | } 172 | 173 | public static func ==(left: FunctionDescription, right: FunctionDescription) -> Bool { 174 | return left.comment == right.comment 175 | && left.annotations == right.annotations 176 | && left.accessibility == right.accessibility 177 | && left.name == right.name 178 | && left.arguments == right.arguments 179 | && left.returnType == right.returnType 180 | && left.declaration == right.declaration 181 | && left.kind == right.kind 182 | && left.body == right.body 183 | } 184 | 185 | public var debugDescription: String { 186 | return "FUNCTION: name = \(name)" + (nil != returnType ? "; return type = \(returnType!)" : "") 187 | } 188 | 189 | public enum FunctionTemplateError: Error { 190 | case nameLacksRoundBrackets(name: String) 191 | } 192 | 193 | public enum Kind { 194 | case free 195 | case `class` 196 | case `static` 197 | case instance 198 | 199 | public var verse: String { 200 | switch self { 201 | case .`class`: return "class" 202 | case .`static`: return "static" 203 | case .instance, .free: return "" 204 | } 205 | } 206 | } 207 | 208 | } 209 | 210 | 211 | public extension Sequence where Iterator.Element: FunctionDescription { 212 | 213 | public subscript(name: String) -> Iterator.Element? { 214 | for item in self { 215 | if item.name == name { 216 | return item 217 | } 218 | } 219 | return nil 220 | } 221 | 222 | public func contains(name: String) -> Bool { 223 | return nil != self[name] 224 | } 225 | 226 | } 227 | 228 | 229 | private extension FunctionDescription { 230 | static func checkRoundBrackets(inName name: String) -> Bool { 231 | let lex = LexemeString(name) 232 | 233 | var left = false 234 | var right = false 235 | 236 | for lexeme in lex.lexemes { 237 | if lexeme.isSourceCodeKind() { 238 | if name[lexeme.left...lexeme.right].contains("(") { 239 | left = true 240 | } 241 | if name[lexeme.left...lexeme.right].contains(")") { 242 | right = true 243 | } 244 | } 245 | } 246 | 247 | return left && right 248 | } 249 | } 250 | -------------------------------------------------------------------------------- /Sources/Synopsis/Utility/FunctionDescriptionParser.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Project «Synopsis» 3 | // Created by Jeorge Taflanidi 4 | // 5 | 6 | 7 | import Foundation 8 | import SourceKittenFramework 9 | 10 | 11 | class FunctionDescriptionParser { 12 | 13 | func parse(files pathList: [URL]) -> ParsingResult { 14 | var ioErrors: [SynopsisError] = [] 15 | 16 | let files: [GoodFile] = pathList.flatMap { (fileURL: URL) -> GoodFile? in 17 | if let file = File(path: fileURL.path) { 18 | return GoodFile(path: fileURL, file: file) 19 | } 20 | 21 | ioErrors.append(SynopsisError.errorReadingFile(file: fileURL)) 22 | return nil 23 | } 24 | 25 | let result: ParsingResult = parse(files: files) 26 | return ParsingResult(errors: result.errors + ioErrors, models: result.models) 27 | } 28 | 29 | func parse(files: [GoodFile]) -> ParsingResult { 30 | var compilingErrors: [SynopsisError] = [] 31 | 32 | let compiledStructures: [CompiledStructure] = files.flatMap { (goodFile: GoodFile) -> CompiledStructure? in 33 | if let structure: CompiledStructure = CompiledStructure(file: goodFile) { 34 | return structure 35 | } 36 | 37 | compilingErrors.append(SynopsisError.errorCompilingFile(file: goodFile.path)) 38 | return nil 39 | } 40 | 41 | let result: [Function] = translate(compiledStructures) 42 | return ParsingResult(errors: compilingErrors, models: result) 43 | } 44 | 45 | func isRawFunctionDescription(_ element: [String: AnyObject]) -> Bool { 46 | guard let kind: String = element.kind 47 | else { return false } 48 | return SwiftDeclarationKind.functionFree.rawValue == kind 49 | } 50 | 51 | func parse(rawStructureElements: [[String: AnyObject]], file: GoodFile) -> [Function] { 52 | return rawStructureElements 53 | .filter { isRawFunctionDescription($0) } 54 | .map { (rawFunctionDescription: [String: AnyObject]) -> Function in 55 | let declarationOffset: Int = rawFunctionDescription.offset 56 | let declarationString: String = 57 | deduceDeclaration( 58 | rawFunctionDescription: rawFunctionDescription, 59 | file: file.file.contents, 60 | declarationOffset: declarationOffset 61 | ) 62 | 63 | let name: String = rawFunctionDescription.name 64 | let comment: String? = rawFunctionDescription.comment 65 | let annotations: [Annotation] = nil != comment ? AnnotationParser().parse(comment: comment!) : [] 66 | let accessibility: Accessibility = Accessibility.deduce(forRawStructureElement: rawFunctionDescription) 67 | let typename: String = rawFunctionDescription.typename 68 | let returnType: TypeDescription? = TypeParser().parse(functionTypename: typename, declarationString: declarationString) 69 | let kind: Function.Kind = getKind(rawFunctionDescription: rawFunctionDescription) 70 | let body: String? = getBody(rawFunctionDescription: rawFunctionDescription, file: file) 71 | let arguments: [ArgumentDescription] = ArgumentDescriptionParser().parse(functionParsedDeclaration: declarationString) 72 | 73 | let declaration: Declaration = 74 | Declaration( 75 | filePath: file.path, 76 | fileContents: file.file.contents, 77 | rawText: declarationString, 78 | offset: declarationOffset 79 | ) 80 | 81 | return Function( 82 | comment: comment, 83 | annotations: annotations, 84 | accessibility: accessibility, 85 | name: name, 86 | arguments: arguments, 87 | returnType: returnType, 88 | declaration: declaration, 89 | kind: kind, 90 | body: body 91 | ) 92 | } 93 | } 94 | } 95 | 96 | 97 | private extension FunctionDescriptionParser { 98 | 99 | func deduceDeclaration(rawFunctionDescription: [String: AnyObject], file: String, declarationOffset: Int) -> String { 100 | /** 101 | Sometimes SourceKit can't accurately parse full method declaration 102 | This constantly happens for multiline method declarations in protocols, where 103 | 104 | func abc( 105 | argument: Int 106 | ) -> String 107 | 108 | gets truncated to "func abc(". 109 | 110 | This is why defaultDeclarationString needs to be checked. 111 | */ 112 | let defaultDeclarationString: String = rawFunctionDescription.parsedDeclaration 113 | let defaultDeclarationLex = LexemeString(defaultDeclarationString) 114 | 115 | // Simple check searches for the right round bracket: 116 | for index in defaultDeclarationString.indices { 117 | if defaultDeclarationLex.inSourceCodeRange(index) && ")" == defaultDeclarationString[index] { 118 | return defaultDeclarationString 119 | } 120 | } 121 | // defaultDeclarationString is enough for most cases. 122 | 123 | // If defaultDeclarationString is not enough, full method declaration needs to be parsed manually 124 | let startIndex: String.Index = file.index(file.startIndex, offsetBy: declarationOffset) 125 | let endIndex: String.Index = file.index(startIndex, offsetBy: rawFunctionDescription.length) 126 | let fullText: String = String(file[startIndex...endIndex]) 127 | let fullTextLex = LexemeString(fullText) 128 | 129 | var declarationString: String = "" 130 | for index in fullText.indices { 131 | // Detect the end of method signature by the opening curly brace: 132 | if fullTextLex.inSourceCodeRange(index) && "{" == fullText[index] { break } 133 | declarationString.append(fullText[index]) 134 | } 135 | 136 | return declarationString.trimmingCharacters(in: CharacterSet.whitespacesAndNewlines) 137 | } 138 | 139 | func translate(_ structures: [CompiledStructure]) -> [Function] { 140 | return structures.flatMap { translate($0) } 141 | } 142 | 143 | func translate(_ structure: CompiledStructure) -> [Function] { 144 | return parse(rawStructureElements: structure.topElements, file: structure.file) 145 | 146 | } 147 | 148 | func getKind(rawFunctionDescription: [String: AnyObject]) -> Function.Kind { 149 | guard let kind: String = rawFunctionDescription.kind 150 | else { return Function.Kind.free } 151 | 152 | switch kind { 153 | case SwiftDeclarationKind.functionMethodInstance.rawValue: return Function.Kind.instance 154 | case SwiftDeclarationKind.functionMethodClass.rawValue: return Function.Kind.`class` 155 | case SwiftDeclarationKind.functionMethodStatic.rawValue: return Function.Kind.`static` 156 | 157 | default: return Function.Kind.free 158 | } 159 | } 160 | 161 | func getBody(rawFunctionDescription: [String: AnyObject], file: GoodFile) -> String? { 162 | guard 163 | let bodyOffset: Int = rawFunctionDescription.bodyOffset, 164 | let bodyLength: Int = rawFunctionDescription.bodyLength 165 | else { 166 | return nil 167 | } 168 | 169 | let fileContents: String = file.file.contents 170 | 171 | let bodyStartIndex: String.Index = fileContents.index(fileContents.startIndex, offsetBy: bodyOffset) 172 | let bodyEndIndex: String.Index = fileContents.index(bodyStartIndex, offsetBy: bodyLength) 173 | 174 | return String(fileContents[bodyStartIndex.. TypeDescription { 14 | let typename: String = rawDescription.typename 15 | let declaration: String = rawDescription.parsedDeclaration 16 | 17 | switch typename { 18 | // TODO: incorporate all possible rawDescription.typename values 19 | case "Bool": return TypeDescription.boolean 20 | case "Int": return TypeDescription.integer 21 | case "Float": return TypeDescription.floatingPoint 22 | case "Double": return TypeDescription.doublePrecision 23 | case "String": return TypeDescription.string 24 | case "Void": return TypeDescription.void 25 | 26 | default: return deduceType(fromDeclaration: declaration) 27 | } 28 | } 29 | 30 | func parse(functionTypename: String, declarationString: String) -> TypeDescription? { 31 | let returnTypename: String = 32 | String(functionTypename.split(separator: " ").last!) 33 | 34 | switch returnTypename { 35 | case "()", "Void": return TypeDescription.void 36 | 37 | case "Bool": return TypeDescription.boolean 38 | case "Int": return TypeDescription.integer 39 | case "Float": return TypeDescription.floatingPoint 40 | case "Double": return TypeDescription.doublePrecision 41 | case "String": return TypeDescription.string 42 | 43 | default: 44 | if functionTypename != "<>" { 45 | return parse(rawType: returnTypename) 46 | } 47 | 48 | // FIXME: Make a LexemeString, exclude comments 49 | 50 | if !declarationString.contains("->") { 51 | return nil 52 | } 53 | 54 | if let rawReturnType: String = declarationString.components(separatedBy: "->").last?.trimmingCharacters(in: CharacterSet.whitespacesAndNewlines) { 55 | return parse(rawType: rawReturnType) 56 | } 57 | 58 | return nil 59 | } 60 | } 61 | 62 | func deduceType(fromDeclaration declaration: String) -> TypeDescription { 63 | if let type: TypeDescription = parseExplicitType(fromDeclaration: declaration) { 64 | return type 65 | } 66 | 67 | return deduceType(fromDefaultValue: declaration) 68 | } 69 | } 70 | 71 | 72 | private extension TypeParser { 73 | 74 | func parseExplicitType(fromDeclaration declaration: String) -> TypeDescription? { 75 | guard declaration.contains(":") 76 | else { return nil } 77 | 78 | let declarationWithoutDefultValue: String = 79 | declaration 80 | .truncateAfter(word: "=", deleteWord: true) 81 | .trimmingCharacters(in: CharacterSet.whitespacesAndNewlines) 82 | 83 | let typename: String = 84 | declarationWithoutDefultValue 85 | .truncateUntilExist(word: ":") 86 | .trimmingCharacters(in: CharacterSet.whitespacesAndNewlines) 87 | 88 | return parse(rawType: typename) 89 | } 90 | 91 | func deduceType(fromDefaultValue declaration: String) -> TypeDescription { 92 | guard declaration.contains("=") 93 | else { return TypeDescription.object(name: "") } 94 | 95 | let defaultValue: String = 96 | declaration 97 | .truncateUntilExist(word: "=") 98 | .trimmingCharacters(in: CharacterSet.whitespacesAndNewlines) 99 | 100 | return guessType(ofVariableValue: defaultValue) 101 | } 102 | 103 | /** 104 | Parse raw type line without any other garbage. 105 | 106 | ```MyType<[Entity]>``` 107 | */ 108 | func parse(rawType: String) -> TypeDescription { 109 | // check type ends with ? 110 | if rawType.hasSuffix("?") { 111 | return TypeDescription.optional( 112 | wrapped: parse(rawType: String(rawType.dropLast())) 113 | ) 114 | } 115 | 116 | if rawType.contains("<") && rawType.contains(">") { 117 | let name: String = rawType.truncateAfter(word: "<", deleteWord: true).trimmingCharacters(in: CharacterSet.whitespacesAndNewlines) 118 | let itemName: String = String(rawType.truncateUntilExist(word: "<").truncateAfter(word: ">", deleteWord: true)) 119 | let itemType: TypeDescription = self.parse(rawType: itemName) 120 | return TypeDescription.generic(name: name, constraints: [itemType]) 121 | } 122 | 123 | if rawType.contains("[") && rawType.contains("]") { 124 | let collecitonItemTypeName: String = 125 | rawType 126 | .truncateUntilExist(word: "[") 127 | .truncateAfter(word: "]", deleteWord: true) 128 | .trimmingCharacters(in: CharacterSet.whitespaces) 129 | return self.parseCollectionItemType(collecitonItemTypeName) 130 | } 131 | 132 | if rawType == "Bool" { 133 | return TypeDescription.boolean 134 | } 135 | 136 | if rawType.contains("Int") { 137 | return TypeDescription.integer 138 | } 139 | 140 | if rawType == "Float" { 141 | return TypeDescription.floatingPoint 142 | } 143 | 144 | if rawType == "Double" { 145 | return TypeDescription.doublePrecision 146 | } 147 | 148 | if rawType == "Date" { 149 | return TypeDescription.date 150 | } 151 | 152 | if rawType == "Data" { 153 | return TypeDescription.data 154 | } 155 | 156 | if rawType == "String" { 157 | return TypeDescription.string 158 | } 159 | 160 | if rawType == "Void" { 161 | return TypeDescription.void 162 | } 163 | 164 | var objectTypeName: String = String(rawType.firstWord()) 165 | if objectTypeName.last == "?" { 166 | objectTypeName = String(objectTypeName.dropLast()) 167 | } 168 | 169 | return TypeDescription.object(name: objectTypeName) 170 | } 171 | 172 | func guessType(ofVariableValue value: String) -> TypeDescription { 173 | // collections are not supported yet 174 | if value.contains("[") { 175 | return TypeDescription.object(name: "") 176 | } 177 | 178 | // check value is text in quotes: 179 | // let abc = "abcd" 180 | if let _ = value.range(of: "^\"(.*)\"$", options: .regularExpression) { 181 | return TypeDescription.string 182 | } 183 | 184 | // check value is double: 185 | // let abc = 123.45 186 | if let _ = value.range(of: "^(\\d+)\\.(\\d+)$", options: .regularExpression) { 187 | return TypeDescription.doublePrecision 188 | } 189 | 190 | // check value is int: 191 | // let abc = 123 192 | if let _ = value.range(of: "^(\\d+)$", options: .regularExpression) { 193 | return TypeDescription.integer 194 | } 195 | 196 | // check value is bool 197 | // let abc = true 198 | if value.contains("true") || value.contains("false") { 199 | return TypeDescription.boolean 200 | } 201 | 202 | // check value contains object init statement: 203 | // let abc = Object(some: 123) 204 | if let _ = value.range(of: "^(\\w+)\\((.*)\\)$", options: .regularExpression) { 205 | let rawValueTypeName: String = String(value.truncateAfter(word: "(", deleteWord: true)) 206 | return parse(rawType: rawValueTypeName) 207 | } 208 | 209 | return TypeDescription.object(name: "") 210 | } 211 | 212 | func parseCollectionItemType(_ collecitonItemTypeName: String) -> TypeDescription { 213 | if collecitonItemTypeName.contains(":") { 214 | let keyTypeName: String = String(collecitonItemTypeName.truncateAfter(word: ":", deleteWord: true)) 215 | let valueTypeName: String = String(collecitonItemTypeName.truncateUntilExist(word: ":")) 216 | 217 | return TypeDescription.map(key: self.parse(rawType: keyTypeName), value: self.parse(rawType: valueTypeName)) 218 | } else { 219 | return TypeDescription.array(element: self.parse(rawType: collecitonItemTypeName)) 220 | } 221 | } 222 | 223 | } 224 | -------------------------------------------------------------------------------- /Tests/SynopsisTests/Utility/ProtocolDescriptionParserTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Project «Synopsis» 3 | // Created by Jeorge Taflanidi 4 | // 5 | 6 | 7 | import XCTest 8 | @testable import Synopsis 9 | 10 | 11 | class ProtocolDescriptionParserTests: SynopsisTestCase { 12 | 13 | override func setUp() { 14 | super.setUp() 15 | storeContents(basicProtocol, asFile: "BasicProtocol.swift") 16 | storeContents(protocolWithMultilineMethods, asFile: "MultilineMethodProtocol.swift") 17 | } 18 | 19 | override func tearDown() { 20 | deleteFile(named: "MultilineMethodProtocol.swift") 21 | deleteFile(named: "BasicProtocol.swift") 22 | super.tearDown() 23 | } 24 | 25 | func testParse_basicFile_returnsAsExpected() { 26 | let inputFile: URL = urlForFile(named: "BasicProtocol.swift") 27 | let parser = ProtocolDescriptionParser() 28 | let result: ParsingResult = parser.parse(files: [inputFile]) 29 | 30 | let expectedAnnotations: [Annotation] = [] 31 | 32 | let expectedDeclaration: Declaration = Declaration( 33 | filePath: inputFile, 34 | rawText: "public protocol Doer", 35 | offset: 176, 36 | lineNumber: 21, 37 | columnNumber: 8 38 | ) 39 | 40 | let expectedProperties: [PropertyDescription] = [] 41 | 42 | let expectedMethods: [MethodDescription] = [ 43 | MethodDescription( 44 | comment: "Do anything.", 45 | annotations: [], 46 | accessibility: Accessibility.`public`, 47 | name: "abc()", 48 | arguments: [], 49 | returnType: TypeDescription.void, 50 | declaration: Declaration( 51 | filePath: inputFile, 52 | rawText: "func abc()", 53 | offset: 230, 54 | lineNumber: 25, 55 | columnNumber: 5 56 | ), 57 | kind: .instance, 58 | body: nil 59 | ), 60 | ] 61 | 62 | XCTAssertEqual(result.models.count, 1) 63 | let basicProtocolDescription: ProtocolDescription = result.models.first! 64 | 65 | XCTAssertEqual(basicProtocolDescription.comment, "Protocol docs") 66 | XCTAssertEqual(basicProtocolDescription.annotations, expectedAnnotations) 67 | XCTAssertEqual(basicProtocolDescription.declaration, expectedDeclaration) 68 | XCTAssertEqual(basicProtocolDescription.accessibility, Accessibility.`public`) 69 | XCTAssertEqual(basicProtocolDescription.name, "Doer") 70 | XCTAssertEqual(basicProtocolDescription.inheritedTypes.count, 0) 71 | XCTAssertEqual(basicProtocolDescription.properties, expectedProperties) 72 | XCTAssertEqual(basicProtocolDescription.methods, expectedMethods) 73 | } 74 | 75 | func testParse_protocolWithMultilineMethodDeclarations_returnsCorrectReturnTypes() { 76 | let inputFile: URL = urlForFile(named: "MultilineMethodProtocol.swift") 77 | let parser = ProtocolDescriptionParser() 78 | let result: ParsingResult = parser.parse(files: [inputFile]) 79 | 80 | XCTAssertEqual(result.models.count, 1) 81 | let actualProtocolDescription: ProtocolDescription = result.models.first! 82 | 83 | XCTAssertEqual( 84 | actualProtocolDescription, 85 | ProtocolDescription( 86 | comment: nil, 87 | annotations: [], 88 | declaration: Declaration( 89 | filePath: inputFile, 90 | rawText: "protocol Multiline", 91 | offset: 0, 92 | lineNumber: 1, 93 | columnNumber: 1 94 | ), 95 | accessibility: Accessibility.`internal`, 96 | name: "Multiline", 97 | inheritedTypes: [], 98 | properties: [], 99 | methods: [ 100 | MethodDescription( 101 | comment: "Get `Person` by id", 102 | annotations: [], 103 | accessibility: Accessibility.`internal`, 104 | name: "get(personId:)", 105 | arguments: [ 106 | ArgumentDescription( 107 | name: "personId", 108 | bodyName: "id", 109 | type: TypeDescription.integer, 110 | defaultValue: nil, 111 | annotations: [Annotation(name: "url", value: nil, declaration: nil)], 112 | declaration: nil, 113 | comment: " @url" 114 | ), 115 | ], 116 | returnType: TypeDescription.generic(name: "ServiceCall", constraints: [TypeDescription.object(name: "Person")]), 117 | declaration: Declaration( 118 | filePath: inputFile, 119 | rawText: "func get(\n personId id: Int // @url\n ) -> ServiceCall", 120 | offset: 52, 121 | lineNumber: 3, 122 | columnNumber: 5 123 | ), 124 | kind: FunctionDescription.Kind.instance, 125 | body: nil 126 | ), 127 | MethodDescription( 128 | comment: "Search for `Person`", 129 | annotations: [], 130 | accessibility: Accessibility.`internal`, 131 | name: "search(firstName:lastName:)", 132 | arguments: [ 133 | ArgumentDescription( 134 | name: "firstName", 135 | bodyName: "firstName", 136 | type: TypeDescription.string, 137 | defaultValue: nil, 138 | annotations: [Annotation(name: "query", value: "first_name", declaration: nil)], 139 | declaration: nil, 140 | comment: " @query first_name" 141 | ), 142 | ArgumentDescription( 143 | name: "lastName", 144 | bodyName: "lastName", 145 | type: TypeDescription.string, 146 | defaultValue: nil, 147 | annotations: [Annotation(name: "query", value: "last_name", declaration: nil)], 148 | declaration: nil, 149 | comment: " @query last_name" 150 | ), 151 | ], 152 | returnType: TypeDescription.array(element: TypeDescription.object(name: "Person")), 153 | declaration: Declaration( 154 | filePath: inputFile, 155 | rawText: "func search(\n firstName: String, // @query first_name\n lastName: String // @query last_name\n ) -> [Person]", 156 | offset: 157, 157 | lineNumber: 8, 158 | columnNumber: 5 159 | ), 160 | kind: FunctionDescription.Kind.instance, 161 | body: nil 162 | ), 163 | ] 164 | ) 165 | ) 166 | } 167 | 168 | static var allTests = [ 169 | ("testParse_basicFile_returnsAsExpected", testParse_basicFile_returnsAsExpected), 170 | ("testParse_protocolWithMultilineMethodDeclarations_returnsCorrectReturnTypes", testParse_protocolWithMultilineMethodDeclarations_returnsCorrectReturnTypes), 171 | ] 172 | } 173 | 174 | 175 | let basicProtocol = """ 176 | // 177 | // Basic.swift 178 | // SynopsisTests 179 | // 180 | // Created by John Appleseed on 10.11.29H. 181 | // 182 | 183 | 184 | import Foundation 185 | 186 | 187 | /** 188 | Basic docs 189 | */ 190 | class Basic {} 191 | 192 | 193 | /** 194 | Protocol docs 195 | */ 196 | public protocol Doer { 197 | /** 198 | Do anything. 199 | */ 200 | func abc() 201 | } 202 | """ 203 | 204 | 205 | let protocolWithMultilineMethods = """ 206 | protocol Multiline { 207 | /// Get `Person` by id 208 | func get( 209 | personId id: Int // @url 210 | ) -> ServiceCall 211 | 212 | /// Search for `Person` 213 | func search( 214 | firstName: String, // @query first_name 215 | lastName: String // @query last_name 216 | ) -> [Person] 217 | } 218 | """ 219 | -------------------------------------------------------------------------------- /Sources/Synopsis/Utility/LexemeString.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Project «Synopsis» 3 | // Created by Jeorge Taflanidi 4 | // 5 | 6 | 7 | import Foundation 8 | 9 | 10 | struct LexemeString { 11 | 12 | let string: String 13 | let lexemes: [Lexeme] 14 | 15 | init(_ string: String) { 16 | self.string = string 17 | if string.isEmpty { 18 | self.lexemes = [] 19 | } else { 20 | self.lexemes = LexemeString.returnLexeme( 21 | currentIndex: string.startIndex, 22 | string: string, 23 | currentLexeme: Lexeme(left: string.startIndex, right: string.startIndex, kind: LexemeString.Lexeme.Kind.sourceCode), 24 | initial: [] 25 | ) 26 | } 27 | } 28 | 29 | func inCommentRange(_ index: String.Index) -> Bool { 30 | if !string.indices.contains(index) || string.endIndex == index { 31 | return false 32 | } 33 | for lexeme in lexemes { 34 | if (lexeme.left...lexeme.right).contains(index) { 35 | return lexeme.isCommentKind() 36 | } 37 | } 38 | return false 39 | } 40 | 41 | func inStringLiteralRange(_ index: String.Index) -> Bool { 42 | if !string.indices.contains(index) || string.endIndex == index { 43 | return false 44 | } 45 | for lexeme in lexemes { 46 | if (lexeme.left...lexeme.right).contains(index) { 47 | return lexeme.isStringLiteralKind() 48 | } 49 | } 50 | return false 51 | } 52 | 53 | func inSourceCodeRange(_ index: String.Index) -> Bool { 54 | if !string.indices.contains(index) || string.endIndex == index { 55 | return false 56 | } 57 | for lexeme in lexemes { 58 | if (lexeme.left...lexeme.right).contains(index) { 59 | return lexeme.isSourceCodeKind() 60 | } 61 | } 62 | return false 63 | } 64 | 65 | private static func returnLexeme( 66 | currentIndex: String.Index, 67 | string: String, 68 | currentLexeme: Lexeme, 69 | initial: [Lexeme] 70 | ) -> [Lexeme] { 71 | if string.endIndex == currentIndex { 72 | return initial + [currentLexeme] 73 | } else { 74 | switch currentLexeme.kind { 75 | case .sourceCode: 76 | if string.detectInlineComment(startingAt: currentIndex) { 77 | return returnLexeme( 78 | currentIndex: string.index(after: currentIndex), 79 | string: string, 80 | currentLexeme: Lexeme(left: currentIndex, right: currentIndex, kind: LexemeString.Lexeme.Kind.inlineComment), 81 | initial: initial + [currentLexeme] 82 | ) 83 | } 84 | 85 | if string.detectBlockComment(startingAt: currentIndex) { 86 | return returnLexeme( 87 | currentIndex: string.index(after: currentIndex), 88 | string: string, 89 | currentLexeme: Lexeme(left: currentIndex, right: currentIndex, kind: LexemeString.Lexeme.Kind.blockComment), 90 | initial: initial + [currentLexeme] 91 | ) 92 | } 93 | 94 | if string.detectTextLiteral(startingAt: currentIndex) { 95 | return returnLexeme( 96 | currentIndex: string.index(currentIndex, offsetBy: 3), 97 | string: string, 98 | currentLexeme: Lexeme(left: currentIndex, right: currentIndex, kind: LexemeString.Lexeme.Kind.textLiteral), 99 | initial: initial + [currentLexeme.adjusted(right: string.index(currentIndex, offsetBy: 2))] 100 | ) 101 | } 102 | 103 | if string.detectStringLiteral(startingAt: currentIndex) { 104 | return returnLexeme( 105 | currentIndex: string.index(after: currentIndex), 106 | string: string, 107 | currentLexeme: Lexeme(left: currentIndex, right: currentIndex, kind: LexemeString.Lexeme.Kind.stringLiteral), 108 | initial: initial + [currentLexeme] 109 | ) 110 | } 111 | 112 | return returnLexeme( 113 | currentIndex: string.index(after: currentIndex), 114 | string: string, 115 | currentLexeme: currentLexeme.adjusted(right: currentIndex), 116 | initial: initial 117 | ) 118 | 119 | case .blockComment: 120 | if string.detectBlockComment(endingAt: currentIndex) { 121 | return returnLexeme( 122 | currentIndex: string.index(after: currentIndex), 123 | string: string, 124 | currentLexeme: Lexeme(left: currentIndex, right: currentIndex, kind: LexemeString.Lexeme.Kind.sourceCode), 125 | initial: initial + [currentLexeme] 126 | ) 127 | } 128 | 129 | return returnLexeme( 130 | currentIndex: string.index(after: currentIndex), 131 | string: string, 132 | currentLexeme: currentLexeme.adjusted(right: currentIndex), 133 | initial: initial 134 | ) 135 | 136 | case .inlineComment: 137 | if string.detectInlineComment(endingAt: currentIndex) { 138 | return returnLexeme( 139 | currentIndex: string.index(after: currentIndex), 140 | string: string, 141 | currentLexeme: Lexeme(left: currentIndex, right: currentIndex, kind: LexemeString.Lexeme.Kind.sourceCode), 142 | initial: initial + [currentLexeme] 143 | ) 144 | } 145 | 146 | return returnLexeme( 147 | currentIndex: string.index(after: currentIndex), 148 | string: string, 149 | currentLexeme: currentLexeme.adjusted(right: currentIndex), 150 | initial: initial 151 | ) 152 | 153 | case .stringLiteral: 154 | if string.detectStringLiteral(endingAt: currentIndex) { 155 | return returnLexeme( 156 | currentIndex: string.index(after: currentIndex), 157 | string: string, 158 | currentLexeme: Lexeme(left: currentIndex, right: currentIndex, kind: LexemeString.Lexeme.Kind.sourceCode), 159 | initial: initial + [currentLexeme] 160 | ) 161 | } 162 | 163 | return returnLexeme( 164 | currentIndex: string.index(after: currentIndex), 165 | string: string, 166 | currentLexeme: currentLexeme.adjusted(right: currentIndex), 167 | initial: initial 168 | ) 169 | 170 | case .textLiteral: 171 | if string.detectTextLiteral(endingAt: currentIndex) { 172 | return returnLexeme( 173 | currentIndex: string.index(currentIndex, offsetBy: 3), 174 | string: string, 175 | currentLexeme: Lexeme(left: currentIndex, right: currentIndex, kind: LexemeString.Lexeme.Kind.sourceCode), 176 | initial: initial + [currentLexeme.adjusted(right: string.index(currentIndex, offsetBy: 2))] 177 | ) 178 | } 179 | 180 | return returnLexeme( 181 | currentIndex: string.index(after: currentIndex), 182 | string: string, 183 | currentLexeme: currentLexeme.adjusted(right: currentIndex), 184 | initial: initial 185 | ) 186 | } 187 | } 188 | } 189 | 190 | struct Lexeme { 191 | let left: String.Index 192 | var right: String.Index 193 | let kind: Kind 194 | 195 | func isCommentKind() -> Bool { 196 | switch kind { 197 | case .inlineComment, .blockComment: return true 198 | default: return false 199 | } 200 | } 201 | 202 | func isStringLiteralKind() -> Bool { 203 | switch kind { 204 | case .stringLiteral, .textLiteral: return true 205 | default: return false 206 | } 207 | } 208 | 209 | func isSourceCodeKind() -> Bool { 210 | switch kind { 211 | case .sourceCode: return true 212 | default: return false 213 | } 214 | } 215 | 216 | enum Kind { 217 | case sourceCode 218 | case inlineComment 219 | case blockComment 220 | case stringLiteral 221 | case textLiteral 222 | } 223 | 224 | func adjusted(right: String.Index) -> Lexeme { 225 | return Lexeme(left: left, right: right, kind: kind) 226 | } 227 | } 228 | 229 | } 230 | -------------------------------------------------------------------------------- /Sources/Synopsis/Synopsis.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Project «Synopsis» 3 | // Created by Jeorge Taflanidi 4 | // 5 | 6 | 7 | import Foundation 8 | 9 | 10 | /** 11 | Structure information about given source code. 12 | */ 13 | public struct Synopsis { 14 | 15 | /** 16 | Found classes. 17 | */ 18 | public let classes: [ClassDescription] 19 | 20 | /** 21 | Found structures. 22 | */ 23 | public let structures: [StructDescription] 24 | 25 | /** 26 | Found protocols. 27 | */ 28 | public let protocols: [ProtocolDescription] 29 | 30 | /** 31 | Found enums. 32 | */ 33 | public let enums: [EnumDescription] 34 | 35 | /** 36 | Found free functions. 37 | */ 38 | public let functions: [FunctionDescription] 39 | 40 | /** 41 | Errors. 42 | */ 43 | public let parsingErrors: [SynopsisError] 44 | 45 | /** 46 | Analyze source code files and extract a `Synopsis` instance. 47 | */ 48 | public init(files: [URL]) { 49 | var errors: [SynopsisError] = [] 50 | 51 | let classParsingResult: ParsingResult = ClassDescriptionParser().parse(files: files) 52 | let structParsingResult: ParsingResult = StructDescriptionParser().parse(files: files) 53 | let protocolParsingResult: ParsingResult = ProtocolDescriptionParser().parse(files: files) 54 | let enumParsingResult: ParsingResult = EnumDescriptionParser().parse(files: files) 55 | let functionParsingResult: ParsingResult = FunctionDescriptionParser().parse(files: files) 56 | 57 | errors += classParsingResult.errors 58 | errors += structParsingResult.errors 59 | errors += protocolParsingResult.errors 60 | errors += enumParsingResult.errors 61 | errors += functionParsingResult.errors 62 | 63 | classes = classParsingResult.models 64 | structures = structParsingResult.models 65 | protocols = protocolParsingResult.models 66 | enums = enumParsingResult.models 67 | functions = functionParsingResult.models 68 | 69 | parsingErrors = errors 70 | } 71 | 72 | /** 73 | Print out parsed Synopsis through Xcode warnings. 74 | */ 75 | public func printToXcode() { 76 | var messages: [XcodeMessage] = [] 77 | 78 | classes.forEach { (classDescription: ClassDescription) in 79 | messages.append( 80 | XcodeMessage( 81 | declaration: classDescription.declaration, 82 | message: classDescription.debugDescription, 83 | type: XcodeMessage.MessageType.warning 84 | ) 85 | ) 86 | 87 | classDescription.annotations.forEach { (annotation: Annotation) in 88 | messages.append( 89 | XcodeMessage( 90 | declaration: classDescription.declaration, // TODO: replace with Annotation.declaration 91 | message: annotation.debugDescription, 92 | type: XcodeMessage.MessageType.warning 93 | ) 94 | ) 95 | } 96 | 97 | classDescription.properties.forEach { (propertyDescription: PropertyDescription) in 98 | messages.append( 99 | XcodeMessage( 100 | declaration: propertyDescription.declaration, 101 | message: propertyDescription.debugDescription, 102 | type: XcodeMessage.MessageType.warning 103 | ) 104 | ) 105 | } 106 | 107 | classDescription.methods.forEach { (methodDescription: MethodDescription) in 108 | messages.append( 109 | XcodeMessage( 110 | declaration: methodDescription.declaration, 111 | message: methodDescription.debugDescription, 112 | type: XcodeMessage.MessageType.warning 113 | ) 114 | ) 115 | 116 | methodDescription.arguments.forEach { (argumentDescription: ArgumentDescription) in 117 | messages.append( 118 | XcodeMessage( 119 | declaration: methodDescription.declaration, // TODO: replace with ArgumentDescription.declaration 120 | message: argumentDescription.debugDescription, 121 | type: XcodeMessage.MessageType.warning 122 | ) 123 | ) 124 | } 125 | } 126 | } // classes.forEach 127 | 128 | structures.forEach { (structDescription: StructDescription) in 129 | messages.append( 130 | XcodeMessage( 131 | declaration: structDescription.declaration, 132 | message: structDescription.debugDescription, 133 | type: XcodeMessage.MessageType.warning 134 | ) 135 | ) 136 | 137 | structDescription.annotations.forEach { (annotation: Annotation) in 138 | messages.append( 139 | XcodeMessage( 140 | declaration: structDescription.declaration, // TODO: replace with Annotation.declaration 141 | message: annotation.debugDescription, 142 | type: XcodeMessage.MessageType.warning 143 | ) 144 | ) 145 | } 146 | 147 | structDescription.properties.forEach { (propertyDescription: PropertyDescription) in 148 | messages.append( 149 | XcodeMessage( 150 | declaration: propertyDescription.declaration, 151 | message: propertyDescription.debugDescription, 152 | type: XcodeMessage.MessageType.warning 153 | ) 154 | ) 155 | } 156 | 157 | structDescription.methods.forEach { (methodDescription: MethodDescription) in 158 | messages.append( 159 | XcodeMessage( 160 | declaration: methodDescription.declaration, 161 | message: methodDescription.debugDescription, 162 | type: XcodeMessage.MessageType.warning 163 | ) 164 | ) 165 | 166 | methodDescription.arguments.forEach { (argumentDescription: ArgumentDescription) in 167 | messages.append( 168 | XcodeMessage( 169 | declaration: methodDescription.declaration, // TODO: replace with ArgumentDescription.declaration 170 | message: argumentDescription.debugDescription, 171 | type: XcodeMessage.MessageType.warning 172 | ) 173 | ) 174 | } 175 | } 176 | } // structures.forEach 177 | 178 | protocols.forEach { (protocolDescription: ProtocolDescription) in 179 | messages.append( 180 | XcodeMessage( 181 | declaration: protocolDescription.declaration, 182 | message: protocolDescription.debugDescription, 183 | type: XcodeMessage.MessageType.warning 184 | ) 185 | ) 186 | 187 | protocolDescription.annotations.forEach { (annotation: Annotation) in 188 | messages.append( 189 | XcodeMessage( 190 | declaration: protocolDescription.declaration, // TODO: replace with Annotation.declaration 191 | message: annotation.debugDescription, 192 | type: XcodeMessage.MessageType.warning 193 | ) 194 | ) 195 | } 196 | 197 | protocolDescription.properties.forEach { (propertyDescription: PropertyDescription) in 198 | messages.append( 199 | XcodeMessage( 200 | declaration: propertyDescription.declaration, 201 | message: propertyDescription.debugDescription, 202 | type: XcodeMessage.MessageType.warning 203 | ) 204 | ) 205 | } 206 | 207 | protocolDescription.methods.forEach { (methodDescription: MethodDescription) in 208 | messages.append( 209 | XcodeMessage( 210 | declaration: methodDescription.declaration, 211 | message: methodDescription.debugDescription, 212 | type: XcodeMessage.MessageType.warning 213 | ) 214 | ) 215 | 216 | methodDescription.arguments.forEach { (argumentDescription: ArgumentDescription) in 217 | messages.append( 218 | XcodeMessage( 219 | declaration: methodDescription.declaration, // TODO: replace with ArgumentDescription.declaration 220 | message: argumentDescription.debugDescription, 221 | type: XcodeMessage.MessageType.warning 222 | ) 223 | ) 224 | } 225 | } 226 | } // protocols.forEach 227 | 228 | messages.forEach { print($0) } 229 | } 230 | 231 | } 232 | -------------------------------------------------------------------------------- /Tests/SynopsisTests/Utility/ClassDescriptionParserTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Project «Synopsis» 3 | // Created by Jeorge Taflanidi 4 | // 5 | 6 | 7 | import XCTest 8 | @testable import Synopsis 9 | 10 | 11 | class ClassDescriptionParserTests: SynopsisTestCase { 12 | 13 | override func setUp() { 14 | super.setUp() 15 | storeContents(basic, asFile: "Basic.swift") 16 | storeContents(propertyWithGetter, asFile: "PropertyWithGetter.swift") 17 | storeContents(classProperty, asFile: "ClassProperty.swift") 18 | } 19 | 20 | override func tearDown() { 21 | deleteFile(named: "ClassProperty.swift") 22 | deleteFile(named: "PropertyWithGetter.swift") 23 | deleteFile(named: "Basic.swift") 24 | super.tearDown() 25 | } 26 | 27 | func testParse_basicFile_returnsAsExpected() { 28 | let inputFile: URL = urlForFile(named: "Basic.swift") 29 | let parser: ClassDescriptionParser = ClassDescriptionParser() 30 | let result: ParsingResult = parser.parse(files: [inputFile]) 31 | 32 | let expectedAnnotations: [Annotation] = [ 33 | Annotation(name: "model", value: nil, declaration: nil), 34 | Annotation(name: "realm", value: "DBBasic", declaration: nil), 35 | ] 36 | 37 | let expectedDeclaration: Declaration = Declaration( 38 | filePath: inputFile, 39 | rawText: "class Basic: Codable", 40 | offset: 154, 41 | lineNumber: 18, 42 | columnNumber: 1 43 | ) 44 | 45 | let expectedProperties: [PropertyDescription] = [ 46 | PropertyDescription( 47 | comment: "inferredString docs", 48 | annotations: [], 49 | accessibility: Accessibility.`internal`, 50 | constant: true, 51 | name: "inferredString", 52 | type: TypeDescription.string, 53 | defaultValue: "\"inferredStringValue\"", 54 | declaration: Declaration( 55 | filePath: inputFile, 56 | rawText: "let inferredString = \"inferredStringValue\"", 57 | offset: 223, 58 | lineNumber: 23, 59 | columnNumber: 5 60 | ), 61 | kind: .instance, 62 | body: nil 63 | ), 64 | PropertyDescription( 65 | comment: "basicInt docs", 66 | annotations: [], 67 | accessibility: Accessibility.`internal`, 68 | constant: false, 69 | name: "basicInt", 70 | type: TypeDescription.integer, 71 | defaultValue: nil, 72 | declaration: Declaration( 73 | filePath: inputFile, 74 | rawText: "var basicInt: Int", 75 | offset: 306, 76 | lineNumber: 28, 77 | columnNumber: 5 78 | ), 79 | kind: .instance, 80 | body: nil 81 | ), 82 | PropertyDescription( 83 | comment: "complexType docs", 84 | annotations: [], 85 | accessibility: Accessibility.`private`, 86 | constant: true, 87 | name: "complexType", 88 | type: TypeDescription.array(element: TypeDescription.object(name: "Basic")), 89 | defaultValue: nil, 90 | declaration: Declaration( 91 | filePath: inputFile, 92 | rawText: "private let complexType: [Basic]", 93 | offset: 375, 94 | lineNumber: 33, 95 | columnNumber: 13 96 | ), 97 | kind: .instance, 98 | body: nil 99 | ), 100 | ] 101 | 102 | let expectedMethods: [MethodDescription] = [ 103 | MethodDescription( 104 | comment: "init docs", 105 | annotations: [], 106 | accessibility: Accessibility.`internal`, 107 | name: "init(basicInt:)", 108 | arguments: [ 109 | ArgumentDescription( 110 | name: "basicInt", 111 | bodyName: "basicInt", 112 | type: TypeDescription.integer, 113 | defaultValue: nil, 114 | annotations: [], 115 | declaration: nil, 116 | comment: nil 117 | ) 118 | ], 119 | returnType: TypeDescription.object(name: "Basic"), 120 | declaration: Declaration( 121 | filePath: inputFile, 122 | rawText: "init(basicInt: Int)", 123 | offset: 436, 124 | lineNumber: 38, 125 | columnNumber: 5 126 | ), 127 | kind: .instance, 128 | body: "\n self.basicInt = basicInt\n self.complexType = Date()\n " 129 | ), 130 | MethodDescription( 131 | comment: "internalMethod docs", 132 | annotations: [], 133 | accessibility: Accessibility.`internal`, 134 | name: "internalMethod(parameter:asInteger:)", 135 | arguments: [ 136 | ArgumentDescription( 137 | name: "parameter", 138 | bodyName: "parameter", 139 | type: TypeDescription.string, 140 | defaultValue: nil, 141 | annotations: [ 142 | Annotation( 143 | name: "string", 144 | value: nil, 145 | declaration: nil 146 | ) 147 | ], 148 | declaration: nil, 149 | comment: " @string" 150 | ), 151 | ArgumentDescription( 152 | name: "asInteger", 153 | bodyName: "integer", 154 | type: TypeDescription.integer, 155 | defaultValue: "0", 156 | annotations: [], 157 | declaration: nil, 158 | comment: nil 159 | ), 160 | ], 161 | returnType: TypeDescription.generic(name: "ServiceCall", constraints: [TypeDescription.object(name: "Person")]), 162 | declaration: Declaration( 163 | filePath: inputFile, 164 | rawText: "func internalMethod(\n parameter: String, // @string\n asInteger integer: Int = 0\n) -> ServiceCall", 165 | offset: 564, 166 | lineNumber: 44, 167 | columnNumber: 5 168 | ), 169 | kind: .instance, 170 | body: "\n print(basicInt)\n " 171 | ), 172 | MethodDescription( 173 | comment: nil, 174 | annotations: [], 175 | accessibility: Accessibility.`private`, 176 | name: "privateMethod()", 177 | arguments: [], 178 | returnType: TypeDescription.void, 179 | declaration: Declaration( 180 | filePath: inputFile, 181 | rawText: "private func privateMethod()", 182 | offset: 771, 183 | lineNumber: 54, 184 | columnNumber: 13 185 | ), 186 | kind: .instance, 187 | body: "\n print(inferredString)\n " 188 | ), 189 | ] 190 | 191 | XCTAssertEqual(result.models.count, 1) 192 | let basicClassDescription: ClassDescription = result.models.first! 193 | 194 | XCTAssertEqual(basicClassDescription.comment, "Basic docs\n\n@model\n@realm DBBasic") 195 | XCTAssertEqual(basicClassDescription.annotations, expectedAnnotations) 196 | XCTAssertEqual(basicClassDescription.declaration, expectedDeclaration) 197 | XCTAssertEqual(basicClassDescription.accessibility, Accessibility.`internal`) 198 | XCTAssertEqual(basicClassDescription.name, "Basic") 199 | XCTAssertEqual(basicClassDescription.inheritedTypes.first, "Codable") 200 | XCTAssertEqual(basicClassDescription.properties, expectedProperties) 201 | XCTAssertEqual(basicClassDescription.methods, expectedMethods) 202 | } 203 | 204 | func testParse_propertyGetter_returnsPropertyDescriptionWithBody() { 205 | let inputFile: URL = urlForFile(named: "PropertyWithGetter.swift") 206 | let parser: ClassDescriptionParser = ClassDescriptionParser() 207 | let result: ParsingResult = parser.parse(files: [inputFile]) 208 | 209 | let expectedAnnotations: [Annotation] = [] 210 | 211 | let expectedDeclaration: Declaration = Declaration( 212 | filePath: inputFile, 213 | rawText: "class Basic", 214 | offset: 40, 215 | lineNumber: 7, 216 | columnNumber: 1 217 | ) 218 | 219 | let expectedProperties: [PropertyDescription] = [ 220 | PropertyDescription( 221 | comment: "inferredString docs", 222 | annotations: [], 223 | accessibility: Accessibility.`internal`, 224 | constant: false, 225 | name: "inferredString", 226 | type: TypeDescription.string, 227 | defaultValue: nil, 228 | declaration: Declaration( 229 | filePath: inputFile, 230 | rawText: "var inferredString: String", 231 | offset: 100, 232 | lineNumber: 12, 233 | columnNumber: 5 234 | ), 235 | kind: .instance, 236 | body: "\n return \"inferredStringValue\"\n " 237 | ), 238 | ] 239 | 240 | let expectedMethods: [MethodDescription] = [] 241 | 242 | XCTAssertEqual(result.models.count, 1) 243 | let basicClassDescription: ClassDescription = result.models.first! 244 | 245 | XCTAssertEqual(basicClassDescription.comment, "Basic docs") 246 | XCTAssertEqual(basicClassDescription.annotations, expectedAnnotations) 247 | XCTAssertEqual(basicClassDescription.declaration, expectedDeclaration) 248 | XCTAssertEqual(basicClassDescription.accessibility, Accessibility.`internal`) 249 | XCTAssertEqual(basicClassDescription.name, "Basic") 250 | XCTAssertEqual(basicClassDescription.properties, expectedProperties) 251 | XCTAssertEqual(basicClassDescription.methods, expectedMethods) 252 | } 253 | 254 | func testParse_classProperty_returnsAsExpected() { 255 | let inputFile: URL = urlForFile(named: "ClassProperty.swift") 256 | let parser: ClassDescriptionParser = ClassDescriptionParser() 257 | let result: ParsingResult = parser.parse(files: [inputFile]) 258 | 259 | let expectedAnnotations: [Annotation] = [] 260 | 261 | let expectedDeclaration: Declaration = Declaration( 262 | filePath: inputFile, 263 | rawText: "class Basic", 264 | offset: 40, 265 | lineNumber: 7, 266 | columnNumber: 1 267 | ) 268 | 269 | let expectedProperties: [PropertyDescription] = [ 270 | PropertyDescription( 271 | comment: "asd docs", 272 | annotations: [], 273 | accessibility: Accessibility.`internal`, 274 | constant: false, 275 | name: "asd", 276 | type: TypeDescription.string, 277 | defaultValue: nil, 278 | declaration: Declaration( 279 | filePath: inputFile, 280 | rawText: "class var asd: String", 281 | offset: 89, 282 | lineNumber: 12, 283 | columnNumber: 5 284 | ), 285 | kind: .`class`, 286 | body: "\n return \"asd\"\n " 287 | ), 288 | ] 289 | 290 | let expectedMethods: [MethodDescription] = [] 291 | 292 | XCTAssertEqual(result.models.count, 1) 293 | let basicClassDescription: ClassDescription = result.models.first! 294 | 295 | XCTAssertEqual(basicClassDescription.comment, "Basic docs") 296 | XCTAssertEqual(basicClassDescription.annotations, expectedAnnotations) 297 | XCTAssertEqual(basicClassDescription.declaration, expectedDeclaration) 298 | XCTAssertEqual(basicClassDescription.accessibility, Accessibility.`internal`) 299 | XCTAssertEqual(basicClassDescription.name, "Basic") 300 | XCTAssertEqual(basicClassDescription.properties, expectedProperties) 301 | XCTAssertEqual(basicClassDescription.methods, expectedMethods) 302 | } 303 | 304 | static var allTests = [ 305 | ("testParse_basicFile_returnsAsExpected", testParse_basicFile_returnsAsExpected), 306 | ("testParse_propertyGetter_returnsPropertyDescriptionWithBody", testParse_propertyGetter_returnsPropertyDescriptionWithBody), 307 | ] 308 | } 309 | 310 | 311 | let basic = """ 312 | // 313 | // Basic.swift 314 | // SynopsisTests 315 | // 316 | // Created by John Appleseed on 10.11.29H. 317 | // 318 | 319 | 320 | import Foundation 321 | 322 | 323 | /** 324 | Basic docs 325 | 326 | @model 327 | @realm DBBasic 328 | */ 329 | class Basic: Codable { 330 | 331 | /** 332 | inferredString docs 333 | */ 334 | let inferredString = "inferredStringValue" 335 | 336 | /** 337 | basicInt docs 338 | */ 339 | var basicInt: Int 340 | 341 | /** 342 | complexType docs 343 | */ 344 | private let complexType: [Basic] 345 | 346 | /** 347 | init docs 348 | */ 349 | init(basicInt: Int) { 350 | self.basicInt = basicInt 351 | self.complexType = Date() 352 | } 353 | 354 | /// internalMethod docs 355 | func internalMethod( 356 | parameter: String, // @string 357 | asInteger integer: Int = 0 358 | ) -> ServiceCall { 359 | print(basicInt) 360 | } 361 | 362 | /* 363 | privateMethod docs 364 | */ 365 | private func privateMethod() { 366 | print(inferredString) 367 | } 368 | 369 | } 370 | """ 371 | 372 | 373 | let propertyWithGetter = """ 374 | import Foundation 375 | 376 | 377 | /** 378 | Basic docs 379 | */ 380 | class Basic { 381 | 382 | /** 383 | inferredString docs 384 | */ 385 | var inferredString: String { 386 | return "inferredStringValue" 387 | } 388 | 389 | } 390 | """ 391 | 392 | 393 | let classProperty = """ 394 | import Foundation 395 | 396 | 397 | /** 398 | Basic docs 399 | */ 400 | class Basic { 401 | 402 | /** 403 | asd docs 404 | */ 405 | class var asd: String { 406 | return "asd" 407 | } 408 | 409 | } 410 | """ 411 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![](main-illustration.png) 2 | ## Description 3 | 4 | The package is designed to gather information from Swift source files and compile this information into concrete objects with 5 | strongly typed properties containing descriptions of found symbols. 6 | 7 | In other words, if you have a source code file like 8 | ```swift 9 | // MyClass.swift 10 | 11 | /// My class does nothing. 12 | open class MyClass {} 13 | ``` 14 | — **Synopsis** will give you structurized information that there's a `class`, it's `open` and named `MyClass`, with no methods nor properties, 15 | and the class is documented as `My class does nothing`. Also, it has no parents. 16 | 17 | ## Installation 18 | ### Swift Package Manager dependency 19 | 20 | ```swift 21 | Package.Dependency.package( 22 | url: "https://github.com/RedMadRobot/synopsis", 23 | from: "1.0.0" 24 | ) 25 | ``` 26 | 27 | ## Usage 28 | 29 | * [Synopsis struct](#synopsis-struct) 30 | - [Classes, structs and protocols](#classdescription) 31 | - [Enums](#enums) 32 | - [Methods and functions](#functions) 33 | - [Properties](#properties) 34 | - [Annotations](#annotation) 35 | - [Property types, argument types, return types](#types) 36 | - [Declaration](#declarations) 37 | * [Code generation, templates and versing](#versing) 38 | * [Running tests](#tests) 39 | 40 | 41 | 42 | ### Synopsis struct 43 | 44 | `Synopsis` structure is your starting point. This structure provides you with an `init(files:)` initializer that accepts a list of file URLs 45 | of your `*.swift` source code files. 46 | 47 | ```swift 48 | let mySwiftFiles: [URL] = getFiles() 49 | 50 | let synopsis = Synopsis(files: mySwiftFiles) 51 | ``` 52 | 53 | Initialized `Synopsis` structure has properties `classes`, `structures`, `protocols`, `enums` and `functions` containing descirpitons 54 | of found classes, structs, protocols, enums and high-level free functions respectively. You may also examine `parsingErrors` property 55 | with a list of problems occured during the compilation process. 56 | 57 | ```swift 58 | struct Synopsis { 59 | let classes: [ClassDescription] 60 | let structures: [StructDescription] 61 | let protocols: [ProtocolDescription] 62 | let enums: [EnumDescription] 63 | let functions: [FunctionDescription] 64 | let parsingErrors: [SynopsisError] 65 | } 66 | ``` 67 | 68 | 69 | 70 | ### Classes, structs and protocols 71 | 72 | Meta-information about found classes, structs and protocols is organized as `ClassDescription`, `StructDescription` 73 | or `ProtocolDescription` structs respectively. Each of these implements an `Extensible` protocol. 74 | 75 | ```swift 76 | struct ClassDescription: Extensible {} 77 | struct StructDescription: Extensible {} 78 | struct ProtocolDescription: Extensible {} 79 | ``` 80 | 81 | 82 | 83 | #### Extensible 84 | 85 | ```swift 86 | protocol Extensible: Equatable, CustomDebugStringConvertible { 87 | var comment: String? 88 | var annotations: [Annotation] 89 | var declaration: Declaration 90 | var accessibility: Accessibility 91 | var name: String 92 | var inheritedTypes: [String] 93 | var properties: [PropertyDescription] 94 | var methods: [MethodDescription] 95 | 96 | var verse: String // this one is special 97 | } 98 | ``` 99 | 100 | Extensibles (read like «classes», «structs» or «protocols») include 101 | 102 | * `comment` — an optional documentation above the extensible. 103 | * `annotations` — a list of `Annotation` instances parsed from the `comment`; see [Annotation](#annotation) for more details. 104 | * `declaration` — an information, where this current extensible could be found (file, line number, column number etc.); see [Declaration](#declarations) for more details. 105 | * `accessibility` — an `enum` of `private`, `internal`, `public` and `open`. 106 | * `name` — an extensible name. 107 | * `inheritedTypes` — a list of all parents, if any. 108 | * `properties` — a list of all properties; see [Property](#properties) for more details. 109 | * `methods` — a list of methods, including initializers; see [Methods and functions](#functions) for more details. 110 | 111 | There's also a special computed property `verse: String`, which allows to obtain the `Extensible` as a source code. 112 | This is a convenient way of composing new utility classes, see [Code generation, templates and versing](#versing) for more information. 113 | 114 | All extensibles support `Equatable` and `CustomDebugStringConvertible` protocols, and extend `Sequence` with 115 | `subscript(name:)` and `contains(name:)` methods. 116 | 117 | ```swift 118 | extension Sequence where Iterator.Element: Extensible { 119 | subscript(name: String) -> Iterator.Element? 120 | func contains(name: String) -> Bool 121 | } 122 | ``` 123 | 124 | 125 | 126 | ### Enums 127 | 128 | ```swift 129 | struct EnumDescription: Equatable, CustomDebugStringConvertible { 130 | let comment: String? 131 | let annotations: [Annotation] 132 | let declaration: Declaration 133 | let accessibility: Accessibility 134 | let name: String 135 | let inheritedTypes: [String] 136 | let cases: [EnumCase] // !!! enum cases !!! 137 | let properties: [PropertyDescription] 138 | let methods: [MethodDescription] 139 | 140 | var verse: String 141 | } 142 | ``` 143 | 144 | Enum descriptions contain almost the same information as the extensibles, but also include a list of cases. 145 | 146 | 147 | 148 | #### Enum cases 149 | 150 | ```swift 151 | struct EnumCase: Equatable, CustomDebugStringConvertible { 152 | let comment: String? 153 | let annotations: [Annotation] 154 | let name: String 155 | let defaultValue: String? // everything after "=", e.g. case firstName = "first_name" 156 | let declaration: Declaration 157 | 158 | var verse: String 159 | } 160 | ``` 161 | 162 | All enum cases have `String` names, and declarations. They may also have documentation (with [annotations](#annotation)) and optional `defaultValue: String?`. 163 | 164 | You should know, that `defaultValue` is a raw text, which may contain symbols like quotes. 165 | 166 | ```swift 167 | enum CodingKeys { 168 | case firstName = "first_name" // defaultValue == "\"first_name\"" 169 | } 170 | ``` 171 | 172 | 173 | 174 | ### Methods and functions 175 | 176 | ```swift 177 | class FunctionDescription: Equatable, CustomDebugStringConvertible { 178 | let comment: String? 179 | let annotations: [Annotation] 180 | let accessibility: Accessibility 181 | let name: String 182 | let arguments: [ArgumentDescription] 183 | let returnType: TypeDescription? 184 | let declaration: Declaration 185 | let kind: Kind // see below 186 | let body: String? 187 | 188 | var verse: String 189 | 190 | enum Kind { 191 | case free 192 | case class 193 | case static 194 | case instance 195 | } 196 | } 197 | ``` 198 | 199 | **Synopsis** assumes that method is a function subclass with a couple additional features. 200 | 201 | All functions have 202 | 203 | * optional documentation; 204 | * [annotations](#annotation); 205 | * accessibility (`private`, `internal`, `public` or `open`); 206 | * name; 207 | * list of arguments (of type `ArgumentDescription`, [see below](#arguments)); 208 | * optional return type (of type `TypeDescription`, [see below](#types)); 209 | * a declaration (of type `Declaration`, [see below](#declarations)); 210 | * kind; 211 | * optional body. 212 | 213 | Methods also have a computed property `isInitializer: Bool`. 214 | 215 | ```swift 216 | class MethodDescription: FunctionDescription { 217 | var isInitializer: Bool { 218 | return name.hasPrefix("init(") 219 | } 220 | } 221 | // literally no more reasonable code 222 | ``` 223 | 224 | While most of the `FunctionDescription` properties are self-explanatory, some of them have their own quirks and tricky details behind. 225 | For instance, method names must contain round brackets `()` and are actually a kind of a signature without types, e.g. `myFunction(argument:count:)`. 226 | 227 | ```swift 228 | func myFunction(arg argument: String) -> Int {} 229 | // this function is named "myFunction(arg:)" 230 | ``` 231 | 232 | Function `kind` could only be `free`, while methods could have a `class`, `static` or `instance` kind. 233 | 234 | Methods inside protocols have the same set of properties, but contain no body. 235 | The body itself is a text inside curly brackets `{...}`, but without brackets. 236 | 237 | ```swift 238 | func topLevelFunction() { 239 | } 240 | // this function body is equal to "\n" 241 | ``` 242 | 243 | 244 | 245 | #### Arguments 246 | 247 | ```swift 248 | struct ArgumentDescription: Equatable, CustomDebugStringConvertible { 249 | let name: String 250 | let bodyName: String 251 | let type: TypeDescription 252 | let defaultValue: String? 253 | let annotations: [Annotation] 254 | let comment: String? 255 | 256 | var verse: String 257 | } 258 | ``` 259 | 260 | Function and method arguments all have external and internal names, a type, an optional `defaultValue`, own optional documentation and [annotations](#annotation). 261 | 262 | External `name` is an argument name when the function is called. Internal `bodyName` is used insibe function body. Both are mandatory, though they could be equal. 263 | 264 | Argument type is described below, see [TypeDescription](#types). 265 | 266 | 267 | 268 | ### Properties 269 | 270 | Properties are represented with a `PropertyDescription` struct. 271 | 272 | ```swift 273 | struct PropertyDescription: Equatable, CustomDebugStringConvertible { 274 | let comment: String? 275 | let annotations: [Annotation] 276 | let accessibility: Accessibility 277 | let constant: Bool // is it "let"? If not, it's "var" 278 | let name: String 279 | let type: TypeDescription 280 | let defaultValue: String? // literally everything after "=", if there is a "=" 281 | let declaration: Declaration 282 | let kind: Kind // see below 283 | let body: String? // literally everything between curly brackets, but without brackets 284 | 285 | var verse: String 286 | 287 | enum Kind { 288 | case class 289 | case static 290 | case instance 291 | } 292 | } 293 | ``` 294 | 295 | Properties could have documentation and [annotations](#annotation). All properties have own `kind` of `class`, `static` or `instance`. 296 | All properties have names, `constant` boolean flag, accessibility, type (see [TypeDescription](#types)), a raw `defaultValue: String?` 297 | and a `declaration: Declaration`. 298 | 299 | Computed properties could also have a `body`, like functions. The body itself is a text inside curly brackets `{...}`, 300 | but without brackets. 301 | 302 | 303 | 304 | ### Annotations 305 | 306 | ```swift 307 | struct Annotation: Equatable, CustomDebugStringConvertible { 308 | let name: String 309 | let value: String? 310 | } 311 | ``` 312 | 313 | Extensibles, enums, functions, methods and properties are all allowed to have documentation. 314 | 315 | **Synopsis** parses documentation in order to gather special annotation elements with important meta-information. 316 | These annotations resemble Java annotations, but lack their compile-time checks. 317 | 318 | All annotations are required to have a name. Annotations can also contain an optional `String` value. 319 | 320 | Annotations are recognized by the `@` symbol, for instance: 321 | 322 | ```swift 323 | /// @model 324 | class Model {} 325 | ``` 326 | 327 | > N.B. Documentation comment syntax is inherited from the Swift compiler, and for now supports block comments and triple slash comments. 328 | > Method or function arguments usually contain documentation in the nearby inline comments, see below. 329 | 330 | Use line breaks or semicolons `;` to divide separate annotations: 331 | 332 | ```swift 333 | /** 334 | @annotation1 335 | @annotation2; @annotation3 336 | @annotation4 value1 337 | @annotation5 value2; @annotation5 value3 338 | @anontation6; @annotation7 value4 339 | */ 340 | ``` 341 | 342 | Keep annotated function or method arguments on their own separate lines for readability: 343 | 344 | ```swift 345 | func doSomething( 346 | with argument: String, // @annotation1 347 | or argument2: Int, /* @annotation2 value1; @annotation3 value2 */ 348 | finally argument3: Double // @annotation4; annotation5 value3 349 | ) -> Int 350 | ``` 351 | 352 | Though it is not prohibited to have annotations above arguments: 353 | 354 | ```swift 355 | func doSomething( 356 | // @annotation1 357 | with argument: String, 358 | /* @annotation2 value1; @annotation3 value2 */ 359 | or argument2: Int, 360 | // @annotation4; annotation5 value3 361 | finally argument3: Double 362 | ) -> Int 363 | ``` 364 | 365 | 366 | 367 | ### Types 368 | 369 | Property types, argument types, function return types are represented with a `TypeDescription` enum with cases: 370 | 371 | * `boolean` 372 | * `integer` 373 | * `floatingPoint` 374 | * `doublePrecision` 375 | * `string` 376 | * `date` 377 | * `data` 378 | * `optional(wrapped: TypeDescription)` 379 | * `object(name: String)` 380 | * `array(element: TypeDescription)` 381 | * `map(key: TypeDescription, value: TypeDescription)` 382 | * `generic(name: String, constraints: [TypeDescription])` 383 | 384 | While some of these cases are self-explanatory, others need additional clarification. 385 | 386 | `integer` type for now has a limitation, as it represents all `Int` types like `Int16`, `Int32` etc. This means **Synopsis** won't let you determine the `Int` size. 387 | 388 | `optional` type contains a wrapped `TypeDescription` for the actual value type. Same happens for arrays, maps and generics. 389 | 390 | All object types except for `Data`, `Date`, `NSData` and `NSDate` are represented with an `object(name: String)` case. So, while `CGRect` is a struct, `Synopsis` will still thinks it is an `object("CGRect")`. 391 | 392 | 393 | 394 | ### Declaration 395 | 396 | ```swift 397 | struct Declaration: Equatable { 398 | public let filePath: URL 399 | public let rawText: String 400 | public let offset: Int 401 | public let lineNumber: Int 402 | public let columnNumber: Int 403 | } 404 | ``` 405 | 406 | Classes, structs, protocols, properties, methods etc. — almost all detected source code elements have a `declaration: Declaration` property. 407 | 408 | `Declaration` structure encapsulates several properties: 409 | 410 | * filePath — a URL to the end file, where the source code element was detected; 411 | * rawText — a raw line, which was parsed in order to detect source code element; 412 | * offset — a numer of symbols from the beginning of file to the detected source code element; 413 | * lineNumber — self-explanatory; 414 | * columnNumber — self-explanatory; starts from 1. 415 | 416 | 417 | 418 | ### Code generation, templates and versing 419 | 420 | Each source code element provides a computed `String` property `verse`, which allows to obtain this element's source code. 421 | 422 | This source code is composed programmatically, thus it may differ from the by-hand implementation. 423 | 424 | This allows to generate new source code by composing, e.g, `ClassDescrption` instances by hand. 425 | 426 | Though, each `ClassDescription` instance requires a `Declaration`, which contains a `filePath`, `rawText`, `offset` and other properties yet to be defined, because such source code hasn't been generated yet. 427 | 428 | This is why `ClassDescription` and others provide you with a `template(...)` constructor, which replaces declaration with a special mock object. 429 | 430 | Please, consider reviewing `Tests/SynopsisTests/Versing` test cases in order to get familiar with the concept. 431 | 432 | ```swift 433 | func testVerse_fullyPacked_returnsAsExpected() { 434 | let enumDescription = EnumDescription.template( 435 | comment: "Docs", 436 | accessibility: Accessibility.`private`, 437 | name: "MyEnum", 438 | inheritedTypes: ["String"], 439 | cases: [ 440 | EnumCase.template(comment: "First", name: "firstName", defaultValue: "\"first_name\""), 441 | EnumCase.template(comment: "Second", name: "lastName", defaultValue: "\"last_name\""), 442 | ], 443 | properties: [], 444 | methods: [] 445 | ) 446 | 447 | let expectedVerse = """ 448 | /// Docs 449 | private enum MyEnum: String { 450 | /// First 451 | case firstName = "first_name" 452 | 453 | /// Second 454 | case lastName = "last_name" 455 | } 456 | 457 | """ 458 | 459 | XCTAssertEqual(enumDescription.verse, expectedVerse) 460 | } 461 | ``` 462 | 463 | 464 | 465 | ### Running tests 466 | 467 | Use `spm_resolve.command` to load all dependencies and `spm_generate_xcodeproj.command` to assemble an Xcode project file. 468 | Also, ensure Xcode targets macOS when running tests. 469 | --------------------------------------------------------------------------------