From 7e52a1dd0100640f80092be42d4616924d4fd905 Mon Sep 17 00:00:00 2001 From: leogdion Date: Thu, 2 Apr 2026 08:16:38 -0400 Subject: [PATCH 1/2] V1.0.0 beta.1 (#11) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit feat: Swift App Template v1.0.0-beta.1 (#11) - Copy and restructure MonthBar infrastructure into template layout - Replace all hardcoded values with `{{TEMPLATE_*}}` placeholder tokens - Add `Packages/TemplateGenerator` — Swift executable that generates app skeleton files using SyntaxKit - Add `Scripts/setup.sh` — interactive/automated setup with 19 token substitutions, Swift file generation, CI activation, full-stack mode, cleanup, and git reset - Add `README.md`, `README.template.md`, and `CLAUDE.template.md` template documentation - Move `workflows/app.yml` → `.github/app.workflow.yml` so GitHub skips parsing it until `setup.sh` activates it - Add `.github/workflows/validate-template.yml` — CI that confirms placeholder tokens haven't been replaced on the template repo - Replace placeholder-specific icon assets with a generic gradient placeholder - Sync `_reference/MonthBar` to latest upstream - Add `_reference/AtLeast` — iOS/watchOS companion app reference with multi-platform Fastlane and dynamic CI matrix - Extract `private_lane :setup_api_key` in Fastfile; add `sync_build_number`, `sync_last_release`, `upload_metadata`, `upload_privacy_details`, `submit_for_review` lanes - Add `pull_request` trigger, concurrency group, and dynamic matrix to CI workflow - Split `build-macos` into always-run and full-matrix jobs; gate Windows/Android on cross-platform condition - Add conditional server test jobs wrapped in `FULL_STACK_ONLY` markers for `setup.sh` processing - Bump actions: `checkout@v6`, `swift-build@v1.5.2`, `swift-coverage-action@v5`, `codecov-action@v6`, `mise-action@v4` - Prefix all Makefile `fastlane` calls with `mise exec --`; add screenshot targets - Expand CLAUDE.md Commands and Code Signing sections with new Fastlane lanes and make targets - Add automation guide TODO tracker linked to issues #28–#50 - Fix `bundle install` and git section in `setup.sh`; harden with `printf -v`, required-field validation, and `swift` availability check - Replace `keywords.txt` with lorem ipsum placeholder to force users to update before App Store submission - Bump Dockerfile base image from `swift:6.0-jammy` to `swift:6.3-noble` - Add `client` to `openapi-generator-config.yaml` generate list - Remove LFS exclusion section from `.gitattributes` --------- Co-authored-by: Claude Opus 4.6 --- Sources/SyntaxKit/Declarations/Class.swift | 24 +++- Sources/SyntaxKit/Declarations/Enum.swift | 5 +- .../SyntaxKit/Declarations/Extension.swift | 5 +- .../SyntaxKit/Declarations/IfCanImport.swift | 81 ++++++++++++ Sources/SyntaxKit/Declarations/Import.swift | 5 +- .../SyntaxKit/Declarations/Initializer.swift | 121 ++++++++++++++++++ Sources/SyntaxKit/Declarations/Protocol.swift | 5 +- Sources/SyntaxKit/Declarations/Struct.swift | 5 +- .../Functions/Function+Modifiers.swift | 15 +++ .../SyntaxKit/Functions/Function+Syntax.swift | 12 +- Sources/SyntaxKit/Functions/Function.swift | 1 + .../Utilities/AttributeArgument.swift | 51 ++++++++ .../Variables/Variable+Attributes.swift | 5 +- .../Unit/Attributes/AttributeTests.swift | 33 +++++ .../Unit/Declarations/ClassTests.swift | 45 +++++++ .../Unit/Declarations/ImportTests.swift | 31 +++++ .../Unit/Declarations/InitializerTests.swift | 74 +++++++++++ .../Unit/Functions/FunctionTests.swift | 45 +++++++ 18 files changed, 544 insertions(+), 19 deletions(-) create mode 100644 Sources/SyntaxKit/Declarations/IfCanImport.swift create mode 100644 Sources/SyntaxKit/Declarations/Initializer.swift create mode 100644 Sources/SyntaxKit/Utilities/AttributeArgument.swift create mode 100644 Tests/SyntaxKitTests/Unit/Declarations/ImportTests.swift create mode 100644 Tests/SyntaxKitTests/Unit/Declarations/InitializerTests.swift diff --git a/Sources/SyntaxKit/Declarations/Class.swift b/Sources/SyntaxKit/Declarations/Class.swift index 2b6c03f..f20e577 100644 --- a/Sources/SyntaxKit/Declarations/Class.swift +++ b/Sources/SyntaxKit/Declarations/Class.swift @@ -37,6 +37,7 @@ public struct Class: CodeBlock, Sendable { private var genericParameters: [String] = [] private var isFinal: Bool = false private var attributes: [AttributeInfo] = [] + private var accessModifier: AccessModifier? /// The SwiftSyntax representation of this class declaration. public var syntax: any SyntaxProtocol { @@ -107,11 +108,16 @@ public struct Class: CodeBlock, Sendable { // Modifiers var modifiers: DeclModifierListSyntax = [] - if isFinal { + if let access = accessModifier { modifiers = DeclModifierListSyntax([ - DeclModifierSyntax(name: .keyword(.final, trailingTrivia: .space)) + DeclModifierSyntax(name: .keyword(access.keyword, trailingTrivia: .space)) ]) } + if isFinal { + modifiers = DeclModifierListSyntax( + modifiers + [DeclModifierSyntax(name: .keyword(.final, trailingTrivia: .space))] + ) + } return ClassDeclSyntax( attributes: attributeList, @@ -161,6 +167,15 @@ public struct Class: CodeBlock, Sendable { return copy } + /// Sets the access modifier for the class declaration. + /// - Parameter access: The access modifier. + /// - Returns: A copy of the class with the access modifier set. + public func access(_ access: AccessModifier) -> Self { + var copy = self + copy.accessModifier = access + return copy + } + /// Adds an attribute to the class declaration. /// - Parameters: /// - attribute: The attribute name (without the @ symbol). @@ -189,13 +204,13 @@ public struct Class: CodeBlock, Sendable { rightParen = .rightParenToken() let argumentList = arguments.map { argument in - DeclReferenceExprSyntax(baseName: .identifier(argument)) + buildAttributeArgumentExpr(from: argument) } argumentsSyntax = .argumentList( LabeledExprListSyntax( argumentList.enumerated().map { index, expr in - var element = LabeledExprSyntax(expression: ExprSyntax(expr)) + var element = LabeledExprSyntax(expression: expr) if index < argumentList.count - 1 { element = element.with(\.trailingComma, .commaToken(trailingTrivia: .space)) } @@ -213,6 +228,7 @@ public struct Class: CodeBlock, Sendable { arguments: argumentsSyntax, rightParen: rightParen ) + .with(\.trailingTrivia, .newline) ) } diff --git a/Sources/SyntaxKit/Declarations/Enum.swift b/Sources/SyntaxKit/Declarations/Enum.swift index d915fd1..a633a63 100644 --- a/Sources/SyntaxKit/Declarations/Enum.swift +++ b/Sources/SyntaxKit/Declarations/Enum.swift @@ -134,13 +134,13 @@ public struct Enum: CodeBlock, Sendable { rightParen = .rightParenToken() let argumentList = arguments.map { argument in - DeclReferenceExprSyntax(baseName: .identifier(argument)) + buildAttributeArgumentExpr(from: argument) } argumentsSyntax = .argumentList( LabeledExprListSyntax( argumentList.enumerated().map { index, expr in - var element = LabeledExprSyntax(expression: ExprSyntax(expr)) + var element = LabeledExprSyntax(expression: expr) if index < argumentList.count - 1 { element = element.with(\.trailingComma, .commaToken(trailingTrivia: .space)) } @@ -158,6 +158,7 @@ public struct Enum: CodeBlock, Sendable { arguments: argumentsSyntax, rightParen: rightParen ) + .with(\.trailingTrivia, .newline) ) } return AttributeListSyntax(attributeElements) diff --git a/Sources/SyntaxKit/Declarations/Extension.swift b/Sources/SyntaxKit/Declarations/Extension.swift index fe87fb1..175d163 100644 --- a/Sources/SyntaxKit/Declarations/Extension.swift +++ b/Sources/SyntaxKit/Declarations/Extension.swift @@ -131,13 +131,13 @@ public struct Extension: CodeBlock, Sendable { rightParen = .rightParenToken() let argumentList = arguments.map { argument in - DeclReferenceExprSyntax(baseName: .identifier(argument)) + buildAttributeArgumentExpr(from: argument) } argumentsSyntax = .argumentList( LabeledExprListSyntax( argumentList.enumerated().map { index, expr in - var element = LabeledExprSyntax(expression: ExprSyntax(expr)) + var element = LabeledExprSyntax(expression: expr) if index < argumentList.count - 1 { element = element.with(\.trailingComma, .commaToken(trailingTrivia: .space)) } @@ -155,6 +155,7 @@ public struct Extension: CodeBlock, Sendable { arguments: argumentsSyntax, rightParen: rightParen ) + .with(\.trailingTrivia, .newline) ) } return AttributeListSyntax(attributeElements) diff --git a/Sources/SyntaxKit/Declarations/IfCanImport.swift b/Sources/SyntaxKit/Declarations/IfCanImport.swift new file mode 100644 index 0000000..0eecd77 --- /dev/null +++ b/Sources/SyntaxKit/Declarations/IfCanImport.swift @@ -0,0 +1,81 @@ +// +// IfCanImport.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. +// + +public import SwiftSyntax + +/// A `#if canImport(Module)` … `#endif` conditional compilation block. +public struct IfCanImport: CodeBlock, Sendable { + private let moduleName: String + private let content: [any CodeBlock] + + /// Creates a `#if canImport(moduleName)` block wrapping the given content. + /// - Parameters: + /// - moduleName: The module name passed to `canImport`. + /// - content: The code blocks to emit when the module is available. + public init(_ moduleName: String, @CodeBlockBuilderResult _ content: () -> [any CodeBlock]) { + self.moduleName = moduleName + self.content = content() + } + + public var syntax: any SyntaxProtocol { + let condition = FunctionCallExprSyntax( + calledExpression: ExprSyntax(DeclReferenceExprSyntax( + baseName: .identifier("canImport") + )), + leftParen: .leftParenToken(), + arguments: LabeledExprListSyntax([ + LabeledExprSyntax( + expression: ExprSyntax(DeclReferenceExprSyntax( + baseName: .identifier(moduleName) + )) + ) + ]), + rightParen: .rightParenToken() + ) + + let items = CodeBlockItemListSyntax( + content.compactMap { block -> CodeBlockItemSyntax? in + CodeBlockItemSyntax.Item.create(from: block.syntax).map { + CodeBlockItemSyntax(item: $0, trailingTrivia: .newline) + } + } + ) + + let clause = IfConfigClauseSyntax( + poundKeyword: .poundIfToken(trailingTrivia: .space), + condition: ExprSyntax(condition).with(\.trailingTrivia, .newline), + elements: .statements(items) + ) + + return IfConfigDeclSyntax( + clauses: IfConfigClauseListSyntax([clause]), + poundEndif: .poundEndifToken(leadingTrivia: .newline) + ) + } +} diff --git a/Sources/SyntaxKit/Declarations/Import.swift b/Sources/SyntaxKit/Declarations/Import.swift index 6443de5..f452b58 100644 --- a/Sources/SyntaxKit/Declarations/Import.swift +++ b/Sources/SyntaxKit/Declarations/Import.swift @@ -101,13 +101,13 @@ public struct Import: CodeBlock, Sendable { rightParen = .rightParenToken() let argumentList = arguments.map { argument in - DeclReferenceExprSyntax(baseName: .identifier(argument)) + buildAttributeArgumentExpr(from: argument) } argumentsSyntax = .argumentList( LabeledExprListSyntax( argumentList.enumerated().map { index, expr in - var element = LabeledExprSyntax(expression: ExprSyntax(expr)) + var element = LabeledExprSyntax(expression: expr) if index < argumentList.count - 1 { element = element.with(\.trailingComma, .commaToken(trailingTrivia: .space)) } @@ -125,6 +125,7 @@ public struct Import: CodeBlock, Sendable { arguments: argumentsSyntax, rightParen: rightParen ) + .with(\.trailingTrivia, .space) ) } return AttributeListSyntax(attributeElements) diff --git a/Sources/SyntaxKit/Declarations/Initializer.swift b/Sources/SyntaxKit/Declarations/Initializer.swift new file mode 100644 index 0000000..8fd34cf --- /dev/null +++ b/Sources/SyntaxKit/Declarations/Initializer.swift @@ -0,0 +1,121 @@ +// +// Initializer.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. +// + +public import SwiftSyntax + +/// A Swift `init` declaration. +public struct Initializer: CodeBlock, Sendable { + private let body: [any CodeBlock] + private var accessModifier: AccessModifier? + private var isAsync: Bool = false + private var isThrowing: Bool = false + + /// The SwiftSyntax representation of this initializer declaration. + public var syntax: any SyntaxProtocol { + var modifiers: DeclModifierListSyntax = [] + if let access = accessModifier { + modifiers = DeclModifierListSyntax([ + DeclModifierSyntax(name: .keyword(access.keyword, trailingTrivia: .space)) + ]) + } + + var effectSpecifiers: FunctionEffectSpecifiersSyntax? + if isAsync || isThrowing { + effectSpecifiers = FunctionEffectSpecifiersSyntax( + asyncSpecifier: isAsync + ? .keyword(.async, leadingTrivia: .space, trailingTrivia: .space) + : nil, + throwsSpecifier: isThrowing ? .keyword(.throws, leadingTrivia: .space) : nil + ) + } + + let bodyBlock = CodeBlockSyntax( + leftBrace: .leftBraceToken(leadingTrivia: .space, trailingTrivia: .newline), + statements: CodeBlockItemListSyntax( + body.compactMap { item in + var codeBlockItem: CodeBlockItemSyntax? + if let decl = item.syntax.as(DeclSyntax.self) { + codeBlockItem = CodeBlockItemSyntax(item: .decl(decl)) + } else if let expr = item.syntax.as(ExprSyntax.self) { + codeBlockItem = CodeBlockItemSyntax(item: .expr(expr)) + } else if let stmt = item.syntax.as(StmtSyntax.self) { + codeBlockItem = CodeBlockItemSyntax(item: .stmt(stmt)) + } + return codeBlockItem?.with(\.trailingTrivia, .newline) + } + ), + rightBrace: .rightBraceToken(leadingTrivia: .newline) + ) + + return InitializerDeclSyntax( + modifiers: modifiers, + initKeyword: .keyword(.`init`), + signature: FunctionSignatureSyntax( + parameterClause: FunctionParameterClauseSyntax( + leftParen: .leftParenToken(), + parameters: FunctionParameterListSyntax([]), + rightParen: .rightParenToken() + ), + effectSpecifiers: effectSpecifiers + ), + body: bodyBlock + ) + } + + /// Creates an `init` declaration. + /// - Parameter content: A ``CodeBlockBuilder`` that provides the body of the initializer. + public init(@CodeBlockBuilderResult _ content: () throws -> [any CodeBlock]) rethrows { + self.body = try content() + } + + /// Sets the access modifier for the initializer declaration. + /// - Parameter access: The access modifier. + /// - Returns: A copy of the initializer with the access modifier set. + public func access(_ access: AccessModifier) -> Self { + var copy = self + copy.accessModifier = access + return copy + } + + /// Marks the initializer as `throws`. + /// - Returns: A copy of the initializer marked as `throws`. + public func throwing() -> Self { + var copy = self + copy.isThrowing = true + return copy + } + + /// Marks the initializer as `async`. + /// - Returns: A copy of the initializer marked as `async`. + public func async() -> Self { + var copy = self + copy.isAsync = true + return copy + } +} diff --git a/Sources/SyntaxKit/Declarations/Protocol.swift b/Sources/SyntaxKit/Declarations/Protocol.swift index db19c7a..dffec6c 100644 --- a/Sources/SyntaxKit/Declarations/Protocol.swift +++ b/Sources/SyntaxKit/Declarations/Protocol.swift @@ -136,13 +136,13 @@ public struct Protocol: CodeBlock, Sendable { rightParen = .rightParenToken() let argumentList = arguments.map { argument in - DeclReferenceExprSyntax(baseName: .identifier(argument)) + buildAttributeArgumentExpr(from: argument) } argumentsSyntax = .argumentList( LabeledExprListSyntax( argumentList.enumerated().map { index, expr in - var element = LabeledExprSyntax(expression: ExprSyntax(expr)) + var element = LabeledExprSyntax(expression: expr) if index < argumentList.count - 1 { element = element.with(\.trailingComma, .commaToken(trailingTrivia: .space)) } @@ -160,6 +160,7 @@ public struct Protocol: CodeBlock, Sendable { arguments: argumentsSyntax, rightParen: rightParen ) + .with(\.trailingTrivia, .newline) ) } return AttributeListSyntax(attributeElements) diff --git a/Sources/SyntaxKit/Declarations/Struct.swift b/Sources/SyntaxKit/Declarations/Struct.swift index caaec04..ec9fdee 100644 --- a/Sources/SyntaxKit/Declarations/Struct.swift +++ b/Sources/SyntaxKit/Declarations/Struct.swift @@ -70,13 +70,13 @@ public struct Struct: CodeBlock, Sendable { rightParen = .rightParenToken() let argumentList = arguments.map { argument in - DeclReferenceExprSyntax(baseName: .identifier(argument)) + buildAttributeArgumentExpr(from: argument) } argumentsSyntax = .argumentList( LabeledExprListSyntax( argumentList.enumerated().map { index, expr in - var element = LabeledExprSyntax(expression: ExprSyntax(expr)) + var element = LabeledExprSyntax(expression: expr) if index < argumentList.count - 1 { element = element.with(\.trailingComma, .commaToken(trailingTrivia: .space)) } @@ -94,6 +94,7 @@ public struct Struct: CodeBlock, Sendable { arguments: argumentsSyntax, rightParen: rightParen ) + .with(\.trailingTrivia, .newline) ) } return AttributeListSyntax(attributeElements) diff --git a/Sources/SyntaxKit/Functions/Function+Modifiers.swift b/Sources/SyntaxKit/Functions/Function+Modifiers.swift index f50e427..2aa4c2a 100644 --- a/Sources/SyntaxKit/Functions/Function+Modifiers.swift +++ b/Sources/SyntaxKit/Functions/Function+Modifiers.swift @@ -46,6 +46,21 @@ extension Function { return copy } + /// Sets the access modifier for the function declaration. + /// - Parameter access: The access modifier. + /// - Returns: A copy of the function with the access modifier set. + public func access(_ access: AccessModifier) -> Self { + var copy = self + copy.accessModifier = access + return copy + } + + /// Marks the function as `throws` (alias for `.throws()` that avoids keyword escaping). + /// - Returns: A copy of the function marked as `throws`. + public func throwing() -> Self { + `throws`() + } + /// Adds an attribute to the function declaration. /// - Parameters: /// - attribute: The attribute name (without the @ symbol). diff --git a/Sources/SyntaxKit/Functions/Function+Syntax.swift b/Sources/SyntaxKit/Functions/Function+Syntax.swift index e766bea..7ab9bd4 100644 --- a/Sources/SyntaxKit/Functions/Function+Syntax.swift +++ b/Sources/SyntaxKit/Functions/Function+Syntax.swift @@ -83,9 +83,14 @@ extension Function { // Build modifiers var modifiers: DeclModifierListSyntax = [] + if let access = accessModifier { + modifiers = DeclModifierListSyntax([ + DeclModifierSyntax(name: .keyword(access.keyword, trailingTrivia: .space)) + ]) + } if isStatic { modifiers = DeclModifierListSyntax( - [ + modifiers + [ DeclModifierSyntax(name: .keyword(.static, trailingTrivia: .space)) ] ) @@ -133,13 +138,13 @@ extension Function { rightParen = .rightParenToken() let argumentList = arguments.map { argument in - DeclReferenceExprSyntax(baseName: .identifier(argument)) + buildAttributeArgumentExpr(from: argument) } argumentsSyntax = .argumentList( LabeledExprListSyntax( argumentList.enumerated().map { index, expr in - var element = LabeledExprSyntax(expression: ExprSyntax(expr)) + var element = LabeledExprSyntax(expression: expr) if index < argumentList.count - 1 { element = element.with( \.trailingComma, @@ -160,6 +165,7 @@ extension Function { arguments: argumentsSyntax, rightParen: rightParen ) + .with(\.trailingTrivia, .newline) ) } diff --git a/Sources/SyntaxKit/Functions/Function.swift b/Sources/SyntaxKit/Functions/Function.swift index dabae9f..9c9100b 100644 --- a/Sources/SyntaxKit/Functions/Function.swift +++ b/Sources/SyntaxKit/Functions/Function.swift @@ -39,6 +39,7 @@ public struct Function: CodeBlock { internal var isMutating: Bool = false internal var effect: Effect = .none internal var attributes: [AttributeInfo] = [] + internal var accessModifier: AccessModifier? /// Creates a `func` declaration. /// - Parameters: diff --git a/Sources/SyntaxKit/Utilities/AttributeArgument.swift b/Sources/SyntaxKit/Utilities/AttributeArgument.swift new file mode 100644 index 0000000..b940b45 --- /dev/null +++ b/Sources/SyntaxKit/Utilities/AttributeArgument.swift @@ -0,0 +1,51 @@ +// +// AttributeArgument.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 + +/// Builds an expression syntax node from an attribute argument string. +/// - Parameter argument: If surrounded by double-quotes, produces a string literal expression; +/// otherwise produces an identifier reference expression. +/// - Returns: An `ExprSyntax` representing the argument. +internal func buildAttributeArgumentExpr(from argument: String) -> ExprSyntax { + if argument.hasPrefix("\"") && argument.hasSuffix("\"") && argument.count >= 2 { + let content = String(argument.dropFirst().dropLast()) + return ExprSyntax( + StringLiteralExprSyntax( + openingQuote: .stringQuoteToken(), + segments: StringLiteralSegmentListSyntax([ + .stringSegment(StringSegmentSyntax(content: .stringSegment(content))) + ]), + closingQuote: .stringQuoteToken() + ) + ) + } else { + return ExprSyntax(DeclReferenceExprSyntax(baseName: .identifier(argument))) + } +} diff --git a/Sources/SyntaxKit/Variables/Variable+Attributes.swift b/Sources/SyntaxKit/Variables/Variable+Attributes.swift index 6f4f954..bc2511d 100644 --- a/Sources/SyntaxKit/Variables/Variable+Attributes.swift +++ b/Sources/SyntaxKit/Variables/Variable+Attributes.swift @@ -54,6 +54,7 @@ extension Variable { arguments: attributeArgs.arguments, rightParen: attributeArgs.rightParen ) + .with(\.trailingTrivia, .space) ) } @@ -67,13 +68,13 @@ extension Variable { let rightParen: TokenSyntax = .rightParenToken() let argumentList = arguments.map { argument in - DeclReferenceExprSyntax(baseName: .identifier(argument)) + buildAttributeArgumentExpr(from: argument) } let argumentsSyntax = AttributeSyntax.Arguments.argumentList( LabeledExprListSyntax( argumentList.enumerated().map { index, expr in - var element = LabeledExprSyntax(expression: ExprSyntax(expr)) + var element = LabeledExprSyntax(expression: expr) if index < argumentList.count - 1 { element = element.with(\.trailingComma, .commaToken(trailingTrivia: .space)) } diff --git a/Tests/SyntaxKitTests/Unit/Attributes/AttributeTests.swift b/Tests/SyntaxKitTests/Unit/Attributes/AttributeTests.swift index 1d9ce81..e7d5de4 100644 --- a/Tests/SyntaxKitTests/Unit/Attributes/AttributeTests.swift +++ b/Tests/SyntaxKitTests/Unit/Attributes/AttributeTests.swift @@ -181,6 +181,39 @@ import Testing #expect(generated.contains("func process")) } + @Test("Struct with quoted string attribute argument generates string literal, not identifier") + internal func testStructWithStringLiteralAttributeArgument() throws { + // Fix 3 regression: "\"App Model\"" must produce @Suite("App Model"), not @Suite(App Model) + let structDecl = Struct("AppModelTests") {} + .attribute("Suite", arguments: ["\"App Model\""]) + + let generated = structDecl.syntax.description + #expect(generated.contains("@Suite(\"App Model\")") || generated.contains("@Suite( \"App Model\")")) + #expect(!generated.contains("@Suite(App Model)")) + } + + @Test("Function with quoted string attribute argument generates string literal") + internal func testFunctionWithStringLiteralAttributeArgument() throws { + // Fix 3 regression: quoted argument must produce string literal token + let function = Function("initialCount") {} + .attribute("Test", arguments: ["\"Initial count is zero\""]) + + let generated = function.syntax.description + // The argument should be a string literal: @Test("Initial count is zero") + #expect(generated.contains("@Test(\"Initial count is zero\")") || generated.contains("@Test( \"Initial count is zero\")")) + } + + @Test("Struct with unquoted attribute argument generates identifier, not string literal") + internal func testStructWithIdentifierAttributeArgument() throws { + // Unquoted args should remain as identifier references + let structDecl = Struct("Serve") {} + .attribute("main") + + let generated = structDecl.syntax.description + #expect(generated.contains("@main")) + #expect(generated.contains("struct Serve")) + } + @Test("Parameter with attribute arguments generates correct syntax") internal func testParameterWithAttributeArguments() throws { let function = Function("validate") { diff --git a/Tests/SyntaxKitTests/Unit/Declarations/ClassTests.swift b/Tests/SyntaxKitTests/Unit/Declarations/ClassTests.swift index ccd6573..aa3c813 100644 --- a/Tests/SyntaxKitTests/Unit/Declarations/ClassTests.swift +++ b/Tests/SyntaxKitTests/Unit/Declarations/ClassTests.swift @@ -162,6 +162,51 @@ internal struct ClassTests { #expect(normalizedGenerated == normalizedExpected) } + @Test internal func testPublicClass() { + let publicClass = Class("AppModel") {}.access(.public) + + let expected = """ + public class AppModel { + } + """ + + let normalizedGenerated = publicClass.generateCode().normalize() + let normalizedExpected = expected.normalize() + #expect(normalizedGenerated == normalizedExpected) + } + + @Test internal func testPublicFinalClass() { + let publicFinalClass = Class("AppModel") {} + .attribute("Observable") + .access(.public) + .final() + .inherits("Sendable") + + let generated = publicFinalClass.generateCode() + // Fix 2 regression: Class must support .access() + #expect(generated.contains("public")) + #expect(generated.contains("final")) + #expect(generated.contains("class AppModel")) + #expect(generated.contains("Sendable")) + // Access modifier must precede final + let publicRange = generated.range(of: "public")! + let finalRange = generated.range(of: "final")! + #expect(publicRange.lowerBound < finalRange.lowerBound) + } + + @Test internal func testInternalClass() { + let internalClass = Class("MyClass") {}.access(.internal) + + let expected = """ + internal class MyClass { + } + """ + + let normalizedGenerated = internalClass.generateCode().normalize() + let normalizedExpected = expected.normalize() + #expect(normalizedGenerated == normalizedExpected) + } + @Test internal func testClassWithFunctions() { let classWithFunctions = Class("Calculator") { Function("add", returns: "Int") { diff --git a/Tests/SyntaxKitTests/Unit/Declarations/ImportTests.swift b/Tests/SyntaxKitTests/Unit/Declarations/ImportTests.swift new file mode 100644 index 0000000..e6a9d66 --- /dev/null +++ b/Tests/SyntaxKitTests/Unit/Declarations/ImportTests.swift @@ -0,0 +1,31 @@ +import Foundation +import Testing + +@testable import SyntaxKit + +internal struct ImportTests { + @Test internal func testBasicImport() { + let importDecl = Import("Foundation") + + let generated = importDecl.generateCode() + #expect(generated.normalize() == "import Foundation") + } + + @Test internal func testImportWithTestableAttribute() { + let importDecl = Import("XCTest").attribute("testable") + + let generated = importDecl.generateCode() + // Fix 1 regression: must have a space between @testable and import + #expect(generated.contains("@testable import")) + #expect(!generated.contains("@testableimport")) + #expect(generated.contains("XCTest")) + } + + @Test internal func testImportWithGenericAttribute() { + let importDecl = Import("Foundation").attribute("_implementationOnly") + + let generated = importDecl.generateCode() + #expect(generated.contains("@_implementationOnly import")) + #expect(generated.contains("Foundation")) + } +} diff --git a/Tests/SyntaxKitTests/Unit/Declarations/InitializerTests.swift b/Tests/SyntaxKitTests/Unit/Declarations/InitializerTests.swift new file mode 100644 index 0000000..80f2f99 --- /dev/null +++ b/Tests/SyntaxKitTests/Unit/Declarations/InitializerTests.swift @@ -0,0 +1,74 @@ +import Foundation +import Testing + +@testable import SyntaxKit + +internal struct InitializerTests { + @Test internal func testEmptyInit() { + let initDecl = Initializer {} + + let expected = """ + init() { + } + """ + + let normalizedGenerated = initDecl.generateCode().normalize() + let normalizedExpected = expected.normalize() + #expect(normalizedGenerated == normalizedExpected) + } + + @Test internal func testPublicInit() { + let initDecl = Initializer {}.access(.public) + + let expected = """ + public init() { + } + """ + + let normalizedGenerated = initDecl.generateCode().normalize() + let normalizedExpected = expected.normalize() + #expect(normalizedGenerated == normalizedExpected) + } + + @Test internal func testThrowingInit() { + let initDecl = Initializer {}.throwing() + + let expected = """ + init() throws { + } + """ + + let normalizedGenerated = initDecl.generateCode().normalize() + let normalizedExpected = expected.normalize() + #expect(normalizedGenerated == normalizedExpected) + } + + @Test internal func testAsyncInit() { + let initDecl = Initializer {}.async() + + let expected = """ + init() async { + } + """ + + let normalizedGenerated = initDecl.generateCode().normalize() + let normalizedExpected = expected.normalize() + #expect(normalizedGenerated == normalizedExpected) + } + + @Test internal func testPublicInitWithBody() { + let initDecl = Initializer { + Call("setup") + }.access(.internal) + + let expected = """ + internal init() { + setup() + } + """ + + let normalizedGenerated = initDecl.generateCode().normalize() + let normalizedExpected = expected.normalize() + #expect(normalizedGenerated == normalizedExpected) + } +} diff --git a/Tests/SyntaxKitTests/Unit/Functions/FunctionTests.swift b/Tests/SyntaxKitTests/Unit/Functions/FunctionTests.swift index 4e5605a..dba1f78 100644 --- a/Tests/SyntaxKitTests/Unit/Functions/FunctionTests.swift +++ b/Tests/SyntaxKitTests/Unit/Functions/FunctionTests.swift @@ -87,6 +87,51 @@ internal struct FunctionTests { #expect(normalizedGenerated == normalizedExpected) } + @Test internal func testFunctionWithAccessModifier() throws { + let function = Function("run") { + Call("print") { + ParameterExp(unlabeled: Literal.string("hello")) + } + } + .access(.internal) + + let expected = """ + internal func run() { + print("hello") + } + """ + + let normalizedGenerated = function.syntax.description.normalize() + let normalizedExpected = expected.normalize() + #expect(normalizedGenerated == normalizedExpected) + } + + @Test internal func testFunctionThrowingAlias() throws { + // .throwing() is an alias for .throws() that avoids keyword escaping at call sites + let function = Function("load") {} + .throwing() + + let generated = function.syntax.description + #expect(generated.contains("throws")) + #expect(generated.contains("func load")) + } + + @Test internal func testAsyncThrowingFunctionWithAccess() throws { + let function = Function("run") {} + .access(.internal) + .async() + .throwing() + + let expected = """ + internal func run() async throws { + } + """ + + let normalizedGenerated = function.syntax.description.normalize() + let normalizedExpected = expected.normalize() + #expect(normalizedGenerated == normalizedExpected) + } + @Test internal func testMutatingFunction() throws { let function = Function( "updateValue", From 1fd6c9ec3804c3cb53f2457735c24a4d70fc5101 Mon Sep 17 00:00:00 2001 From: Leo Dion Date: Wed, 10 Jun 2026 21:19:46 -0400 Subject: [PATCH 2/2] Fix lint findings and lint.sh tooling after v0.0.5 rebase Resolve all swift-format and swiftlint findings in the V1.0.0 code and make Scripts/lint.sh self-recover when mise.toml is untrusted. - lint.sh: trust + install via mise before sourcing its env, so the mise-managed tools (swiftlint, periphery) land on PATH instead of silently no-op'ing on a fresh/untrusted mise.toml. - IfCanImport: add doc comment on `syntax`, order instance properties before the initializer, and flatten nested calls to satisfy multiline_arguments_brackets. - Class: split modifier methods into Class+Modifiers.swift (mirroring Function/Function+Modifiers.swift) to clear file_length; stored properties widened private -> internal to match. - ClassTests / AttributeTests: split into parent @Suite namespace enums with per-category child suites to clear file_length; replace force unwraps with try #require. swift build + 468 tests pass; lint.sh completes clean. Co-Authored-By: Claude Opus 4.8 (1M context) --- Scripts/lint.sh | 6 + .../Declarations/Class+Modifiers.swift | 76 +++++++ Sources/SyntaxKit/Declarations/Class.swift | 60 +---- .../SyntaxKit/Declarations/IfCanImport.swift | 37 ++-- .../SyntaxKit/Declarations/Initializer.swift | 6 +- .../Utilities/AttributeArgument.swift | 6 +- .../Attributes/AttributeTests+Arguments.swift | 137 ++++++++++++ .../Attributes/AttributeTests+Targets.swift | 137 ++++++++++++ .../Unit/Attributes/AttributeTests.swift | 204 +---------------- .../ClassTests+Declarations.swift | 159 ++++++++++++++ .../Declarations/ClassTests+Modifiers.swift | 114 ++++++++++ .../Unit/Declarations/ClassTests.swift | 205 +----------------- .../Unit/Declarations/ImportTests.swift | 29 +++ .../Unit/Declarations/InitializerTests.swift | 29 +++ 14 files changed, 721 insertions(+), 484 deletions(-) create mode 100644 Sources/SyntaxKit/Declarations/Class+Modifiers.swift create mode 100644 Tests/SyntaxKitTests/Unit/Attributes/AttributeTests+Arguments.swift create mode 100644 Tests/SyntaxKitTests/Unit/Attributes/AttributeTests+Targets.swift create mode 100644 Tests/SyntaxKitTests/Unit/Declarations/ClassTests+Declarations.swift create mode 100644 Tests/SyntaxKitTests/Unit/Declarations/ClassTests+Modifiers.swift diff --git a/Scripts/lint.sh b/Scripts/lint.sh index 5769c60..457bc4c 100755 --- a/Scripts/lint.sh +++ b/Scripts/lint.sh @@ -25,6 +25,12 @@ fi # Ensure mise-managed tools are on PATH outside CI (CI uses jdx/mise-action) if command -v mise >/dev/null 2>&1 && [ -z "$CI" ]; then + # Trust the config (no-op if already trusted) so `mise env` doesn't silently + # emit nothing when mise.toml is new/untrusted, which would leave the + # mise-managed tools (swiftlint, periphery, ...) off PATH. + mise trust --quiet "$PACKAGE_DIR/mise.toml" >/dev/null 2>&1 || true + # Install any declared-but-missing tools so they resolve on PATH. + mise -C "$PACKAGE_DIR" install >/dev/null 2>&1 || true eval "$(mise -C "$PACKAGE_DIR" env -s bash)" fi diff --git a/Sources/SyntaxKit/Declarations/Class+Modifiers.swift b/Sources/SyntaxKit/Declarations/Class+Modifiers.swift new file mode 100644 index 0000000..a747667 --- /dev/null +++ b/Sources/SyntaxKit/Declarations/Class+Modifiers.swift @@ -0,0 +1,76 @@ +// +// Class+Modifiers.swift +// SyntaxKit +// +// Created by Leo Dion. +// Copyright © 2026 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 Class { + /// Sets the generic parameters for the class. + /// - Parameter generics: The list of generic parameter names. + /// - Returns: A copy of the class with the generic parameters set. + public func generic(_ generics: String...) -> Self { + var copy = self + copy.genericParameters = generics + return copy + } + + /// Sets the inheritance for the class. + /// - Parameter inheritance: The types to inherit from. + /// - Returns: A copy of the class with the inheritance set. + public func inherits(_ inheritance: String...) -> Self { + var copy = self + copy.inheritance = inheritance + 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 + } + + /// Sets the access modifier for the class declaration. + /// - Parameter access: The access modifier. + /// - Returns: A copy of the class with the access modifier set. + public func access(_ access: AccessModifier) -> Self { + var copy = self + copy.accessModifier = access + return copy + } + + /// Adds an attribute to the class declaration. + /// - Parameters: + /// - attribute: The attribute name (without the @ symbol). + /// - arguments: The arguments for the attribute, if any. + /// - Returns: A copy of the class with the attribute added. + public func attribute(_ attribute: String, arguments: [String] = []) -> Self { + var copy = self + copy.attributes.append(AttributeInfo(name: attribute, arguments: arguments)) + return copy + } +} diff --git a/Sources/SyntaxKit/Declarations/Class.swift b/Sources/SyntaxKit/Declarations/Class.swift index f20e577..98c2ca9 100644 --- a/Sources/SyntaxKit/Declarations/Class.swift +++ b/Sources/SyntaxKit/Declarations/Class.swift @@ -31,13 +31,13 @@ public import SwiftSyntax /// A Swift `class` declaration. public struct Class: CodeBlock, Sendable { - private let name: String - private let members: [any CodeBlock] - private var inheritance: [String] = [] - private var genericParameters: [String] = [] - private var isFinal: Bool = false - private var attributes: [AttributeInfo] = [] - private var accessModifier: AccessModifier? + internal let name: String + internal let members: [any CodeBlock] + internal var inheritance: [String] = [] + internal var genericParameters: [String] = [] + internal var isFinal: Bool = false + internal var attributes: [AttributeInfo] = [] + internal var accessModifier: AccessModifier? /// The SwiftSyntax representation of this class declaration. public var syntax: any SyntaxProtocol { @@ -141,52 +141,6 @@ public struct Class: CodeBlock, Sendable { self.members = try content() } - /// Sets the generic parameters for the class. - /// - Parameter generics: The list of generic parameter names. - /// - Returns: A copy of the class with the generic parameters set. - public func generic(_ generics: String...) -> Self { - var copy = self - copy.genericParameters = generics - return copy - } - - /// Sets the inheritance for the class. - /// - Parameter inheritance: The types to inherit from. - /// - Returns: A copy of the class with the inheritance set. - public func inherits(_ inheritance: String...) -> Self { - var copy = self - copy.inheritance = inheritance - 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 - } - - /// Sets the access modifier for the class declaration. - /// - Parameter access: The access modifier. - /// - Returns: A copy of the class with the access modifier set. - public func access(_ access: AccessModifier) -> Self { - var copy = self - copy.accessModifier = access - return copy - } - - /// Adds an attribute to the class declaration. - /// - Parameters: - /// - attribute: The attribute name (without the @ symbol). - /// - arguments: The arguments for the attribute, if any. - /// - Returns: A copy of the class with the attribute added. - public func attribute(_ attribute: String, arguments: [String] = []) -> Self { - var copy = self - copy.attributes.append(AttributeInfo(name: attribute, arguments: arguments)) - return copy - } - private func buildAttributeList(from attributes: [AttributeInfo]) -> AttributeListSyntax { if attributes.isEmpty { return AttributeListSyntax([]) diff --git a/Sources/SyntaxKit/Declarations/IfCanImport.swift b/Sources/SyntaxKit/Declarations/IfCanImport.swift index 0eecd77..948f4c1 100644 --- a/Sources/SyntaxKit/Declarations/IfCanImport.swift +++ b/Sources/SyntaxKit/Declarations/IfCanImport.swift @@ -3,11 +3,11 @@ // SyntaxKit // // Created by Leo Dion. -// Copyright © 2025 BrightDigit. +// Copyright © 2026 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 +// 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 @@ -17,7 +17,7 @@ // 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, +// 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 @@ -34,27 +34,15 @@ public struct IfCanImport: CodeBlock, Sendable { private let moduleName: String private let content: [any CodeBlock] - /// Creates a `#if canImport(moduleName)` block wrapping the given content. - /// - Parameters: - /// - moduleName: The module name passed to `canImport`. - /// - content: The code blocks to emit when the module is available. - public init(_ moduleName: String, @CodeBlockBuilderResult _ content: () -> [any CodeBlock]) { - self.moduleName = moduleName - self.content = content() - } - + /// The SwiftSyntax representation of this conditional compilation block. public var syntax: any SyntaxProtocol { + let canImportRef = DeclReferenceExprSyntax(baseName: .identifier("canImport")) + let moduleRef = DeclReferenceExprSyntax(baseName: .identifier(moduleName)) let condition = FunctionCallExprSyntax( - calledExpression: ExprSyntax(DeclReferenceExprSyntax( - baseName: .identifier("canImport") - )), + calledExpression: ExprSyntax(canImportRef), leftParen: .leftParenToken(), arguments: LabeledExprListSyntax([ - LabeledExprSyntax( - expression: ExprSyntax(DeclReferenceExprSyntax( - baseName: .identifier(moduleName) - )) - ) + LabeledExprSyntax(expression: ExprSyntax(moduleRef)) ]), rightParen: .rightParenToken() ) @@ -78,4 +66,13 @@ public struct IfCanImport: CodeBlock, Sendable { poundEndif: .poundEndifToken(leadingTrivia: .newline) ) } + + /// Creates a `#if canImport(moduleName)` block wrapping the given content. + /// - Parameters: + /// - moduleName: The module name passed to `canImport`. + /// - content: The code blocks to emit when the module is available. + public init(_ moduleName: String, @CodeBlockBuilderResult _ content: () -> [any CodeBlock]) { + self.moduleName = moduleName + self.content = content() + } } diff --git a/Sources/SyntaxKit/Declarations/Initializer.swift b/Sources/SyntaxKit/Declarations/Initializer.swift index 8fd34cf..2e732ec 100644 --- a/Sources/SyntaxKit/Declarations/Initializer.swift +++ b/Sources/SyntaxKit/Declarations/Initializer.swift @@ -3,11 +3,11 @@ // SyntaxKit // // Created by Leo Dion. -// Copyright © 2025 BrightDigit. +// Copyright © 2026 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 +// 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 @@ -17,7 +17,7 @@ // 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, +// 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 diff --git a/Sources/SyntaxKit/Utilities/AttributeArgument.swift b/Sources/SyntaxKit/Utilities/AttributeArgument.swift index b940b45..dbf97e8 100644 --- a/Sources/SyntaxKit/Utilities/AttributeArgument.swift +++ b/Sources/SyntaxKit/Utilities/AttributeArgument.swift @@ -3,11 +3,11 @@ // SyntaxKit // // Created by Leo Dion. -// Copyright © 2025 BrightDigit. +// Copyright © 2026 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 +// 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 @@ -17,7 +17,7 @@ // 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, +// 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 diff --git a/Tests/SyntaxKitTests/Unit/Attributes/AttributeTests+Arguments.swift b/Tests/SyntaxKitTests/Unit/Attributes/AttributeTests+Arguments.swift new file mode 100644 index 0000000..d85c700 --- /dev/null +++ b/Tests/SyntaxKitTests/Unit/Attributes/AttributeTests+Arguments.swift @@ -0,0 +1,137 @@ +// +// AttributeTests+Arguments.swift +// SyntaxKit +// +// Created by Leo Dion. +// Copyright © 2026 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 +import Testing + +extension AttributeTests { + @Suite("Arguments") internal struct Arguments { + @Test("Attribute with arguments generates correct syntax") + internal func testAttributeWithArguments() throws { + let attribute = Attribute("available", arguments: ["iOS", "17.0", "*"]) + + let generated = attribute.syntax.description + #expect(generated.contains("@available")) + #expect(generated.contains("iOS")) + #expect(generated.contains("17.0")) + #expect(generated.contains("*")) + } + + @Test("Attribute with single argument generates correct syntax") + internal func testAttributeWithSingleArgument() throws { + let attribute = Attribute("available", argument: "iOS 17.0") + + let generated = attribute.syntax.description + #expect(generated.contains("@available")) + #expect(generated.contains("iOS 17.0")) + } + + @Test("Function with attribute arguments generates correct syntax") + internal func testFunctionWithAttributeArguments() throws { + let function = Function("bar") { + Variable(.let, name: "message", type: "String", equals: "bar") + }.attribute("available", arguments: ["iOS", "17.0", "*"]) + + let generated = function.syntax.description + #expect(generated.contains("@available")) + #expect(generated.contains("iOS")) + #expect(generated.contains("17.0")) + #expect(generated.contains("*")) + #expect(generated.contains("func bar")) + } + + @Test("Class with attribute arguments generates correct syntax") + internal func testClassWithAttributeArguments() throws { + let classDecl = Class("Foo") { + Variable(.var, name: "bar", type: "String", equals: "bar") + }.attribute("available", arguments: ["iOS", "17.0"]) + + let generated = classDecl.syntax.description + #expect(generated.contains("@available")) + #expect(generated.contains("iOS")) + #expect(generated.contains("17.0")) + #expect(generated.contains("class Foo")) + } + + @Test("Variable with attribute arguments generates correct syntax") + internal func testVariableWithAttributeArguments() throws { + let variable = Variable(.var, name: "bar", type: "String", equals: "bar") + .attribute("available", arguments: ["iOS", "17.0"]) + + let generated = variable.syntax.description + #expect(generated.contains("@available")) + #expect(generated.contains("iOS")) + #expect(generated.contains("17.0")) + #expect(generated.contains("var bar")) + } + + @Test("Struct with quoted string attribute argument generates string literal, not identifier") + internal func testStructWithStringLiteralAttributeArgument() throws { + // Fix 3 regression: "\"App Model\"" must produce @Suite("App Model"), not @Suite(App Model) + let structDecl = Struct("AppModelTests") {} + .attribute("Suite", arguments: ["\"App Model\""]) + + let generated = structDecl.syntax.description + #expect( + generated.contains("@Suite(\"App Model\")") || generated.contains("@Suite( \"App Model\")")) + #expect(!generated.contains("@Suite(App Model)")) + } + + @Test("Function with quoted string attribute argument generates string literal") + internal func testFunctionWithStringLiteralAttributeArgument() throws { + // Fix 3 regression: quoted argument must produce string literal token + let function = Function("initialCount") {} + .attribute("Test", arguments: ["\"Initial count is zero\""]) + + let generated = function.syntax.description + // The argument should be a string literal: @Test("Initial count is zero") + #expect( + generated.contains("@Test(\"Initial count is zero\")") + || generated.contains("@Test( \"Initial count is zero\")")) + } + + @Test("Parameter with attribute arguments generates correct syntax") + internal func testParameterWithAttributeArguments() throws { + let function = Function("validate") { + Parameter(name: "input", type: "String") + .attribute("available", arguments: ["iOS", "17.0"]) + } _: { + Variable(.let, name: "result", type: "Bool", equals: "true") + } + + let generated = function.syntax.description + #expect(generated.contains("@available")) + #expect(generated.contains("iOS")) + #expect(generated.contains("17.0")) + #expect(generated.contains("input : String")) + #expect(generated.contains("func validate")) + } + } +} diff --git a/Tests/SyntaxKitTests/Unit/Attributes/AttributeTests+Targets.swift b/Tests/SyntaxKitTests/Unit/Attributes/AttributeTests+Targets.swift new file mode 100644 index 0000000..59236b7 --- /dev/null +++ b/Tests/SyntaxKitTests/Unit/Attributes/AttributeTests+Targets.swift @@ -0,0 +1,137 @@ +// +// AttributeTests+Targets.swift +// SyntaxKit +// +// Created by Leo Dion. +// Copyright © 2026 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 +import Testing + +extension AttributeTests { + @Suite("Targets") internal struct Targets { + @Test("Class with attribute generates correct syntax") + internal func testClassWithAttribute() throws { + let classDecl = Class("Foo") { + Variable(.var, name: "bar", type: "String", equals: "bar") + }.attribute("objc") + + let generated = classDecl.syntax.description + #expect(generated.contains("@objc")) + #expect(generated.contains("class Foo")) + } + + @Test("Function with attribute generates correct syntax") + internal func testFunctionWithAttribute() throws { + let function = Function("bar") { + Variable(.let, name: "message", type: "String", equals: "bar") + }.attribute("available") + + let generated = function.syntax.description + #expect(generated.contains("@available")) + #expect(generated.contains("func bar")) + } + + @Test("Variable with attribute generates correct syntax") + internal func testVariableWithAttribute() throws { + let variable = Variable(.var, name: "bar", type: "String", equals: "bar") + .attribute("Published") + + let generated = variable.syntax.description + #expect(generated.contains("@Published")) + #expect(generated.contains("var bar")) + } + + @Test("Multiple attributes on class generates correct syntax") + internal func testMultipleAttributesOnClass() throws { + let classDecl = Class("Foo") { + Variable(.var, name: "bar", type: "String", equals: "bar") + } + .attribute("objc") + .attribute("MainActor") + + let generated = classDecl.syntax.description + #expect(generated.contains("@objc")) + #expect(generated.contains("@MainActor")) + #expect(generated.contains("class Foo")) + } + + @Test("Comprehensive attribute example generates correct syntax") + internal func testComprehensiveAttributeExample() throws { + let classDecl = Class("Foo") { + Variable(.var, name: "bar", type: "String", equals: "bar") + .attribute("Published") + + Function("bar") { + Variable(.let, name: "message", type: "String", equals: "bar") + } + .attribute("available") + .attribute("MainActor") + + Function("baz") { + Variable(.let, name: "message", type: "String", equals: "baz") + } + .attribute("MainActor") + }.attribute("objc") + + let generated = classDecl.syntax.description + #expect(generated.contains("@objc")) + #expect(generated.contains("@Published")) + #expect(generated.contains("@available")) + #expect(generated.contains("@MainActor")) + #expect(generated.contains("class Foo")) + #expect(generated.contains("var bar")) + #expect(generated.contains("func bar")) + #expect(generated.contains("func baz")) + } + + @Test("Parameter with attribute generates correct syntax") + internal func testParameterWithAttribute() throws { + let function = Function("process") { + Parameter(name: "data", type: "Data") + .attribute("escaping") + } _: { + Variable(.let, name: "result", type: "String", equals: "processed") + } + + let generated = function.syntax.description + #expect(generated.contains("@escaping")) + #expect(generated.contains("data : Data")) + #expect(generated.contains("func process")) + } + + @Test("Struct with unquoted attribute argument generates identifier, not string literal") + internal func testStructWithIdentifierAttributeArgument() throws { + // Unquoted args should remain as identifier references + let structDecl = Struct("Serve") {} + .attribute("main") + + let generated = structDecl.syntax.description + #expect(generated.contains("@main")) + #expect(generated.contains("struct Serve")) + } + } +} diff --git a/Tests/SyntaxKitTests/Unit/Attributes/AttributeTests.swift b/Tests/SyntaxKitTests/Unit/Attributes/AttributeTests.swift index e7d5de4..88d33b3 100644 --- a/Tests/SyntaxKitTests/Unit/Attributes/AttributeTests.swift +++ b/Tests/SyntaxKitTests/Unit/Attributes/AttributeTests.swift @@ -27,207 +27,7 @@ // OTHER DEALINGS IN THE SOFTWARE. // -import Foundation -import SyntaxKit import Testing -@Suite internal struct AttributeTests { - @Test("Class with attribute generates correct syntax") - internal func testClassWithAttribute() throws { - let classDecl = Class("Foo") { - Variable(.var, name: "bar", type: "String", equals: "bar") - }.attribute("objc") - - let generated = classDecl.syntax.description - #expect(generated.contains("@objc")) - #expect(generated.contains("class Foo")) - } - - @Test("Function with attribute generates correct syntax") - internal func testFunctionWithAttribute() throws { - let function = Function("bar") { - Variable(.let, name: "message", type: "String", equals: "bar") - }.attribute("available") - - let generated = function.syntax.description - #expect(generated.contains("@available")) - #expect(generated.contains("func bar")) - } - - @Test("Variable with attribute generates correct syntax") - internal func testVariableWithAttribute() throws { - let variable = Variable(.var, name: "bar", type: "String", equals: "bar") - .attribute("Published") - - let generated = variable.syntax.description - #expect(generated.contains("@Published")) - #expect(generated.contains("var bar")) - } - - @Test("Multiple attributes on class generates correct syntax") - internal func testMultipleAttributesOnClass() throws { - let classDecl = Class("Foo") { - Variable(.var, name: "bar", type: "String", equals: "bar") - } - .attribute("objc") - .attribute("MainActor") - - let generated = classDecl.syntax.description - #expect(generated.contains("@objc")) - #expect(generated.contains("@MainActor")) - #expect(generated.contains("class Foo")) - } - - @Test("Attribute with arguments generates correct syntax") - internal func testAttributeWithArguments() throws { - let attribute = Attribute("available", arguments: ["iOS", "17.0", "*"]) - - let generated = attribute.syntax.description - #expect(generated.contains("@available")) - #expect(generated.contains("iOS")) - #expect(generated.contains("17.0")) - #expect(generated.contains("*")) - } - - @Test("Attribute with single argument generates correct syntax") - internal func testAttributeWithSingleArgument() throws { - let attribute = Attribute("available", argument: "iOS 17.0") - - let generated = attribute.syntax.description - #expect(generated.contains("@available")) - #expect(generated.contains("iOS 17.0")) - } - - @Test("Comprehensive attribute example generates correct syntax") - internal func testComprehensiveAttributeExample() throws { - let classDecl = Class("Foo") { - Variable(.var, name: "bar", type: "String", equals: "bar") - .attribute("Published") - - Function("bar") { - Variable(.let, name: "message", type: "String", equals: "bar") - } - .attribute("available") - .attribute("MainActor") - - Function("baz") { - Variable(.let, name: "message", type: "String", equals: "baz") - } - .attribute("MainActor") - }.attribute("objc") - - let generated = classDecl.syntax.description - #expect(generated.contains("@objc")) - #expect(generated.contains("@Published")) - #expect(generated.contains("@available")) - #expect(generated.contains("@MainActor")) - #expect(generated.contains("class Foo")) - #expect(generated.contains("var bar")) - #expect(generated.contains("func bar")) - #expect(generated.contains("func baz")) - } - - @Test("Function with attribute arguments generates correct syntax") - internal func testFunctionWithAttributeArguments() throws { - let function = Function("bar") { - Variable(.let, name: "message", type: "String", equals: "bar") - }.attribute("available", arguments: ["iOS", "17.0", "*"]) - - let generated = function.syntax.description - #expect(generated.contains("@available")) - #expect(generated.contains("iOS")) - #expect(generated.contains("17.0")) - #expect(generated.contains("*")) - #expect(generated.contains("func bar")) - } - - @Test("Class with attribute arguments generates correct syntax") - internal func testClassWithAttributeArguments() throws { - let classDecl = Class("Foo") { - Variable(.var, name: "bar", type: "String", equals: "bar") - }.attribute("available", arguments: ["iOS", "17.0"]) - - let generated = classDecl.syntax.description - #expect(generated.contains("@available")) - #expect(generated.contains("iOS")) - #expect(generated.contains("17.0")) - #expect(generated.contains("class Foo")) - } - - @Test("Variable with attribute arguments generates correct syntax") - internal func testVariableWithAttributeArguments() throws { - let variable = Variable(.var, name: "bar", type: "String", equals: "bar") - .attribute("available", arguments: ["iOS", "17.0"]) - - let generated = variable.syntax.description - #expect(generated.contains("@available")) - #expect(generated.contains("iOS")) - #expect(generated.contains("17.0")) - #expect(generated.contains("var bar")) - } - - @Test("Parameter with attribute generates correct syntax") - internal func testParameterWithAttribute() throws { - let function = Function("process") { - Parameter(name: "data", type: "Data") - .attribute("escaping") - } _: { - Variable(.let, name: "result", type: "String", equals: "processed") - } - - let generated = function.syntax.description - #expect(generated.contains("@escaping")) - #expect(generated.contains("data : Data")) - #expect(generated.contains("func process")) - } - - @Test("Struct with quoted string attribute argument generates string literal, not identifier") - internal func testStructWithStringLiteralAttributeArgument() throws { - // Fix 3 regression: "\"App Model\"" must produce @Suite("App Model"), not @Suite(App Model) - let structDecl = Struct("AppModelTests") {} - .attribute("Suite", arguments: ["\"App Model\""]) - - let generated = structDecl.syntax.description - #expect(generated.contains("@Suite(\"App Model\")") || generated.contains("@Suite( \"App Model\")")) - #expect(!generated.contains("@Suite(App Model)")) - } - - @Test("Function with quoted string attribute argument generates string literal") - internal func testFunctionWithStringLiteralAttributeArgument() throws { - // Fix 3 regression: quoted argument must produce string literal token - let function = Function("initialCount") {} - .attribute("Test", arguments: ["\"Initial count is zero\""]) - - let generated = function.syntax.description - // The argument should be a string literal: @Test("Initial count is zero") - #expect(generated.contains("@Test(\"Initial count is zero\")") || generated.contains("@Test( \"Initial count is zero\")")) - } - - @Test("Struct with unquoted attribute argument generates identifier, not string literal") - internal func testStructWithIdentifierAttributeArgument() throws { - // Unquoted args should remain as identifier references - let structDecl = Struct("Serve") {} - .attribute("main") - - let generated = structDecl.syntax.description - #expect(generated.contains("@main")) - #expect(generated.contains("struct Serve")) - } - - @Test("Parameter with attribute arguments generates correct syntax") - internal func testParameterWithAttributeArguments() throws { - let function = Function("validate") { - Parameter(name: "input", type: "String") - .attribute("available", arguments: ["iOS", "17.0"]) - } _: { - Variable(.let, name: "result", type: "Bool", equals: "true") - } - - let generated = function.syntax.description - #expect(generated.contains("@available")) - #expect(generated.contains("iOS")) - #expect(generated.contains("17.0")) - #expect(generated.contains("input : String")) - #expect(generated.contains("func validate")) - } -} +/// Namespace for the attribute generation test suites. +@Suite("Attributes") internal enum AttributeTests {} diff --git a/Tests/SyntaxKitTests/Unit/Declarations/ClassTests+Declarations.swift b/Tests/SyntaxKitTests/Unit/Declarations/ClassTests+Declarations.swift new file mode 100644 index 0000000..00e1a96 --- /dev/null +++ b/Tests/SyntaxKitTests/Unit/Declarations/ClassTests+Declarations.swift @@ -0,0 +1,159 @@ +// +// ClassTests+Declarations.swift +// SyntaxKit +// +// Created by Leo Dion. +// Copyright © 2026 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 Testing + +@testable import SyntaxKit + +extension ClassTests { + @Suite("Declarations") internal struct Declarations { + @Test internal func testClassWithInheritance() { + let carClass = Class("Car") { + Variable(.var, name: "brand", type: "String").withExplicitType() + Variable(.var, name: "numberOfWheels", type: "Int").withExplicitType() + }.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 internal func testEmptyClass() { + let emptyClass = Class("EmptyClass") {} + + let expected = """ + class EmptyClass { + } + """ + + let normalizedGenerated = emptyClass.generateCode().normalize() + let normalizedExpected = expected.normalize() + #expect(normalizedGenerated == normalizedExpected) + } + + @Test internal func testClassWithGenerics() { + let genericClass = Class("Container") { + Variable(.var, name: "value", type: "T").withExplicitType() + }.generic("T") + + let expected = """ + class Container { + var value: T + } + """ + + let normalizedGenerated = genericClass.generateCode().normalize() + let normalizedExpected = expected.normalize() + #expect(normalizedGenerated == normalizedExpected) + } + + @Test internal func testClassWithMultipleGenerics() { + let multiGenericClass = Class("Pair") { + Variable(.var, name: "first", type: "T").withExplicitType() + Variable(.var, name: "second", type: "U").withExplicitType() + }.generic("T", "U") + + let expected = """ + class Pair { + var first: T + var second: U + } + """ + + let normalizedGenerated = multiGenericClass.generateCode().normalize() + let normalizedExpected = expected.normalize() + #expect(normalizedGenerated == normalizedExpected) + } + + @Test internal func testClassWithMultipleInheritance() { + let classWithMultipleInheritance = Class("AdvancedVehicle") { + Variable(.var, name: "speed", type: "Int").withExplicitType() + }.inherits("Vehicle") + + let expected = """ + class AdvancedVehicle: Vehicle { + var speed: Int + } + """ + + let normalizedGenerated = classWithMultipleInheritance.generateCode().normalize() + let normalizedExpected = expected.normalize() + #expect(normalizedGenerated == normalizedExpected) + } + + @Test internal func testClassWithGenericsAndInheritance() { + let genericClassWithInheritance = Class("GenericContainer") { + Variable(.var, name: "items", type: "[T]").withExplicitType() + }.generic("T").inherits("Collection") + + let expected = """ + class GenericContainer: Collection { + var items: [T] + } + """ + + let normalizedGenerated = genericClassWithInheritance.generateCode().normalize() + let normalizedExpected = expected.normalize() + #expect(normalizedGenerated == normalizedExpected) + } + + @Test internal 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/Unit/Declarations/ClassTests+Modifiers.swift b/Tests/SyntaxKitTests/Unit/Declarations/ClassTests+Modifiers.swift new file mode 100644 index 0000000..2996797 --- /dev/null +++ b/Tests/SyntaxKitTests/Unit/Declarations/ClassTests+Modifiers.swift @@ -0,0 +1,114 @@ +// +// ClassTests+Modifiers.swift +// SyntaxKit +// +// Created by Leo Dion. +// Copyright © 2026 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 Testing + +@testable import SyntaxKit + +extension ClassTests { + @Suite("Modifiers") internal struct Modifiers { + @Test internal func testFinalClass() { + let finalClass = Class("FinalClass") { + Variable(.var, name: "value", type: "String").withExplicitType() + }.final() + + let expected = """ + final class FinalClass { + var value: String + } + """ + + let normalizedGenerated = finalClass.generateCode().normalize() + let normalizedExpected = expected.normalize() + #expect(normalizedGenerated == normalizedExpected) + } + + @Test internal func testFinalClassWithInheritanceAndGenerics() { + let finalGenericClass = Class("FinalGenericClass") { + Variable(.var, name: "value", type: "T").withExplicitType() + }.generic("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 internal func testPublicClass() { + let publicClass = Class("AppModel") {}.access(.public) + + let expected = """ + public class AppModel { + } + """ + + let normalizedGenerated = publicClass.generateCode().normalize() + let normalizedExpected = expected.normalize() + #expect(normalizedGenerated == normalizedExpected) + } + + @Test internal func testPublicFinalClass() throws { + let publicFinalClass = Class("AppModel") {} + .attribute("Observable") + .access(.public) + .final() + .inherits("Sendable") + + let generated = publicFinalClass.generateCode() + // Fix 2 regression: Class must support .access() + #expect(generated.contains("public")) + #expect(generated.contains("final")) + #expect(generated.contains("class AppModel")) + #expect(generated.contains("Sendable")) + // Access modifier must precede final + let publicRange = try #require(generated.range(of: "public")) + let finalRange = try #require(generated.range(of: "final")) + #expect(publicRange.lowerBound < finalRange.lowerBound) + } + + @Test internal func testInternalClass() { + let internalClass = Class("MyClass") {}.access(.internal) + + let expected = """ + internal class MyClass { + } + """ + + let normalizedGenerated = internalClass.generateCode().normalize() + let normalizedExpected = expected.normalize() + #expect(normalizedGenerated == normalizedExpected) + } + } +} diff --git a/Tests/SyntaxKitTests/Unit/Declarations/ClassTests.swift b/Tests/SyntaxKitTests/Unit/Declarations/ClassTests.swift index aa3c813..995afe5 100644 --- a/Tests/SyntaxKitTests/Unit/Declarations/ClassTests.swift +++ b/Tests/SyntaxKitTests/Unit/Declarations/ClassTests.swift @@ -27,208 +27,7 @@ // OTHER DEALINGS IN THE SOFTWARE. // -import Foundation import Testing -@testable import SyntaxKit - -internal struct ClassTests { - @Test internal func testClassWithInheritance() { - let carClass = Class("Car") { - Variable(.var, name: "brand", type: "String").withExplicitType() - Variable(.var, name: "numberOfWheels", type: "Int").withExplicitType() - }.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 internal func testEmptyClass() { - let emptyClass = Class("EmptyClass") {} - - let expected = """ - class EmptyClass { - } - """ - - let normalizedGenerated = emptyClass.generateCode().normalize() - let normalizedExpected = expected.normalize() - #expect(normalizedGenerated == normalizedExpected) - } - - @Test internal func testClassWithGenerics() { - let genericClass = Class("Container") { - Variable(.var, name: "value", type: "T").withExplicitType() - }.generic("T") - - let expected = """ - class Container { - var value: T - } - """ - - let normalizedGenerated = genericClass.generateCode().normalize() - let normalizedExpected = expected.normalize() - #expect(normalizedGenerated == normalizedExpected) - } - - @Test internal func testClassWithMultipleGenerics() { - let multiGenericClass = Class("Pair") { - Variable(.var, name: "first", type: "T").withExplicitType() - Variable(.var, name: "second", type: "U").withExplicitType() - }.generic("T", "U") - - let expected = """ - class Pair { - var first: T - var second: U - } - """ - - let normalizedGenerated = multiGenericClass.generateCode().normalize() - let normalizedExpected = expected.normalize() - #expect(normalizedGenerated == normalizedExpected) - } - - @Test internal func testFinalClass() { - let finalClass = Class("FinalClass") { - Variable(.var, name: "value", type: "String").withExplicitType() - }.final() - - let expected = """ - final class FinalClass { - var value: String - } - """ - - let normalizedGenerated = finalClass.generateCode().normalize() - let normalizedExpected = expected.normalize() - #expect(normalizedGenerated == normalizedExpected) - } - - @Test internal func testClassWithMultipleInheritance() { - let classWithMultipleInheritance = Class("AdvancedVehicle") { - Variable(.var, name: "speed", type: "Int").withExplicitType() - }.inherits("Vehicle") - - let expected = """ - class AdvancedVehicle: Vehicle { - var speed: Int - } - """ - - let normalizedGenerated = classWithMultipleInheritance.generateCode().normalize() - let normalizedExpected = expected.normalize() - #expect(normalizedGenerated == normalizedExpected) - } - - @Test internal func testClassWithGenericsAndInheritance() { - let genericClassWithInheritance = Class("GenericContainer") { - Variable(.var, name: "items", type: "[T]").withExplicitType() - }.generic("T").inherits("Collection") - - let expected = """ - class GenericContainer: Collection { - var items: [T] - } - """ - - let normalizedGenerated = genericClassWithInheritance.generateCode().normalize() - let normalizedExpected = expected.normalize() - #expect(normalizedGenerated == normalizedExpected) - } - - @Test internal func testFinalClassWithInheritanceAndGenerics() { - let finalGenericClass = Class("FinalGenericClass") { - Variable(.var, name: "value", type: "T").withExplicitType() - }.generic("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 internal func testPublicClass() { - let publicClass = Class("AppModel") {}.access(.public) - - let expected = """ - public class AppModel { - } - """ - - let normalizedGenerated = publicClass.generateCode().normalize() - let normalizedExpected = expected.normalize() - #expect(normalizedGenerated == normalizedExpected) - } - - @Test internal func testPublicFinalClass() { - let publicFinalClass = Class("AppModel") {} - .attribute("Observable") - .access(.public) - .final() - .inherits("Sendable") - - let generated = publicFinalClass.generateCode() - // Fix 2 regression: Class must support .access() - #expect(generated.contains("public")) - #expect(generated.contains("final")) - #expect(generated.contains("class AppModel")) - #expect(generated.contains("Sendable")) - // Access modifier must precede final - let publicRange = generated.range(of: "public")! - let finalRange = generated.range(of: "final")! - #expect(publicRange.lowerBound < finalRange.lowerBound) - } - - @Test internal func testInternalClass() { - let internalClass = Class("MyClass") {}.access(.internal) - - let expected = """ - internal class MyClass { - } - """ - - let normalizedGenerated = internalClass.generateCode().normalize() - let normalizedExpected = expected.normalize() - #expect(normalizedGenerated == normalizedExpected) - } - - @Test internal 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) - } -} +/// Namespace for the `Class` declaration test suites. +@Suite("Class") internal enum ClassTests {} diff --git a/Tests/SyntaxKitTests/Unit/Declarations/ImportTests.swift b/Tests/SyntaxKitTests/Unit/Declarations/ImportTests.swift index e6a9d66..93fc42c 100644 --- a/Tests/SyntaxKitTests/Unit/Declarations/ImportTests.swift +++ b/Tests/SyntaxKitTests/Unit/Declarations/ImportTests.swift @@ -1,3 +1,32 @@ +// +// ImportTests.swift +// SyntaxKit +// +// Created by Leo Dion. +// Copyright © 2026 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 Testing diff --git a/Tests/SyntaxKitTests/Unit/Declarations/InitializerTests.swift b/Tests/SyntaxKitTests/Unit/Declarations/InitializerTests.swift index 80f2f99..63cb6ec 100644 --- a/Tests/SyntaxKitTests/Unit/Declarations/InitializerTests.swift +++ b/Tests/SyntaxKitTests/Unit/Declarations/InitializerTests.swift @@ -1,3 +1,32 @@ +// +// InitializerTests.swift +// SyntaxKit +// +// Created by Leo Dion. +// Copyright © 2026 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 Testing