From e04031103c35fb6fa9021c4826c0e3c874dfc288 Mon Sep 17 00:00:00 2001 From: Leo Dion Date: Fri, 13 Jun 2025 16:30:41 -0400 Subject: [PATCH 01/21] adding initial working test --- Examples/BlackjackCard.swift | 16 ++ Examples/BlackjackCardCode.swift | 15 ++ Package.resolved | 7 +- Package.swift | 13 +- Sources/SwiftBuilder/CodeBlock.swift | 4 + .../SwiftBuilder/StructuralComponents.swift | 193 ++++++++++++++++++ Sources/SwiftBuilder/SwiftBuilder.swift | 27 +++ .../SwiftBuilderTests/SwiftBuilderTests.swift | 42 ++++ 8 files changed, 302 insertions(+), 15 deletions(-) create mode 100644 Examples/BlackjackCard.swift create mode 100644 Examples/BlackjackCardCode.swift create mode 100644 Sources/SwiftBuilder/StructuralComponents.swift create mode 100644 Sources/SwiftBuilder/SwiftBuilder.swift create mode 100644 Tests/SwiftBuilderTests/SwiftBuilderTests.swift diff --git a/Examples/BlackjackCard.swift b/Examples/BlackjackCard.swift new file mode 100644 index 0000000..b3fc1bb --- /dev/null +++ b/Examples/BlackjackCard.swift @@ -0,0 +1,16 @@ +import SwiftBuilder + +// Example of generating a BlackjackCard struct with a nested Suit enum +let code = Struct("BlackjackCard") { + Enum("Suit") { + Case("spades").equals("♠") + Case("hearts").equals("♡") + Case("diamonds").equals("♢") + Case("clubs").equals("♣") + } + .inherits("Character") + .comment("nested Suit enumeration") +} + +// Generate and print the code +print(code.generateCode()) \ No newline at end of file diff --git a/Examples/BlackjackCardCode.swift b/Examples/BlackjackCardCode.swift new file mode 100644 index 0000000..e22e261 --- /dev/null +++ b/Examples/BlackjackCardCode.swift @@ -0,0 +1,15 @@ +// +// BlackjackCardCode.swift +// SwiftBuilder +// +// Created by Leo Dion on 6/13/25. +// + +struct BlackjackCard { +public enum Suit :Character { +case spades = "♠" +case hearts = "♡" +case diamonds = "♢" +case clubs = "♣" + +} diff --git a/Package.resolved b/Package.resolved index 9a2ef48..fa0a833 100644 --- a/Package.resolved +++ b/Package.resolved @@ -1,15 +1,14 @@ { - "originHash" : "b5881f7ff763cf360a3639d99029de2b3ab4a457e0d8f08ce744bae51c2bf670", "pins" : [ { "identity" : "swift-syntax", "kind" : "remoteSourceControl", "location" : "https://github.com/apple/swift-syntax.git", "state" : { - "revision" : "f99ae8aa18f0cf0d53481901f88a0991dc3bd4a2", - "version" : "601.0.1" + "revision" : "64889f0c732f210a935a0ad7cda38f77f876262d", + "version" : "509.1.1" } } ], - "version" : 3 + "version" : 2 } diff --git a/Package.swift b/Package.swift index 966d30d..4eed1f0 100644 --- a/Package.swift +++ b/Package.swift @@ -1,4 +1,4 @@ -// swift-tools-version: 6.2 +// swift-tools-version: 5.9 // The swift-tools-version declares the minimum version of Swift required to build this package. import PackageDescription @@ -14,13 +14,9 @@ let package = Package( name: "SwiftBuilder", targets: ["SwiftBuilder"] ), - .executable( - name: "SwiftBuilderCLI", - targets: ["SwiftBuilderCLI"] - ), ], dependencies: [ - .package(url: "https://github.com/apple/swift-syntax.git", from: "601.0.1") + .package(url: "https://github.com/apple/swift-syntax.git", from: "509.0.0") ], targets: [ // Targets are the basic building blocks of a package, defining a module or a test suite. @@ -29,14 +25,9 @@ let package = Package( 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"] diff --git a/Sources/SwiftBuilder/CodeBlock.swift b/Sources/SwiftBuilder/CodeBlock.swift index 610636e..fcc007e 100644 --- a/Sources/SwiftBuilder/CodeBlock.swift +++ b/Sources/SwiftBuilder/CodeBlock.swift @@ -16,6 +16,10 @@ public struct CodeBlockBuilderResult { components } + public static func buildExpression(_ expression: CodeBlock) -> CodeBlock { + expression + } + public static func buildOptional(_ component: CodeBlock?) -> CodeBlock { component ?? EmptyCodeBlock() } diff --git a/Sources/SwiftBuilder/StructuralComponents.swift b/Sources/SwiftBuilder/StructuralComponents.swift new file mode 100644 index 0000000..250f0ba --- /dev/null +++ b/Sources/SwiftBuilder/StructuralComponents.swift @@ -0,0 +1,193 @@ +import Foundation +import SwiftSyntax + +public struct Struct: CodeBlock { + private let name: String + private let members: [CodeBlock] + private var inheritance: String? + private var comment: 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 func comment(_ text: String) -> Self { + var copy = self + copy.comment = text + return copy + } + + public var syntax: SyntaxProtocol { + let structKeyword = TokenSyntax.keyword(.struct, trailingTrivia: .space) + let identifier = TokenSyntax.identifier(name, trailingTrivia: .space) + + var inheritanceClause: TypeInheritanceClauseSyntax? + if let inheritance = inheritance { + let inheritedType = InheritedTypeSyntax(type: SimpleTypeIdentifierSyntax(name: .identifier(inheritance))) + inheritanceClause = TypeInheritanceClauseSyntax(colon: .colonToken(), inheritedTypeCollection: InheritedTypeListSyntax([inheritedType])) + } + + let memberBlock = MemberBlockSyntax( + leftBrace: .leftBraceToken(leadingTrivia: .space, trailingTrivia: .newline), + members: MemberDeclListSyntax(members.compactMap { member in + guard let syntax = member.syntax.as(DeclSyntax.self) else { return nil } + return MemberDeclListItemSyntax(decl: syntax, trailingTrivia: .newline) + }), + rightBrace: .rightBraceToken(leadingTrivia: .newline) + ) + + let modifiers = comment.map { _ in + DeclModifierListSyntax([ + DeclModifierSyntax(name: .keyword(.public, trailingTrivia: .space)) + ]) + } + + return StructDeclSyntax( + modifiers: modifiers ?? [], + structKeyword: structKeyword, + identifier: identifier, + inheritanceClause: inheritanceClause, + memberBlock: memberBlock + ) + } +} + +public struct Enum: CodeBlock { + private let name: String + private let content: [CodeBlock] + private var inheritance: String? + private var comment: String? + + public init(_ name: String, @CodeBlockBuilderResult _ content: () -> [CodeBlock]) { + self.name = name + self.content = content() + } + + public func inherits(_ type: String) -> Self { + var copy = self + copy.inheritance = type + return copy + } + + public func comment(_ text: String) -> Self { + var copy = self + copy.comment = text + return copy + } + + public var syntax: SyntaxProtocol { + let enumKeyword = TokenSyntax.keyword(.enum, trailingTrivia: .space) + let identifier = TokenSyntax.identifier(name, trailingTrivia: .space) + + var inheritanceClause: TypeInheritanceClauseSyntax? + if let inheritance = inheritance { + let inheritedType = InheritedTypeSyntax(type: SimpleTypeIdentifierSyntax(name: .identifier(inheritance))) + inheritanceClause = TypeInheritanceClauseSyntax(colon: .colonToken(), inheritedTypeCollection: InheritedTypeListSyntax([inheritedType])) + } + + let memberBlock = MemberBlockSyntax( + leftBrace: .leftBraceToken(leadingTrivia: .space, trailingTrivia: .newline), + members: MemberDeclListSyntax(content.compactMap { item in + guard let caseBlock = item as? Case else { return nil } + return MemberDeclListItemSyntax(decl: caseBlock.enumCaseDeclaration, trailingTrivia: .newline) + }), + rightBrace: .rightBraceToken(leadingTrivia: .newline) + ) + + let modifiers = comment.map { _ in + DeclModifierListSyntax([ + DeclModifierSyntax(name: .keyword(.public, trailingTrivia: .space)) + ]) + } + + return EnumDeclSyntax( + modifiers: modifiers ?? [], + enumKeyword: enumKeyword, + identifier: identifier, + inheritanceClause: inheritanceClause, + memberBlock: memberBlock + ) + } +} + +public struct Case: CodeBlock { + private let name: String + private var value: String? + + public init(_ name: String) { + self.name = name + } + + public func equals(_ value: String) -> Self { + var copy = self + copy.value = value + return copy + } + + public var enumCaseDeclaration: EnumCaseDeclSyntax { + let caseKeyword = TokenSyntax.keyword(.case, trailingTrivia: .space) + let identifier = TokenSyntax.identifier(name, trailingTrivia: .space) + + var rawValue: InitializerClauseSyntax? + if let value = value { + let stringLiteral = StringLiteralExprSyntax( + openingQuote: .stringQuoteToken(), + segments: StringLiteralSegmentListSyntax([ + .stringSegment(StringSegmentSyntax(content: .stringSegment(value))) + ]), + closingQuote: .stringQuoteToken() + ) + rawValue = InitializerClauseSyntax( + equal: .equalToken(leadingTrivia: .space, trailingTrivia: .space), + value: stringLiteral + ) + } + + return EnumCaseDeclSyntax( + caseKeyword: caseKeyword, + elements: EnumCaseElementListSyntax([ + EnumCaseElementSyntax( + name: identifier, + rawValue: rawValue + ) + ]) + ) + } + + public var syntax: SyntaxProtocol { + return self.enumCaseDeclaration + } +} + +public extension CodeBlock { + func generateCode() -> String { + guard let decl = syntax as? DeclSyntaxProtocol else { + fatalError("Only declaration syntax is supported at the top level.") + } + let sourceFile = SourceFileSyntax( + statements: CodeBlockItemListSyntax([ + CodeBlockItemSyntax(item: .decl(DeclSyntax(decl))) + ]) + ) + return sourceFile.description + } +} + +public extension Array where Element == CodeBlock { + func generateCode() -> String { + let decls = compactMap { $0.syntax as? DeclSyntaxProtocol } + let sourceFile = SourceFileSyntax( + statements: CodeBlockItemListSyntax(decls.map { decl in + CodeBlockItemSyntax(item: .decl(DeclSyntax(decl))) + }) + ) + return sourceFile.description + } +} diff --git a/Sources/SwiftBuilder/SwiftBuilder.swift b/Sources/SwiftBuilder/SwiftBuilder.swift new file mode 100644 index 0000000..6496a74 --- /dev/null +++ b/Sources/SwiftBuilder/SwiftBuilder.swift @@ -0,0 +1,27 @@ +// The Swift Programming Language +// https://docs.swift.org/swift-book + +import Foundation +import SwiftSyntax +// +//public extension CodeBlock { +// func generateCode() -> String { +// let sourceFile = SourceFileSyntax( +// statements: CodeBlockItemListSyntax([ +// CodeBlockItemSyntax(item: .decl(DeclSyntax(syntax))) +// ]) +// ) +// return sourceFile.formatted().description +// } +//} +// +//public extension Array where Element == CodeBlock { +// func generateCode() -> String { +// let sourceFile = SourceFileSyntax( +// statements: CodeBlockItemListSyntax(map { block in +// CodeBlockItemSyntax(item: .decl(DeclSyntax(block.syntax))) +// }) +// ) +// return sourceFile.formatted().description +// } +//} diff --git a/Tests/SwiftBuilderTests/SwiftBuilderTests.swift b/Tests/SwiftBuilderTests/SwiftBuilderTests.swift new file mode 100644 index 0000000..151b82a --- /dev/null +++ b/Tests/SwiftBuilderTests/SwiftBuilderTests.swift @@ -0,0 +1,42 @@ +import XCTest +@testable import SwiftBuilder + +final class SwiftBuilderTests: XCTestCase { + func testBlackjackCardExample() throws { + let blackjackCard = Struct("BlackjackCard") { + Enum("Suit") { + Case("spades").equals("♠") + Case("hearts").equals("♡") + Case("diamonds").equals("♢") + Case("clubs").equals("♣") + }.inherits("Character") + } + + let expected = """ + struct BlackjackCard { + // nested Suit enumeration + enum Suit: Character { + case spades = "♠" + case hearts = "♡" + case diamonds = "♢" + case clubs = "♣" + } + } + """ + + // Normalize whitespace and remove comments and modifiers + 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+", 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+", with: " ", options: .regularExpression) // Normalize whitespace + .trimmingCharacters(in: .whitespacesAndNewlines) + + XCTAssertEqual(normalizedGenerated, normalizedExpected) + } +} From 864d2fab4d8e40685886ff0b04fe1dccb3084265 Mon Sep 17 00:00:00 2001 From: Leo Dion Date: Sun, 15 Jun 2025 13:29:31 -0400 Subject: [PATCH 02/21] fixing tests --- Tests/SwiftBuilderTests/SwiftBuilderTests.swift | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/Tests/SwiftBuilderTests/SwiftBuilderTests.swift b/Tests/SwiftBuilderTests/SwiftBuilderTests.swift index 151b82a..d277e2b 100644 --- a/Tests/SwiftBuilderTests/SwiftBuilderTests.swift +++ b/Tests/SwiftBuilderTests/SwiftBuilderTests.swift @@ -14,7 +14,6 @@ final class SwiftBuilderTests: XCTestCase { let expected = """ struct BlackjackCard { - // nested Suit enumeration enum Suit: Character { case spades = "♠" case hearts = "♡" @@ -24,16 +23,18 @@ final class SwiftBuilderTests: XCTestCase { } """ - // Normalize whitespace and remove comments and modifiers + // 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) From 21a2959b018f774cd3a78dd01e585aede0632f18 Mon Sep 17 00:00:00 2001 From: Leo Dion Date: Sun, 15 Jun 2025 14:59:39 -0400 Subject: [PATCH 03/21] adding new code generation --- Examples/BlackjackCard.swift | 78 ++- Examples/BlackjackCardCode.swift | 66 +- Package.resolved | 7 +- Package.swift | 4 +- Sources/SwiftBuilder/CodeBlock.swift | 4 - .../SwiftBuilder/StructuralComponents.swift | 588 ++++++++++++++++-- .../SwiftBuilderTests/SwiftBuilderTests.swift | 43 -- .../SwiftBuilderTestsB.swift | 27 +- 8 files changed, 674 insertions(+), 143 deletions(-) delete mode 100644 Tests/SwiftBuilderTests/SwiftBuilderTests.swift diff --git a/Examples/BlackjackCard.swift b/Examples/BlackjackCard.swift index b3fc1bb..5709fa3 100644 --- a/Examples/BlackjackCard.swift +++ b/Examples/BlackjackCard.swift @@ -1,16 +1,80 @@ import SwiftBuilder // Example of generating a BlackjackCard struct with a nested Suit enum -let code = Struct("BlackjackCard") { +let structExample = Struct("BlackjackCard") { Enum("Suit") { - Case("spades").equals("♠") - Case("hearts").equals("♡") - Case("diamonds").equals("♢") - Case("clubs").equals("♣") + 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") + + Variable(.let, name: "rank", type: "Rank") + Variable(.let, name: "suit", type: "Suit") + + 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") + } + } } // Generate and print the code -print(code.generateCode()) \ No newline at end of file +print(structExample.generateCode()) \ No newline at end of file diff --git a/Examples/BlackjackCardCode.swift b/Examples/BlackjackCardCode.swift index e22e261..fe8dc64 100644 --- a/Examples/BlackjackCardCode.swift +++ b/Examples/BlackjackCardCode.swift @@ -1,15 +1,55 @@ -// -// BlackjackCardCode.swift -// SwiftBuilder -// -// Created by Leo Dion on 6/13/25. -// - -struct BlackjackCard { -public enum Suit :Character { -case spades = "♠" -case hearts = "♡" -case diamonds = "♢" -case clubs = "♣" +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 + } } diff --git a/Package.resolved b/Package.resolved index fa0a833..df7cfda 100644 --- a/Package.resolved +++ b/Package.resolved @@ -1,14 +1,15 @@ { + "originHash" : "229a09bb6d44c64be46f5fe39a7296eed19f631fbdda7aa5a22a4431cca37b10", "pins" : [ { "identity" : "swift-syntax", "kind" : "remoteSourceControl", "location" : "https://github.com/apple/swift-syntax.git", "state" : { - "revision" : "64889f0c732f210a935a0ad7cda38f77f876262d", - "version" : "509.1.1" + "revision" : "0dff260d3d1bb99c382d8dfcd6bb093e5e9cbd36", + "version" : "602.0.0-prerelease-2025-05-29" } } ], - "version" : 2 + "version" : 3 } diff --git a/Package.swift b/Package.swift index 4eed1f0..ff0179b 100644 --- a/Package.swift +++ b/Package.swift @@ -1,4 +1,4 @@ -// swift-tools-version: 5.9 +// swift-tools-version: 6.2 // The swift-tools-version declares the minimum version of Swift required to build this package. import PackageDescription @@ -16,7 +16,7 @@ let package = Package( ), ], dependencies: [ - .package(url: "https://github.com/apple/swift-syntax.git", from: "509.0.0") + .package(url: "https://github.com/apple/swift-syntax.git", from: "602.0.0-prerelease-2025-05-29") ], targets: [ // Targets are the basic building blocks of a package, defining a module or a test suite. diff --git a/Sources/SwiftBuilder/CodeBlock.swift b/Sources/SwiftBuilder/CodeBlock.swift index fcc007e..610636e 100644 --- a/Sources/SwiftBuilder/CodeBlock.swift +++ b/Sources/SwiftBuilder/CodeBlock.swift @@ -16,10 +16,6 @@ public struct CodeBlockBuilderResult { components } - public static func buildExpression(_ expression: CodeBlock) -> CodeBlock { - expression - } - public static func buildOptional(_ component: CodeBlock?) -> CodeBlock { component ?? EmptyCodeBlock() } diff --git a/Sources/SwiftBuilder/StructuralComponents.swift b/Sources/SwiftBuilder/StructuralComponents.swift index 250f0ba..32d77b3 100644 --- a/Sources/SwiftBuilder/StructuralComponents.swift +++ b/Sources/SwiftBuilder/StructuralComponents.swift @@ -1,11 +1,16 @@ import Foundation import SwiftSyntax +import SwiftParser + +public enum VariableKind { + case `let` + case `var` +} public struct Struct: CodeBlock { private let name: String private let members: [CodeBlock] private var inheritance: String? - private var comment: String? public init(_ name: String, @CodeBlockBuilderResult _ content: () -> [CodeBlock]) { self.name = name @@ -18,41 +23,28 @@ public struct Struct: CodeBlock { return copy } - public func comment(_ text: String) -> Self { - var copy = self - copy.comment = text - return copy - } - public var syntax: SyntaxProtocol { let structKeyword = TokenSyntax.keyword(.struct, trailingTrivia: .space) let identifier = TokenSyntax.identifier(name, trailingTrivia: .space) - var inheritanceClause: TypeInheritanceClauseSyntax? + var inheritanceClause: InheritanceClauseSyntax? if let inheritance = inheritance { - let inheritedType = InheritedTypeSyntax(type: SimpleTypeIdentifierSyntax(name: .identifier(inheritance))) - inheritanceClause = TypeInheritanceClauseSyntax(colon: .colonToken(), inheritedTypeCollection: InheritedTypeListSyntax([inheritedType])) + 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: MemberDeclListSyntax(members.compactMap { member in + members: MemberBlockItemListSyntax(members.compactMap { member in guard let syntax = member.syntax.as(DeclSyntax.self) else { return nil } - return MemberDeclListItemSyntax(decl: syntax, trailingTrivia: .newline) + return MemberBlockItemSyntax(decl: syntax, trailingTrivia: .newline) }), rightBrace: .rightBraceToken(leadingTrivia: .newline) ) - let modifiers = comment.map { _ in - DeclModifierListSyntax([ - DeclModifierSyntax(name: .keyword(.public, trailingTrivia: .space)) - ]) - } - return StructDeclSyntax( - modifiers: modifiers ?? [], structKeyword: structKeyword, - identifier: identifier, + name: identifier, inheritanceClause: inheritanceClause, memberBlock: memberBlock ) @@ -61,13 +53,12 @@ public struct Struct: CodeBlock { public struct Enum: CodeBlock { private let name: String - private let content: [CodeBlock] + private let members: [CodeBlock] private var inheritance: String? - private var comment: String? public init(_ name: String, @CodeBlockBuilderResult _ content: () -> [CodeBlock]) { self.name = name - self.content = content() + self.members = content() } public func inherits(_ type: String) -> Self { @@ -76,48 +67,35 @@ public struct Enum: CodeBlock { return copy } - public func comment(_ text: String) -> Self { - var copy = self - copy.comment = text - return copy - } - public var syntax: SyntaxProtocol { let enumKeyword = TokenSyntax.keyword(.enum, trailingTrivia: .space) let identifier = TokenSyntax.identifier(name, trailingTrivia: .space) - var inheritanceClause: TypeInheritanceClauseSyntax? + var inheritanceClause: InheritanceClauseSyntax? if let inheritance = inheritance { - let inheritedType = InheritedTypeSyntax(type: SimpleTypeIdentifierSyntax(name: .identifier(inheritance))) - inheritanceClause = TypeInheritanceClauseSyntax(colon: .colonToken(), inheritedTypeCollection: InheritedTypeListSyntax([inheritedType])) + 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: MemberDeclListSyntax(content.compactMap { item in - guard let caseBlock = item as? Case else { return nil } - return MemberDeclListItemSyntax(decl: caseBlock.enumCaseDeclaration, 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) ) - let modifiers = comment.map { _ in - DeclModifierListSyntax([ - DeclModifierSyntax(name: .keyword(.public, trailingTrivia: .space)) - ]) - } - return EnumDeclSyntax( - modifiers: modifiers ?? [], enumKeyword: enumKeyword, - identifier: identifier, + name: identifier, inheritanceClause: inheritanceClause, memberBlock: memberBlock ) } } -public struct Case: CodeBlock { +public struct EnumCase: CodeBlock { private let name: String private var value: String? @@ -131,22 +109,27 @@ public struct Case: CodeBlock { return copy } - public var enumCaseDeclaration: EnumCaseDeclSyntax { + public func equals(_ value: Int) -> Self { + var copy = self + copy.value = String(value) + return copy + } + + public var syntax: SyntaxProtocol { let caseKeyword = TokenSyntax.keyword(.case, trailingTrivia: .space) let identifier = TokenSyntax.identifier(name, trailingTrivia: .space) - var rawValue: InitializerClauseSyntax? + var initializer: InitializerClauseSyntax? if let value = value { - let stringLiteral = StringLiteralExprSyntax( - openingQuote: .stringQuoteToken(), - segments: StringLiteralSegmentListSyntax([ - .stringSegment(StringSegmentSyntax(content: .stringSegment(value))) - ]), - closingQuote: .stringQuoteToken() - ) - rawValue = InitializerClauseSyntax( + initializer = InitializerClauseSyntax( equal: .equalToken(leadingTrivia: .space, trailingTrivia: .space), - value: stringLiteral + value: StringLiteralExprSyntax( + openingQuote: .stringQuoteToken(), + segments: StringLiteralSegmentListSyntax([ + .stringSegment(StringSegmentSyntax(content: .stringSegment(value))) + ]), + closingQuote: .stringQuoteToken() + ) ) } @@ -154,15 +137,505 @@ public struct Case: CodeBlock { caseKeyword: caseKeyword, elements: EnumCaseElementListSyntax([ EnumCaseElementSyntax( + leadingTrivia: .space, + _: nil, name: identifier, - rawValue: rawValue + _: nil, + parameterClause: nil, + _: nil, + rawValue: initializer, + _: nil, + trailingComma: nil, + trailingTrivia: .newline + ) + ]) + ) + } +} + +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 patternList = TuplePatternElementListSyntax( + patterns.map { TuplePatternElementSyntax( + label: nil, + colon: nil, + pattern: PatternSyntax(IdentifierPatternSyntax(identifier: .identifier($0))) + )} + ) + let caseItems = CaseItemListSyntax([ + CaseItemSyntax( + 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 } +} + +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 = CaseItemListSyntax([ + CaseItemSyntax( + 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 } +} + +public struct Variable: CodeBlock { + private let kind: VariableKind + private let name: String + private let type: String + + public init(_ kind: VariableKind, name: String, type: String) { + self.kind = kind + self.name = name + self.type = type + } + + 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)) + ) + + return VariableDeclSyntax( + bindingSpecifier: bindingKeyword, + bindings: PatternBindingListSyntax([ + PatternBindingSyntax( + pattern: IdentifierPatternSyntax(identifier: identifier), + typeAnnotation: typeAnnotation + ) + ]) + ) + } +} + +public struct ComputedProperty: CodeBlock { + private let name: String + private let type: String? + private let body: [CodeBlock] + + public init(_ name: String, type: String? = nil, @CodeBlockBuilderResult _ content: () -> [CodeBlock]) { + self.name = name + self.type = type + self.body = content() + } + + public var syntax: SyntaxProtocol { + let getKeyword = TokenSyntax.keyword(.get, trailingTrivia: .space) + let statements = CodeBlockItemListSyntax(self.body.compactMap { item in + guard let syntax = item.syntax.as(CodeBlockItemSyntax.self) else { return nil } + return syntax + }) + let accessor = AccessorBlockSyntax( + leftBrace: TokenSyntax.leftBraceToken(leadingTrivia: .space, trailingTrivia: .newline), + accessors: .getter(statements), + rightBrace: TokenSyntax.rightBraceToken(leadingTrivia: .newline) + ) + let identifier = TokenSyntax.identifier(name, trailingTrivia: .space) + var typeAnnotation: TypeAnnotationSyntax? + if let type = type { + 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, + accessor: accessor + ) + ]) + ) + } +} + +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) + }) + return SwitchExprSyntax( + switchKeyword: .keyword(.switch, trailingTrivia: .space), + expression: expr, + leftBrace: .leftBraceToken(leadingTrivia: .space, trailingTrivia: .newline), + cases: cases, + rightBrace: .rightBraceToken(leadingTrivia: .newline) + ) + } +} + +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 { $0.syntax.as(CodeBlockItemSyntax.self) }) + let label = SwitchDefaultLabelSyntax( + defaultKeyword: .keyword(.default, trailingTrivia: .space), + colon: .colonToken() + ) + return SwitchCaseSyntax( + label: .default(label), + statements: statements + ) + } + public var syntax: SyntaxProtocol { switchCaseSyntax } +} + +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 + InitializerClauseSyntax( + equal: .equalToken(leadingTrivia: .space, trailingTrivia: .space), + value: StringLiteralExprSyntax( + openingQuote: .stringQuoteToken(), + segments: StringLiteralSegmentListSyntax([ + .stringSegment(StringSegmentSyntax(content: .stringSegment(value))) + ]), + closingQuote: .stringQuoteToken() + ) + ) + } + + return VariableDeclSyntax( + bindingSpecifier: bindingKeyword, + bindings: PatternBindingListSyntax([ + PatternBindingSyntax( + pattern: IdentifierPatternSyntax(identifier: identifier), + typeAnnotation: nil, + initializer: initializer ) ]) ) } +} + +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 { - return self.enumCaseDeclaration + let left = ExprSyntax(DeclReferenceExprSyntax(baseName: .identifier(target))) + let right = ExprSyntax(StringLiteralExprSyntax( + openingQuote: .stringQuoteToken(), + segments: StringLiteralSegmentListSyntax([ + .stringSegment(StringSegmentSyntax(content: .stringSegment(value))) + ]), + closingQuote: .stringQuoteToken() + )) + let assign = ExprSyntax(BinaryOperatorExprSyntax(operatorToken: .binaryOperator("+=", trailingTrivia: .space))) + return CodeBlockItemSyntax( + item: .expr( + ExprSyntax( + SequenceExprSyntax( + elements: ExprListSyntax([ + left, + assign, + right + ]) + ) + ) + ) + ) + } +} + +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( + condition: .expression(ExprSyntax(fromProtocol: condition.syntax.as(ExprSyntax.self) ?? DeclReferenceExprSyntax(baseName: .identifier("")))) + ) + let bodyBlock = CodeBlockSyntax(statements: CodeBlockItemListSyntax(body.compactMap { $0.syntax.as(CodeBlockItemSyntax.self) })) + let elseBlock = elseBody.map { + IfExprSyntax.ElseBody.codeBlock(CodeBlockSyntax(statements: CodeBlockItemListSyntax($0.compactMap { $0.syntax.as(CodeBlockItemSyntax.self) }))) + } + return IfExprSyntax( + ifKeyword: .keyword(.if, trailingTrivia: .space), + conditions: ConditionElementListSyntax([cond]), + body: bodyBlock, + elseKeyword: elseBlock != nil ? .keyword(.else, trailingTrivia: .space) : nil, + elseBody: elseBlock + ) + } +} + +public struct Let: CodeBlock { + private let name: String + private 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))) + ) + ) + ]) + ) + ) + ) + ) + } +} + +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.") + } + return CodeBlockItemSyntax( + item: .stmt( + StmtSyntax( + ReturnStmtSyntax( + returnKeyword: .keyword(.return, trailingTrivia: .space), + expression: ExprSyntax(expr.syntax) + ) + ) + ) + ) + } +} + +@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 + } +} + +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.map { $0.syntax as! TupleExprElementSyntax }) + return ExprSyntax(FunctionCallExprSyntax( + calledExpression: ExprSyntax(DeclReferenceExprSyntax(baseName: .identifier(type))), + leftParen: .leftParenToken(), + argumentList: args, + rightParen: .rightParenToken() + )) + } +} + +public struct Parameter: CodeBlock { + private let name: String + private let value: String + public init(name: String, value: String) { + self.name = name + self.value = value + } + public var syntax: SyntaxProtocol { + return TupleExprElementSyntax( + label: .identifier(name), + colon: .colonToken(), + expression: ExprSyntax(DeclReferenceExprSyntax(baseName: .identifier(value))) + ) + } +} + +public struct VariableExp: CodeBlock { + private let name: String + + public init(_ name: String) { + self.name = name + } + + public var syntax: SyntaxProtocol { + return TokenSyntax.identifier(self.name) + } +} +public struct OldVariableExp: CodeBlock { + private let name: String + private let value: String + + public init(_ name: String, _ value: String) { + self.name = name + self.value = value + } + + public var syntax: SyntaxProtocol { + let name = TokenSyntax.identifier(self.name) + let value = IdentifierExprSyntax(identifier: .identifier(self.value)) + + return OptionalBindingConditionSyntax( + bindingSpecifier: .keyword(.let, trailingTrivia: .space), + pattern: IdentifierPatternSyntax(identifier: name), + initializer: InitializerClauseSyntax( + equal: .equalToken(leadingTrivia: .space, trailingTrivia: .space), + value: value + ) + ) + } +} + +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(StringLiteralExprSyntax( + openingQuote: .stringQuoteToken(), + segments: StringLiteralSegmentListSyntax([ + .stringSegment(StringSegmentSyntax(content: .stringSegment(value))) + ]), + closingQuote: .stringQuoteToken() + )) + let assign = ExprSyntax(AssignmentExprSyntax(assignToken: .equalToken())) + return CodeBlockItemSyntax( + item: .expr( + ExprSyntax( + SequenceExprSyntax( + elements: ExprListSyntax([ + left, + assign, + right + ]) + ) + ) + ) + ) } } @@ -191,3 +664,4 @@ public extension Array where Element == CodeBlock { return sourceFile.description } } + diff --git a/Tests/SwiftBuilderTests/SwiftBuilderTests.swift b/Tests/SwiftBuilderTests/SwiftBuilderTests.swift deleted file mode 100644 index d277e2b..0000000 --- a/Tests/SwiftBuilderTests/SwiftBuilderTests.swift +++ /dev/null @@ -1,43 +0,0 @@ -import XCTest -@testable import SwiftBuilder - -final class SwiftBuilderTests: XCTestCase { - func testBlackjackCardExample() throws { - let blackjackCard = Struct("BlackjackCard") { - Enum("Suit") { - Case("spades").equals("♠") - Case("hearts").equals("♡") - Case("diamonds").equals("♢") - Case("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 index eb1b702..2bd3188 100644 --- a/Tests/SwiftBuilderTests/SwiftBuilderTestsB.swift +++ b/Tests/SwiftBuilderTests/SwiftBuilderTestsB.swift @@ -107,29 +107,29 @@ final class SwiftBuilderTestsB: XCTestCase { Variable(.let, name: "first", type: "Int") Variable(.let, name: "second", type: "Int?") } - ComputedProperty("values", type: "Values") { + ComputedProperty("values") { Switch("self") { SwitchCase(".ace") { Return { Init("Values") { - Parameter(name: "first", type: "", defaultValue: "1") - Parameter(name: "second", type: "", defaultValue: "11") + Parameter(name: "first", value: "1") + Parameter(name: "second", value: "11") } } } SwitchCase(".jack", ".queen", ".king") { Return { Init("Values") { - Parameter(name: "first", type: "", defaultValue: "10") - Parameter(name: "second", type: "", defaultValue: "nil") + Parameter(name: "first", value: "10") + Parameter(name: "second", value: "nil") } } } Default { Return { Init("Values") { - Parameter(name: "first", type: "", defaultValue: "self.rawValue") - Parameter(name: "second", type: "", defaultValue: "nil") + Parameter(name: "first", value: "self.rawValue") + Parameter(name: "second", value: "nil") } } } @@ -140,8 +140,8 @@ final class SwiftBuilderTestsB: XCTestCase { 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),\"") + 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: { @@ -179,8 +179,7 @@ final class SwiftBuilderTestsB: XCTestCase { case ace struct Values { - let first: Int - let second: Int? + let first: Int, second: Int? } var values: Values { @@ -198,10 +197,10 @@ final class SwiftBuilderTestsB: XCTestCase { let rank: Rank let suit: Suit var description: String { - var output = \"suit is \\(suit.rawValue),\" - output += \" value is \\(rank.values.first)\" + var output = \"suit is \u{005C}(suit.rawValue),\" + output += \" value is \u{005C}(rank.values.first)\" if let second = rank.values.second { - output += \" or \\(second)\" + output += \" or \u{005C}(second)\" } return output } From 4f79d3d64855d4100b25007557775ce42d6959d8 Mon Sep 17 00:00:00 2001 From: leogdion Date: Sun, 15 Jun 2025 15:48:11 -0400 Subject: [PATCH 04/21] working sample from code base --- Examples/BlackjackCard.swift | 14 +- .../SwiftBuilder/StructuralComponents.swift | 317 +++++++++++------- .../SwiftBuilderTestsB.swift | 15 +- 3 files changed, 206 insertions(+), 140 deletions(-) diff --git a/Examples/BlackjackCard.swift b/Examples/BlackjackCard.swift index 5709fa3..ba8eaa7 100644 --- a/Examples/BlackjackCard.swift +++ b/Examples/BlackjackCard.swift @@ -63,14 +63,12 @@ let structExample = Struct("BlackjackCard") { Variable(.let, name: "suit", type: "Suit") 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{ + 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") } } diff --git a/Sources/SwiftBuilder/StructuralComponents.swift b/Sources/SwiftBuilder/StructuralComponents.swift index 32d77b3..da75628 100644 --- a/Sources/SwiftBuilder/StructuralComponents.swift +++ b/Sources/SwiftBuilder/StructuralComponents.swift @@ -98,6 +98,7 @@ public struct Enum: CodeBlock { public struct EnumCase: CodeBlock { private let name: String private var value: String? + private var intValue: Int? public init(_ name: String) { self.name = name @@ -106,12 +107,14 @@ public struct EnumCase: CodeBlock { 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 = String(value) + copy.value = nil + copy.intValue = value return copy } @@ -131,6 +134,11 @@ public struct EnumCase: CodeBlock { closingQuote: .stringQuoteToken() ) ) + } else if let intValue = intValue { + initializer = InitializerClauseSyntax( + equal: .equalToken(leadingTrivia: .space, trailingTrivia: .space), + value: IntegerLiteralExprSyntax(digits: .integerLiteral(String(intValue))) + ) } return EnumCaseDeclSyntax( @@ -163,27 +171,20 @@ public struct SwitchCase: CodeBlock { } public var switchCaseSyntax: SwitchCaseSyntax { - let patternList = TuplePatternElementListSyntax( - patterns.map { TuplePatternElementSyntax( - label: nil, - colon: nil, - pattern: PatternSyntax(IdentifierPatternSyntax(identifier: .identifier($0))) - )} - ) - let caseItems = CaseItemListSyntax([ - CaseItemSyntax( - pattern: TuplePatternSyntax( - leftParen: .leftParenToken(), - elements: patternList, - rightParen: .rightParenToken() - ) + 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 { $0.syntax.as(CodeBlockItemSyntax.self) }) let label = SwitchCaseLabelSyntax( caseKeyword: .keyword(.case, trailingTrivia: .space), caseItems: caseItems, - colon: .colonToken() + colon: .colonToken(trailingTrivia: .newline) ) return SwitchCaseSyntax( label: .case(label), @@ -268,20 +269,25 @@ public struct Variable: CodeBlock { public struct ComputedProperty: CodeBlock { private let name: String - private let type: String? + private let type: String private let body: [CodeBlock] - public init(_ name: String, type: String? = nil, @CodeBlockBuilderResult _ content: () -> [CodeBlock]) { + public init(_ name: String, type: String, @CodeBlockBuilderResult _ content: () -> [CodeBlock]) { self.name = name self.type = type self.body = content() } public var syntax: SyntaxProtocol { - let getKeyword = TokenSyntax.keyword(.get, trailingTrivia: .space) let statements = CodeBlockItemListSyntax(self.body.compactMap { item in - guard let syntax = item.syntax.as(CodeBlockItemSyntax.self) else { return nil } - return syntax + if let cb = item.syntax as? CodeBlockItemSyntax { return cb.with(\ .trailingTrivia, .newline) } + if let stmt = item.syntax as? StmtSyntax { + return CodeBlockItemSyntax(item: .stmt(stmt), trailingTrivia: .newline) + } + if let expr = item.syntax as? ExprSyntax { + return CodeBlockItemSyntax(item: .expr(expr), trailingTrivia: .newline) + } + return nil }) let accessor = AccessorBlockSyntax( leftBrace: TokenSyntax.leftBraceToken(leadingTrivia: .space, trailingTrivia: .newline), @@ -289,20 +295,17 @@ public struct ComputedProperty: CodeBlock { rightBrace: TokenSyntax.rightBraceToken(leadingTrivia: .newline) ) let identifier = TokenSyntax.identifier(name, trailingTrivia: .space) - var typeAnnotation: TypeAnnotationSyntax? - if let type = type { - typeAnnotation = TypeAnnotationSyntax( - colon: TokenSyntax.colonToken(leadingTrivia: .space, trailingTrivia: .space), - type: IdentifierTypeSyntax(name: .identifier(type)) - ) - } + 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, - accessor: accessor + accessorBlock: accessor ) ]) ) @@ -325,17 +328,15 @@ public struct Switch: CodeBlock { if let d = $0 as? Default { return d.switchCaseSyntax } return nil } - - let cases = SwitchCaseListSyntax(casesArr.map{ - SwitchCaseListSyntax.Element.init($0) - }) - return SwitchExprSyntax( + let cases = SwitchCaseListSyntax(casesArr.map { SwitchCaseListSyntax.Element.init($0) }) + let switchExpr = SwitchExprSyntax( switchKeyword: .keyword(.switch, trailingTrivia: .space), - expression: expr, + subject: expr, leftBrace: .leftBraceToken(leadingTrivia: .space, trailingTrivia: .newline), cases: cases, rightBrace: .rightBraceToken(leadingTrivia: .newline) ) + return CodeBlockItemSyntax(item: .expr(ExprSyntax(switchExpr))) } } @@ -372,29 +373,41 @@ public struct VariableDecl: CodeBlock { 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 - InitializerClauseSyntax( - equal: .equalToken(leadingTrivia: .space, trailingTrivia: .space), - value: StringLiteralExprSyntax( - openingQuote: .stringQuoteToken(), - segments: StringLiteralSegmentListSyntax([ - .stringSegment(StringSegmentSyntax(content: .stringSegment(value))) - ]), - closingQuote: .stringQuoteToken() + 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 + return CodeBlockItemSyntax( + item: .decl( + DeclSyntax( + VariableDeclSyntax( + bindingSpecifier: bindingKeyword, + bindings: PatternBindingListSyntax([ + PatternBindingSyntax( + pattern: IdentifierPatternSyntax(identifier: identifier), + typeAnnotation: nil, + initializer: initializer + ) + ]) + ) ) - ]) + ), + trailingTrivia: .newline ) } } @@ -408,6 +421,45 @@ public struct PlusAssign: CodeBlock { 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 CodeBlockItemSyntax( + item: .expr( + ExprSyntax( + SequenceExprSyntax( + elements: ExprListSyntax([ + left, + assign, + right + ]) + ) + ) + ), + trailingTrivia: .newline + ) + } +} + +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(StringLiteralExprSyntax( @@ -417,7 +469,7 @@ public struct PlusAssign: CodeBlock { ]), closingQuote: .stringQuoteToken() )) - let assign = ExprSyntax(BinaryOperatorExprSyntax(operatorToken: .binaryOperator("+=", trailingTrivia: .space))) + let assign = ExprSyntax(AssignmentExprSyntax(assignToken: .equalToken())) return CodeBlockItemSyntax( item: .expr( ExprSyntax( @@ -429,7 +481,44 @@ public struct PlusAssign: CodeBlock { ]) ) ) + ), + trailingTrivia: .newline + ) + } +} + +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 CodeBlockItemSyntax( + item: .stmt( + StmtSyntax( + ReturnStmtSyntax( + returnKeyword: .keyword(.return, trailingTrivia: .space), + expression: ExprSyntax(DeclReferenceExprSyntax(baseName: .identifier(varExp.name))) + ) + ) + ), + trailingTrivia: .newline ) + } + return CodeBlockItemSyntax( + item: .stmt( + StmtSyntax( + ReturnStmtSyntax( + returnKeyword: .keyword(.return, trailingTrivia: .space), + expression: ExprSyntax(expr.syntax) + ) + ) + ), + trailingTrivia: .newline ) } } @@ -446,26 +535,52 @@ public struct If: CodeBlock { } public var syntax: SyntaxProtocol { - let cond = ConditionElementSyntax( - condition: .expression(ExprSyntax(fromProtocol: condition.syntax.as(ExprSyntax.self) ?? DeclReferenceExprSyntax(baseName: .identifier("")))) + 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 { $0.syntax.as(CodeBlockItemSyntax.self) }), + rightBrace: .rightBraceToken(leadingTrivia: .newline) ) - let bodyBlock = CodeBlockSyntax(statements: CodeBlockItemListSyntax(body.compactMap { $0.syntax.as(CodeBlockItemSyntax.self) })) let elseBlock = elseBody.map { - IfExprSyntax.ElseBody.codeBlock(CodeBlockSyntax(statements: CodeBlockItemListSyntax($0.compactMap { $0.syntax.as(CodeBlockItemSyntax.self) }))) + IfExprSyntax.ElseBody(CodeBlockSyntax( + leftBrace: .leftBraceToken(leadingTrivia: .space, trailingTrivia: .newline), + statements: CodeBlockItemListSyntax($0.compactMap { $0.syntax.as(CodeBlockItemSyntax.self) }), + rightBrace: .rightBraceToken(leadingTrivia: .newline) + )) } - return IfExprSyntax( - ifKeyword: .keyword(.if, trailingTrivia: .space), - conditions: ConditionElementListSyntax([cond]), - body: bodyBlock, - elseKeyword: elseBlock != nil ? .keyword(.else, trailingTrivia: .space) : nil, - elseBody: elseBlock + return ExprSyntax( + IfExprSyntax( + ifKeyword: .keyword(.if, trailingTrivia: .space), + conditions: ConditionElementListSyntax([cond]), + body: bodyBlock, + elseKeyword: elseBlock != nil ? .keyword(.else, trailingTrivia: .space) : nil, + elseBody: elseBlock + ) ) } } public struct Let: CodeBlock { - private let name: String - private let value: String + let name: String + let value: String public init(_ name: String, _ value: String) { self.name = name self.value = value @@ -492,28 +607,6 @@ public struct Let: CodeBlock { } } -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.") - } - return CodeBlockItemSyntax( - item: .stmt( - StmtSyntax( - ReturnStmtSyntax( - returnKeyword: .keyword(.return, trailingTrivia: .space), - expression: ExprSyntax(expr.syntax) - ) - ) - ) - ) - } -} - @resultBuilder public struct ParameterBuilderResult { public static func buildBlock(_ components: Parameter...) -> [Parameter] { @@ -545,7 +638,13 @@ public struct Init: CodeBlock { self.parameters = params() } public var syntax: SyntaxProtocol { - let args = TupleExprElementListSyntax(parameters.map { $0.syntax as! TupleExprElementSyntax }) + 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(), @@ -572,7 +671,7 @@ public struct Parameter: CodeBlock { } public struct VariableExp: CodeBlock { - private let name: String + let name: String public init(_ name: String) { self.name = name @@ -582,6 +681,7 @@ public struct VariableExp: CodeBlock { return TokenSyntax.identifier(self.name) } } + public struct OldVariableExp: CodeBlock { private let name: String private let value: String @@ -606,39 +706,6 @@ public struct OldVariableExp: CodeBlock { } } -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(StringLiteralExprSyntax( - openingQuote: .stringQuoteToken(), - segments: StringLiteralSegmentListSyntax([ - .stringSegment(StringSegmentSyntax(content: .stringSegment(value))) - ]), - closingQuote: .stringQuoteToken() - )) - let assign = ExprSyntax(AssignmentExprSyntax(assignToken: .equalToken())) - return CodeBlockItemSyntax( - item: .expr( - ExprSyntax( - SequenceExprSyntax( - elements: ExprListSyntax([ - left, - assign, - right - ]) - ) - ) - ) - ) - } -} - public extension CodeBlock { func generateCode() -> String { guard let decl = syntax as? DeclSyntaxProtocol else { diff --git a/Tests/SwiftBuilderTests/SwiftBuilderTestsB.swift b/Tests/SwiftBuilderTests/SwiftBuilderTestsB.swift index 2bd3188..f294243 100644 --- a/Tests/SwiftBuilderTests/SwiftBuilderTestsB.swift +++ b/Tests/SwiftBuilderTests/SwiftBuilderTestsB.swift @@ -107,7 +107,7 @@ final class SwiftBuilderTestsB: XCTestCase { Variable(.let, name: "first", type: "Int") Variable(.let, name: "second", type: "Int?") } - ComputedProperty("values") { + ComputedProperty("values", type: "Values") { Switch("self") { SwitchCase(".ace") { Return { @@ -140,8 +140,8 @@ final class SwiftBuilderTestsB: XCTestCase { Variable(.let, name: "rank", type: "Rank") Variable(.let, name: "suit", type: "Suit") - ComputedProperty("description") { - VariableDecl(.var, name: "output", equals: "\"suit is \\(suit.rawValue)\"") + 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: { @@ -179,7 +179,8 @@ final class SwiftBuilderTestsB: XCTestCase { case ace struct Values { - let first: Int, second: Int? + let first: Int + let second: Int? } var values: Values { @@ -197,10 +198,10 @@ final class SwiftBuilderTestsB: XCTestCase { let rank: Rank let suit: Suit var description: String { - var output = \"suit is \u{005C}(suit.rawValue),\" - output += \" value is \u{005C}(rank.values.first)\" + var output = \"suit is \\(suit.rawValue),\" + output += \" value is \\(rank.values.first)\" if let second = rank.values.second { - output += \" or \u{005C}(second)\" + output += \" or \\(second)\" } return output } From 5edd478464fff78133b1683db18404864cb0dbba Mon Sep 17 00:00:00 2001 From: Leo Dion Date: Sun, 15 Jun 2025 16:30:53 -0400 Subject: [PATCH 05/21] added function --- .../SwiftBuilder/StructuralComponents.swift | 162 ++++++++++++++++-- .../SwiftBuilderTestsB.swift | 12 +- .../SwiftBuilderTestsC.swift | 12 +- 3 files changed, 157 insertions(+), 29 deletions(-) diff --git a/Sources/SwiftBuilder/StructuralComponents.swift b/Sources/SwiftBuilder/StructuralComponents.swift index da75628..ee9330f 100644 --- a/Sources/SwiftBuilder/StructuralComponents.swift +++ b/Sources/SwiftBuilder/StructuralComponents.swift @@ -462,14 +462,19 @@ public struct Assignment: CodeBlock { } public var syntax: SyntaxProtocol { let left = ExprSyntax(DeclReferenceExprSyntax(baseName: .identifier(target))) - let right = ExprSyntax(StringLiteralExprSyntax( - openingQuote: .stringQuoteToken(), - segments: StringLiteralSegmentListSyntax([ - .stringSegment(StringSegmentSyntax(content: .stringSegment(value))) - ]), - closingQuote: .stringQuoteToken() - )) - let assign = ExprSyntax(AssignmentExprSyntax(assignToken: .equalToken())) + 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(assignToken: .equalToken(leadingTrivia: .space, trailingTrivia: .space))) return CodeBlockItemSyntax( item: .expr( ExprSyntax( @@ -655,18 +660,29 @@ public struct Init: CodeBlock { } public struct Parameter: CodeBlock { - private let name: String - private let value: String - public init(name: String, value: String) { + let name: String + let type: String + let defaultValue: String? + public init(name: String, type: String, defaultValue: String? = nil) { self.name = name - self.value = value + self.type = type + self.defaultValue = defaultValue } public var syntax: SyntaxProtocol { - return TupleExprElementSyntax( - label: .identifier(name), - colon: .colonToken(), - expression: ExprSyntax(DeclReferenceExprSyntax(baseName: .identifier(value))) - ) + // 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))) + ) + } } } @@ -706,6 +722,118 @@ public struct OldVariableExp: CodeBlock { } } +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, @ParameterBuilderResult _ params: () -> [Parameter], returns returnType: String? = nil, @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(parameters.enumerated().map { index, param in + var paramSyntax = FunctionParameterSyntax( + firstName: .identifier(param.name), + secondName: 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 statements = CodeBlockItemListSyntax(body.compactMap { item in + if let cb = item.syntax as? CodeBlockItemSyntax { return cb.with(\.trailingTrivia, .newline) } + if let stmt = item.syntax as? StmtSyntax { + return CodeBlockItemSyntax(item: .stmt(stmt), trailingTrivia: .newline) + } + if let expr = item.syntax as? ExprSyntax { + return CodeBlockItemSyntax(item: .expr(expr), trailingTrivia: .newline) + } + return nil + }) + + let bodyBlock = CodeBlockSyntax( + leftBrace: .leftBraceToken(leadingTrivia: .space, trailingTrivia: .newline), + statements: statements, + 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 + ) + } +} + public extension CodeBlock { func generateCode() -> String { guard let decl = syntax as? DeclSyntaxProtocol else { diff --git a/Tests/SwiftBuilderTests/SwiftBuilderTestsB.swift b/Tests/SwiftBuilderTests/SwiftBuilderTestsB.swift index f294243..eb1b702 100644 --- a/Tests/SwiftBuilderTests/SwiftBuilderTestsB.swift +++ b/Tests/SwiftBuilderTests/SwiftBuilderTestsB.swift @@ -112,24 +112,24 @@ final class SwiftBuilderTestsB: XCTestCase { SwitchCase(".ace") { Return { Init("Values") { - Parameter(name: "first", value: "1") - Parameter(name: "second", value: "11") + Parameter(name: "first", type: "", defaultValue: "1") + Parameter(name: "second", type: "", defaultValue: "11") } } } SwitchCase(".jack", ".queen", ".king") { Return { Init("Values") { - Parameter(name: "first", value: "10") - Parameter(name: "second", value: "nil") + Parameter(name: "first", type: "", defaultValue: "10") + Parameter(name: "second", type: "", defaultValue: "nil") } } } Default { Return { Init("Values") { - Parameter(name: "first", value: "self.rawValue") - Parameter(name: "second", value: "nil") + Parameter(name: "first", type: "", defaultValue: "self.rawValue") + Parameter(name: "second", type: "", defaultValue: "nil") } } } diff --git a/Tests/SwiftBuilderTests/SwiftBuilderTestsC.swift b/Tests/SwiftBuilderTests/SwiftBuilderTestsC.swift index 9471921..15bbd6c 100644 --- a/Tests/SwiftBuilderTests/SwiftBuilderTestsC.swift +++ b/Tests/SwiftBuilderTests/SwiftBuilderTestsC.swift @@ -3,10 +3,10 @@ import XCTest final class SwiftBuilderTestsC: XCTestCase { func testBasicFunction() throws { - let function = Function("calculateSum", returns: "Int") { + let function = Function("calculateSum", { Parameter(name: "a", type: "Int") Parameter(name: "b", type: "Int") - } _: { + }, returns: "Int") { Return { VariableExp("a + b") } @@ -37,9 +37,9 @@ final class SwiftBuilderTestsC: XCTestCase { } func testStaticFunction() throws { - let function = Function("createInstance", returns: "MyType", { - Parameter(name: "value", type: "String") - }) { + let function = Function("createInstance", { + Parameter(name: "value", type: "String") + }, returns: "MyType") { Return { Init("MyType") { Parameter(name: "value", type: "String") @@ -101,4 +101,4 @@ final class SwiftBuilderTestsC: XCTestCase { XCTAssertEqual(normalizedGenerated, normalizedExpected) } -} +} \ No newline at end of file From 7c8289179d9f98a9cd310f8f40adf25ed3394f00 Mon Sep 17 00:00:00 2001 From: Leo Dion Date: Sun, 15 Jun 2025 16:44:27 -0400 Subject: [PATCH 06/21] refactoring the files --- Sources/SwiftBuilder/Assignment.swift | 19 +- Sources/SwiftBuilder/CodeBlock+Generate.swift | 38 +- Sources/SwiftBuilder/ComputedProperty.swift | 22 +- Sources/SwiftBuilder/Default.swift | 12 +- Sources/SwiftBuilder/Function.swift | 73 +- Sources/SwiftBuilder/If.swift | 24 +- Sources/SwiftBuilder/Parameter.swift | 6 +- Sources/SwiftBuilder/PlusAssign.swift | 19 +- Sources/SwiftBuilder/Return.swift | 26 +- Sources/SwiftBuilder/Struct.swift | 20 +- .../SwiftBuilder/StructuralComponents.swift | 862 ------------------ Sources/SwiftBuilder/SwiftBuilder.swift | 27 - Sources/SwiftBuilder/Switch.swift | 2 +- Sources/SwiftBuilder/SwitchCase.swift | 12 +- Sources/SwiftBuilder/Variable.swift | 14 +- Sources/SwiftBuilder/VariableDecl.swift | 23 +- Sources/SwiftBuilder/VariableExp.swift | 86 +- 17 files changed, 134 insertions(+), 1151 deletions(-) delete mode 100644 Sources/SwiftBuilder/StructuralComponents.swift delete mode 100644 Sources/SwiftBuilder/SwiftBuilder.swift diff --git a/Sources/SwiftBuilder/Assignment.swift b/Sources/SwiftBuilder/Assignment.swift index c5152aa..c5fcf2f 100644 --- a/Sources/SwiftBuilder/Assignment.swift +++ b/Sources/SwiftBuilder/Assignment.swift @@ -33,12 +33,19 @@ public struct Assignment: CodeBlock { right = ExprSyntax(DeclReferenceExprSyntax(baseName: .identifier(value))) } let assign = ExprSyntax(AssignmentExprSyntax(equal: .equalToken(leadingTrivia: .space, trailingTrivia: .space))) - return SequenceExprSyntax( - elements: ExprListSyntax([ - left, - assign, - right - ]) + return CodeBlockItemSyntax( + item: .expr( + ExprSyntax( + SequenceExprSyntax( + elements: ExprListSyntax([ + left, + assign, + right + ]) + ) + ) + ), + trailingTrivia: .newline ) } } diff --git a/Sources/SwiftBuilder/CodeBlock+Generate.swift b/Sources/SwiftBuilder/CodeBlock+Generate.swift index 46af657..6792c58 100644 --- a/Sources/SwiftBuilder/CodeBlock+Generate.swift +++ b/Sources/SwiftBuilder/CodeBlock+Generate.swift @@ -3,24 +3,26 @@ 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)]) + guard let decl = syntax as? DeclSyntaxProtocol else { + fatalError("Only declaration syntax is supported at the top level.") } - - let sourceFile = SourceFileSyntax(statements: statements) - return sourceFile.description.trimmingCharacters(in: .whitespacesAndNewlines) + let sourceFile = SourceFileSyntax( + statements: CodeBlockItemListSyntax([ + CodeBlockItemSyntax(item: .decl(DeclSyntax(decl))) + ]) + ) + return sourceFile.description + } +} + +public extension Array where Element == CodeBlock { + func generateCode() -> String { + let decls = compactMap { $0.syntax as? DeclSyntaxProtocol } + let sourceFile = SourceFileSyntax( + statements: CodeBlockItemListSyntax(decls.map { decl in + CodeBlockItemSyntax(item: .decl(DeclSyntax(decl))) + }) + ) + return sourceFile.description } } \ No newline at end of file diff --git a/Sources/SwiftBuilder/ComputedProperty.swift b/Sources/SwiftBuilder/ComputedProperty.swift index f263b36..71e45d7 100644 --- a/Sources/SwiftBuilder/ComputedProperty.swift +++ b/Sources/SwiftBuilder/ComputedProperty.swift @@ -12,19 +12,19 @@ public struct ComputedProperty: CodeBlock { } public var syntax: SyntaxProtocol { + let statements = CodeBlockItemListSyntax(self.body.compactMap { item in + if let cb = item.syntax as? CodeBlockItemSyntax { return cb.with(\.trailingTrivia, .newline) } + if let stmt = item.syntax as? StmtSyntax { + return CodeBlockItemSyntax(item: .stmt(stmt), trailingTrivia: .newline) + } + if let expr = item.syntax as? ExprSyntax { + return CodeBlockItemSyntax(item: .expr(expr), trailingTrivia: .newline) + } + return nil + }) 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) - })), + accessors: .getter(statements), rightBrace: TokenSyntax.rightBraceToken(leadingTrivia: .newline) ) let identifier = TokenSyntax.identifier(name, trailingTrivia: .space) diff --git a/Sources/SwiftBuilder/Default.swift b/Sources/SwiftBuilder/Default.swift index 713c595..4760a94 100644 --- a/Sources/SwiftBuilder/Default.swift +++ b/Sources/SwiftBuilder/Default.swift @@ -12,17 +12,7 @@ public struct Default: 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 statements = CodeBlockItemListSyntax(body.compactMap { $0.syntax.as(CodeBlockItemSyntax.self) }) let label = SwitchDefaultLabelSyntax( defaultKeyword: .keyword(.default, trailingTrivia: .space), colon: .colonToken() diff --git a/Sources/SwiftBuilder/Function.swift b/Sources/SwiftBuilder/Function.swift index 63e1ff5..bb313c9 100644 --- a/Sources/SwiftBuilder/Function.swift +++ b/Sources/SwiftBuilder/Function.swift @@ -1,3 +1,4 @@ + import SwiftSyntax public struct Function: CodeBlock { @@ -8,14 +9,7 @@ public struct Function: 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]) { + public init(_ name: String, @ParameterBuilderResult _ params: () -> [Parameter], returns returnType: String? = nil, @CodeBlockBuilderResult _ content: () -> [CodeBlock]) { self.name = name self.parameters = params() self.returnType = returnType @@ -39,30 +33,24 @@ public struct Function: CodeBlock { 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)) + let paramList = FunctionParameterListSyntax(parameters.enumerated().map { index, param in + var paramSyntax = FunctionParameterSyntax( + firstName: .identifier(param.name), + secondName: 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))) + ) } - return paramSyntax - }) - } + ) + if index < parameters.count - 1 { + paramSyntax = paramSyntax.with(\.trailingComma, .commaToken(trailingTrivia: .space)) + } + return paramSyntax + }) // Build return type if specified var returnClause: ReturnClauseSyntax? @@ -74,19 +62,20 @@ public struct Function: CodeBlock { } // Build function body + let statements = CodeBlockItemListSyntax(body.compactMap { item in + if let cb = item.syntax as? CodeBlockItemSyntax { return cb.with(\.trailingTrivia, .newline) } + if let stmt = item.syntax as? StmtSyntax { + return CodeBlockItemSyntax(item: .stmt(stmt), trailingTrivia: .newline) + } + if let expr = item.syntax as? ExprSyntax { + return CodeBlockItemSyntax(item: .expr(expr), trailingTrivia: .newline) + } + return nil + }) + 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) - }), + statements: statements, rightBrace: .rightBraceToken(leadingTrivia: .newline) ) diff --git a/Sources/SwiftBuilder/If.swift b/Sources/SwiftBuilder/If.swift index 2a85dba..8923d15 100644 --- a/Sources/SwiftBuilder/If.swift +++ b/Sources/SwiftBuilder/If.swift @@ -33,33 +33,13 @@ public struct If: CodeBlock { } 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) - }), + statements: CodeBlockItemListSyntax(body.compactMap { $0.syntax.as(CodeBlockItemSyntax.self) }), 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) - }), + statements: CodeBlockItemListSyntax($0.compactMap { $0.syntax.as(CodeBlockItemSyntax.self) }), rightBrace: .rightBraceToken(leadingTrivia: .newline) )) } diff --git a/Sources/SwiftBuilder/Parameter.swift b/Sources/SwiftBuilder/Parameter.swift index 1c533ce..3f74200 100644 --- a/Sources/SwiftBuilder/Parameter.swift +++ b/Sources/SwiftBuilder/Parameter.swift @@ -6,15 +6,11 @@ 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) { + public init(name: String, type: String, defaultValue: String? = nil) { 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 { diff --git a/Sources/SwiftBuilder/PlusAssign.swift b/Sources/SwiftBuilder/PlusAssign.swift index b65e08a..219f568 100644 --- a/Sources/SwiftBuilder/PlusAssign.swift +++ b/Sources/SwiftBuilder/PlusAssign.swift @@ -30,12 +30,19 @@ public struct PlusAssign: CodeBlock { right = ExprSyntax(DeclReferenceExprSyntax(baseName: .identifier(value))) } let assign = ExprSyntax(BinaryOperatorExprSyntax(operator: .binaryOperator("+=", leadingTrivia: .space, trailingTrivia: .space))) - return SequenceExprSyntax( - elements: ExprListSyntax([ - left, - assign, - right - ]) + return CodeBlockItemSyntax( + item: .expr( + ExprSyntax( + SequenceExprSyntax( + elements: ExprListSyntax([ + left, + assign, + right + ]) + ) + ) + ), + trailingTrivia: .newline ) } } diff --git a/Sources/SwiftBuilder/Return.swift b/Sources/SwiftBuilder/Return.swift index fb848e8..afb5741 100644 --- a/Sources/SwiftBuilder/Return.swift +++ b/Sources/SwiftBuilder/Return.swift @@ -10,14 +10,28 @@ public struct Return: CodeBlock { 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 CodeBlockItemSyntax( + item: .stmt( + StmtSyntax( + ReturnStmtSyntax( + returnKeyword: .keyword(.return, trailingTrivia: .space), + expression: ExprSyntax(DeclReferenceExprSyntax(baseName: .identifier(varExp.name))) + ) + ) + ), + trailingTrivia: .newline ) } - return ReturnStmtSyntax( - returnKeyword: .keyword(.return, trailingTrivia: .space), - expression: ExprSyntax(expr.syntax) + return CodeBlockItemSyntax( + item: .stmt( + StmtSyntax( + ReturnStmtSyntax( + returnKeyword: .keyword(.return, trailingTrivia: .space), + expression: ExprSyntax(expr.syntax) + ) + ) + ), + trailingTrivia: .newline ) } } \ No newline at end of file diff --git a/Sources/SwiftBuilder/Struct.swift b/Sources/SwiftBuilder/Struct.swift index cdee138..7600900 100644 --- a/Sources/SwiftBuilder/Struct.swift +++ b/Sources/SwiftBuilder/Struct.swift @@ -4,12 +4,10 @@ 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]) { + public init(_ name: String, @CodeBlockBuilderResult _ content: () -> [CodeBlock]) { self.name = name self.members = content() - self.genericParameter = generic } public func inherits(_ type: String) -> Self { @@ -20,20 +18,7 @@ public struct Struct: CodeBlock { 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() - ) - } + let identifier = TokenSyntax.identifier(name, trailingTrivia: .space) var inheritanceClause: InheritanceClauseSyntax? if let inheritance = inheritance { @@ -53,7 +38,6 @@ public struct Struct: CodeBlock { return StructDeclSyntax( structKeyword: structKeyword, name: identifier, - genericParameterClause: genericParameterClause, inheritanceClause: inheritanceClause, memberBlock: memberBlock ) diff --git a/Sources/SwiftBuilder/StructuralComponents.swift b/Sources/SwiftBuilder/StructuralComponents.swift deleted file mode 100644 index ee9330f..0000000 --- a/Sources/SwiftBuilder/StructuralComponents.swift +++ /dev/null @@ -1,862 +0,0 @@ -import Foundation -import SwiftSyntax -import SwiftParser - -public enum VariableKind { - case `let` - case `var` -} - -public struct Struct: 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 structKeyword = TokenSyntax.keyword(.struct, 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 StructDeclSyntax( - structKeyword: structKeyword, - name: identifier, - inheritanceClause: inheritanceClause, - memberBlock: memberBlock - ) - } -} - -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 - ) - ]) - ) - } -} - -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 { $0.syntax.as(CodeBlockItemSyntax.self) }) - 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 } -} - -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 = CaseItemListSyntax([ - CaseItemSyntax( - 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 } -} - -public struct Variable: CodeBlock { - private let kind: VariableKind - private let name: String - private let type: String - - public init(_ kind: VariableKind, name: String, type: String) { - self.kind = kind - self.name = name - self.type = type - } - - 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)) - ) - - return VariableDeclSyntax( - bindingSpecifier: bindingKeyword, - bindings: PatternBindingListSyntax([ - PatternBindingSyntax( - pattern: IdentifierPatternSyntax(identifier: identifier), - typeAnnotation: typeAnnotation - ) - ]) - ) - } -} - -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 statements = CodeBlockItemListSyntax(self.body.compactMap { item in - if let cb = item.syntax as? CodeBlockItemSyntax { return cb.with(\ .trailingTrivia, .newline) } - if let stmt = item.syntax as? StmtSyntax { - return CodeBlockItemSyntax(item: .stmt(stmt), trailingTrivia: .newline) - } - if let expr = item.syntax as? ExprSyntax { - return CodeBlockItemSyntax(item: .expr(expr), trailingTrivia: .newline) - } - return nil - }) - let accessor = AccessorBlockSyntax( - leftBrace: TokenSyntax.leftBraceToken(leadingTrivia: .space, trailingTrivia: .newline), - accessors: .getter(statements), - 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 - ) - ]) - ) - } -} - -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 CodeBlockItemSyntax(item: .expr(ExprSyntax(switchExpr))) - } -} - -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 { $0.syntax.as(CodeBlockItemSyntax.self) }) - let label = SwitchDefaultLabelSyntax( - defaultKeyword: .keyword(.default, trailingTrivia: .space), - colon: .colonToken() - ) - return SwitchCaseSyntax( - label: .default(label), - statements: statements - ) - } - public var syntax: SyntaxProtocol { switchCaseSyntax } -} - -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 CodeBlockItemSyntax( - item: .decl( - DeclSyntax( - VariableDeclSyntax( - bindingSpecifier: bindingKeyword, - bindings: PatternBindingListSyntax([ - PatternBindingSyntax( - pattern: IdentifierPatternSyntax(identifier: identifier), - typeAnnotation: nil, - initializer: initializer - ) - ]) - ) - ) - ), - trailingTrivia: .newline - ) - } -} - -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 CodeBlockItemSyntax( - item: .expr( - ExprSyntax( - SequenceExprSyntax( - elements: ExprListSyntax([ - left, - assign, - right - ]) - ) - ) - ), - trailingTrivia: .newline - ) - } -} - -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(assignToken: .equalToken(leadingTrivia: .space, trailingTrivia: .space))) - return CodeBlockItemSyntax( - item: .expr( - ExprSyntax( - SequenceExprSyntax( - elements: ExprListSyntax([ - left, - assign, - right - ]) - ) - ) - ), - trailingTrivia: .newline - ) - } -} - -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 CodeBlockItemSyntax( - item: .stmt( - StmtSyntax( - ReturnStmtSyntax( - returnKeyword: .keyword(.return, trailingTrivia: .space), - expression: ExprSyntax(DeclReferenceExprSyntax(baseName: .identifier(varExp.name))) - ) - ) - ), - trailingTrivia: .newline - ) - } - return CodeBlockItemSyntax( - item: .stmt( - StmtSyntax( - ReturnStmtSyntax( - returnKeyword: .keyword(.return, trailingTrivia: .space), - expression: ExprSyntax(expr.syntax) - ) - ) - ), - trailingTrivia: .newline - ) - } -} - -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 { $0.syntax.as(CodeBlockItemSyntax.self) }), - rightBrace: .rightBraceToken(leadingTrivia: .newline) - ) - let elseBlock = elseBody.map { - IfExprSyntax.ElseBody(CodeBlockSyntax( - leftBrace: .leftBraceToken(leadingTrivia: .space, trailingTrivia: .newline), - statements: CodeBlockItemListSyntax($0.compactMap { $0.syntax.as(CodeBlockItemSyntax.self) }), - 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 - ) - ) - } -} - -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))) - ) - ) - ]) - ) - ) - ) - ) - } -} - -@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 - } -} - -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() - )) - } -} - -public struct Parameter: CodeBlock { - let name: String - let type: String - let defaultValue: String? - public init(name: String, type: String, defaultValue: String? = nil) { - self.name = name - self.type = type - self.defaultValue = defaultValue - } - 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))) - ) - } - } -} - -public struct VariableExp: CodeBlock { - let name: String - - public init(_ name: String) { - self.name = name - } - - public var syntax: SyntaxProtocol { - return TokenSyntax.identifier(self.name) - } -} - -public struct OldVariableExp: CodeBlock { - private let name: String - private let value: String - - public init(_ name: String, _ value: String) { - self.name = name - self.value = value - } - - public var syntax: SyntaxProtocol { - let name = TokenSyntax.identifier(self.name) - let value = IdentifierExprSyntax(identifier: .identifier(self.value)) - - return OptionalBindingConditionSyntax( - bindingSpecifier: .keyword(.let, trailingTrivia: .space), - pattern: IdentifierPatternSyntax(identifier: name), - initializer: InitializerClauseSyntax( - equal: .equalToken(leadingTrivia: .space, trailingTrivia: .space), - value: value - ) - ) - } -} - -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, @ParameterBuilderResult _ params: () -> [Parameter], returns returnType: String? = nil, @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(parameters.enumerated().map { index, param in - var paramSyntax = FunctionParameterSyntax( - firstName: .identifier(param.name), - secondName: 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 statements = CodeBlockItemListSyntax(body.compactMap { item in - if let cb = item.syntax as? CodeBlockItemSyntax { return cb.with(\.trailingTrivia, .newline) } - if let stmt = item.syntax as? StmtSyntax { - return CodeBlockItemSyntax(item: .stmt(stmt), trailingTrivia: .newline) - } - if let expr = item.syntax as? ExprSyntax { - return CodeBlockItemSyntax(item: .expr(expr), trailingTrivia: .newline) - } - return nil - }) - - let bodyBlock = CodeBlockSyntax( - leftBrace: .leftBraceToken(leadingTrivia: .space, trailingTrivia: .newline), - statements: statements, - 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 - ) - } -} - -public extension CodeBlock { - func generateCode() -> String { - guard let decl = syntax as? DeclSyntaxProtocol else { - fatalError("Only declaration syntax is supported at the top level.") - } - let sourceFile = SourceFileSyntax( - statements: CodeBlockItemListSyntax([ - CodeBlockItemSyntax(item: .decl(DeclSyntax(decl))) - ]) - ) - return sourceFile.description - } -} - -public extension Array where Element == CodeBlock { - func generateCode() -> String { - let decls = compactMap { $0.syntax as? DeclSyntaxProtocol } - let sourceFile = SourceFileSyntax( - statements: CodeBlockItemListSyntax(decls.map { decl in - CodeBlockItemSyntax(item: .decl(DeclSyntax(decl))) - }) - ) - return sourceFile.description - } -} - diff --git a/Sources/SwiftBuilder/SwiftBuilder.swift b/Sources/SwiftBuilder/SwiftBuilder.swift deleted file mode 100644 index 6496a74..0000000 --- a/Sources/SwiftBuilder/SwiftBuilder.swift +++ /dev/null @@ -1,27 +0,0 @@ -// The Swift Programming Language -// https://docs.swift.org/swift-book - -import Foundation -import SwiftSyntax -// -//public extension CodeBlock { -// func generateCode() -> String { -// let sourceFile = SourceFileSyntax( -// statements: CodeBlockItemListSyntax([ -// CodeBlockItemSyntax(item: .decl(DeclSyntax(syntax))) -// ]) -// ) -// return sourceFile.formatted().description -// } -//} -// -//public extension Array where Element == CodeBlock { -// func generateCode() -> String { -// let sourceFile = SourceFileSyntax( -// statements: CodeBlockItemListSyntax(map { block in -// CodeBlockItemSyntax(item: .decl(DeclSyntax(block.syntax))) -// }) -// ) -// return sourceFile.formatted().description -// } -//} diff --git a/Sources/SwiftBuilder/Switch.swift b/Sources/SwiftBuilder/Switch.swift index c3f3617..5a3ebc7 100644 --- a/Sources/SwiftBuilder/Switch.swift +++ b/Sources/SwiftBuilder/Switch.swift @@ -30,6 +30,6 @@ public struct Switch: CodeBlock { cases: cases, rightBrace: .rightBraceToken(leadingTrivia: .newline) ) - return switchExpr + return CodeBlockItemSyntax(item: .expr(ExprSyntax(switchExpr))) } } diff --git a/Sources/SwiftBuilder/SwitchCase.swift b/Sources/SwiftBuilder/SwitchCase.swift index 0894450..7d566e5 100644 --- a/Sources/SwiftBuilder/SwitchCase.swift +++ b/Sources/SwiftBuilder/SwitchCase.swift @@ -25,17 +25,7 @@ public struct SwitchCase: CodeBlock { } 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 statements = CodeBlockItemListSyntax(body.compactMap { $0.syntax.as(CodeBlockItemSyntax.self) }) let label = SwitchCaseLabelSyntax( caseKeyword: .keyword(.case, trailingTrivia: .space), caseItems: caseItems, diff --git a/Sources/SwiftBuilder/Variable.swift b/Sources/SwiftBuilder/Variable.swift index 990852e..b7e6e3e 100644 --- a/Sources/SwiftBuilder/Variable.swift +++ b/Sources/SwiftBuilder/Variable.swift @@ -10,13 +10,11 @@ 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) { + public init(_ kind: VariableKind, name: String, type: String) { self.kind = kind self.name = name self.type = type - self.defaultValue = defaultValue } public var syntax: SyntaxProtocol { @@ -27,20 +25,12 @@ public struct Variable: CodeBlock { 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 + typeAnnotation: typeAnnotation ) ]) ) diff --git a/Sources/SwiftBuilder/VariableDecl.swift b/Sources/SwiftBuilder/VariableDecl.swift index 5c9a913..8348c0b 100644 --- a/Sources/SwiftBuilder/VariableDecl.swift +++ b/Sources/SwiftBuilder/VariableDecl.swift @@ -33,15 +33,22 @@ public struct VariableDecl: CodeBlock { ) } } - return VariableDeclSyntax( - bindingSpecifier: bindingKeyword, - bindings: PatternBindingListSyntax([ - PatternBindingSyntax( - pattern: IdentifierPatternSyntax(identifier: identifier), - typeAnnotation: nil, - initializer: initializer + return CodeBlockItemSyntax( + item: .decl( + DeclSyntax( + VariableDeclSyntax( + bindingSpecifier: bindingKeyword, + bindings: PatternBindingListSyntax([ + PatternBindingSyntax( + pattern: IdentifierPatternSyntax(identifier: identifier), + typeAnnotation: nil, + initializer: initializer + ) + ]) + ) ) - ]) + ), + trailingTrivia: .newline ) } } \ No newline at end of file diff --git a/Sources/SwiftBuilder/VariableExp.swift b/Sources/SwiftBuilder/VariableExp.swift index c933468..359c2a3 100644 --- a/Sources/SwiftBuilder/VariableExp.swift +++ b/Sources/SwiftBuilder/VariableExp.swift @@ -13,91 +13,7 @@ public struct VariableExp: CodeBlock { 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() - )) + return TokenSyntax.identifier(self.name) } } From e5530646f65cf35b0031210bebd8708c25666a03 Mon Sep 17 00:00:00 2001 From: Leo Dion Date: Mon, 16 Jun 2025 09:29:43 -0400 Subject: [PATCH 07/21] adding parser code --- Package.swift | 1 + .../SwiftBuilder/parser/SyntaxParser.swift | 9 +- .../SwiftBuilder/parser/SyntaxResponse.swift | 8 +- .../SwiftBuilder/parser/TokenVisitor.swift | 156 +++++++++++------- 4 files changed, 107 insertions(+), 67 deletions(-) diff --git a/Package.swift b/Package.swift index ff0179b..aa92fb7 100644 --- a/Package.swift +++ b/Package.swift @@ -25,6 +25,7 @@ let package = Package( name: "SwiftBuilder", dependencies: [ .product(name: "SwiftSyntax", package: "swift-syntax"), + .product(name: "SwiftOperators", package: "swift-syntax"), .product(name: "SwiftParser", package: "swift-syntax") ] ), diff --git a/Sources/SwiftBuilder/parser/SyntaxParser.swift b/Sources/SwiftBuilder/parser/SyntaxParser.swift index f492600..d2248c7 100644 --- a/Sources/SwiftBuilder/parser/SyntaxParser.swift +++ b/Sources/SwiftBuilder/parser/SyntaxParser.swift @@ -3,8 +3,8 @@ import SwiftSyntax import SwiftOperators import SwiftParser -package struct SyntaxParser { - package static func parse(code: String, options: [String] = []) throws -> SyntaxResponse { +struct SyntaxParser { + static func parse(code: String, options: [String] = []) throws -> SyntaxResponse { let sourceFile = Parser.parse(source: code) let syntax: Syntax @@ -15,16 +15,17 @@ package struct SyntaxParser { } let visitor = TokenVisitor( - locationConverter: SourceLocationConverter(fileName: "", tree: sourceFile), + locationConverter: SourceLocationConverter(file: "", tree: sourceFile), showMissingTokens: options.contains("showmissing") ) _ = visitor.rewrite(syntax) + let html = "\(visitor.list.joined())" let tree = visitor.tree let encoder = JSONEncoder() let json = String(decoding: try encoder.encode(tree), as: UTF8.self) - return SyntaxResponse(syntaxJSON: json, swiftVersion: version) + return SyntaxResponse(syntaxHTML: html, syntaxJSON: json, swiftVersion: version) } } diff --git a/Sources/SwiftBuilder/parser/SyntaxResponse.swift b/Sources/SwiftBuilder/parser/SyntaxResponse.swift index 394b467..2e7d3f7 100644 --- a/Sources/SwiftBuilder/parser/SyntaxResponse.swift +++ b/Sources/SwiftBuilder/parser/SyntaxResponse.swift @@ -1,7 +1,7 @@ import Foundation -package struct SyntaxResponse: Codable { - //package let syntaxHTML: String - package let syntaxJSON: String - package let swiftVersion: String +struct SyntaxResponse: Codable { + let syntaxHTML: String + let syntaxJSON: String + let swiftVersion: String } diff --git a/Sources/SwiftBuilder/parser/TokenVisitor.swift b/Sources/SwiftBuilder/parser/TokenVisitor.swift index 6c2025e..dcb4e37 100644 --- a/Sources/SwiftBuilder/parser/TokenVisitor.swift +++ b/Sources/SwiftBuilder/parser/TokenVisitor.swift @@ -1,5 +1,5 @@ import Foundation -@_spi(RawSyntax) import SwiftSyntax +import SwiftSyntax final class TokenVisitor: SyntaxRewriter { var list = [String]() @@ -14,10 +14,17 @@ final class TokenVisitor: SyntaxRewriter { init(locationConverter: SourceLocationConverter, showMissingTokens: Bool) { self.locationConverter = locationConverter self.showMissingTokens = showMissingTokens - super.init(viewMode: showMissingTokens ? .all : .sourceAccurate) + } + + func rewrite(_ node: Syntax) -> Syntax { + visit(node) } override func visitPre(_ node: Syntax) { + if let token = node.as(TokenSyntax.self), token.presence == .missing, !showMissingTokens { + return + } + let syntaxNodeType = node.syntaxNodeType let className: String @@ -30,12 +37,12 @@ final class TokenVisitor: SyntaxRewriter { let title: String let content: String let type: String - if let tokenSyntax = node.as(TokenSyntax.self) { - title = tokenSyntax.text - content = "\(tokenSyntax.tokenKind)" + if let token = node.as(TokenSyntax.self) { + title = sourceAccurateText(token) + content = "\(token.tokenKind)" type = "Token" } else { - title = "\(node.trimmed)" + title = sourceAccurateText(node) content = "\(syntaxNodeType)" type = "Syntax" } @@ -43,18 +50,22 @@ final class TokenVisitor: SyntaxRewriter { let sourceRange = node.sourceRange(converter: locationConverter) let start = sourceRange.start let end = sourceRange.end + let startRow = start.line ?? 1 + let startColumn = start.column ?? 1 + let endRow = end.line ?? 1 + let endColumn = end.column ?? 1 let graphemeStartColumn: Int - if let prefix = String(locationConverter.sourceLines[start.line - 1].utf8.prefix(start.column - 1)) { + if let prefix = String(locationConverter.sourceLines[startRow - 1].utf8.prefix(startColumn - 1)) { graphemeStartColumn = prefix.utf16.count + 1 } else { - graphemeStartColumn = start.column + graphemeStartColumn = startColumn } let graphemeEndColumn: Int - if let prefix = String(locationConverter.sourceLines[end.line - 1].utf8.prefix(end.column - 1)) { + if let prefix = String(locationConverter.sourceLines[endRow - 1].utf8.prefix(endColumn - 1)) { graphemeEndColumn = prefix.utf16.count + 1 } else { - graphemeEndColumn = end.column + graphemeEndColumn = endColumn } list.append( @@ -62,7 +73,7 @@ final class TokenVisitor: SyntaxRewriter { "data-title='\(title.escapeHTML().replaceInvisiblesWithSymbols())' " + "data-content='\(content.escapeHTML().replaceInvisiblesWithHTML())' " + "data-type='\(type.escapeHTML())' " + - #"data-range='{"startRow":\#(start.line),"startColumn":\#(start.column),"endRow":\#(end.line),"endColumn":\#(end.column)}'>"# + #"data-range='{"startRow":\#(startRow),"startColumn":\#(startColumn),"endRow":\#(endRow),"endColumn":\#(endColumn)}'>"# ) let syntaxType: SyntaxType @@ -83,11 +94,11 @@ final class TokenVisitor: SyntaxRewriter { id: index, text: className, range: Range( - startRow: start.line, - startColumn: start.column, + startRow: startRow, + startColumn: startColumn, graphemeStartColumn: graphemeStartColumn, - endRow: end.line, - endColumn: end.column, + endRow: endRow, + endColumn: endColumn, graphemeEndColumn: graphemeEndColumn ), type: syntaxType @@ -96,52 +107,34 @@ final class TokenVisitor: SyntaxRewriter { tree.append(treeNode) index += 1 - let allChildren = node.children(viewMode: .all) - switch node.syntaxNodeType.structure { case .layout(let keyPaths): if let syntaxNode = node.as(node.syntaxNodeType) { - for keyPath in keyPaths { - 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"))) - continue - } - - let keyPath = keyPath as AnyKeyPath - switch syntaxNode[keyPath: keyPath] { - case let value as TokenSyntax: - if value.presence == .missing { + for (index, keyPath) in keyPaths.enumerated() { + let mirror = Mirror(reflecting: syntaxNode) + if let label = mirror.children.map({ $0 })[index].label { + let key = label + switch syntaxNode[keyPath: keyPath] { + case let value as TokenSyntax: treeNode.structure.append( StructureProperty( - name: name, + name: key, value: StructureValue( text: value.text, kind: "\(value.tokenKind)" ) ) ) - } else { - treeNode.structure.append( - StructureProperty( - name: name, - value: StructureValue( - text: value.text, - 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)")) - } else { - treeNode.structure.append(StructureProperty(name: name, value: StructureValue(text: "\(value)"))) + case let value?: + if let value = value as? SyntaxProtocol { + let type = "\(value.syntaxNodeType)" + treeNode.structure.append(StructureProperty(name: key, value: StructureValue(text: "\(type)"), ref: "\(type)")) + } else { + treeNode.structure.append(StructureProperty(name: key, value: StructureValue(text: "\(value)"))) + } + case .none: + treeNode.structure.append(StructureProperty(name: key)) } - case .none: - treeNode.structure.append(StructureProperty(name: name)) } } } @@ -161,13 +154,17 @@ final class TokenVisitor: SyntaxRewriter { } override func visit(_ token: TokenSyntax) -> TokenSyntax { - current.text = token - .text + if token.presence == .missing && !showMissingTokens { + return token + } + + let text = sourceAccurateText(token) + current.text = text .escapeHTML() .replaceInvisiblesWithHTML() .replaceHTMLWhitespacesWithSymbols() if token.presence == .missing { - current.class = "\(token.presence)" + current.class = token.text.lowercased() } current.token = Token(kind: "\(token.tokenKind)", leadingTrivia: "", trailingTrivia: "") @@ -187,6 +184,10 @@ final class TokenVisitor: SyntaxRewriter { } override func visitPost(_ node: Syntax) { + if let token = node.as(TokenSyntax.self), token.presence == .missing, !showMissingTokens { + return + } + list.append("") if let parent = current.parent { current = tree[parent] @@ -207,13 +208,28 @@ final class TokenVisitor: SyntaxRewriter { let sourceRange = token.sourceRange(converter: locationConverter) let start = sourceRange.start let end = sourceRange.end - let text = token.presence == .present || showMissingTokens ? token.text : "" + let startRow = start.line ?? 1 + let startColumn = start.column ?? 1 + let endRow = end.line ?? 1 + let endColumn = end.column ?? 1 + let text: String + switch token.presence { + case .present: + text = sourceAccurateText(token) + case .missing: + if showMissingTokens { + text = sourceAccurateText(token) + } else { + text = "" + } + } + list.append( - ""# + + #"data-range='{"startRow":\#(startRow),"startColumn":\#(startColumn),"endRow":\#(endRow),"endColumn":\#(endColumn)}'>"# + "\(text.escapeHTML().replaceInvisiblesWithHTML())" ) } @@ -246,15 +262,37 @@ final class TokenVisitor: SyntaxRewriter { trivia += wrapWithSpanTag(class: "docBlockComment", text: text) case .unexpectedText(let text): trivia += wrapWithSpanTag(class: "unexpectedText", text: text) - case .backslashes(let count): - trivia += String(repeating: #"\"#, count: count) - case .pounds(let count): - trivia += String(repeating: "#", count: count) +// case .shebang(let text): +// trivia += wrapWithSpanTag(class: "shebang", text: text) + case .backslashes(_): + break; + case .pounds(_): + break; } return trivia } } +private func sourceAccurateText(_ syntax: Syntax) -> String { + let text = "\(syntax.withoutTrivia())" + let utf8Length = syntax.contentLength.utf8Length + if text.utf8.count == utf8Length { + return text + } else { + return String(decoding: syntax.syntaxTextBytes.prefix(utf8Length), as: UTF8.self) + } +} + +private func sourceAccurateText(_ token: TokenSyntax) -> String { + let text = token.text + let utf8Length = token.contentLength.utf8Length + if text.utf8.count == utf8Length { + return text + } else { + return String(decoding: token.syntaxTextBytes.prefix(utf8Length), as: UTF8.self) + } +} + private extension String { func escapeHTML() -> String { var string = self From d5e4e944f9b500f7c39a53c9d32d9270b1b35462 Mon Sep 17 00:00:00 2001 From: Leo Dion Date: Mon, 16 Jun 2025 10:16:08 -0400 Subject: [PATCH 08/21] fixing ast parser --- Package.swift | 8 ++++ .../SwiftBuilder/parser/SyntaxParser.swift | 8 ++-- .../SwiftBuilder/parser/SyntaxResponse.swift | 8 ++-- .../SwiftBuilder/parser/TokenVisitor.swift | 42 +++++++++---------- 4 files changed, 37 insertions(+), 29 deletions(-) diff --git a/Package.swift b/Package.swift index aa92fb7..03e785f 100644 --- a/Package.swift +++ b/Package.swift @@ -14,6 +14,10 @@ let package = Package( name: "SwiftBuilder", targets: ["SwiftBuilder"] ), + .executable( + name: "SwiftBuilderCLI", + targets: ["SwiftBuilderCLI"] + ), ], dependencies: [ .package(url: "https://github.com/apple/swift-syntax.git", from: "602.0.0-prerelease-2025-05-29") @@ -29,6 +33,10 @@ let package = Package( .product(name: "SwiftParser", package: "swift-syntax") ] ), + .executableTarget( + name: "SwiftBuilderCLI", + dependencies: ["SwiftBuilder"] + ), .testTarget( name: "SwiftBuilderTests", dependencies: ["SwiftBuilder"] diff --git a/Sources/SwiftBuilder/parser/SyntaxParser.swift b/Sources/SwiftBuilder/parser/SyntaxParser.swift index d2248c7..95ec15e 100644 --- a/Sources/SwiftBuilder/parser/SyntaxParser.swift +++ b/Sources/SwiftBuilder/parser/SyntaxParser.swift @@ -3,8 +3,8 @@ import SwiftSyntax import SwiftOperators import SwiftParser -struct SyntaxParser { - static func parse(code: String, options: [String] = []) throws -> SyntaxResponse { +package struct SyntaxParser { + package static func parse(code: String, options: [String] = []) throws -> SyntaxResponse { let sourceFile = Parser.parse(source: code) let syntax: Syntax @@ -20,12 +20,12 @@ struct SyntaxParser { ) _ = visitor.rewrite(syntax) - let html = "\(visitor.list.joined())" + //let html = "\(visitor.list.joined())" let tree = visitor.tree let encoder = JSONEncoder() let json = String(decoding: try encoder.encode(tree), as: UTF8.self) - return SyntaxResponse(syntaxHTML: html, syntaxJSON: json, swiftVersion: version) + return SyntaxResponse( syntaxJSON: json, swiftVersion: version) } } diff --git a/Sources/SwiftBuilder/parser/SyntaxResponse.swift b/Sources/SwiftBuilder/parser/SyntaxResponse.swift index 2e7d3f7..394b467 100644 --- a/Sources/SwiftBuilder/parser/SyntaxResponse.swift +++ b/Sources/SwiftBuilder/parser/SyntaxResponse.swift @@ -1,7 +1,7 @@ import Foundation -struct SyntaxResponse: Codable { - let syntaxHTML: String - let syntaxJSON: String - let swiftVersion: String +package struct SyntaxResponse: Codable { + //package let syntaxHTML: String + package let syntaxJSON: String + package let swiftVersion: String } diff --git a/Sources/SwiftBuilder/parser/TokenVisitor.swift b/Sources/SwiftBuilder/parser/TokenVisitor.swift index dcb4e37..0700352 100644 --- a/Sources/SwiftBuilder/parser/TokenVisitor.swift +++ b/Sources/SwiftBuilder/parser/TokenVisitor.swift @@ -2,7 +2,7 @@ import Foundation import SwiftSyntax final class TokenVisitor: SyntaxRewriter { - var list = [String]() + //var list = [String]() var tree = [TreeNode]() private var current: TreeNode! @@ -67,14 +67,14 @@ final class TokenVisitor: SyntaxRewriter { } else { graphemeEndColumn = endColumn } - - list.append( - ""# - ) +// +// list.append( +// ""# +// ) let syntaxType: SyntaxType switch node { @@ -170,13 +170,13 @@ final class TokenVisitor: SyntaxRewriter { token.leadingTrivia.forEach { (piece) in let trivia = processTriviaPiece(piece) - list.append(trivia) + //list.append(trivia) current.token?.leadingTrivia += trivia.replaceHTMLWhitespacesWithSymbols() } processToken(token) token.trailingTrivia.forEach { (piece) in let trivia = processTriviaPiece(piece) - list.append(trivia) + //list.append(trivia) current.token?.trailingTrivia += trivia.replaceHTMLWhitespacesWithSymbols() } @@ -188,7 +188,7 @@ final class TokenVisitor: SyntaxRewriter { return } - list.append("") + //list.append("") if let parent = current.parent { current = tree[parent] } else { @@ -224,14 +224,14 @@ final class TokenVisitor: SyntaxRewriter { } } - list.append( - ""# + - "\(text.escapeHTML().replaceInvisiblesWithHTML())" - ) +// list.append( +// ""# + +// "\(text.escapeHTML().replaceInvisiblesWithHTML())" +// ) } private func processTriviaPiece(_ piece: TriviaPiece) -> String { @@ -274,7 +274,7 @@ final class TokenVisitor: SyntaxRewriter { } private func sourceAccurateText(_ syntax: Syntax) -> String { - let text = "\(syntax.withoutTrivia())" + let text = "\(syntax.debugDescription)" let utf8Length = syntax.contentLength.utf8Length if text.utf8.count == utf8Length { return text From fd29add9454af1a26cd016c0c619d3d171337151 Mon Sep 17 00:00:00 2001 From: Leo Dion Date: Mon, 16 Jun 2025 10:35:14 -0400 Subject: [PATCH 09/21] fixing ast parser --- Package.resolved | 6 +- Package.swift | 2 +- .../SwiftBuilder/parser/SyntaxParser.swift | 5 +- .../SwiftBuilder/parser/TokenVisitor.swift | 190 +++++++----------- 4 files changed, 82 insertions(+), 121 deletions(-) diff --git a/Package.resolved b/Package.resolved index df7cfda..9a2ef48 100644 --- a/Package.resolved +++ b/Package.resolved @@ -1,13 +1,13 @@ { - "originHash" : "229a09bb6d44c64be46f5fe39a7296eed19f631fbdda7aa5a22a4431cca37b10", + "originHash" : "b5881f7ff763cf360a3639d99029de2b3ab4a457e0d8f08ce744bae51c2bf670", "pins" : [ { "identity" : "swift-syntax", "kind" : "remoteSourceControl", "location" : "https://github.com/apple/swift-syntax.git", "state" : { - "revision" : "0dff260d3d1bb99c382d8dfcd6bb093e5e9cbd36", - "version" : "602.0.0-prerelease-2025-05-29" + "revision" : "f99ae8aa18f0cf0d53481901f88a0991dc3bd4a2", + "version" : "601.0.1" } } ], diff --git a/Package.swift b/Package.swift index 03e785f..966d30d 100644 --- a/Package.swift +++ b/Package.swift @@ -20,7 +20,7 @@ let package = Package( ), ], dependencies: [ - .package(url: "https://github.com/apple/swift-syntax.git", from: "602.0.0-prerelease-2025-05-29") + .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. diff --git a/Sources/SwiftBuilder/parser/SyntaxParser.swift b/Sources/SwiftBuilder/parser/SyntaxParser.swift index 95ec15e..f492600 100644 --- a/Sources/SwiftBuilder/parser/SyntaxParser.swift +++ b/Sources/SwiftBuilder/parser/SyntaxParser.swift @@ -15,17 +15,16 @@ package struct SyntaxParser { } let visitor = TokenVisitor( - locationConverter: SourceLocationConverter(file: "", tree: sourceFile), + locationConverter: SourceLocationConverter(fileName: "", tree: sourceFile), showMissingTokens: options.contains("showmissing") ) _ = visitor.rewrite(syntax) - //let html = "\(visitor.list.joined())" let tree = visitor.tree let encoder = JSONEncoder() let json = String(decoding: try encoder.encode(tree), as: UTF8.self) - return SyntaxResponse( syntaxJSON: json, swiftVersion: version) + return SyntaxResponse(syntaxJSON: json, swiftVersion: version) } } diff --git a/Sources/SwiftBuilder/parser/TokenVisitor.swift b/Sources/SwiftBuilder/parser/TokenVisitor.swift index 0700352..6c2025e 100644 --- a/Sources/SwiftBuilder/parser/TokenVisitor.swift +++ b/Sources/SwiftBuilder/parser/TokenVisitor.swift @@ -1,8 +1,8 @@ import Foundation -import SwiftSyntax +@_spi(RawSyntax) import SwiftSyntax final class TokenVisitor: SyntaxRewriter { - //var list = [String]() + var list = [String]() var tree = [TreeNode]() private var current: TreeNode! @@ -14,17 +14,10 @@ final class TokenVisitor: SyntaxRewriter { init(locationConverter: SourceLocationConverter, showMissingTokens: Bool) { self.locationConverter = locationConverter self.showMissingTokens = showMissingTokens - } - - func rewrite(_ node: Syntax) -> Syntax { - visit(node) + super.init(viewMode: showMissingTokens ? .all : .sourceAccurate) } override func visitPre(_ node: Syntax) { - if let token = node.as(TokenSyntax.self), token.presence == .missing, !showMissingTokens { - return - } - let syntaxNodeType = node.syntaxNodeType let className: String @@ -37,12 +30,12 @@ final class TokenVisitor: SyntaxRewriter { let title: String let content: String let type: String - if let token = node.as(TokenSyntax.self) { - title = sourceAccurateText(token) - content = "\(token.tokenKind)" + if let tokenSyntax = node.as(TokenSyntax.self) { + title = tokenSyntax.text + content = "\(tokenSyntax.tokenKind)" type = "Token" } else { - title = sourceAccurateText(node) + title = "\(node.trimmed)" content = "\(syntaxNodeType)" type = "Syntax" } @@ -50,31 +43,27 @@ final class TokenVisitor: SyntaxRewriter { let sourceRange = node.sourceRange(converter: locationConverter) let start = sourceRange.start let end = sourceRange.end - let startRow = start.line ?? 1 - let startColumn = start.column ?? 1 - let endRow = end.line ?? 1 - let endColumn = end.column ?? 1 let graphemeStartColumn: Int - if let prefix = String(locationConverter.sourceLines[startRow - 1].utf8.prefix(startColumn - 1)) { + if let prefix = String(locationConverter.sourceLines[start.line - 1].utf8.prefix(start.column - 1)) { graphemeStartColumn = prefix.utf16.count + 1 } else { - graphemeStartColumn = startColumn + graphemeStartColumn = start.column } let graphemeEndColumn: Int - if let prefix = String(locationConverter.sourceLines[endRow - 1].utf8.prefix(endColumn - 1)) { + if let prefix = String(locationConverter.sourceLines[end.line - 1].utf8.prefix(end.column - 1)) { graphemeEndColumn = prefix.utf16.count + 1 } else { - graphemeEndColumn = endColumn + graphemeEndColumn = end.column } -// -// list.append( -// ""# -// ) + + list.append( + ""# + ) let syntaxType: SyntaxType switch node { @@ -94,11 +83,11 @@ final class TokenVisitor: SyntaxRewriter { id: index, text: className, range: Range( - startRow: startRow, - startColumn: startColumn, + startRow: start.line, + startColumn: start.column, graphemeStartColumn: graphemeStartColumn, - endRow: endRow, - endColumn: endColumn, + endRow: end.line, + endColumn: end.column, graphemeEndColumn: graphemeEndColumn ), type: syntaxType @@ -107,34 +96,52 @@ final class TokenVisitor: SyntaxRewriter { tree.append(treeNode) index += 1 + let allChildren = node.children(viewMode: .all) + switch node.syntaxNodeType.structure { case .layout(let keyPaths): if let syntaxNode = node.as(node.syntaxNodeType) { - for (index, keyPath) in keyPaths.enumerated() { - let mirror = Mirror(reflecting: syntaxNode) - if let label = mirror.children.map({ $0 })[index].label { - let key = label - switch syntaxNode[keyPath: keyPath] { - case let value as TokenSyntax: + for keyPath in keyPaths { + 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"))) + continue + } + + let keyPath = keyPath as AnyKeyPath + switch syntaxNode[keyPath: keyPath] { + case let value as TokenSyntax: + if value.presence == .missing { treeNode.structure.append( StructureProperty( - name: key, + name: name, value: StructureValue( text: value.text, kind: "\(value.tokenKind)" ) ) ) - case let value?: - if let value = value as? SyntaxProtocol { - let type = "\(value.syntaxNodeType)" - treeNode.structure.append(StructureProperty(name: key, value: StructureValue(text: "\(type)"), ref: "\(type)")) - } else { - treeNode.structure.append(StructureProperty(name: key, value: StructureValue(text: "\(value)"))) - } - case .none: - treeNode.structure.append(StructureProperty(name: key)) + } else { + treeNode.structure.append( + StructureProperty( + name: name, + value: StructureValue( + text: value.text, + 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)")) + } else { + treeNode.structure.append(StructureProperty(name: name, value: StructureValue(text: "\(value)"))) } + case .none: + treeNode.structure.append(StructureProperty(name: name)) } } } @@ -154,29 +161,25 @@ final class TokenVisitor: SyntaxRewriter { } override func visit(_ token: TokenSyntax) -> TokenSyntax { - if token.presence == .missing && !showMissingTokens { - return token - } - - let text = sourceAccurateText(token) - current.text = text + current.text = token + .text .escapeHTML() .replaceInvisiblesWithHTML() .replaceHTMLWhitespacesWithSymbols() if token.presence == .missing { - current.class = token.text.lowercased() + current.class = "\(token.presence)" } current.token = Token(kind: "\(token.tokenKind)", leadingTrivia: "", trailingTrivia: "") token.leadingTrivia.forEach { (piece) in let trivia = processTriviaPiece(piece) - //list.append(trivia) + list.append(trivia) current.token?.leadingTrivia += trivia.replaceHTMLWhitespacesWithSymbols() } processToken(token) token.trailingTrivia.forEach { (piece) in let trivia = processTriviaPiece(piece) - //list.append(trivia) + list.append(trivia) current.token?.trailingTrivia += trivia.replaceHTMLWhitespacesWithSymbols() } @@ -184,11 +187,7 @@ final class TokenVisitor: SyntaxRewriter { } override func visitPost(_ node: Syntax) { - if let token = node.as(TokenSyntax.self), token.presence == .missing, !showMissingTokens { - return - } - - //list.append("") + list.append("") if let parent = current.parent { current = tree[parent] } else { @@ -208,30 +207,15 @@ final class TokenVisitor: SyntaxRewriter { let sourceRange = token.sourceRange(converter: locationConverter) let start = sourceRange.start let end = sourceRange.end - let startRow = start.line ?? 1 - let startColumn = start.column ?? 1 - let endRow = end.line ?? 1 - let endColumn = end.column ?? 1 - let text: String - switch token.presence { - case .present: - text = sourceAccurateText(token) - case .missing: - if showMissingTokens { - text = sourceAccurateText(token) - } else { - text = "" - } - } - -// list.append( -// ""# + -// "\(text.escapeHTML().replaceInvisiblesWithHTML())" -// ) + let text = token.presence == .present || showMissingTokens ? token.text : "" + list.append( + ""# + + "\(text.escapeHTML().replaceInvisiblesWithHTML())" + ) } private func processTriviaPiece(_ piece: TriviaPiece) -> String { @@ -262,37 +246,15 @@ final class TokenVisitor: SyntaxRewriter { trivia += wrapWithSpanTag(class: "docBlockComment", text: text) case .unexpectedText(let text): trivia += wrapWithSpanTag(class: "unexpectedText", text: text) -// case .shebang(let text): -// trivia += wrapWithSpanTag(class: "shebang", text: text) - case .backslashes(_): - break; - case .pounds(_): - break; + case .backslashes(let count): + trivia += String(repeating: #"\"#, count: count) + case .pounds(let count): + trivia += String(repeating: "#", count: count) } return trivia } } -private func sourceAccurateText(_ syntax: Syntax) -> String { - let text = "\(syntax.debugDescription)" - let utf8Length = syntax.contentLength.utf8Length - if text.utf8.count == utf8Length { - return text - } else { - return String(decoding: syntax.syntaxTextBytes.prefix(utf8Length), as: UTF8.self) - } -} - -private func sourceAccurateText(_ token: TokenSyntax) -> String { - let text = token.text - let utf8Length = token.contentLength.utf8Length - if text.utf8.count == utf8Length { - return text - } else { - return String(decoding: token.syntaxTextBytes.prefix(utf8Length), as: UTF8.self) - } -} - private extension String { func escapeHTML() -> String { var string = self From 4d87aad61996814c71e2e46550ed876d803819c3 Mon Sep 17 00:00:00 2001 From: leogdion Date: Mon, 16 Jun 2025 12:31:55 -0400 Subject: [PATCH 10/21] adding more examples --- Examples/BlackjackCard.swift | 78 -------------------------------- Examples/BlackjackCardCode.swift | 55 ---------------------- 2 files changed, 133 deletions(-) delete mode 100644 Examples/BlackjackCard.swift delete mode 100644 Examples/BlackjackCardCode.swift diff --git a/Examples/BlackjackCard.swift b/Examples/BlackjackCard.swift deleted file mode 100644 index ba8eaa7..0000000 --- a/Examples/BlackjackCard.swift +++ /dev/null @@ -1,78 +0,0 @@ -import SwiftBuilder - -// Example of generating a BlackjackCard struct with a nested Suit enum -let structExample = 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") { - 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") - - Variable(.let, name: "rank", type: "Rank") - Variable(.let, name: "suit", type: "Suit") - - 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") - } - } -} - -// Generate and print the code -print(structExample.generateCode()) \ No newline at end of file diff --git a/Examples/BlackjackCardCode.swift b/Examples/BlackjackCardCode.swift deleted file mode 100644 index fe8dc64..0000000 --- a/Examples/BlackjackCardCode.swift +++ /dev/null @@ -1,55 +0,0 @@ -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 - } -} From 74a3b40b1d1f2af6473917d961a6cd45007ed8c0 Mon Sep 17 00:00:00 2001 From: Leo Dion Date: Mon, 16 Jun 2025 13:07:45 -0400 Subject: [PATCH 11/21] fixing generics test --- Sources/SwiftBuilder/Function.swift | 50 +++++++---- Sources/SwiftBuilder/Parameter.swift | 6 +- Sources/SwiftBuilder/Struct.swift | 20 ++++- Sources/SwiftBuilder/Variable.swift | 14 ++- Sources/SwiftBuilder/VariableExp.swift | 86 ++++++++++++++++++- .../SwiftBuilderTestsC.swift | 12 +-- 6 files changed, 157 insertions(+), 31 deletions(-) diff --git a/Sources/SwiftBuilder/Function.swift b/Sources/SwiftBuilder/Function.swift index bb313c9..0fdf30c 100644 --- a/Sources/SwiftBuilder/Function.swift +++ b/Sources/SwiftBuilder/Function.swift @@ -1,4 +1,3 @@ - import SwiftSyntax public struct Function: CodeBlock { @@ -9,7 +8,14 @@ public struct Function: CodeBlock { private var isStatic: Bool = false private var isMutating: Bool = false - public init(_ name: String, @ParameterBuilderResult _ params: () -> [Parameter], returns returnType: String? = nil, @CodeBlockBuilderResult _ content: () -> [CodeBlock]) { + 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 @@ -33,24 +39,30 @@ public struct Function: CodeBlock { let identifier = TokenSyntax.identifier(name) // Build parameter list - let paramList = FunctionParameterListSyntax(parameters.enumerated().map { index, param in - var paramSyntax = FunctionParameterSyntax( - firstName: .identifier(param.name), - secondName: 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))) - ) + 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)) } - ) - if index < parameters.count - 1 { - paramSyntax = paramSyntax.with(\.trailingComma, .commaToken(trailingTrivia: .space)) - } - return paramSyntax - }) + return paramSyntax + }) + } // Build return type if specified var returnClause: ReturnClauseSyntax? diff --git a/Sources/SwiftBuilder/Parameter.swift b/Sources/SwiftBuilder/Parameter.swift index 3f74200..1c533ce 100644 --- a/Sources/SwiftBuilder/Parameter.swift +++ b/Sources/SwiftBuilder/Parameter.swift @@ -6,11 +6,15 @@ public struct Parameter: CodeBlock { let name: String let type: String let defaultValue: String? - public init(name: String, type: String, defaultValue: String? = nil) { + 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 { diff --git a/Sources/SwiftBuilder/Struct.swift b/Sources/SwiftBuilder/Struct.swift index 7600900..cdee138 100644 --- a/Sources/SwiftBuilder/Struct.swift +++ b/Sources/SwiftBuilder/Struct.swift @@ -4,10 +4,12 @@ public struct Struct: CodeBlock { private let name: String private let members: [CodeBlock] private var inheritance: String? + private var genericParameter: String? - public init(_ name: String, @CodeBlockBuilderResult _ content: () -> [CodeBlock]) { + 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 { @@ -18,7 +20,20 @@ public struct Struct: CodeBlock { public var syntax: SyntaxProtocol { let structKeyword = TokenSyntax.keyword(.struct, trailingTrivia: .space) - let identifier = TokenSyntax.identifier(name, 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 { @@ -38,6 +53,7 @@ public struct Struct: CodeBlock { return StructDeclSyntax( structKeyword: structKeyword, name: identifier, + genericParameterClause: genericParameterClause, inheritanceClause: inheritanceClause, memberBlock: memberBlock ) diff --git a/Sources/SwiftBuilder/Variable.swift b/Sources/SwiftBuilder/Variable.swift index b7e6e3e..990852e 100644 --- a/Sources/SwiftBuilder/Variable.swift +++ b/Sources/SwiftBuilder/Variable.swift @@ -10,11 +10,13 @@ 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) { + 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 { @@ -25,12 +27,20 @@ public struct Variable: CodeBlock { 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 + typeAnnotation: typeAnnotation, + initializer: initializer ) ]) ) diff --git a/Sources/SwiftBuilder/VariableExp.swift b/Sources/SwiftBuilder/VariableExp.swift index 359c2a3..c933468 100644 --- a/Sources/SwiftBuilder/VariableExp.swift +++ b/Sources/SwiftBuilder/VariableExp.swift @@ -13,7 +13,91 @@ public struct VariableExp: CodeBlock { 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 { - return TokenSyntax.identifier(self.name) + 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/Tests/SwiftBuilderTests/SwiftBuilderTestsC.swift b/Tests/SwiftBuilderTests/SwiftBuilderTestsC.swift index 15bbd6c..9471921 100644 --- a/Tests/SwiftBuilderTests/SwiftBuilderTestsC.swift +++ b/Tests/SwiftBuilderTests/SwiftBuilderTestsC.swift @@ -3,10 +3,10 @@ import XCTest final class SwiftBuilderTestsC: XCTestCase { func testBasicFunction() throws { - let function = Function("calculateSum", { + let function = Function("calculateSum", returns: "Int") { Parameter(name: "a", type: "Int") Parameter(name: "b", type: "Int") - }, returns: "Int") { + } _: { Return { VariableExp("a + b") } @@ -37,9 +37,9 @@ final class SwiftBuilderTestsC: XCTestCase { } func testStaticFunction() throws { - let function = Function("createInstance", { - Parameter(name: "value", type: "String") - }, returns: "MyType") { + let function = Function("createInstance", returns: "MyType", { + Parameter(name: "value", type: "String") + }) { Return { Init("MyType") { Parameter(name: "value", type: "String") @@ -101,4 +101,4 @@ final class SwiftBuilderTestsC: XCTestCase { XCTAssertEqual(normalizedGenerated, normalizedExpected) } -} \ No newline at end of file +} From a84b9bf42531a2e1e7b0adf90a3b0fa5c8df7599 Mon Sep 17 00:00:00 2001 From: Leo Dion Date: Mon, 16 Jun 2025 15:08:01 -0400 Subject: [PATCH 12/21] adding comments setup --- .../SwiftBuilderCommentTests.swift | 116 +++--------------- 1 file changed, 15 insertions(+), 101 deletions(-) diff --git a/Tests/SwiftBuilderTests/SwiftBuilderCommentTests.swift b/Tests/SwiftBuilderTests/SwiftBuilderCommentTests.swift index 7201eb8..a029f13 100644 --- a/Tests/SwiftBuilderTests/SwiftBuilderCommentTests.swift +++ b/Tests/SwiftBuilderTests/SwiftBuilderCommentTests.swift @@ -3,107 +3,21 @@ import XCTest 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.") - } + let syntax = Struct("Foo") { + Variable(.let, name: "bar", type: "Int") + } + .comment { + Line("MARK: - Models") + Line(.doc, "Foo struct docs") + } - 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) + let generated = syntax.syntax.description 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") + + 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") } -} +} \ No newline at end of file From 8fb0546d0947837f049709f59625f0a55a71735e Mon Sep 17 00:00:00 2001 From: leogdion Date: Mon, 16 Jun 2025 15:44:47 -0400 Subject: [PATCH 13/21] Adding comments and literals --- Sources/SwiftBuilder/Assignment.swift | 19 +-- Sources/SwiftBuilder/CodeBlock+Generate.swift | 38 +++--- Sources/SwiftBuilder/ComputedProperty.swift | 22 ++-- Sources/SwiftBuilder/Default.swift | 12 +- Sources/SwiftBuilder/Function.swift | 23 ++-- Sources/SwiftBuilder/If.swift | 24 +++- Sources/SwiftBuilder/PlusAssign.swift | 19 +-- Sources/SwiftBuilder/Return.swift | 26 +--- Sources/SwiftBuilder/Switch.swift | 2 +- Sources/SwiftBuilder/SwitchCase.swift | 12 +- Sources/SwiftBuilder/VariableDecl.swift | 23 ++-- .../SwiftBuilderCommentTests.swift | 116 +++++++++++++++--- 12 files changed, 212 insertions(+), 124 deletions(-) diff --git a/Sources/SwiftBuilder/Assignment.swift b/Sources/SwiftBuilder/Assignment.swift index c5fcf2f..c5152aa 100644 --- a/Sources/SwiftBuilder/Assignment.swift +++ b/Sources/SwiftBuilder/Assignment.swift @@ -33,19 +33,12 @@ public struct Assignment: CodeBlock { right = ExprSyntax(DeclReferenceExprSyntax(baseName: .identifier(value))) } let assign = ExprSyntax(AssignmentExprSyntax(equal: .equalToken(leadingTrivia: .space, trailingTrivia: .space))) - return CodeBlockItemSyntax( - item: .expr( - ExprSyntax( - SequenceExprSyntax( - elements: ExprListSyntax([ - left, - assign, - right - ]) - ) - ) - ), - trailingTrivia: .newline + return SequenceExprSyntax( + elements: ExprListSyntax([ + left, + assign, + right + ]) ) } } diff --git a/Sources/SwiftBuilder/CodeBlock+Generate.swift b/Sources/SwiftBuilder/CodeBlock+Generate.swift index 6792c58..46af657 100644 --- a/Sources/SwiftBuilder/CodeBlock+Generate.swift +++ b/Sources/SwiftBuilder/CodeBlock+Generate.swift @@ -3,26 +3,24 @@ import SwiftSyntax public extension CodeBlock { func generateCode() -> String { - guard let decl = syntax as? DeclSyntaxProtocol else { - fatalError("Only declaration syntax is supported at the top level.") + 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: CodeBlockItemListSyntax([ - CodeBlockItemSyntax(item: .decl(DeclSyntax(decl))) - ]) - ) - return sourceFile.description - } -} - -public extension Array where Element == CodeBlock { - func generateCode() -> String { - let decls = compactMap { $0.syntax as? DeclSyntaxProtocol } - let sourceFile = SourceFileSyntax( - statements: CodeBlockItemListSyntax(decls.map { decl in - CodeBlockItemSyntax(item: .decl(DeclSyntax(decl))) - }) - ) - return sourceFile.description + + let sourceFile = SourceFileSyntax(statements: statements) + return sourceFile.description.trimmingCharacters(in: .whitespacesAndNewlines) } } \ No newline at end of file diff --git a/Sources/SwiftBuilder/ComputedProperty.swift b/Sources/SwiftBuilder/ComputedProperty.swift index 71e45d7..f263b36 100644 --- a/Sources/SwiftBuilder/ComputedProperty.swift +++ b/Sources/SwiftBuilder/ComputedProperty.swift @@ -12,19 +12,19 @@ public struct ComputedProperty: CodeBlock { } public var syntax: SyntaxProtocol { - let statements = CodeBlockItemListSyntax(self.body.compactMap { item in - if let cb = item.syntax as? CodeBlockItemSyntax { return cb.with(\.trailingTrivia, .newline) } - if let stmt = item.syntax as? StmtSyntax { - return CodeBlockItemSyntax(item: .stmt(stmt), trailingTrivia: .newline) - } - if let expr = item.syntax as? ExprSyntax { - return CodeBlockItemSyntax(item: .expr(expr), trailingTrivia: .newline) - } - return nil - }) let accessor = AccessorBlockSyntax( leftBrace: TokenSyntax.leftBraceToken(leadingTrivia: .space, trailingTrivia: .newline), - accessors: .getter(statements), + 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) diff --git a/Sources/SwiftBuilder/Default.swift b/Sources/SwiftBuilder/Default.swift index 4760a94..713c595 100644 --- a/Sources/SwiftBuilder/Default.swift +++ b/Sources/SwiftBuilder/Default.swift @@ -12,7 +12,17 @@ public struct Default: CodeBlock { self.body = content() } public var switchCaseSyntax: SwitchCaseSyntax { - let statements = CodeBlockItemListSyntax(body.compactMap { $0.syntax.as(CodeBlockItemSyntax.self) }) + 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() diff --git a/Sources/SwiftBuilder/Function.swift b/Sources/SwiftBuilder/Function.swift index 0fdf30c..63e1ff5 100644 --- a/Sources/SwiftBuilder/Function.swift +++ b/Sources/SwiftBuilder/Function.swift @@ -74,20 +74,19 @@ public struct Function: CodeBlock { } // Build function body - let statements = CodeBlockItemListSyntax(body.compactMap { item in - if let cb = item.syntax as? CodeBlockItemSyntax { return cb.with(\.trailingTrivia, .newline) } - if let stmt = item.syntax as? StmtSyntax { - return CodeBlockItemSyntax(item: .stmt(stmt), trailingTrivia: .newline) - } - if let expr = item.syntax as? ExprSyntax { - return CodeBlockItemSyntax(item: .expr(expr), trailingTrivia: .newline) - } - return nil - }) - let bodyBlock = CodeBlockSyntax( leftBrace: .leftBraceToken(leadingTrivia: .space, trailingTrivia: .newline), - statements: statements, + 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) ) diff --git a/Sources/SwiftBuilder/If.swift b/Sources/SwiftBuilder/If.swift index 8923d15..2a85dba 100644 --- a/Sources/SwiftBuilder/If.swift +++ b/Sources/SwiftBuilder/If.swift @@ -33,13 +33,33 @@ public struct If: CodeBlock { } let bodyBlock = CodeBlockSyntax( leftBrace: .leftBraceToken(leadingTrivia: .space, trailingTrivia: .newline), - statements: CodeBlockItemListSyntax(body.compactMap { $0.syntax.as(CodeBlockItemSyntax.self) }), + 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 { $0.syntax.as(CodeBlockItemSyntax.self) }), + 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) )) } diff --git a/Sources/SwiftBuilder/PlusAssign.swift b/Sources/SwiftBuilder/PlusAssign.swift index 219f568..b65e08a 100644 --- a/Sources/SwiftBuilder/PlusAssign.swift +++ b/Sources/SwiftBuilder/PlusAssign.swift @@ -30,19 +30,12 @@ public struct PlusAssign: CodeBlock { right = ExprSyntax(DeclReferenceExprSyntax(baseName: .identifier(value))) } let assign = ExprSyntax(BinaryOperatorExprSyntax(operator: .binaryOperator("+=", leadingTrivia: .space, trailingTrivia: .space))) - return CodeBlockItemSyntax( - item: .expr( - ExprSyntax( - SequenceExprSyntax( - elements: ExprListSyntax([ - left, - assign, - right - ]) - ) - ) - ), - trailingTrivia: .newline + return SequenceExprSyntax( + elements: ExprListSyntax([ + left, + assign, + right + ]) ) } } diff --git a/Sources/SwiftBuilder/Return.swift b/Sources/SwiftBuilder/Return.swift index afb5741..fb848e8 100644 --- a/Sources/SwiftBuilder/Return.swift +++ b/Sources/SwiftBuilder/Return.swift @@ -10,28 +10,14 @@ public struct Return: CodeBlock { fatalError("Return must have at least one expression.") } if let varExp = expr as? VariableExp { - return CodeBlockItemSyntax( - item: .stmt( - StmtSyntax( - ReturnStmtSyntax( - returnKeyword: .keyword(.return, trailingTrivia: .space), - expression: ExprSyntax(DeclReferenceExprSyntax(baseName: .identifier(varExp.name))) - ) - ) - ), - trailingTrivia: .newline + return ReturnStmtSyntax( + returnKeyword: .keyword(.return, trailingTrivia: .space), + expression: ExprSyntax(DeclReferenceExprSyntax(baseName: .identifier(varExp.name))) ) } - return CodeBlockItemSyntax( - item: .stmt( - StmtSyntax( - ReturnStmtSyntax( - returnKeyword: .keyword(.return, trailingTrivia: .space), - expression: ExprSyntax(expr.syntax) - ) - ) - ), - trailingTrivia: .newline + return ReturnStmtSyntax( + returnKeyword: .keyword(.return, trailingTrivia: .space), + expression: ExprSyntax(expr.syntax) ) } } \ No newline at end of file diff --git a/Sources/SwiftBuilder/Switch.swift b/Sources/SwiftBuilder/Switch.swift index 5a3ebc7..c3f3617 100644 --- a/Sources/SwiftBuilder/Switch.swift +++ b/Sources/SwiftBuilder/Switch.swift @@ -30,6 +30,6 @@ public struct Switch: CodeBlock { cases: cases, rightBrace: .rightBraceToken(leadingTrivia: .newline) ) - return CodeBlockItemSyntax(item: .expr(ExprSyntax(switchExpr))) + return switchExpr } } diff --git a/Sources/SwiftBuilder/SwitchCase.swift b/Sources/SwiftBuilder/SwitchCase.swift index 7d566e5..0894450 100644 --- a/Sources/SwiftBuilder/SwitchCase.swift +++ b/Sources/SwiftBuilder/SwitchCase.swift @@ -25,7 +25,17 @@ public struct SwitchCase: CodeBlock { } return item }) - let statements = CodeBlockItemListSyntax(body.compactMap { $0.syntax.as(CodeBlockItemSyntax.self) }) + 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, diff --git a/Sources/SwiftBuilder/VariableDecl.swift b/Sources/SwiftBuilder/VariableDecl.swift index 8348c0b..5c9a913 100644 --- a/Sources/SwiftBuilder/VariableDecl.swift +++ b/Sources/SwiftBuilder/VariableDecl.swift @@ -33,22 +33,15 @@ public struct VariableDecl: CodeBlock { ) } } - return CodeBlockItemSyntax( - item: .decl( - DeclSyntax( - VariableDeclSyntax( - bindingSpecifier: bindingKeyword, - bindings: PatternBindingListSyntax([ - PatternBindingSyntax( - pattern: IdentifierPatternSyntax(identifier: identifier), - typeAnnotation: nil, - initializer: initializer - ) - ]) - ) + return VariableDeclSyntax( + bindingSpecifier: bindingKeyword, + bindings: PatternBindingListSyntax([ + PatternBindingSyntax( + pattern: IdentifierPatternSyntax(identifier: identifier), + typeAnnotation: nil, + initializer: initializer ) - ), - trailingTrivia: .newline + ]) ) } } \ No newline at end of file diff --git a/Tests/SwiftBuilderTests/SwiftBuilderCommentTests.swift b/Tests/SwiftBuilderTests/SwiftBuilderCommentTests.swift index a029f13..7201eb8 100644 --- a/Tests/SwiftBuilderTests/SwiftBuilderCommentTests.swift +++ b/Tests/SwiftBuilderTests/SwiftBuilderCommentTests.swift @@ -3,21 +3,107 @@ import XCTest final class SwiftBuilderCommentTests: XCTestCase { func testCommentInjection() { - let syntax = Struct("Foo") { - Variable(.let, name: "bar", type: "Int") - } - .comment { - Line("MARK: - Models") - Line(.doc, "Foo struct docs") - } + 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.") + } - let generated = syntax.syntax.description - print("Generated:\n", generated) + 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") + } - 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") + } + + 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") } -} \ No newline at end of file +} From 6dfa06a40114605a25a031cc3224bcc5fff7d49e Mon Sep 17 00:00:00 2001 From: leogdion Date: Mon, 16 Jun 2025 19:25:29 -0400 Subject: [PATCH 14/21] Adding Dev Containers (#12) --- .devcontainer/devcontainer.json | 40 +++++++++++++++++++++++ .devcontainer/swift-6.1/devcontainer.json | 40 +++++++++++++++++++++++ .devcontainer/swift-6.2/devcontainer.json | 40 +++++++++++++++++++++++ .vscode/launch.json | 22 +++++++++++++ 4 files changed, 142 insertions(+) create mode 100644 .devcontainer/devcontainer.json create mode 100644 .devcontainer/swift-6.1/devcontainer.json create mode 100644 .devcontainer/swift-6.2/devcontainer.json create mode 100644 .vscode/launch.json 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/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 0000000..71779c7 --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,22 @@ +{ + "configurations": [ + { + "type": "swift", + "request": "launch", + "args": [], + "cwd": "${workspaceFolder:SwiftBuilder}", + "name": "Debug SwiftBuilderCLI", + "program": "${workspaceFolder:SwiftBuilder}/.build/debug/SwiftBuilderCLI", + "preLaunchTask": "swift: Build Debug SwiftBuilderCLI" + }, + { + "type": "swift", + "request": "launch", + "args": [], + "cwd": "${workspaceFolder:SwiftBuilder}", + "name": "Release SwiftBuilderCLI", + "program": "${workspaceFolder:SwiftBuilder}/.build/release/SwiftBuilderCLI", + "preLaunchTask": "swift: Build Release SwiftBuilderCLI" + } + ] +} \ No newline at end of file From 736aa28684733f0fcee90c18f764564ba26a8b82 Mon Sep 17 00:00:00 2001 From: leogdion Date: Tue, 17 Jun 2025 00:44:43 +0000 Subject: [PATCH 15/21] renaming Package --- .vscode/launch.json | 16 +++++++-------- Examples/blackjack/dsl.swift | 2 +- Examples/card_game/dsl.swift | 2 +- Examples/generics/dsl.swift | 2 +- Package.swift | 20 +++++++++---------- README.md | 12 +++++------ .../Assignment.swift | 0 .../{SwiftBuilder => SyntaxKit}/Case.swift | 2 +- .../CodeBlock+Generate.swift | 0 .../CodeBlock.swift | 0 .../{SwiftBuilder => SyntaxKit}/Comment.swift | 0 .../ComputedProperty.swift | 0 .../{SwiftBuilder => SyntaxKit}/Default.swift | 2 +- .../{SwiftBuilder => SyntaxKit}/Enum.swift | 0 .../Function.swift | 0 .../{SwiftBuilder => SyntaxKit}/Group.swift | 0 Sources/{SwiftBuilder => SyntaxKit}/If.swift | 0 .../{SwiftBuilder => SyntaxKit}/Init.swift | 0 Sources/{SwiftBuilder => SyntaxKit}/Let.swift | 0 .../{SwiftBuilder => SyntaxKit}/Literal.swift | 0 .../Parameter.swift | 0 .../ParameterBuilderResult.swift | 0 .../ParameterExp.swift | 0 .../ParameterExpBuilderResult.swift | 0 .../PlusAssign.swift | 2 +- .../{SwiftBuilder => SyntaxKit}/Return.swift | 0 .../{SwiftBuilder => SyntaxKit}/Struct.swift | 0 .../{SwiftBuilder => SyntaxKit}/Switch.swift | 2 +- .../SwitchCase.swift | 2 +- .../Trivia+Comments.swift | 0 .../Variable.swift | 2 +- .../VariableDecl.swift | 0 .../VariableExp.swift | 2 +- .../VariableKind.swift | 0 .../parser/OldMain.swift | 0 .../parser/SyntaxParser.swift | 0 .../parser/SyntaxResponse.swift | 0 .../parser/TokenVisitor.swift | 0 .../parser/TreeNode.swift | 0 .../parser/Version.swift | 0 Sources/{SwiftBuilderCLI => skit}/main.swift | 4 ++-- .../SwiftBuilderCommentTests.swift | 4 ++-- .../SwiftBuilderLiteralTests.swift | 4 ++-- .../SwiftBuilderTestsA.swift | 4 ++-- .../SwiftBuilderTestsB.swift | 4 ++-- .../SwiftBuilderTestsC.swift | 4 ++-- .../SwiftBuilderTestsD.swift | 4 ++-- 47 files changed, 48 insertions(+), 48 deletions(-) rename Sources/{SwiftBuilder => SyntaxKit}/Assignment.swift (100%) rename Sources/{SwiftBuilder => SyntaxKit}/Case.swift (98%) rename Sources/{SwiftBuilder => SyntaxKit}/CodeBlock+Generate.swift (100%) rename Sources/{SwiftBuilder => SyntaxKit}/CodeBlock.swift (100%) rename Sources/{SwiftBuilder => SyntaxKit}/Comment.swift (100%) rename Sources/{SwiftBuilder => SyntaxKit}/ComputedProperty.swift (100%) rename Sources/{SwiftBuilder => SyntaxKit}/Default.swift (98%) rename Sources/{SwiftBuilder => SyntaxKit}/Enum.swift (100%) rename Sources/{SwiftBuilder => SyntaxKit}/Function.swift (100%) rename Sources/{SwiftBuilder => SyntaxKit}/Group.swift (100%) rename Sources/{SwiftBuilder => SyntaxKit}/If.swift (100%) rename Sources/{SwiftBuilder => SyntaxKit}/Init.swift (100%) rename Sources/{SwiftBuilder => SyntaxKit}/Let.swift (100%) rename Sources/{SwiftBuilder => SyntaxKit}/Literal.swift (100%) rename Sources/{SwiftBuilder => SyntaxKit}/Parameter.swift (100%) rename Sources/{SwiftBuilder => SyntaxKit}/ParameterBuilderResult.swift (100%) rename Sources/{SwiftBuilder => SyntaxKit}/ParameterExp.swift (100%) rename Sources/{SwiftBuilder => SyntaxKit}/ParameterExpBuilderResult.swift (100%) rename Sources/{SwiftBuilder => SyntaxKit}/PlusAssign.swift (98%) rename Sources/{SwiftBuilder => SyntaxKit}/Return.swift (100%) rename Sources/{SwiftBuilder => SyntaxKit}/Struct.swift (100%) rename Sources/{SwiftBuilder => SyntaxKit}/Switch.swift (98%) rename Sources/{SwiftBuilder => SyntaxKit}/SwitchCase.swift (99%) rename Sources/{SwiftBuilder => SyntaxKit}/Trivia+Comments.swift (100%) rename Sources/{SwiftBuilder => SyntaxKit}/Variable.swift (98%) rename Sources/{SwiftBuilder => SyntaxKit}/VariableDecl.swift (100%) rename Sources/{SwiftBuilder => SyntaxKit}/VariableExp.swift (99%) rename Sources/{SwiftBuilder => SyntaxKit}/VariableKind.swift (100%) rename Sources/{SwiftBuilder => SyntaxKit}/parser/OldMain.swift (100%) rename Sources/{SwiftBuilder => SyntaxKit}/parser/SyntaxParser.swift (100%) rename Sources/{SwiftBuilder => SyntaxKit}/parser/SyntaxResponse.swift (100%) rename Sources/{SwiftBuilder => SyntaxKit}/parser/TokenVisitor.swift (100%) rename Sources/{SwiftBuilder => SyntaxKit}/parser/TreeNode.swift (100%) rename Sources/{SwiftBuilder => SyntaxKit}/parser/Version.swift (100%) rename Sources/{SwiftBuilderCLI => skit}/main.swift (90%) diff --git a/.vscode/launch.json b/.vscode/launch.json index 71779c7..e74b79e 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -4,19 +4,19 @@ "type": "swift", "request": "launch", "args": [], - "cwd": "${workspaceFolder:SwiftBuilder}", - "name": "Debug SwiftBuilderCLI", - "program": "${workspaceFolder:SwiftBuilder}/.build/debug/SwiftBuilderCLI", - "preLaunchTask": "swift: Build Debug SwiftBuilderCLI" + "cwd": "${workspaceFolder:SyntaxKit}", + "name": "Debug skit", + "program": "${workspaceFolder:SyntaxKit}/.build/debug/skit", + "preLaunchTask": "swift: Build Debug skit" }, { "type": "swift", "request": "launch", "args": [], - "cwd": "${workspaceFolder:SwiftBuilder}", - "name": "Release SwiftBuilderCLI", - "program": "${workspaceFolder:SwiftBuilder}/.build/release/SwiftBuilderCLI", - "preLaunchTask": "swift: Build Release SwiftBuilderCLI" + "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/dsl.swift b/Examples/blackjack/dsl.swift index ba8eaa7..6514652 100644 --- a/Examples/blackjack/dsl.swift +++ b/Examples/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") { diff --git a/Examples/card_game/dsl.swift b/Examples/card_game/dsl.swift index a4cb6ad..a05ced1 100644 --- a/Examples/card_game/dsl.swift +++ b/Examples/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 { diff --git a/Examples/generics/dsl.swift b/Examples/generics/dsl.swift index 16f5e16..d6585cf 100644 --- a/Examples/generics/dsl.swift +++ b/Examples/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/Package.swift b/Package.swift index 966d30d..2330040 100644 --- a/Package.swift +++ b/Package.swift @@ -4,19 +4,19 @@ import PackageDescription let package = Package( - name: "SwiftBuilder", + name: "SyntaxKit", platforms: [ .macOS(.v13) ], products: [ // Products define the executables and libraries a package produces, making them visible to other packages. .library( - name: "SwiftBuilder", - targets: ["SwiftBuilder"] + name: "SyntaxKit", + targets: ["SyntaxKit"] ), .executable( - name: "SwiftBuilderCLI", - targets: ["SwiftBuilderCLI"] + name: "skit", + targets: ["skit"] ), ], dependencies: [ @@ -26,7 +26,7 @@ let package = Package( // 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", + name: "SyntaxKit", dependencies: [ .product(name: "SwiftSyntax", package: "swift-syntax"), .product(name: "SwiftOperators", package: "swift-syntax"), @@ -34,12 +34,12 @@ let package = Package( ] ), .executableTarget( - name: "SwiftBuilderCLI", - dependencies: ["SwiftBuilder"] + name: "skit", + dependencies: ["SyntaxKit"] ), .testTarget( - name: "SwiftBuilderTests", - dependencies: ["SwiftBuilder"] + name: "SyntaxKitTests", + dependencies: ["SyntaxKit"] ), ] ) diff --git a/README.md b/README.md index e1a66a1..3dc21c9 100644 --- a/README.md +++ b/README.md @@ -1,23 +1,23 @@ -# 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. It 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: "1.0.0") ] ``` ## 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") { diff --git a/Sources/SwiftBuilder/Assignment.swift b/Sources/SyntaxKit/Assignment.swift similarity index 100% rename from Sources/SwiftBuilder/Assignment.swift rename to Sources/SyntaxKit/Assignment.swift diff --git a/Sources/SwiftBuilder/Case.swift b/Sources/SyntaxKit/Case.swift similarity index 98% rename from Sources/SwiftBuilder/Case.swift rename to Sources/SyntaxKit/Case.swift index e674a2b..a3f0904 100644 --- a/Sources/SwiftBuilder/Case.swift +++ b/Sources/SyntaxKit/Case.swift @@ -1,6 +1,6 @@ // // Case.swift -// SwiftBuilder +// SyntaxKit // // Created by Leo Dion on 6/15/25. // diff --git a/Sources/SwiftBuilder/CodeBlock+Generate.swift b/Sources/SyntaxKit/CodeBlock+Generate.swift similarity index 100% rename from Sources/SwiftBuilder/CodeBlock+Generate.swift rename to Sources/SyntaxKit/CodeBlock+Generate.swift diff --git a/Sources/SwiftBuilder/CodeBlock.swift b/Sources/SyntaxKit/CodeBlock.swift similarity index 100% rename from Sources/SwiftBuilder/CodeBlock.swift rename to Sources/SyntaxKit/CodeBlock.swift diff --git a/Sources/SwiftBuilder/Comment.swift b/Sources/SyntaxKit/Comment.swift similarity index 100% rename from Sources/SwiftBuilder/Comment.swift rename to Sources/SyntaxKit/Comment.swift diff --git a/Sources/SwiftBuilder/ComputedProperty.swift b/Sources/SyntaxKit/ComputedProperty.swift similarity index 100% rename from Sources/SwiftBuilder/ComputedProperty.swift rename to Sources/SyntaxKit/ComputedProperty.swift diff --git a/Sources/SwiftBuilder/Default.swift b/Sources/SyntaxKit/Default.swift similarity index 98% rename from Sources/SwiftBuilder/Default.swift rename to Sources/SyntaxKit/Default.swift index 713c595..b994259 100644 --- a/Sources/SwiftBuilder/Default.swift +++ b/Sources/SyntaxKit/Default.swift @@ -1,6 +1,6 @@ // // Default.swift -// SwiftBuilder +// SyntaxKit // // Created by Leo Dion on 6/15/25. // diff --git a/Sources/SwiftBuilder/Enum.swift b/Sources/SyntaxKit/Enum.swift similarity index 100% rename from Sources/SwiftBuilder/Enum.swift rename to Sources/SyntaxKit/Enum.swift diff --git a/Sources/SwiftBuilder/Function.swift b/Sources/SyntaxKit/Function.swift similarity index 100% rename from Sources/SwiftBuilder/Function.swift rename to Sources/SyntaxKit/Function.swift diff --git a/Sources/SwiftBuilder/Group.swift b/Sources/SyntaxKit/Group.swift similarity index 100% rename from Sources/SwiftBuilder/Group.swift rename to Sources/SyntaxKit/Group.swift diff --git a/Sources/SwiftBuilder/If.swift b/Sources/SyntaxKit/If.swift similarity index 100% rename from Sources/SwiftBuilder/If.swift rename to Sources/SyntaxKit/If.swift diff --git a/Sources/SwiftBuilder/Init.swift b/Sources/SyntaxKit/Init.swift similarity index 100% rename from Sources/SwiftBuilder/Init.swift rename to Sources/SyntaxKit/Init.swift diff --git a/Sources/SwiftBuilder/Let.swift b/Sources/SyntaxKit/Let.swift similarity index 100% rename from Sources/SwiftBuilder/Let.swift rename to Sources/SyntaxKit/Let.swift diff --git a/Sources/SwiftBuilder/Literal.swift b/Sources/SyntaxKit/Literal.swift similarity index 100% rename from Sources/SwiftBuilder/Literal.swift rename to Sources/SyntaxKit/Literal.swift diff --git a/Sources/SwiftBuilder/Parameter.swift b/Sources/SyntaxKit/Parameter.swift similarity index 100% rename from Sources/SwiftBuilder/Parameter.swift rename to Sources/SyntaxKit/Parameter.swift diff --git a/Sources/SwiftBuilder/ParameterBuilderResult.swift b/Sources/SyntaxKit/ParameterBuilderResult.swift similarity index 100% rename from Sources/SwiftBuilder/ParameterBuilderResult.swift rename to Sources/SyntaxKit/ParameterBuilderResult.swift diff --git a/Sources/SwiftBuilder/ParameterExp.swift b/Sources/SyntaxKit/ParameterExp.swift similarity index 100% rename from Sources/SwiftBuilder/ParameterExp.swift rename to Sources/SyntaxKit/ParameterExp.swift diff --git a/Sources/SwiftBuilder/ParameterExpBuilderResult.swift b/Sources/SyntaxKit/ParameterExpBuilderResult.swift similarity index 100% rename from Sources/SwiftBuilder/ParameterExpBuilderResult.swift rename to Sources/SyntaxKit/ParameterExpBuilderResult.swift diff --git a/Sources/SwiftBuilder/PlusAssign.swift b/Sources/SyntaxKit/PlusAssign.swift similarity index 98% rename from Sources/SwiftBuilder/PlusAssign.swift rename to Sources/SyntaxKit/PlusAssign.swift index b65e08a..ea331ab 100644 --- a/Sources/SwiftBuilder/PlusAssign.swift +++ b/Sources/SyntaxKit/PlusAssign.swift @@ -1,6 +1,6 @@ // // PlusAssign.swift -// SwiftBuilder +// SyntaxKit // // Created by Leo Dion on 6/15/25. // diff --git a/Sources/SwiftBuilder/Return.swift b/Sources/SyntaxKit/Return.swift similarity index 100% rename from Sources/SwiftBuilder/Return.swift rename to Sources/SyntaxKit/Return.swift diff --git a/Sources/SwiftBuilder/Struct.swift b/Sources/SyntaxKit/Struct.swift similarity index 100% rename from Sources/SwiftBuilder/Struct.swift rename to Sources/SyntaxKit/Struct.swift diff --git a/Sources/SwiftBuilder/Switch.swift b/Sources/SyntaxKit/Switch.swift similarity index 98% rename from Sources/SwiftBuilder/Switch.swift rename to Sources/SyntaxKit/Switch.swift index c3f3617..84f90ba 100644 --- a/Sources/SwiftBuilder/Switch.swift +++ b/Sources/SyntaxKit/Switch.swift @@ -1,6 +1,6 @@ // // Switch.swift -// SwiftBuilder +// SyntaxKit // // Created by Leo Dion on 6/15/25. // diff --git a/Sources/SwiftBuilder/SwitchCase.swift b/Sources/SyntaxKit/SwitchCase.swift similarity index 99% rename from Sources/SwiftBuilder/SwitchCase.swift rename to Sources/SyntaxKit/SwitchCase.swift index 0894450..f2b231f 100644 --- a/Sources/SwiftBuilder/SwitchCase.swift +++ b/Sources/SyntaxKit/SwitchCase.swift @@ -1,6 +1,6 @@ // // SwitchCase.swift -// SwiftBuilder +// SyntaxKit // // Created by Leo Dion on 6/15/25. // diff --git a/Sources/SwiftBuilder/Trivia+Comments.swift b/Sources/SyntaxKit/Trivia+Comments.swift similarity index 100% rename from Sources/SwiftBuilder/Trivia+Comments.swift rename to Sources/SyntaxKit/Trivia+Comments.swift diff --git a/Sources/SwiftBuilder/Variable.swift b/Sources/SyntaxKit/Variable.swift similarity index 98% rename from Sources/SwiftBuilder/Variable.swift rename to Sources/SyntaxKit/Variable.swift index 990852e..6e82d32 100644 --- a/Sources/SwiftBuilder/Variable.swift +++ b/Sources/SyntaxKit/Variable.swift @@ -1,6 +1,6 @@ // // Variable.swift -// SwiftBuilder +// SyntaxKit // // Created by Leo Dion on 6/15/25. // diff --git a/Sources/SwiftBuilder/VariableDecl.swift b/Sources/SyntaxKit/VariableDecl.swift similarity index 100% rename from Sources/SwiftBuilder/VariableDecl.swift rename to Sources/SyntaxKit/VariableDecl.swift diff --git a/Sources/SwiftBuilder/VariableExp.swift b/Sources/SyntaxKit/VariableExp.swift similarity index 99% rename from Sources/SwiftBuilder/VariableExp.swift rename to Sources/SyntaxKit/VariableExp.swift index c933468..afe3780 100644 --- a/Sources/SwiftBuilder/VariableExp.swift +++ b/Sources/SyntaxKit/VariableExp.swift @@ -1,6 +1,6 @@ // // VariableExp.swift -// SwiftBuilder +// SyntaxKit // // Created by Leo Dion on 6/15/25. // diff --git a/Sources/SwiftBuilder/VariableKind.swift b/Sources/SyntaxKit/VariableKind.swift similarity index 100% rename from Sources/SwiftBuilder/VariableKind.swift rename to Sources/SyntaxKit/VariableKind.swift diff --git a/Sources/SwiftBuilder/parser/OldMain.swift b/Sources/SyntaxKit/parser/OldMain.swift similarity index 100% rename from Sources/SwiftBuilder/parser/OldMain.swift rename to Sources/SyntaxKit/parser/OldMain.swift diff --git a/Sources/SwiftBuilder/parser/SyntaxParser.swift b/Sources/SyntaxKit/parser/SyntaxParser.swift similarity index 100% rename from Sources/SwiftBuilder/parser/SyntaxParser.swift rename to Sources/SyntaxKit/parser/SyntaxParser.swift diff --git a/Sources/SwiftBuilder/parser/SyntaxResponse.swift b/Sources/SyntaxKit/parser/SyntaxResponse.swift similarity index 100% rename from Sources/SwiftBuilder/parser/SyntaxResponse.swift rename to Sources/SyntaxKit/parser/SyntaxResponse.swift diff --git a/Sources/SwiftBuilder/parser/TokenVisitor.swift b/Sources/SyntaxKit/parser/TokenVisitor.swift similarity index 100% rename from Sources/SwiftBuilder/parser/TokenVisitor.swift rename to Sources/SyntaxKit/parser/TokenVisitor.swift diff --git a/Sources/SwiftBuilder/parser/TreeNode.swift b/Sources/SyntaxKit/parser/TreeNode.swift similarity index 100% rename from Sources/SwiftBuilder/parser/TreeNode.swift rename to Sources/SyntaxKit/parser/TreeNode.swift diff --git a/Sources/SwiftBuilder/parser/Version.swift b/Sources/SyntaxKit/parser/Version.swift similarity index 100% rename from Sources/SwiftBuilder/parser/Version.swift rename to Sources/SyntaxKit/parser/Version.swift diff --git a/Sources/SwiftBuilderCLI/main.swift b/Sources/skit/main.swift similarity index 90% rename from Sources/SwiftBuilderCLI/main.swift rename to Sources/skit/main.swift index bb6d9dd..ec91882 100644 --- a/Sources/SwiftBuilderCLI/main.swift +++ b/Sources/skit/main.swift @@ -1,11 +1,11 @@ import Foundation -import SwiftBuilder +import SyntaxKit // Read Swift code from stdin let code = String(data: FileHandle.standardInput.readDataToEndOfFile(), encoding: .utf8) ?? "" do { - // Parse the code using SwiftBuilder + // Parse the code using SyntaxKit let response = try SyntaxParser.parse(code: code, options: ["fold"]) // Output the JSON to stdout diff --git a/Tests/SwiftBuilderTests/SwiftBuilderCommentTests.swift b/Tests/SwiftBuilderTests/SwiftBuilderCommentTests.swift index 7201eb8..32f13db 100644 --- a/Tests/SwiftBuilderTests/SwiftBuilderCommentTests.swift +++ b/Tests/SwiftBuilderTests/SwiftBuilderCommentTests.swift @@ -1,7 +1,7 @@ import XCTest -@testable import SwiftBuilder +@testable import SyntaxKit -final class SwiftBuilderCommentTests: XCTestCase { +final class SyntaxKitCommentTests: XCTestCase { func testCommentInjection() { let syntax = Group { Struct("Card") { diff --git a/Tests/SwiftBuilderTests/SwiftBuilderLiteralTests.swift b/Tests/SwiftBuilderTests/SwiftBuilderLiteralTests.swift index 32354e8..98f7056 100644 --- a/Tests/SwiftBuilderTests/SwiftBuilderLiteralTests.swift +++ b/Tests/SwiftBuilderTests/SwiftBuilderLiteralTests.swift @@ -1,7 +1,7 @@ import XCTest -@testable import SwiftBuilder +@testable import SyntaxKit -final class SwiftBuilderLiteralTests: XCTestCase { +final class SyntaxKitLiteralTests: XCTestCase { func testGroupWithLiterals() { let group = Group { Return { diff --git a/Tests/SwiftBuilderTests/SwiftBuilderTestsA.swift b/Tests/SwiftBuilderTests/SwiftBuilderTestsA.swift index 6063008..5e7089f 100644 --- a/Tests/SwiftBuilderTests/SwiftBuilderTestsA.swift +++ b/Tests/SwiftBuilderTests/SwiftBuilderTestsA.swift @@ -1,7 +1,7 @@ import XCTest -@testable import SwiftBuilder +@testable import SyntaxKit -final class SwiftBuilderTestsA: XCTestCase { +final class SyntaxKitTestsA: XCTestCase { func testBlackjackCardExample() throws { let blackjackCard = Struct("BlackjackCard") { Enum("Suit") { diff --git a/Tests/SwiftBuilderTests/SwiftBuilderTestsB.swift b/Tests/SwiftBuilderTests/SwiftBuilderTestsB.swift index eb1b702..0afadc3 100644 --- a/Tests/SwiftBuilderTests/SwiftBuilderTestsB.swift +++ b/Tests/SwiftBuilderTests/SwiftBuilderTestsB.swift @@ -1,7 +1,7 @@ import XCTest -@testable import SwiftBuilder +@testable import SyntaxKit -final class SwiftBuilderTestsB: XCTestCase { +final class SyntaxKitTestsB: XCTestCase { func testBlackjackCardExample() throws { let syntax = Struct("BlackjackCard") { Enum("Suit") { diff --git a/Tests/SwiftBuilderTests/SwiftBuilderTestsC.swift b/Tests/SwiftBuilderTests/SwiftBuilderTestsC.swift index 9471921..51484c9 100644 --- a/Tests/SwiftBuilderTests/SwiftBuilderTestsC.swift +++ b/Tests/SwiftBuilderTests/SwiftBuilderTestsC.swift @@ -1,7 +1,7 @@ import XCTest -@testable import SwiftBuilder +@testable import SyntaxKit -final class SwiftBuilderTestsC: XCTestCase { +final class SyntaxKitTestsC: XCTestCase { func testBasicFunction() throws { let function = Function("calculateSum", returns: "Int") { Parameter(name: "a", type: "Int") diff --git a/Tests/SwiftBuilderTests/SwiftBuilderTestsD.swift b/Tests/SwiftBuilderTests/SwiftBuilderTestsD.swift index 2c2a9d8..9d5b9b2 100644 --- a/Tests/SwiftBuilderTests/SwiftBuilderTestsD.swift +++ b/Tests/SwiftBuilderTests/SwiftBuilderTestsD.swift @@ -1,7 +1,7 @@ import XCTest -@testable import SwiftBuilder +@testable import SyntaxKit -final class SwiftBuilderTestsD: XCTestCase { +final class SyntaxKitTestsD: XCTestCase { func normalize(_ code: String) -> String { return code .replacingOccurrences(of: "//.*$", with: "", options: .regularExpression) // Remove comments From 12ac3d5aae97aa58dbdecab3bf8f21de5f106226 Mon Sep 17 00:00:00 2001 From: leogdion Date: Mon, 16 Jun 2025 21:40:44 -0400 Subject: [PATCH 16/21] Adding Continuous Integration (#13) --- .github/workflows/SyntaxKit.yml | 131 ++++++++++++++++++++++++++++++++ Package.swift | 8 +- 2 files changed, 137 insertions(+), 2 deletions(-) create mode 100644 .github/workflows/SyntaxKit.yml diff --git a/.github/workflows/SyntaxKit.yml b/.github/workflows/SyntaxKit.yml new file mode 100644 index 0000000..695b48b --- /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/Package.swift b/Package.swift index 2330040..1eac097 100644 --- a/Package.swift +++ b/Package.swift @@ -1,4 +1,4 @@ -// 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 @@ -6,7 +6,11 @@ import PackageDescription let package = Package( name: "SyntaxKit", platforms: [ - .macOS(.v13) + .macOS(.v13), + .iOS(.v13), + .watchOS(.v6), + .tvOS(.v13), + .visionOS(.v1) ], products: [ // Products define the executables and libraries a package produces, making them visible to other packages. From 044b06a610bf8e48abf3235918a90d06755cf20b Mon Sep 17 00:00:00 2001 From: leogdion Date: Tue, 17 Jun 2025 07:39:31 -0400 Subject: [PATCH 17/21] reorganized repo (#15) --- .github/workflows/SyntaxKit.yml | 60 +-- .gitignore | 141 +++++- .spi.yml | 5 + .swift-format | 70 +++ .swift-version | 1 + .swiftlint.yml | 131 ++++++ Examples/{ => Completed}/blackjack/code.swift | 0 Examples/{ => Completed}/blackjack/dsl.swift | 14 +- Examples/{ => Completed}/card_game/code.swift | 4 +- Examples/{ => Completed}/card_game/dsl.swift | 6 +- .../{ => Completed}/card_game/syntax.json | 0 Examples/{ => Remaining}/comments/code.swift | 22 +- .../{ => Remaining}/concurrency/code.swift | 19 +- Examples/{ => Remaining}/generics/code.swift | 19 +- Examples/{ => Remaining}/generics/dsl.swift | 0 Examples/{ => Remaining}/generics/syntax.json | 0 Examples/{ => Remaining}/protocols/code.swift | 8 +- Examples/{ => Remaining}/swiftui/code.swift | 18 +- Line.swift | 57 +++ Mintfile | 3 + Scripts/gh-md-toc | 421 ++++++++++++++++++ Scripts/header.sh | 98 ++++ Scripts/lint.sh | 80 ++++ Scripts/swift-doc.sh | 193 ++++++++ Sources/SyntaxKit/Assignment.swift | 101 +++-- Sources/SyntaxKit/Case.swift | 100 +++-- Sources/SyntaxKit/CodeBlock+Generate.swift | 76 +++- Sources/SyntaxKit/CodeBlock.swift | 81 ++-- Sources/SyntaxKit/Comment.swift | 117 ----- Sources/SyntaxKit/CommentBuilderResult.swift | 51 +++ Sources/SyntaxKit/CommentedCodeBlock.swift | 70 +++ Sources/SyntaxKit/ComputedProperty.swift | 115 +++-- Sources/SyntaxKit/Default.swift | 78 ++-- Sources/SyntaxKit/Enum.swift | 236 +++++----- Sources/SyntaxKit/Function.swift | 274 +++++++----- Sources/SyntaxKit/Group.swift | 75 +++- Sources/SyntaxKit/If.swift | 178 +++++--- Sources/SyntaxKit/Init.swift | 77 +++- Sources/SyntaxKit/Let.swift | 79 ++-- Sources/SyntaxKit/Line.swift | 81 ++++ Sources/SyntaxKit/Literal.swift | 79 ++-- Sources/SyntaxKit/Parameter.swift | 87 ++-- .../SyntaxKit/ParameterBuilderResult.swift | 69 ++- Sources/SyntaxKit/ParameterExp.swift | 67 ++- .../SyntaxKit/ParameterExpBuilderResult.swift | 69 ++- Sources/SyntaxKit/PlusAssign.swift | 89 ++-- Sources/SyntaxKit/Return.swift | 65 ++- Sources/SyntaxKit/Struct.swift | 146 +++--- Sources/SyntaxKit/Switch.swift | 73 +-- Sources/SyntaxKit/SwitchCase.swift | 107 +++-- Sources/SyntaxKit/Trivia+Comments.swift | 67 ++- Sources/SyntaxKit/Variable.swift | 98 ++-- Sources/SyntaxKit/VariableDecl.swift | 114 +++-- Sources/SyntaxKit/VariableExp.swift | 205 +++++---- Sources/SyntaxKit/VariableKind.swift | 35 +- Sources/SyntaxKit/parser/OldMain.swift | 25 -- Sources/SyntaxKit/parser/String.swift | 64 +++ Sources/SyntaxKit/parser/SyntaxParser.swift | 32 +- Sources/SyntaxKit/parser/SyntaxResponse.swift | 31 +- Sources/SyntaxKit/parser/TokenVisitor.swift | 143 +++--- Sources/SyntaxKit/parser/TreeNode.swift | 63 +-- Sources/SyntaxKit/parser/Version.swift | 30 ++ Sources/skit/main.swift | 56 ++- .../SwiftBuilderCommentTests.swift | 109 ----- .../SwiftBuilderLiteralTests.swift | 14 - .../SwiftBuilderTestsA.swift | 43 -- .../SwiftBuilderTestsB.swift | 228 ---------- .../SwiftBuilderTestsC.swift | 104 ----- .../SwiftBuilderTestsD.swift | 107 ----- Tests/SyntaxKitTests/BasicTests.swift | 45 ++ Tests/SyntaxKitTests/BlackjackTests.swift | 249 +++++++++++ Tests/SyntaxKitTests/CommentTests.swift | 113 +++++ Tests/SyntaxKitTests/FunctionTests.swift | 114 +++++ Tests/SyntaxKitTests/LiteralTests.swift | 15 + Tests/SyntaxKitTests/StructTests.swift | 108 +++++ codecov.yml | 2 + project.yml | 13 + 77 files changed, 4243 insertions(+), 1994 deletions(-) create mode 100644 .spi.yml create mode 100644 .swift-format create mode 100644 .swift-version create mode 100644 .swiftlint.yml rename Examples/{ => Completed}/blackjack/code.swift (100%) rename Examples/{ => Completed}/blackjack/dsl.swift (96%) rename Examples/{ => Completed}/card_game/code.swift (99%) rename Examples/{ => Completed}/card_game/dsl.swift (97%) rename Examples/{ => Completed}/card_game/syntax.json (100%) rename Examples/{ => Remaining}/comments/code.swift (96%) rename Examples/{ => Remaining}/concurrency/code.swift (94%) rename Examples/{ => Remaining}/generics/code.swift (66%) rename Examples/{ => Remaining}/generics/dsl.swift (100%) rename Examples/{ => Remaining}/generics/syntax.json (100%) rename Examples/{ => Remaining}/protocols/code.swift (96%) rename Examples/{ => Remaining}/swiftui/code.swift (97%) create mode 100644 Line.swift create mode 100644 Mintfile create mode 100755 Scripts/gh-md-toc create mode 100755 Scripts/header.sh create mode 100755 Scripts/lint.sh create mode 100755 Scripts/swift-doc.sh delete mode 100644 Sources/SyntaxKit/Comment.swift create mode 100644 Sources/SyntaxKit/CommentBuilderResult.swift create mode 100644 Sources/SyntaxKit/CommentedCodeBlock.swift create mode 100644 Sources/SyntaxKit/Line.swift delete mode 100644 Sources/SyntaxKit/parser/OldMain.swift create mode 100644 Sources/SyntaxKit/parser/String.swift delete mode 100644 Tests/SwiftBuilderTests/SwiftBuilderCommentTests.swift delete mode 100644 Tests/SwiftBuilderTests/SwiftBuilderLiteralTests.swift delete mode 100644 Tests/SwiftBuilderTests/SwiftBuilderTestsA.swift delete mode 100644 Tests/SwiftBuilderTests/SwiftBuilderTestsB.swift delete mode 100644 Tests/SwiftBuilderTests/SwiftBuilderTestsC.swift delete mode 100644 Tests/SwiftBuilderTests/SwiftBuilderTestsD.swift create mode 100644 Tests/SyntaxKitTests/BasicTests.swift create mode 100644 Tests/SyntaxKitTests/BlackjackTests.swift create mode 100644 Tests/SyntaxKitTests/CommentTests.swift create mode 100644 Tests/SyntaxKitTests/FunctionTests.swift create mode 100644 Tests/SyntaxKitTests/LiteralTests.swift create mode 100644 Tests/SyntaxKitTests/StructTests.swift create mode 100644 codecov.yml create mode 100644 project.yml diff --git a/.github/workflows/SyntaxKit.yml b/.github/workflows/SyntaxKit.yml index 695b48b..dd72dfa 100644 --- a/.github/workflows/SyntaxKit.yml +++ b/.github/workflows/SyntaxKit.yml @@ -99,33 +99,33 @@ jobs: 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 + 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..2640454 100644 --- a/.gitignore +++ b/.gitignore @@ -1,8 +1,139 @@ +# 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 \ No newline at end of file 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/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 96% rename from Examples/blackjack/dsl.swift rename to Examples/Completed/blackjack/dsl.swift index 6514652..d0af68d 100644 --- a/Examples/blackjack/dsl.swift +++ b/Examples/Completed/blackjack/dsl.swift @@ -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 97% rename from Examples/card_game/dsl.swift rename to Examples/Completed/card_game/dsl.swift index a05ced1..f6f9bdd 100644 --- a/Examples/card_game/dsl.swift +++ b/Examples/Completed/card_game/dsl.swift @@ -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 100% rename from Examples/generics/dsl.swift rename to Examples/Remaining/generics/dsl.swift 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/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..533a00a --- /dev/null +++ b/Scripts/lint.sh @@ -0,0 +1,80 @@ +#!/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 + +#$MINT_RUN periphery scan $PERIPHERY_OPTIONS --disable-update-check + +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/SyntaxKit/Assignment.swift b/Sources/SyntaxKit/Assignment.swift index c5152aa..7fb5894 100644 --- a/Sources/SyntaxKit/Assignment.swift +++ b/Sources/SyntaxKit/Assignment.swift @@ -1,46 +1,65 @@ -import SwiftSyntax - - - - - - - - - - +// +// 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 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 - ]) - ) + 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/SyntaxKit/Case.swift b/Sources/SyntaxKit/Case.swift index a3f0904..2358b41 100644 --- a/Sources/SyntaxKit/Case.swift +++ b/Sources/SyntaxKit/Case.swift @@ -2,47 +2,73 @@ // Case.swift // SyntaxKit // -// Created by Leo Dion on 6/15/25. +// 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 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() + 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))) ) - return SwitchCaseSyntax( - label: .case(label), - statements: statements + } + ) + let caseItems = SwitchCaseItemListSyntax([ + SwitchCaseItemSyntax( + pattern: TuplePatternSyntax( + leftParen: .leftParenToken(), + elements: patternList, + rightParen: .rightParenToken() ) - } - - public var syntax: SyntaxProtocol { switchCaseSyntax } + ) + ]) + 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 index 46af657..7fcb330 100644 --- a/Sources/SyntaxKit/CodeBlock+Generate.swift +++ b/Sources/SyntaxKit/CodeBlock+Generate.swift @@ -1,26 +1,58 @@ +// +// 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 -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) +extension CodeBlock { + 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) + ]) } -} \ No newline at end of file + + let sourceFile = SourceFileSyntax(statements: statements) + return sourceFile.description.trimmingCharacters(in: .whitespacesAndNewlines) + } +} diff --git a/Sources/SyntaxKit/CodeBlock.swift b/Sources/SyntaxKit/CodeBlock.swift index 610636e..51e3455 100644 --- a/Sources/SyntaxKit/CodeBlock.swift +++ b/Sources/SyntaxKit/CodeBlock.swift @@ -1,40 +1,69 @@ +// +// 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 public protocol CodeBlock { - var syntax: SyntaxProtocol { get } + var syntax: SyntaxProtocol { get } } public protocol CodeBlockBuilder { - associatedtype Result: CodeBlock - func build() -> Result + 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 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("")) - } -} + public var syntax: SyntaxProtocol { + StringSegmentSyntax(content: .unknown("")) + } +} diff --git a/Sources/SyntaxKit/Comment.swift b/Sources/SyntaxKit/Comment.swift deleted file mode 100644 index b62ad44..0000000 --- a/Sources/SyntaxKit/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/SyntaxKit/CommentBuilderResult.swift b/Sources/SyntaxKit/CommentBuilderResult.swift new file mode 100644 index 0000000..e0550a5 --- /dev/null +++ b/Sources/SyntaxKit/CommentBuilderResult.swift @@ -0,0 +1,51 @@ +// +// 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. +// + +@resultBuilder +public enum CommentBuilderResult { + public static func buildBlock(_ components: Line...) -> [Line] { components } +} + +// MARK: - Public DSL surface + +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`. + 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 index f263b36..4da7312 100644 --- a/Sources/SyntaxKit/ComputedProperty.swift +++ b/Sources/SyntaxKit/ComputedProperty.swift @@ -1,46 +1,77 @@ +// +// 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 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 - ) - ]) + 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/SyntaxKit/Default.swift b/Sources/SyntaxKit/Default.swift index b994259..bfd10c4 100644 --- a/Sources/SyntaxKit/Default.swift +++ b/Sources/SyntaxKit/Default.swift @@ -2,35 +2,59 @@ // Default.swift // SyntaxKit // -// Created by Leo Dion on 6/15/25. +// 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 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 } + 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/SyntaxKit/Enum.swift b/Sources/SyntaxKit/Enum.swift index 8970aaa..36f5917 100644 --- a/Sources/SyntaxKit/Enum.swift +++ b/Sources/SyntaxKit/Enum.swift @@ -1,111 +1,143 @@ +// +// 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 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 - ) + 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 - ) - ]) + 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/SyntaxKit/Function.swift b/Sources/SyntaxKit/Function.swift index 63e1ff5..da829f2 100644 --- a/Sources/SyntaxKit/Function.swift +++ b/Sources/SyntaxKit/Function.swift @@ -1,125 +1,165 @@ +// +// 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 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() + 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 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 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 + }) } - - public func `static`() -> Self { - var copy = self - copy.isStatic = true - return copy + + // 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)) + ) } - - public func mutating() -> Self { - var copy = self - copy.isMutating = true - return copy + + // 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)) + ]) } - - 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 - ) + 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 index ec390b5..6d56817 100644 --- a/Sources/SyntaxKit/Group.swift +++ b/Sources/SyntaxKit/Group.swift @@ -1,30 +1,59 @@ +// +// 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 public struct Group: CodeBlock { - let members: [CodeBlock] + let members: [CodeBlock] - public init(@CodeBlockBuilderResult _ content: () -> [CodeBlock]) { - self.members = content() - } + 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) - } + 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) + 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)] } -} \ No newline at end of file + return CodeBlockItemListSyntax(statements) + } +} diff --git a/Sources/SyntaxKit/If.swift b/Sources/SyntaxKit/If.swift index 2a85dba..5935920 100644 --- a/Sources/SyntaxKit/If.swift +++ b/Sources/SyntaxKit/If.swift @@ -1,76 +1,114 @@ +// +// 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 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 + 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) + )) } -} \ No newline at end of file + 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 index 75c10f6..3206de0 100644 --- a/Sources/SyntaxKit/Init.swift +++ b/Sources/SyntaxKit/Init.swift @@ -1,25 +1,58 @@ +// +// 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 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 + 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().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 index 6a5c47d..5e4f53d 100644 --- a/Sources/SyntaxKit/Let.swift +++ b/Sources/SyntaxKit/Let.swift @@ -1,30 +1,59 @@ +// +// 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 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))) - ) - ) - ]) - ) + let name: String + let value: String + 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))) ) - ) + ) + ]) + ) ) - } -} \ No newline at end of file + ) + ) + } +} diff --git a/Sources/SyntaxKit/Line.swift b/Sources/SyntaxKit/Line.swift new file mode 100644 index 0000000..8389e13 --- /dev/null +++ b/Sources/SyntaxKit/Line.swift @@ -0,0 +1,81 @@ +// +// 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 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`. + 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 index 6ec0e71..9d991b3 100644 --- a/Sources/SyntaxKit/Literal.swift +++ b/Sources/SyntaxKit/Literal.swift @@ -1,31 +1,60 @@ +// +// 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 public enum Literal: CodeBlock { - case string(String) - case float(Double) - case integer(Int) - case `nil` - case boolean(Bool) + 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))) + 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)) - } + 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 index 1c533ce..7aae1fb 100644 --- a/Sources/SyntaxKit/Parameter.swift +++ b/Sources/SyntaxKit/Parameter.swift @@ -1,34 +1,63 @@ +// +// 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 SwiftSyntax import SwiftParser +import SwiftSyntax 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))) - ) - } + 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/SyntaxKit/ParameterBuilderResult.swift b/Sources/SyntaxKit/ParameterBuilderResult.swift index 8324afc..9f73803 100644 --- a/Sources/SyntaxKit/ParameterBuilderResult.swift +++ b/Sources/SyntaxKit/ParameterBuilderResult.swift @@ -1,24 +1,53 @@ +// +// 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 @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 + 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 + } +} diff --git a/Sources/SyntaxKit/ParameterExp.swift b/Sources/SyntaxKit/ParameterExp.swift index fd8504e..a8065a7 100644 --- a/Sources/SyntaxKit/ParameterExp.swift +++ b/Sources/SyntaxKit/ParameterExp.swift @@ -1,23 +1,52 @@ +// +// 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 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))) - ) - } + 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/SyntaxKit/ParameterExpBuilderResult.swift b/Sources/SyntaxKit/ParameterExpBuilderResult.swift index 256924e..8899213 100644 --- a/Sources/SyntaxKit/ParameterExpBuilderResult.swift +++ b/Sources/SyntaxKit/ParameterExpBuilderResult.swift @@ -1,24 +1,53 @@ +// +// 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 @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 + 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 + } +} diff --git a/Sources/SyntaxKit/PlusAssign.swift b/Sources/SyntaxKit/PlusAssign.swift index ea331ab..bf322d7 100644 --- a/Sources/SyntaxKit/PlusAssign.swift +++ b/Sources/SyntaxKit/PlusAssign.swift @@ -2,40 +2,67 @@ // PlusAssign.swift // SyntaxKit // -// Created by Leo Dion on 6/15/25. +// 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 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 - ]) - ) + 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/SyntaxKit/Return.swift b/Sources/SyntaxKit/Return.swift index fb848e8..6abfda7 100644 --- a/Sources/SyntaxKit/Return.swift +++ b/Sources/SyntaxKit/Return.swift @@ -1,23 +1,52 @@ +// +// 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 public struct Return: CodeBlock { - private let exprs: [CodeBlock] - public init(@CodeBlockBuilderResult _ content: () -> [CodeBlock]) { - self.exprs = content() + 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.") } - 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) - ) + if let varExp = expr as? VariableExp { + return ReturnStmtSyntax( + returnKeyword: .keyword(.return, trailingTrivia: .space), + expression: ExprSyntax(DeclReferenceExprSyntax(baseName: .identifier(varExp.name))) + ) } -} \ No newline at end of file + return ReturnStmtSyntax( + returnKeyword: .keyword(.return, trailingTrivia: .space), + expression: ExprSyntax(expr.syntax) + ) + } +} diff --git a/Sources/SyntaxKit/Struct.swift b/Sources/SyntaxKit/Struct.swift index cdee138..63c474d 100644 --- a/Sources/SyntaxKit/Struct.swift +++ b/Sources/SyntaxKit/Struct.swift @@ -1,61 +1,95 @@ +// +// 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 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 + 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() + ) } - - 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 - ) + + 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 index 84f90ba..9e306f3 100644 --- a/Sources/SyntaxKit/Switch.swift +++ b/Sources/SyntaxKit/Switch.swift @@ -2,34 +2,57 @@ // Switch.swift // SyntaxKit // -// Created by Leo Dion on 6/15/25. +// 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 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 + 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 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 index f2b231f..a67ee68 100644 --- a/Sources/SyntaxKit/SwitchCase.swift +++ b/Sources/SyntaxKit/SwitchCase.swift @@ -2,50 +2,75 @@ // SwitchCase.swift // SyntaxKit // -// Created by Leo Dion on 6/15/25. +// 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 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 + 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))) ) - } - - public var syntax: SyntaxProtocol { switchCaseSyntax } + 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 index 9a61246..04049be 100644 --- a/Sources/SyntaxKit/Trivia+Comments.swift +++ b/Sources/SyntaxKit/Trivia+Comments.swift @@ -1,23 +1,52 @@ +// +// 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 -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 - } - } +extension Trivia { + /// Extract comment strings (line comments, doc comments, block comments) from the trivia collection. + 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 + } } + } - /// Indicates whether the trivia contains any comments. - var hasComments: Bool { - !comments.isEmpty - } -} \ No newline at end of file + /// 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 index 6e82d32..2cf0e87 100644 --- a/Sources/SyntaxKit/Variable.swift +++ b/Sources/SyntaxKit/Variable.swift @@ -2,47 +2,71 @@ // Variable.swift // SyntaxKit // -// Created by Leo Dion on 6/15/25. +// 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 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 + 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))) + ) } - - 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 - ) - ]) + + 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 index 5c9a913..592a810 100644 --- a/Sources/SyntaxKit/VariableDecl.swift +++ b/Sources/SyntaxKit/VariableDecl.swift @@ -1,47 +1,77 @@ +// +// 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 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 - ) - ]) + 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))) ) + } } -} \ No newline at end of file + 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 index afe3780..c58a335 100644 --- a/Sources/SyntaxKit/VariableExp.swift +++ b/Sources/SyntaxKit/VariableExp.swift @@ -2,102 +2,131 @@ // VariableExp.swift // SyntaxKit // -// Created by Leo Dion on 6/15/25. +// 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 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) - } + let name: String + + public init(_ name: String) { + self.name = name + } + + public func property(_ propertyName: String) -> CodeBlock { + PropertyAccessExp(baseName: name, propertyName: propertyName) + } + + public func call(_ methodName: String) -> CodeBlock { + FunctionCallExp(baseName: name, methodName: methodName) + } + + public func call(_ methodName: String, @ParameterExpBuilderResult _ params: () -> [ParameterExp]) + -> CodeBlock + { + FunctionCallExp(baseName: name, methodName: methodName, parameters: params()) + } + + public var syntax: SyntaxProtocol { + 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 - )) - } + 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() - )) - } + 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/SyntaxKit/VariableKind.swift b/Sources/SyntaxKit/VariableKind.swift index bbfb5b5..8eecaca 100644 --- a/Sources/SyntaxKit/VariableKind.swift +++ b/Sources/SyntaxKit/VariableKind.swift @@ -1,6 +1,35 @@ +// +// 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 public enum VariableKind { - case `let` - case `var` -} \ No newline at end of file + case `let` + case `var` +} diff --git a/Sources/SyntaxKit/parser/OldMain.swift b/Sources/SyntaxKit/parser/OldMain.swift deleted file mode 100644 index 62850f9..0000000 --- a/Sources/SyntaxKit/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/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 index f492600..a58afdb 100644 --- a/Sources/SyntaxKit/parser/SyntaxParser.swift +++ b/Sources/SyntaxKit/parser/SyntaxParser.swift @@ -1,7 +1,36 @@ +// +// 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 SwiftSyntax import SwiftOperators import SwiftParser +import SwiftSyntax package struct SyntaxParser { package static func parse(code: String, options: [String] = []) throws -> SyntaxResponse { @@ -20,7 +49,6 @@ package struct SyntaxParser { ) _ = visitor.rewrite(syntax) - let tree = visitor.tree let encoder = JSONEncoder() let json = String(decoding: try encoder.encode(tree), as: UTF8.self) diff --git a/Sources/SyntaxKit/parser/SyntaxResponse.swift b/Sources/SyntaxKit/parser/SyntaxResponse.swift index 394b467..1774a1a 100644 --- a/Sources/SyntaxKit/parser/SyntaxResponse.swift +++ b/Sources/SyntaxKit/parser/SyntaxResponse.swift @@ -1,7 +1,36 @@ +// +// 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 syntaxHTML: String + // package let syntaxHTML: String package let syntaxJSON: String package let swiftVersion: String } diff --git a/Sources/SyntaxKit/parser/TokenVisitor.swift b/Sources/SyntaxKit/parser/TokenVisitor.swift index 6c2025e..88ab4ed 100644 --- a/Sources/SyntaxKit/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): @@ -105,8 +117,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 +145,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 +164,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 } @@ -171,15 +190,13 @@ final class TokenVisitor: SyntaxRewriter { } 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 +204,6 @@ final class TokenVisitor: SyntaxRewriter { } override func visitPost(_ node: Syntax) { - list.append("") if let parent = current.parent { current = tree[parent] } else { @@ -208,22 +224,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 +262,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/SyntaxKit/parser/TreeNode.swift b/Sources/SyntaxKit/parser/TreeNode.swift index b85e603..cb6b808 100644 --- a/Sources/SyntaxKit/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,7 +34,9 @@ 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, graphemeStartColumn: 0, endRow: 0, endColumn: 0, + graphemeEndColumn: 0) var structure = [StructureProperty]() var type: SyntaxType var token: Token? @@ -21,13 +52,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 } } @@ -147,25 +173,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/SyntaxKit/parser/Version.swift b/Sources/SyntaxKit/parser/Version.swift index cab8a40..3a5f2ac 100644 --- a/Sources/SyntaxKit/parser/Version.swift +++ b/Sources/SyntaxKit/parser/Version.swift @@ -1,2 +1,32 @@ +// +// Version.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 + let version = "6.01.0" diff --git a/Sources/skit/main.swift b/Sources/skit/main.swift index ec91882..d7952fe 100644 --- a/Sources/skit/main.swift +++ b/Sources/skit/main.swift @@ -1,3 +1,32 @@ +// +// 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 @@ -5,17 +34,18 @@ import SyntaxKit 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) + // 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) -} \ No newline at end of file + // 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 32f13db..0000000 --- a/Tests/SwiftBuilderTests/SwiftBuilderCommentTests.swift +++ /dev/null @@ -1,109 +0,0 @@ -import XCTest -@testable import SyntaxKit - -final class SyntaxKitCommentTests: 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 98f7056..0000000 --- a/Tests/SwiftBuilderTests/SwiftBuilderLiteralTests.swift +++ /dev/null @@ -1,14 +0,0 @@ -import XCTest -@testable import SyntaxKit - -final class SyntaxKitLiteralTests: 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 5e7089f..0000000 --- a/Tests/SwiftBuilderTests/SwiftBuilderTestsA.swift +++ /dev/null @@ -1,43 +0,0 @@ -import XCTest -@testable import SyntaxKit - -final class SyntaxKitTestsA: 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 0afadc3..0000000 --- a/Tests/SwiftBuilderTests/SwiftBuilderTestsB.swift +++ /dev/null @@ -1,228 +0,0 @@ -import XCTest -@testable import SyntaxKit - -final class SyntaxKitTestsB: 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 51484c9..0000000 --- a/Tests/SwiftBuilderTests/SwiftBuilderTestsC.swift +++ /dev/null @@ -1,104 +0,0 @@ -import XCTest -@testable import SyntaxKit - -final class SyntaxKitTestsC: 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 9d5b9b2..0000000 --- a/Tests/SwiftBuilderTests/SwiftBuilderTestsD.swift +++ /dev/null @@ -1,107 +0,0 @@ -import XCTest -@testable import SyntaxKit - -final class SyntaxKitTestsD: 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/BasicTests.swift b/Tests/SyntaxKitTests/BasicTests.swift new file mode 100644 index 0000000..c8bb006 --- /dev/null +++ b/Tests/SyntaxKitTests/BasicTests.swift @@ -0,0 +1,45 @@ +import XCTest + +@testable import SyntaxKit + +final class BasicTests: 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/SyntaxKitTests/BlackjackTests.swift b/Tests/SyntaxKitTests/BlackjackTests.swift new file mode 100644 index 0000000..6e5eee1 --- /dev/null +++ b/Tests/SyntaxKitTests/BlackjackTests.swift @@ -0,0 +1,249 @@ +import XCTest + +@testable import SyntaxKit + +final class BlackjackTests: 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) + } + + // swiftlint:disable:next function_body_length + 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 + .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/SyntaxKitTests/CommentTests.swift b/Tests/SyntaxKitTests/CommentTests.swift new file mode 100644 index 0000000..488c583 --- /dev/null +++ b/Tests/SyntaxKitTests/CommentTests.swift @@ -0,0 +1,113 @@ +import XCTest + +@testable import SyntaxKit + +final class CommentTests: XCTestCase { + // swiftlint:disable:next function_body_length + 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) + 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/SyntaxKitTests/FunctionTests.swift b/Tests/SyntaxKitTests/FunctionTests.swift new file mode 100644 index 0000000..0fee02a --- /dev/null +++ b/Tests/SyntaxKitTests/FunctionTests.swift @@ -0,0 +1,114 @@ +import XCTest + +@testable import SyntaxKit + +final class FunctionTests: 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/SyntaxKitTests/LiteralTests.swift b/Tests/SyntaxKitTests/LiteralTests.swift new file mode 100644 index 0000000..666402c --- /dev/null +++ b/Tests/SyntaxKitTests/LiteralTests.swift @@ -0,0 +1,15 @@ +import XCTest + +@testable import SyntaxKit + +final class LiteralTests: XCTestCase { + func testGroupWithLiterals() { + let group = Group { + Return { + Literal.integer(1) + } + } + let generated = group.generateCode() + XCTAssertEqual(generated.trimmingCharacters(in: .whitespacesAndNewlines), "return 1") + } +} diff --git a/Tests/SyntaxKitTests/StructTests.swift b/Tests/SyntaxKitTests/StructTests.swift new file mode 100644 index 0000000..de6aba4 --- /dev/null +++ b/Tests/SyntaxKitTests/StructTests.swift @@ -0,0 +1,108 @@ +import XCTest + +@testable import SyntaxKit + +final class StructTests: XCTestCase { + func normalize(_ code: String) -> String { + 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/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: {} From 40a9f63ee7fc57b48fce1db2722ea2fc3da78d72 Mon Sep 17 00:00:00 2001 From: leogdion Date: Tue, 17 Jun 2025 10:18:17 -0400 Subject: [PATCH 18/21] Adding Documentation (#19) --- .gitignore | 3 +- Sources/SyntaxKit/Assignment.swift | 6 + Sources/SyntaxKit/Case.swift | 5 + Sources/SyntaxKit/CodeBlock+Generate.swift | 2 + Sources/SyntaxKit/CodeBlock.swift | 13 ++ Sources/SyntaxKit/CommentBuilderResult.swift | 10 +- Sources/SyntaxKit/ComputedProperty.swift | 6 + Sources/SyntaxKit/Default.swift | 4 + .../Documentation.docc/Documentation.md | 218 ++++++++++++++++++ Sources/SyntaxKit/Enum.swift | 17 ++ Sources/SyntaxKit/Function.swift | 16 ++ Sources/SyntaxKit/Group.swift | 3 + Sources/SyntaxKit/If.swift | 6 + Sources/SyntaxKit/Init.swift | 6 + Sources/SyntaxKit/Let.swift | 6 + Sources/SyntaxKit/Line.swift | 11 +- Sources/SyntaxKit/Literal.swift | 6 + Sources/SyntaxKit/Parameter.swift | 7 + .../SyntaxKit/ParameterBuilderResult.swift | 6 + Sources/SyntaxKit/ParameterExp.swift | 5 + .../SyntaxKit/ParameterExpBuilderResult.swift | 6 + Sources/SyntaxKit/PlusAssign.swift | 5 + Sources/SyntaxKit/Return.swift | 4 + Sources/SyntaxKit/Struct.swift | 9 + Sources/SyntaxKit/Switch.swift | 5 + Sources/SyntaxKit/SwitchCase.swift | 5 + Sources/SyntaxKit/Trivia+Comments.swift | 6 +- Sources/SyntaxKit/Variable.swift | 7 + Sources/SyntaxKit/VariableDecl.swift | 6 + Sources/SyntaxKit/VariableExp.swift | 29 +++ Sources/SyntaxKit/VariableKind.swift | 3 + 31 files changed, 434 insertions(+), 7 deletions(-) create mode 100644 Sources/SyntaxKit/Documentation.docc/Documentation.md diff --git a/.gitignore b/.gitignore index 2640454..95334f2 100644 --- a/.gitignore +++ b/.gitignore @@ -136,4 +136,5 @@ Support/*/macOS.entitlements vendor/ruby public .mint -*.lcov \ No newline at end of file +*.lcov +.docc-build \ No newline at end of file diff --git a/Sources/SyntaxKit/Assignment.swift b/Sources/SyntaxKit/Assignment.swift index 7fb5894..4a0af2e 100644 --- a/Sources/SyntaxKit/Assignment.swift +++ b/Sources/SyntaxKit/Assignment.swift @@ -29,9 +29,15 @@ 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 diff --git a/Sources/SyntaxKit/Case.swift b/Sources/SyntaxKit/Case.swift index 2358b41..2ff34da 100644 --- a/Sources/SyntaxKit/Case.swift +++ b/Sources/SyntaxKit/Case.swift @@ -29,10 +29,15 @@ 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() diff --git a/Sources/SyntaxKit/CodeBlock+Generate.swift b/Sources/SyntaxKit/CodeBlock+Generate.swift index 7fcb330..c912232 100644 --- a/Sources/SyntaxKit/CodeBlock+Generate.swift +++ b/Sources/SyntaxKit/CodeBlock+Generate.swift @@ -31,6 +31,8 @@ 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) { diff --git a/Sources/SyntaxKit/CodeBlock.swift b/Sources/SyntaxKit/CodeBlock.swift index 51e3455..2da3ecc 100644 --- a/Sources/SyntaxKit/CodeBlock.swift +++ b/Sources/SyntaxKit/CodeBlock.swift @@ -30,39 +30,52 @@ 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 struct 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 index e0550a5..609be94 100644 --- a/Sources/SyntaxKit/CommentBuilderResult.swift +++ b/Sources/SyntaxKit/CommentBuilderResult.swift @@ -27,15 +27,20 @@ // 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 { - /// Attach comments to the current `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") { ... } @@ -44,7 +49,8 @@ extension CodeBlock { /// Line(.doc, "This is a documentation comment") /// } /// ``` - /// The provided lines are injected as leading trivia to the declaration produced by this `CodeBlock`. + /// - 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/ComputedProperty.swift b/Sources/SyntaxKit/ComputedProperty.swift index 4da7312..d78e040 100644 --- a/Sources/SyntaxKit/ComputedProperty.swift +++ b/Sources/SyntaxKit/ComputedProperty.swift @@ -29,11 +29,17 @@ 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 diff --git a/Sources/SyntaxKit/Default.swift b/Sources/SyntaxKit/Default.swift index bfd10c4..470a7e9 100644 --- a/Sources/SyntaxKit/Default.swift +++ b/Sources/SyntaxKit/Default.swift @@ -29,8 +29,12 @@ 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() } 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 index 36f5917..170f78b 100644 --- a/Sources/SyntaxKit/Enum.swift +++ b/Sources/SyntaxKit/Enum.swift @@ -29,16 +29,24 @@ 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 @@ -76,15 +84,21 @@ public struct Enum: CodeBlock { } } +/// 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 @@ -92,6 +106,9 @@ public struct EnumCase: CodeBlock { 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 diff --git a/Sources/SyntaxKit/Function.swift b/Sources/SyntaxKit/Function.swift index da829f2..bac906f 100644 --- a/Sources/SyntaxKit/Function.swift +++ b/Sources/SyntaxKit/Function.swift @@ -29,6 +29,7 @@ import SwiftSyntax +/// A Swift `func` declaration. public struct Function: CodeBlock { private let name: String private let parameters: [Parameter] @@ -37,6 +38,11 @@ public struct Function: 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] @@ -47,6 +53,12 @@ public struct Function: CodeBlock { 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], @@ -58,12 +70,16 @@ public struct Function: CodeBlock { 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 diff --git a/Sources/SyntaxKit/Group.swift b/Sources/SyntaxKit/Group.swift index 6d56817..7b3e824 100644 --- a/Sources/SyntaxKit/Group.swift +++ b/Sources/SyntaxKit/Group.swift @@ -29,9 +29,12 @@ 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() } diff --git a/Sources/SyntaxKit/If.swift b/Sources/SyntaxKit/If.swift index 5935920..53e7bd5 100644 --- a/Sources/SyntaxKit/If.swift +++ b/Sources/SyntaxKit/If.swift @@ -29,11 +29,17 @@ 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 diff --git a/Sources/SyntaxKit/Init.swift b/Sources/SyntaxKit/Init.swift index 3206de0..c94a9e8 100644 --- a/Sources/SyntaxKit/Init.swift +++ b/Sources/SyntaxKit/Init.swift @@ -29,9 +29,15 @@ 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() diff --git a/Sources/SyntaxKit/Let.swift b/Sources/SyntaxKit/Let.swift index 5e4f53d..cd2d46d 100644 --- a/Sources/SyntaxKit/Let.swift +++ b/Sources/SyntaxKit/Let.swift @@ -29,9 +29,15 @@ 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 diff --git a/Sources/SyntaxKit/Line.swift b/Sources/SyntaxKit/Line.swift index 8389e13..3017ae6 100644 --- a/Sources/SyntaxKit/Line.swift +++ b/Sources/SyntaxKit/Line.swift @@ -29,8 +29,9 @@ import SwiftSyntax -/// Represents a single comment line that can be attached to a syntax node when using `.comment { ... }` in the DSL. +/// 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 @@ -38,10 +39,13 @@ public struct Line { case doc } + /// The kind of comment. public let kind: Kind + /// The text of the comment. public let text: String? - /// Convenience initializer for a regular line comment without specifying the kind explicitly. + /// Creates a regular line comment. + /// - Parameter text: The text of the comment. public init(_ text: String) { self.kind = .line self.text = text @@ -55,6 +59,9 @@ public struct Line { /// 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 diff --git a/Sources/SyntaxKit/Literal.swift b/Sources/SyntaxKit/Literal.swift index 9d991b3..7e0e1dd 100644 --- a/Sources/SyntaxKit/Literal.swift +++ b/Sources/SyntaxKit/Literal.swift @@ -29,11 +29,17 @@ 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 { diff --git a/Sources/SyntaxKit/Parameter.swift b/Sources/SyntaxKit/Parameter.swift index 7aae1fb..08c76d9 100644 --- a/Sources/SyntaxKit/Parameter.swift +++ b/Sources/SyntaxKit/Parameter.swift @@ -31,12 +31,19 @@ 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 diff --git a/Sources/SyntaxKit/ParameterBuilderResult.swift b/Sources/SyntaxKit/ParameterBuilderResult.swift index 9f73803..3e3bad6 100644 --- a/Sources/SyntaxKit/ParameterBuilderResult.swift +++ b/Sources/SyntaxKit/ParameterBuilderResult.swift @@ -29,24 +29,30 @@ import Foundation +/// A result builder for creating arrays of ``Parameter``s. @resultBuilder public struct 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 index a8065a7..02274e8 100644 --- a/Sources/SyntaxKit/ParameterExp.swift +++ b/Sources/SyntaxKit/ParameterExp.swift @@ -29,10 +29,15 @@ 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 diff --git a/Sources/SyntaxKit/ParameterExpBuilderResult.swift b/Sources/SyntaxKit/ParameterExpBuilderResult.swift index 8899213..4cf937a 100644 --- a/Sources/SyntaxKit/ParameterExpBuilderResult.swift +++ b/Sources/SyntaxKit/ParameterExpBuilderResult.swift @@ -29,24 +29,30 @@ import Foundation +/// A result builder for creating arrays of ``ParameterExp``s. @resultBuilder public struct 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 index bf322d7..8b79cc8 100644 --- a/Sources/SyntaxKit/PlusAssign.swift +++ b/Sources/SyntaxKit/PlusAssign.swift @@ -29,10 +29,15 @@ 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 diff --git a/Sources/SyntaxKit/Return.swift b/Sources/SyntaxKit/Return.swift index 6abfda7..78c05d4 100644 --- a/Sources/SyntaxKit/Return.swift +++ b/Sources/SyntaxKit/Return.swift @@ -29,8 +29,12 @@ 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() } diff --git a/Sources/SyntaxKit/Struct.swift b/Sources/SyntaxKit/Struct.swift index 63c474d..50d6dc6 100644 --- a/Sources/SyntaxKit/Struct.swift +++ b/Sources/SyntaxKit/Struct.swift @@ -29,12 +29,18 @@ 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] ) { @@ -43,6 +49,9 @@ public struct Struct: CodeBlock { 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 diff --git a/Sources/SyntaxKit/Switch.swift b/Sources/SyntaxKit/Switch.swift index 9e306f3..b03f7fb 100644 --- a/Sources/SyntaxKit/Switch.swift +++ b/Sources/SyntaxKit/Switch.swift @@ -29,10 +29,15 @@ 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() diff --git a/Sources/SyntaxKit/SwitchCase.swift b/Sources/SyntaxKit/SwitchCase.swift index a67ee68..1d219b4 100644 --- a/Sources/SyntaxKit/SwitchCase.swift +++ b/Sources/SyntaxKit/SwitchCase.swift @@ -29,10 +29,15 @@ 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() diff --git a/Sources/SyntaxKit/Trivia+Comments.swift b/Sources/SyntaxKit/Trivia+Comments.swift index 04049be..ecc7008 100644 --- a/Sources/SyntaxKit/Trivia+Comments.swift +++ b/Sources/SyntaxKit/Trivia+Comments.swift @@ -30,7 +30,9 @@ import SwiftSyntax extension Trivia { - /// Extract comment strings (line comments, doc comments, block comments) from the trivia collection. + /// 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 { @@ -45,7 +47,7 @@ extension Trivia { } } - /// Indicates whether the trivia contains any comments. + /// 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 index 2cf0e87..b37ec0d 100644 --- a/Sources/SyntaxKit/Variable.swift +++ b/Sources/SyntaxKit/Variable.swift @@ -29,12 +29,19 @@ 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 diff --git a/Sources/SyntaxKit/VariableDecl.swift b/Sources/SyntaxKit/VariableDecl.swift index 592a810..64da71b 100644 --- a/Sources/SyntaxKit/VariableDecl.swift +++ b/Sources/SyntaxKit/VariableDecl.swift @@ -29,11 +29,17 @@ 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 diff --git a/Sources/SyntaxKit/VariableExp.swift b/Sources/SyntaxKit/VariableExp.swift index c58a335..5616290 100644 --- a/Sources/SyntaxKit/VariableExp.swift +++ b/Sources/SyntaxKit/VariableExp.swift @@ -29,21 +29,35 @@ 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 { @@ -55,10 +69,15 @@ public struct VariableExp: CodeBlock { } } +/// 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 @@ -76,17 +95,27 @@ public struct PropertyAccessExp: CodeBlock { } } +/// 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 diff --git a/Sources/SyntaxKit/VariableKind.swift b/Sources/SyntaxKit/VariableKind.swift index 8eecaca..a81a0f6 100644 --- a/Sources/SyntaxKit/VariableKind.swift +++ b/Sources/SyntaxKit/VariableKind.swift @@ -29,7 +29,10 @@ import Foundation +/// The kind of a variable declaration. public enum VariableKind { + /// A `let` declaration. case `let` + /// A `var` declaration. case `var` } From a2e89d20d524b999beb1e3af7c8f8c24205ed102 Mon Sep 17 00:00:00 2001 From: leogdion Date: Tue, 17 Jun 2025 10:30:53 -0400 Subject: [PATCH 19/21] Migrated from XCTest to Swift Testing (#16) * Migrated from XCTest to Swift Testing * Add Tests for PR#16 (#17) --------- Co-authored-by: codecov-ai[bot] <156709835+codecov-ai[bot]@users.noreply.github.com> --- .../AssertionMigrationTests.swift | 79 ++++++++++ Tests/SyntaxKitTests/BasicTests.swift | 23 +-- Tests/SyntaxKitTests/BlackjackTests.swift | 69 ++------ .../CodeStyleMigrationTests.swift | 81 ++++++++++ Tests/SyntaxKitTests/CommentTests.swift | 17 +- .../FrameworkCompatibilityTests.swift | 148 ++++++++++++++++++ Tests/SyntaxKitTests/FunctionTests.swift | 73 +++------ Tests/SyntaxKitTests/LiteralTests.swift | 8 +- Tests/SyntaxKitTests/MigrationTests.swift | 131 ++++++++++++++++ Tests/SyntaxKitTests/String+Normalize.swift | 11 ++ Tests/SyntaxKitTests/StructTests.swift | 37 ++--- 11 files changed, 518 insertions(+), 159 deletions(-) create mode 100644 Tests/SyntaxKitTests/AssertionMigrationTests.swift create mode 100644 Tests/SyntaxKitTests/CodeStyleMigrationTests.swift create mode 100644 Tests/SyntaxKitTests/FrameworkCompatibilityTests.swift create mode 100644 Tests/SyntaxKitTests/MigrationTests.swift create mode 100644 Tests/SyntaxKitTests/String+Normalize.swift 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 index c8bb006..bfc9433 100644 --- a/Tests/SyntaxKitTests/BasicTests.swift +++ b/Tests/SyntaxKitTests/BasicTests.swift @@ -1,9 +1,9 @@ -import XCTest +import Testing @testable import SyntaxKit -final class BasicTests: XCTestCase { - func testBlackjackCardExample() throws { +struct BasicTests { + @Test func testBlackjackCardExample() throws { let blackjackCard = Struct("BlackjackCard") { Enum("Suit") { EnumCase("spades").equals("♠") @@ -25,21 +25,10 @@ final class BasicTests: XCTestCase { """ // 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 normalizedGenerated = blackjackCard.syntax.description.normalize() - 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) + let normalizedExpected = expected.normalize() - XCTAssertEqual(normalizedGenerated, normalizedExpected) + #expect(normalizedGenerated == normalizedExpected) } } diff --git a/Tests/SyntaxKitTests/BlackjackTests.swift b/Tests/SyntaxKitTests/BlackjackTests.swift index 6e5eee1..16ab0d0 100644 --- a/Tests/SyntaxKitTests/BlackjackTests.swift +++ b/Tests/SyntaxKitTests/BlackjackTests.swift @@ -1,9 +1,9 @@ -import XCTest +import Testing @testable import SyntaxKit -final class BlackjackTests: XCTestCase { - func testBlackjackCardExample() throws { +struct BlackjackTests { + @Test func testBlackjackCardExample() throws { let syntax = Struct("BlackjackCard") { Enum("Suit") { EnumCase("spades").equals("♠") @@ -63,34 +63,15 @@ final class BlackjackTests: XCTestCase { """ // 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) + let normalizedGenerated = syntax.syntax.description.normalize() + + let normalizedExpected = expected.normalize() + + #expect(normalizedGenerated == normalizedExpected) } // swiftlint:disable:next function_body_length - func testFullBlackjackCardExample() throws { + @Test func testFullBlackjackCardExample() throws { // swiftlint:disable:next closure_body_length let syntax = Struct("BlackjackCard") { Enum("Suit") { @@ -159,7 +140,8 @@ final class BlackjackTests: XCTestCase { Let("second", "rank.values.second"), then: { PlusAssign("output", "\" or \\(second)\"") - }) + } + ) Return { VariableExp("output") } @@ -221,29 +203,10 @@ final class BlackjackTests: XCTestCase { """ // 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) + 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 index 488c583..331852e 100644 --- a/Tests/SyntaxKitTests/CommentTests.swift +++ b/Tests/SyntaxKitTests/CommentTests.swift @@ -1,10 +1,10 @@ -import XCTest +import Testing @testable import SyntaxKit -final class CommentTests: XCTestCase { +struct CommentTests { // swiftlint:disable:next function_body_length - func testCommentInjection() { + @Test func testCommentInjection() { // swiftlint:disable:next closure_body_length let syntax = Group { Struct("Card") { @@ -100,14 +100,13 @@ final class CommentTests: XCTestCase { } let generated = syntax.generateCode().trimmingCharacters(in: .whitespacesAndNewlines) - print("Generated:\n", generated) - XCTAssertFalse(generated.isEmpty) + #expect(!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") + // #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 - // XCTAssertTrue(generated.contains("struct Foo")) - // XCTAssertTrue(generated.contains("bar"), "Variable declaration should be present") + // #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 index 0fee02a..797002e 100644 --- a/Tests/SyntaxKitTests/FunctionTests.swift +++ b/Tests/SyntaxKitTests/FunctionTests.swift @@ -1,9 +1,9 @@ -import XCTest +import Testing @testable import SyntaxKit -final class FunctionTests: XCTestCase { - func testBasicFunction() throws { +struct FunctionTests { + @Test func testBasicFunction() throws { let function = Function("calculateSum", returns: "Int") { Parameter(name: "a", type: "Int") Parameter(name: "b", type: "Int") @@ -20,25 +20,14 @@ final class FunctionTests: XCTestCase { """ // 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) + let normalizedGenerated = function.syntax.description.normalize() + + let normalizedExpected = expected.normalize() + + #expect(normalizedGenerated == normalizedExpected) } - func testStaticFunction() throws { + @Test func testStaticFunction() throws { let function = Function( "createInstance", returns: "MyType", { @@ -59,25 +48,14 @@ final class FunctionTests: XCTestCase { """ // 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) + let normalizedGenerated = function.syntax.description.normalize() + + let normalizedExpected = expected.normalize() + + #expect(normalizedGenerated == normalizedExpected) } - func testMutatingFunction() throws { + @Test func testMutatingFunction() throws { let function = Function( "updateValue", { @@ -94,21 +72,10 @@ final class FunctionTests: XCTestCase { """ // 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) + 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 index 666402c..255675a 100644 --- a/Tests/SyntaxKitTests/LiteralTests.swift +++ b/Tests/SyntaxKitTests/LiteralTests.swift @@ -1,15 +1,15 @@ -import XCTest +import Testing @testable import SyntaxKit -final class LiteralTests: XCTestCase { - func testGroupWithLiterals() { +struct LiteralTests { + @Test func testGroupWithLiterals() { let group = Group { Return { Literal.integer(1) } } let generated = group.generateCode() - XCTAssertEqual(generated.trimmingCharacters(in: .whitespacesAndNewlines), "return 1") + #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 index de6aba4..e578664 100644 --- a/Tests/SyntaxKitTests/StructTests.swift +++ b/Tests/SyntaxKitTests/StructTests.swift @@ -1,18 +1,9 @@ -import XCTest +import Testing @testable import SyntaxKit -final class StructTests: XCTestCase { - func normalize(_ code: String) -> String { - 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() { +struct StructTests { + @Test func testGenericStruct() { let stackStruct = Struct("Stack", generic: "Element") { Variable(.var, name: "items", type: "[Element]", equals: "[]") @@ -67,12 +58,12 @@ final class StructTests: XCTestCase { } """ - let normalizedGenerated = normalize(stackStruct.generateCode()) - let normalizedExpected = normalize(expectedCode) - XCTAssertEqual(normalizedGenerated, normalizedExpected) + let normalizedGenerated = stackStruct.generateCode().normalize() + let normalizedExpected = expectedCode.normalize() + #expect(normalizedGenerated == normalizedExpected) } - func testGenericStructWithInheritance() { + @Test func testGenericStructWithInheritance() { let containerStruct = Struct("Container", generic: "T") { Variable(.var, name: "value", type: "T") }.inherits("Equatable") @@ -83,12 +74,12 @@ final class StructTests: XCTestCase { } """ - let normalizedGenerated = normalize(containerStruct.generateCode()) - let normalizedExpected = normalize(expectedCode) - XCTAssertEqual(normalizedGenerated, normalizedExpected) + let normalizedGenerated = containerStruct.generateCode().normalize() + let normalizedExpected = expectedCode.normalize() + #expect(normalizedGenerated == normalizedExpected) } - func testNonGenericStruct() { + @Test func testNonGenericStruct() { let simpleStruct = Struct("Point") { Variable(.var, name: "x", type: "Double") Variable(.var, name: "y", type: "Double") @@ -101,8 +92,8 @@ final class StructTests: XCTestCase { } """ - let normalizedGenerated = normalize(simpleStruct.generateCode()) - let normalizedExpected = normalize(expectedCode) - XCTAssertEqual(normalizedGenerated, normalizedExpected) + let normalizedGenerated = simpleStruct.generateCode().normalize() + let normalizedExpected = expectedCode.normalize() + #expect(normalizedGenerated == normalizedExpected) } } From 57990c21fc9f56646fa04cdc3476e22806712f9b Mon Sep 17 00:00:00 2001 From: leogdion Date: Tue, 17 Jun 2025 10:52:49 -0400 Subject: [PATCH 20/21] Adding Periphery and Fixing Linting Issues (#20) * adding periphery and fixing issues * disable periphery for CI --- .periphery.yml | 1 + Package.swift | 81 +++++++++---------- Scripts/lint.sh | 4 +- Sources/SyntaxKit/CodeBlock.swift | 2 +- .../SyntaxKit/ParameterBuilderResult.swift | 2 +- .../SyntaxKit/ParameterExpBuilderResult.swift | 2 +- Sources/SyntaxKit/parser/SyntaxParser.swift | 4 +- Sources/SyntaxKit/parser/SyntaxResponse.swift | 2 - Sources/SyntaxKit/parser/TokenVisitor.swift | 8 +- Sources/SyntaxKit/parser/TreeNode.swift | 6 +- Sources/SyntaxKit/parser/Version.swift | 32 -------- 11 files changed, 51 insertions(+), 93 deletions(-) create mode 100644 .periphery.yml delete mode 100644 Sources/SyntaxKit/parser/Version.swift 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/Package.swift b/Package.swift index 1eac097..42c9345 100644 --- a/Package.swift +++ b/Package.swift @@ -4,46 +4,43 @@ import PackageDescription let package = Package( - name: "SyntaxKit", - platforms: [ - .macOS(.v13), - .iOS(.v13), - .watchOS(.v6), - .tvOS(.v13), - .visionOS(.v1) - ], - products: [ - // Products define the executables and libraries a package produces, making them visible to other packages. - .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: [ - // 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: "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"] - ), - ] + 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/Scripts/lint.sh b/Scripts/lint.sh index 533a00a..e0e82f6 100755 --- a/Scripts/lint.sh +++ b/Scripts/lint.sh @@ -75,6 +75,8 @@ $PACKAGE_DIR/Scripts/header.sh -d $PACKAGE_DIR/Sources -c "Leo Dion" -o "Bright run_command $MINT_RUN swiftlint lint $SWIFTLINT_OPTIONS run_command $MINT_RUN swift-format lint --recursive --parallel $SWIFTFORMAT_OPTIONS Sources Tests -#$MINT_RUN periphery scan $PERIPHERY_OPTIONS --disable-update-check +if [ -z "$CI" ]; then + run_command $MINT_RUN periphery scan $PERIPHERY_OPTIONS --disable-update-check +fi popd diff --git a/Sources/SyntaxKit/CodeBlock.swift b/Sources/SyntaxKit/CodeBlock.swift index 2da3ecc..086e2a1 100644 --- a/Sources/SyntaxKit/CodeBlock.swift +++ b/Sources/SyntaxKit/CodeBlock.swift @@ -46,7 +46,7 @@ public protocol CodeBlockBuilder { /// A result builder for creating arrays of ``CodeBlock``s. @resultBuilder -public struct CodeBlockBuilderResult { +public enum CodeBlockBuilderResult { /// Builds a block of ``CodeBlock``s. public static func buildBlock(_ components: CodeBlock...) -> [CodeBlock] { components diff --git a/Sources/SyntaxKit/ParameterBuilderResult.swift b/Sources/SyntaxKit/ParameterBuilderResult.swift index 3e3bad6..43c641e 100644 --- a/Sources/SyntaxKit/ParameterBuilderResult.swift +++ b/Sources/SyntaxKit/ParameterBuilderResult.swift @@ -31,7 +31,7 @@ import Foundation /// A result builder for creating arrays of ``Parameter``s. @resultBuilder -public struct ParameterBuilderResult { +public enum ParameterBuilderResult { /// Builds a block of ``Parameter``s. public static func buildBlock(_ components: Parameter...) -> [Parameter] { components diff --git a/Sources/SyntaxKit/ParameterExpBuilderResult.swift b/Sources/SyntaxKit/ParameterExpBuilderResult.swift index 4cf937a..ef98f12 100644 --- a/Sources/SyntaxKit/ParameterExpBuilderResult.swift +++ b/Sources/SyntaxKit/ParameterExpBuilderResult.swift @@ -31,7 +31,7 @@ import Foundation /// A result builder for creating arrays of ``ParameterExp``s. @resultBuilder -public struct ParameterExpBuilderResult { +public enum ParameterExpBuilderResult { /// Builds a block of ``ParameterExp``s. public static func buildBlock(_ components: ParameterExp...) -> [ParameterExp] { components diff --git a/Sources/SyntaxKit/parser/SyntaxParser.swift b/Sources/SyntaxKit/parser/SyntaxParser.swift index a58afdb..f4f38e5 100644 --- a/Sources/SyntaxKit/parser/SyntaxParser.swift +++ b/Sources/SyntaxKit/parser/SyntaxParser.swift @@ -32,7 +32,7 @@ import SwiftOperators import SwiftParser import SwiftSyntax -package struct SyntaxParser { +package enum SyntaxParser { package static func parse(code: String, options: [String] = []) throws -> SyntaxResponse { let sourceFile = Parser.parse(source: code) @@ -53,6 +53,6 @@ package struct SyntaxParser { let encoder = JSONEncoder() let json = String(decoding: try encoder.encode(tree), as: UTF8.self) - return SyntaxResponse(syntaxJSON: json, swiftVersion: version) + return SyntaxResponse(syntaxJSON: json) } } diff --git a/Sources/SyntaxKit/parser/SyntaxResponse.swift b/Sources/SyntaxKit/parser/SyntaxResponse.swift index 1774a1a..7dbb636 100644 --- a/Sources/SyntaxKit/parser/SyntaxResponse.swift +++ b/Sources/SyntaxKit/parser/SyntaxResponse.swift @@ -30,7 +30,5 @@ import Foundation package struct SyntaxResponse: Codable { - // package let syntaxHTML: String package let syntaxJSON: String - package let swiftVersion: String } diff --git a/Sources/SyntaxKit/parser/TokenVisitor.swift b/Sources/SyntaxKit/parser/TokenVisitor.swift index 88ab4ed..aad7ca5 100644 --- a/Sources/SyntaxKit/parser/TokenVisitor.swift +++ b/Sources/SyntaxKit/parser/TokenVisitor.swift @@ -97,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 ) @@ -185,9 +183,7 @@ 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 diff --git a/Sources/SyntaxKit/parser/TreeNode.swift b/Sources/SyntaxKit/parser/TreeNode.swift index cb6b808..4c35089 100644 --- a/Sources/SyntaxKit/parser/TreeNode.swift +++ b/Sources/SyntaxKit/parser/TreeNode.swift @@ -35,12 +35,10 @@ final class TreeNode: Codable { var text: String var range = Range( - startRow: 0, startColumn: 0, graphemeStartColumn: 0, endRow: 0, endColumn: 0, - graphemeEndColumn: 0) + 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 @@ -76,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 { diff --git a/Sources/SyntaxKit/parser/Version.swift b/Sources/SyntaxKit/parser/Version.swift deleted file mode 100644 index 3a5f2ac..0000000 --- a/Sources/SyntaxKit/parser/Version.swift +++ /dev/null @@ -1,32 +0,0 @@ -// -// Version.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 - -let version = "6.01.0" From 16409e17d10033c092f23a475a854aaa992b9a64 Mon Sep 17 00:00:00 2001 From: leogdion Date: Tue, 17 Jun 2025 11:31:57 -0400 Subject: [PATCH 21/21] Update README.md (#23) --- README.md | 28 +++++++++++++++++++++++----- 1 file changed, 23 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 3dc21c9..d9372c3 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,22 @@ # SyntaxKit -SyntaxKit 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 @@ -8,7 +24,7 @@ Add SyntaxKit to your project using Swift Package Manager: ```swift dependencies: [ - .package(url: "https://github.com/yourusername/SyntaxKit.git", from: "1.0.0") + .package(url: "https://github.com/yourusername/SyntaxKit.git", from: "0.0.1") ] ``` @@ -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)