Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
db33503
Update `swift-syntax` version and improve type constraints
qizh Nov 27, 2025
a9b3f01
Bring indentation back in `EnumVariable.swift`
qizh Nov 27, 2025
551c9a1
Update protocol constraint from `SendableMetatype` to `Sendable`
qizh Nov 27, 2025
af382df
Fix enum case encoding for cases without arguments
qizh Nov 27, 2025
44558a5
Align `misuseWithCodable` diagnostics with actual macro output
qizh Nov 27, 2025
6ca0d90
Merge remote-tracking branch 'refs/remotes/upstream/main'
qizh Jan 21, 2026
67ab745
fix: remove unreachable default case warnings for Bool type switches
qizh Jan 21, 2026
37c080c
fix: add SwiftSyntaxMacroExpansion dependency to PluginCore
qizh Jan 21, 2026
ea720d5
chore: add *.dia to gitignore
qizh Jan 21, 2026
0b3f7dc
fix: remove duplicate diagnostic expectations in ConformCodableTests
qizh Jan 21, 2026
6da09a6
Merge pull request #1 from qizh/fix/warnings-cleanup
qizh Jan 21, 2026
4a3190a
Update `CHANGELOG.md`
qizh Jan 30, 2026
a405f1b
Fix: Handle partial `Bool` coverage in `switch` statements
qizh Jan 30, 2026
342f6ff
test: add coverage tests for HelperCoder and related types
qizh Jan 30, 2026
3873f76
Merge branch 'merge/upstream'
qizh Jan 30, 2026
2d44730
test: add @Suite declarations and meaningful test names to all test f…
qizh Jan 30, 2026
edeba0b
test: improve all test names with meaningful, unique descriptions
qizh Jan 30, 2026
c1dd2be
test: add comprehensive tagging system for all tests
qizh Jan 30, 2026
b031947
style: format long @Test declarations as multiline for readability
qizh Jan 30, 2026
aa9002e
docs: fix markdown lint error - add blank line before code block
qizh Jan 30, 2026
bb8f9a5
fix: handle partial Bool coverage in TaggedEnumSwitcherVariable
qizh Jan 30, 2026
e6e5a12
fix: handle partial Bool coverage in TaggedEnumSwitcherVariable
qizh Jan 30, 2026
161b144
fix: remove merge conflict markers from `CodedAsMixedTypesTests.swift`
qizh Jan 30, 2026
6cb6b70
Merge origin/main into tests/improve/coverage/30-01-26
qizh Jan 30, 2026
2344087
Add Swift CI workflow for build and test automation
qizh Jan 30, 2026
fae5691
Disable main.yml workflow in favor of swift.yml
qizh Jan 30, 2026
1f3e57c
Re-enable `main.yml` workflow by removing the `.disabled` extension.
qizh Jan 30, 2026
4e57d33
chore: simplify Swift workflow name to "Swift `build` & `test`"
qizh Jan 30, 2026
3ffa20e
Update `README.md`
qizh Jan 30, 2026
6ca3eea
Fix: Downgrade to Swift 6.1 to avoid compiler crash in macro expansion
qizh Jan 30, 2026
c589543
Fix: Use default Xcode toolchain instead of swift-actions/setup-swift
qizh Jan 30, 2026
4b6fe3d
Merge pull request #2 from qizh/tests/improve/coverage/30-01-26
qizh Jan 30, 2026
201ce15
Refactor test organization with nested suites and improve test descri…
qizh Jan 30, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 42 additions & 0 deletions .github/workflows/swift.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
# This workflow will build a Swift project
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

please remove this, this repo already has CI that tests various configurations

# For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-swift

name: Swift `build` & `test`

permissions:
contents: read

on:
push:
branches: [ "main" ]
paths-ignore:
- '**/*.md'
pull_request:
paths-ignore:
- '**/*.md'

jobs:
build-and-test:
runs-on: macos-latest
timeout-minutes: 20

steps:
- name: Checkout
uses: actions/checkout@v5

- name: Show Swift version
run: swift --version

- name: Cache SwiftPM dependencies
uses: actions/cache@v4
with:
path: .build
key: ${{ runner.os }}-swiftpm-${{ hashFiles('Package.resolved') }}
restore-keys: |
${{ runner.os }}-swiftpm-

- name: Build
run: swift build --configuration debug

- name: Run tests
run: swift test --configuration debug --parallel --verbose
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ Package.resolved
*.o
*.d
*.swiftdeps*
*.dia

# CocoaPods
#
Expand Down
11 changes: 11 additions & 0 deletions .markdownlint.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can you explain what this file does? and why is it needed?

"default": true,
"MD012": false,
"MD013": false,
"MD024": {
"siblings_only": true
},
"MD033": false,
"MD041": false,
"MD060": false
}
1 change: 1 addition & 0 deletions Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ let package = Package(
.product(name: "SwiftDiagnostics", package: "swift-syntax"),
.product(name: "SwiftSyntaxBuilder", package: "swift-syntax"),
.product(name: "SwiftSyntaxMacros", package: "swift-syntax"),
.product(name: "SwiftSyntaxMacroExpansion", package: "swift-syntax"),
.product(name: "OrderedCollections", package: "swift-collections"),
]
),
Expand Down
1 change: 1 addition & 0 deletions Package@swift-5.swift
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ let package = Package(
.product(name: "SwiftDiagnostics", package: "swift-syntax"),
.product(name: "SwiftSyntaxBuilder", package: "swift-syntax"),
.product(name: "SwiftSyntaxMacros", package: "swift-syntax"),
.product(name: "SwiftSyntaxMacroExpansion", package: "swift-syntax"),
.product(name: "OrderedCollections", package: "swift-collections"),
]
),
Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
# MetaCodable

[![Swift `build` & `test`](https://github.com/qizh/MetaCodable/actions/workflows/swift.yml/badge.svg?branch=tests%2Fimprove%2Fcoverage%2F30-01-26)](https://github.com/qizh/MetaCodable/actions/workflows/swift.yml)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

after removing the unnecessary workflow file this can be removed as well

[![API Docs](http://img.shields.io/badge/Read_the-docs-2196f3.svg)](https://swiftpackageindex.com/SwiftyLab/MetaCodable/documentation/metacodable)
[![Swift Package Manager Compatible](https://img.shields.io/github/v/tag/SwiftyLab/MetaCodable?label=SPM&color=orange)](https://badge.fury.io/gh/SwiftyLab%2FMetaCodable)
[![CocoaPods Compatible](https://img.shields.io/cocoapods/v/MetaCodable.svg?label=CocoaPods&color=C90005)](https://badge.fury.io/co/MetaCodable)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,23 +11,34 @@ import SwiftSyntaxMacros
protocol TaggedEnumSwitcherVariable: EnumSwitcherVariable {}

extension TaggedEnumSwitcherVariable {
/// Provides the switch expression for decoding.
/// Provides a `switch` expression used to decode a tagged enum variable.
///
/// Based on enum-cases the each case for switch expression is generated.
/// Final expression generated combining all cases with provided parameters.
/// Generates a `switch` over `header` by mapping each eligible enum case in `location`
/// to a corresponding `case` clause. Each generated clause runs `preSyntax` for the
/// matched tag value and then emits the decoding code for that enum case.
///
/// - Parameters:
/// - header: The switch header cases are compared to.
/// - location: The decoding location.
/// - coder: The decoder for cases.
/// - context: The context in which to perform the macro expansion.
/// - default: Whether default case is needed.
/// - forceDecodingReturn: Whether to force explicit `return` statements in each
/// switch case. When `true`, adds a `return` statement after the case assignment
/// for early exit. Defaults to `false` for backward compatibility.
/// - preSyntax: The callback to generate case variation data.
/// - header: The expression whose value is matched by the `switch`.
/// - location: The decoding location containing tagged enum cases.
/// - coder: The decoder token used by generated decoding code.
/// - context: The macro expansion context.
/// - default: Whether to include a `default` case.
/// - forceDecodingReturn:
/// - When `true`, emits an explicit `return` after each `case` assignment
/// for early exit.
/// - Defaults to `false` for backward compatibility.
/// - preSyntax: A callback used to generate case-variation syntax for the matched
/// tag value.
///
/// - Returns: The generated switch expression.
/// - Important: For `Bool` tags, the `default` case is omitted only when both `true`
/// and `false` are explicitly covered. With partial coverage (only one of the two
/// values), the `default` case is retained.
///
/// - Returns: A `SwitchExprSyntax` when at least one matching switch case can be
/// generated; otherwise `nil`.
///
/// - Complexity: `O(𝑛)` in the number of tagged cases in `location`.
/// - Plus the number of tag expressions scanned for `Bool` coverage.
func decodeSwitchExpression(
over header: EnumVariable.CaseValue.Expr,
at location: EnumSwitcherLocation,
Expand All @@ -38,6 +49,25 @@ extension TaggedEnumSwitcherVariable {
preSyntax: (TokenSyntax) -> CodeBlockItemListSyntax
) -> SwitchExprSyntax? {
var switchable = false

// For Bool type, check if both true and false values are present
var hasBoolTrue = false
var hasBoolFalse = false
if header.type == .bool {
for (_, value) in location.cases {
let boolValues = value.decodeExprs.filter { $0.type == .bool }
for boolValue in boolValues {
let valueStr = boolValue.syntax.trimmedDescription
if valueStr == "true" {
hasBoolTrue = true
} else if valueStr == "false" {
hasBoolFalse = true
}
}
}
}
let skipDefaultForBool = header.type == .bool && hasBoolTrue && hasBoolFalse

let switchExpr = SwitchExprSyntax(subject: header.syntax) {
for (`case`, value) in location.cases where `case`.decode ?? true {
let values = value.decodeExprs
Expand All @@ -60,7 +90,7 @@ extension TaggedEnumSwitcherVariable {
}
}

if `default` {
if `default` && !skipDefaultForBool {
SwitchCaseSyntax(label: .default(.init())) {
"break"
}
Expand Down
22 changes: 13 additions & 9 deletions Sources/PluginCore/Variables/Type/EnumVariable.swift
Original file line number Diff line number Diff line change
Expand Up @@ -84,15 +84,19 @@ package struct EnumVariable: TypeVariable, DeclaredVariable {
let caseEncodeExpr: CaseCode = { name, variables in
let args = Self.encodingArgs(representing: variables)
let callee: ExprSyntax = ".\(name)"
let fExpr =
if !args.isEmpty {
FunctionCallExprSyntax(callee: callee) { args }
} else {
FunctionCallExprSyntax(
calledExpression: callee, leftParen: nil, rightParen: nil
) {}
}
return ExprSyntax(fExpr)
if args.isEmpty {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Any reason for this change? May be you can add some test cases that demonstrates what this fixes?

Copy link
Author

@qizh qizh Jan 30, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@soumyamahunt, I've updated the existing WithAnyCodableLiteralEnum/expansion test at the same time with this fix.
The test should be self-explanatory, though let me know if it's not and a separate test is required.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

BTW I'd like to give names to @Suites and @Tests.
@soumyamahunt what do you think about it, would you approve?

Like the following:

@Suite("Logical tests group")
struct SomeTestSuite {
    @Test("Test Bool expansion")
    func textBoolExpansion() async {
        /// ...
    }
}

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

BTW I'd like to give names to @suites and @tests.
@soumyamahunt what do you think about it, would you approve?

I am okay with having readable test suite and method names.

@soumyamahunt, I've updated the existing WithAnyCodableLiteralEnum/expansion test at the same time with this fix.
The test should be self-explanatory, though let me know if it's not and a separate test is required.

@qizh the tests you have mentioned here is for the changes you have done in Sources/PluginCore/Variables/Enum/Switcher/TaggedEnumSwitcherVariable.swift file. Can you point to the test that is for changes to this file? If this change doesn't address anything please revert this change.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@soumyamahunt Good catch — I clearly need more coffee when referencing tests. ☕

The test you're looking for is CodedAsMixedTypesTests.swift (added in commit a405f1b), specifically:

Previously existing indirect tests:

  • Tests/MetaCodableTests/CodedAt/CodedAtEnumTests.swift - tests tagged enums with @CodedAt
  • Tests/MetaCodableTests/CodedAs/CodedAsTests.swift:275-450 - tests @CodedAs with mixed literal types (Bool, Int, String, Double) where both true and false are covered

What the fix addresses:
The previous code skipped the default case for all Bool switches (header.type != .bool), which worked fine when both true and false were covered. But when @CodedAs specifies only one Bool value (e.g., @CodedAs("load", 12, true) on one case, nothing on the other), the generated switch would be missing a default case — causing Swift to complain about non-exhaustive switch.

The fix checks if both true AND false are present before skipping the default case. The tests verify exactly this scenario with mixed literal types.


Let me know if this clarifies things or if you'd like the tests restructured.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@qizh you are still describing the changes for Sources/PluginCore/Variables/Enum/Switcher/TaggedEnumSwitcherVariable.swift. Can you provide me the result of tests without the change in this file? If you revert the changes in this file Sources/PluginCore/Variables/Type/EnumVariable.swift does any tests fail?

/// No associated values: return just the case name without parentheses
return callee
} else {
let fExpr = FunctionCallExprSyntax(
calledExpression: callee,
leftParen: .leftParenToken(),
arguments: args,
rightParen: .rightParenToken(),
trailingClosure: nil
)
return ExprSyntax(fExpr)
}
}
self.init(
from: decl, in: context,
Expand Down
19 changes: 12 additions & 7 deletions Tests/MetaCodableTests/AccessModifierTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,16 @@ import Testing

@testable import PluginCore

@Suite("Access Modifier Tests")
struct AccessModifierTests {
@Suite("Access Modifier - Open")
struct Open {
@Codable
open class SomeCodable {
let value: String
}

@Test
@Test("Generates @Codable conformance for class with 'open' access", .tags(.accessModifiers, .classes, .codable, .decoding, .encoding, .enums, .macroExpansion))
func expansion() throws {
assertMacroExpansion(
"""
Expand Down Expand Up @@ -49,7 +51,7 @@ struct AccessModifierTests {
)
}

@Test
@Test("Decodes class from JSON successfully", .tags(.accessModifiers, .classes, .decoding))
func openClassDecodingOnly() throws {
// Open class doesn't have memberwise init, only decoder init
let jsonStr = """
Expand All @@ -63,7 +65,7 @@ struct AccessModifierTests {
#expect(decoded.value == "open_test")
}

@Test
@Test("Decodes from JSON successfully", .tags(.accessModifiers, .decoding))
func openClassFromJSON() throws {
let jsonStr = """
{
Expand All @@ -77,14 +79,15 @@ struct AccessModifierTests {
}
}

@Suite("Access Modifier - Public")
struct Public {
@Codable
@MemberInit
public struct SomeCodable {
let value: String
}

@Test
@Test("Generates macro expansion with @Codable for struct with 'public' access", .tags(.accessModifiers, .codable, .decoding, .encoding, .enums, .macroExpansion, .memberInit, .structs))
func expansion() throws {
assertMacroExpansion(
"""
Expand Down Expand Up @@ -127,7 +130,7 @@ struct AccessModifierTests {
)
}

@Test
@Test("Encodes and decodes successfully", .tags(.accessModifiers, .decoding, .encoding))
func publicStructDecodingAndEncoding() throws {
let original = SomeCodable(value: "public_test")
let encoded = try JSONEncoder().encode(original)
Expand All @@ -136,7 +139,7 @@ struct AccessModifierTests {
#expect(decoded.value == "public_test")
}

@Test
@Test("Decodes from JSON successfully (AccessModifierTests #1)", .tags(.accessModifiers, .decoding))
func publicStructFromJSON() throws {
let jsonStr = """
{
Expand All @@ -150,14 +153,15 @@ struct AccessModifierTests {
}
}

@Suite("Access Modifier - Package")
struct Package {
@Codable
@MemberInit
package struct SomeCodable {
let value: String
}

@Test
@Test("Generates macro expansion with @Codable for struct", .tags(.accessModifiers, .codable, .decoding, .encoding, .enums, .macroExpansion, .memberInit, .structs))
func expansion() throws {
assertMacroExpansion(
"""
Expand Down Expand Up @@ -201,6 +205,7 @@ struct AccessModifierTests {
}
}

@Suite("Access Modifier - Others")
struct Others {
struct Internal {
@Codable
Expand Down
7 changes: 4 additions & 3 deletions Tests/MetaCodableTests/Attributes/CodedByTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,9 @@ import Testing

@testable import PluginCore

@Suite("Coded By Tests")
struct CodedByTests {
@Test
@Test("Reports error for @CodedBy misuse", .tags(.codedBy, .errorHandling, .macroExpansion, .structs))
func misuseOnNonVariableDeclaration() throws {
assertMacroExpansion(
"""
Expand Down Expand Up @@ -34,7 +35,7 @@ struct CodedByTests {
)
}

@Test
@Test("Reports error for @CodedBy misuse (CodedByTests #1)", .tags(.codedBy, .errorHandling, .macroExpansion, .structs))
func misuseOnStaticVariable() throws {
assertMacroExpansion(
"""
Expand Down Expand Up @@ -63,7 +64,7 @@ struct CodedByTests {
)
}

@Test
@Test("Reports error when @CodedBy is applied multiple times", .tags(.codedBy, .errorHandling, .macroExpansion, .structs))
func duplicatedMisuse() throws {
assertMacroExpansion(
"""
Expand Down
14 changes: 8 additions & 6 deletions Tests/MetaCodableTests/Attributes/DefaultTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,9 @@ import Testing

@testable import PluginCore

@Suite("Default Tests")
struct DefaultTests {
@Test
@Test("Reports error for @Default misuse", .tags(.default, .errorHandling, .macroExpansion, .structs))
func misuseOnNonVariableDeclaration() throws {
assertMacroExpansion(
"""
Expand Down Expand Up @@ -36,7 +37,7 @@ struct DefaultTests {
)
}

@Test
@Test("Reports error for @Default misuse (DefaultTests #1)", .tags(.default, .errorHandling, .macroExpansion, .structs))
func misuseOnStaticVariable() throws {
assertMacroExpansion(
"""
Expand Down Expand Up @@ -65,7 +66,7 @@ struct DefaultTests {
)
}

@Test
@Test("Reports error when @Default is applied multiple times", .tags(.default, .errorHandling, .macroExpansion, .structs))
func duplicatedMisuse() throws {
assertMacroExpansion(
"""
Expand Down Expand Up @@ -104,6 +105,7 @@ struct DefaultTests {
)
}

@Suite("Default - Default Value Behavior")
struct DefaultValueBehavior {
@Codable
struct SomeCodable {
Expand All @@ -113,7 +115,7 @@ struct DefaultTests {
let number: Int
}

@Test
@Test("Decodes from JSON successfully (DefaultTests #2)", .tags(.decoding, .default))
func defaultValueUsage() throws {
// Test with missing keys in JSON
let jsonStr = "{}"
Expand All @@ -124,7 +126,7 @@ struct DefaultTests {
#expect(decoded.number == 42)
}

@Test
@Test("Decodes from JSON successfully (DefaultTests #3)", .tags(.decoding, .default))
func overrideDefaultValues() throws {
// Test with provided values in JSON
let jsonStr = """
Expand All @@ -140,7 +142,7 @@ struct DefaultTests {
#expect(decoded.number == 100)
}

@Test
@Test("Encodes and decodes successfully (DefaultTests #1)", .tags(.decoding, .default, .encoding))
func encodingWithDefaults() throws {
let original = SomeCodable(value: "test", number: 99)
let encoded = try JSONEncoder().encode(original)
Expand Down
Loading