From fe15366f2ff57aba8ed9e55daeaa0e7050872200 Mon Sep 17 00:00:00 2001 From: Leo Dion Date: Wed, 18 Jun 2025 13:03:32 -0400 Subject: [PATCH 1/3] adding Class and Protocol --- Sources/SyntaxKit/Class.swift | 150 ++++++++++++++++++ Sources/SyntaxKit/FunctionRequirement.swift | 147 +++++++++++++++++ Sources/SyntaxKit/PropertyRequirement.swift | 102 ++++++++++++ Sources/SyntaxKit/Protocol.swift | 104 ++++++++++++ .../ClassAndProtocolTests.swift | 47 ++++++ 5 files changed, 550 insertions(+) create mode 100644 Sources/SyntaxKit/Class.swift create mode 100644 Sources/SyntaxKit/FunctionRequirement.swift create mode 100644 Sources/SyntaxKit/PropertyRequirement.swift create mode 100644 Sources/SyntaxKit/Protocol.swift create mode 100644 Tests/SyntaxKitTests/ClassAndProtocolTests.swift diff --git a/Sources/SyntaxKit/Class.swift b/Sources/SyntaxKit/Class.swift new file mode 100644 index 0000000..0967e3e --- /dev/null +++ b/Sources/SyntaxKit/Class.swift @@ -0,0 +1,150 @@ +// +// Class.swift +// SyntaxKit +// +// Created by Leo Dion. +// Copyright © 2025 BrightDigit. +// +// Permission is hereby granted, free of charge, to any person +// obtaining a copy of this software and associated documentation +// files (the “Software”), to deal in the Software without +// restriction, including without limitation the rights to use, +// copy, modify, merge, publish, distribute, sublicense, and/or +// sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following +// conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +// OTHER DEALINGS IN THE SOFTWARE. +// + +import SwiftSyntax + +/// A Swift `class` declaration. +public struct Class: CodeBlock { + private let name: String + private let members: [CodeBlock] + private var inheritance: [String] = [] + private var genericParameters: [String] = [] + private var isFinal: Bool = false + + /// Creates a `class` declaration. + /// - Parameters: + /// - name: The name of the class. + /// - generics: A list of generic parameters for the class. + /// - content: A ``CodeBlockBuilder`` that provides the members of the class. + public init( + _ name: String, + generics: [String] = [], + @CodeBlockBuilderResult _ content: () -> [CodeBlock] + ) { + self.name = name + self.members = content() + self.genericParameters = generics + } + + /// Sets one or more inherited types (superclass first followed by any protocols). + /// - Parameter types: The list of types to inherit from. + /// - Returns: A copy of the class with the inheritance set. + public func inherits(_ types: String...) -> Self { + var copy = self + copy.inheritance = types + return copy + } + + /// Marks the class declaration as `final`. + /// - Returns: A copy of the class marked as `final`. + public func final() -> Self { + var copy = self + copy.isFinal = true + return copy + } + + public var syntax: SyntaxProtocol { + let classKeyword = TokenSyntax.keyword(.class, trailingTrivia: .space) + let identifier = TokenSyntax.identifier(name) + + // Generic parameter clause + var genericParameterClause: GenericParameterClauseSyntax? + if !genericParameters.isEmpty { + let parameterList = GenericParameterListSyntax( + genericParameters.enumerated().map { idx, name in + var param = GenericParameterSyntax(name: .identifier(name)) + if idx < genericParameters.count - 1 { + param = param.with( + \.trailingComma, + TokenSyntax.commaToken(trailingTrivia: .space) + ) + } + return param + } + ) + genericParameterClause = GenericParameterClauseSyntax( + leftAngle: .leftAngleToken(), + parameters: parameterList, + rightAngle: .rightAngleToken() + ) + } + + // Inheritance clause + var inheritanceClause: InheritanceClauseSyntax? + if !inheritance.isEmpty { + let inheritedTypes = inheritance.map { type in + InheritedTypeSyntax(type: IdentifierTypeSyntax(name: .identifier(type))) + } + inheritanceClause = InheritanceClauseSyntax( + colon: .colonToken(), + inheritedTypes: InheritedTypeListSyntax( + inheritedTypes.enumerated().map { idx, inherited in + var inheritedType = inherited + if idx < inheritedTypes.count - 1 { + inheritedType = inheritedType.with( + \.trailingComma, + TokenSyntax.commaToken(trailingTrivia: .space) + ) + } + return inheritedType + } + ) + ) + } + + // Member block + let memberBlock = MemberBlockSyntax( + leftBrace: .leftBraceToken(leadingTrivia: .space, trailingTrivia: .newline), + members: MemberBlockItemListSyntax( + members.compactMap { member in + guard let decl = member.syntax.as(DeclSyntax.self) else { return nil } + return MemberBlockItemSyntax(decl: decl, trailingTrivia: .newline) + } + ), + rightBrace: .rightBraceToken(leadingTrivia: .newline) + ) + + // Modifiers + var modifiers: DeclModifierListSyntax = [] + if isFinal { + modifiers = DeclModifierListSyntax([ + DeclModifierSyntax(name: .keyword(.final, trailingTrivia: .space)) + ]) + } + + return ClassDeclSyntax( + modifiers: modifiers, + classKeyword: classKeyword, + name: identifier, + genericParameterClause: genericParameterClause, + inheritanceClause: inheritanceClause, + memberBlock: memberBlock + ) + } +} \ No newline at end of file diff --git a/Sources/SyntaxKit/FunctionRequirement.swift b/Sources/SyntaxKit/FunctionRequirement.swift new file mode 100644 index 0000000..f978673 --- /dev/null +++ b/Sources/SyntaxKit/FunctionRequirement.swift @@ -0,0 +1,147 @@ +// +// FunctionRequirement.swift +// SyntaxKit +// +// Created by Leo Dion. +// Copyright © 2025 BrightDigit. +// +// Permission is hereby granted, free of charge, to any person +// obtaining a copy of this software and associated documentation +// files (the “Software”), to deal in the Software without +// restriction, including without limitation the rights to use, +// copy, modify, merge, publish, distribute, sublicense, and/or +// sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following +// conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +// OTHER DEALINGS IN THE SOFTWARE. +// + +import SwiftSyntax + +/// A function requirement within a protocol declaration (no body). +public struct FunctionRequirement: CodeBlock { + private let name: String + private let parameters: [Parameter] + private let returnType: String? + private var isStatic: Bool = false + private var isMutating: Bool = false + + /// Creates a parameterless function requirement. + /// - Parameters: + /// - name: The function name. + /// - returnType: Optional return type. + public init(_ name: String, returns returnType: String? = nil) { + self.name = name + self.parameters = [] + self.returnType = returnType + } + + /// Creates a function requirement with parameters. + /// - Parameters: + /// - name: The function name. + /// - returnType: Optional return type. + /// - params: A ParameterBuilderResult providing the parameters. + public init( + _ name: String, returns returnType: String? = nil, + @ParameterBuilderResult _ params: () -> [Parameter] + ) { + self.name = name + self.parameters = params() + self.returnType = returnType + } + + /// Marks the function requirement as `static`. + public func `static`() -> Self { + var copy = self + copy.isStatic = true + return copy + } + + /// Marks the function requirement as `mutating`. + public func mutating() -> Self { + var copy = self + copy.isMutating = true + return copy + } + + public var syntax: SyntaxProtocol { + let funcKeyword = TokenSyntax.keyword(.func, trailingTrivia: .space) + let identifier = TokenSyntax.identifier(name) + + // Parameters + 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 + }) + } + + // Return clause + var returnClause: ReturnClauseSyntax? + if let returnType = returnType { + returnClause = ReturnClauseSyntax( + arrow: .arrowToken(leadingTrivia: .space, trailingTrivia: .space), + type: IdentifierTypeSyntax(name: .identifier(returnType)) + ) + } + + // 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, + signature: FunctionSignatureSyntax( + parameterClause: FunctionParameterClauseSyntax( + leftParen: .leftParenToken(), parameters: paramList, rightParen: .rightParenToken() + ), + effectSpecifiers: nil, + returnClause: returnClause + ), + body: nil + ) + } +} \ No newline at end of file diff --git a/Sources/SyntaxKit/PropertyRequirement.swift b/Sources/SyntaxKit/PropertyRequirement.swift new file mode 100644 index 0000000..b6642b0 --- /dev/null +++ b/Sources/SyntaxKit/PropertyRequirement.swift @@ -0,0 +1,102 @@ +// +// PropertyRequirement.swift +// SyntaxKit +// +// Created by Leo Dion. +// Copyright © 2025 BrightDigit. +// +// Permission is hereby granted, free of charge, to any person +// obtaining a copy of this software and associated documentation +// files (the “Software”), to deal in the Software without +// restriction, including without limitation the rights to use, +// copy, modify, merge, publish, distribute, sublicense, and/or +// sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following +// conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +// OTHER DEALINGS IN THE SOFTWARE. +// + +import SwiftSyntax + +/// A property requirement inside a protocol declaration. +public struct PropertyRequirement: CodeBlock { + /// The accessor options for the property. + public enum Access { + case get + case getSet + } + + private let name: String + private let type: String + private let access: Access + + /// Creates a property requirement. + /// - Parameters: + /// - name: The property name. + /// - type: The property type. + /// - access: Whether the property is get-only or get/set. + public init(_ name: String, type: String, access: Access = .get) { + self.name = name + self.type = type + self.access = access + } + + public var syntax: SyntaxProtocol { + let varKeyword = TokenSyntax.keyword(.var, trailingTrivia: .space) + let identifier = TokenSyntax.identifier(name, trailingTrivia: .space) + + let typeAnnotation = TypeAnnotationSyntax( + colon: .colonToken(leadingTrivia: .space, trailingTrivia: .space), + type: IdentifierTypeSyntax(name: .identifier(type)) + ) + + // Build accessor list + let accessorList: AccessorDeclListSyntax = { + switch access { + case .get: + return AccessorDeclListSyntax([ + AccessorDeclSyntax( + accessorSpecifier: .keyword(.get, trailingTrivia: .space) + ) + ]) + case .getSet: + return AccessorDeclListSyntax([ + AccessorDeclSyntax( + accessorSpecifier: .keyword(.get, trailingTrivia: .space) + ), + AccessorDeclSyntax( + accessorSpecifier: .keyword(.set, trailingTrivia: .space) + ) + ]) + } + }() + + let accessorBlock = AccessorBlockSyntax( + leftBrace: .leftBraceToken(leadingTrivia: .space, trailingTrivia: .space), + accessors: .accessors(accessorList), + rightBrace: .rightBraceToken(leadingTrivia: .space, trailingTrivia: .newline) + ) + + return VariableDeclSyntax( + bindingSpecifier: varKeyword, + bindings: PatternBindingListSyntax([ + PatternBindingSyntax( + pattern: IdentifierPatternSyntax(identifier: identifier), + typeAnnotation: typeAnnotation, + accessorBlock: accessorBlock + ) + ]) + ) + } +} \ No newline at end of file diff --git a/Sources/SyntaxKit/Protocol.swift b/Sources/SyntaxKit/Protocol.swift new file mode 100644 index 0000000..e405b7c --- /dev/null +++ b/Sources/SyntaxKit/Protocol.swift @@ -0,0 +1,104 @@ +// +// Protocol.swift +// SyntaxKit +// +// Created by Leo Dion. +// Copyright © 2025 BrightDigit. +// +// Permission is hereby granted, free of charge, to any person +// obtaining a copy of this software and associated documentation +// files (the “Software”), to deal in the Software without +// restriction, including without limitation the rights to use, +// copy, modify, merge, publish, distribute, sublicense, and/or +// sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following +// conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +// OTHER DEALINGS IN THE SOFTWARE. +// + +import SwiftSyntax + +/// A Swift `protocol` declaration. +public struct Protocol: CodeBlock { + private let name: String + private let members: [CodeBlock] + private var inheritance: [String] = [] + + /// Creates a `protocol` declaration. + /// - Parameters: + /// - name: The name of the protocol. + /// - content: A ``CodeBlockBuilder`` that provides the members of the protocol. + public init(_ name: String, @CodeBlockBuilderResult _ content: () -> [CodeBlock]) { + self.name = name + self.members = content() + } + + /// Sets one or more inherited protocols. + /// - Parameter types: The list of protocols this protocol inherits from. + /// - Returns: A copy of the protocol with the inheritance set. + public func inherits(_ types: String...) -> Self { + var copy = self + copy.inheritance = types + return copy + } + + public var syntax: SyntaxProtocol { + let protocolKeyword = TokenSyntax.keyword(.protocol, trailingTrivia: .space) + let identifier = TokenSyntax.identifier(name) + + // Inheritance clause + var inheritanceClause: InheritanceClauseSyntax? + if !inheritance.isEmpty { + let inheritedTypes = inheritance.map { type in + InheritedTypeSyntax(type: IdentifierTypeSyntax(name: .identifier(type))) + } + inheritanceClause = InheritanceClauseSyntax( + colon: .colonToken(), + inheritedTypes: InheritedTypeListSyntax( + inheritedTypes.enumerated().map { idx, inherited in + var inheritedType = inherited + if idx < inheritedTypes.count - 1 { + inheritedType = inheritedType.with( + \.trailingComma, + TokenSyntax.commaToken(trailingTrivia: .space) + ) + } + return inheritedType + } + ) + ) + } + + // Member block + let memberBlock = MemberBlockSyntax( + leftBrace: .leftBraceToken(leadingTrivia: .space, trailingTrivia: .newline), + members: MemberBlockItemListSyntax( + members.compactMap { member in + guard let decl = member.syntax.as(DeclSyntax.self) else { return nil } + return MemberBlockItemSyntax(decl: decl, trailingTrivia: .newline) + } + ), + rightBrace: .rightBraceToken(leadingTrivia: .newline) + ) + + return ProtocolDeclSyntax( + protocolKeyword: protocolKeyword, + name: identifier, + primaryAssociatedTypeClause: nil, + inheritanceClause: inheritanceClause, + genericWhereClause: nil, + memberBlock: memberBlock + ) + } +} \ No newline at end of file diff --git a/Tests/SyntaxKitTests/ClassAndProtocolTests.swift b/Tests/SyntaxKitTests/ClassAndProtocolTests.swift new file mode 100644 index 0000000..67dd362 --- /dev/null +++ b/Tests/SyntaxKitTests/ClassAndProtocolTests.swift @@ -0,0 +1,47 @@ +import Testing + +@testable import SyntaxKit + +struct ClassAndProtocolTests { + @Test func testSimpleProtocol() { + let vehicleProtocol = Protocol("Vehicle") { + PropertyRequirement("numberOfWheels", type: "Int", access: .get) + PropertyRequirement("brand", type: "String", access: .getSet) + FunctionRequirement("start") + FunctionRequirement("stop") + FunctionRequirement("speed", returns: "Int") + } + + let expected = """ + protocol Vehicle { + var numberOfWheels: Int { get } + var brand: String { get set } + func start() + func stop() + func speed() -> Int + } + """ + + let normalizedGenerated = vehicleProtocol.generateCode().normalize() + let normalizedExpected = expected.normalize() + #expect(normalizedGenerated == normalizedExpected) + } + + @Test func testClassWithInheritance() { + let carClass = Class("Car") { + Variable(.var, name: "brand", type: "String") + Variable(.var, name: "numberOfWheels", type: "Int") + }.inherits("Vehicle") + + let expected = """ + class Car: Vehicle { + var brand: String + var numberOfWheels: Int + } + """ + + let normalizedGenerated = carClass.generateCode().normalize() + let normalizedExpected = expected.normalize() + #expect(normalizedGenerated == normalizedExpected) + } +} \ No newline at end of file From 51dcb41504ae5cc9a8405e22d2da96f93bf1e871 Mon Sep 17 00:00:00 2001 From: Leo Dion Date: Wed, 18 Jun 2025 13:08:08 -0400 Subject: [PATCH 2/3] linting files --- Sources/SyntaxKit/Class.swift | 2 +- Sources/SyntaxKit/Documentation.docc/Documentation.md | 8 ++++++++ Sources/SyntaxKit/FunctionRequirement.swift | 2 +- Sources/SyntaxKit/PropertyRequirement.swift | 4 ++-- Sources/SyntaxKit/Protocol.swift | 2 +- Tests/SyntaxKitTests/ClassAndProtocolTests.swift | 2 +- 6 files changed, 14 insertions(+), 6 deletions(-) diff --git a/Sources/SyntaxKit/Class.swift b/Sources/SyntaxKit/Class.swift index 0967e3e..36d702c 100644 --- a/Sources/SyntaxKit/Class.swift +++ b/Sources/SyntaxKit/Class.swift @@ -147,4 +147,4 @@ public struct Class: CodeBlock { memberBlock: memberBlock ) } -} \ No newline at end of file +} diff --git a/Sources/SyntaxKit/Documentation.docc/Documentation.md b/Sources/SyntaxKit/Documentation.docc/Documentation.md index 091cbee..cb4a2ca 100644 --- a/Sources/SyntaxKit/Documentation.docc/Documentation.md +++ b/Sources/SyntaxKit/Documentation.docc/Documentation.md @@ -198,6 +198,14 @@ struct BlackjackCard { - ``VariableDecl`` - ``Let`` - ``Variable`` +- ``Extension`` +- ``Class`` +- ``Protocol`` +- ``Tuple`` +- ``TypeAlias`` +- ``Infix`` +- ``PropertyRequirement`` +- ``FunctionRequirement`` ### Expressions & Statements - ``Assignment`` diff --git a/Sources/SyntaxKit/FunctionRequirement.swift b/Sources/SyntaxKit/FunctionRequirement.swift index f978673..3faf7ae 100644 --- a/Sources/SyntaxKit/FunctionRequirement.swift +++ b/Sources/SyntaxKit/FunctionRequirement.swift @@ -144,4 +144,4 @@ public struct FunctionRequirement: CodeBlock { body: nil ) } -} \ No newline at end of file +} diff --git a/Sources/SyntaxKit/PropertyRequirement.swift b/Sources/SyntaxKit/PropertyRequirement.swift index b6642b0..0a41fb1 100644 --- a/Sources/SyntaxKit/PropertyRequirement.swift +++ b/Sources/SyntaxKit/PropertyRequirement.swift @@ -77,7 +77,7 @@ public struct PropertyRequirement: CodeBlock { ), AccessorDeclSyntax( accessorSpecifier: .keyword(.set, trailingTrivia: .space) - ) + ), ]) } }() @@ -99,4 +99,4 @@ public struct PropertyRequirement: CodeBlock { ]) ) } -} \ No newline at end of file +} diff --git a/Sources/SyntaxKit/Protocol.swift b/Sources/SyntaxKit/Protocol.swift index e405b7c..71945a8 100644 --- a/Sources/SyntaxKit/Protocol.swift +++ b/Sources/SyntaxKit/Protocol.swift @@ -101,4 +101,4 @@ public struct Protocol: CodeBlock { memberBlock: memberBlock ) } -} \ No newline at end of file +} diff --git a/Tests/SyntaxKitTests/ClassAndProtocolTests.swift b/Tests/SyntaxKitTests/ClassAndProtocolTests.swift index 67dd362..895b055 100644 --- a/Tests/SyntaxKitTests/ClassAndProtocolTests.swift +++ b/Tests/SyntaxKitTests/ClassAndProtocolTests.swift @@ -44,4 +44,4 @@ struct ClassAndProtocolTests { let normalizedExpected = expected.normalize() #expect(normalizedGenerated == normalizedExpected) } -} \ No newline at end of file +} From 209bffb9cb8862465edebd4b5c4840d0bf008d0c Mon Sep 17 00:00:00 2001 From: "codecov-ai[bot]" <156709835+codecov-ai[bot]@users.noreply.github.com> Date: Wed, 18 Jun 2025 13:43:57 -0400 Subject: [PATCH 3/3] Add Tests for PR#69 (#71) --- .../ClassAndProtocolTests.swift | 47 ----- Tests/SyntaxKitTests/ClassTests.swift | 159 +++++++++++++++ Tests/SyntaxKitTests/ProtocolTests.swift | 189 ++++++++++++++++++ 3 files changed, 348 insertions(+), 47 deletions(-) delete mode 100644 Tests/SyntaxKitTests/ClassAndProtocolTests.swift create mode 100644 Tests/SyntaxKitTests/ClassTests.swift create mode 100644 Tests/SyntaxKitTests/ProtocolTests.swift diff --git a/Tests/SyntaxKitTests/ClassAndProtocolTests.swift b/Tests/SyntaxKitTests/ClassAndProtocolTests.swift deleted file mode 100644 index 895b055..0000000 --- a/Tests/SyntaxKitTests/ClassAndProtocolTests.swift +++ /dev/null @@ -1,47 +0,0 @@ -import Testing - -@testable import SyntaxKit - -struct ClassAndProtocolTests { - @Test func testSimpleProtocol() { - let vehicleProtocol = Protocol("Vehicle") { - PropertyRequirement("numberOfWheels", type: "Int", access: .get) - PropertyRequirement("brand", type: "String", access: .getSet) - FunctionRequirement("start") - FunctionRequirement("stop") - FunctionRequirement("speed", returns: "Int") - } - - let expected = """ - protocol Vehicle { - var numberOfWheels: Int { get } - var brand: String { get set } - func start() - func stop() - func speed() -> Int - } - """ - - let normalizedGenerated = vehicleProtocol.generateCode().normalize() - let normalizedExpected = expected.normalize() - #expect(normalizedGenerated == normalizedExpected) - } - - @Test func testClassWithInheritance() { - let carClass = Class("Car") { - Variable(.var, name: "brand", type: "String") - Variable(.var, name: "numberOfWheels", type: "Int") - }.inherits("Vehicle") - - let expected = """ - class Car: Vehicle { - var brand: String - var numberOfWheels: Int - } - """ - - let normalizedGenerated = carClass.generateCode().normalize() - let normalizedExpected = expected.normalize() - #expect(normalizedGenerated == normalizedExpected) - } -} diff --git a/Tests/SyntaxKitTests/ClassTests.swift b/Tests/SyntaxKitTests/ClassTests.swift new file mode 100644 index 0000000..f9c3836 --- /dev/null +++ b/Tests/SyntaxKitTests/ClassTests.swift @@ -0,0 +1,159 @@ +import Testing + +@testable import SyntaxKit + +struct ClassTests { + @Test func testClassWithInheritance() { + let carClass = Class("Car") { + Variable(.var, name: "brand", type: "String") + Variable(.var, name: "numberOfWheels", type: "Int") + }.inherits("Vehicle") + + let expected = """ + class Car: Vehicle { + var brand: String + var numberOfWheels: Int + } + """ + + let normalizedGenerated = carClass.generateCode().normalize() + let normalizedExpected = expected.normalize() + #expect(normalizedGenerated == normalizedExpected) + } + + @Test func testEmptyClass() { + let emptyClass = Class("EmptyClass") {} + + let expected = """ + class EmptyClass { + } + """ + + let normalizedGenerated = emptyClass.generateCode().normalize() + let normalizedExpected = expected.normalize() + #expect(normalizedGenerated == normalizedExpected) + } + + @Test func testClassWithGenerics() { + let genericClass = Class("Container", generics: ["T"]) { + Variable(.var, name: "value", type: "T") + } + + let expected = """ + class Container { + var value: T + } + """ + + let normalizedGenerated = genericClass.generateCode().normalize() + let normalizedExpected = expected.normalize() + #expect(normalizedGenerated == normalizedExpected) + } + + @Test func testClassWithMultipleGenerics() { + let multiGenericClass = Class("Pair", generics: ["T", "U"]) { + Variable(.var, name: "first", type: "T") + Variable(.var, name: "second", type: "U") + } + + let expected = """ + class Pair { + var first: T + var second: U + } + """ + + let normalizedGenerated = multiGenericClass.generateCode().normalize() + let normalizedExpected = expected.normalize() + #expect(normalizedGenerated == normalizedExpected) + } + + @Test func testFinalClass() { + let finalClass = Class("FinalClass") { + Variable(.var, name: "value", type: "String") + }.final() + + let expected = """ + final class FinalClass { + var value: String + } + """ + + let normalizedGenerated = finalClass.generateCode().normalize() + let normalizedExpected = expected.normalize() + #expect(normalizedGenerated == normalizedExpected) + } + + @Test func testClassWithMultipleInheritance() { + let classWithMultipleInheritance = Class("AdvancedVehicle") { + Variable(.var, name: "speed", type: "Int") + }.inherits("Vehicle", "Codable", "Equatable") + + let expected = """ + class AdvancedVehicle: Vehicle, Codable, Equatable { + var speed: Int + } + """ + + let normalizedGenerated = classWithMultipleInheritance.generateCode().normalize() + let normalizedExpected = expected.normalize() + #expect(normalizedGenerated == normalizedExpected) + } + + @Test func testClassWithGenericsAndInheritance() { + let genericClassWithInheritance = Class("GenericContainer", generics: ["T"]) { + Variable(.var, name: "items", type: "[T]") + }.inherits("Collection") + + let expected = """ + class GenericContainer: Collection { + var items: [T] + } + """ + + let normalizedGenerated = genericClassWithInheritance.generateCode().normalize() + let normalizedExpected = expected.normalize() + #expect(normalizedGenerated == normalizedExpected) + } + + @Test func testFinalClassWithInheritanceAndGenerics() { + let finalGenericClass = Class("FinalGenericClass", generics: ["T"]) { + Variable(.var, name: "value", type: "T") + }.inherits("BaseClass").final() + + let expected = """ + final class FinalGenericClass: BaseClass { + var value: T + } + """ + + let normalizedGenerated = finalGenericClass.generateCode().normalize() + let normalizedExpected = expected.normalize() + #expect(normalizedGenerated == normalizedExpected) + } + + @Test func testClassWithFunctions() { + let classWithFunctions = Class("Calculator") { + Function("add", returns: "Int") { + Parameter(name: "a", type: "Int") + Parameter(name: "b", type: "Int") + } _: { + Return { + VariableExp("a + b") + } + } + } + + let expected = """ + class Calculator { + func add(a: Int, b: Int) -> Int { + return a + b + } + } + """ + + let normalizedGenerated = classWithFunctions.generateCode().normalize() + let normalizedExpected = expected.normalize() + #expect(normalizedGenerated == normalizedExpected) + } +} diff --git a/Tests/SyntaxKitTests/ProtocolTests.swift b/Tests/SyntaxKitTests/ProtocolTests.swift new file mode 100644 index 0000000..31a3c55 --- /dev/null +++ b/Tests/SyntaxKitTests/ProtocolTests.swift @@ -0,0 +1,189 @@ +import Testing + +@testable import SyntaxKit + +struct ProtocolTests { + @Test func testSimpleProtocol() { + let vehicleProtocol = Protocol("Vehicle") { + PropertyRequirement("numberOfWheels", type: "Int", access: .get) + PropertyRequirement("brand", type: "String", access: .getSet) + FunctionRequirement("start") + FunctionRequirement("stop") + FunctionRequirement("speed", returns: "Int") + } + + let expected = """ + protocol Vehicle { + var numberOfWheels: Int { get } + var brand: String { get set } + func start() + func stop() + func speed() -> Int + } + """ + + let normalizedGenerated = vehicleProtocol.generateCode().normalize() + let normalizedExpected = expected.normalize() + #expect(normalizedGenerated == normalizedExpected) + } + + @Test func testEmptyProtocol() { + let emptyProtocol = Protocol("EmptyProtocol") {} + + let expected = """ + protocol EmptyProtocol { + } + """ + + let normalizedGenerated = emptyProtocol.generateCode().normalize() + let normalizedExpected = expected.normalize() + #expect(normalizedGenerated == normalizedExpected) + } + + @Test func testProtocolWithInheritance() { + let protocolWithInheritance = Protocol("MyProtocol") { + PropertyRequirement("value", type: "String", access: .getSet) + }.inherits("Equatable", "Hashable") + + let expected = """ + protocol MyProtocol: Equatable, Hashable { + var value: String { get set } + } + """ + + let normalizedGenerated = protocolWithInheritance.generateCode().normalize() + let normalizedExpected = expected.normalize() + #expect(normalizedGenerated == normalizedExpected) + } + + @Test func testFunctionRequirementWithParameters() { + let protocolWithFunction = Protocol("Calculator") { + FunctionRequirement("add", returns: "Int") { + Parameter(name: "a", type: "Int") + Parameter(name: "b", type: "Int") + } + } + + let expected = """ + protocol Calculator { + func add(a: Int, b: Int) -> Int + } + """ + + let normalizedGenerated = protocolWithFunction.generateCode().normalize() + let normalizedExpected = expected.normalize() + #expect(normalizedGenerated == normalizedExpected) + } + + @Test func testStaticFunctionRequirement() { + let protocolWithStaticFunction = Protocol("Factory") { + FunctionRequirement("create", returns: "Self").static() + } + + let expected = """ + protocol Factory { + static func create() -> Self + } + """ + + let normalizedGenerated = protocolWithStaticFunction.generateCode().normalize() + let normalizedExpected = expected.normalize() + #expect(normalizedGenerated == normalizedExpected) + } + + @Test func testMutatingFunctionRequirement() { + let protocolWithMutatingFunction = Protocol("Resettable") { + FunctionRequirement("reset").mutating() + } + + let expected = """ + protocol Resettable { + mutating func reset() + } + """ + + let normalizedGenerated = protocolWithMutatingFunction.generateCode().normalize() + let normalizedExpected = expected.normalize() + #expect(normalizedGenerated == normalizedExpected) + } + + @Test func testPropertyRequirementGetOnly() { + let propertyReq = PropertyRequirement("readOnlyProperty", type: "String", access: .get) + let prtcl = Protocol("TestProtocol") { + propertyReq + } + + let expected = """ + protocol TestProtocol { + var readOnlyProperty: String { get } + } + """ + + let normalizedGenerated = prtcl.generateCode().normalize() + let normalizedExpected = expected.normalize() + #expect(normalizedGenerated == normalizedExpected) + } + + @Test func testPropertyRequirementGetSet() { + let propertyReq = PropertyRequirement("readWriteProperty", type: "Int", access: .getSet) + let prtcl = Protocol("TestProtocol") { + propertyReq + } + + let expected = """ + protocol TestProtocol { + var readWriteProperty: Int { get set } + } + """ + + let normalizedGenerated = prtcl.generateCode().normalize() + let normalizedExpected = expected.normalize() + #expect(normalizedGenerated == normalizedExpected) + } + + @Test func testFunctionRequirementWithDefaultParameters() { + let functionReq = FunctionRequirement("process", returns: "String") { + Parameter(name: "input", type: "String") + Parameter(name: "options", type: "ProcessingOptions", defaultValue: "ProcessingOptions()") + } + let prtcl = Protocol("TestProtocol") { + functionReq + } + + let expected = """ + protocol TestProtocol { + func process(input: String, options: ProcessingOptions = ProcessingOptions()) -> String + } + """ + + let normalizedGenerated = prtcl.generateCode().normalize() + let normalizedExpected = expected.normalize() + #expect(normalizedGenerated == normalizedExpected) + } + + @Test func testComplexProtocolWithMixedRequirements() { + let complexProtocol = Protocol("ComplexProtocol") { + PropertyRequirement("id", type: "UUID", access: .get) + PropertyRequirement("name", type: "String", access: .getSet) + FunctionRequirement("initialize").mutating() + FunctionRequirement("process", returns: "Result") { + Parameter(name: "input", type: "Data") + } + FunctionRequirement("factory", returns: "Self").static() + }.inherits("Identifiable") + + let expected = """ + protocol ComplexProtocol: Identifiable { + var id: UUID { get } + var name: String { get set } + mutating func initialize() + func process(input: Data) -> Result + static func factory() -> Self + } + """ + + let normalizedGenerated = complexProtocol.generateCode().normalize() + let normalizedExpected = expected.normalize() + #expect(normalizedGenerated == normalizedExpected) + } +}