Skip to content

Abstraction built on top of Apple's SwiftSyntax library to traverse constituent declaration types for Swift code.

License

Notifications You must be signed in to change notification settings

CheekyGhost-Labs/SyntaxSparrow

Repository files navigation

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 Swift Unit Tests
develop Swift Unit Tests

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: SyntaxSparrow enables asking for where a declaration is within the provided source.

  • Heirachy Based: Rather than flatten nested declarations into a single array, Declarations in SyntaxSparrow are 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]

syntaxTree.structures[0].name // MyStruct
syntaxTree.structures[0].enumerations[0] // MyEnum
syntaxTree.structures[0].enumerations[0].cases[0].associatedValues.map(\.name) // [title]

syntaxTree.functions[0].identifier // performOperation
syntaxTree.functions[0].genericParameters.map(\.name) // [T]
syntaxTree.functions[0].genericParameters.map(\.type) // [Any]
syntaxTree.functions[0].genericRequirements.map(\.name) // [T]
syntaxTree.functions[0].genericRequirements[0].leftTypeIdentifier // T
syntaxTree.functions[0].genericRequirements[0].rightTypeIdentifier // NSFetchResult
syntaxTree.functions[0].genericRequirements[0].relation // .sameType
syntaxTree.functions[0].typealiases[0].name // SampleAlias
syntaxTree.functions[0].typealiases[0].initializedType.type // .simple("String")

Modifier Conveniences

Where applicable, types will conform to the ModifierAssessing protocol, which lets you assess what modifiers are present based on a SwiftSyntax.Keyword type. This is also available on any Collection where the element type is Modifier

// Assess if the instnace has `private(set)` and is `public`
conformingInstance.containsModifierWithKeyword(.private, withDetail: "set")
conformingInstance.containsModifierWithKeyword(.public)
// or on a collection of modifiers
variable.modifiers.containsKeyword(.private, withDetail: "set")

some common scenarios are available as direct getters:

conformingInstance.isPrivate
conformingInstance.isPublic
conformingInstance.isOpen
// etc
variable.isPrivateSetter
variable.isFilePrivateSetter
initializer.isConvenience
initializer.isRequired
// etc

Conforming types are:

  • Actor
  • Class
  • Enumeration
  • Extension
  • Function
  • ProtocolDecl
  • Structure
  • Subscript
  • Typealias
  • Variable
  • Initializer
  • Any collection where the type is Modifier

Source Locations and Bounds:

Declaration types can can also be sent to the SyntaxTree to extract source location and content:

let sourceCode = """
    enum Section {
        case summary
        case people
    }
    
    @available(*, unavailable, message: "my message")
    struct MyStruct {
        var name: String = "name"
    }
"""
let syntaxTree = SyntaxTree(viewMode: .fixedUp, sourceBuffer: sourceCode)
syntaxTree.collectChildren()
let sourceDetails = try syntaxTree.extractSource(forDeclaration: structure)

sourceDetails.location.start.line // 5
sourceDetails.location.start.column // 4
sourceDetails.location.end.line // 8
sourceDetails.location.end.line // 4
sourceDetails.source // "@available(*, unavailable, message: \"my message\")\nstruct MyStruct {\n    var name: String = \"name\"\n}"

// The `SyntaxSourceDetails` struct also has some conveniences for calculating ranges
sourceDetails.substringRange(in: source) // Range<String.Index>
sourceDetails.stringRange(in: source) // NSRange(location: 51, length: 99)

Entity Types:

EntityType is a vital part of SyntaxSparrow. It represents the type of an entity within a Swift source code. These entities can include parameters, variables, function return types, etc.

It provides a structured representation for type information extracted from the Swift code. EntityType has a comprehensive support for many Swift types, handling simple, optional, tuple, function, and result types.

The various EntityType options include:

  • simple: Represents a simple type like Int, String, Bool, or any other user-defined types.
  • tuple: Represents tuple types, such as (Int, String)
  • closure: Represents function or closure types, like (Int, String) -> Bool
  • Array: Represents a swift array via shorthand [Type] or keyword Array<Type>
  • Set: Represents a swift set via keyword Set<Type>
  • Dictionary: Represents a swift dictionary via shorthand [Type: Type] or keyword Dictionary<Type, Type>
  • result: Represents Swift's Result type, capturing the Success and Failure types.
  • void: Represents a void block type. i.e Void or ()
  • empty: Used to capture partial declarations where a type is not defined yet. i.e var myName:

EntityType provides an easily accessible way to extract type-related information from your Swift source code.

For Example,

let function = syntaxTree.functions.first
let returnType = function?.returnType  // This is an EntityType

You can then inspect the returnType to determine its specifics

switch returnType {
case .simple(let typeName):
    print("Simple type: \(typeName)")
case .tuple(let tuple):
    tuple.isOptional // true/false
    tuple.elements // Array of `Parameter` types
case .array(let array):
    array.isOptional // true/false
    array.elementType // Entity Type
    array.declType // .squareBrackets/.generic
case .set(let set):
    set.isOptional // true/false
    set.elementType // Entity Type
case .dictionary(let dict):
    dict.isOptional // true/false
    dict.keyType // Entity Type
    dict.valueType // Entity Type
    dict.declType // .squareBrackets/.generics
case .closure(let closure):
    closure.input // Entity Type
    closure.output // Entity Type
    closure.isEscaping // true/false
    closure.isAutoEscaping // true/false
    closure.isOptional // true/false
    closure.isVoidInput // true/false
    closure.isVoidOutput // true/false
    // see `Closure`
case .result(let result):
    print(result.successType) // EntityType
    print(result.failureType) // EntityType
    // see `Tuple`
case .void(let rawType: let isOptional):
   print(rawType) // "Void" or "()?" etc
case .empty:
   print("undefined or partial")
}

Installation

Currently, SyntaxSparrow supports Swift Package Manager (SPM).

To add SyntaxSparrow to your project, add the following line to your dependencies in your Package.swift file:

.package(url: "https://github.com/CheekyGhost-Labs/SyntaxSparrow", from: "5.0.0")

Then, add SyntaxSparrow as a dependency for your target:

.target(name: "YourTarget", dependencies: ["SyntaxSparrow"]),

Requirements

  • Swift 5.7+
  • macOS 10.15+
  • iOS 13+

License

SyntaxSparrow is released under the MIT License. See the LICENSE file for more information.

Contributing

Contributions to SyntaxSparrow are welcomed! If you have a bug to report, feel free to help out by opening a new issue or submitting a pull request.

SyntaxSparrow follows pretty closely to a standard git flow process. For the most part, pull requests should be made against the develop branch to coordinate any releases. This also provides a means to test from the develop branch in the wild to further test pending releases. Once a release is ready it will be merged into main, tagged, and have a release branch cut.

❗️❗️ Please ensure you create any pull requests based on the develop branch ❗️❗️

To get started:

  1. Fork the repository: Start by creating a fork of the project to your own GitHub account.

  2. Clone the forked repository: After forking, clone your forked repository to your local machine so you can make changes.

git clone https://github.com/CheekyGhost-Labs/SyntaxSparrow.git
  1. Create a new branch: Before making changes, create a new branch for your feature or bug fix. Use a descriptive name that reflects the purpose of your changes.
git checkout -b your-feature-branch
  1. Follow the Swift Language Guide: Ensure that your code adheres to the Swift Language Guide for styling and syntax conventions.

  2. Make your changes: Implement your feature or bug fix, following the project's code style and best practices. Don't forget to add tests and update documentation as needed.

  3. Commit your changes: Commit your changes with a descriptive and concise commit message. Use the imperative mood, and explain what your commit does, rather than what you did.

# Feature
git commit -m "Feature: Adding convenience method for resolving awesomeness"


# Bug
git commit -m "Bug: Fixing issue where awesome query was not including awesome"
  1. Pull the latest changes from the upstream: Before submitting your changes, make sure to pull the latest changes from the upstream repository and merge them into your branch. This helps to avoid any potential merge conflicts.
git pull origin develop
  1. Push your changes: Push your changes to your forked repository on GitHub.
git push origin your-feature-branch
  1. Submit a pull request: Finally, create a pull request from your forked repository to the original repository, targeting the develop branch. Fill in the pull request template with the necessary details, and wait for the project maintainers to review your contribution.

Unit Testing

Please ensure you add unit tests for any changes. The aim is not 100% coverage, but rather meaningful test coverage that ensures your changes are behaving as expected without negatively effecting existing behavior.

Please note that the project maintainers may ask you to make changes to your contribution or provide additional information. Be open to feedback and willing to make adjustments as needed. Once your pull request is approved and merged, your changes will become part of the project!

About

Abstraction built on top of Apple's SwiftSyntax library to traverse constituent declaration types for Swift code.

Topics

Resources

License

Stars

Watchers

Forks

Packages

No packages published

Languages