Skip to content

Conversation

@ecito
Copy link
Contributor

@ecito ecito commented Nov 3, 2025

Overview

This PR adds extensive compile-time validation to the @Schemable macro to catch common configuration errors before they result in confusing compiler errors or invalid JSON schemas. All diagnostics emit clear, actionable error messages during macro expansion. #120

There's some limitations in macros that don't allow us to cover the full spectrum of possible diagnostic messages, as macros can only see the syntax tree, not the semantic type system.

What this means:

  • We can't check if a type conforms to a protocol (like Schemable)
  • We can't resolve type aliases
  • We can't check if a custom type actually exists
  • We can't do true type checking

Problem Statement

Previously, when the generated schema didn't match the memberwise initializer or when schema options were misconfigured, users would encounter confusing compiler errors like:

  • cannot convert value of type '(String, Int) -> Person' to expected argument type '@Sendable ((String)) -> Person'
  • type 'X' has no member 'schema'
  • Silent generation of invalid JSON schemas that fail at runtime

These errors didn't clearly indicate the root cause, making debugging difficult.

Solution

Added three new diagnostic systems:

  1. Initializer Diagnostics - Validates that the generated schema matches the memberwise initializer
  2. SchemaOptions Diagnostics - Validates that @SchemaOptions and type-specific option macros are used correctly
  3. Unsupported Type Diagnostics - Warns when properties have unsupported types and will be excluded

Diagnostics Added (11 Total)

Initializer Diagnostics (4)

1. Property with Default Value (Warning)

Detects when properties have default values that will be excluded from the synthesized memberwise initializer.

@Schemable
struct Person {
  let name: String
  let age: Int = 0  // ⚠️ Warning: excluded from init
}

Message: Property 'age' has a default value which will be excluded from the memberwise initializer

2. Initializer Parameter Order Mismatch (Error)

Detects when an explicit initializer has parameters in a different order than the schema expects.

@Schemable
struct Person {
  let name: String
  let age: Int

  init(age: Int, name: String) { ... }  // ❌ Wrong order
}

Message: Initializer parameter at position 1 is 'age' but schema expects 'name'. The schema will generate properties in a different order than the initializer parameters.

3. Initializer Parameter Type Mismatch (Error)

Detects when parameter types don't match property types.

@Schemable
struct Product {
  let price: Double

  init(price: Int) { ... }  // ❌ Wrong type
}

Message: Parameter 'price' has type 'Int' but schema expects 'Double'. This type mismatch will cause the generated schema to fail.

4. No Matching Initializer (Error)

Detects when a type has explicit initializers but none match the schema signature, especially when using @ExcludeFromSchema.

@Schemable
struct Config {
  let host: String
  let port: Int

  @ExcludeFromSchema
  let internal: Bool

  init(host: String, port: Int, internal: Bool) { ... }  // ❌ Includes excluded property
}

Message: Detailed message showing expected signature, available initializers, and excluded properties with suggestions.

SchemaOptions Diagnostics (6)

5. Type Mismatch for Option Macros (Error)

Detects when type-specific option macros are used on incompatible property types.

@Schemable
struct Person {
  @StringOptions(.minLength(5))  // ❌ String options on Int
  let age: Int
}

Message: @StringOptions can only be used on String properties, but 'age' has type 'Int'

Applies to:

  • @StringOptions → must be used on String properties
  • @NumberOptions → must be used on numeric properties (Int, Double, etc.)
  • @ArrayOptions → must be used on Array properties

6. Min Greater Than Max Constraints (Error)

Detects logically impossible constraint combinations.

@StringOptions(.minLength(10), .maxLength(5))  // ❌ Impossible
@NumberOptions(.minimum(100), .maximum(50))    // ❌ Impossible
@ArrayOptions(.minItems(10), .maxItems(5))     // ❌ Impossible

Message: Property 'username' has minLength (10) greater than maxLength (5). This string length constraint can never be satisfied.

7. Negative Constraint Values (Error)

Detects invalid negative values for size/length constraints.

@StringOptions(.minLength(-5))   // ❌ Invalid
@ArrayOptions(.minItems(-1))     // ❌ Invalid
@NumberOptions(.multipleOf(-2))  // ❌ Invalid

Message: Property 'text' has minLength with negative value (-5). This constraint must be non-negative.

8. ReadOnly and WriteOnly Conflict (Error)

Detects when a property is marked as both read-only and write-only.

@SchemaOptions(.readOnly(true), .writeOnly(true))  // ❌ Impossible
let value: String

Message: Property 'value' cannot be both readOnly and writeOnly

9. Conflicting Constraint Types (Warning)

Warns when both inclusive and exclusive boundary constraints are specified.

@NumberOptions(.minimum(0), .exclusiveMinimum(0))  // ⚠️ Conflicting
@NumberOptions(.maximum(100), .exclusiveMaximum(100))  // ⚠️ Conflicting

Message: Property 'value' has both minimum and exclusiveMinimum specified. Use only one of minimum or exclusiveMinimum.

10. Duplicate Options (Warning)

Warns when the same option is specified multiple times.

@StringOptions(.minLength(5), .minLength(10))  // ⚠️ Duplicate

Message: Property 'text' has minLength specified 2 times. Only the last value will be used.

Unsupported Type Diagnostics (1)

11. Unsupported Property Type (Warning)

Warns when properties have types that are not supported by the @Schemable macro and will be silently excluded from schema generation.

@Schemable
struct Handler {
  let name: String
  let callback: () -> Void  // ⚠️ Function type not supported
}

Message: Property 'callback' has type '() -> Void' which is not supported by the @Schemable macro. This property will be excluded from the generated schema, which may cause the schema to not match the memberwise initializer.

Catches:

  • Function types: () -> Void, (Int) -> String
  • Tuple types: (Int, Int), (x: Int, y: Int)
  • Metatypes: Any.Type, String.Type
  • Other unsupported Swift types

Why this matters: This diagnostic would have made the MemberType bug (where qualified type names like Weather.Condition were silently excluded) much more obvious by warning about the exclusion.

ecito added 8 commits November 3, 2025 20:41
This change adds compile-time validation to catch common issues where
the generated schema doesn't match the memberwise initializer, preventing
confusing compiler errors.

Diagnostics implemented:
- Warning when properties have default values (excluded from init)
- Error when explicit init has wrong parameter order
- Error when explicit init has parameter type mismatches
- Error when no matching init is found for schema

Key changes:
- New InitializerDiagnostics.swift with all validation logic
- SchemaGenerator now emits diagnostics during macro expansion
- Fixed bug where inline comments were captured in default values
- Comprehensive tests using assertMacroExpansion

All diagnostics use clear, actionable error messages that explain the
problem and suggest fixes.
This change adds compile-time validation for @SchemaOptions and type-specific
option macros (@StringOptions, @NumberOptions, @ArrayOptions, @ObjectOptions)
to catch configuration errors before they create invalid schemas.

Diagnostics implemented:
- Error when type-specific options don't match property type
  (e.g., @StringOptions on Int property)
- Error when min > max constraints (minLength > maxLength, etc.)
- Error when constraint values are negative (minLength < 0)
- Error when property is both readOnly and writeOnly
- Warning when conflicting constraint types are used
  (minimum + exclusiveMinimum)
- Warning when same option is specified multiple times

Key changes:
- New SchemaOptionsDiagnostics.swift with all validation logic
- SchemableMember.validateOptions() validates options during macro expansion
- SchemaGenerator calls validation for each member
- Comprehensive tests covering all diagnostic scenarios

All validations prevent generation of invalid JSON schemas while providing
clear, actionable error messages.
This change adds a warning diagnostic when properties have types that are
not supported by the @Schemable macro and will be silently excluded from
schema generation.

The diagnostic catches:
- Function types: () -> Void, (Int) -> String, etc.
- Tuple types: (Int, Int), (x: Int, y: Int), etc.
- Metatypes: Any.Type, String.Type, etc.
- Other unsupported Swift types

This prevents silent failures where properties disappear from the schema
without warning, which would have made issues like the MemberType bug
(qualified type names like Weather.Condition) much more obvious.

Key changes:
- SchemableMember.generateSchema() now accepts optional context parameter
- Emits warning when type.typeInformation() returns .notSupported
- SchemaGenerator passes context to generateSchema()
- New UnsupportedTypeDiagnostic with clear, actionable message
- 5 comprehensive tests covering various unsupported type scenarios

Example diagnostic:
  Property 'callback' has type '() -> Void' which is not supported by
  the @Schemable macro. This property will be excluded from the generated
  schema, which may cause the schema to not match the memberwise initializer.

This complements existing initializer diagnostics by catching silent schema
generation issues at compile time.
The keyword 'internal' is reserved in Swift and cannot be used as an
identifier. Renamed test property to 'internalFlag' to fix compilation
errors in CI.
The .default() call should be at the same indentation level as the
component above it (e.g., JSONString()), not indented 2 spaces further.

This matches the existing test expectations in SchemableExpansionTests
and fixes the 20 test failures on CI in InitializerDiagnosticsTests.
- Fix SchemaOptionsGenerator to generate method calls at the same
  indentation level as the component (not 2 spaces more)
- Fix test expectations in SchemaOptionsDiagnosticsTests to match
  correct indentation
- Remove attribute macros from expandedSource (they should be consumed
  during expansion)
- Fix readOnly/writeOnly placement to be inside JSONProperty closure

This brings SchemaOptions behavior in line with the existing tests in
SchemaOptionsTests.swift.
@ecito ecito marked this pull request as draft November 4, 2025 00:59
ecito added 2 commits November 4, 2025 02:03
- Only emit default value warnings for mixed cases (some properties with
  defaults, some without) rather than all cases with defaults
- Fix findMatchingInit to match by parameter name set regardless of order,
  then validate order separately
- Only emit type mismatch errors when parameter names match at that position
  (avoid spurious errors when order is wrong)
- Update test expectations to include .default() in schema output
- Rename test to reflect corrected behavior (no diagnostics when all
  properties have defaults)
- Fix diagnostic location expectation

This eliminates false positive warnings while still catching real
initializer/schema mismatches.
@ecito ecito marked this pull request as ready for review November 4, 2025 01:05
@ajevans99 ajevans99 requested a review from Copilot November 4, 2025 01:44
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull Request Overview

This PR enhances the @Schemable macro with comprehensive diagnostic capabilities to help developers identify configuration issues at compile-time. The changes add validation for unsupported types, schema options mismatches, and initializer compatibility problems.

  • Adds diagnostics for unsupported property types (functions, tuples, metatypes)
  • Implements validation for schema options (type mismatches, constraint logic errors, negative values)
  • Adds initializer mismatch detection to ensure schemas match memberwise initializers

Reviewed Changes

Copilot reviewed 10 out of 10 changed files in this pull request and generated 1 comment.

Show a summary per file
File Description
UnsupportedTypeDiagnosticsTests.swift Tests for diagnostic warnings when properties have unsupported types
SimpleDiagnosticsTests.swift Basic smoke test for macro instantiation
SchemaOptionsDiagnosticsTests.swift Comprehensive tests for schema option validation diagnostics
InitializerDiagnosticsTests.swift Tests for initializer parameter matching and default value diagnostics
MacroExpansion+SwiftTesting.swift Adds diagnostics parameter to test helper function
SchemableMember.swift Adds option validation and unsupported type diagnostics
SchemableMacro.swift Threads MacroExpansionContext through to enable diagnostics
SchemaOptionsDiagnostics.swift New file implementing schema options validation logic
SchemaGenerator.swift Integrates diagnostics validation into schema generation
InitializerDiagnostics.swift New file implementing initializer matching validation

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

ecito added 3 commits November 4, 2025 11:42
The isExcluded field was never accessed after being set, and the logic
was confusing due to the misleading name of shouldExcludeFromSchema.
Since excludedProperties is computed by filtering, the field is redundant.
Add explicit dependencies for:
- JSONSchemaMacro: SwiftBasicFormat, SwiftDiagnostics, SwiftSyntaxBuilder
- JSONSchemaMacroTests: SwiftParser, SwiftParserDiagnostics, SwiftBasicFormat, SwiftDiagnostics

These are required by the diagnostic functionality added in the initializer
and schema options diagnostics features.
Copy link
Owner

@ajevans99 ajevans99 left a comment

Choose a reason for hiding this comment

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

Cool

ecito added 3 commits November 5, 2025 00:48
During the merge from main, the isStatic extension on VariableDeclSyntax
and the static property filter in schemableMembers() were accidentally lost.
This commit restores:

- isStatic computed property on VariableDeclSyntax
- Static property filtering in schemableMembers()
- Missing comma in SchemableMember.generateSchema parameter list (from merge conflict)
@ajevans99 ajevans99 merged commit 8c98a44 into ajevans99:main Nov 5, 2025
8 checks passed
@ajevans99 ajevans99 mentioned this pull request Nov 5, 2025
4 tasks
ajevans99 added a commit that referenced this pull request Nov 5, 2025
## Description

Fix main CI failure by updating tests. #122 and #127 collided.

## Type of Change

- [x] Bug fix
- [ ] New feature
- [ ] Breaking change
- [ ] Documentation update

## Additional Notes

Add any other context or screenshots about the pull request here.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants