SyntaxSparrow
Abstraction built on top of the Swift Syntax library to traverse constituent declaration types for Swift code.
Install / Use
/learn @CheekyGhost-Labs/SyntaxSparrowREADME
SyntaxSparrow
SyntaxSparrow is a Swift library designed to facilitate the analysis and interaction with Swift source code. It leverages SwiftSyntax to parse Swift code and produce a syntax tree which collects and traverses constituent declaration types for Swift code.
Workflows:
| Branch | Latest Swift/Xcode |
|:---------|:------------------:|
| main | |
| develop |
|
Swift 6 Support
SyntaxSparrow added explicit support for Swift 6 (and the Swift 6 language mode) as of version 5.0.0. It is still compatible back to Swift 5.8, however, the package manifests for version 5.8, 5.9, and 5.10 do not include the explicit swift 6 language mode setting.
Note on Swift 5.7 Support
The latest version of the underlying swift-syntax library no longer supports swift 5.7, as such, Syntax Sparrow will stop active support for Swift 5.7 from version 5.0.0
If you need support for 5.7, and specifically want features added after the 5.0.0 bump, you may need to fork the repo and add support yourself. If there is a backwards compatible change that can be captured as a minor update we will gladly release it via the PR process. However, if there are changes resulting in a major bump with 5.7 support, we would like to look at an alternate release process for that version (or let you maintain it on your own fork).
About the Project
SyntaxSparrow was built on heavy inspiration from the now archived SwiftSemantics project. SwiftSemantics was awesome, but being archived the only option is to fork and add features yourself, or hope someone has added your feature to their fork. SyntaxSparrow aims to pick up where this left off and add more support for conveniences, features, and harden parsing where needed.
The primary goal of producing semantic types to abstract the underlying Syntax expressions produced by SwiftSyntax remains the same, however there are a few other goals that SyntaxSparrow tries to achieve:
-
On-request evaluation: As some source can be quite verbose and complex, SyntaxSparrow aims to only process and iterate through nodes as you request them. The goal being to improve performance and lets the collectors focus on high-level traversal. Whether this is worth the internal trade off from a code complexity perspective will be reviewed over updates. The publicly visible semantic types are not effected by any internal updates fortunately.
-
Source Locations:
SyntaxSparrowenables asking for where a declaration is within the provided source. -
Heirachy Based: Rather than flatten nested declarations into a single array, Declarations in
SyntaxSparroware able to collect child declarations as they are supported in swift. For example, nesting structs within an enum or extensions etc -
Performance: In the future, we aim to improve performance through more efficient parsing algorithms and data structures. This will be coupled with an expanded test suite, to ensure accuracy across a wider range of Swift code patterns and idioms. We're also looking at ways to allow users to tailor the library's behavior to their specific needs, such as customizable traversal strategies and fine-grained control over the amount of information collected.
Features
-
Swift Macro Development: Parse the raw SwiftSyntax declarations a macro provides into their semantic code to focus on your generated code.
-
Swift Code Analysis: Parse Swift code and create a syntax tree for in-depth analysis.
-
Swift Code Generation: Use parsed semantic types to generate code in a far more readable manner.
-
Semantic Extraction: Extracts various semantic structures like classes, functions, enumerations, structures, protocols, etc. from the syntax tree into constituent types.
-
Source Code Updates: Ability to update the source code on a tree instance, allowing subsequent collections as code changes.
-
Different View Modes: Control the parsing and traversal strategy when processing the source code.
-
On-demand Evaluation: The details of a semantic type are only loaded on request.
-
Heirachy Based: Semantic types support child declarations (where relevant) to allow for a more heirachy-based traversal.
Use Cases:
SyntaxSparrow is designed to enable source exploration, and to compliment tooling to achieve some common tasks. For example:
-
Code Generation: Iterate through a readable semantic type to generate code to add to source via an IDE plugin, CLI, Swift Package Plugin etc
-
Static Code Analysis: Explore parsed source code with more accuracy to compliment code analysis tasks. i.e Resolving function names to look up index symbols and check if they are tested or unused.
Usage
General
Initialize SyntaxTree with the path of a Swift source file, directly with a Swift source code string, or by asking to parse a SwiftSyntax.DeclSyntaxProtocol conforming type. Then, use the various properties of SyntaxTree to access the collected semantic structures.
From Source File
let syntaxTree = try SyntaxTree(viewMode: .fixedUp, sourceAtPath: "/path/to/your/swift/file")
syntaxTree.collectChildren()
From Source
let syntaxTree = try SyntaxTree(viewMode: .fixedUp, sourceBuffer: "source code")
syntaxTree.collectChildren()
From SwiftSyntax.DeclSyntaxProtocol
let syntaxTree = try SyntaxTree(viewMode: .fixedUp, declarationSyntax: declaration)
syntaxTree.collectChildren()
Updating To a New Source:
If you want to update the source code and refresh the semantic structures:
syntaxTree.updateToSource(newSourceCode)
syntaxTree.collectChildren()
Using Constituent Declarations:
After initialization and collection, you can access the collected semantic structures and their properties, such as attributes, modifiers, name, etc:
let sourceCode = """
class MyViewController: UIViewController, UICollectionViewDelegate, ListItemDisplaying {
@available(*, unavailable, message: "my message")
enum Section {
case summary, people
}
var people: [People], places: [Place]
var person: (name: String, age: Int)?
weak var delegate: MyDelegate?
@IBOutlet private(set) var tableView: UITableView!
struct MyStruct {
enum MyEnum {
case sample(title: String)
case otherSample
}
}
func performOperation<T: Any>(input: T, _ completion: (Int) -> String) where T: NSFetchResult {
typealias SampleAlias = String
}
}
"""
let syntaxTree = SyntaxTree(viewMode: .fixedUp, sourceBuffer: sourceCode)
syntaxTree.collectChildren()
//
syntaxTree.protocols[0].name // "ListItemDisplaying"
syntaxTree.protocols[0].functions[0].identifier // setListItems
syntaxTree.protocols[0].functions[0].signature.input[0].secondName // items
syntaxTree.protocols[0].functions[0].signature.input[0].isLabelOmitted // true
syntaxTree.protocols[0].functions[0].signature.input[0].type // .simple("[ListItem]")
syntaxTree.classes[0].name // MyViewController
syntaxTree.classes[0].inheritance // [UIViewController, UICollectionViewDelegate, ListItemDisplaying]
syntaxTree.classes[0].enumerations[0].name // Section
syntaxTree.classes[0].enumerations[0].cases.map(\.name) // [summary, people]
syntaxTree.classes[0].enumerations[0].attributes[0].name // available
syntaxTree.classes[0].enumerations[0].attributes[0].arguments[0].name // nil
syntaxTree.classes[0].enumerations[0].attributes[0].arguments[0].value // *
syntaxTree.classes[0].enumerations[0].attributes[0].arguments[1].name // nil
syntaxTree.classes[0].enumerations[0].attributes[0].arguments[1].value // unavailable
syntaxTree.classes[0].enumerations[0].attributes[0].arguments[2].name // "message"
syntaxTree.classes[0].enumerations[0].attributes[0].arguments[2].value // "my message"
syntaxTree.classes[0].variables[0].name // people
syntaxTree.classes[0].variables[0].type // .simple("[People]")
syntaxTree.classes[0].variables[1].name // places
syntaxTree.classes[0].variables[1].type // .simple("[Place]")
syntaxTree.classes[0].variables[2].name // person
syntaxTree.classes[0].variables[2].type // .tuple(Tuple)
syntaxTree.classes[0].variables[2].isOptional // true
switch syntaxTree.classes[0].variables[1].type {
case .tuple(let tuple):
tuple.elements.map(\.name) // [name, age]
tuple.isOptional // true
}
syntaxTree.classes[0].variables[3].type // .simple("MyDelegate")
syntaxTree.classes[0].variables[3].isOptional // true
syntaxTree.classes[0].variables[3].modifiers.map(\.name) // [weak]
sy
