diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json new file mode 100644 index 0000000..9bf2e71 --- /dev/null +++ b/.devcontainer/devcontainer.json @@ -0,0 +1,40 @@ +{ + "name": "Swift", + "image": "swiftlang/swift:nightly-6.2-noble", + "features": { + "ghcr.io/devcontainers/features/common-utils:2": { + "installZsh": "false", + "username": "vscode", + "upgradePackages": "false" + }, + "ghcr.io/devcontainers/features/git:1": { + "version": "os-provided", + "ppa": "false" + } + }, + "postStartCommand": "git config --global --add safe.directory ${containerWorkspaceFolder}", + "runArgs": [ + "--cap-add=SYS_PTRACE", + "--security-opt", + "seccomp=unconfined" + ], + // Configure tool-specific properties. + "customizations": { + // Configure properties specific to VS Code. + "vscode": { + // Set *default* container specific settings.json values on container create. + "settings": { + "lldb.library": "/usr/lib/liblldb.so" + }, + // Add the IDs of extensions you want installed when the container is created. + "extensions": [ + "sswg.swift-lang" + ] + } + }, + // Use 'forwardPorts' to make a list of ports inside the container available locally. + // "forwardPorts": [], + + // Set `remoteUser` to `root` to connect as root instead. More info: https://aka.ms/vscode-remote/containers/non-root. + "remoteUser": "root" +} \ No newline at end of file diff --git a/.devcontainer/swift-6.1/devcontainer.json b/.devcontainer/swift-6.1/devcontainer.json new file mode 100644 index 0000000..c8fb002 --- /dev/null +++ b/.devcontainer/swift-6.1/devcontainer.json @@ -0,0 +1,40 @@ +{ + "name": "Swift", + "image": "swift:6.1", + "features": { + "ghcr.io/devcontainers/features/common-utils:2": { + "installZsh": "false", + "username": "vscode", + "upgradePackages": "false" + }, + "ghcr.io/devcontainers/features/git:1": { + "version": "os-provided", + "ppa": "false" + } + }, + "postStartCommand": "git config --global --add safe.directory ${containerWorkspaceFolder}", + "runArgs": [ + "--cap-add=SYS_PTRACE", + "--security-opt", + "seccomp=unconfined" + ], + // Configure tool-specific properties. + "customizations": { + // Configure properties specific to VS Code. + "vscode": { + // Set *default* container specific settings.json values on container create. + "settings": { + "lldb.library": "/usr/lib/liblldb.so" + }, + // Add the IDs of extensions you want installed when the container is created. + "extensions": [ + "sswg.swift-lang" + ] + } + }, + // Use 'forwardPorts' to make a list of ports inside the container available locally. + // "forwardPorts": [], + + // Set `remoteUser` to `root` to connect as root instead. More info: https://aka.ms/vscode-remote/containers/non-root. + "remoteUser": "root" +} \ No newline at end of file diff --git a/.devcontainer/swift-6.2/devcontainer.json b/.devcontainer/swift-6.2/devcontainer.json new file mode 100644 index 0000000..9bf2e71 --- /dev/null +++ b/.devcontainer/swift-6.2/devcontainer.json @@ -0,0 +1,40 @@ +{ + "name": "Swift", + "image": "swiftlang/swift:nightly-6.2-noble", + "features": { + "ghcr.io/devcontainers/features/common-utils:2": { + "installZsh": "false", + "username": "vscode", + "upgradePackages": "false" + }, + "ghcr.io/devcontainers/features/git:1": { + "version": "os-provided", + "ppa": "false" + } + }, + "postStartCommand": "git config --global --add safe.directory ${containerWorkspaceFolder}", + "runArgs": [ + "--cap-add=SYS_PTRACE", + "--security-opt", + "seccomp=unconfined" + ], + // Configure tool-specific properties. + "customizations": { + // Configure properties specific to VS Code. + "vscode": { + // Set *default* container specific settings.json values on container create. + "settings": { + "lldb.library": "/usr/lib/liblldb.so" + }, + // Add the IDs of extensions you want installed when the container is created. + "extensions": [ + "sswg.swift-lang" + ] + } + }, + // Use 'forwardPorts' to make a list of ports inside the container available locally. + // "forwardPorts": [], + + // Set `remoteUser` to `root` to connect as root instead. More info: https://aka.ms/vscode-remote/containers/non-root. + "remoteUser": "root" +} \ No newline at end of file diff --git a/.github/workflows/SyntaxKit.yml b/.github/workflows/SyntaxKit.yml new file mode 100644 index 0000000..dd72dfa --- /dev/null +++ b/.github/workflows/SyntaxKit.yml @@ -0,0 +1,131 @@ +name: SyntaxKit +on: + push: + branches-ignore: + - '*WIP' +env: + PACKAGE_NAME: SyntaxKit +jobs: + build-ubuntu: + name: Build on Ubuntu + runs-on: ubuntu-latest + container: swiftlang/swift:nightly-${{ matrix.swift-version }}-${{ matrix.os }} + if: ${{ !contains(github.event.head_commit.message, 'ci skip') }} + strategy: + matrix: + os: ["noble", "jammy"] + swift-version: ["6.1", "6.2"] + steps: + - uses: actions/checkout@v4 + - uses: brightdigit/swift-build@v1.1.1 + - uses: sersoft-gmbh/swift-coverage-action@v4 + id: coverage-files + with: + fail-on-empty-output: true + - name: Upload coverage to Codecov + uses: codecov/codecov-action@v4 + with: + fail_ci_if_error: true + flags: swift-${{ matrix.swift-version }},ubuntu + verbose: true + token: ${{ secrets.CODECOV_TOKEN }} + files: ${{ join(fromJSON(steps.coverage-files.outputs.files), ',') }} + build-macos: + name: Build on macOS + env: + PACKAGE_NAME: SyntaxKit + runs-on: ${{ matrix.runs-on }} + if: "!contains(github.event.head_commit.message, 'ci skip')" + strategy: + fail-fast: false + matrix: + include: + # SPM Build Matrix + - runs-on: macos-15 + xcode: "/Applications/Xcode_16.4.app" + + # macOS Build Matrix + - type: macos + runs-on: macos-15 + xcode: "/Applications/Xcode_16.4.app" + + # iOS Build Matrix + - type: ios + runs-on: macos-15 + xcode: "/Applications/Xcode_16.4.app" + deviceName: "iPhone 16 Pro" + osVersion: "18.5" + + # watchOS Build Matrix + - type: watchos + runs-on: macos-15 + xcode: "/Applications/Xcode_16.4.app" + deviceName: "Apple Watch Ultra 2 (49mm)" + osVersion: "11.5" + + # tvOS Build Matrix + - type: tvos + runs-on: macos-15 + xcode: "/Applications/Xcode_16.4.app" + deviceName: "Apple TV" + osVersion: "18.5" + + # visionOS Build Matrix + - type: visionos + runs-on: macos-15 + xcode: "/Applications/Xcode_16.4.app" + deviceName: "Apple Vision Pro" + osVersion: "2.5" + + steps: + - uses: actions/checkout@v4 + + - name: Build and Test + uses: brightdigit/swift-build@v1.1.1 + with: + scheme: ${{ env.PACKAGE_NAME }}-Package + type: ${{ matrix.type }} + xcode: ${{ matrix.xcode }} + deviceName: ${{ matrix.deviceName }} + osVersion: ${{ matrix.osVersion }} + + # Common Coverage Steps + - name: Process Coverage + uses: sersoft-gmbh/swift-coverage-action@v4 + + - name: Upload Coverage + uses: codecov/codecov-action@v4 + with: + token: ${{ secrets.CODECOV_TOKEN }} + flags: ${{ matrix.type && format('{0}{1}', matrix.type, matrix.osVersion) || 'spm' }} + + lint: + name: Linting + if: "!contains(github.event.head_commit.message, 'ci skip')" + runs-on: ubuntu-latest + needs: [build-ubuntu, build-macos] + env: + MINT_PATH: .mint/lib + MINT_LINK_PATH: .mint/bin + steps: + - uses: actions/checkout@v4 + - name: Cache mint + id: cache-mint + uses: actions/cache@v4 + env: + cache-name: cache + with: + path: | + .mint + Mint + key: ${{ runner.os }}-mint-${{ hashFiles('**/Mintfile') }} + restore-keys: | + ${{ runner.os }}-mint- + - name: Install mint + if: steps.cache-mint.outputs.cache-hit != 'true' + run: | + git clone https://github.com/yonaskolb/Mint.git + cd Mint + swift run mint install yonaskolb/mint + - name: Lint + run: ./Scripts/lint.sh diff --git a/.gitignore b/.gitignore index 0023a53..95334f2 100644 --- a/.gitignore +++ b/.gitignore @@ -1,8 +1,140 @@ +# Created by https://www.toptal.com/developers/gitignore/api/xcode,macos,swift +# Edit at https://www.toptal.com/developers/gitignore?templates=xcode,macos,swift + +### macOS ### +# General .DS_Store -/.build -/Packages +.AppleDouble +.LSOverride + +# Icon must end with two \r +Icon + + +# Thumbnails +._* + +# Files that might appear in the root of a volume +.DocumentRevisions-V100 +.fseventsd +.Spotlight-V100 +.TemporaryItems +.Trashes +.VolumeIcon.icns +.com.apple.timemachine.donotpresent + +# Directories potentially created on remote AFP share +.AppleDB +.AppleDesktop +Network Trash Folder +Temporary Items +.apdisk + +### Swift ### +# Xcode +# +# gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore + +## User settings xcuserdata/ + +## compatibility with Xcode 8 and earlier (ignoring not required starting Xcode 9) +*.xcscmblueprint +*.xccheckout + +## compatibility with Xcode 3 and earlier (ignoring not required starting Xcode 4) +build/ DerivedData/ -.swiftpm/configuration/registries.json -.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata -.netrc +*.moved-aside +*.pbxuser +!default.pbxuser +*.mode1v3 +!default.mode1v3 +*.mode2v3 +!default.mode2v3 +*.perspectivev3 +!default.perspectivev3 + +## Obj-C/Swift specific +*.hmap + +## App packaging +*.ipa +*.dSYM.zip +*.dSYM + +## Playgrounds +timeline.xctimeline +playground.xcworkspace + +# Swift Package Manager +# Add this line if you want to avoid checking in source code from Swift Package Manager dependencies. +# Packages/ +# Package.pins +# Package.resolved +*.xcodeproj +# Xcode automatically generates this directory with a .xcworkspacedata file and xcuserdata +# hence it is not needed unless you have added a package configuration file to your project +.swiftpm + +.build/ + +# CocoaPods +# We recommend against adding the Pods directory to your .gitignore. However +# you should judge for yourself, the pros and cons are mentioned at: +# https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control +# Pods/ +# Add this line if you want to avoid checking in source code from the Xcode workspace +# *.xcworkspace + +# Carthage +# Add this line if you want to avoid checking in source code from Carthage dependencies. +# Carthage/Checkouts + +Carthage/Build/ + +# fastlane +# It is recommended to not store the screenshots in the git repo. +# Instead, use fastlane to re-generate the screenshots whenever they are needed. +# For more information about the recommended setup visit: +# https://docs.fastlane.tools/best-practices/source-control/#source-control + +fastlane/report.xml +fastlane/Preview.html +fastlane/screenshots/**/*.png +fastlane/test_output +fastlane/reviews.csv + +# Code Injection +# After new code Injection tools there's a generated folder /iOSInjectionProject +# https://github.com/johnno1962/injectionforxcode + +iOSInjectionProject/ + +### Xcode ### +# Xcode +# gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore + + + + +## Gcc Patch +/*.gcno + +### Xcode Patch ### +*.xcodeproj/* +#!*.xcodeproj/project.pbxproj +#!*.xcodeproj/xcshareddata/ +#!*.xcworkspace/contents.xcworkspacedata +**/xcshareddata/WorkspaceSettings.xcsettings + +# End of https://www.toptal.com/developers/gitignore/api/xcode,macos,swift + +Support/*/Info.plist +Support/*/macOS.entitlements + +vendor/ruby +public +.mint +*.lcov +.docc-build \ No newline at end of file diff --git a/.periphery.yml b/.periphery.yml new file mode 100644 index 0000000..85b884a --- /dev/null +++ b/.periphery.yml @@ -0,0 +1 @@ +retain_public: true diff --git a/.spi.yml b/.spi.yml new file mode 100644 index 0000000..af606f6 --- /dev/null +++ b/.spi.yml @@ -0,0 +1,5 @@ +version: 1 +builder: + configs: + - documentation_targets: [SyntaxKit] + swift_version: 6.1 \ No newline at end of file diff --git a/.swift-format b/.swift-format new file mode 100644 index 0000000..a657e6c --- /dev/null +++ b/.swift-format @@ -0,0 +1,70 @@ +{ + "fileScopedDeclarationPrivacy" : { + "accessLevel" : "private" + }, + "indentConditionalCompilationBlocks" : true, + "indentSwitchCaseLabels" : false, + "indentation" : { + "spaces" : 2 + }, + "lineBreakAroundMultilineExpressionChainComponents" : false, + "lineBreakBeforeControlFlowKeywords" : false, + "lineBreakBeforeEachArgument" : false, + "lineBreakBeforeEachGenericRequirement" : false, + "lineLength" : 100, + "maximumBlankLines" : 1, + "multiElementCollectionTrailingCommas" : true, + "noAssignmentInExpressions" : { + "allowedFunctions" : [ + "XCTAssertNoThrow" + ] + }, + "prioritizeKeepingFunctionOutputTogether" : false, + "respectsExistingLineBreaks" : true, + "rules" : { + "AllPublicDeclarationsHaveDocumentation" : true, + "AlwaysUseLiteralForEmptyCollectionInit" : false, + "AlwaysUseLowerCamelCase" : true, + "AmbiguousTrailingClosureOverload" : true, + "BeginDocumentationCommentWithOneLineSummary" : false, + "DoNotUseSemicolons" : true, + "DontRepeatTypeInStaticProperties" : true, + "FileScopedDeclarationPrivacy" : true, + "FullyIndirectEnum" : true, + "GroupNumericLiterals" : true, + "IdentifiersMustBeASCII" : true, + "NeverForceUnwrap" : false, + "NeverUseForceTry" : false, + "NeverUseImplicitlyUnwrappedOptionals" : false, + "NoAccessLevelOnExtensionDeclaration" : true, + "NoAssignmentInExpressions" : true, + "NoBlockComments" : true, + "NoCasesWithOnlyFallthrough" : true, + "NoEmptyTrailingClosureParentheses" : true, + "NoLabelsInCasePatterns" : true, + "NoLeadingUnderscores" : true, + "NoParensAroundConditions" : true, + "NoPlaygroundLiterals" : true, + "NoVoidReturnOnFunctionSignature" : true, + "OmitExplicitReturns" : false, + "OneCasePerLine" : true, + "OneVariableDeclarationPerLine" : true, + "OnlyOneTrailingClosureArgument" : true, + "OrderedImports" : true, + "ReplaceForEachWithForLoop" : true, + "ReturnVoidInsteadOfEmptyTuple" : true, + "TypeNamesShouldBeCapitalized" : true, + "UseEarlyExits" : false, + "UseExplicitNilCheckInConditions" : true, + "UseLetInEveryBoundCaseVariable" : false, + "UseShorthandTypeNames" : true, + "UseSingleLinePropertyGetter" : true, + "UseSynthesizedInitializer" : true, + "UseTripleSlashForDocumentationComments" : true, + "UseWhereClausesInForLoops" : true, + "ValidateDocumentationComments" : false + }, + "spacesAroundRangeFormationOperators" : false, + "tabWidth" : 2, + "version" : 1 +} diff --git a/.swift-version b/.swift-version new file mode 100644 index 0000000..a435f5a --- /dev/null +++ b/.swift-version @@ -0,0 +1 @@ +6.1 diff --git a/.swiftlint.yml b/.swiftlint.yml new file mode 100644 index 0000000..e4222bf --- /dev/null +++ b/.swiftlint.yml @@ -0,0 +1,131 @@ +opt_in_rules: + - array_init + - closure_body_length + - closure_end_indentation + - closure_spacing + - collection_alignment + - conditional_returns_on_newline + - contains_over_filter_count + - contains_over_filter_is_empty + - contains_over_first_not_nil + - contains_over_range_nil_comparison + - convenience_type + - discouraged_object_literal + - discouraged_optional_boolean + - empty_collection_literal + - empty_count + - empty_string + - empty_xctest_method + - enum_case_associated_values_count + - expiring_todo + - explicit_acl + - explicit_init + - explicit_top_level_acl + # - fallthrough + - fatal_error_message + - file_name + - file_name_no_space + - file_types_order + - first_where + - flatmap_over_map_reduce + - force_unwrapping +# - function_default_parameter_at_end + - ibinspectable_in_extension + - identical_operands + - implicit_return + - implicitly_unwrapped_optional + - indentation_width + - joined_default_parameter + - last_where + - legacy_multiple + - legacy_random + - literal_expression_end_indentation + - lower_acl_than_parent +# - missing_docs + - modifier_order + - multiline_arguments + - multiline_arguments_brackets + - multiline_function_chains + - multiline_literal_brackets + - multiline_parameters + - nimble_operator + - nslocalizedstring_key + - nslocalizedstring_require_bundle + - number_separator + - object_literal + - operator_usage_whitespace + - optional_enum_case_matching + - overridden_super_call + - override_in_extension + - pattern_matching_keywords + - prefer_self_type_over_type_of_self + - prefer_zero_over_explicit_init + - private_action + - private_outlet + - prohibited_interface_builder + - prohibited_super_call + - quick_discouraged_call + - quick_discouraged_focused_test + - quick_discouraged_pending_test + - reduce_into + - redundant_nil_coalescing + - redundant_type_annotation + - required_enum_case + - single_test_class + - sorted_first_last + - sorted_imports + - static_operator + - strong_iboutlet + - toggle_bool +# - trailing_closure + - type_contents_order + - unavailable_function + - unneeded_parentheses_in_closure_argument + - unowned_variable_capture + - untyped_error_in_catch + - vertical_parameter_alignment_on_call + - vertical_whitespace_closing_braces + - vertical_whitespace_opening_braces + - xct_specific_matcher + - yoda_condition +analyzer_rules: + - unused_import + - unused_declaration +cyclomatic_complexity: + - 6 + - 12 +file_length: + warning: 225 + error: 300 +function_body_length: + - 50 + - 76 +function_parameter_count: 8 +line_length: + - 108 + - 200 +closure_body_length: + - 50 + - 60 +identifier_name: + excluded: + - id + - no +excluded: + - DerivedData + - .build + - Mint + - Examples +indentation_width: + indentation_width: 2 +file_name: + severity: error +fatal_error_message: + severity: error +disabled_rules: + - nesting + - implicit_getter + - switch_case_alignment + - closure_parameter_position + - trailing_comma + - opening_brace diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 0000000..e74b79e --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,22 @@ +{ + "configurations": [ + { + "type": "swift", + "request": "launch", + "args": [], + "cwd": "${workspaceFolder:SyntaxKit}", + "name": "Debug skit", + "program": "${workspaceFolder:SyntaxKit}/.build/debug/skit", + "preLaunchTask": "swift: Build Debug skit" + }, + { + "type": "swift", + "request": "launch", + "args": [], + "cwd": "${workspaceFolder:SyntaxKit}", + "name": "Release skit", + "program": "${workspaceFolder:SyntaxKit}/.build/release/skit", + "preLaunchTask": "swift: Build Release skit" + } + ] +} \ No newline at end of file diff --git a/Examples/blackjack/code.swift b/Examples/Completed/blackjack/code.swift similarity index 100% rename from Examples/blackjack/code.swift rename to Examples/Completed/blackjack/code.swift diff --git a/Examples/blackjack/dsl.swift b/Examples/Completed/blackjack/dsl.swift similarity index 95% rename from Examples/blackjack/dsl.swift rename to Examples/Completed/blackjack/dsl.swift index ba8eaa7..d0af68d 100644 --- a/Examples/blackjack/dsl.swift +++ b/Examples/Completed/blackjack/dsl.swift @@ -1,4 +1,4 @@ -import SwiftBuilder +import SyntaxKit // Example of generating a BlackjackCard struct with a nested Suit enum let structExample = Struct("BlackjackCard") { @@ -33,24 +33,24 @@ let structExample = Struct("BlackjackCard") { SwitchCase(".ace") { Return{ Init("Values") { - Parameter(name: "first", value: "1") - Parameter(name: "second", value: "11") + Parameter(name: "first", value: "1") + Parameter(name: "second", value: "11") } } } SwitchCase(".jack", ".queen", ".king") { Return{ Init("Values") { - Parameter(name: "first", value: "10") - Parameter(name: "second", value: "nil") + Parameter(name: "first", value: "10") + Parameter(name: "second", value: "nil") } } } Default { Return{ Init("Values") { - Parameter(name: "first", value: "self.rawValue") - Parameter(name: "second", value: "nil") + Parameter(name: "first", value: "self.rawValue") + Parameter(name: "second", value: "nil") } } } @@ -75,4 +75,4 @@ let structExample = Struct("BlackjackCard") { } // Generate and print the code -print(structExample.generateCode()) \ No newline at end of file +print(structExample.generateCode()) diff --git a/Examples/card_game/code.swift b/Examples/Completed/card_game/code.swift similarity index 99% rename from Examples/card_game/code.swift rename to Examples/Completed/card_game/code.swift index 4199994..1704595 100644 --- a/Examples/card_game/code.swift +++ b/Examples/Completed/card_game/code.swift @@ -28,7 +28,7 @@ enum Rank: Int, CaseIterable { case queen case king case ace - + /// Returns a string representation of the rank var description: String { switch self { @@ -47,4 +47,4 @@ enum Suit: String, CaseIterable { case diamonds = "♦" case clubs = "♣" case spades = "♠" -} \ No newline at end of file +} diff --git a/Examples/card_game/dsl.swift b/Examples/Completed/card_game/dsl.swift similarity index 96% rename from Examples/card_game/dsl.swift rename to Examples/Completed/card_game/dsl.swift index a4cb6ad..f6f9bdd 100644 --- a/Examples/card_game/dsl.swift +++ b/Examples/Completed/card_game/dsl.swift @@ -1,4 +1,4 @@ -import SwiftBuilder +import SyntaxKit // Example of generating a BlackjackCard struct with a nested Suit enum let structExample = Group { @@ -56,7 +56,7 @@ let structExample = Group { Literal("\"K\"") } } - SwitchCase(".ace") { + SwitchCase(".ace") { Return{ Literal("\"A\"") } @@ -90,9 +90,7 @@ let structExample = Group { .comment{ Line(.doc, "Represents the possible suits of a playing card") } - } - // Generate and print the code -print(structExample.generateCode()) \ No newline at end of file +print(structExample.generateCode()) diff --git a/Examples/card_game/syntax.json b/Examples/Completed/card_game/syntax.json similarity index 100% rename from Examples/card_game/syntax.json rename to Examples/Completed/card_game/syntax.json diff --git a/Examples/comments/code.swift b/Examples/Remaining/comments/code.swift similarity index 96% rename from Examples/comments/code.swift rename to Examples/Remaining/comments/code.swift index dff37cb..4c4b4f7 100644 --- a/Examples/comments/code.swift +++ b/Examples/Remaining/comments/code.swift @@ -29,27 +29,27 @@ class Calculator { /// - b: The second number /// - Returns: The sum of the two numbers func add(_ a: Int, _ b: Int) -> Int { - return a + b + a + b } - + /// Subtracts the second number from the first /// - Parameters: /// - a: The number to subtract from /// - b: The number to subtract /// - Returns: The difference between the two numbers func subtract(_ a: Int, _ b: Int) -> Int { - return a - b + a - b } - + /// Multiplies two numbers /// - Parameters: /// - a: The first number /// - b: The second number /// - Returns: The product of the two numbers func multiply(_ a: Int, _ b: Int) -> Int { - return a * b + a * b } - + /// Divides the first number by the second /// - Parameters: /// - a: The dividend @@ -103,18 +103,18 @@ let calculator = Calculator() do { let sum = calculator.add(10, 5) print("Sum: \(sum)") // Prints: Sum: 15 - + let difference = calculator.subtract(10, 5) print("Difference: \(difference)") // Prints: Difference: 5 - + let product = calculator.multiply(10, 5) print("Product: \(product)") // Prints: Product: 50 - + let quotient = try calculator.divide(10, 5) print("Quotient: \(quotient)") // Prints: Quotient: 2.0 - + // This will throw an error let error = try calculator.divide(10, 0) } catch CalculatorError.divisionByZero { print("Error: Division by zero is not allowed") -} \ No newline at end of file +} diff --git a/Examples/concurrency/code.swift b/Examples/Remaining/concurrency/code.swift similarity index 94% rename from Examples/concurrency/code.swift rename to Examples/Remaining/concurrency/code.swift index 3c1c73d..21be8c3 100644 --- a/Examples/concurrency/code.swift +++ b/Examples/Remaining/concurrency/code.swift @@ -16,19 +16,19 @@ func fetchUserPosts(id: Int) async throws -> [String] { // MARK: - Async Sequence struct Countdown: AsyncSequence { let start: Int - + struct AsyncIterator: AsyncIteratorProtocol { var count: Int - + mutating func next() async -> Int? { - guard count > 0 else { return nil } + guard !isEmpty else { return nil } try? await Task.sleep(nanoseconds: 500_000_000) // 0.5 seconds let current = count count -= 1 return current } } - + func makeAsyncIterator() -> AsyncIterator { AsyncIterator(count: start) } @@ -42,7 +42,7 @@ func fetchMultipleUsers(ids: [Int]) async throws -> [String] { try await fetchUserData(id: id) } } - + var results: [String] = [] for try await result in group { results.append(result) @@ -60,7 +60,7 @@ struct ConcurrencyExample { print("Fetching user data...") let userData = try await fetchUserData(id: 1) print(userData) - + // Demonstrate concurrent tasks print("\nFetching user data and posts concurrently...") async let data = fetchUserData(id: 1) @@ -68,21 +68,20 @@ struct ConcurrencyExample { let (fetchedData, fetchedPosts) = try await (data, posts) print("Data: \(fetchedData)") print("Posts: \(fetchedPosts)") - + // Demonstrate async sequence print("\nStarting countdown:") for await number in Countdown(start: 3) { print(number) } print("Liftoff!") - + // Demonstrate task groups print("\nFetching multiple users:") let users = try await fetchMultipleUsers(ids: [1, 2, 3]) print(users) - } catch { print("Error: \(error)") } } -} \ No newline at end of file +} diff --git a/Examples/generics/code.swift b/Examples/Remaining/generics/code.swift similarity index 66% rename from Examples/generics/code.swift rename to Examples/Remaining/generics/code.swift index e2b0416..7dbbad6 100644 --- a/Examples/generics/code.swift +++ b/Examples/Remaining/generics/code.swift @@ -1,24 +1,23 @@ struct Stack { private var items: [Element] = [] - + mutating func push(_ item: Element) { items.append(item) } - + mutating func pop() -> Element? { - return items.popLast() + items.popLast() } - + func peek() -> Element? { - return items.last + items.last } - + var isEmpty: Bool { - return items.isEmpty + items.isEmpty } - + var count: Int { - return items.count + items.count } } - diff --git a/Examples/generics/dsl.swift b/Examples/Remaining/generics/dsl.swift similarity index 97% rename from Examples/generics/dsl.swift rename to Examples/Remaining/generics/dsl.swift index 16f5e16..d6585cf 100644 --- a/Examples/generics/dsl.swift +++ b/Examples/Remaining/generics/dsl.swift @@ -1,4 +1,4 @@ -import SwiftBuilder +import SyntaxKit // Example of generating a BlackjackCard struct with a nested Suit enum let structExample = Struct("Stack", generic: "Element") { diff --git a/Examples/generics/syntax.json b/Examples/Remaining/generics/syntax.json similarity index 100% rename from Examples/generics/syntax.json rename to Examples/Remaining/generics/syntax.json diff --git a/Examples/protocols/code.swift b/Examples/Remaining/protocols/code.swift similarity index 96% rename from Examples/protocols/code.swift rename to Examples/Remaining/protocols/code.swift index a7eadfc..e0a642d 100644 --- a/Examples/protocols/code.swift +++ b/Examples/Remaining/protocols/code.swift @@ -13,7 +13,7 @@ extension Vehicle { func start() { print("Starting \(brand) vehicle...") } - + func stop() { print("Stopping \(brand) vehicle...") } @@ -29,7 +29,7 @@ protocol Electric { struct Car: Vehicle { let numberOfWheels: Int = 4 let brand: String - + func start() { print("Starting \(brand) car engine...") } @@ -39,7 +39,7 @@ struct ElectricCar: Vehicle, Electric { let numberOfWheels: Int = 4 let brand: String var batteryLevel: Double - + func charge() { print("Charging \(brand) electric car...") batteryLevel = 100.0 @@ -70,4 +70,4 @@ print("Testing regular car:") demonstrateVehicle(toyota) print("\nTesting electric car:") -demonstrateElectricVehicle(tesla) \ No newline at end of file +demonstrateElectricVehicle(tesla) diff --git a/Examples/swiftui/code.swift b/Examples/Remaining/swiftui/code.swift similarity index 97% rename from Examples/swiftui/code.swift rename to Examples/Remaining/swiftui/code.swift index 2039911..3a1c97d 100644 --- a/Examples/swiftui/code.swift +++ b/Examples/Remaining/swiftui/code.swift @@ -11,19 +11,19 @@ struct TodoItem: Identifiable { class TodoListViewModel: ObservableObject { @Published var items: [TodoItem] = [] @Published var newItemTitle: String = "" - + func addItem() { guard !newItemTitle.isEmpty else { return } items.append(TodoItem(title: newItemTitle, isCompleted: false)) newItemTitle = "" } - + func toggleItem(_ item: TodoItem) { if let index = items.firstIndex(where: { $0.id == item.id }) { items[index].isCompleted.toggle() } } - + func deleteItem(_ item: TodoItem) { items.removeAll { $0.id == item.id } } @@ -32,7 +32,7 @@ class TodoListViewModel: ObservableObject { // MARK: - Views struct TodoListView: View { @StateObject private var viewModel = TodoListViewModel() - + var body: some View { NavigationView { VStack { @@ -40,14 +40,14 @@ struct TodoListView: View { HStack { TextField("New todo item", text: $viewModel.newItemTitle) .textFieldStyle(RoundedBorderTextFieldStyle()) - + Button(action: viewModel.addItem) { Image(systemName: "plus.circle.fill") .foregroundColor(.blue) } } .padding() - + // List of items List { ForEach(viewModel.items) { item in @@ -70,14 +70,14 @@ struct TodoListView: View { struct TodoItemRow: View { let item: TodoItem let onToggle: () -> Void - + var body: some View { HStack { Button(action: onToggle) { Image(systemName: item.isCompleted ? "checkmark.circle.fill" : "circle") .foregroundColor(item.isCompleted ? .green : .gray) } - + Text(item.title) .strikethrough(item.isCompleted) .foregroundColor(item.isCompleted ? .gray : .primary) @@ -100,4 +100,4 @@ struct TodoApp: App { TodoListView() } } -} \ No newline at end of file +} diff --git a/Line.swift b/Line.swift new file mode 100644 index 0000000..29be4c6 --- /dev/null +++ b/Line.swift @@ -0,0 +1,57 @@ +// +// Line.swift +// Lint +// +// Created by Leo Dion on 6/16/25. +// + +/// Represents a single comment line that can be attached to a syntax node when using `.comment { ... }` in the DSL. +public struct Line { + public enum Kind { + /// Regular line comment that starts with `//`. + case line + /// Documentation line comment that starts with `///`. + case doc + } + + public let kind: Kind + public let text: String? + + /// Convenience initializer for a regular line comment without specifying the kind explicitly. + public init(_ text: String) { + self.kind = .line + self.text = text + } + + /// Convenience initialiser. Passing only `kind` will create an empty comment line of that kind. + /// + /// Examples: + /// ```swift + /// Line("MARK: - Models") // defaults to `.line` kind + /// Line(.doc, "Represents a model") // documentation comment + /// Line(.doc) // empty `///` line + /// ``` + public init(_ kind: Kind = .line, _ text: String? = nil) { + self.kind = kind + self.text = text + } +} + +// MARK: - Internal helpers + +extension Line { + /// Convert the `Line` to a SwiftSyntax `TriviaPiece`. + fileprivate var triviaPiece: TriviaPiece { + switch kind { + case .line: + return .lineComment("// " + (text ?? "")) + case .doc: + // Empty doc line should still contain the comment marker so we keep a single `/` if no text. + if let text = text, !text.isEmpty { + return .docLineComment("/// " + text) + } else { + return .docLineComment("///") + } + } + } +} diff --git a/Mintfile b/Mintfile new file mode 100644 index 0000000..4386537 --- /dev/null +++ b/Mintfile @@ -0,0 +1,3 @@ +swiftlang/swift-format@600.0.0 +realm/SwiftLint@0.58.2 +peripheryapp/periphery@3.0.1 diff --git a/Package.swift b/Package.swift index 966d30d..42c9345 100644 --- a/Package.swift +++ b/Package.swift @@ -1,45 +1,46 @@ -// swift-tools-version: 6.2 +// swift-tools-version: 6.1 // The swift-tools-version declares the minimum version of Swift required to build this package. import PackageDescription let package = Package( - name: "SwiftBuilder", - platforms: [ - .macOS(.v13) - ], - products: [ - // Products define the executables and libraries a package produces, making them visible to other packages. - .library( - name: "SwiftBuilder", - targets: ["SwiftBuilder"] - ), - .executable( - name: "SwiftBuilderCLI", - targets: ["SwiftBuilderCLI"] - ), - ], - dependencies: [ - .package(url: "https://github.com/apple/swift-syntax.git", from: "601.0.1") - ], - targets: [ - // Targets are the basic building blocks of a package, defining a module or a test suite. - // Targets can depend on other targets in this package and products from dependencies. - .target( - name: "SwiftBuilder", - dependencies: [ - .product(name: "SwiftSyntax", package: "swift-syntax"), - .product(name: "SwiftOperators", package: "swift-syntax"), - .product(name: "SwiftParser", package: "swift-syntax") - ] - ), - .executableTarget( - name: "SwiftBuilderCLI", - dependencies: ["SwiftBuilder"] - ), - .testTarget( - name: "SwiftBuilderTests", - dependencies: ["SwiftBuilder"] - ), - ] + name: "SyntaxKit", + platforms: [ + .macOS(.v13), + .iOS(.v13), + .watchOS(.v6), + .tvOS(.v13), + .visionOS(.v1) + ], + products: [ + .library( + name: "SyntaxKit", + targets: ["SyntaxKit"] + ), + .executable( + name: "skit", + targets: ["skit"] + ), + ], + dependencies: [ + .package(url: "https://github.com/apple/swift-syntax.git", from: "601.0.1") + ], + targets: [ + .target( + name: "SyntaxKit", + dependencies: [ + .product(name: "SwiftSyntax", package: "swift-syntax"), + .product(name: "SwiftOperators", package: "swift-syntax"), + .product(name: "SwiftParser", package: "swift-syntax") + ] + ), + .executableTarget( + name: "skit", + dependencies: ["SyntaxKit"] + ), + .testTarget( + name: "SyntaxKitTests", + dependencies: ["SyntaxKit"] + ), + ] ) diff --git a/README.md b/README.md index e1a66a1..d9372c3 100644 --- a/README.md +++ b/README.md @@ -1,23 +1,39 @@ -# SwiftBuilder +# SyntaxKit -SwiftBuilder is a Swift package that allows developers to build Swift code using result builders. It provides a declarative way to generate Swift code structures using SwiftSyntax. +SyntaxKit is a Swift package that allows developers to build Swift code using result builders. + +[![](https://img.shields.io/badge/docc-read_documentation-blue)](https://swiftpackageindex.com/brightdigit/SyntaxKit/documentation) +[![SwiftPM](https://img.shields.io/badge/SPM-Linux%20%7C%20iOS%20%7C%20macOS%20%7C%20watchOS%20%7C%20tvOS-success?logo=swift)](https://swift.org) +![GitHub](https://img.shields.io/github/license/brightdigit/SyntaxKit) +![GitHub issues](https://img.shields.io/github/issues/brightdigit/SyntaxKit) +![GitHub Workflow Status](https://img.shields.io/github/actions/workflow/status/brightdigit/SyntaxKit/SyntaxKit.yml?label=actions&logo=github&?branch=main) + +[![](https://img.shields.io/endpoint?url=https%3A%2F%2Fswiftpackageindex.com%2Fapi%2Fpackages%2Fbrightdigit%2FSyntaxKit%2Fbadge%3Ftype%3Dswift-versions)](https://swiftpackageindex.com/brightdigit/SyntaxKit) +[![](https://img.shields.io/endpoint?url=https%3A%2F%2Fswiftpackageindex.com%2Fapi%2Fpackages%2Fbrightdigit%2FSyntaxKit%2Fbadge%3Ftype%3Dplatforms)](https://swiftpackageindex.com/brightdigit/SyntaxKit) + +[![Codecov](https://img.shields.io/codecov/c/github/brightdigit/SyntaxKit)](https://codecov.io/gh/brightdigit/SyntaxKit) +[![CodeFactor Grade](https://img.shields.io/codefactor/grade/github/brightdigit/SyntaxKit)](https://www.codefactor.io/repository/github/brightdigit/SyntaxKit) +[![codebeat badge](https://codebeat.co/badges/ad53f31b-de7a-4579-89db-d94eb57dfcaa)](https://codebeat.co/projects/github-com-brightdigit-SyntaxKit-main) +[![Maintainability](https://qlty.sh/badges/55637213-d307-477e-a710-f9dba332d955/maintainability.svg)](https://qlty.sh/gh/brightdigit/projects/SyntaxKit) + +SyntaxKit provides a declarative way to generate Swift code structures using SwiftSyntax. ## Installation -Add SwiftBuilder to your project using Swift Package Manager: +Add SyntaxKit to your project using Swift Package Manager: ```swift dependencies: [ - .package(url: "https://github.com/yourusername/SwiftBuilder.git", from: "1.0.0") + .package(url: "https://github.com/yourusername/SyntaxKit.git", from: "0.0.1") ] ``` ## Usage -SwiftBuilder provides a set of result builders that allow you to create Swift code structures in a declarative way. Here's an example: +SyntaxKit provides a set of result builders that allow you to create Swift code structures in a declarative way. Here's an example: ```swift -import SwiftBuilder +import SyntaxKit let code = Struct("BlackjackCard") { Enum("Suit") { @@ -27,7 +43,9 @@ let code = Struct("BlackjackCard") { Case("clubs").equals("♣") } .inherits("Character") - .comment("nested Suit enumeration") + .comment{ + Line("nested Suit enumeration") + } } let generatedCode = code.generateCode() @@ -56,9 +74,9 @@ struct BlackjackCard { ## Requirements -- Swift 5.9+ +- Swift 6.1+ - macOS 13.0+ ## License -This project is licensed under the MIT License - see the LICENSE file for details. \ No newline at end of file +This project is licensed under the MIT License - [see the LICENSE file for details.](LICENSE) diff --git a/Scripts/gh-md-toc b/Scripts/gh-md-toc new file mode 100755 index 0000000..03b5ddd --- /dev/null +++ b/Scripts/gh-md-toc @@ -0,0 +1,421 @@ +#!/usr/bin/env bash + +# +# Steps: +# +# 1. Download corresponding html file for some README.md: +# curl -s $1 +# +# 2. Discard rows where no substring 'user-content-' (github's markup): +# awk '/user-content-/ { ... +# +# 3.1 Get last number in each row like ' ... sitemap.js.*<\/h/)+2, RLENGTH-5) +# +# 5. Find anchor and insert it inside "(...)": +# substr($0, match($0, "href=\"[^\"]+?\" ")+6, RLENGTH-8) +# + +gh_toc_version="0.10.0" + +gh_user_agent="gh-md-toc v$gh_toc_version" + +# +# Download rendered into html README.md by its url. +# +# +gh_toc_load() { + local gh_url=$1 + + if type curl &>/dev/null; then + curl --user-agent "$gh_user_agent" -s "$gh_url" + elif type wget &>/dev/null; then + wget --user-agent="$gh_user_agent" -qO- "$gh_url" + else + echo "Please, install 'curl' or 'wget' and try again." + exit 1 + fi +} + +# +# Converts local md file into html by GitHub +# +# -> curl -X POST --data '{"text": "Hello world github/linguist#1 **cool**, and #1!"}' https://api.github.com/markdown +#

Hello world github/linguist#1 cool, and #1!

'" +gh_toc_md2html() { + local gh_file_md=$1 + local skip_header=$2 + + URL=https://api.github.com/markdown/raw + + if [ -n "$GH_TOC_TOKEN" ]; then + TOKEN=$GH_TOC_TOKEN + else + TOKEN_FILE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/token.txt" + if [ -f "$TOKEN_FILE" ]; then + TOKEN="$(cat "$TOKEN_FILE")" + fi + fi + if [ -n "${TOKEN}" ]; then + AUTHORIZATION="Authorization: token ${TOKEN}" + fi + + local gh_tmp_file_md=$gh_file_md + if [ "$skip_header" = "yes" ]; then + if grep -Fxq "" "$gh_src"; then + # cut everything before the toc + gh_tmp_file_md=$gh_file_md~~ + sed '1,//d' "$gh_file_md" > "$gh_tmp_file_md" + fi + fi + + # echo $URL 1>&2 + OUTPUT=$(curl -s \ + --user-agent "$gh_user_agent" \ + --data-binary @"$gh_tmp_file_md" \ + -H "Content-Type:text/plain" \ + -H "$AUTHORIZATION" \ + "$URL") + + rm -f "${gh_file_md}~~" + + if [ "$?" != "0" ]; then + echo "XXNetworkErrorXX" + fi + if [ "$(echo "${OUTPUT}" | awk '/API rate limit exceeded/')" != "" ]; then + echo "XXRateLimitXX" + else + echo "${OUTPUT}" + fi +} + + +# +# Is passed string url +# +gh_is_url() { + case $1 in + https* | http*) + echo "yes";; + *) + echo "no";; + esac +} + +# +# TOC generator +# +gh_toc(){ + local gh_src=$1 + local gh_src_copy=$1 + local gh_ttl_docs=$2 + local need_replace=$3 + local no_backup=$4 + local no_footer=$5 + local indent=$6 + local skip_header=$7 + + if [ "$gh_src" = "" ]; then + echo "Please, enter URL or local path for a README.md" + exit 1 + fi + + + # Show "TOC" string only if working with one document + if [ "$gh_ttl_docs" = "1" ]; then + + echo "Table of Contents" + echo "=================" + echo "" + gh_src_copy="" + + fi + + if [ "$(gh_is_url "$gh_src")" == "yes" ]; then + gh_toc_load "$gh_src" | gh_toc_grab "$gh_src_copy" "$indent" + if [ "${PIPESTATUS[0]}" != "0" ]; then + echo "Could not load remote document." + echo "Please check your url or network connectivity" + exit 1 + fi + if [ "$need_replace" = "yes" ]; then + echo + echo "!! '$gh_src' is not a local file" + echo "!! Can't insert the TOC into it." + echo + fi + else + local rawhtml + rawhtml=$(gh_toc_md2html "$gh_src" "$skip_header") + if [ "$rawhtml" == "XXNetworkErrorXX" ]; then + echo "Parsing local markdown file requires access to github API" + echo "Please make sure curl is installed and check your network connectivity" + exit 1 + fi + if [ "$rawhtml" == "XXRateLimitXX" ]; then + echo "Parsing local markdown file requires access to github API" + echo "Error: You exceeded the hourly limit. See: https://developer.github.com/v3/#rate-limiting" + TOKEN_FILE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/token.txt" + echo "or place GitHub auth token here: ${TOKEN_FILE}" + exit 1 + fi + local toc + toc=`echo "$rawhtml" | gh_toc_grab "$gh_src_copy" "$indent"` + echo "$toc" + if [ "$need_replace" = "yes" ]; then + if grep -Fxq "" "$gh_src" && grep -Fxq "" "$gh_src"; then + echo "Found markers" + else + echo "You don't have or in your file...exiting" + exit 1 + fi + local ts="<\!--ts-->" + local te="<\!--te-->" + local dt + dt=$(date +'%F_%H%M%S') + local ext=".orig.${dt}" + local toc_path="${gh_src}.toc.${dt}" + local toc_createdby="" + local toc_footer + toc_footer="" + # http://fahdshariff.blogspot.ru/2012/12/sed-mutli-line-replacement-between-two.html + # clear old TOC + sed -i"${ext}" "/${ts}/,/${te}/{//!d;}" "$gh_src" + # create toc file + echo "${toc}" > "${toc_path}" + if [ "${no_footer}" != "yes" ]; then + echo -e "\n${toc_createdby}\n${toc_footer}\n" >> "$toc_path" + fi + + # insert toc file + if ! sed --version > /dev/null 2>&1; then + sed -i "" "/${ts}/r ${toc_path}" "$gh_src" + else + sed -i "/${ts}/r ${toc_path}" "$gh_src" + fi + echo + if [ "${no_backup}" = "yes" ]; then + rm "$toc_path" "$gh_src$ext" + fi + echo "!! TOC was added into: '$gh_src'" + if [ -z "${no_backup}" ]; then + echo "!! Origin version of the file: '${gh_src}${ext}'" + echo "!! TOC added into a separate file: '${toc_path}'" + fi + echo + fi + fi +} + +# +# Grabber of the TOC from rendered html +# +# $1 - a source url of document. +# It's need if TOC is generated for multiple documents. +# $2 - number of spaces used to indent. +# +gh_toc_grab() { + + href_regex="/href=\"[^\"]+?\"/" + common_awk_script=' + modified_href = "" + split(href, chars, "") + for (i=1;i <= length(href); i++) { + c = chars[i] + res = "" + if (c == "+") { + res = " " + } else { + if (c == "%") { + res = "\\x" + } else { + res = c "" + } + } + modified_href = modified_href res + } + print sprintf("%*s", (level-1)*'"$2"', "") "* [" text "](" gh_url modified_href ")" + ' + if [ "`uname -s`" == "OS/390" ]; then + grepcmd="pcregrep -o" + echoargs="" + awkscript='{ + level = substr($0, 3, 1) + text = substr($0, match($0, /<\/span><\/a>[^<]*<\/h/)+11, RLENGTH-14) + href = substr($0, match($0, '$href_regex')+6, RLENGTH-7) + '"$common_awk_script"' + }' + else + grepcmd="grep -Eo" + echoargs="-e" + awkscript='{ + level = substr($0, 3, 1) + text = substr($0, match($0, /">.*<\/h/)+2, RLENGTH-5) + href = substr($0, match($0, '$href_regex')+6, RLENGTH-7) + '"$common_awk_script"' + }' + fi + + # if closed is on the new line, then move it on the prev line + # for example: + # was: The command foo1 + # + # became: The command foo1 + sed -e ':a' -e 'N' -e '$!ba' -e 's/\n<\/h/<\/h/g' | + + # Sometimes a line can start with . Fix that. + sed -e ':a' -e 'N' -e '$!ba' -e 's/\n//g' | sed 's/<\/code>//g' | + + # remove g-emoji + sed 's/]*[^<]*<\/g-emoji> //g' | + + # now all rows are like: + #

title

.. + # format result line + # * $0 - whole string + # * last element of each row: "/dev/null; then + $tool --version | head -n 1 + else + echo "not installed" + fi + done +} + +show_help() { + local app_name + app_name=$(basename "$0") + echo "GitHub TOC generator ($app_name): $gh_toc_version" + echo "" + echo "Usage:" + echo " $app_name [options] src [src] Create TOC for a README file (url or local path)" + echo " $app_name - Create TOC for markdown from STDIN" + echo " $app_name --help Show help" + echo " $app_name --version Show version" + echo "" + echo "Options:" + echo " --indent Set indent size. Default: 3." + echo " --insert Insert new TOC into original file. For local files only. Default: false." + echo " See https://github.com/ekalinin/github-markdown-toc/issues/41 for details." + echo " --no-backup Remove backup file. Set --insert as well. Default: false." + echo " --hide-footer Do not write date & author of the last TOC update. Set --insert as well. Default: false." + echo " --skip-header Hide entry of the topmost headlines. Default: false." + echo " See https://github.com/ekalinin/github-markdown-toc/issues/125 for details." + echo "" +} + +# +# Options handlers +# +gh_toc_app() { + local need_replace="no" + local indent=3 + + if [ "$1" = '--help' ] || [ $# -eq 0 ] ; then + show_help + return + fi + + if [ "$1" = '--version' ]; then + show_version + return + fi + + if [ "$1" = '--indent' ]; then + indent="$2" + shift 2 + fi + + if [ "$1" = "-" ]; then + if [ -z "$TMPDIR" ]; then + TMPDIR="/tmp" + elif [ -n "$TMPDIR" ] && [ ! -d "$TMPDIR" ]; then + mkdir -p "$TMPDIR" + fi + local gh_tmp_md + if [ "`uname -s`" == "OS/390" ]; then + local timestamp + timestamp=$(date +%m%d%Y%H%M%S) + gh_tmp_md="$TMPDIR/tmp.$timestamp" + else + gh_tmp_md=$(mktemp "$TMPDIR/tmp.XXXXXX") + fi + while read -r input; do + echo "$input" >> "$gh_tmp_md" + done + gh_toc_md2html "$gh_tmp_md" | gh_toc_grab "" "$indent" + return + fi + + if [ "$1" = '--insert' ]; then + need_replace="yes" + shift + fi + + if [ "$1" = '--no-backup' ]; then + need_replace="yes" + no_backup="yes" + shift + fi + + if [ "$1" = '--hide-footer' ]; then + need_replace="yes" + no_footer="yes" + shift + fi + + if [ "$1" = '--skip-header' ]; then + skip_header="yes" + shift + fi + + + for md in "$@" + do + echo "" + gh_toc "$md" "$#" "$need_replace" "$no_backup" "$no_footer" "$indent" "$skip_header" + done + + echo "" + echo "" +} + +# +# Entry point +# +gh_toc_app "$@" \ No newline at end of file diff --git a/Scripts/header.sh b/Scripts/header.sh new file mode 100755 index 0000000..4ed7446 --- /dev/null +++ b/Scripts/header.sh @@ -0,0 +1,98 @@ +#!/bin/bash + +# Function to print usage +usage() { + echo "Usage: $0 -d directory -c creator -o company -p package [-y year]" + echo " -d directory Directory to read from (including subdirectories)" + echo " -c creator Name of the creator" + echo " -o company Name of the company with the copyright" + echo " -p package Package or library name" + echo " -y year Copyright year (optional, defaults to current year)" + exit 1 +} + +# Get the current year if not provided +current_year=$(date +"%Y") + +# Default values +year="$current_year" + +# Parse arguments +while getopts ":d:c:o:p:y:" opt; do + case $opt in + d) directory="$OPTARG" ;; + c) creator="$OPTARG" ;; + o) company="$OPTARG" ;; + p) package="$OPTARG" ;; + y) year="$OPTARG" ;; + *) usage ;; + esac +done + +# Check for mandatory arguments +if [ -z "$directory" ] || [ -z "$creator" ] || [ -z "$company" ] || [ -z "$package" ]; then + usage +fi + +# Define the header template +header_template="// +// %s +// %s +// +// Created by %s. +// Copyright © %s %s. +// +// Permission is hereby granted, free of charge, to any person +// obtaining a copy of this software and associated documentation +// files (the “Software”), to deal in the Software without +// restriction, including without limitation the rights to use, +// copy, modify, merge, publish, distribute, sublicense, and/or +// sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following +// conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +// OTHER DEALINGS IN THE SOFTWARE. +//" + +# Loop through each Swift file in the specified directory and subdirectories +find "$directory" -type f -name "*.swift" | while read -r file; do + # Check if the first line is the swift-format-ignore indicator + first_line=$(head -n 1 "$file") + if [[ "$first_line" == "// swift-format-ignore-file" ]]; then + echo "Skipping $file due to swift-format-ignore directive." + continue + fi + + # Create the header with the current filename + filename=$(basename "$file") + header=$(printf "$header_template" "$filename" "$package" "$creator" "$year" "$company") + + # Remove all consecutive lines at the beginning which start with "// ", contain only whitespace, or only "//" + awk ' + BEGIN { skip = 1 } + { + if (skip && ($0 ~ /^\/\/ / || $0 ~ /^\/\/$/ || $0 ~ /^$/)) { + next + } + skip = 0 + print + }' "$file" > temp_file + + # Add the header to the cleaned file + (echo "$header"; echo; cat temp_file) > "$file" + + # Remove the temporary file + rm temp_file +done + +echo "Headers added or files skipped appropriately across all Swift files in the directory and subdirectories." \ No newline at end of file diff --git a/Scripts/lint.sh b/Scripts/lint.sh new file mode 100755 index 0000000..e0e82f6 --- /dev/null +++ b/Scripts/lint.sh @@ -0,0 +1,82 @@ +#!/bin/bash + +set -e # Exit on any error + +ERRORS=0 + +run_command() { + if [ "$LINT_MODE" = "STRICT" ]; then + "$@" || ERRORS=$((ERRORS + 1)) + else + "$@" + fi +} + +if [ "$LINT_MODE" = "INSTALL" ]; then + exit +fi + +echo "LintMode: $LINT_MODE" + +# More portable way to get script directory +if [ -z "$SRCROOT" ]; then + SCRIPT_DIR=$(dirname "$(readlink -f "$0")") + PACKAGE_DIR="${SCRIPT_DIR}/.." +else + PACKAGE_DIR="${SRCROOT}" +fi + +# Detect OS and set paths accordingly +if [ "$(uname)" = "Darwin" ]; then + DEFAULT_MINT_PATH="/opt/homebrew/bin/mint" +elif [ "$(uname)" = "Linux" ] && [ -n "$GITHUB_ACTIONS" ]; then + DEFAULT_MINT_PATH="$GITHUB_WORKSPACE/Mint/.mint/bin/mint" +elif [ "$(uname)" = "Linux" ]; then + DEFAULT_MINT_PATH="/usr/local/bin/mint" +else + echo "Unsupported operating system" + exit 1 +fi + +# Use environment MINT_CMD if set, otherwise use default path +MINT_CMD=${MINT_CMD:-$DEFAULT_MINT_PATH} + +export MINT_PATH="$PACKAGE_DIR/.mint" +MINT_ARGS="-n -m $PACKAGE_DIR/Mintfile --silent" +MINT_RUN="$MINT_CMD run $MINT_ARGS" + +if [ "$LINT_MODE" = "NONE" ]; then + exit +elif [ "$LINT_MODE" = "STRICT" ]; then + SWIFTFORMAT_OPTIONS="--strict --configuration .swift-format" + SWIFTLINT_OPTIONS="--strict" + STRINGSLINT_OPTIONS="--config .strict.stringslint.yml" +else + SWIFTFORMAT_OPTIONS="--configuration .swift-format" + SWIFTLINT_OPTIONS="" + STRINGSLINT_OPTIONS="--config .stringslint.yml" +fi + +pushd $PACKAGE_DIR +run_command $MINT_CMD bootstrap -m Mintfile + +if [ -z "$CI" ]; then + run_command $MINT_RUN swift-format format $SWIFTFORMAT_OPTIONS --recursive --parallel --in-place Sources Tests + run_command $MINT_RUN swiftlint --fix +fi + +if [ -z "$FORMAT_ONLY" ]; then + run_command $MINT_RUN swift-format lint --configuration .swift-format --recursive --parallel $SWIFTFORMAT_OPTIONS Sources Tests || exit 1 + run_command $MINT_RUN swiftlint lint $SWIFTLINT_OPTIONS || exit 1 +fi + +$PACKAGE_DIR/Scripts/header.sh -d $PACKAGE_DIR/Sources -c "Leo Dion" -o "BrightDigit" -p "SyntaxKit" + +run_command $MINT_RUN swiftlint lint $SWIFTLINT_OPTIONS +run_command $MINT_RUN swift-format lint --recursive --parallel $SWIFTFORMAT_OPTIONS Sources Tests + +if [ -z "$CI" ]; then + run_command $MINT_RUN periphery scan $PERIPHERY_OPTIONS --disable-update-check +fi + +popd diff --git a/Scripts/swift-doc.sh b/Scripts/swift-doc.sh new file mode 100755 index 0000000..436cfb2 --- /dev/null +++ b/Scripts/swift-doc.sh @@ -0,0 +1,193 @@ +#!/bin/bash + +# Check if ANTHROPIC_API_KEY is set +if [ -z "$ANTHROPIC_API_KEY" ]; then + echo "Error: ANTHROPIC_API_KEY environment variable is not set" + echo "Please set it with: export ANTHROPIC_API_KEY='your-key-here'" + exit 1 +fi + +# Check if jq is installed +if ! command -v jq &> /dev/null; then + echo "Error: jq is required but not installed." + echo "Please install it:" + echo " - On macOS: brew install jq" + echo " - On Ubuntu/Debian: sudo apt-get install jq" + echo " - On CentOS/RHEL: sudo yum install jq" + exit 1 +fi + +# Check if an argument was provided +if [ $# -eq 0 ]; then + echo "Usage: $0 [--skip-backup]" + exit 1 +fi + +TARGET=$1 +SKIP_BACKUP=0 + +# Check for optional flags +if [ "$2" = "--skip-backup" ]; then + SKIP_BACKUP=1 +fi + +# Function to extract header (comments, imports, etc.) +extract_header() { + local file="$1" + perl -0777 -ne ' + # Match the entire header block including license + if (/^(\/\/[^\n]*\n)+/ || /^(\/\*.*?\*\/\s*\n)/s) { + print $&; + } + # Match all imports + while (/^(?:public )?import[^\n]+\n/gm) { + print $&; + } + ' "$file" +} + +# Function to ensure implementations are preserved +ensure_implementations() { + local new_content="$1" + local original_content="$2" + + # If the new content is missing parts of the original implementation, return the original + if [ ${#new_content} -lt ${#original_content} ]; then + echo "$original_content" + else + echo "$new_content" + fi +} + +# Function to clean markdown code blocks +clean_markdown() { + local content="$1" + # Remove ```swift from the start and ``` from the end, if present + echo "$content" | perl -pe 's/^```swift\s*\n//; s/```\s*$//' +} + +# Function to process a single Swift file +process_swift_file() { + local SWIFT_FILE=$1 + echo "Processing: $SWIFT_FILE" + + # Create backup unless skipped + if [ $SKIP_BACKUP -eq 0 ]; then + cp "$SWIFT_FILE" "${SWIFT_FILE}.backup" + echo "Created backup: ${SWIFT_FILE}.backup" + fi + + # Read the entire file content + local original_content + original_content=$(cat "$SWIFT_FILE") + + # Get the header section + local header + header=$(extract_header "$SWIFT_FILE") + + # Create the JSON payload for Claude + local JSON_PAYLOAD + JSON_PAYLOAD=$(jq -n \ + --arg code "$original_content" \ + '{ + model: "claude-3-haiku-20240307", + max_tokens: 2000, + messages: [{ + role: "user", + content: "Add Swift documentation comments to this code. Preserve ALL existing functionality, implementations, and structure exactly as is. Do not modify or remove any existing code, including imports, implementations, and conditional compilation blocks. Only add documentation comments:\n\n\($code)" + }] + }') + + # Make the API call to Claude + local response + response=$(curl -s https://api.anthropic.com/v1/messages \ + -H "Content-Type: application/json" \ + -H "x-api-key: $ANTHROPIC_API_KEY" \ + -H "anthropic-version: 2023-06-01" \ + -d "$JSON_PAYLOAD") + + # Check if the API call was successful + if [ $? -ne 0 ]; then + echo "Error: API call failed for $SWIFT_FILE" + return 1 + fi + + # Extract the content from the response using jq + local documented_code + documented_code=$(echo "$response" | jq -r '.content[0].text // empty') + + # Check if we got valid content back + if [ -z "$documented_code" ]; then + echo "Error: No valid response received for $SWIFT_FILE" + echo "API Response: $response" + return 1 + fi + + # Clean the markdown formatting from the response + documented_code=$(clean_markdown "$documented_code") + + # Ensure all implementations are preserved + documented_code=$(ensure_implementations "$documented_code" "$original_content") + + # Write to a temporary file first + local tmp_file=$(mktemp) + echo "$documented_code" > "$tmp_file" + + # Move the temporary file to the target + mv "$tmp_file" "$SWIFT_FILE" + + # Show diff if available and backup exists + if [ $SKIP_BACKUP -eq 0 ] && command -v diff &> /dev/null; then + echo -e "\nChanges made to $SWIFT_FILE:" + diff "${SWIFT_FILE}.backup" "$SWIFT_FILE" || true + fi + + echo "✓ Documentation added to $SWIFT_FILE" + echo "----------------------------------------" +} + +# Function to process directory +process_directory() { + local DIR=$1 + local SWIFT_FILES=0 + local PROCESSED=0 + local FAILED=0 + + # Count total Swift files + SWIFT_FILES=$(find "$DIR" -name "*.swift" | wc -l) + echo "Found $SWIFT_FILES Swift files in $DIR" + echo "----------------------------------------" + + # Process each Swift file + while IFS= read -r file; do + if process_swift_file "$file"; then + ((PROCESSED++)) + else + ((FAILED++)) + fi + # Add a small delay to avoid API rate limits + sleep 1 + done < <(find "$DIR" -name "*.swift") + + echo "Summary:" + echo "- Total Swift files found: $SWIFT_FILES" + echo "- Successfully processed: $PROCESSED" + echo "- Failed: $FAILED" +} + +# Main logic +if [ -f "$TARGET" ]; then + # Single file processing + if [[ "$TARGET" == *.swift ]]; then + process_swift_file "$TARGET" + else + echo "Error: File must have .swift extension" + exit 1 + fi +elif [ -d "$TARGET" ]; then + # Directory processing + process_directory "$TARGET" +else + echo "Error: $TARGET is neither a valid file nor directory" + exit 1 +fi \ No newline at end of file diff --git a/Sources/SwiftBuilder/Assignment.swift b/Sources/SwiftBuilder/Assignment.swift deleted file mode 100644 index c5152aa..0000000 --- a/Sources/SwiftBuilder/Assignment.swift +++ /dev/null @@ -1,46 +0,0 @@ -import SwiftSyntax - - - - - - - - - - - - -public struct Assignment: CodeBlock { - private let target: String - private let value: String - public init(_ target: String, _ value: String) { - self.target = target - self.value = value - } - public var syntax: SyntaxProtocol { - let left = ExprSyntax(DeclReferenceExprSyntax(baseName: .identifier(target))) - let right: ExprSyntax - if value.hasPrefix("\"") && value.hasSuffix("\"") || value.contains("\\(") { - right = ExprSyntax(StringLiteralExprSyntax( - openingQuote: .stringQuoteToken(), - segments: StringLiteralSegmentListSyntax([ - .stringSegment(StringSegmentSyntax(content: .stringSegment(String(value.dropFirst().dropLast())))) - ]), - closingQuote: .stringQuoteToken() - )) - } else { - right = ExprSyntax(DeclReferenceExprSyntax(baseName: .identifier(value))) - } - let assign = ExprSyntax(AssignmentExprSyntax(equal: .equalToken(leadingTrivia: .space, trailingTrivia: .space))) - return SequenceExprSyntax( - elements: ExprListSyntax([ - left, - assign, - right - ]) - ) - } -} - - diff --git a/Sources/SwiftBuilder/Case.swift b/Sources/SwiftBuilder/Case.swift deleted file mode 100644 index e674a2b..0000000 --- a/Sources/SwiftBuilder/Case.swift +++ /dev/null @@ -1,48 +0,0 @@ -// -// Case.swift -// SwiftBuilder -// -// Created by Leo Dion on 6/15/25. -// -import SwiftSyntax - -public struct Case: CodeBlock { - private let patterns: [String] - private let body: [CodeBlock] - - public init(_ patterns: String..., @CodeBlockBuilderResult content: () -> [CodeBlock]) { - self.patterns = patterns - self.body = content() - } - - public var switchCaseSyntax: SwitchCaseSyntax { - let patternList = TuplePatternElementListSyntax( - patterns.map { TuplePatternElementSyntax( - label: nil, - colon: nil, - pattern: PatternSyntax(IdentifierPatternSyntax(identifier: .identifier($0))) - )} - ) - let caseItems = SwitchCaseItemListSyntax([ - SwitchCaseItemSyntax( - pattern: TuplePatternSyntax( - leftParen: .leftParenToken(), - elements: patternList, - rightParen: .rightParenToken() - ) - ) - ]) - let statements = CodeBlockItemListSyntax(body.compactMap { $0.syntax.as(CodeBlockItemSyntax.self) }) - let label = SwitchCaseLabelSyntax( - caseKeyword: .keyword(.case, trailingTrivia: .space), - caseItems: caseItems, - colon: .colonToken() - ) - return SwitchCaseSyntax( - label: .case(label), - statements: statements - ) - } - - public var syntax: SyntaxProtocol { switchCaseSyntax } -} diff --git a/Sources/SwiftBuilder/CodeBlock+Generate.swift b/Sources/SwiftBuilder/CodeBlock+Generate.swift deleted file mode 100644 index 46af657..0000000 --- a/Sources/SwiftBuilder/CodeBlock+Generate.swift +++ /dev/null @@ -1,26 +0,0 @@ -import Foundation -import SwiftSyntax - -public extension CodeBlock { - func generateCode() -> String { - let statements: CodeBlockItemListSyntax - if let list = self.syntax.as(CodeBlockItemListSyntax.self) { - statements = list - } else { - let item: CodeBlockItemSyntax.Item - if let decl = self.syntax.as(DeclSyntax.self) { - item = .decl(decl) - } else if let stmt = self.syntax.as(StmtSyntax.self) { - item = .stmt(stmt) - } else if let expr = self.syntax.as(ExprSyntax.self) { - item = .expr(expr) - } else { - fatalError("Unsupported syntax type at top level: \(type(of: self.syntax)) generating from \(self)") - } - statements = CodeBlockItemListSyntax([CodeBlockItemSyntax(item: item, trailingTrivia: .newline)]) - } - - let sourceFile = SourceFileSyntax(statements: statements) - return sourceFile.description.trimmingCharacters(in: .whitespacesAndNewlines) - } -} \ No newline at end of file diff --git a/Sources/SwiftBuilder/CodeBlock.swift b/Sources/SwiftBuilder/CodeBlock.swift deleted file mode 100644 index 610636e..0000000 --- a/Sources/SwiftBuilder/CodeBlock.swift +++ /dev/null @@ -1,40 +0,0 @@ -import Foundation -import SwiftSyntax - -public protocol CodeBlock { - var syntax: SyntaxProtocol { get } -} - -public protocol CodeBlockBuilder { - associatedtype Result: CodeBlock - func build() -> Result -} - -@resultBuilder -public struct CodeBlockBuilderResult { - public static func buildBlock(_ components: CodeBlock...) -> [CodeBlock] { - components - } - - public static func buildOptional(_ component: CodeBlock?) -> CodeBlock { - component ?? EmptyCodeBlock() - } - - public static func buildEither(first: CodeBlock) -> CodeBlock { - first - } - - public static func buildEither(second: CodeBlock) -> CodeBlock { - second - } - - public static func buildArray(_ components: [CodeBlock]) -> [CodeBlock] { - components - } -} - -public struct EmptyCodeBlock: CodeBlock { - public var syntax: SyntaxProtocol { - StringSegmentSyntax(content:.unknown("")) - } -} diff --git a/Sources/SwiftBuilder/Comment.swift b/Sources/SwiftBuilder/Comment.swift deleted file mode 100644 index b62ad44..0000000 --- a/Sources/SwiftBuilder/Comment.swift +++ /dev/null @@ -1,117 +0,0 @@ -import SwiftSyntax -import Foundation - -/// Represents a single comment line that can be attached to a syntax node when using `.comment { ... }` in the DSL. -public struct Line { - public enum Kind { - /// Regular line comment that starts with `//`. - case line - /// Documentation line comment that starts with `///`. - case doc - } - - public let kind: Kind - public let text: String? - - /// Convenience initializer for a regular line comment without specifying the kind explicitly. - public init(_ text: String) { - self.kind = .line - self.text = text - } - - /// Convenience initialiser. Passing only `kind` will create an empty comment line of that kind. - /// - /// Examples: - /// ```swift - /// Line("MARK: - Models") // defaults to `.line` kind - /// Line(.doc, "Represents a model") // documentation comment - /// Line(.doc) // empty `///` line - /// ``` - public init(_ kind: Kind = .line, _ text: String? = nil) { - self.kind = kind - self.text = text - } -} - -// MARK: - Internal helpers - -private extension Line { - /// Convert the `Line` to a SwiftSyntax `TriviaPiece`. - var triviaPiece: TriviaPiece { - switch kind { - case .line: - return .lineComment("// " + (text ?? "")) - case .doc: - // Empty doc line should still contain the comment marker so we keep a single `/` if no text. - if let text = text, !text.isEmpty { - return .docLineComment("/// " + text) - } else { - return .docLineComment("///") - } - } - } -} - -// MARK: - Result builder used in trailing closure form - -@resultBuilder -public enum CommentBuilderResult { - public static func buildBlock(_ components: Line...) -> [Line] { components } -} - -// MARK: - Wrapper `CodeBlock` that injects leading trivia - -private struct CommentedCodeBlock: CodeBlock { - let base: CodeBlock - let lines: [Line] - - var syntax: SyntaxProtocol { - // Shortcut if there are no comment lines - guard !lines.isEmpty else { return base.syntax } - - let commentTrivia = Trivia(pieces: lines.flatMap { [$0.triviaPiece, TriviaPiece.newlines(1)] }) - - // Re-write the first token of the underlying syntax node to prepend the trivia. - final class FirstTokenRewriter: SyntaxRewriter { - let newToken: TokenSyntax - private var replaced = false - init(newToken: TokenSyntax) { self.newToken = newToken } - override func visit(_ token: TokenSyntax) -> TokenSyntax { - if !replaced { - replaced = true - return newToken - } - return token - } - } - - guard let firstToken = base.syntax.firstToken(viewMode: .sourceAccurate) else { - // Fallback – no tokens? return original syntax - return base.syntax - } - - let newFirstToken = firstToken.with(\.leadingTrivia, commentTrivia + firstToken.leadingTrivia) - - let rewriter = FirstTokenRewriter(newToken: newFirstToken) - let rewritten = rewriter.visit(Syntax(base.syntax)) - return rewritten - } -} - -// MARK: - Public DSL surface - -public extension CodeBlock { - /// Attach comments to the current `CodeBlock`. - /// Usage: - /// ```swift - /// Struct("MyStruct") { ... } - /// .comment { - /// Line("MARK: - Models") - /// Line(.doc, "This is a documentation comment") - /// } - /// ``` - /// The provided lines are injected as leading trivia to the declaration produced by this `CodeBlock`. - func comment(@CommentBuilderResult _ content: () -> [Line]) -> CodeBlock { - CommentedCodeBlock(base: self, lines: content()) - } -} \ No newline at end of file diff --git a/Sources/SwiftBuilder/ComputedProperty.swift b/Sources/SwiftBuilder/ComputedProperty.swift deleted file mode 100644 index f263b36..0000000 --- a/Sources/SwiftBuilder/ComputedProperty.swift +++ /dev/null @@ -1,46 +0,0 @@ -import SwiftSyntax - -public struct ComputedProperty: CodeBlock { - private let name: String - private let type: String - private let body: [CodeBlock] - - public init(_ name: String, type: String, @CodeBlockBuilderResult _ content: () -> [CodeBlock]) { - self.name = name - self.type = type - self.body = content() - } - - public var syntax: SyntaxProtocol { - let accessor = AccessorBlockSyntax( - leftBrace: TokenSyntax.leftBraceToken(leadingTrivia: .space, trailingTrivia: .newline), - accessors: .getter(CodeBlockItemListSyntax(body.compactMap { - var item: CodeBlockItemSyntax? - if let decl = $0.syntax.as(DeclSyntax.self) { - item = CodeBlockItemSyntax(item: .decl(decl)) - } else if let expr = $0.syntax.as(ExprSyntax.self) { - item = CodeBlockItemSyntax(item: .expr(expr)) - } else if let stmt = $0.syntax.as(StmtSyntax.self) { - item = CodeBlockItemSyntax(item: .stmt(stmt)) - } - return item?.with(\.trailingTrivia, .newline) - })), - rightBrace: TokenSyntax.rightBraceToken(leadingTrivia: .newline) - ) - let identifier = TokenSyntax.identifier(name, trailingTrivia: .space) - let typeAnnotation = TypeAnnotationSyntax( - colon: TokenSyntax.colonToken(leadingTrivia: .space, trailingTrivia: .space), - type: IdentifierTypeSyntax(name: .identifier(type)) - ) - return VariableDeclSyntax( - bindingSpecifier: TokenSyntax.keyword(.var, trailingTrivia: .space), - bindings: PatternBindingListSyntax([ - PatternBindingSyntax( - pattern: IdentifierPatternSyntax(identifier: identifier), - typeAnnotation: typeAnnotation, - accessorBlock: accessor - ) - ]) - ) - } -} \ No newline at end of file diff --git a/Sources/SwiftBuilder/Default.swift b/Sources/SwiftBuilder/Default.swift deleted file mode 100644 index 713c595..0000000 --- a/Sources/SwiftBuilder/Default.swift +++ /dev/null @@ -1,36 +0,0 @@ -// -// Default.swift -// SwiftBuilder -// -// Created by Leo Dion on 6/15/25. -// -import SwiftSyntax - -public struct Default: CodeBlock { - private let body: [CodeBlock] - public init(@CodeBlockBuilderResult _ content: () -> [CodeBlock]) { - self.body = content() - } - public var switchCaseSyntax: SwitchCaseSyntax { - let statements = CodeBlockItemListSyntax(body.compactMap { - var item: CodeBlockItemSyntax? - if let decl = $0.syntax.as(DeclSyntax.self) { - item = CodeBlockItemSyntax(item: .decl(decl)) - } else if let expr = $0.syntax.as(ExprSyntax.self) { - item = CodeBlockItemSyntax(item: .expr(expr)) - } else if let stmt = $0.syntax.as(StmtSyntax.self) { - item = CodeBlockItemSyntax(item: .stmt(stmt)) - } - return item?.with(\.trailingTrivia, .newline) - }) - let label = SwitchDefaultLabelSyntax( - defaultKeyword: .keyword(.default, trailingTrivia: .space), - colon: .colonToken() - ) - return SwitchCaseSyntax( - label: .default(label), - statements: statements - ) - } - public var syntax: SyntaxProtocol { switchCaseSyntax } -} diff --git a/Sources/SwiftBuilder/Enum.swift b/Sources/SwiftBuilder/Enum.swift deleted file mode 100644 index 8970aaa..0000000 --- a/Sources/SwiftBuilder/Enum.swift +++ /dev/null @@ -1,111 +0,0 @@ -import SwiftSyntax - -public struct Enum: CodeBlock { - private let name: String - private let members: [CodeBlock] - private var inheritance: String? - - public init(_ name: String, @CodeBlockBuilderResult _ content: () -> [CodeBlock]) { - self.name = name - self.members = content() - } - - public func inherits(_ type: String) -> Self { - var copy = self - copy.inheritance = type - return copy - } - - public var syntax: SyntaxProtocol { - let enumKeyword = TokenSyntax.keyword(.enum, trailingTrivia: .space) - let identifier = TokenSyntax.identifier(name, trailingTrivia: .space) - - var inheritanceClause: InheritanceClauseSyntax? - if let inheritance = inheritance { - let inheritedType = InheritedTypeSyntax(type: IdentifierTypeSyntax(name: .identifier(inheritance))) - inheritanceClause = InheritanceClauseSyntax(colon: .colonToken(), inheritedTypes: InheritedTypeListSyntax([inheritedType])) - } - - let memberBlock = MemberBlockSyntax( - leftBrace: .leftBraceToken(leadingTrivia: .space, trailingTrivia: .newline), - members: MemberBlockItemListSyntax(members.compactMap { member in - guard let syntax = member.syntax.as(DeclSyntax.self) else { return nil } - return MemberBlockItemSyntax(decl: syntax, trailingTrivia: .newline) - }), - rightBrace: .rightBraceToken(leadingTrivia: .newline) - ) - - return EnumDeclSyntax( - enumKeyword: enumKeyword, - name: identifier, - inheritanceClause: inheritanceClause, - memberBlock: memberBlock - ) - } -} - -public struct EnumCase: CodeBlock { - private let name: String - private var value: String? - private var intValue: Int? - - public init(_ name: String) { - self.name = name - } - - public func equals(_ value: String) -> Self { - var copy = self - copy.value = value - copy.intValue = nil - return copy - } - - public func equals(_ value: Int) -> Self { - var copy = self - copy.value = nil - copy.intValue = value - return copy - } - - public var syntax: SyntaxProtocol { - let caseKeyword = TokenSyntax.keyword(.case, trailingTrivia: .space) - let identifier = TokenSyntax.identifier(name, trailingTrivia: .space) - - var initializer: InitializerClauseSyntax? - if let value = value { - initializer = InitializerClauseSyntax( - equal: .equalToken(leadingTrivia: .space, trailingTrivia: .space), - value: StringLiteralExprSyntax( - openingQuote: .stringQuoteToken(), - segments: StringLiteralSegmentListSyntax([ - .stringSegment(StringSegmentSyntax(content: .stringSegment(value))) - ]), - closingQuote: .stringQuoteToken() - ) - ) - } else if let intValue = intValue { - initializer = InitializerClauseSyntax( - equal: .equalToken(leadingTrivia: .space, trailingTrivia: .space), - value: IntegerLiteralExprSyntax(digits: .integerLiteral(String(intValue))) - ) - } - - return EnumCaseDeclSyntax( - caseKeyword: caseKeyword, - elements: EnumCaseElementListSyntax([ - EnumCaseElementSyntax( - leadingTrivia: .space, - _: nil, - name: identifier, - _: nil, - parameterClause: nil, - _: nil, - rawValue: initializer, - _: nil, - trailingComma: nil, - trailingTrivia: .newline - ) - ]) - ) - } -} diff --git a/Sources/SwiftBuilder/Function.swift b/Sources/SwiftBuilder/Function.swift deleted file mode 100644 index 63e1ff5..0000000 --- a/Sources/SwiftBuilder/Function.swift +++ /dev/null @@ -1,125 +0,0 @@ -import SwiftSyntax - -public struct Function: CodeBlock { - private let name: String - private let parameters: [Parameter] - private let returnType: String? - private let body: [CodeBlock] - private var isStatic: Bool = false - private var isMutating: Bool = false - - public init(_ name: String, returns returnType: String? = nil, @CodeBlockBuilderResult _ content: () -> [CodeBlock]) { - self.name = name - self.parameters = [] - self.returnType = returnType - self.body = content() - } - - public init(_ name: String, returns returnType: String? = nil, @ParameterBuilderResult _ params: () -> [Parameter], @CodeBlockBuilderResult _ content: () -> [CodeBlock]) { - self.name = name - self.parameters = params() - self.returnType = returnType - self.body = content() - } - - public func `static`() -> Self { - var copy = self - copy.isStatic = true - return copy - } - - public func mutating() -> Self { - var copy = self - copy.isMutating = true - return copy - } - - public var syntax: SyntaxProtocol { - let funcKeyword = TokenSyntax.keyword(.func, trailingTrivia: .space) - let identifier = TokenSyntax.identifier(name) - - // Build parameter list - let paramList: FunctionParameterListSyntax - if parameters.isEmpty { - paramList = FunctionParameterListSyntax([]) - } else { - paramList = FunctionParameterListSyntax(parameters.enumerated().compactMap { index, param in - guard !param.name.isEmpty, !param.type.isEmpty else { return nil } - var paramSyntax = FunctionParameterSyntax( - firstName: param.isUnnamed ? .wildcardToken(trailingTrivia: .space) : .identifier(param.name), - secondName: param.isUnnamed ? .identifier(param.name) : nil, - colon: .colonToken(leadingTrivia: .space, trailingTrivia: .space), - type: IdentifierTypeSyntax(name: .identifier(param.type)), - defaultValue: param.defaultValue.map { - InitializerClauseSyntax( - equal: .equalToken(leadingTrivia: .space, trailingTrivia: .space), - value: ExprSyntax(DeclReferenceExprSyntax(baseName: .identifier($0))) - ) - } - ) - if index < parameters.count - 1 { - paramSyntax = paramSyntax.with(\.trailingComma, .commaToken(trailingTrivia: .space)) - } - return paramSyntax - }) - } - - // Build return type if specified - var returnClause: ReturnClauseSyntax? - if let returnType = returnType { - returnClause = ReturnClauseSyntax( - arrow: .arrowToken(leadingTrivia: .space, trailingTrivia: .space), - type: IdentifierTypeSyntax(name: .identifier(returnType)) - ) - } - - // Build function body - let bodyBlock = CodeBlockSyntax( - leftBrace: .leftBraceToken(leadingTrivia: .space, trailingTrivia: .newline), - statements: CodeBlockItemListSyntax(body.compactMap { - var item: CodeBlockItemSyntax? - if let decl = $0.syntax.as(DeclSyntax.self) { - item = CodeBlockItemSyntax(item: .decl(decl)) - } else if let expr = $0.syntax.as(ExprSyntax.self) { - item = CodeBlockItemSyntax(item: .expr(expr)) - } else if let stmt = $0.syntax.as(StmtSyntax.self) { - item = CodeBlockItemSyntax(item: .stmt(stmt)) - } - return item?.with(\.trailingTrivia, .newline) - }), - rightBrace: .rightBraceToken(leadingTrivia: .newline) - ) - - // Build modifiers - var modifiers: DeclModifierListSyntax = [] - if isStatic { - modifiers = DeclModifierListSyntax([ - DeclModifierSyntax(name: .keyword(.static, trailingTrivia: .space)) - ]) - } - if isMutating { - modifiers = DeclModifierListSyntax(modifiers + [ - DeclModifierSyntax(name: .keyword(.mutating, trailingTrivia: .space)) - ]) - } - - return FunctionDeclSyntax( - attributes: AttributeListSyntax([]), - modifiers: modifiers, - funcKeyword: funcKeyword, - name: identifier, - genericParameterClause: nil, - signature: FunctionSignatureSyntax( - parameterClause: FunctionParameterClauseSyntax( - leftParen: .leftParenToken(), - parameters: paramList, - rightParen: .rightParenToken() - ), - effectSpecifiers: nil, - returnClause: returnClause - ), - genericWhereClause: nil, - body: bodyBlock - ) - } -} diff --git a/Sources/SwiftBuilder/Group.swift b/Sources/SwiftBuilder/Group.swift deleted file mode 100644 index ec390b5..0000000 --- a/Sources/SwiftBuilder/Group.swift +++ /dev/null @@ -1,30 +0,0 @@ -import SwiftSyntax - -public struct Group: CodeBlock { - let members: [CodeBlock] - - public init(@CodeBlockBuilderResult _ content: () -> [CodeBlock]) { - self.members = content() - } - - public var syntax: SyntaxProtocol { - let statements = members.flatMap { block -> [CodeBlockItemSyntax] in - if let list = block.syntax.as(CodeBlockItemListSyntax.self) { - return Array(list) - } - - let item: CodeBlockItemSyntax.Item - if let decl = block.syntax.as(DeclSyntax.self) { - item = .decl(decl) - } else if let stmt = block.syntax.as(StmtSyntax.self) { - item = .stmt(stmt) - } else if let expr = block.syntax.as(ExprSyntax.self) { - item = .expr(expr) - } else { - fatalError("Unsupported syntax type in group: \(type(of: block.syntax)) from \(block)") - } - return [CodeBlockItemSyntax(item: item, trailingTrivia: .newline)] - } - return CodeBlockItemListSyntax(statements) - } -} \ No newline at end of file diff --git a/Sources/SwiftBuilder/If.swift b/Sources/SwiftBuilder/If.swift deleted file mode 100644 index 2a85dba..0000000 --- a/Sources/SwiftBuilder/If.swift +++ /dev/null @@ -1,76 +0,0 @@ -import SwiftSyntax - -public struct If: CodeBlock { - private let condition: CodeBlock - private let body: [CodeBlock] - private let elseBody: [CodeBlock]? - - public init(_ condition: CodeBlock, @CodeBlockBuilderResult then: () -> [CodeBlock], else elseBody: (() -> [CodeBlock])? = nil) { - self.condition = condition - self.body = then() - self.elseBody = elseBody?() - } - - public var syntax: SyntaxProtocol { - let cond: ConditionElementSyntax - if let letCond = condition as? Let { - cond = ConditionElementSyntax( - condition: .optionalBinding( - OptionalBindingConditionSyntax( - bindingSpecifier: .keyword(.let, trailingTrivia: .space), - pattern: IdentifierPatternSyntax(identifier: .identifier(letCond.name)), - initializer: InitializerClauseSyntax( - equal: .equalToken(leadingTrivia: .space, trailingTrivia: .space), - value: ExprSyntax(DeclReferenceExprSyntax(baseName: .identifier(letCond.value))) - ) - ) - ) - ) - } else { - cond = ConditionElementSyntax( - condition: .expression(ExprSyntax(fromProtocol: condition.syntax.as(ExprSyntax.self) ?? DeclReferenceExprSyntax(baseName: .identifier("")))) - ) - } - let bodyBlock = CodeBlockSyntax( - leftBrace: .leftBraceToken(leadingTrivia: .space, trailingTrivia: .newline), - statements: CodeBlockItemListSyntax(body.compactMap { - var item: CodeBlockItemSyntax? - if let decl = $0.syntax.as(DeclSyntax.self) { - item = CodeBlockItemSyntax(item: .decl(decl)) - } else if let expr = $0.syntax.as(ExprSyntax.self) { - item = CodeBlockItemSyntax(item: .expr(expr)) - } else if let stmt = $0.syntax.as(StmtSyntax.self) { - item = CodeBlockItemSyntax(item: .stmt(stmt)) - } - return item?.with(\.trailingTrivia, .newline) - }), - rightBrace: .rightBraceToken(leadingTrivia: .newline) - ) - let elseBlock = elseBody.map { - IfExprSyntax.ElseBody(CodeBlockSyntax( - leftBrace: .leftBraceToken(leadingTrivia: .space, trailingTrivia: .newline), - statements: CodeBlockItemListSyntax($0.compactMap { - var item: CodeBlockItemSyntax? - if let decl = $0.syntax.as(DeclSyntax.self) { - item = CodeBlockItemSyntax(item: .decl(decl)) - } else if let expr = $0.syntax.as(ExprSyntax.self) { - item = CodeBlockItemSyntax(item: .expr(expr)) - } else if let stmt = $0.syntax.as(StmtSyntax.self) { - item = CodeBlockItemSyntax(item: .stmt(stmt)) - } - return item?.with(\.trailingTrivia, .newline) - }), - rightBrace: .rightBraceToken(leadingTrivia: .newline) - )) - } - return ExprSyntax( - IfExprSyntax( - ifKeyword: .keyword(.if, trailingTrivia: .space), - conditions: ConditionElementListSyntax([cond]), - body: bodyBlock, - elseKeyword: elseBlock != nil ? .keyword(.else, trailingTrivia: .space) : nil, - elseBody: elseBlock - ) - ) - } -} \ No newline at end of file diff --git a/Sources/SwiftBuilder/Init.swift b/Sources/SwiftBuilder/Init.swift deleted file mode 100644 index 75c10f6..0000000 --- a/Sources/SwiftBuilder/Init.swift +++ /dev/null @@ -1,25 +0,0 @@ -import SwiftSyntax - -public struct Init: CodeBlock { - private let type: String - private let parameters: [Parameter] - public init(_ type: String, @ParameterBuilderResult _ params: () -> [Parameter]) { - self.type = type - self.parameters = params() - } - public var syntax: SyntaxProtocol { - let args = TupleExprElementListSyntax(parameters.enumerated().map { index, param in - let element = param.syntax as! TupleExprElementSyntax - if index < parameters.count - 1 { - return element.with(\.trailingComma, .commaToken(trailingTrivia: .space)) - } - return element - }) - return ExprSyntax(FunctionCallExprSyntax( - calledExpression: ExprSyntax(DeclReferenceExprSyntax(baseName: .identifier(type))), - leftParen: .leftParenToken(), - argumentList: args, - rightParen: .rightParenToken() - )) - } -} \ No newline at end of file diff --git a/Sources/SwiftBuilder/Let.swift b/Sources/SwiftBuilder/Let.swift deleted file mode 100644 index 6a5c47d..0000000 --- a/Sources/SwiftBuilder/Let.swift +++ /dev/null @@ -1,30 +0,0 @@ -import SwiftSyntax - -public struct Let: CodeBlock { - let name: String - let value: String - public init(_ name: String, _ value: String) { - self.name = name - self.value = value - } - public var syntax: SyntaxProtocol { - return CodeBlockItemSyntax( - item: .decl( - DeclSyntax( - VariableDeclSyntax( - bindingSpecifier: .keyword(.let, trailingTrivia: .space), - bindings: PatternBindingListSyntax([ - PatternBindingSyntax( - pattern: IdentifierPatternSyntax(identifier: .identifier(name)), - initializer: InitializerClauseSyntax( - equal: .equalToken(leadingTrivia: .space, trailingTrivia: .space), - value: ExprSyntax(DeclReferenceExprSyntax(baseName: .identifier(value))) - ) - ) - ]) - ) - ) - ) - ) - } -} \ No newline at end of file diff --git a/Sources/SwiftBuilder/Literal.swift b/Sources/SwiftBuilder/Literal.swift deleted file mode 100644 index 6ec0e71..0000000 --- a/Sources/SwiftBuilder/Literal.swift +++ /dev/null @@ -1,31 +0,0 @@ -import SwiftSyntax - -public enum Literal: CodeBlock { - case string(String) - case float(Double) - case integer(Int) - case `nil` - case boolean(Bool) - - public var syntax: SyntaxProtocol { - switch self { - case .string(let value): - return StringLiteralExprSyntax( - openingQuote: .stringQuoteToken(), - segments: .init([ - .stringSegment(.init(content: .stringSegment(value))) - ]), - closingQuote: .stringQuoteToken() - ) - case .float(let value): - return FloatLiteralExprSyntax(literal: .floatLiteral(String(value))) - - case .integer(let value): - return IntegerLiteralExprSyntax(digits: .integerLiteral(String(value))) - case .nil: - return NilLiteralExprSyntax(nilKeyword: .keyword(.nil)) - case .boolean(let value): - return BooleanLiteralExprSyntax(literal: value ? .keyword(.true) : .keyword(.false)) - } - } -} diff --git a/Sources/SwiftBuilder/Parameter.swift b/Sources/SwiftBuilder/Parameter.swift deleted file mode 100644 index 1c533ce..0000000 --- a/Sources/SwiftBuilder/Parameter.swift +++ /dev/null @@ -1,34 +0,0 @@ -import Foundation -import SwiftSyntax -import SwiftParser - -public struct Parameter: CodeBlock { - let name: String - let type: String - let defaultValue: String? - let isUnnamed: Bool - - public init(name: String, type: String, defaultValue: String? = nil, isUnnamed: Bool = false) { - self.name = name - self.type = type - self.defaultValue = defaultValue - self.isUnnamed = isUnnamed - } - - public var syntax: SyntaxProtocol { - // Not used for function signature, but for call sites (Init, etc.) - if let defaultValue = defaultValue { - return LabeledExprSyntax( - label: .identifier(name), - colon: .colonToken(), - expression: ExprSyntax(DeclReferenceExprSyntax(baseName: .identifier(defaultValue))) - ) - } else { - return LabeledExprSyntax( - label: .identifier(name), - colon: .colonToken(), - expression: ExprSyntax(DeclReferenceExprSyntax(baseName: .identifier(name))) - ) - } - } -} \ No newline at end of file diff --git a/Sources/SwiftBuilder/ParameterBuilderResult.swift b/Sources/SwiftBuilder/ParameterBuilderResult.swift deleted file mode 100644 index 8324afc..0000000 --- a/Sources/SwiftBuilder/ParameterBuilderResult.swift +++ /dev/null @@ -1,24 +0,0 @@ -import Foundation - -@resultBuilder -public struct ParameterBuilderResult { - public static func buildBlock(_ components: Parameter...) -> [Parameter] { - components - } - - public static func buildOptional(_ component: Parameter?) -> [Parameter] { - component.map { [$0] } ?? [] - } - - public static func buildEither(first: Parameter) -> [Parameter] { - [first] - } - - public static func buildEither(second: Parameter) -> [Parameter] { - [second] - } - - public static func buildArray(_ components: [Parameter]) -> [Parameter] { - components - } -} \ No newline at end of file diff --git a/Sources/SwiftBuilder/ParameterExp.swift b/Sources/SwiftBuilder/ParameterExp.swift deleted file mode 100644 index fd8504e..0000000 --- a/Sources/SwiftBuilder/ParameterExp.swift +++ /dev/null @@ -1,23 +0,0 @@ -import SwiftSyntax - -public struct ParameterExp: CodeBlock { - let name: String - let value: String - - public init(name: String, value: String) { - self.name = name - self.value = value - } - - public var syntax: SyntaxProtocol { - if name.isEmpty { - return ExprSyntax(DeclReferenceExprSyntax(baseName: .identifier(value))) - } else { - return LabeledExprSyntax( - label: .identifier(name), - colon: .colonToken(), - expression: ExprSyntax(DeclReferenceExprSyntax(baseName: .identifier(value))) - ) - } - } -} \ No newline at end of file diff --git a/Sources/SwiftBuilder/ParameterExpBuilderResult.swift b/Sources/SwiftBuilder/ParameterExpBuilderResult.swift deleted file mode 100644 index 256924e..0000000 --- a/Sources/SwiftBuilder/ParameterExpBuilderResult.swift +++ /dev/null @@ -1,24 +0,0 @@ -import Foundation - -@resultBuilder -public struct ParameterExpBuilderResult { - public static func buildBlock(_ components: ParameterExp...) -> [ParameterExp] { - components - } - - public static func buildOptional(_ component: ParameterExp?) -> [ParameterExp] { - component.map { [$0] } ?? [] - } - - public static func buildEither(first: ParameterExp) -> [ParameterExp] { - [first] - } - - public static func buildEither(second: ParameterExp) -> [ParameterExp] { - [second] - } - - public static func buildArray(_ components: [ParameterExp]) -> [ParameterExp] { - components - } -} \ No newline at end of file diff --git a/Sources/SwiftBuilder/PlusAssign.swift b/Sources/SwiftBuilder/PlusAssign.swift deleted file mode 100644 index b65e08a..0000000 --- a/Sources/SwiftBuilder/PlusAssign.swift +++ /dev/null @@ -1,41 +0,0 @@ -// -// PlusAssign.swift -// SwiftBuilder -// -// Created by Leo Dion on 6/15/25. -// -import SwiftSyntax - -public struct PlusAssign: CodeBlock { - private let target: String - private let value: String - - public init(_ target: String, _ value: String) { - self.target = target - self.value = value - } - - public var syntax: SyntaxProtocol { - let left = ExprSyntax(DeclReferenceExprSyntax(baseName: .identifier(target))) - let right: ExprSyntax - if value.hasPrefix("\"") && value.hasSuffix("\"") || value.contains("\\(") { - right = ExprSyntax(StringLiteralExprSyntax( - openingQuote: .stringQuoteToken(), - segments: StringLiteralSegmentListSyntax([ - .stringSegment(StringSegmentSyntax(content: .stringSegment(String(value.dropFirst().dropLast())))) - ]), - closingQuote: .stringQuoteToken() - )) - } else { - right = ExprSyntax(DeclReferenceExprSyntax(baseName: .identifier(value))) - } - let assign = ExprSyntax(BinaryOperatorExprSyntax(operator: .binaryOperator("+=", leadingTrivia: .space, trailingTrivia: .space))) - return SequenceExprSyntax( - elements: ExprListSyntax([ - left, - assign, - right - ]) - ) - } -} diff --git a/Sources/SwiftBuilder/Return.swift b/Sources/SwiftBuilder/Return.swift deleted file mode 100644 index fb848e8..0000000 --- a/Sources/SwiftBuilder/Return.swift +++ /dev/null @@ -1,23 +0,0 @@ -import SwiftSyntax - -public struct Return: CodeBlock { - private let exprs: [CodeBlock] - public init(@CodeBlockBuilderResult _ content: () -> [CodeBlock]) { - self.exprs = content() - } - public var syntax: SyntaxProtocol { - guard let expr = exprs.first else { - fatalError("Return must have at least one expression.") - } - if let varExp = expr as? VariableExp { - return ReturnStmtSyntax( - returnKeyword: .keyword(.return, trailingTrivia: .space), - expression: ExprSyntax(DeclReferenceExprSyntax(baseName: .identifier(varExp.name))) - ) - } - return ReturnStmtSyntax( - returnKeyword: .keyword(.return, trailingTrivia: .space), - expression: ExprSyntax(expr.syntax) - ) - } -} \ No newline at end of file diff --git a/Sources/SwiftBuilder/Struct.swift b/Sources/SwiftBuilder/Struct.swift deleted file mode 100644 index cdee138..0000000 --- a/Sources/SwiftBuilder/Struct.swift +++ /dev/null @@ -1,61 +0,0 @@ -import SwiftSyntax - -public struct Struct: CodeBlock { - private let name: String - private let members: [CodeBlock] - private var inheritance: String? - private var genericParameter: String? - - public init(_ name: String, generic: String? = nil, @CodeBlockBuilderResult _ content: () -> [CodeBlock]) { - self.name = name - self.members = content() - self.genericParameter = generic - } - - public func inherits(_ type: String) -> Self { - var copy = self - copy.inheritance = type - return copy - } - - public var syntax: SyntaxProtocol { - let structKeyword = TokenSyntax.keyword(.struct, trailingTrivia: .space) - let identifier = TokenSyntax.identifier(name) - - var genericParameterClause: GenericParameterClauseSyntax? - if let generic = genericParameter { - let genericParameter = GenericParameterSyntax( - name: .identifier(generic), - trailingComma: nil - ) - genericParameterClause = GenericParameterClauseSyntax( - leftAngle: .leftAngleToken(), - parameters: GenericParameterListSyntax([genericParameter]), - rightAngle: .rightAngleToken() - ) - } - - var inheritanceClause: InheritanceClauseSyntax? - if let inheritance = inheritance { - let inheritedType = InheritedTypeSyntax(type: IdentifierTypeSyntax(name: .identifier(inheritance))) - inheritanceClause = InheritanceClauseSyntax(colon: .colonToken(), inheritedTypes: InheritedTypeListSyntax([inheritedType])) - } - - let memberBlock = MemberBlockSyntax( - leftBrace: .leftBraceToken(leadingTrivia: .space, trailingTrivia: .newline), - members: MemberBlockItemListSyntax(members.compactMap { member in - guard let syntax = member.syntax.as(DeclSyntax.self) else { return nil } - return MemberBlockItemSyntax(decl: syntax, trailingTrivia: .newline) - }), - rightBrace: .rightBraceToken(leadingTrivia: .newline) - ) - - return StructDeclSyntax( - structKeyword: structKeyword, - name: identifier, - genericParameterClause: genericParameterClause, - inheritanceClause: inheritanceClause, - memberBlock: memberBlock - ) - } -} diff --git a/Sources/SwiftBuilder/Switch.swift b/Sources/SwiftBuilder/Switch.swift deleted file mode 100644 index c3f3617..0000000 --- a/Sources/SwiftBuilder/Switch.swift +++ /dev/null @@ -1,35 +0,0 @@ -// -// Switch.swift -// SwiftBuilder -// -// Created by Leo Dion on 6/15/25. -// -import SwiftSyntax - -public struct Switch: CodeBlock { - private let expression: String - private let cases: [CodeBlock] - - public init(_ expression: String, @CodeBlockBuilderResult _ content: () -> [CodeBlock]) { - self.expression = expression - self.cases = content() - } - - public var syntax: SyntaxProtocol { - let expr = ExprSyntax(DeclReferenceExprSyntax(baseName: .identifier(expression))) - let casesArr: [SwitchCaseSyntax] = self.cases.compactMap { - if let c = $0 as? SwitchCase { return c.switchCaseSyntax } - if let d = $0 as? Default { return d.switchCaseSyntax } - return nil - } - let cases = SwitchCaseListSyntax(casesArr.map { SwitchCaseListSyntax.Element.init($0) }) - let switchExpr = SwitchExprSyntax( - switchKeyword: .keyword(.switch, trailingTrivia: .space), - subject: expr, - leftBrace: .leftBraceToken(leadingTrivia: .space, trailingTrivia: .newline), - cases: cases, - rightBrace: .rightBraceToken(leadingTrivia: .newline) - ) - return switchExpr - } -} diff --git a/Sources/SwiftBuilder/SwitchCase.swift b/Sources/SwiftBuilder/SwitchCase.swift deleted file mode 100644 index 0894450..0000000 --- a/Sources/SwiftBuilder/SwitchCase.swift +++ /dev/null @@ -1,51 +0,0 @@ -// -// SwitchCase.swift -// SwiftBuilder -// -// Created by Leo Dion on 6/15/25. -// -import SwiftSyntax - -public struct SwitchCase: CodeBlock { - private let patterns: [String] - private let body: [CodeBlock] - - public init(_ patterns: String..., @CodeBlockBuilderResult content: () -> [CodeBlock]) { - self.patterns = patterns - self.body = content() - } - - public var switchCaseSyntax: SwitchCaseSyntax { - let caseItems = SwitchCaseItemListSyntax(patterns.enumerated().map { index, pattern in - var item = SwitchCaseItemSyntax( - pattern: PatternSyntax(IdentifierPatternSyntax(identifier: .identifier(pattern))) - ) - if index < patterns.count - 1 { - item = item.with(\.trailingComma, .commaToken(trailingTrivia: .space)) - } - return item - }) - let statements = CodeBlockItemListSyntax(body.compactMap { - var item: CodeBlockItemSyntax? - if let decl = $0.syntax.as(DeclSyntax.self) { - item = CodeBlockItemSyntax(item: .decl(decl)) - } else if let expr = $0.syntax.as(ExprSyntax.self) { - item = CodeBlockItemSyntax(item: .expr(expr)) - } else if let stmt = $0.syntax.as(StmtSyntax.self) { - item = CodeBlockItemSyntax(item: .stmt(stmt)) - } - return item?.with(\.trailingTrivia, .newline) - }) - let label = SwitchCaseLabelSyntax( - caseKeyword: .keyword(.case, trailingTrivia: .space), - caseItems: caseItems, - colon: .colonToken(trailingTrivia: .newline) - ) - return SwitchCaseSyntax( - label: .case(label), - statements: statements - ) - } - - public var syntax: SyntaxProtocol { switchCaseSyntax } -} diff --git a/Sources/SwiftBuilder/Trivia+Comments.swift b/Sources/SwiftBuilder/Trivia+Comments.swift deleted file mode 100644 index 9a61246..0000000 --- a/Sources/SwiftBuilder/Trivia+Comments.swift +++ /dev/null @@ -1,23 +0,0 @@ -import SwiftSyntax - -public extension Trivia { - /// Extract comment strings (line comments, doc comments, block comments) from the trivia collection. - var comments: [String] { - compactMap { piece in - switch piece { - case .lineComment(let text), - .blockComment(let text), - .docLineComment(let text), - .docBlockComment(let text): - return text - default: - return nil - } - } - } - - /// Indicates whether the trivia contains any comments. - var hasComments: Bool { - !comments.isEmpty - } -} \ No newline at end of file diff --git a/Sources/SwiftBuilder/Variable.swift b/Sources/SwiftBuilder/Variable.swift deleted file mode 100644 index 990852e..0000000 --- a/Sources/SwiftBuilder/Variable.swift +++ /dev/null @@ -1,48 +0,0 @@ -// -// Variable.swift -// SwiftBuilder -// -// Created by Leo Dion on 6/15/25. -// -import SwiftSyntax - -public struct Variable: CodeBlock { - private let kind: VariableKind - private let name: String - private let type: String - private let defaultValue: String? - - public init(_ kind: VariableKind, name: String, type: String, equals defaultValue: String? = nil) { - self.kind = kind - self.name = name - self.type = type - self.defaultValue = defaultValue - } - - public var syntax: SyntaxProtocol { - let bindingKeyword = TokenSyntax.keyword(kind == .let ? .let : .var, trailingTrivia: .space) - let identifier = TokenSyntax.identifier(name, trailingTrivia: .space) - let typeAnnotation = TypeAnnotationSyntax( - colon: .colonToken(leadingTrivia: .space, trailingTrivia: .space), - type: IdentifierTypeSyntax(name: .identifier(type)) - ) - - let initializer = defaultValue.map { value in - InitializerClauseSyntax( - equal: .equalToken(leadingTrivia: .space, trailingTrivia: .space), - value: ExprSyntax(DeclReferenceExprSyntax(baseName: .identifier(value))) - ) - } - - return VariableDeclSyntax( - bindingSpecifier: bindingKeyword, - bindings: PatternBindingListSyntax([ - PatternBindingSyntax( - pattern: IdentifierPatternSyntax(identifier: identifier), - typeAnnotation: typeAnnotation, - initializer: initializer - ) - ]) - ) - } -} diff --git a/Sources/SwiftBuilder/VariableDecl.swift b/Sources/SwiftBuilder/VariableDecl.swift deleted file mode 100644 index 5c9a913..0000000 --- a/Sources/SwiftBuilder/VariableDecl.swift +++ /dev/null @@ -1,47 +0,0 @@ -import SwiftSyntax - -public struct VariableDecl: CodeBlock { - private let kind: VariableKind - private let name: String - private let value: String? - - public init(_ kind: VariableKind, name: String, equals value: String? = nil) { - self.kind = kind - self.name = name - self.value = value - } - - public var syntax: SyntaxProtocol { - let bindingKeyword = TokenSyntax.keyword(kind == .let ? .let : .var, trailingTrivia: .space) - let identifier = TokenSyntax.identifier(name, trailingTrivia: .space) - let initializer = value.map { value in - if value.hasPrefix("\"") && value.hasSuffix("\"") || value.contains("\\(") { - return InitializerClauseSyntax( - equal: .equalToken(leadingTrivia: .space, trailingTrivia: .space), - value: StringLiteralExprSyntax( - openingQuote: .stringQuoteToken(), - segments: StringLiteralSegmentListSyntax([ - .stringSegment(StringSegmentSyntax(content: .stringSegment(String(value.dropFirst().dropLast())))) - ]), - closingQuote: .stringQuoteToken() - ) - ) - } else { - return InitializerClauseSyntax( - equal: .equalToken(leadingTrivia: .space, trailingTrivia: .space), - value: ExprSyntax(DeclReferenceExprSyntax(baseName: .identifier(value))) - ) - } - } - return VariableDeclSyntax( - bindingSpecifier: bindingKeyword, - bindings: PatternBindingListSyntax([ - PatternBindingSyntax( - pattern: IdentifierPatternSyntax(identifier: identifier), - typeAnnotation: nil, - initializer: initializer - ) - ]) - ) - } -} \ No newline at end of file diff --git a/Sources/SwiftBuilder/VariableExp.swift b/Sources/SwiftBuilder/VariableExp.swift deleted file mode 100644 index c933468..0000000 --- a/Sources/SwiftBuilder/VariableExp.swift +++ /dev/null @@ -1,103 +0,0 @@ -// -// VariableExp.swift -// SwiftBuilder -// -// Created by Leo Dion on 6/15/25. -// -import SwiftSyntax - -public struct VariableExp: CodeBlock { - let name: String - - public init(_ name: String) { - self.name = name - } - - public func property(_ propertyName: String) -> CodeBlock { - return PropertyAccessExp(baseName: name, propertyName: propertyName) - } - - public func call(_ methodName: String) -> CodeBlock { - return FunctionCallExp(baseName: name, methodName: methodName) - } - - public func call(_ methodName: String, @ParameterExpBuilderResult _ params: () -> [ParameterExp]) -> CodeBlock { - return FunctionCallExp(baseName: name, methodName: methodName, parameters: params()) - } - - public var syntax: SyntaxProtocol { - return TokenSyntax.identifier(name) - } -} - -public struct PropertyAccessExp: CodeBlock { - let baseName: String - let propertyName: String - - public init(baseName: String, propertyName: String) { - self.baseName = baseName - self.propertyName = propertyName - } - - public var syntax: SyntaxProtocol { - let base = ExprSyntax(DeclReferenceExprSyntax(baseName: .identifier(baseName))) - let property = TokenSyntax.identifier(propertyName) - return ExprSyntax(MemberAccessExprSyntax( - base: base, - dot: .periodToken(), - name: property - )) - } -} - -public struct FunctionCallExp: CodeBlock { - let baseName: String - let methodName: String - let parameters: [ParameterExp] - - public init(baseName: String, methodName: String) { - self.baseName = baseName - self.methodName = methodName - self.parameters = [] - } - - public init(baseName: String, methodName: String, parameters: [ParameterExp]) { - self.baseName = baseName - self.methodName = methodName - self.parameters = parameters - } - - public var syntax: SyntaxProtocol { - let base = ExprSyntax(DeclReferenceExprSyntax(baseName: .identifier(baseName))) - let method = TokenSyntax.identifier(methodName) - let args = LabeledExprListSyntax(parameters.enumerated().map { index, param in - let expr = param.syntax - if let labeled = expr as? LabeledExprSyntax { - var element = labeled - if index < parameters.count - 1 { - element = element.with(\.trailingComma, .commaToken(trailingTrivia: .space)) - } - return element - } else if let unlabeled = expr as? ExprSyntax { - return TupleExprElementSyntax( - label: nil, - colon: nil, - expression: unlabeled, - trailingComma: index < parameters.count - 1 ? .commaToken(trailingTrivia: .space) : nil - ) - } else { - fatalError("ParameterExp.syntax must return LabeledExprSyntax or ExprSyntax") - } - }) - return ExprSyntax(FunctionCallExprSyntax( - calledExpression: ExprSyntax(MemberAccessExprSyntax( - base: base, - dot: .periodToken(), - name: method - )), - leftParen: .leftParenToken(), - arguments: args, - rightParen: .rightParenToken() - )) - } -} diff --git a/Sources/SwiftBuilder/VariableKind.swift b/Sources/SwiftBuilder/VariableKind.swift deleted file mode 100644 index bbfb5b5..0000000 --- a/Sources/SwiftBuilder/VariableKind.swift +++ /dev/null @@ -1,6 +0,0 @@ -import Foundation - -public enum VariableKind { - case `let` - case `var` -} \ No newline at end of file diff --git a/Sources/SwiftBuilder/parser/OldMain.swift b/Sources/SwiftBuilder/parser/OldMain.swift deleted file mode 100644 index 62850f9..0000000 --- a/Sources/SwiftBuilder/parser/OldMain.swift +++ /dev/null @@ -1,25 +0,0 @@ -//import Foundation -// -//@main -//struct Main { -// static func main() throws { -// do { -// let code = String(decoding: FileHandle.standardInput.availableData, as: UTF8.self) -// let options = Array(CommandLine.arguments.dropFirst(1)) -// -// let response = try SyntaxParser.parse(code: code, options: options) -// -// let data = try JSONEncoder().encode(response) -// print(String(decoding: data, as: UTF8.self)) -// } catch { -// var standardError = FileHandle.standardError -// print("\(error)", to:&standardError) -// } -// } -//} -// -//extension FileHandle: @retroactive TextOutputStream { -// public func write(_ string: String) { -// self.write(Data(string.utf8)) -// } -//} diff --git a/Sources/SwiftBuilder/parser/SyntaxParser.swift b/Sources/SwiftBuilder/parser/SyntaxParser.swift deleted file mode 100644 index f492600..0000000 --- a/Sources/SwiftBuilder/parser/SyntaxParser.swift +++ /dev/null @@ -1,30 +0,0 @@ -import Foundation -import SwiftSyntax -import SwiftOperators -import SwiftParser - -package struct SyntaxParser { - package static func parse(code: String, options: [String] = []) throws -> SyntaxResponse { - let sourceFile = Parser.parse(source: code) - - let syntax: Syntax - if options.contains("fold") { - syntax = OperatorTable.standardOperators.foldAll(sourceFile, errorHandler: { _ in }) - } else { - syntax = Syntax(sourceFile) - } - - let visitor = TokenVisitor( - locationConverter: SourceLocationConverter(fileName: "", tree: sourceFile), - showMissingTokens: options.contains("showmissing") - ) - _ = visitor.rewrite(syntax) - - - let tree = visitor.tree - let encoder = JSONEncoder() - let json = String(decoding: try encoder.encode(tree), as: UTF8.self) - - return SyntaxResponse(syntaxJSON: json, swiftVersion: version) - } -} diff --git a/Sources/SwiftBuilder/parser/SyntaxResponse.swift b/Sources/SwiftBuilder/parser/SyntaxResponse.swift deleted file mode 100644 index 394b467..0000000 --- a/Sources/SwiftBuilder/parser/SyntaxResponse.swift +++ /dev/null @@ -1,7 +0,0 @@ -import Foundation - -package struct SyntaxResponse: Codable { - //package let syntaxHTML: String - package let syntaxJSON: String - package let swiftVersion: String -} diff --git a/Sources/SwiftBuilder/parser/Version.swift b/Sources/SwiftBuilder/parser/Version.swift deleted file mode 100644 index cab8a40..0000000 --- a/Sources/SwiftBuilder/parser/Version.swift +++ /dev/null @@ -1,2 +0,0 @@ -import Foundation -let version = "6.01.0" diff --git a/Sources/SwiftBuilderCLI/main.swift b/Sources/SwiftBuilderCLI/main.swift deleted file mode 100644 index bb6d9dd..0000000 --- a/Sources/SwiftBuilderCLI/main.swift +++ /dev/null @@ -1,21 +0,0 @@ -import Foundation -import SwiftBuilder - -// Read Swift code from stdin -let code = String(data: FileHandle.standardInput.readDataToEndOfFile(), encoding: .utf8) ?? "" - -do { - // Parse the code using SwiftBuilder - let response = try SyntaxParser.parse(code: code, options: ["fold"]) - - // Output the JSON to stdout - print(response.syntaxJSON) -} catch { - // If there's an error, output it as JSON - let errorResponse = ["error": error.localizedDescription] - if let jsonData = try? JSONSerialization.data(withJSONObject: errorResponse), - let jsonString = String(data: jsonData, encoding: .utf8) { - print(jsonString) - } - exit(1) -} \ No newline at end of file diff --git a/Sources/SyntaxKit/Assignment.swift b/Sources/SyntaxKit/Assignment.swift new file mode 100644 index 0000000..4a0af2e --- /dev/null +++ b/Sources/SyntaxKit/Assignment.swift @@ -0,0 +1,71 @@ +// +// Assignment.swift +// SyntaxKit +// +// Created by Leo Dion. +// Copyright © 2025 BrightDigit. +// +// Permission is hereby granted, free of charge, to any person +// obtaining a copy of this software and associated documentation +// files (the “Software”), to deal in the Software without +// restriction, including without limitation the rights to use, +// copy, modify, merge, publish, distribute, sublicense, and/or +// sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following +// conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +// OTHER DEALINGS IN THE SOFTWARE. +// + +import SwiftSyntax + +/// An assignment expression. +public struct Assignment: CodeBlock { + private let target: String + private let value: String + + /// Creates an assignment expression. + /// - Parameters: + /// - target: The variable to assign to. + /// - value: The value to assign. + public init(_ target: String, _ value: String) { + self.target = target + self.value = value + } + public var syntax: SyntaxProtocol { + let left = ExprSyntax(DeclReferenceExprSyntax(baseName: .identifier(target))) + let right: ExprSyntax + if value.hasPrefix("\"") && value.hasSuffix("\"") || value.contains("\\(") { + right = ExprSyntax( + StringLiteralExprSyntax( + openingQuote: .stringQuoteToken(), + segments: StringLiteralSegmentListSyntax([ + .stringSegment( + StringSegmentSyntax(content: .stringSegment(String(value.dropFirst().dropLast())))) + ]), + closingQuote: .stringQuoteToken() + )) + } else { + right = ExprSyntax(DeclReferenceExprSyntax(baseName: .identifier(value))) + } + let assign = ExprSyntax( + AssignmentExprSyntax(equal: .equalToken(leadingTrivia: .space, trailingTrivia: .space))) + return SequenceExprSyntax( + elements: ExprListSyntax([ + left, + assign, + right, + ]) + ) + } +} diff --git a/Sources/SyntaxKit/Case.swift b/Sources/SyntaxKit/Case.swift new file mode 100644 index 0000000..2ff34da --- /dev/null +++ b/Sources/SyntaxKit/Case.swift @@ -0,0 +1,79 @@ +// +// Case.swift +// SyntaxKit +// +// Created by Leo Dion. +// Copyright © 2025 BrightDigit. +// +// Permission is hereby granted, free of charge, to any person +// obtaining a copy of this software and associated documentation +// files (the “Software”), to deal in the Software without +// restriction, including without limitation the rights to use, +// copy, modify, merge, publish, distribute, sublicense, and/or +// sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following +// conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +// OTHER DEALINGS IN THE SOFTWARE. +// + +import SwiftSyntax + +/// A `case` in a `switch` statement with tuple-style patterns. +public struct Case: CodeBlock { + private let patterns: [String] + private let body: [CodeBlock] + + /// Creates a `case` for a `switch` statement. + /// - Parameters: + /// - patterns: The patterns to match for the case. + /// - content: A ``CodeBlockBuilder`` that provides the body of the case. + public init(_ patterns: String..., @CodeBlockBuilderResult content: () -> [CodeBlock]) { + self.patterns = patterns + self.body = content() + } + + public var switchCaseSyntax: SwitchCaseSyntax { + let patternList = TuplePatternElementListSyntax( + patterns.map { + TuplePatternElementSyntax( + label: nil, + colon: nil, + pattern: PatternSyntax(IdentifierPatternSyntax(identifier: .identifier($0))) + ) + } + ) + let caseItems = SwitchCaseItemListSyntax([ + SwitchCaseItemSyntax( + pattern: TuplePatternSyntax( + leftParen: .leftParenToken(), + elements: patternList, + rightParen: .rightParenToken() + ) + ) + ]) + let statements = CodeBlockItemListSyntax( + body.compactMap { $0.syntax.as(CodeBlockItemSyntax.self) }) + let label = SwitchCaseLabelSyntax( + caseKeyword: .keyword(.case, trailingTrivia: .space), + caseItems: caseItems, + colon: .colonToken() + ) + return SwitchCaseSyntax( + label: .case(label), + statements: statements + ) + } + + public var syntax: SyntaxProtocol { switchCaseSyntax } +} diff --git a/Sources/SyntaxKit/CodeBlock+Generate.swift b/Sources/SyntaxKit/CodeBlock+Generate.swift new file mode 100644 index 0000000..c912232 --- /dev/null +++ b/Sources/SyntaxKit/CodeBlock+Generate.swift @@ -0,0 +1,60 @@ +// +// CodeBlock+Generate.swift +// SyntaxKit +// +// Created by Leo Dion. +// Copyright © 2025 BrightDigit. +// +// Permission is hereby granted, free of charge, to any person +// obtaining a copy of this software and associated documentation +// files (the “Software”), to deal in the Software without +// restriction, including without limitation the rights to use, +// copy, modify, merge, publish, distribute, sublicense, and/or +// sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following +// conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +// OTHER DEALINGS IN THE SOFTWARE. +// + +import Foundation +import SwiftSyntax + +extension CodeBlock { + /// Generates the Swift code for the ``CodeBlock``. + /// - Returns: The generated Swift code as a string. + public func generateCode() -> String { + let statements: CodeBlockItemListSyntax + if let list = self.syntax.as(CodeBlockItemListSyntax.self) { + statements = list + } else { + let item: CodeBlockItemSyntax.Item + if let decl = self.syntax.as(DeclSyntax.self) { + item = .decl(decl) + } else if let stmt = self.syntax.as(StmtSyntax.self) { + item = .stmt(stmt) + } else if let expr = self.syntax.as(ExprSyntax.self) { + item = .expr(expr) + } else { + fatalError( + "Unsupported syntax type at top level: \(type(of: self.syntax)) generating from \(self)") + } + statements = CodeBlockItemListSyntax([ + CodeBlockItemSyntax(item: item, trailingTrivia: .newline) + ]) + } + + let sourceFile = SourceFileSyntax(statements: statements) + return sourceFile.description.trimmingCharacters(in: .whitespacesAndNewlines) + } +} diff --git a/Sources/SyntaxKit/CodeBlock.swift b/Sources/SyntaxKit/CodeBlock.swift new file mode 100644 index 0000000..086e2a1 --- /dev/null +++ b/Sources/SyntaxKit/CodeBlock.swift @@ -0,0 +1,82 @@ +// +// CodeBlock.swift +// SyntaxKit +// +// Created by Leo Dion. +// Copyright © 2025 BrightDigit. +// +// Permission is hereby granted, free of charge, to any person +// obtaining a copy of this software and associated documentation +// files (the “Software”), to deal in the Software without +// restriction, including without limitation the rights to use, +// copy, modify, merge, publish, distribute, sublicense, and/or +// sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following +// conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +// OTHER DEALINGS IN THE SOFTWARE. +// + +import Foundation +import SwiftSyntax + +/// A protocol for types that can be represented as a SwiftSyntax node. +public protocol CodeBlock { + /// The SwiftSyntax representation of the code block. + var syntax: SyntaxProtocol { get } +} + +/// A protocol for types that can build a ``CodeBlock``. +public protocol CodeBlockBuilder { + /// The type of ``CodeBlock`` that this builder creates. + associatedtype Result: CodeBlock + /// Builds the ``CodeBlock``. + func build() -> Result +} + +/// A result builder for creating arrays of ``CodeBlock``s. +@resultBuilder +public enum CodeBlockBuilderResult { + /// Builds a block of ``CodeBlock``s. + public static func buildBlock(_ components: CodeBlock...) -> [CodeBlock] { + components + } + + /// Builds an optional ``CodeBlock``. + public static func buildOptional(_ component: CodeBlock?) -> CodeBlock { + component ?? EmptyCodeBlock() + } + + /// Builds a ``CodeBlock`` from an `if` statement. + public static func buildEither(first: CodeBlock) -> CodeBlock { + first + } + + /// Builds a ``CodeBlock`` from an `else` statement. + public static func buildEither(second: CodeBlock) -> CodeBlock { + second + } + + /// Builds an array of ``CodeBlock``s from a `for` loop. + public static func buildArray(_ components: [CodeBlock]) -> [CodeBlock] { + components + } +} + +/// An empty ``CodeBlock``. +public struct EmptyCodeBlock: CodeBlock { + /// The syntax for an empty code block. + public var syntax: SyntaxProtocol { + StringSegmentSyntax(content: .unknown("")) + } +} diff --git a/Sources/SyntaxKit/CommentBuilderResult.swift b/Sources/SyntaxKit/CommentBuilderResult.swift new file mode 100644 index 0000000..609be94 --- /dev/null +++ b/Sources/SyntaxKit/CommentBuilderResult.swift @@ -0,0 +1,57 @@ +// +// CommentBuilderResult.swift +// SyntaxKit +// +// Created by Leo Dion. +// Copyright © 2025 BrightDigit. +// +// Permission is hereby granted, free of charge, to any person +// obtaining a copy of this software and associated documentation +// files (the “Software”), to deal in the Software without +// restriction, including without limitation the rights to use, +// copy, modify, merge, publish, distribute, sublicense, and/or +// sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following +// conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +// OTHER DEALINGS IN THE SOFTWARE. +// + +/// A result builder for creating arrays of ``Line``s for comments. +@resultBuilder +public enum CommentBuilderResult { + /// Builds a block of ``Line``s. + public static func buildBlock(_ components: Line...) -> [Line] { components } +} + +// MARK: - Public DSL surface + +extension CodeBlock { + /// Attaches comments to the current ``CodeBlock``. + /// + /// The provided lines are injected as leading trivia to the declaration produced by this ``CodeBlock``. + /// + /// Usage: + /// ```swift + /// Struct("MyStruct") { ... } + /// .comment { + /// Line("MARK: - Models") + /// Line(.doc, "This is a documentation comment") + /// } + /// ``` + /// - Parameter content: A ``CommentBuilderResult`` that provides the comment lines. + /// - Returns: A new ``CodeBlock`` with the comments attached. + public func comment(@CommentBuilderResult _ content: () -> [Line]) -> CodeBlock { + CommentedCodeBlock(base: self, lines: content()) + } +} diff --git a/Sources/SyntaxKit/CommentedCodeBlock.swift b/Sources/SyntaxKit/CommentedCodeBlock.swift new file mode 100644 index 0000000..351a5df --- /dev/null +++ b/Sources/SyntaxKit/CommentedCodeBlock.swift @@ -0,0 +1,70 @@ +// +// CommentedCodeBlock.swift +// SyntaxKit +// +// Created by Leo Dion. +// Copyright © 2025 BrightDigit. +// +// Permission is hereby granted, free of charge, to any person +// obtaining a copy of this software and associated documentation +// files (the “Software”), to deal in the Software without +// restriction, including without limitation the rights to use, +// copy, modify, merge, publish, distribute, sublicense, and/or +// sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following +// conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +// OTHER DEALINGS IN THE SOFTWARE. +// + +import Foundation +import SwiftSyntax + +// MARK: - Wrapper `CodeBlock` that injects leading trivia + +internal struct CommentedCodeBlock: CodeBlock { + let base: CodeBlock + let lines: [Line] + + var syntax: SyntaxProtocol { + // Shortcut if there are no comment lines + guard !lines.isEmpty else { return base.syntax } + + let commentTrivia = Trivia(pieces: lines.flatMap { [$0.triviaPiece, TriviaPiece.newlines(1)] }) + + // Re-write the first token of the underlying syntax node to prepend the trivia. + final class FirstTokenRewriter: SyntaxRewriter { + let newToken: TokenSyntax + private var replaced = false + init(newToken: TokenSyntax) { self.newToken = newToken } + override func visit(_ token: TokenSyntax) -> TokenSyntax { + if !replaced { + replaced = true + return newToken + } + return token + } + } + + guard let firstToken = base.syntax.firstToken(viewMode: .sourceAccurate) else { + // Fallback – no tokens? return original syntax + return base.syntax + } + + let newFirstToken = firstToken.with(\.leadingTrivia, commentTrivia + firstToken.leadingTrivia) + + let rewriter = FirstTokenRewriter(newToken: newFirstToken) + let rewritten = rewriter.visit(Syntax(base.syntax)) + return rewritten + } +} diff --git a/Sources/SyntaxKit/ComputedProperty.swift b/Sources/SyntaxKit/ComputedProperty.swift new file mode 100644 index 0000000..d78e040 --- /dev/null +++ b/Sources/SyntaxKit/ComputedProperty.swift @@ -0,0 +1,83 @@ +// +// ComputedProperty.swift +// SyntaxKit +// +// Created by Leo Dion. +// Copyright © 2025 BrightDigit. +// +// Permission is hereby granted, free of charge, to any person +// obtaining a copy of this software and associated documentation +// files (the “Software”), to deal in the Software without +// restriction, including without limitation the rights to use, +// copy, modify, merge, publish, distribute, sublicense, and/or +// sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following +// conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +// OTHER DEALINGS IN THE SOFTWARE. +// + +import SwiftSyntax + +/// A Swift `var` declaration with a computed value. +public struct ComputedProperty: CodeBlock { + private let name: String + private let type: String + private let body: [CodeBlock] + + /// Creates a computed property declaration. + /// - Parameters: + /// - name: The name of the property. + /// - type: The type of the property. + /// - content: A ``CodeBlockBuilder`` that provides the body of the getter. + public init(_ name: String, type: String, @CodeBlockBuilderResult _ content: () -> [CodeBlock]) { + self.name = name + self.type = type + self.body = content() + } + + public var syntax: SyntaxProtocol { + let accessor = AccessorBlockSyntax( + leftBrace: TokenSyntax.leftBraceToken(leadingTrivia: .space, trailingTrivia: .newline), + accessors: .getter( + CodeBlockItemListSyntax( + body.compactMap { + var item: CodeBlockItemSyntax? + if let decl = $0.syntax.as(DeclSyntax.self) { + item = CodeBlockItemSyntax(item: .decl(decl)) + } else if let expr = $0.syntax.as(ExprSyntax.self) { + item = CodeBlockItemSyntax(item: .expr(expr)) + } else if let stmt = $0.syntax.as(StmtSyntax.self) { + item = CodeBlockItemSyntax(item: .stmt(stmt)) + } + return item?.with(\.trailingTrivia, .newline) + })), + rightBrace: TokenSyntax.rightBraceToken(leadingTrivia: .newline) + ) + let identifier = TokenSyntax.identifier(name, trailingTrivia: .space) + let typeAnnotation = TypeAnnotationSyntax( + colon: TokenSyntax.colonToken(leadingTrivia: .space, trailingTrivia: .space), + type: IdentifierTypeSyntax(name: .identifier(type)) + ) + return VariableDeclSyntax( + bindingSpecifier: TokenSyntax.keyword(.var, trailingTrivia: .space), + bindings: PatternBindingListSyntax([ + PatternBindingSyntax( + pattern: IdentifierPatternSyntax(identifier: identifier), + typeAnnotation: typeAnnotation, + accessorBlock: accessor + ) + ]) + ) + } +} diff --git a/Sources/SyntaxKit/Default.swift b/Sources/SyntaxKit/Default.swift new file mode 100644 index 0000000..470a7e9 --- /dev/null +++ b/Sources/SyntaxKit/Default.swift @@ -0,0 +1,64 @@ +// +// Default.swift +// SyntaxKit +// +// Created by Leo Dion. +// Copyright © 2025 BrightDigit. +// +// Permission is hereby granted, free of charge, to any person +// obtaining a copy of this software and associated documentation +// files (the “Software”), to deal in the Software without +// restriction, including without limitation the rights to use, +// copy, modify, merge, publish, distribute, sublicense, and/or +// sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following +// conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +// OTHER DEALINGS IN THE SOFTWARE. +// + +import SwiftSyntax + +/// A `default` case in a `switch` statement. +public struct Default: CodeBlock { + private let body: [CodeBlock] + + /// Creates a `default` case for a `switch` statement. + /// - Parameter content: A ``CodeBlockBuilder`` that provides the body of the case. + public init(@CodeBlockBuilderResult _ content: () -> [CodeBlock]) { + self.body = content() + } + public var switchCaseSyntax: SwitchCaseSyntax { + let statements = CodeBlockItemListSyntax( + body.compactMap { + var item: CodeBlockItemSyntax? + if let decl = $0.syntax.as(DeclSyntax.self) { + item = CodeBlockItemSyntax(item: .decl(decl)) + } else if let expr = $0.syntax.as(ExprSyntax.self) { + item = CodeBlockItemSyntax(item: .expr(expr)) + } else if let stmt = $0.syntax.as(StmtSyntax.self) { + item = CodeBlockItemSyntax(item: .stmt(stmt)) + } + return item?.with(\.trailingTrivia, .newline) + }) + let label = SwitchDefaultLabelSyntax( + defaultKeyword: .keyword(.default, trailingTrivia: .space), + colon: .colonToken() + ) + return SwitchCaseSyntax( + label: .default(label), + statements: statements + ) + } + public var syntax: SyntaxProtocol { switchCaseSyntax } +} diff --git a/Sources/SyntaxKit/Documentation.docc/Documentation.md b/Sources/SyntaxKit/Documentation.docc/Documentation.md new file mode 100644 index 0000000..091cbee --- /dev/null +++ b/Sources/SyntaxKit/Documentation.docc/Documentation.md @@ -0,0 +1,218 @@ +# ``SyntaxKit`` + +SyntaxKit provides a declarative way to generate Swift code structures using SwiftSyntax. + +## Overview + +SyntaxKit allows developers to build Swift code using result builders which enable the creation of Swift code structures in a declarative way. Here's an example: + +```swift +import SyntaxKit + +let code = Struct("BlackjackCard") { + Enum("Suit") { + EnumCase("spades").equals("♠") + EnumCase("hearts").equals("♡") + EnumCase("diamonds").equals("♢") + EnumCase("clubs").equals("♣") + } + .inherits("Character") + .comment("nested Suit enumeration") +} + +let generatedCode = code.generateCode() +``` + +This will generate the following Swift code: + +```swift +struct BlackjackCard { + // nested Suit enumeration + enum Suit: Character { + case spades = "♠" + case hearts = "♡" + case diamonds = "♢" + case clubs = "♣" + } +} +``` + +## Full Example + +Here is a more comprehensive example that demonstrates many of SyntaxKit's features to generate a `BlackjackCard` struct. + +### DSL Code + +```swift +import SyntaxKit + +let structExample = Struct("BlackjackCard") { + Enum("Suit") { + EnumCase("spades").equals("♠") + EnumCase("hearts").equals("♡") + EnumCase("diamonds").equals("♢") + EnumCase("clubs").equals("♣") + } + .inherits("Character") + .comment("nested Suit enumeration") + + Enum("Rank") { + EnumCase("two").equals(2) + EnumCase("three") + EnumCase("four") + EnumCase("five") + EnumCase("six") + EnumCase("seven") + EnumCase("eight") + EnumCase("nine") + EnumCase("ten") + EnumCase("jack") + EnumCase("queen") + EnumCase("king") + EnumCase("ace") + + Struct("Values") { + Variable(.let, name: "first", type: "Int") + Variable(.let, name: "second", type: "Int?") + } + + ComputedProperty("values") { + Switch("self") { + SwitchCase(".ace") { + Return { + Init("Values") { + Parameter(name: "first", value: "1") + Parameter(name: "second", value: "11") + } + } + } + SwitchCase(".jack", ".queen", ".king") { + Return { + Init("Values") { + Parameter(name: "first", value: "10") + Parameter(name: "second", value: "nil") + } + } + } + Default { + Return { + Init("Values") { + Parameter(name: "first", value: "self.rawValue") + Parameter(name: "second", value: "nil") + } + } + } + } + } + } + .inherits("Int") + .comment("nested Rank enumeration") + + Variable(.let, name: "rank", type: "Rank") + Variable(.let, name: "suit", type: "Suit") + .comment("BlackjackCard properties and methods") + + ComputedProperty("description") { + VariableDecl(.var, name: "output", equals: "\"suit is \\(suit.rawValue),\"") + PlusAssign("output", "\" value is \\(rank.values.first)\"") + If(Let("second", "rank.values.second"), then: { + PlusAssign("output", "\" or \\(second)\"") + }) + Return { + VariableExp("output") + } + } +} +``` + +### Generated Code + +```swift +import Foundation + +struct BlackjackCard { + // nested Suit enumeration + enum Suit: Character { + case spades = "♠" + case hearts = "♡" + case diamonds = "♢" + case clubs = "♣" + } + + // nested Rank enumeration + enum Rank: Int { + case two = 2 + case three + case four + case five + case six + case seven + case eight + case nine + case ten + case jack + case queen + case king + case ace + + struct Values { + let first: Int, second: Int? + } + + var values: Values { + switch self { + case .ace: + return Values(first: 1, second: 11) + case .jack, .queen, .king: + return Values(first: 10, second: nil) + default: + return Values(first: self.rawValue, second: nil) + } + } + } + + // BlackjackCard properties and methods + let rank: Rank + let suit: Suit + var description: String { + var output = "suit is \\(suit.rawValue)," + output += " value is \\(rank.values.first)" + if let second = rank.values.second { + output += " or \\(second)" + } + return output + } +} +``` + +## Topics + +### Declarations + +- ``Struct`` +- ``Enum`` +- ``EnumCase`` +- ``Function`` +- ``Init`` +- ``ComputedProperty`` +- ``VariableDecl`` +- ``Let`` +- ``Variable`` + +### Expressions & Statements +- ``Assignment`` +- ``PlusAssign`` +- ``Return`` +- ``VariableExp`` + +### Control Flow +- ``If`` +- ``Switch`` +- ``SwitchCase`` +- ``Default`` + +### Building Blocks +- ``CodeBlock`` +- ``Parameter`` +- ``Literal`` + diff --git a/Sources/SyntaxKit/Enum.swift b/Sources/SyntaxKit/Enum.swift new file mode 100644 index 0000000..170f78b --- /dev/null +++ b/Sources/SyntaxKit/Enum.swift @@ -0,0 +1,160 @@ +// +// Enum.swift +// SyntaxKit +// +// Created by Leo Dion. +// Copyright © 2025 BrightDigit. +// +// Permission is hereby granted, free of charge, to any person +// obtaining a copy of this software and associated documentation +// files (the “Software”), to deal in the Software without +// restriction, including without limitation the rights to use, +// copy, modify, merge, publish, distribute, sublicense, and/or +// sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following +// conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +// OTHER DEALINGS IN THE SOFTWARE. +// + +import SwiftSyntax + +/// A Swift `enum` declaration. +public struct Enum: CodeBlock { + private let name: String + private let members: [CodeBlock] + private var inheritance: String? + + /// Creates an `enum` declaration. + /// - Parameters: + /// - name: The name of the enum. + /// - content: A ``CodeBlockBuilder`` that provides the members of the enum. + public init(_ name: String, @CodeBlockBuilderResult _ content: () -> [CodeBlock]) { + self.name = name + self.members = content() + } + + /// Sets the inheritance for the enum. + /// - Parameter type: The type to inherit from. + /// - Returns: A copy of the enum with the inheritance set. + public func inherits(_ type: String) -> Self { + var copy = self + copy.inheritance = type + return copy + } + + public var syntax: SyntaxProtocol { + let enumKeyword = TokenSyntax.keyword(.enum, trailingTrivia: .space) + let identifier = TokenSyntax.identifier(name, trailingTrivia: .space) + + var inheritanceClause: InheritanceClauseSyntax? + if let inheritance = inheritance { + let inheritedType = InheritedTypeSyntax( + type: IdentifierTypeSyntax(name: .identifier(inheritance))) + inheritanceClause = InheritanceClauseSyntax( + colon: .colonToken(), inheritedTypes: InheritedTypeListSyntax([inheritedType])) + } + + let memberBlock = MemberBlockSyntax( + leftBrace: .leftBraceToken(leadingTrivia: .space, trailingTrivia: .newline), + members: MemberBlockItemListSyntax( + members.compactMap { member in + guard let syntax = member.syntax.as(DeclSyntax.self) else { return nil } + return MemberBlockItemSyntax(decl: syntax, trailingTrivia: .newline) + }), + rightBrace: .rightBraceToken(leadingTrivia: .newline) + ) + + return EnumDeclSyntax( + enumKeyword: enumKeyword, + name: identifier, + inheritanceClause: inheritanceClause, + memberBlock: memberBlock + ) + } +} + +/// A Swift `case` declaration inside an `enum`. +public struct EnumCase: CodeBlock { + private let name: String + private var value: String? + private var intValue: Int? + + /// Creates a `case` declaration. + /// - Parameter name: The name of the case. + public init(_ name: String) { + self.name = name + } + + /// Sets the raw value of the case to a string. + /// - Parameter value: The string value. + /// - Returns: A copy of the case with the raw value set. + public func equals(_ value: String) -> Self { + var copy = self + copy.value = value + copy.intValue = nil + return copy + } + + /// Sets the raw value of the case to an integer. + /// - Parameter value: The integer value. + /// - Returns: A copy of the case with the raw value set. + public func equals(_ value: Int) -> Self { + var copy = self + copy.value = nil + copy.intValue = value + return copy + } + + public var syntax: SyntaxProtocol { + let caseKeyword = TokenSyntax.keyword(.case, trailingTrivia: .space) + let identifier = TokenSyntax.identifier(name, trailingTrivia: .space) + + var initializer: InitializerClauseSyntax? + if let value = value { + initializer = InitializerClauseSyntax( + equal: .equalToken(leadingTrivia: .space, trailingTrivia: .space), + value: StringLiteralExprSyntax( + openingQuote: .stringQuoteToken(), + segments: StringLiteralSegmentListSyntax([ + .stringSegment(StringSegmentSyntax(content: .stringSegment(value))) + ]), + closingQuote: .stringQuoteToken() + ) + ) + } else if let intValue = intValue { + initializer = InitializerClauseSyntax( + equal: .equalToken(leadingTrivia: .space, trailingTrivia: .space), + value: IntegerLiteralExprSyntax(digits: .integerLiteral(String(intValue))) + ) + } + + return EnumCaseDeclSyntax( + caseKeyword: caseKeyword, + elements: EnumCaseElementListSyntax([ + EnumCaseElementSyntax( + leadingTrivia: .space, + _: nil, + name: identifier, + _: nil, + parameterClause: nil, + _: nil, + rawValue: initializer, + _: nil, + trailingComma: nil, + trailingTrivia: .newline + ) + ]) + ) + } +} diff --git a/Sources/SyntaxKit/Function.swift b/Sources/SyntaxKit/Function.swift new file mode 100644 index 0000000..bac906f --- /dev/null +++ b/Sources/SyntaxKit/Function.swift @@ -0,0 +1,181 @@ +// +// Function.swift +// SyntaxKit +// +// Created by Leo Dion. +// Copyright © 2025 BrightDigit. +// +// Permission is hereby granted, free of charge, to any person +// obtaining a copy of this software and associated documentation +// files (the “Software”), to deal in the Software without +// restriction, including without limitation the rights to use, +// copy, modify, merge, publish, distribute, sublicense, and/or +// sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following +// conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +// OTHER DEALINGS IN THE SOFTWARE. +// + +import SwiftSyntax + +/// A Swift `func` declaration. +public struct Function: CodeBlock { + private let name: String + private let parameters: [Parameter] + private let returnType: String? + private let body: [CodeBlock] + private var isStatic: Bool = false + private var isMutating: Bool = false + + /// Creates a `func` declaration. + /// - Parameters: + /// - name: The name of the function. + /// - returnType: The return type of the function, if any. + /// - content: A ``CodeBlockBuilder`` that provides the body of the function. + public init( + _ name: String, returns returnType: String? = nil, + @CodeBlockBuilderResult _ content: () -> [CodeBlock] + ) { + self.name = name + self.parameters = [] + self.returnType = returnType + self.body = content() + } + + /// Creates a `func` declaration. + /// - Parameters: + /// - name: The name of the function. + /// - returnType: The return type of the function, if any. + /// - params: A ``ParameterBuilder`` that provides the parameters of the function. + /// - content: A ``CodeBlockBuilder`` that provides the body of the function. + public init( + _ name: String, returns returnType: String? = nil, + @ParameterBuilderResult _ params: () -> [Parameter], + @CodeBlockBuilderResult _ content: () -> [CodeBlock] + ) { + self.name = name + self.parameters = params() + self.returnType = returnType + self.body = content() + } + + /// Marks the function as `static`. + /// - Returns: A copy of the function marked as `static`. + public func `static`() -> Self { + var copy = self + copy.isStatic = true + return copy + } + + /// Marks the function as `mutating`. + /// - Returns: A copy of the function marked as `mutating`. + public func mutating() -> Self { + var copy = self + copy.isMutating = true + return copy + } + + public var syntax: SyntaxProtocol { + let funcKeyword = TokenSyntax.keyword(.func, trailingTrivia: .space) + let identifier = TokenSyntax.identifier(name) + + // Build parameter list + let paramList: FunctionParameterListSyntax + if parameters.isEmpty { + paramList = FunctionParameterListSyntax([]) + } else { + paramList = FunctionParameterListSyntax( + parameters.enumerated().compactMap { index, param in + guard !param.name.isEmpty, !param.type.isEmpty else { return nil } + var paramSyntax = FunctionParameterSyntax( + firstName: param.isUnnamed + ? .wildcardToken(trailingTrivia: .space) : .identifier(param.name), + secondName: param.isUnnamed ? .identifier(param.name) : nil, + colon: .colonToken(leadingTrivia: .space, trailingTrivia: .space), + type: IdentifierTypeSyntax(name: .identifier(param.type)), + defaultValue: param.defaultValue.map { + InitializerClauseSyntax( + equal: .equalToken(leadingTrivia: .space, trailingTrivia: .space), + value: ExprSyntax(DeclReferenceExprSyntax(baseName: .identifier($0))) + ) + } + ) + if index < parameters.count - 1 { + paramSyntax = paramSyntax.with(\.trailingComma, .commaToken(trailingTrivia: .space)) + } + return paramSyntax + }) + } + + // Build return type if specified + var returnClause: ReturnClauseSyntax? + if let returnType = returnType { + returnClause = ReturnClauseSyntax( + arrow: .arrowToken(leadingTrivia: .space, trailingTrivia: .space), + type: IdentifierTypeSyntax(name: .identifier(returnType)) + ) + } + + // Build function body + let bodyBlock = CodeBlockSyntax( + leftBrace: .leftBraceToken(leadingTrivia: .space, trailingTrivia: .newline), + statements: CodeBlockItemListSyntax( + body.compactMap { + var item: CodeBlockItemSyntax? + if let decl = $0.syntax.as(DeclSyntax.self) { + item = CodeBlockItemSyntax(item: .decl(decl)) + } else if let expr = $0.syntax.as(ExprSyntax.self) { + item = CodeBlockItemSyntax(item: .expr(expr)) + } else if let stmt = $0.syntax.as(StmtSyntax.self) { + item = CodeBlockItemSyntax(item: .stmt(stmt)) + } + return item?.with(\.trailingTrivia, .newline) + }), + rightBrace: .rightBraceToken(leadingTrivia: .newline) + ) + + // Build modifiers + var modifiers: DeclModifierListSyntax = [] + if isStatic { + modifiers = DeclModifierListSyntax([ + DeclModifierSyntax(name: .keyword(.static, trailingTrivia: .space)) + ]) + } + if isMutating { + modifiers = DeclModifierListSyntax( + modifiers + [ + DeclModifierSyntax(name: .keyword(.mutating, trailingTrivia: .space)) + ]) + } + + return FunctionDeclSyntax( + attributes: AttributeListSyntax([]), + modifiers: modifiers, + funcKeyword: funcKeyword, + name: identifier, + genericParameterClause: nil, + signature: FunctionSignatureSyntax( + parameterClause: FunctionParameterClauseSyntax( + leftParen: .leftParenToken(), + parameters: paramList, + rightParen: .rightParenToken() + ), + effectSpecifiers: nil, + returnClause: returnClause + ), + genericWhereClause: nil, + body: bodyBlock + ) + } +} diff --git a/Sources/SyntaxKit/Group.swift b/Sources/SyntaxKit/Group.swift new file mode 100644 index 0000000..7b3e824 --- /dev/null +++ b/Sources/SyntaxKit/Group.swift @@ -0,0 +1,62 @@ +// +// Group.swift +// SyntaxKit +// +// Created by Leo Dion. +// Copyright © 2025 BrightDigit. +// +// Permission is hereby granted, free of charge, to any person +// obtaining a copy of this software and associated documentation +// files (the “Software”), to deal in the Software without +// restriction, including without limitation the rights to use, +// copy, modify, merge, publish, distribute, sublicense, and/or +// sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following +// conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +// OTHER DEALINGS IN THE SOFTWARE. +// + +import SwiftSyntax + +/// A group of code blocks. +public struct Group: CodeBlock { + let members: [CodeBlock] + + /// Creates a group of code blocks. + /// - Parameter content: A ``CodeBlockBuilder`` that provides the members of the group. + public init(@CodeBlockBuilderResult _ content: () -> [CodeBlock]) { + self.members = content() + } + + public var syntax: SyntaxProtocol { + let statements = members.flatMap { block -> [CodeBlockItemSyntax] in + if let list = block.syntax.as(CodeBlockItemListSyntax.self) { + return Array(list) + } + + let item: CodeBlockItemSyntax.Item + if let decl = block.syntax.as(DeclSyntax.self) { + item = .decl(decl) + } else if let stmt = block.syntax.as(StmtSyntax.self) { + item = .stmt(stmt) + } else if let expr = block.syntax.as(ExprSyntax.self) { + item = .expr(expr) + } else { + fatalError("Unsupported syntax type in group: \(type(of: block.syntax)) from \(block)") + } + return [CodeBlockItemSyntax(item: item, trailingTrivia: .newline)] + } + return CodeBlockItemListSyntax(statements) + } +} diff --git a/Sources/SyntaxKit/If.swift b/Sources/SyntaxKit/If.swift new file mode 100644 index 0000000..53e7bd5 --- /dev/null +++ b/Sources/SyntaxKit/If.swift @@ -0,0 +1,120 @@ +// +// If.swift +// SyntaxKit +// +// Created by Leo Dion. +// Copyright © 2025 BrightDigit. +// +// Permission is hereby granted, free of charge, to any person +// obtaining a copy of this software and associated documentation +// files (the “Software”), to deal in the Software without +// restriction, including without limitation the rights to use, +// copy, modify, merge, publish, distribute, sublicense, and/or +// sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following +// conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +// OTHER DEALINGS IN THE SOFTWARE. +// + +import SwiftSyntax + +/// An `if` statement. +public struct If: CodeBlock { + private let condition: CodeBlock + private let body: [CodeBlock] + private let elseBody: [CodeBlock]? + + /// Creates an `if` statement. + /// - Parameters: + /// - condition: The condition to evaluate. This can be a ``Let`` for optional binding. + /// - then: A ``CodeBlockBuilder`` that provides the body of the `if` block. + /// - elseBody: A ``CodeBlockBuilder`` that provides the body of the `else` block, if any. + public init( + _ condition: CodeBlock, @CodeBlockBuilderResult then: () -> [CodeBlock], + else elseBody: (() -> [CodeBlock])? = nil + ) { + self.condition = condition + self.body = then() + self.elseBody = elseBody?() + } + + public var syntax: SyntaxProtocol { + let cond: ConditionElementSyntax + if let letCond = condition as? Let { + cond = ConditionElementSyntax( + condition: .optionalBinding( + OptionalBindingConditionSyntax( + bindingSpecifier: .keyword(.let, trailingTrivia: .space), + pattern: IdentifierPatternSyntax(identifier: .identifier(letCond.name)), + initializer: InitializerClauseSyntax( + equal: .equalToken(leadingTrivia: .space, trailingTrivia: .space), + value: ExprSyntax(DeclReferenceExprSyntax(baseName: .identifier(letCond.value))) + ) + ) + ) + ) + } else { + cond = ConditionElementSyntax( + condition: .expression( + ExprSyntax( + fromProtocol: condition.syntax.as(ExprSyntax.self) + ?? DeclReferenceExprSyntax(baseName: .identifier("")))) + ) + } + let bodyBlock = CodeBlockSyntax( + leftBrace: .leftBraceToken(leadingTrivia: .space, trailingTrivia: .newline), + statements: CodeBlockItemListSyntax( + body.compactMap { + var item: CodeBlockItemSyntax? + if let decl = $0.syntax.as(DeclSyntax.self) { + item = CodeBlockItemSyntax(item: .decl(decl)) + } else if let expr = $0.syntax.as(ExprSyntax.self) { + item = CodeBlockItemSyntax(item: .expr(expr)) + } else if let stmt = $0.syntax.as(StmtSyntax.self) { + item = CodeBlockItemSyntax(item: .stmt(stmt)) + } + return item?.with(\.trailingTrivia, .newline) + }), + rightBrace: .rightBraceToken(leadingTrivia: .newline) + ) + let elseBlock = elseBody.map { + IfExprSyntax.ElseBody( + CodeBlockSyntax( + leftBrace: .leftBraceToken(leadingTrivia: .space, trailingTrivia: .newline), + statements: CodeBlockItemListSyntax( + $0.compactMap { + var item: CodeBlockItemSyntax? + if let decl = $0.syntax.as(DeclSyntax.self) { + item = CodeBlockItemSyntax(item: .decl(decl)) + } else if let expr = $0.syntax.as(ExprSyntax.self) { + item = CodeBlockItemSyntax(item: .expr(expr)) + } else if let stmt = $0.syntax.as(StmtSyntax.self) { + item = CodeBlockItemSyntax(item: .stmt(stmt)) + } + return item?.with(\.trailingTrivia, .newline) + }), + rightBrace: .rightBraceToken(leadingTrivia: .newline) + )) + } + return ExprSyntax( + IfExprSyntax( + ifKeyword: .keyword(.if, trailingTrivia: .space), + conditions: ConditionElementListSyntax([cond]), + body: bodyBlock, + elseKeyword: elseBlock != nil ? .keyword(.else, trailingTrivia: .space) : nil, + elseBody: elseBlock + ) + ) + } +} diff --git a/Sources/SyntaxKit/Init.swift b/Sources/SyntaxKit/Init.swift new file mode 100644 index 0000000..c94a9e8 --- /dev/null +++ b/Sources/SyntaxKit/Init.swift @@ -0,0 +1,64 @@ +// +// Init.swift +// SyntaxKit +// +// Created by Leo Dion. +// Copyright © 2025 BrightDigit. +// +// Permission is hereby granted, free of charge, to any person +// obtaining a copy of this software and associated documentation +// files (the “Software”), to deal in the Software without +// restriction, including without limitation the rights to use, +// copy, modify, merge, publish, distribute, sublicense, and/or +// sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following +// conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +// OTHER DEALINGS IN THE SOFTWARE. +// + +import SwiftSyntax + +/// An initializer expression. +public struct Init: CodeBlock { + private let type: String + private let parameters: [Parameter] + + /// Creates an initializer expression. + /// - Parameters: + /// - type: The type to initialize. + /// - params: A ``ParameterBuilder`` that provides the parameters for the initializer. + public init(_ type: String, @ParameterBuilderResult _ params: () -> [Parameter]) { + self.type = type + self.parameters = params() + } + public var syntax: SyntaxProtocol { + let args = TupleExprElementListSyntax( + parameters.enumerated().compactMap { index, param in + guard let element = param.syntax as? TupleExprElementSyntax else { + return nil + } + if index < parameters.count - 1 { + return element.with(\.trailingComma, .commaToken(trailingTrivia: .space)) + } + return element + }) + return ExprSyntax( + FunctionCallExprSyntax( + calledExpression: ExprSyntax(DeclReferenceExprSyntax(baseName: .identifier(type))), + leftParen: .leftParenToken(), + argumentList: args, + rightParen: .rightParenToken() + )) + } +} diff --git a/Sources/SyntaxKit/Let.swift b/Sources/SyntaxKit/Let.swift new file mode 100644 index 0000000..cd2d46d --- /dev/null +++ b/Sources/SyntaxKit/Let.swift @@ -0,0 +1,65 @@ +// +// Let.swift +// SyntaxKit +// +// Created by Leo Dion. +// Copyright © 2025 BrightDigit. +// +// Permission is hereby granted, free of charge, to any person +// obtaining a copy of this software and associated documentation +// files (the “Software”), to deal in the Software without +// restriction, including without limitation the rights to use, +// copy, modify, merge, publish, distribute, sublicense, and/or +// sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following +// conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +// OTHER DEALINGS IN THE SOFTWARE. +// + +import SwiftSyntax + +/// A Swift `let` declaration for use in an `if` statement. +public struct Let: CodeBlock { + let name: String + let value: String + + /// Creates a `let` declaration for an `if` statement. + /// - Parameters: + /// - name: The name of the constant. + /// - value: The value to assign to the constant. + public init(_ name: String, _ value: String) { + self.name = name + self.value = value + } + public var syntax: SyntaxProtocol { + CodeBlockItemSyntax( + item: .decl( + DeclSyntax( + VariableDeclSyntax( + bindingSpecifier: .keyword(.let, trailingTrivia: .space), + bindings: PatternBindingListSyntax([ + PatternBindingSyntax( + pattern: IdentifierPatternSyntax(identifier: .identifier(name)), + initializer: InitializerClauseSyntax( + equal: .equalToken(leadingTrivia: .space, trailingTrivia: .space), + value: ExprSyntax(DeclReferenceExprSyntax(baseName: .identifier(value))) + ) + ) + ]) + ) + ) + ) + ) + } +} diff --git a/Sources/SyntaxKit/Line.swift b/Sources/SyntaxKit/Line.swift new file mode 100644 index 0000000..3017ae6 --- /dev/null +++ b/Sources/SyntaxKit/Line.swift @@ -0,0 +1,88 @@ +// +// Line.swift +// SyntaxKit +// +// Created by Leo Dion. +// Copyright © 2025 BrightDigit. +// +// Permission is hereby granted, free of charge, to any person +// obtaining a copy of this software and associated documentation +// files (the “Software”), to deal in the Software without +// restriction, including without limitation the rights to use, +// copy, modify, merge, publish, distribute, sublicense, and/or +// sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following +// conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +// OTHER DEALINGS IN THE SOFTWARE. +// + +import SwiftSyntax + +/// Represents a single comment line that can be attached to a syntax node. +public struct Line { + /// The kind of comment line. + public enum Kind { + /// Regular line comment that starts with `//`. + case line + /// Documentation line comment that starts with `///`. + case doc + } + + /// The kind of comment. + public let kind: Kind + /// The text of the comment. + public let text: String? + + /// Creates a regular line comment. + /// - Parameter text: The text of the comment. + public init(_ text: String) { + self.kind = .line + self.text = text + } + + /// Convenience initialiser. Passing only `kind` will create an empty comment line of that kind. + /// + /// Examples: + /// ```swift + /// Line("MARK: - Models") // defaults to `.line` kind + /// Line(.doc, "Represents a model") // documentation comment + /// Line(.doc) // empty `///` line + /// ``` + /// - Parameters: + /// - kind: The kind of comment. Defaults to `.line`. + /// - text: The text of the comment. Defaults to `nil`. + public init(_ kind: Kind = .line, _ text: String? = nil) { + self.kind = kind + self.text = text + } +} + +// MARK: - Internal helpers + +extension Line { + /// Convert the `Line` to a SwiftSyntax `TriviaPiece`. + internal var triviaPiece: TriviaPiece { + switch kind { + case .line: + return .lineComment("// " + (text ?? "")) + case .doc: + // Empty doc line should still contain the comment marker so we keep a single `/` if no text. + if let text = text, !text.isEmpty { + return .docLineComment("/// " + text) + } else { + return .docLineComment("///") + } + } + } +} diff --git a/Sources/SyntaxKit/Literal.swift b/Sources/SyntaxKit/Literal.swift new file mode 100644 index 0000000..7e0e1dd --- /dev/null +++ b/Sources/SyntaxKit/Literal.swift @@ -0,0 +1,66 @@ +// +// Literal.swift +// SyntaxKit +// +// Created by Leo Dion. +// Copyright © 2025 BrightDigit. +// +// Permission is hereby granted, free of charge, to any person +// obtaining a copy of this software and associated documentation +// files (the “Software”), to deal in the Software without +// restriction, including without limitation the rights to use, +// copy, modify, merge, publish, distribute, sublicense, and/or +// sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following +// conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +// OTHER DEALINGS IN THE SOFTWARE. +// + +import SwiftSyntax + +/// A literal value. +public enum Literal: CodeBlock { + /// A string literal. + case string(String) + /// A floating-point literal. + case float(Double) + /// An integer literal. + case integer(Int) + /// A `nil` literal. + case `nil` + /// A boolean literal. + case boolean(Bool) + + public var syntax: SyntaxProtocol { + switch self { + case .string(let value): + return StringLiteralExprSyntax( + openingQuote: .stringQuoteToken(), + segments: .init([ + .stringSegment(.init(content: .stringSegment(value))) + ]), + closingQuote: .stringQuoteToken() + ) + case .float(let value): + return FloatLiteralExprSyntax(literal: .floatLiteral(String(value))) + + case .integer(let value): + return IntegerLiteralExprSyntax(digits: .integerLiteral(String(value))) + case .nil: + return NilLiteralExprSyntax(nilKeyword: .keyword(.nil)) + case .boolean(let value): + return BooleanLiteralExprSyntax(literal: value ? .keyword(.true) : .keyword(.false)) + } + } +} diff --git a/Sources/SyntaxKit/Parameter.swift b/Sources/SyntaxKit/Parameter.swift new file mode 100644 index 0000000..08c76d9 --- /dev/null +++ b/Sources/SyntaxKit/Parameter.swift @@ -0,0 +1,70 @@ +// +// Parameter.swift +// SyntaxKit +// +// Created by Leo Dion. +// Copyright © 2025 BrightDigit. +// +// Permission is hereby granted, free of charge, to any person +// obtaining a copy of this software and associated documentation +// files (the “Software”), to deal in the Software without +// restriction, including without limitation the rights to use, +// copy, modify, merge, publish, distribute, sublicense, and/or +// sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following +// conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +// OTHER DEALINGS IN THE SOFTWARE. +// + +import Foundation +import SwiftParser +import SwiftSyntax + +/// A parameter for a function or initializer. +public struct Parameter: CodeBlock { + let name: String + let type: String + let defaultValue: String? + let isUnnamed: Bool + + /// Creates a parameter for a function or initializer. + /// - Parameters: + /// - name: The name of the parameter. + /// - type: The type of the parameter. + /// - defaultValue: The default value of the parameter, if any. + /// - isUnnamed: A Boolean value that indicates whether the parameter is unnamed. + public init(name: String, type: String, defaultValue: String? = nil, isUnnamed: Bool = false) { + self.name = name + self.type = type + self.defaultValue = defaultValue + self.isUnnamed = isUnnamed + } + + public var syntax: SyntaxProtocol { + // Not used for function signature, but for call sites (Init, etc.) + if let defaultValue = defaultValue { + return LabeledExprSyntax( + label: .identifier(name), + colon: .colonToken(), + expression: ExprSyntax(DeclReferenceExprSyntax(baseName: .identifier(defaultValue))) + ) + } else { + return LabeledExprSyntax( + label: .identifier(name), + colon: .colonToken(), + expression: ExprSyntax(DeclReferenceExprSyntax(baseName: .identifier(name))) + ) + } + } +} diff --git a/Sources/SyntaxKit/ParameterBuilderResult.swift b/Sources/SyntaxKit/ParameterBuilderResult.swift new file mode 100644 index 0000000..43c641e --- /dev/null +++ b/Sources/SyntaxKit/ParameterBuilderResult.swift @@ -0,0 +1,59 @@ +// +// ParameterBuilderResult.swift +// SyntaxKit +// +// Created by Leo Dion. +// Copyright © 2025 BrightDigit. +// +// Permission is hereby granted, free of charge, to any person +// obtaining a copy of this software and associated documentation +// files (the “Software”), to deal in the Software without +// restriction, including without limitation the rights to use, +// copy, modify, merge, publish, distribute, sublicense, and/or +// sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following +// conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +// OTHER DEALINGS IN THE SOFTWARE. +// + +import Foundation + +/// A result builder for creating arrays of ``Parameter``s. +@resultBuilder +public enum ParameterBuilderResult { + /// Builds a block of ``Parameter``s. + public static func buildBlock(_ components: Parameter...) -> [Parameter] { + components + } + + /// Builds an optional ``Parameter``. + public static func buildOptional(_ component: Parameter?) -> [Parameter] { + component.map { [$0] } ?? [] + } + + /// Builds a ``Parameter`` from an `if` statement. + public static func buildEither(first: Parameter) -> [Parameter] { + [first] + } + + /// Builds a ``Parameter`` from an `else` statement. + public static func buildEither(second: Parameter) -> [Parameter] { + [second] + } + + /// Builds an array of ``Parameter``s from a `for` loop. + public static func buildArray(_ components: [Parameter]) -> [Parameter] { + components + } +} diff --git a/Sources/SyntaxKit/ParameterExp.swift b/Sources/SyntaxKit/ParameterExp.swift new file mode 100644 index 0000000..02274e8 --- /dev/null +++ b/Sources/SyntaxKit/ParameterExp.swift @@ -0,0 +1,57 @@ +// +// ParameterExp.swift +// SyntaxKit +// +// Created by Leo Dion. +// Copyright © 2025 BrightDigit. +// +// Permission is hereby granted, free of charge, to any person +// obtaining a copy of this software and associated documentation +// files (the “Software”), to deal in the Software without +// restriction, including without limitation the rights to use, +// copy, modify, merge, publish, distribute, sublicense, and/or +// sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following +// conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +// OTHER DEALINGS IN THE SOFTWARE. +// + +import SwiftSyntax + +/// A parameter for a function call. +public struct ParameterExp: CodeBlock { + let name: String + let value: String + + /// Creates a parameter for a function call. + /// - Parameters: + /// - name: The name of the parameter. + /// - value: The value of the parameter. + public init(name: String, value: String) { + self.name = name + self.value = value + } + + public var syntax: SyntaxProtocol { + if name.isEmpty { + return ExprSyntax(DeclReferenceExprSyntax(baseName: .identifier(value))) + } else { + return LabeledExprSyntax( + label: .identifier(name), + colon: .colonToken(), + expression: ExprSyntax(DeclReferenceExprSyntax(baseName: .identifier(value))) + ) + } + } +} diff --git a/Sources/SyntaxKit/ParameterExpBuilderResult.swift b/Sources/SyntaxKit/ParameterExpBuilderResult.swift new file mode 100644 index 0000000..ef98f12 --- /dev/null +++ b/Sources/SyntaxKit/ParameterExpBuilderResult.swift @@ -0,0 +1,59 @@ +// +// ParameterExpBuilderResult.swift +// SyntaxKit +// +// Created by Leo Dion. +// Copyright © 2025 BrightDigit. +// +// Permission is hereby granted, free of charge, to any person +// obtaining a copy of this software and associated documentation +// files (the “Software”), to deal in the Software without +// restriction, including without limitation the rights to use, +// copy, modify, merge, publish, distribute, sublicense, and/or +// sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following +// conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +// OTHER DEALINGS IN THE SOFTWARE. +// + +import Foundation + +/// A result builder for creating arrays of ``ParameterExp``s. +@resultBuilder +public enum ParameterExpBuilderResult { + /// Builds a block of ``ParameterExp``s. + public static func buildBlock(_ components: ParameterExp...) -> [ParameterExp] { + components + } + + /// Builds an optional ``ParameterExp``. + public static func buildOptional(_ component: ParameterExp?) -> [ParameterExp] { + component.map { [$0] } ?? [] + } + + /// Builds a ``ParameterExp`` from an `if` statement. + public static func buildEither(first: ParameterExp) -> [ParameterExp] { + [first] + } + + /// Builds a ``ParameterExp`` from an `else` statement. + public static func buildEither(second: ParameterExp) -> [ParameterExp] { + [second] + } + + /// Builds an array of ``ParameterExp``s from a `for` loop. + public static func buildArray(_ components: [ParameterExp]) -> [ParameterExp] { + components + } +} diff --git a/Sources/SyntaxKit/PlusAssign.swift b/Sources/SyntaxKit/PlusAssign.swift new file mode 100644 index 0000000..8b79cc8 --- /dev/null +++ b/Sources/SyntaxKit/PlusAssign.swift @@ -0,0 +1,73 @@ +// +// PlusAssign.swift +// SyntaxKit +// +// Created by Leo Dion. +// Copyright © 2025 BrightDigit. +// +// Permission is hereby granted, free of charge, to any person +// obtaining a copy of this software and associated documentation +// files (the “Software”), to deal in the Software without +// restriction, including without limitation the rights to use, +// copy, modify, merge, publish, distribute, sublicense, and/or +// sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following +// conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +// OTHER DEALINGS IN THE SOFTWARE. +// + +import SwiftSyntax + +/// A `+=` expression. +public struct PlusAssign: CodeBlock { + private let target: String + private let value: String + + /// Creates a `+=` expression. + /// - Parameters: + /// - target: The variable to assign to. + /// - value: The value to add and assign. + public init(_ target: String, _ value: String) { + self.target = target + self.value = value + } + + public var syntax: SyntaxProtocol { + let left = ExprSyntax(DeclReferenceExprSyntax(baseName: .identifier(target))) + let right: ExprSyntax + if value.hasPrefix("\"") && value.hasSuffix("\"") || value.contains("\\(") { + right = ExprSyntax( + StringLiteralExprSyntax( + openingQuote: .stringQuoteToken(), + segments: StringLiteralSegmentListSyntax([ + .stringSegment( + StringSegmentSyntax(content: .stringSegment(String(value.dropFirst().dropLast())))) + ]), + closingQuote: .stringQuoteToken() + )) + } else { + right = ExprSyntax(DeclReferenceExprSyntax(baseName: .identifier(value))) + } + let assign = ExprSyntax( + BinaryOperatorExprSyntax( + operator: .binaryOperator("+=", leadingTrivia: .space, trailingTrivia: .space))) + return SequenceExprSyntax( + elements: ExprListSyntax([ + left, + assign, + right, + ]) + ) + } +} diff --git a/Sources/SyntaxKit/Return.swift b/Sources/SyntaxKit/Return.swift new file mode 100644 index 0000000..78c05d4 --- /dev/null +++ b/Sources/SyntaxKit/Return.swift @@ -0,0 +1,56 @@ +// +// Return.swift +// SyntaxKit +// +// Created by Leo Dion. +// Copyright © 2025 BrightDigit. +// +// Permission is hereby granted, free of charge, to any person +// obtaining a copy of this software and associated documentation +// files (the “Software”), to deal in the Software without +// restriction, including without limitation the rights to use, +// copy, modify, merge, publish, distribute, sublicense, and/or +// sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following +// conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +// OTHER DEALINGS IN THE SOFTWARE. +// + +import SwiftSyntax + +/// A `return` statement. +public struct Return: CodeBlock { + private let exprs: [CodeBlock] + + /// Creates a `return` statement. + /// - Parameter content: A ``CodeBlockBuilder`` that provides the expression to return. + public init(@CodeBlockBuilderResult _ content: () -> [CodeBlock]) { + self.exprs = content() + } + public var syntax: SyntaxProtocol { + guard let expr = exprs.first else { + fatalError("Return must have at least one expression.") + } + if let varExp = expr as? VariableExp { + return ReturnStmtSyntax( + returnKeyword: .keyword(.return, trailingTrivia: .space), + expression: ExprSyntax(DeclReferenceExprSyntax(baseName: .identifier(varExp.name))) + ) + } + return ReturnStmtSyntax( + returnKeyword: .keyword(.return, trailingTrivia: .space), + expression: ExprSyntax(expr.syntax) + ) + } +} diff --git a/Sources/SyntaxKit/Struct.swift b/Sources/SyntaxKit/Struct.swift new file mode 100644 index 0000000..50d6dc6 --- /dev/null +++ b/Sources/SyntaxKit/Struct.swift @@ -0,0 +1,104 @@ +// +// Struct.swift +// SyntaxKit +// +// Created by Leo Dion. +// Copyright © 2025 BrightDigit. +// +// Permission is hereby granted, free of charge, to any person +// obtaining a copy of this software and associated documentation +// files (the “Software”), to deal in the Software without +// restriction, including without limitation the rights to use, +// copy, modify, merge, publish, distribute, sublicense, and/or +// sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following +// conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +// OTHER DEALINGS IN THE SOFTWARE. +// + +import SwiftSyntax + +/// A Swift `struct` declaration. +public struct Struct: CodeBlock { + private let name: String + private let members: [CodeBlock] + private var inheritance: String? + private var genericParameter: String? + + /// Creates a `struct` declaration. + /// - Parameters: + /// - name: The name of the struct. + /// - generic: A generic parameter for the struct, if any. + /// - content: A ``CodeBlockBuilder`` that provides the members of the struct. + public init( + _ name: String, generic: String? = nil, @CodeBlockBuilderResult _ content: () -> [CodeBlock] + ) { + self.name = name + self.members = content() + self.genericParameter = generic + } + + /// Sets the inheritance for the struct. + /// - Parameter type: The type to inherit from. + /// - Returns: A copy of the struct with the inheritance set. + public func inherits(_ type: String) -> Self { + var copy = self + copy.inheritance = type + return copy + } + + public var syntax: SyntaxProtocol { + let structKeyword = TokenSyntax.keyword(.struct, trailingTrivia: .space) + let identifier = TokenSyntax.identifier(name) + + var genericParameterClause: GenericParameterClauseSyntax? + if let generic = genericParameter { + let genericParameter = GenericParameterSyntax( + name: .identifier(generic), + trailingComma: nil + ) + genericParameterClause = GenericParameterClauseSyntax( + leftAngle: .leftAngleToken(), + parameters: GenericParameterListSyntax([genericParameter]), + rightAngle: .rightAngleToken() + ) + } + + var inheritanceClause: InheritanceClauseSyntax? + if let inheritance = inheritance { + let inheritedType = InheritedTypeSyntax( + type: IdentifierTypeSyntax(name: .identifier(inheritance))) + inheritanceClause = InheritanceClauseSyntax( + colon: .colonToken(), inheritedTypes: InheritedTypeListSyntax([inheritedType])) + } + + let memberBlock = MemberBlockSyntax( + leftBrace: .leftBraceToken(leadingTrivia: .space, trailingTrivia: .newline), + members: MemberBlockItemListSyntax( + members.compactMap { member in + guard let syntax = member.syntax.as(DeclSyntax.self) else { return nil } + return MemberBlockItemSyntax(decl: syntax, trailingTrivia: .newline) + }), + rightBrace: .rightBraceToken(leadingTrivia: .newline) + ) + + return StructDeclSyntax( + structKeyword: structKeyword, + name: identifier, + genericParameterClause: genericParameterClause, + inheritanceClause: inheritanceClause, + memberBlock: memberBlock + ) + } +} diff --git a/Sources/SyntaxKit/Switch.swift b/Sources/SyntaxKit/Switch.swift new file mode 100644 index 0000000..b03f7fb --- /dev/null +++ b/Sources/SyntaxKit/Switch.swift @@ -0,0 +1,63 @@ +// +// Switch.swift +// SyntaxKit +// +// Created by Leo Dion. +// Copyright © 2025 BrightDigit. +// +// Permission is hereby granted, free of charge, to any person +// obtaining a copy of this software and associated documentation +// files (the “Software”), to deal in the Software without +// restriction, including without limitation the rights to use, +// copy, modify, merge, publish, distribute, sublicense, and/or +// sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following +// conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +// OTHER DEALINGS IN THE SOFTWARE. +// + +import SwiftSyntax + +/// A `switch` statement. +public struct Switch: CodeBlock { + private let expression: String + private let cases: [CodeBlock] + + /// Creates a `switch` statement. + /// - Parameters: + /// - expression: The expression to switch on. + /// - content: A ``CodeBlockBuilder`` that provides the cases for the switch. + public init(_ expression: String, @CodeBlockBuilderResult _ content: () -> [CodeBlock]) { + self.expression = expression + self.cases = content() + } + + public var syntax: SyntaxProtocol { + let expr = ExprSyntax(DeclReferenceExprSyntax(baseName: .identifier(expression))) + let casesArr: [SwitchCaseSyntax] = self.cases.compactMap { + if let switchCase = $0 as? SwitchCase { return switchCase.switchCaseSyntax } + if let switchDefault = $0 as? Default { return switchDefault.switchCaseSyntax } + return nil + } + let cases = SwitchCaseListSyntax(casesArr.map { SwitchCaseListSyntax.Element($0) }) + let switchExpr = SwitchExprSyntax( + switchKeyword: .keyword(.switch, trailingTrivia: .space), + subject: expr, + leftBrace: .leftBraceToken(leadingTrivia: .space, trailingTrivia: .newline), + cases: cases, + rightBrace: .rightBraceToken(leadingTrivia: .newline) + ) + return switchExpr + } +} diff --git a/Sources/SyntaxKit/SwitchCase.swift b/Sources/SyntaxKit/SwitchCase.swift new file mode 100644 index 0000000..1d219b4 --- /dev/null +++ b/Sources/SyntaxKit/SwitchCase.swift @@ -0,0 +1,81 @@ +// +// SwitchCase.swift +// SyntaxKit +// +// Created by Leo Dion. +// Copyright © 2025 BrightDigit. +// +// Permission is hereby granted, free of charge, to any person +// obtaining a copy of this software and associated documentation +// files (the “Software”), to deal in the Software without +// restriction, including without limitation the rights to use, +// copy, modify, merge, publish, distribute, sublicense, and/or +// sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following +// conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +// OTHER DEALINGS IN THE SOFTWARE. +// + +import SwiftSyntax + +/// A `case` in a `switch` statement. +public struct SwitchCase: CodeBlock { + private let patterns: [String] + private let body: [CodeBlock] + + /// Creates a `case` for a `switch` statement. + /// - Parameters: + /// - patterns: The patterns to match for the case. + /// - content: A ``CodeBlockBuilder`` that provides the body of the case. + public init(_ patterns: String..., @CodeBlockBuilderResult content: () -> [CodeBlock]) { + self.patterns = patterns + self.body = content() + } + + public var switchCaseSyntax: SwitchCaseSyntax { + let caseItems = SwitchCaseItemListSyntax( + patterns.enumerated().map { index, pattern in + var item = SwitchCaseItemSyntax( + pattern: PatternSyntax(IdentifierPatternSyntax(identifier: .identifier(pattern))) + ) + if index < patterns.count - 1 { + item = item.with(\.trailingComma, .commaToken(trailingTrivia: .space)) + } + return item + }) + let statements = CodeBlockItemListSyntax( + body.compactMap { + var item: CodeBlockItemSyntax? + if let decl = $0.syntax.as(DeclSyntax.self) { + item = CodeBlockItemSyntax(item: .decl(decl)) + } else if let expr = $0.syntax.as(ExprSyntax.self) { + item = CodeBlockItemSyntax(item: .expr(expr)) + } else if let stmt = $0.syntax.as(StmtSyntax.self) { + item = CodeBlockItemSyntax(item: .stmt(stmt)) + } + return item?.with(\.trailingTrivia, .newline) + }) + let label = SwitchCaseLabelSyntax( + caseKeyword: .keyword(.case, trailingTrivia: .space), + caseItems: caseItems, + colon: .colonToken(trailingTrivia: .newline) + ) + return SwitchCaseSyntax( + label: .case(label), + statements: statements + ) + } + + public var syntax: SyntaxProtocol { switchCaseSyntax } +} diff --git a/Sources/SyntaxKit/Trivia+Comments.swift b/Sources/SyntaxKit/Trivia+Comments.swift new file mode 100644 index 0000000..ecc7008 --- /dev/null +++ b/Sources/SyntaxKit/Trivia+Comments.swift @@ -0,0 +1,54 @@ +// +// Trivia+Comments.swift +// SyntaxKit +// +// Created by Leo Dion. +// Copyright © 2025 BrightDigit. +// +// Permission is hereby granted, free of charge, to any person +// obtaining a copy of this software and associated documentation +// files (the “Software”), to deal in the Software without +// restriction, including without limitation the rights to use, +// copy, modify, merge, publish, distribute, sublicense, and/or +// sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following +// conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +// OTHER DEALINGS IN THE SOFTWARE. +// + +import SwiftSyntax + +extension Trivia { + /// Extracts comment strings from the trivia collection. + /// + /// This includes line comments, documentation comments, and block comments. + public var comments: [String] { + compactMap { piece in + switch piece { + case .lineComment(let text), + .blockComment(let text), + .docLineComment(let text), + .docBlockComment(let text): + return text + default: + return nil + } + } + } + + /// A Boolean value that indicates whether the trivia contains any comments. + public var hasComments: Bool { + !comments.isEmpty + } +} diff --git a/Sources/SyntaxKit/Variable.swift b/Sources/SyntaxKit/Variable.swift new file mode 100644 index 0000000..b37ec0d --- /dev/null +++ b/Sources/SyntaxKit/Variable.swift @@ -0,0 +1,79 @@ +// +// Variable.swift +// SyntaxKit +// +// Created by Leo Dion. +// Copyright © 2025 BrightDigit. +// +// Permission is hereby granted, free of charge, to any person +// obtaining a copy of this software and associated documentation +// files (the “Software”), to deal in the Software without +// restriction, including without limitation the rights to use, +// copy, modify, merge, publish, distribute, sublicense, and/or +// sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following +// conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +// OTHER DEALINGS IN THE SOFTWARE. +// + +import SwiftSyntax + +/// A Swift `let` or `var` declaration with an explicit type. +public struct Variable: CodeBlock { + private let kind: VariableKind + private let name: String + private let type: String + private let defaultValue: String? + + /// Creates a `let` or `var` declaration with an explicit type. + /// - Parameters: + /// - kind: The kind of variable, either ``VariableKind/let`` or ``VariableKind/var``. + /// - name: The name of the variable. + /// - type: The type of the variable. + /// - defaultValue: The initial value of the variable, if any. + public init(_ kind: VariableKind, name: String, type: String, equals defaultValue: String? = nil) + { + self.kind = kind + self.name = name + self.type = type + self.defaultValue = defaultValue + } + + public var syntax: SyntaxProtocol { + let bindingKeyword = TokenSyntax.keyword(kind == .let ? .let : .var, trailingTrivia: .space) + let identifier = TokenSyntax.identifier(name, trailingTrivia: .space) + let typeAnnotation = TypeAnnotationSyntax( + colon: .colonToken(leadingTrivia: .space, trailingTrivia: .space), + type: IdentifierTypeSyntax(name: .identifier(type)) + ) + + let initializer = defaultValue.map { value in + InitializerClauseSyntax( + equal: .equalToken(leadingTrivia: .space, trailingTrivia: .space), + value: ExprSyntax(DeclReferenceExprSyntax(baseName: .identifier(value))) + ) + } + + return VariableDeclSyntax( + bindingSpecifier: bindingKeyword, + bindings: PatternBindingListSyntax([ + PatternBindingSyntax( + pattern: IdentifierPatternSyntax(identifier: identifier), + typeAnnotation: typeAnnotation, + initializer: initializer + ) + ]) + ) + } +} diff --git a/Sources/SyntaxKit/VariableDecl.swift b/Sources/SyntaxKit/VariableDecl.swift new file mode 100644 index 0000000..64da71b --- /dev/null +++ b/Sources/SyntaxKit/VariableDecl.swift @@ -0,0 +1,83 @@ +// +// VariableDecl.swift +// SyntaxKit +// +// Created by Leo Dion. +// Copyright © 2025 BrightDigit. +// +// Permission is hereby granted, free of charge, to any person +// obtaining a copy of this software and associated documentation +// files (the “Software”), to deal in the Software without +// restriction, including without limitation the rights to use, +// copy, modify, merge, publish, distribute, sublicense, and/or +// sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following +// conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +// OTHER DEALINGS IN THE SOFTWARE. +// + +import SwiftSyntax + +/// A Swift `let` or `var` declaration. +public struct VariableDecl: CodeBlock { + private let kind: VariableKind + private let name: String + private let value: String? + + /// Creates a `let` or `var` declaration. + /// - Parameters: + /// - kind: The kind of variable, either ``VariableKind/let`` or ``VariableKind/var``. + /// - name: The name of the variable. + /// - value: The initial value of the variable, if any. + public init(_ kind: VariableKind, name: String, equals value: String? = nil) { + self.kind = kind + self.name = name + self.value = value + } + + public var syntax: SyntaxProtocol { + let bindingKeyword = TokenSyntax.keyword(kind == .let ? .let : .var, trailingTrivia: .space) + let identifier = TokenSyntax.identifier(name, trailingTrivia: .space) + let initializer = value.map { value in + if value.hasPrefix("\"") && value.hasSuffix("\"") || value.contains("\\(") { + return InitializerClauseSyntax( + equal: .equalToken(leadingTrivia: .space, trailingTrivia: .space), + value: StringLiteralExprSyntax( + openingQuote: .stringQuoteToken(), + segments: StringLiteralSegmentListSyntax([ + .stringSegment( + StringSegmentSyntax(content: .stringSegment(String(value.dropFirst().dropLast())))) + ]), + closingQuote: .stringQuoteToken() + ) + ) + } else { + return InitializerClauseSyntax( + equal: .equalToken(leadingTrivia: .space, trailingTrivia: .space), + value: ExprSyntax(DeclReferenceExprSyntax(baseName: .identifier(value))) + ) + } + } + return VariableDeclSyntax( + bindingSpecifier: bindingKeyword, + bindings: PatternBindingListSyntax([ + PatternBindingSyntax( + pattern: IdentifierPatternSyntax(identifier: identifier), + typeAnnotation: nil, + initializer: initializer + ) + ]) + ) + } +} diff --git a/Sources/SyntaxKit/VariableExp.swift b/Sources/SyntaxKit/VariableExp.swift new file mode 100644 index 0000000..5616290 --- /dev/null +++ b/Sources/SyntaxKit/VariableExp.swift @@ -0,0 +1,161 @@ +// +// VariableExp.swift +// SyntaxKit +// +// Created by Leo Dion. +// Copyright © 2025 BrightDigit. +// +// Permission is hereby granted, free of charge, to any person +// obtaining a copy of this software and associated documentation +// files (the “Software”), to deal in the Software without +// restriction, including without limitation the rights to use, +// copy, modify, merge, publish, distribute, sublicense, and/or +// sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following +// conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +// OTHER DEALINGS IN THE SOFTWARE. +// + +import SwiftSyntax + +/// An expression that refers to a variable. +public struct VariableExp: CodeBlock { + let name: String + + /// Creates a variable expression. + /// - Parameter name: The name of the variable. + public init(_ name: String) { + self.name = name + } + + /// Accesses a property on the variable. + /// - Parameter propertyName: The name of the property to access. + /// - Returns: A ``PropertyAccessExp`` that represents the property access. + public func property(_ propertyName: String) -> CodeBlock { + PropertyAccessExp(baseName: name, propertyName: propertyName) + } + + /// Calls a method on the variable. + /// - Parameter methodName: The name of the method to call. + /// - Returns: A ``FunctionCallExp`` that represents the method call. + public func call(_ methodName: String) -> CodeBlock { + FunctionCallExp(baseName: name, methodName: methodName) + } + + /// Calls a method on the variable with parameters. + /// - Parameters: + /// - methodName: The name of the method to call. + /// - params: A ``ParameterExpBuilder`` that provides the parameters for the method call. + /// - Returns: A ``FunctionCallExp`` that represents the method call. + public func call(_ methodName: String, @ParameterExpBuilderResult _ params: () -> [ParameterExp]) + -> CodeBlock + { + FunctionCallExp(baseName: name, methodName: methodName, parameters: params()) + } + + public var syntax: SyntaxProtocol { + TokenSyntax.identifier(name) + } +} + +/// An expression that accesses a property on a base expression. +public struct PropertyAccessExp: CodeBlock { + let baseName: String + let propertyName: String + + /// Creates a property access expression. + /// - Parameters: + /// - baseName: The name of the base variable. + /// - propertyName: The name of the property to access. + public init(baseName: String, propertyName: String) { + self.baseName = baseName + self.propertyName = propertyName + } + + public var syntax: SyntaxProtocol { + let base = ExprSyntax(DeclReferenceExprSyntax(baseName: .identifier(baseName))) + let property = TokenSyntax.identifier(propertyName) + return ExprSyntax( + MemberAccessExprSyntax( + base: base, + dot: .periodToken(), + name: property + )) + } +} + +/// An expression that calls a function. +public struct FunctionCallExp: CodeBlock { + let baseName: String + let methodName: String + let parameters: [ParameterExp] + + /// Creates a function call expression. + /// - Parameters: + /// - baseName: The name of the base variable. + /// - methodName: The name of the method to call. + public init(baseName: String, methodName: String) { + self.baseName = baseName + self.methodName = methodName + self.parameters = [] + } + + /// Creates a function call expression with parameters. + /// - Parameters: + /// - baseName: The name of the base variable. + /// - methodName: The name of the method to call. + /// - parameters: The parameters for the method call. + public init(baseName: String, methodName: String, parameters: [ParameterExp]) { + self.baseName = baseName + self.methodName = methodName + self.parameters = parameters + } + + public var syntax: SyntaxProtocol { + let base = ExprSyntax(DeclReferenceExprSyntax(baseName: .identifier(baseName))) + let method = TokenSyntax.identifier(methodName) + let args = LabeledExprListSyntax( + parameters.enumerated().map { index, param in + let expr = param.syntax + if let labeled = expr as? LabeledExprSyntax { + var element = labeled + if index < parameters.count - 1 { + element = element.with(\.trailingComma, .commaToken(trailingTrivia: .space)) + } + return element + } else if let unlabeled = expr as? ExprSyntax { + return TupleExprElementSyntax( + label: nil, + colon: nil, + expression: unlabeled, + trailingComma: index < parameters.count - 1 ? .commaToken(trailingTrivia: .space) : nil + ) + } else { + fatalError("ParameterExp.syntax must return LabeledExprSyntax or ExprSyntax") + } + }) + return ExprSyntax( + FunctionCallExprSyntax( + calledExpression: ExprSyntax( + MemberAccessExprSyntax( + base: base, + dot: .periodToken(), + name: method + )), + leftParen: .leftParenToken(), + arguments: args, + rightParen: .rightParenToken() + )) + } +} diff --git a/Sources/SyntaxKit/VariableKind.swift b/Sources/SyntaxKit/VariableKind.swift new file mode 100644 index 0000000..a81a0f6 --- /dev/null +++ b/Sources/SyntaxKit/VariableKind.swift @@ -0,0 +1,38 @@ +// +// VariableKind.swift +// SyntaxKit +// +// Created by Leo Dion. +// Copyright © 2025 BrightDigit. +// +// Permission is hereby granted, free of charge, to any person +// obtaining a copy of this software and associated documentation +// files (the “Software”), to deal in the Software without +// restriction, including without limitation the rights to use, +// copy, modify, merge, publish, distribute, sublicense, and/or +// sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following +// conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +// OTHER DEALINGS IN THE SOFTWARE. +// + +import Foundation + +/// The kind of a variable declaration. +public enum VariableKind { + /// A `let` declaration. + case `let` + /// A `var` declaration. + case `var` +} diff --git a/Sources/SyntaxKit/parser/String.swift b/Sources/SyntaxKit/parser/String.swift new file mode 100644 index 0000000..ee2b142 --- /dev/null +++ b/Sources/SyntaxKit/parser/String.swift @@ -0,0 +1,64 @@ +// +// String.swift +// SyntaxKit +// +// Created by Leo Dion. +// Copyright © 2025 BrightDigit. +// +// Permission is hereby granted, free of charge, to any person +// obtaining a copy of this software and associated documentation +// files (the “Software”), to deal in the Software without +// restriction, including without limitation the rights to use, +// copy, modify, merge, publish, distribute, sublicense, and/or +// sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following +// conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +// OTHER DEALINGS IN THE SOFTWARE. +// + +extension String { + internal func escapeHTML() -> String { + var string = self + let specialCharacters = [ + ("&", "&"), + ("<", "<"), + (">", ">"), + ("\"", """), + ("'", "'"), + ] + for (unescaped, escaped) in specialCharacters { + string = string.replacingOccurrences( + of: unescaped, with: escaped, options: .literal, range: nil) + } + return string + } + + internal func replaceInvisiblesWithHTML() -> String { + self + .replacingOccurrences(of: " ", with: " ") + .replacingOccurrences(of: "\n", with: "
") + } + + internal func replaceInvisiblesWithSymbols() -> String { + self + .replacingOccurrences(of: " ", with: "␣") + .replacingOccurrences(of: "\n", with: "↲") + } + + internal func replaceHTMLWhitespacesWithSymbols() -> String { + self + .replacingOccurrences(of: " ", with: "") + .replacingOccurrences(of: "
", with: "
") + } +} diff --git a/Sources/SyntaxKit/parser/SyntaxParser.swift b/Sources/SyntaxKit/parser/SyntaxParser.swift new file mode 100644 index 0000000..f4f38e5 --- /dev/null +++ b/Sources/SyntaxKit/parser/SyntaxParser.swift @@ -0,0 +1,58 @@ +// +// SyntaxParser.swift +// SyntaxKit +// +// Created by Leo Dion. +// Copyright © 2025 BrightDigit. +// +// Permission is hereby granted, free of charge, to any person +// obtaining a copy of this software and associated documentation +// files (the “Software”), to deal in the Software without +// restriction, including without limitation the rights to use, +// copy, modify, merge, publish, distribute, sublicense, and/or +// sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following +// conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +// OTHER DEALINGS IN THE SOFTWARE. +// + +import Foundation +import SwiftOperators +import SwiftParser +import SwiftSyntax + +package enum SyntaxParser { + package static func parse(code: String, options: [String] = []) throws -> SyntaxResponse { + let sourceFile = Parser.parse(source: code) + + let syntax: Syntax + if options.contains("fold") { + syntax = OperatorTable.standardOperators.foldAll(sourceFile, errorHandler: { _ in }) + } else { + syntax = Syntax(sourceFile) + } + + let visitor = TokenVisitor( + locationConverter: SourceLocationConverter(fileName: "", tree: sourceFile), + showMissingTokens: options.contains("showmissing") + ) + _ = visitor.rewrite(syntax) + + let tree = visitor.tree + let encoder = JSONEncoder() + let json = String(decoding: try encoder.encode(tree), as: UTF8.self) + + return SyntaxResponse(syntaxJSON: json) + } +} diff --git a/Sources/SyntaxKit/parser/SyntaxResponse.swift b/Sources/SyntaxKit/parser/SyntaxResponse.swift new file mode 100644 index 0000000..7dbb636 --- /dev/null +++ b/Sources/SyntaxKit/parser/SyntaxResponse.swift @@ -0,0 +1,34 @@ +// +// SyntaxResponse.swift +// SyntaxKit +// +// Created by Leo Dion. +// Copyright © 2025 BrightDigit. +// +// Permission is hereby granted, free of charge, to any person +// obtaining a copy of this software and associated documentation +// files (the “Software”), to deal in the Software without +// restriction, including without limitation the rights to use, +// copy, modify, merge, publish, distribute, sublicense, and/or +// sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following +// conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +// OTHER DEALINGS IN THE SOFTWARE. +// + +import Foundation + +package struct SyntaxResponse: Codable { + package let syntaxJSON: String +} diff --git a/Sources/SwiftBuilder/parser/TokenVisitor.swift b/Sources/SyntaxKit/parser/TokenVisitor.swift similarity index 60% rename from Sources/SwiftBuilder/parser/TokenVisitor.swift rename to Sources/SyntaxKit/parser/TokenVisitor.swift index 6c2025e..aad7ca5 100644 --- a/Sources/SwiftBuilder/parser/TokenVisitor.swift +++ b/Sources/SyntaxKit/parser/TokenVisitor.swift @@ -1,8 +1,37 @@ +// +// TokenVisitor.swift +// SyntaxKit +// +// Created by Leo Dion. +// Copyright © 2025 BrightDigit. +// +// Permission is hereby granted, free of charge, to any person +// obtaining a copy of this software and associated documentation +// files (the “Software”), to deal in the Software without +// restriction, including without limitation the rights to use, +// copy, modify, merge, publish, distribute, sublicense, and/or +// sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following +// conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +// OTHER DEALINGS IN THE SOFTWARE. +// + import Foundation @_spi(RawSyntax) import SwiftSyntax final class TokenVisitor: SyntaxRewriter { - var list = [String]() + // var list = [String]() var tree = [TreeNode]() private var current: TreeNode! @@ -17,6 +46,7 @@ final class TokenVisitor: SyntaxRewriter { super.init(viewMode: showMissingTokens ? .all : .sourceAccurate) } + // swiftlint:disable:next cyclomatic_complexity function_body_length override func visitPre(_ node: Syntax) { let syntaxNodeType = node.syntaxNodeType @@ -27,44 +57,26 @@ final class TokenVisitor: SyntaxRewriter { className = "\(syntaxNodeType)" } - let title: String - let content: String - let type: String - if let tokenSyntax = node.as(TokenSyntax.self) { - title = tokenSyntax.text - content = "\(tokenSyntax.tokenKind)" - type = "Token" - } else { - title = "\(node.trimmed)" - content = "\(syntaxNodeType)" - type = "Syntax" - } - let sourceRange = node.sourceRange(converter: locationConverter) let start = sourceRange.start let end = sourceRange.end let graphemeStartColumn: Int - if let prefix = String(locationConverter.sourceLines[start.line - 1].utf8.prefix(start.column - 1)) { + if let prefix = String( + locationConverter.sourceLines[start.line - 1].utf8.prefix(start.column - 1)) + { graphemeStartColumn = prefix.utf16.count + 1 } else { graphemeStartColumn = start.column } let graphemeEndColumn: Int - if let prefix = String(locationConverter.sourceLines[end.line - 1].utf8.prefix(end.column - 1)) { + if let prefix = String(locationConverter.sourceLines[end.line - 1].utf8.prefix(end.column - 1)) + { graphemeEndColumn = prefix.utf16.count + 1 } else { graphemeEndColumn = end.column } - list.append( - ""# - ) - let syntaxType: SyntaxType switch node { case _ where node.is(DeclSyntax.self): @@ -85,10 +97,8 @@ final class TokenVisitor: SyntaxRewriter { range: Range( startRow: start.line, startColumn: start.column, - graphemeStartColumn: graphemeStartColumn, endRow: end.line, - endColumn: end.column, - graphemeEndColumn: graphemeEndColumn + endColumn: end.column ), type: syntaxType ) @@ -105,8 +115,9 @@ final class TokenVisitor: SyntaxRewriter { guard let name = childName(keyPath) else { continue } - guard allChildren.contains(where: { (child) in child.keyPathInParent == keyPath }) else { - treeNode.structure.append(StructureProperty(name: name, value: StructureValue(text: "nil"))) + guard allChildren.contains(where: { child in child.keyPathInParent == keyPath }) else { + treeNode.structure.append( + StructureProperty(name: name, value: StructureValue(text: "nil"))) continue } @@ -132,13 +143,17 @@ final class TokenVisitor: SyntaxRewriter { kind: "\(value.tokenKind)" ) ) - ) } + ) + } case let value?: if let value = value as? SyntaxProtocol { let type = "\(value.syntaxNodeType)" - treeNode.structure.append(StructureProperty(name: name, value: StructureValue(text: "\(type)"), ref: "\(type)")) + treeNode.structure.append( + StructureProperty( + name: name, value: StructureValue(text: "\(type)"), ref: "\(type)")) } else { - treeNode.structure.append(StructureProperty(name: name, value: StructureValue(text: "\(value)"))) + treeNode.structure.append( + StructureProperty(name: name, value: StructureValue(text: "\(value)"))) } case .none: treeNode.structure.append(StructureProperty(name: name)) @@ -147,9 +162,11 @@ final class TokenVisitor: SyntaxRewriter { } case .collection(let syntax): treeNode.type = .collection - treeNode.structure.append(StructureProperty(name: "Element", value: StructureValue(text: "\(syntax)"))) - treeNode.structure.append(StructureProperty(name: "Count", value: StructureValue(text: "\(node.children(viewMode: .all).count)"))) - break + treeNode.structure.append( + StructureProperty(name: "Element", value: StructureValue(text: "\(syntax)"))) + treeNode.structure.append( + StructureProperty( + name: "Count", value: StructureValue(text: "\(node.children(viewMode: .all).count)"))) case .choices: break } @@ -166,20 +183,16 @@ final class TokenVisitor: SyntaxRewriter { .escapeHTML() .replaceInvisiblesWithHTML() .replaceHTMLWhitespacesWithSymbols() - if token.presence == .missing { - current.class = "\(token.presence)" - } + current.token = Token(kind: "\(token.tokenKind)", leadingTrivia: "", trailingTrivia: "") - token.leadingTrivia.forEach { (piece) in + token.leadingTrivia.forEach { piece in let trivia = processTriviaPiece(piece) - list.append(trivia) current.token?.leadingTrivia += trivia.replaceHTMLWhitespacesWithSymbols() } processToken(token) - token.trailingTrivia.forEach { (piece) in + token.trailingTrivia.forEach { piece in let trivia = processTriviaPiece(piece) - list.append(trivia) current.token?.trailingTrivia += trivia.replaceHTMLWhitespacesWithSymbols() } @@ -187,7 +200,6 @@ final class TokenVisitor: SyntaxRewriter { } override func visitPost(_ node: Syntax) { - list.append("") if let parent = current.parent { current = tree[parent] } else { @@ -208,22 +220,14 @@ final class TokenVisitor: SyntaxRewriter { let start = sourceRange.start let end = sourceRange.end let text = token.presence == .present || showMissingTokens ? token.text : "" - list.append( - ""# + - "\(text.escapeHTML().replaceInvisiblesWithHTML())" - ) } private func processTriviaPiece(_ piece: TriviaPiece) -> String { - func wrapWithSpanTag(class c: String, text: String) -> String { - "\(text.escapeHTML().replaceInvisiblesWithHTML())" + func wrapWithSpanTag(class className: String, text: String) -> String { + "\(text.escapeHTML().replaceInvisiblesWithHTML())" } var trivia = "" @@ -254,38 +258,3 @@ final class TokenVisitor: SyntaxRewriter { return trivia } } - -private extension String { - func escapeHTML() -> String { - var string = self - let specialCharacters = [ - ("&", "&"), - ("<", "<"), - (">", ">"), - ("\"", """), - ("'", "'"), - ]; - for (unescaped, escaped) in specialCharacters { - string = string.replacingOccurrences(of: unescaped, with: escaped, options: .literal, range: nil) - } - return string - } - - func replaceInvisiblesWithHTML() -> String { - self - .replacingOccurrences(of: " ", with: " ") - .replacingOccurrences(of: "\n", with: "
") - } - - func replaceInvisiblesWithSymbols() -> String { - self - .replacingOccurrences(of: " ", with: "␣") - .replacingOccurrences(of: "\n", with: "↲") - } - - func replaceHTMLWhitespacesWithSymbols() -> String { - self - .replacingOccurrences(of: " ", with: "") - .replacingOccurrences(of: "
", with: "
") - } -} diff --git a/Sources/SwiftBuilder/parser/TreeNode.swift b/Sources/SyntaxKit/parser/TreeNode.swift similarity index 64% rename from Sources/SwiftBuilder/parser/TreeNode.swift rename to Sources/SyntaxKit/parser/TreeNode.swift index b85e603..4c35089 100644 --- a/Sources/SwiftBuilder/parser/TreeNode.swift +++ b/Sources/SyntaxKit/parser/TreeNode.swift @@ -1,3 +1,32 @@ +// +// TreeNode.swift +// SyntaxKit +// +// Created by Leo Dion. +// Copyright © 2025 BrightDigit. +// +// Permission is hereby granted, free of charge, to any person +// obtaining a copy of this software and associated documentation +// files (the “Software”), to deal in the Software without +// restriction, including without limitation the rights to use, +// copy, modify, merge, publish, distribute, sublicense, and/or +// sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following +// conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +// OTHER DEALINGS IN THE SOFTWARE. +// + import Foundation final class TreeNode: Codable { @@ -5,11 +34,11 @@ final class TreeNode: Codable { var parent: Int? var text: String - var range = Range(startRow: 0, startColumn: 0, graphemeStartColumn: 0, endRow: 0, endColumn: 0, graphemeEndColumn: 0) + var range = Range( + startRow: 0, startColumn: 0, endRow: 0, endColumn: 0) var structure = [StructureProperty]() var type: SyntaxType var token: Token? - var `class`: String? init(id: Int, text: String, range: Range, type: SyntaxType) { self.id = id @@ -21,13 +50,8 @@ final class TreeNode: Codable { extension TreeNode: Equatable { static func == (lhs: TreeNode, rhs: TreeNode) -> Bool { - lhs.id == rhs.id && - lhs.parent == rhs.parent && - lhs.text == rhs.text && - lhs.range == rhs.range && - lhs.structure == rhs.structure && - lhs.type == rhs.type && - lhs.token == rhs.token + lhs.id == rhs.id && lhs.parent == rhs.parent && lhs.text == rhs.text && lhs.range == rhs.range + && lhs.structure == rhs.structure && lhs.type == rhs.type && lhs.token == rhs.token } } @@ -50,10 +74,8 @@ extension TreeNode: CustomStringConvertible { struct Range: Codable, Equatable { let startRow: Int let startColumn: Int - let graphemeStartColumn: Int let endRow: Int let endColumn: Int - let graphemeEndColumn: Int } extension Range: CustomStringConvertible { @@ -147,25 +169,8 @@ extension Token: CustomStringConvertible { } } -private extension String { - func escapeHTML() -> String { - var string = self - let specialCharacters = [ - ("&", "&"), - ("<", "<"), - (">", ">"), - ("\"", """), - ("'", "'"), - ]; - for (unescaped, escaped) in specialCharacters { - string = string.replacingOccurrences(of: unescaped, with: escaped, options: .literal, range: nil) - } - return string - .replacingOccurrences(of: " ", with: " ") - .replacingOccurrences(of: "\n", with: "
") - } - - func replaceHTMLWhitespacesToSymbols() -> String { +extension String { + fileprivate func replaceHTMLWhitespacesToSymbols() -> String { self .replacingOccurrences(of: " ", with: "") .replacingOccurrences(of: "
", with: "") diff --git a/Sources/skit/main.swift b/Sources/skit/main.swift new file mode 100644 index 0000000..d7952fe --- /dev/null +++ b/Sources/skit/main.swift @@ -0,0 +1,51 @@ +// +// main.swift +// SyntaxKit +// +// Created by Leo Dion. +// Copyright © 2025 BrightDigit. +// +// Permission is hereby granted, free of charge, to any person +// obtaining a copy of this software and associated documentation +// files (the “Software”), to deal in the Software without +// restriction, including without limitation the rights to use, +// copy, modify, merge, publish, distribute, sublicense, and/or +// sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following +// conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +// OTHER DEALINGS IN THE SOFTWARE. +// + +import Foundation +import SyntaxKit + +// Read Swift code from stdin +let code = String(data: FileHandle.standardInput.readDataToEndOfFile(), encoding: .utf8) ?? "" + +do { + // Parse the code using SyntaxKit + let response = try SyntaxParser.parse(code: code, options: ["fold"]) + + // Output the JSON to stdout + print(response.syntaxJSON) +} catch { + // If there's an error, output it as JSON + let errorResponse = ["error": error.localizedDescription] + if let jsonData = try? JSONSerialization.data(withJSONObject: errorResponse), + let jsonString = String(data: jsonData, encoding: .utf8) + { + print(jsonString) + } + exit(1) +} diff --git a/Tests/SwiftBuilderTests/SwiftBuilderCommentTests.swift b/Tests/SwiftBuilderTests/SwiftBuilderCommentTests.swift deleted file mode 100644 index 7201eb8..0000000 --- a/Tests/SwiftBuilderTests/SwiftBuilderCommentTests.swift +++ /dev/null @@ -1,109 +0,0 @@ -import XCTest -@testable import SwiftBuilder - -final class SwiftBuilderCommentTests: XCTestCase { - func testCommentInjection() { - let syntax = Group { - Struct("Card") { - Variable(.let, name: "rank", type: "Rank") - .comment{ - Line(.doc, "The rank of the card (2-10, J, Q, K, A)") - } - Variable(.let, name: "suit", type: "Suit") - .comment{ - Line(.doc, "The suit of the card (hearts, diamonds, clubs, spades)") - } - } - .inherits("Comparable") - .comment{ - Line("MARK: - Models") - Line(.doc, "Represents a playing card in a standard 52-card deck") - Line(.doc) - Line(.doc, "A card has a rank (2-10, J, Q, K, A) and a suit (hearts, diamonds, clubs, spades).") - Line(.doc, "Each card can be compared to other cards based on its rank.") - } - - Enum("Rank") { - EnumCase("two").equals(2) - EnumCase("three") - EnumCase("four") - EnumCase("five") - EnumCase("six") - EnumCase("seven") - EnumCase("eight") - EnumCase("nine") - EnumCase("ten") - EnumCase("jack") - EnumCase("queen") - EnumCase("king") - EnumCase("ace") - Struct("Values") { - Variable(.let, name: "first", type: "Int") - Variable(.let, name: "second", type: "Int?") - } - ComputedProperty("description", type: "String") { - Switch("self") { - SwitchCase(".jack") { - Return{ - Literal.string("J") - } - } - SwitchCase(".queen") { - Return{ - Literal.string("Q") - } - } - SwitchCase(".king") { - Return{ - Literal.string("K") - } - } - SwitchCase(".ace") { - Return{ - Literal.string("A") - } - } - Default { - Return{ - Literal.string("\\(rawValue)") - } - } - } - } - .comment{ - Line(.doc, "Returns a string representation of the rank") - } - } - .inherits("Int") - .inherits("CaseIterable") - .comment{ - Line("MARK: - Enums") - Line(.doc, "Represents the possible ranks of a playing card") - } - - Enum("Suit") { - EnumCase("spades").equals("♠") - EnumCase("hearts").equals("♡") - EnumCase("diamonds").equals("♢") - EnumCase("clubs").equals("♣") - } - .inherits("String") - .inherits("CaseIterable") - .comment{ - Line(.doc, "Represents the possible suits of a playing card") - } - - } - - let generated = syntax.generateCode().trimmingCharacters(in: .whitespacesAndNewlines) - print("Generated:\n", generated) - - XCTAssertFalse(generated.isEmpty) -// -// XCTAssertTrue(generated.contains("MARK: - Models"), "MARK line should be present in generated code") -// XCTAssertTrue(generated.contains("Foo struct docs"), "Doc comment line should be present in generated code") -// // Ensure the struct declaration itself is still correct -// XCTAssertTrue(generated.contains("struct Foo")) -// XCTAssertTrue(generated.contains("bar"), "Variable declaration should be present") - } -} diff --git a/Tests/SwiftBuilderTests/SwiftBuilderLiteralTests.swift b/Tests/SwiftBuilderTests/SwiftBuilderLiteralTests.swift deleted file mode 100644 index 32354e8..0000000 --- a/Tests/SwiftBuilderTests/SwiftBuilderLiteralTests.swift +++ /dev/null @@ -1,14 +0,0 @@ -import XCTest -@testable import SwiftBuilder - -final class SwiftBuilderLiteralTests: XCTestCase { - func testGroupWithLiterals() { - let group = Group { - Return { - Literal.integer(1) - } - } - let generated = group.generateCode() - XCTAssertEqual(generated.trimmingCharacters(in: .whitespacesAndNewlines), "return 1") - } -} \ No newline at end of file diff --git a/Tests/SwiftBuilderTests/SwiftBuilderTestsA.swift b/Tests/SwiftBuilderTests/SwiftBuilderTestsA.swift deleted file mode 100644 index 6063008..0000000 --- a/Tests/SwiftBuilderTests/SwiftBuilderTestsA.swift +++ /dev/null @@ -1,43 +0,0 @@ -import XCTest -@testable import SwiftBuilder - -final class SwiftBuilderTestsA: XCTestCase { - func testBlackjackCardExample() throws { - let blackjackCard = Struct("BlackjackCard") { - Enum("Suit") { - EnumCase("spades").equals("♠") - EnumCase("hearts").equals("♡") - EnumCase("diamonds").equals("♢") - EnumCase("clubs").equals("♣") - }.inherits("Character") - } - - let expected = """ - struct BlackjackCard { - enum Suit: Character { - case spades = "♠" - case hearts = "♡" - case diamonds = "♢" - case clubs = "♣" - } - } - """ - - // Normalize whitespace, remove comments and modifiers, and normalize colon spacing - let normalizedGenerated = blackjackCard.syntax.description - .replacingOccurrences(of: "//.*$", with: "", options: .regularExpression) // Remove comments - .replacingOccurrences(of: "public\\s+", with: "", options: .regularExpression) // Remove public modifier - .replacingOccurrences(of: "\\s*:\\s*", with: ": ", options: .regularExpression) // Normalize colon spacing - .replacingOccurrences(of: "\\s+", with: " ", options: .regularExpression) // Normalize whitespace - .trimmingCharacters(in: .whitespacesAndNewlines) - - let normalizedExpected = expected - .replacingOccurrences(of: "//.*$", with: "", options: .regularExpression) // Remove comments - .replacingOccurrences(of: "public\\s+", with: "", options: .regularExpression) // Remove public modifier - .replacingOccurrences(of: "\\s*:\\s*", with: ": ", options: .regularExpression) // Normalize colon spacing - .replacingOccurrences(of: "\\s+", with: " ", options: .regularExpression) // Normalize whitespace - .trimmingCharacters(in: .whitespacesAndNewlines) - - XCTAssertEqual(normalizedGenerated, normalizedExpected) - } -} diff --git a/Tests/SwiftBuilderTests/SwiftBuilderTestsB.swift b/Tests/SwiftBuilderTests/SwiftBuilderTestsB.swift deleted file mode 100644 index eb1b702..0000000 --- a/Tests/SwiftBuilderTests/SwiftBuilderTestsB.swift +++ /dev/null @@ -1,228 +0,0 @@ -import XCTest -@testable import SwiftBuilder - -final class SwiftBuilderTestsB: XCTestCase { - func testBlackjackCardExample() throws { - let syntax = Struct("BlackjackCard") { - Enum("Suit") { - EnumCase("spades").equals("♠") - EnumCase("hearts").equals("♡") - EnumCase("diamonds").equals("♢") - EnumCase("clubs").equals("♣") - } - .inherits("Character") - - Enum("Rank") { - EnumCase("two").equals(2) - EnumCase("three") - EnumCase("four") - EnumCase("five") - EnumCase("six") - EnumCase("seven") - EnumCase("eight") - EnumCase("nine") - EnumCase("ten") - EnumCase("jack") - EnumCase("queen") - EnumCase("king") - EnumCase("ace") - } - .inherits("Int") - - Variable(.let, name: "rank", type: "Rank") - Variable(.let, name: "suit", type: "Suit") - } - - let expected = """ - struct BlackjackCard { - enum Suit: Character { - case spades = "♠" - case hearts = "♡" - case diamonds = "♢" - case clubs = "♣" - } - enum Rank: Int { - case two = 2 - case three - case four - case five - case six - case seven - case eight - case nine - case ten - case jack - case queen - case king - case ace - } - let rank: Rank - let suit: Suit - } - """ - - // Normalize whitespace, remove comments and modifiers, and normalize colon spacing - let normalizedGenerated = syntax.syntax.description - .replacingOccurrences(of: "//.*$", with: "", options: String.CompareOptions.regularExpression) - .replacingOccurrences(of: "public\\s+", with: "", options: String.CompareOptions.regularExpression) - .replacingOccurrences(of: "\\s*:\\s*", with: ": ", options: String.CompareOptions.regularExpression) - .replacingOccurrences(of: "\\s+", with: " ", options: String.CompareOptions.regularExpression) - .trimmingCharacters(in: CharacterSet.whitespacesAndNewlines) - - let normalizedExpected = expected - .replacingOccurrences(of: "//.*$", with: "", options: String.CompareOptions.regularExpression) - .replacingOccurrences(of: "public\\s+", with: "", options: String.CompareOptions.regularExpression) - .replacingOccurrences(of: "\\s*:\\s*", with: ": ", options: String.CompareOptions.regularExpression) - .replacingOccurrences(of: "\\s+", with: " ", options: String.CompareOptions.regularExpression) - .trimmingCharacters(in: CharacterSet.whitespacesAndNewlines) - - XCTAssertEqual(normalizedGenerated, normalizedExpected) - } - - func testFullBlackjackCardExample() throws { - let syntax = Struct("BlackjackCard") { - Enum("Suit") { - EnumCase("spades").equals("♠") - EnumCase("hearts").equals("♡") - EnumCase("diamonds").equals("♢") - EnumCase("clubs").equals("♣") - } - .inherits("Character") - - Enum("Rank") { - EnumCase("two").equals(2) - EnumCase("three") - EnumCase("four") - EnumCase("five") - EnumCase("six") - EnumCase("seven") - EnumCase("eight") - EnumCase("nine") - EnumCase("ten") - EnumCase("jack") - EnumCase("queen") - EnumCase("king") - EnumCase("ace") - Struct("Values") { - Variable(.let, name: "first", type: "Int") - Variable(.let, name: "second", type: "Int?") - } - ComputedProperty("values", type: "Values") { - Switch("self") { - SwitchCase(".ace") { - Return { - Init("Values") { - Parameter(name: "first", type: "", defaultValue: "1") - Parameter(name: "second", type: "", defaultValue: "11") - } - } - } - SwitchCase(".jack", ".queen", ".king") { - Return { - Init("Values") { - Parameter(name: "first", type: "", defaultValue: "10") - Parameter(name: "second", type: "", defaultValue: "nil") - } - } - } - Default { - Return { - Init("Values") { - Parameter(name: "first", type: "", defaultValue: "self.rawValue") - Parameter(name: "second", type: "", defaultValue: "nil") - } - } - } - } - } - } - .inherits("Int") - - Variable(.let, name: "rank", type: "Rank") - Variable(.let, name: "suit", type: "Suit") - ComputedProperty("description", type: "String") { - VariableDecl(.var, name: "output", equals: "\"suit is \\(suit.rawValue),\"") - PlusAssign("output", "\" value is \\(rank.values.first)\"") - If( - Let("second", "rank.values.second"), then: { - PlusAssign("output", "\" or \\(second)\"") - - }) - Return { - VariableExp("output") - } - } - } - - let expected = """ - struct BlackjackCard { - enum Suit: Character { - case spades = \"♠\" - case hearts = \"♡\" - case diamonds = \"♢\" - case clubs = \"♣\" - } - - enum Rank: Int { - case two = 2 - case three - case four - case five - case six - case seven - case eight - case nine - case ten - case jack - case queen - case king - case ace - - struct Values { - let first: Int - let second: Int? - } - - var values: Values { - switch self { - case .ace: - return Values(first: 1, second: 11) - case .jack, .queen, .king: - return Values(first: 10, second: nil) - default: - return Values(first: self.rawValue, second: nil) - } - } - } - - let rank: Rank - let suit: Suit - var description: String { - var output = \"suit is \\(suit.rawValue),\" - output += \" value is \\(rank.values.first)\" - if let second = rank.values.second { - output += \" or \\(second)\" - } - return output - } - } - """ - - // Normalize whitespace, remove comments and modifiers, and normalize colon spacing - let normalizedGenerated = syntax.syntax.description - .replacingOccurrences(of: "//.*$", with: "", options: String.CompareOptions.regularExpression) - .replacingOccurrences(of: "public\\s+", with: "", options: String.CompareOptions.regularExpression) - .replacingOccurrences(of: "\\s*:\\s*", with: ": ", options: String.CompareOptions.regularExpression) - .replacingOccurrences(of: "\\s+", with: " ", options: String.CompareOptions.regularExpression) - .trimmingCharacters(in: CharacterSet.whitespacesAndNewlines) - - let normalizedExpected = expected - .replacingOccurrences(of: "//.*$", with: "", options: String.CompareOptions.regularExpression) - .replacingOccurrences(of: "public\\s+", with: "", options: String.CompareOptions.regularExpression) - .replacingOccurrences(of: "\\s*:\\s*", with: ": ", options: String.CompareOptions.regularExpression) - .replacingOccurrences(of: "\\s+", with: " ", options: String.CompareOptions.regularExpression) - .trimmingCharacters(in: CharacterSet.whitespacesAndNewlines) - - XCTAssertEqual(normalizedGenerated, normalizedExpected) - } -} diff --git a/Tests/SwiftBuilderTests/SwiftBuilderTestsC.swift b/Tests/SwiftBuilderTests/SwiftBuilderTestsC.swift deleted file mode 100644 index 9471921..0000000 --- a/Tests/SwiftBuilderTests/SwiftBuilderTestsC.swift +++ /dev/null @@ -1,104 +0,0 @@ -import XCTest -@testable import SwiftBuilder - -final class SwiftBuilderTestsC: XCTestCase { - func testBasicFunction() throws { - let function = Function("calculateSum", returns: "Int") { - Parameter(name: "a", type: "Int") - Parameter(name: "b", type: "Int") - } _: { - Return { - VariableExp("a + b") - } - } - - let expected = """ - func calculateSum(a: Int, b: Int) -> Int { - return a + b - } - """ - - // Normalize whitespace, remove comments and modifiers, and normalize colon spacing - let normalizedGenerated = function.syntax.description - .replacingOccurrences(of: "//.*$", with: "", options: .regularExpression) // Remove comments - .replacingOccurrences(of: "public\\s+", with: "", options: .regularExpression) // Remove public modifier - .replacingOccurrences(of: "\\s*:\\s*", with: ": ", options: .regularExpression) // Normalize colon spacing - .replacingOccurrences(of: "\\s+", with: " ", options: .regularExpression) // Normalize whitespace - .trimmingCharacters(in: .whitespacesAndNewlines) - - let normalizedExpected = expected - .replacingOccurrences(of: "//.*$", with: "", options: .regularExpression) // Remove comments - .replacingOccurrences(of: "public\\s+", with: "", options: .regularExpression) // Remove public modifier - .replacingOccurrences(of: "\\s*:\\s*", with: ": ", options: .regularExpression) // Normalize colon spacing - .replacingOccurrences(of: "\\s+", with: " ", options: .regularExpression) // Normalize whitespace - .trimmingCharacters(in: .whitespacesAndNewlines) - - XCTAssertEqual(normalizedGenerated, normalizedExpected) - } - - func testStaticFunction() throws { - let function = Function("createInstance", returns: "MyType", { - Parameter(name: "value", type: "String") - }) { - Return { - Init("MyType") { - Parameter(name: "value", type: "String") - } - } - }.static() - - let expected = """ - static func createInstance(value: String) -> MyType { - return MyType(value: value) - } - """ - - // Normalize whitespace, remove comments and modifiers, and normalize colon spacing - let normalizedGenerated = function.syntax.description - .replacingOccurrences(of: "//.*$", with: "", options: .regularExpression) // Remove comments - .replacingOccurrences(of: "public\\s+", with: "", options: .regularExpression) // Remove public modifier - .replacingOccurrences(of: "\\s*:\\s*", with: ": ", options: .regularExpression) // Normalize colon spacing - .replacingOccurrences(of: "\\s+", with: " ", options: .regularExpression) // Normalize whitespace - .trimmingCharacters(in: .whitespacesAndNewlines) - - let normalizedExpected = expected - .replacingOccurrences(of: "//.*$", with: "", options: .regularExpression) // Remove comments - .replacingOccurrences(of: "public\\s+", with: "", options: .regularExpression) // Remove public modifier - .replacingOccurrences(of: "\\s*:\\s*", with: ": ", options: .regularExpression) // Normalize colon spacing - .replacingOccurrences(of: "\\s+", with: " ", options: .regularExpression) // Normalize whitespace - .trimmingCharacters(in: .whitespacesAndNewlines) - - XCTAssertEqual(normalizedGenerated, normalizedExpected) - } - - func testMutatingFunction() throws { - let function = Function("updateValue", { - Parameter(name: "newValue", type: "String") - }) { - Assignment("value", "newValue") - }.mutating() - - let expected = """ - mutating func updateValue(newValue: String) { - value = newValue - } - """ - - // Normalize whitespace, remove comments and modifiers, and normalize colon spacing - let normalizedGenerated = function.syntax.description - .replacingOccurrences(of: "//.*$", with: "", options: .regularExpression) // Remove comments - .replacingOccurrences(of: "public\\s+", with: "", options: .regularExpression) // Remove public modifier - .replacingOccurrences(of: "\\s*:\\s*", with: ": ", options: .regularExpression) // Normalize colon spacing - .replacingOccurrences(of: "\\s+", with: " ", options: .regularExpression) // Normalize whitespace - .trimmingCharacters(in: .whitespacesAndNewlines) - - let normalizedExpected = expected - .replacingOccurrences(of: "//.*$", with: "", options: .regularExpression) // Remove comments - .replacingOccurrences(of: "public\\s+", with: "", options: .regularExpression) // Remove public modifier - .replacingOccurrences(of: "\\s*:\\s*", with: ": ", options: .regularExpression) // Normalize colon spacing - .replacingOccurrences(of: "\\s+", with: " ", options: .regularExpression) // Normalize whitespace - .trimmingCharacters(in: .whitespacesAndNewlines) - - XCTAssertEqual(normalizedGenerated, normalizedExpected) - } -} diff --git a/Tests/SwiftBuilderTests/SwiftBuilderTestsD.swift b/Tests/SwiftBuilderTests/SwiftBuilderTestsD.swift deleted file mode 100644 index 2c2a9d8..0000000 --- a/Tests/SwiftBuilderTests/SwiftBuilderTestsD.swift +++ /dev/null @@ -1,107 +0,0 @@ -import XCTest -@testable import SwiftBuilder - -final class SwiftBuilderTestsD: XCTestCase { - func normalize(_ code: String) -> String { - return code - .replacingOccurrences(of: "//.*$", with: "", options: .regularExpression) // Remove comments - .replacingOccurrences(of: "public\\s+", with: "", options: .regularExpression) // Remove public modifier - .replacingOccurrences(of: "\\s*:\\s*", with: ": ", options: .regularExpression) // Normalize colon spacing - .replacingOccurrences(of: "\\s+", with: " ", options: .regularExpression) // Normalize whitespace - .trimmingCharacters(in: .whitespacesAndNewlines) - } - - func testGenericStruct() { - let stackStruct = Struct("Stack", generic: "Element") { - Variable(.var, name: "items", type: "[Element]", equals: "[]") - - Function("push") { - Parameter(name: "item", type: "Element", isUnnamed: true) - } _: { - VariableExp("items").call("append") { - ParameterExp(name: "", value: "item") - } - }.mutating() - - Function("pop", returns: "Element?") { - Return { VariableExp("items").call("popLast") } - }.mutating() - - Function("peek", returns: "Element?") { - Return { VariableExp("items").property("last") } - } - - ComputedProperty("isEmpty", type: "Bool") { - Return { VariableExp("items").property("isEmpty") } - } - - ComputedProperty("count", type: "Int") { - Return { VariableExp("items").property("count") } - } - } - - let expectedCode = """ - struct Stack { - var items: [Element] = [] - - mutating func push(_ item: Element) { - items.append(item) - } - - mutating func pop() -> Element? { - return items.popLast() - } - - func peek() -> Element? { - return items.last - } - - var isEmpty: Bool { - return items.isEmpty - } - - var count: Int { - return items.count - } - } - """ - - let normalizedGenerated = normalize(stackStruct.generateCode()) - let normalizedExpected = normalize(expectedCode) - XCTAssertEqual(normalizedGenerated, normalizedExpected) - } - - func testGenericStructWithInheritance() { - let containerStruct = Struct("Container", generic: "T") { - Variable(.var, name: "value", type: "T") - }.inherits("Equatable") - - let expectedCode = """ - struct Container: Equatable { - var value: T - } - """ - - let normalizedGenerated = normalize(containerStruct.generateCode()) - let normalizedExpected = normalize(expectedCode) - XCTAssertEqual(normalizedGenerated, normalizedExpected) - } - - func testNonGenericStruct() { - let simpleStruct = Struct("Point") { - Variable(.var, name: "x", type: "Double") - Variable(.var, name: "y", type: "Double") - } - - let expectedCode = """ - struct Point { - var x: Double - var y: Double - } - """ - - let normalizedGenerated = normalize(simpleStruct.generateCode()) - let normalizedExpected = normalize(expectedCode) - XCTAssertEqual(normalizedGenerated, normalizedExpected) - } -} diff --git a/Tests/SyntaxKitTests/AssertionMigrationTests.swift b/Tests/SyntaxKitTests/AssertionMigrationTests.swift new file mode 100644 index 0000000..a6d03f2 --- /dev/null +++ b/Tests/SyntaxKitTests/AssertionMigrationTests.swift @@ -0,0 +1,79 @@ +import Testing + +@testable import SyntaxKit + +/// Tests specifically focused on assertion migration from XCTest to Swift Testing +/// Ensures all assertion patterns from the original tests work correctly with #expect() +struct AssertionMigrationTests { + // MARK: - XCTAssertEqual Migration Tests + + @Test func testEqualityAssertionMigration() throws { + // Test the most common migration: XCTAssertEqual -> #expect(a == b) + let function = Function("test", returns: "String") { + Return { + Literal.string("hello") + } + } + + let generated = function.syntax.description + .replacingOccurrences(of: "\\s+", with: " ", options: .regularExpression) + .trimmingCharacters(in: .whitespacesAndNewlines) + + let expected = "func test() -> String { return \"hello\" }" + .replacingOccurrences(of: "\\s+", with: " ", options: .regularExpression) + .trimmingCharacters(in: .whitespacesAndNewlines) + + // This replaces: XCTAssertEqual(generated, expected) + #expect(generated == expected) + } + + // MARK: - XCTAssertFalse Migration Tests + + @Test func testFalseAssertionMigration() { + let syntax = Group { + Variable(.let, name: "test", type: "String", equals: "\"value\"") + } + + let generated = syntax.generateCode().trimmingCharacters(in: .whitespacesAndNewlines) + + // This replaces: XCTAssertFalse(generated.isEmpty) + #expect(!generated.isEmpty) + } + + // MARK: - Complex Assertion Migration Tests + + @Test func testNormalizedStringComparisonMigration() throws { + let blackjackCard = Struct("Card") { + Enum("Suit") { + EnumCase("hearts").equals("♡") + EnumCase("spades").equals("♠") + }.inherits("Character") + } + + let expected = """ + struct Card { + enum Suit: Character { + case hearts = "♡" + case spades = "♠" + } + } + """ + + // Test the complete normalization pipeline that was used in XCTest + let normalizedGenerated = blackjackCard.syntax.description.normalize() + + let normalizedExpected = expected.normalize() + + // This replaces: XCTAssertEqual(normalizedGenerated, normalizedExpected) + #expect(normalizedGenerated == normalizedExpected) + } + + @Test func testMultipleAssertionsInSingleTest() { + let generated = "struct Test { var value: Int }" + + // Test multiple assertions in one test method + #expect(!generated.isEmpty) + #expect(generated.contains("struct Test")) + #expect(generated.contains("var value: Int")) + } +} diff --git a/Tests/SyntaxKitTests/BasicTests.swift b/Tests/SyntaxKitTests/BasicTests.swift new file mode 100644 index 0000000..bfc9433 --- /dev/null +++ b/Tests/SyntaxKitTests/BasicTests.swift @@ -0,0 +1,34 @@ +import Testing + +@testable import SyntaxKit + +struct BasicTests { + @Test func testBlackjackCardExample() throws { + let blackjackCard = Struct("BlackjackCard") { + Enum("Suit") { + EnumCase("spades").equals("♠") + EnumCase("hearts").equals("♡") + EnumCase("diamonds").equals("♢") + EnumCase("clubs").equals("♣") + }.inherits("Character") + } + + let expected = """ + struct BlackjackCard { + enum Suit: Character { + case spades = "♠" + case hearts = "♡" + case diamonds = "♢" + case clubs = "♣" + } + } + """ + + // Normalize whitespace, remove comments and modifiers, and normalize colon spacing + let normalizedGenerated = blackjackCard.syntax.description.normalize() + + let normalizedExpected = expected.normalize() + + #expect(normalizedGenerated == normalizedExpected) + } +} diff --git a/Tests/SyntaxKitTests/BlackjackTests.swift b/Tests/SyntaxKitTests/BlackjackTests.swift new file mode 100644 index 0000000..16ab0d0 --- /dev/null +++ b/Tests/SyntaxKitTests/BlackjackTests.swift @@ -0,0 +1,212 @@ +import Testing + +@testable import SyntaxKit + +struct BlackjackTests { + @Test func testBlackjackCardExample() throws { + let syntax = Struct("BlackjackCard") { + Enum("Suit") { + EnumCase("spades").equals("♠") + EnumCase("hearts").equals("♡") + EnumCase("diamonds").equals("♢") + EnumCase("clubs").equals("♣") + } + .inherits("Character") + + Enum("Rank") { + EnumCase("two").equals(2) + EnumCase("three") + EnumCase("four") + EnumCase("five") + EnumCase("six") + EnumCase("seven") + EnumCase("eight") + EnumCase("nine") + EnumCase("ten") + EnumCase("jack") + EnumCase("queen") + EnumCase("king") + EnumCase("ace") + } + .inherits("Int") + + Variable(.let, name: "rank", type: "Rank") + Variable(.let, name: "suit", type: "Suit") + } + + let expected = """ + struct BlackjackCard { + enum Suit: Character { + case spades = "♠" + case hearts = "♡" + case diamonds = "♢" + case clubs = "♣" + } + enum Rank: Int { + case two = 2 + case three + case four + case five + case six + case seven + case eight + case nine + case ten + case jack + case queen + case king + case ace + } + let rank: Rank + let suit: Suit + } + """ + + // Normalize whitespace, remove comments and modifiers, and normalize colon spacing + let normalizedGenerated = syntax.syntax.description.normalize() + + let normalizedExpected = expected.normalize() + + #expect(normalizedGenerated == normalizedExpected) + } + + // swiftlint:disable:next function_body_length + @Test func testFullBlackjackCardExample() throws { + // swiftlint:disable:next closure_body_length + let syntax = Struct("BlackjackCard") { + Enum("Suit") { + EnumCase("spades").equals("♠") + EnumCase("hearts").equals("♡") + EnumCase("diamonds").equals("♢") + EnumCase("clubs").equals("♣") + } + .inherits("Character") + + Enum("Rank") { + EnumCase("two").equals(2) + EnumCase("three") + EnumCase("four") + EnumCase("five") + EnumCase("six") + EnumCase("seven") + EnumCase("eight") + EnumCase("nine") + EnumCase("ten") + EnumCase("jack") + EnumCase("queen") + EnumCase("king") + EnumCase("ace") + Struct("Values") { + Variable(.let, name: "first", type: "Int") + Variable(.let, name: "second", type: "Int?") + } + ComputedProperty("values", type: "Values") { + Switch("self") { + SwitchCase(".ace") { + Return { + Init("Values") { + Parameter(name: "first", type: "", defaultValue: "1") + Parameter(name: "second", type: "", defaultValue: "11") + } + } + } + SwitchCase(".jack", ".queen", ".king") { + Return { + Init("Values") { + Parameter(name: "first", type: "", defaultValue: "10") + Parameter(name: "second", type: "", defaultValue: "nil") + } + } + } + Default { + Return { + Init("Values") { + Parameter(name: "first", type: "", defaultValue: "self.rawValue") + Parameter(name: "second", type: "", defaultValue: "nil") + } + } + } + } + } + } + .inherits("Int") + + Variable(.let, name: "rank", type: "Rank") + Variable(.let, name: "suit", type: "Suit") + ComputedProperty("description", type: "String") { + VariableDecl(.var, name: "output", equals: "\"suit is \\(suit.rawValue),\"") + PlusAssign("output", "\" value is \\(rank.values.first)\"") + If( + Let("second", "rank.values.second"), + then: { + PlusAssign("output", "\" or \\(second)\"") + } + ) + Return { + VariableExp("output") + } + } + } + + let expected = """ + struct BlackjackCard { + enum Suit: Character { + case spades = \"♠\" + case hearts = \"♡\" + case diamonds = \"♢\" + case clubs = \"♣\" + } + + enum Rank: Int { + case two = 2 + case three + case four + case five + case six + case seven + case eight + case nine + case ten + case jack + case queen + case king + case ace + + struct Values { + let first: Int + let second: Int? + } + + var values: Values { + switch self { + case .ace: + return Values(first: 1, second: 11) + case .jack, .queen, .king: + return Values(first: 10, second: nil) + default: + return Values(first: self.rawValue, second: nil) + } + } + } + + let rank: Rank + let suit: Suit + var description: String { + var output = \"suit is \\(suit.rawValue),\" + output += \" value is \\(rank.values.first)\" + if let second = rank.values.second { + output += \" or \\(second)\" + } + return output + } + } + """ + + // Normalize whitespace, remove comments and modifiers, and normalize colon spacing + let normalizedGenerated = syntax.syntax.description.normalize() + + let normalizedExpected = expected.normalize() + + #expect(normalizedGenerated == normalizedExpected) + } +} diff --git a/Tests/SyntaxKitTests/CodeStyleMigrationTests.swift b/Tests/SyntaxKitTests/CodeStyleMigrationTests.swift new file mode 100644 index 0000000..08aea89 --- /dev/null +++ b/Tests/SyntaxKitTests/CodeStyleMigrationTests.swift @@ -0,0 +1,81 @@ +import Testing + +@testable import SyntaxKit + +/// Tests for code style and API simplification changes introduced during Swift Testing migration +/// Validates the simplified Swift APIs and formatting changes +struct CodeStyleMigrationTests { + // MARK: - CharacterSet Simplification Tests + + @Test func testCharacterSetSimplification() { + // Test that .whitespacesAndNewlines works instead of CharacterSet.whitespacesAndNewlines + let testString = "\n test content \n\t" + + // Old style: CharacterSet.whitespacesAndNewlines + // New style: .whitespacesAndNewlines + let trimmed = testString.trimmingCharacters(in: .whitespacesAndNewlines) + + #expect(trimmed == "test content") + } + + // MARK: - Indentation and Formatting Tests + + @Test func testConsistentIndentationInMigratedCode() throws { + // Test that the indentation changes in the migrated code work correctly + let syntax = Struct("IndentationTest") { + Variable(.let, name: "property1", type: "String") + Variable(.let, name: "property2", type: "Int") + + Function("method") { + Parameter(name: "param", type: "String") + } _: { + VariableDecl(.let, name: "local", equals: "\"value\"") + Return { + VariableExp("local") + } + } + } + + let generated = syntax.generateCode().normalize() + + // Verify proper indentation is maintained + #expect( + generated + == "struct IndentationTest { let property1: String let property2: Int func method(param: String) { let local = \"value\" return local } }" + ) + } + + // MARK: - Multiline String Formatting Tests + + @Test func testMultilineStringFormatting() { + let expected = """ + struct TestStruct { + let value: String + var count: Int + } + """ + + let syntax = Struct("TestStruct") { + Variable(.let, name: "value", type: "String") + Variable(.var, name: "count", type: "Int") + } + + let normalized = syntax.generateCode().normalize() + + let expectedNormalized = expected.normalize() + + #expect(normalized == expectedNormalized) + } + + @Test func testMigrationPreservesCodeGeneration() { + // Ensure that the style changes don't break core functionality + let group = Group { + Return { + Literal.string("migrated") + } + } + + let generated = group.generateCode().trimmingCharacters(in: .whitespacesAndNewlines) + #expect(generated == "return \"migrated\"") + } +} diff --git a/Tests/SyntaxKitTests/CommentTests.swift b/Tests/SyntaxKitTests/CommentTests.swift new file mode 100644 index 0000000..331852e --- /dev/null +++ b/Tests/SyntaxKitTests/CommentTests.swift @@ -0,0 +1,112 @@ +import Testing + +@testable import SyntaxKit + +struct CommentTests { + // swiftlint:disable:next function_body_length + @Test func testCommentInjection() { + // swiftlint:disable:next closure_body_length + let syntax = Group { + Struct("Card") { + Variable(.let, name: "rank", type: "Rank") + .comment { + Line(.doc, "The rank of the card (2-10, J, Q, K, A)") + } + Variable(.let, name: "suit", type: "Suit") + .comment { + Line(.doc, "The suit of the card (hearts, diamonds, clubs, spades)") + } + } + .inherits("Comparable") + .comment { + Line("MARK: - Models") + Line(.doc, "Represents a playing card in a standard 52-card deck") + Line(.doc) + Line( + .doc, "A card has a rank (2-10, J, Q, K, A) and a suit (hearts, diamonds, clubs, spades)." + ) + Line(.doc, "Each card can be compared to other cards based on its rank.") + } + + Enum("Rank") { + EnumCase("two").equals(2) + EnumCase("three") + EnumCase("four") + EnumCase("five") + EnumCase("six") + EnumCase("seven") + EnumCase("eight") + EnumCase("nine") + EnumCase("ten") + EnumCase("jack") + EnumCase("queen") + EnumCase("king") + EnumCase("ace") + Struct("Values") { + Variable(.let, name: "first", type: "Int") + Variable(.let, name: "second", type: "Int?") + } + ComputedProperty("description", type: "String") { + Switch("self") { + SwitchCase(".jack") { + Return { + Literal.string("J") + } + } + SwitchCase(".queen") { + Return { + Literal.string("Q") + } + } + SwitchCase(".king") { + Return { + Literal.string("K") + } + } + SwitchCase(".ace") { + Return { + Literal.string("A") + } + } + Default { + Return { + Literal.string("\\(rawValue)") + } + } + } + } + .comment { + Line(.doc, "Returns a string representation of the rank") + } + } + .inherits("Int") + .inherits("CaseIterable") + .comment { + Line("MARK: - Enums") + Line(.doc, "Represents the possible ranks of a playing card") + } + + Enum("Suit") { + EnumCase("spades").equals("♠") + EnumCase("hearts").equals("♡") + EnumCase("diamonds").equals("♢") + EnumCase("clubs").equals("♣") + } + .inherits("String") + .inherits("CaseIterable") + .comment { + Line(.doc, "Represents the possible suits of a playing card") + } + } + + let generated = syntax.generateCode().trimmingCharacters(in: .whitespacesAndNewlines) + + #expect(!generated.isEmpty) + // + // #expect(generated.contains("MARK: - Models"), "MARK line should be present in generated code") + // #expect(generated.contains("Foo struct docs"), "Doc comment line should be present in generated code") + // // Ensure the struct declaration itself is still correct + // #expect(generated.contains("struct Foo")) + // #expect(generated.contains("bar"), "Variable declaration should be present") + } +} diff --git a/Tests/SyntaxKitTests/FrameworkCompatibilityTests.swift b/Tests/SyntaxKitTests/FrameworkCompatibilityTests.swift new file mode 100644 index 0000000..d16bdc0 --- /dev/null +++ b/Tests/SyntaxKitTests/FrameworkCompatibilityTests.swift @@ -0,0 +1,148 @@ +import Testing + +@testable import SyntaxKit + +/// Tests to ensure compatibility and feature parity between XCTest and Swift Testing +/// Validates that the migration maintains all testing capabilities +struct FrameworkCompatibilityTests { + // MARK: - Test Organization Migration Tests + + @Test func testStructBasedOrganization() { + // Test that struct-based test organization works + // This replaces: final class TestClass: XCTestCase + let testExecuted = true + #expect(testExecuted) + } + + @Test func testMethodAnnotationMigration() throws { + // Test that @Test annotation works with throws + // This replaces: func testMethod() throws + let syntax = Enum("TestEnum") { + EnumCase("first") + EnumCase("second") + } + + let generated = syntax.syntax.description + #expect(!generated.isEmpty) + #expect(generated.contains("enum TestEnum")) + } + + // MARK: - Error Handling Compatibility Tests + + @Test func testThrowingTestCompatibility() throws { + // Ensure throws declaration works properly with @Test + let function = Function("throwingFunction", returns: "String") { + Parameter(name: "input", type: "String") + } _: { + Return { + VariableExp("input.uppercased()") + } + } + + let generated = try function.syntax.description + #expect(generated.contains("func throwingFunction")) + } + + // MARK: - Complex DSL Compatibility Tests + + @Test func testFullBlackjackCompatibility() throws { + // Test complex DSL patterns work with new framework + let syntax = Struct("BlackjackCard") { + Enum("Suit") { + EnumCase("spades").equals("♠") + EnumCase("hearts").equals("♡") + EnumCase("diamonds").equals("♢") + EnumCase("clubs").equals("♣") + }.inherits("Character") + + Enum("Rank") { + EnumCase("ace").equals(1) + EnumCase("two").equals(2) + EnumCase("jack").equals(11) + EnumCase("queen").equals(12) + EnumCase("king").equals(13) + }.inherits("Int") + + Variable(.let, name: "rank", type: "Rank") + Variable(.let, name: "suit", type: "Suit") + } + + let generated = syntax.syntax.description + let normalized = generated.normalize() + + // Validate all components are present + #expect(normalized.contains("struct BlackjackCard")) + #expect(normalized.contains("enum Suit: Character")) + #expect(normalized.contains("enum Rank: Int")) + #expect(normalized.contains("let rank: Rank")) + #expect(normalized.contains("let suit: Suit")) + } + + // MARK: - Function Generation Compatibility Tests + + @Test func testFunctionGenerationCompatibility() throws { + let function = Function("calculateValue", returns: "Int") { + Parameter(name: "multiplier", type: "Int") + Parameter(name: "base", type: "Int", defaultValue: "10") + } _: { + Return { + VariableExp("multiplier * base") + } + } + + let generated = function.syntax.description + let normalized = + generated + .normalize() + + #expect(normalized.contains("func calculateValue(multiplier: Int, base: Int = 10) -> Int")) + #expect(normalized.contains("return multiplier * base")) + } + + // MARK: - Comment Injection Compatibility Tests + + @Test func testCommentInjectionCompatibility() { + let syntax = Struct("DocumentedStruct") { + Variable(.let, name: "value", type: "String") + .comment { + Line(.doc, "The main value of the struct") + } + }.comment { + Line("MARK: - Data Models") + Line(.doc, "A documented struct for testing") + } + + let generated = syntax.generateCode() + + #expect(!generated.isEmpty) + #expect(generated.contains("struct DocumentedStruct")) + #expect(generated.normalize().contains("let value: String".normalize())) + } + + // MARK: - Migration Regression Tests + + @Test func testNoRegressionInCodeGeneration() { + // Ensure migration doesn't introduce regressions + let simpleStruct = Struct("Point") { + Variable(.var, name: "x", type: "Double", equals: "0.0") + Variable(.var, name: "y", type: "Double", equals: "0.0") + } + + let generated = simpleStruct.generateCode().normalize() + + #expect(generated.contains("struct Point")) + #expect(generated.contains("var x: Double = 0.0".normalize())) + #expect(generated.contains("var y: Double = 0.0".normalize())) + } + + @Test func testLiteralGeneration() { + let group = Group { + Return { + Literal.integer(100) + } + } + + let generated = group.generateCode().trimmingCharacters(in: .whitespacesAndNewlines) + #expect(generated == "return 100") + } +} diff --git a/Tests/SyntaxKitTests/FunctionTests.swift b/Tests/SyntaxKitTests/FunctionTests.swift new file mode 100644 index 0000000..797002e --- /dev/null +++ b/Tests/SyntaxKitTests/FunctionTests.swift @@ -0,0 +1,81 @@ +import Testing + +@testable import SyntaxKit + +struct FunctionTests { + @Test func testBasicFunction() throws { + let function = Function("calculateSum", returns: "Int") { + Parameter(name: "a", type: "Int") + Parameter(name: "b", type: "Int") + } _: { + Return { + VariableExp("a + b") + } + } + + let expected = """ + func calculateSum(a: Int, b: Int) -> Int { + return a + b + } + """ + + // Normalize whitespace, remove comments and modifiers, and normalize colon spacing + let normalizedGenerated = function.syntax.description.normalize() + + let normalizedExpected = expected.normalize() + + #expect(normalizedGenerated == normalizedExpected) + } + + @Test func testStaticFunction() throws { + let function = Function( + "createInstance", returns: "MyType", + { + Parameter(name: "value", type: "String") + } + ) { + Return { + Init("MyType") { + Parameter(name: "value", type: "String") + } + } + }.static() + + let expected = """ + static func createInstance(value: String) -> MyType { + return MyType(value: value) + } + """ + + // Normalize whitespace, remove comments and modifiers, and normalize colon spacing + let normalizedGenerated = function.syntax.description.normalize() + + let normalizedExpected = expected.normalize() + + #expect(normalizedGenerated == normalizedExpected) + } + + @Test func testMutatingFunction() throws { + let function = Function( + "updateValue", + { + Parameter(name: "newValue", type: "String") + } + ) { + Assignment("value", "newValue") + }.mutating() + + let expected = """ + mutating func updateValue(newValue: String) { + value = newValue + } + """ + + // Normalize whitespace, remove comments and modifiers, and normalize colon spacing + let normalizedGenerated = function.syntax.description.normalize() + + let normalizedExpected = expected.normalize() + + #expect(normalizedGenerated == normalizedExpected) + } +} diff --git a/Tests/SyntaxKitTests/LiteralTests.swift b/Tests/SyntaxKitTests/LiteralTests.swift new file mode 100644 index 0000000..255675a --- /dev/null +++ b/Tests/SyntaxKitTests/LiteralTests.swift @@ -0,0 +1,15 @@ +import Testing + +@testable import SyntaxKit + +struct LiteralTests { + @Test func testGroupWithLiterals() { + let group = Group { + Return { + Literal.integer(1) + } + } + let generated = group.generateCode() + #expect(generated.trimmingCharacters(in: .whitespacesAndNewlines) == "return 1") + } +} diff --git a/Tests/SyntaxKitTests/MigrationTests.swift b/Tests/SyntaxKitTests/MigrationTests.swift new file mode 100644 index 0000000..ddcc5a7 --- /dev/null +++ b/Tests/SyntaxKitTests/MigrationTests.swift @@ -0,0 +1,131 @@ +import Testing + +@testable import SyntaxKit + +/// Tests specifically for verifying the Swift Testing framework migration +/// These tests ensure that the migration from XCTest to Swift Testing works correctly +struct MigrationTests { + // MARK: - Basic Test Structure Migration Tests + + @Test func testStructBasedTestExecution() { + // Test that struct-based tests execute properly + let result = true + #expect(result == true) + } + + @Test func testThrowingTestMethod() throws { + // Test that @Test works with throws declaration + let syntax = Struct("TestStruct") { + Variable(.let, name: "value", type: "String") + } + + let generated = syntax.syntax.description + #expect(!generated.isEmpty) + } + + // MARK: - Assertion Migration Tests + + @Test func testExpectEqualityAssertion() { + // Test #expect() replacement for XCTAssertEqual + let actual = "test" + let expected = "test" + #expect(actual == expected) + } + + @Test func testExpectBooleanAssertion() { + // Test #expect() replacement for XCTAssertTrue/XCTAssertFalse + let condition = true + #expect(condition) + #expect(!false) + } + + @Test func testExpectEmptyStringAssertion() { + // Test #expect() replacement for XCTAssertFalse(string.isEmpty) + let generated = "non-empty string" + #expect(!generated.isEmpty) + } + + // MARK: - Code Generation Testing with New Framework + + @Test func testBasicCodeGenerationWithNewFramework() throws { + let blackjackCard = Struct("BlackjackCard") { + Enum("Suit") { + EnumCase("spades").equals("♠") + EnumCase("hearts").equals("♡") + EnumCase("diamonds").equals("♢") + EnumCase("clubs").equals("♣") + }.inherits("Character") + } + + let expected = """ + struct BlackjackCard { + enum Suit: Character { + case spades = "♠" + case hearts = "♡" + case diamonds = "♢" + case clubs = "♣" + } + } + """ + + // Use the same normalization approach as existing tests + let normalizedGenerated = blackjackCard.syntax.description.normalize() + + let normalizedExpected = expected.normalize() + + #expect(normalizedGenerated == normalizedExpected) + } + + // MARK: - String Options Migration Tests + + @Test func testStringCompareOptionsSimplification() { + // Test that .regularExpression works instead of String.CompareOptions.regularExpression + let testString = "public func test() { }" + let result = testString.replacingOccurrences( + of: "public\\s+", with: "", options: .regularExpression) + let expected = "func test() { }" + #expect(result == expected) + } + + @Test func testCharacterSetSimplification() { + // Test that .whitespacesAndNewlines works instead of CharacterSet.whitespacesAndNewlines + let testString = " test \n" + let result = testString.trimmingCharacters(in: .whitespacesAndNewlines) + let expected = "test" + #expect(result == expected) + } + + // MARK: - Complex Code Generation Tests + + @Test func testComplexStructGeneration() throws { + let syntax = Struct("TestCard") { + Variable(.let, name: "rank", type: "String") + Variable(.let, name: "suit", type: "String") + + Function("description", returns: "String") { + Return { + VariableExp("\"\\(rank) of \\(suit)\"") + } + } + } + + let generated = syntax.syntax.description.normalize() + + // Verify generated code contains expected elements + #expect(generated.contains("struct TestCard".normalize())) + #expect(generated.contains("let rank: String".normalize())) + #expect(generated.contains("let suit: String".normalize())) + #expect(generated.contains("func description() -> String".normalize())) + } + + @Test func testMigrationBackwardCompatibility() { + // Ensure that the migrated tests maintain the same functionality + let group = Group { + Return { + Literal.integer(42) + } + } + let generated = group.generateCode() + #expect(generated.trimmingCharacters(in: .whitespacesAndNewlines) == "return 42") + } +} diff --git a/Tests/SyntaxKitTests/String+Normalize.swift b/Tests/SyntaxKitTests/String+Normalize.swift new file mode 100644 index 0000000..5343b37 --- /dev/null +++ b/Tests/SyntaxKitTests/String+Normalize.swift @@ -0,0 +1,11 @@ +import Foundation + +extension String { + func normalize() -> String { + self + .replacingOccurrences(of: "//.*$", with: "", options: .regularExpression) + .replacingOccurrences(of: "\\s*:\\s*", with: ": ", options: .regularExpression) + .replacingOccurrences(of: "\\s+", with: " ", options: .regularExpression) + .trimmingCharacters(in: .whitespacesAndNewlines) + } +} diff --git a/Tests/SyntaxKitTests/StructTests.swift b/Tests/SyntaxKitTests/StructTests.swift new file mode 100644 index 0000000..e578664 --- /dev/null +++ b/Tests/SyntaxKitTests/StructTests.swift @@ -0,0 +1,99 @@ +import Testing + +@testable import SyntaxKit + +struct StructTests { + @Test func testGenericStruct() { + let stackStruct = Struct("Stack", generic: "Element") { + Variable(.var, name: "items", type: "[Element]", equals: "[]") + + Function("push") { + Parameter(name: "item", type: "Element", isUnnamed: true) + } _: { + VariableExp("items").call("append") { + ParameterExp(name: "", value: "item") + } + }.mutating() + + Function("pop", returns: "Element?") { + Return { VariableExp("items").call("popLast") } + }.mutating() + + Function("peek", returns: "Element?") { + Return { VariableExp("items").property("last") } + } + + ComputedProperty("isEmpty", type: "Bool") { + Return { VariableExp("items").property("isEmpty") } + } + + ComputedProperty("count", type: "Int") { + Return { VariableExp("items").property("count") } + } + } + + let expectedCode = """ + struct Stack { + var items: [Element] = [] + + mutating func push(_ item: Element) { + items.append(item) + } + + mutating func pop() -> Element? { + return items.popLast() + } + + func peek() -> Element? { + return items.last + } + + var isEmpty: Bool { + return items.isEmpty + } + + var count: Int { + return items.count + } + } + """ + + let normalizedGenerated = stackStruct.generateCode().normalize() + let normalizedExpected = expectedCode.normalize() + #expect(normalizedGenerated == normalizedExpected) + } + + @Test func testGenericStructWithInheritance() { + let containerStruct = Struct("Container", generic: "T") { + Variable(.var, name: "value", type: "T") + }.inherits("Equatable") + + let expectedCode = """ + struct Container: Equatable { + var value: T + } + """ + + let normalizedGenerated = containerStruct.generateCode().normalize() + let normalizedExpected = expectedCode.normalize() + #expect(normalizedGenerated == normalizedExpected) + } + + @Test func testNonGenericStruct() { + let simpleStruct = Struct("Point") { + Variable(.var, name: "x", type: "Double") + Variable(.var, name: "y", type: "Double") + } + + let expectedCode = """ + struct Point { + var x: Double + var y: Double + } + """ + + let normalizedGenerated = simpleStruct.generateCode().normalize() + let normalizedExpected = expectedCode.normalize() + #expect(normalizedGenerated == normalizedExpected) + } +} diff --git a/codecov.yml b/codecov.yml new file mode 100644 index 0000000..951b97b --- /dev/null +++ b/codecov.yml @@ -0,0 +1,2 @@ +ignore: + - "Tests" diff --git a/project.yml b/project.yml new file mode 100644 index 0000000..d39d844 --- /dev/null +++ b/project.yml @@ -0,0 +1,13 @@ +name: SyntaxKit +settings: + LINT_MODE: ${LINT_MODE} +packages: + SyntaxKit: + path: . +aggregateTargets: + Lint: + buildScripts: + - path: Scripts/lint.sh + name: Lint + basedOnDependencyAnalysis: false + schemes: {}