The package is designed to gather information from Swift source files and compile this information into concrete objects with strongly typed properties containing descriptions of found symbols.
In other words, if you have a source code file like
// MyClass.swift
/// My class does nothing.
open class MyClass {}
— Synopsis will give you structurized information that there's a class
, it's open
and named MyClass
, with no methods nor properties,
and the class is documented as My class does nothing
. Also, it has no parents.
Package.Dependency.package(
url: "https://github.com/RedMadRobot/synopsis",
from: "1.0.0"
)
Synopsis
structure is your starting point. This structure provides you with an init(files:)
initializer that accepts a list of file URLs
of your *.swift
source code files.
let mySwiftFiles: [URL] = getFiles()
let synopsis = Synopsis(files: mySwiftFiles)
Initialized Synopsis
structure has properties classes
, structures
, protocols
, enums
and functions
containing descirpitons
of found classes, structs, protocols, enums and high-level free functions respectively. You may also examine parsingErrors
property
with a list of problems occured during the compilation process.
struct Synopsis {
let classes: [ClassDescription]
let structures: [StructDescription]
let protocols: [ProtocolDescription]
let enums: [EnumDescription]
let functions: [FunctionDescription]
let parsingErrors: [SynopsisError]
}
Meta-information about found classes, structs and protocols is organized as ClassDescription
, StructDescription
or ProtocolDescription
structs respectively. Each of these implements an Extensible
protocol.
struct ClassDescription: Extensible {}
struct StructDescription: Extensible {}
struct ProtocolDescription: Extensible {}
protocol Extensible: Equatable, CustomDebugStringConvertible {
var comment: String?
var annotations: [Annotation]
var declaration: Declaration
var accessibility: Accessibility
var name: String
var inheritedTypes: [String]
var properties: [PropertyDescription]
var methods: [MethodDescription]
var verse: String // this one is special
}
Extensibles (read like «classes», «structs» or «protocols») include
comment
— an optional documentation above the extensible.annotations
— a list ofAnnotation
instances parsed from thecomment
; see Annotation for more details.declaration
— an information, where this current extensible could be found (file, line number, column number etc.); see Declaration for more details.accessibility
— anenum
ofprivate
,internal
,public
andopen
.name
— an extensible name.inheritedTypes
— a list of all parents, if any.properties
— a list of all properties; see Property for more details.methods
— a list of methods, including initializers; see Methods and functions for more details.
There's also a special computed property verse: String
, which allows to obtain the Extensible
as a source code.
This is a convenient way of composing new utility classes, see Code generation, templates and versing for more information.
All extensibles support Equatable
and CustomDebugStringConvertible
protocols, and extend Sequence
with
subscript(name:)
and contains(name:)
methods.
extension Sequence where Iterator.Element: Extensible {
subscript(name: String) -> Iterator.Element?
func contains(name: String) -> Bool
}
struct EnumDescription: Equatable, CustomDebugStringConvertible {
let comment: String?
let annotations: [Annotation]
let declaration: Declaration
let accessibility: Accessibility
let name: String
let inheritedTypes: [String]
let cases: [EnumCase] // !!! enum cases !!!
let properties: [PropertyDescription]
let methods: [MethodDescription]
var verse: String
}
Enum descriptions contain almost the same information as the extensibles, but also include a list of cases.
struct EnumCase: Equatable, CustomDebugStringConvertible {
let comment: String?
let annotations: [Annotation]
let name: String
let defaultValue: String? // everything after "=", e.g. case firstName = "first_name"
let declaration: Declaration
var verse: String
}
All enum cases have String
names, and declarations. They may also have documentation (with annotations) and optional defaultValue: String?
.
You should know, that defaultValue
is a raw text, which may contain symbols like quotes.
enum CodingKeys {
case firstName = "first_name" // defaultValue == "\"first_name\""
}
class FunctionDescription: Equatable, CustomDebugStringConvertible {
let comment: String?
let annotations: [Annotation]
let accessibility: Accessibility
let name: String
let arguments: [ArgumentDescription]
let returnType: TypeDescription?
let declaration: Declaration
let kind: Kind // see below
let body: String?
var verse: String
enum Kind {
case free
case class
case static
case instance
}
}
Synopsis assumes that method is a function subclass with a couple additional features.
All functions have
- optional documentation;
- annotations;
- accessibility (
private
,internal
,public
oropen
); - name;
- list of arguments (of type
ArgumentDescription
, see below); - optional return type (of type
TypeDescription
, see below); - a declaration (of type
Declaration
, see below); - kind;
- optional body.
Methods also have a computed property isInitializer: Bool
.
class MethodDescription: FunctionDescription {
var isInitializer: Bool {
return name.hasPrefix("init(")
}
}
// literally no more reasonable code
While most of the FunctionDescription
properties are self-explanatory, some of them have their own quirks and tricky details behind.
For instance, method names must contain round brackets ()
and are actually a kind of a signature without types, e.g. myFunction(argument:count:)
.
func myFunction(arg argument: String) -> Int {}
// this function is named "myFunction(arg:)"
Function kind
could only be free
, while methods could have a class
, static
or instance
kind.
Methods inside protocols have the same set of properties, but contain no body.
The body itself is a text inside curly brackets {...}
, but without brackets.
func topLevelFunction() {
}
// this function body is equal to "\n"
struct ArgumentDescription: Equatable, CustomDebugStringConvertible {
let name: String
let bodyName: String
let type: TypeDescription
let defaultValue: String?
let annotations: [Annotation]
let comment: String?
var verse: String
}
Function and method arguments all have external and internal names, a type, an optional defaultValue
, own optional documentation and annotations.
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.
Argument type is described below, see TypeDescription.
Properties are represented with a PropertyDescription
struct.
struct PropertyDescription: Equatable, CustomDebugStringConvertible {
let comment: String?
let annotations: [Annotation]
let accessibility: Accessibility
let constant: Bool // is it "let"? If not, it's "var"
let name: String
let type: TypeDescription
let defaultValue: String? // literally everything after "=", if there is a "="
let declaration: Declaration
let kind: Kind // see below
let body: String? // literally everything between curly brackets, but without brackets
var verse: String
enum Kind {
case class
case static
case instance
}
}
Properties could have documentation and annotations. All properties have own kind
of class
, static
or instance
.
All properties have names, constant
boolean flag, accessibility, type (see TypeDescription), a raw defaultValue: String?
and a declaration: Declaration
.
Computed properties could also have a body
, like functions. The body itself is a text inside curly brackets {...}
,
but without brackets.
struct Annotation: Equatable, CustomDebugStringConvertible {
let name: String
let value: String?
}
Extensibles, enums, functions, methods and properties are all allowed to have documentation.
Synopsis parses documentation in order to gather special annotation elements with important meta-information. These annotations resemble Java annotations, but lack their compile-time checks.
All annotations are required to have a name. Annotations can also contain an optional String
value.
Annotations are recognized by the @
symbol, for instance:
/// @model
class Model {}
N.B. Documentation comment syntax is inherited from the Swift compiler, and for now supports block comments and triple slash comments. Method or function arguments usually contain documentation in the nearby inline comments, see below.
Use line breaks or semicolons ;
to divide separate annotations:
/**
@annotation1
@annotation2; @annotation3
@annotation4 value1
@annotation5 value2; @annotation5 value3
@anontation6; @annotation7 value4
*/
Keep annotated function or method arguments on their own separate lines for readability:
func doSomething(
with argument: String, // @annotation1
or argument2: Int, /* @annotation2 value1; @annotation3 value2 */
finally argument3: Double // @annotation4; annotation5 value3
) -> Int
Though it is not prohibited to have annotations above arguments:
func doSomething(
// @annotation1
with argument: String,
/* @annotation2 value1; @annotation3 value2 */
or argument2: Int,
// @annotation4; annotation5 value3
finally argument3: Double
) -> Int
Property types, argument types, function return types are represented with a TypeDescription
enum with cases:
boolean
integer
floatingPoint
doublePrecision
string
date
data
optional(wrapped: TypeDescription)
object(name: String)
array(element: TypeDescription)
map(key: TypeDescription, value: TypeDescription)
generic(name: String, constraints: [TypeDescription])
While some of these cases are self-explanatory, others need additional clarification.
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.
optional
type contains a wrapped TypeDescription
for the actual value type. Same happens for arrays, maps and generics.
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")
.
struct Declaration: Equatable {
public let filePath: URL
public let rawText: String
public let offset: Int
public let lineNumber: Int
public let columnNumber: Int
}
Classes, structs, protocols, properties, methods etc. — almost all detected source code elements have a declaration: Declaration
property.
Declaration
structure encapsulates several properties:
- filePath — a URL to the end file, where the source code element was detected;
- rawText — a raw line, which was parsed in order to detect source code element;
- offset — a numer of symbols from the beginning of file to the detected source code element;
- lineNumber — self-explanatory;
- columnNumber — self-explanatory; starts from 1.
Each source code element provides a computed String
property verse
, which allows to obtain this element's source code.
This source code is composed programmatically, thus it may differ from the by-hand implementation.
This allows to generate new source code by composing, e.g, ClassDescrption
instances by hand.
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.
This is why ClassDescription
and others provide you with a template(...)
constructor, which replaces declaration with a special mock object.
Please, consider reviewing Tests/SynopsisTests/Versing
test cases in order to get familiar with the concept.
func testVerse_fullyPacked_returnsAsExpected() {
let enumDescription = EnumDescription.template(
comment: "Docs",
accessibility: Accessibility.`private`,
name: "MyEnum",
inheritedTypes: ["String"],
cases: [
EnumCase.template(comment: "First", name: "firstName", defaultValue: "\"first_name\""),
EnumCase.template(comment: "Second", name: "lastName", defaultValue: "\"last_name\""),
],
properties: [],
methods: []
)
let expectedVerse = """
/// Docs
private enum MyEnum: String {
/// First
case firstName = "first_name"
/// Second
case lastName = "last_name"
}
"""
XCTAssertEqual(enumDescription.verse, expectedVerse)
}
Use spm_resolve.command
to load all dependencies and spm_generate_xcodeproj.command
to assemble an Xcode project file.
Also, ensure Xcode targets macOS when running tests.