Skip to content

Conversation

@julienzarka
Copy link

@julienzarka julienzarka commented Jan 9, 2026

Fix: Include type parameters in switch cases and copyWith factories for sealed classes

Problem

When generating code for sealed classes with type parameters, the generated code was missing type parameters in two critical places:

  1. Switch case patterns in map and when methods - generated "case ClassName():" instead of "case ClassName():"
  2. CopyWith factory method implementation class references - generated "= _$ClassNameCopyWithImpl;" instead of "= _$ClassNameCopyWithImpl<T, $Res>;"

This caused compilation errors where Dart's analyzer flags switch statements as non-exhaustive when type parameters are missing. It also caused type errors where CopyWith factory methods cannot resolve the correct implementation class. As a result, projects need post-generation patching scripts to fix the generated code.

Solution

I made minimal, surgical changes to include type parameters where required:

  1. Pattern Template (pattern_template.dart):

    • Added data.genericsParameterTemplate to switch case patterns in both _mapImpl and _whenImpl methods
    • Now generates "case ClassName():" instead of "case ClassName():"
  2. CopyWith Template (copy_with.dart):

    • Added genericsParameter.append('$Res') to factory method implementation class reference
    • Now generates "factory $ClassNameCopyWith(...) = _$ClassNameCopyWithImpl<T, $Res>;" instead of "= _$ClassNameCopyWithImpl;"

Impact

Sealed classes with type parameters now generate correct, compilable code. This eliminates the need for post-generation patching scripts. The changes maintain backward compatibility since classes without type parameters are unaffected. All existing tests pass.

Testing

I added comprehensive test coverage:

  • Integration test verifying generated code compiles without errors
  • Unit test verifying generated code includes type parameters in switch cases and copyWith factories

Test Results:

  • sealed class with type parameters generates correct switch cases - PASSED
  • sealed class with type parameters - verify generated switch cases include type params - PASSED
  • All tests passed

Files Changed

  • packages/freezed/lib/src/templates/pattern_template.dart - 2 lines changed
  • packages/freezed/lib/src/templates/copy_with.dart - 1 line changed
  • packages/freezed/test/integration/sealed_generic_switch/sealed_generic.dart - new test fixture
  • packages/freezed/test/sealed_generic_switch_test.dart - new test suite

Example

Before:

switch (_that) {
  case SealedGenericFirst():  // Missing type parameter
    return first(_that);
}

After:

switch (_that) {
  case SealedGenericFirst<T>():  // Correct
    return first(_that);
}

Summary by CodeRabbit

  • Bug Fixes

    • Fixed generic type handling so pattern-matching cases and generated copyWith types consistently include type parameters, improving correctness for generic sealed types.
  • New Features

    • Added a sealed-generic example to exercise and demonstrate type-parameterized variants.
  • Tests

    • Added integration tests validating generic sealed-class code generation and copyWith type propagation across generated artifacts.

✏️ Tip: You can customize this high-level summary in your review settings.

@coderabbitai
Copy link

coderabbitai bot commented Jan 9, 2026

Warning

Rate limit exceeded

@julienzarka has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 11 minutes and 12 seconds before requesting another review.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

📥 Commits

Reviewing files that changed from the base of the PR and between 60e643c and 0d0b28e.

📒 Files selected for processing (5)
  • packages/freezed/lib/src/templates/copy_with.dart
  • packages/freezed/lib/src/templates/pattern_template.dart
  • packages/freezed/test/integration/sealed_generic_switch/sealed_generic.dart
  • packages/freezed/test/integration/sealed_generic_switch/sealed_non_generic.dart
  • packages/freezed/test/sealed_generic_switch_test.dart
📝 Walkthrough

Walkthrough

Templates for copyWith and pattern/when generation were changed to emit explicit generic parameters in generated declarations; a sealed generic test fixture and tests were added to validate generated switch cases and copyWith implementations for generics.

Changes

Cohort / File(s) Summary
Copy With Template
packages/freezed/lib/src/templates/copy_with.dart
In CopyWith._deepCopyInterface the abstract mixin factory target was changed to include the $Res generic on the implementation: = $_implClassName${genericsParameter.append('\$Res')};.
Pattern Template Updates
packages/freezed/lib/src/templates/pattern_template.dart
Case labels in _mapImpl and _whenImpl now include constructor generics via ${data.genericsParameterTemplate} (e.g., case ${constructor.redirectedName}${data.genericsParameterTemplate}()).
Generic Sealed Class Test Fixture
packages/freezed/test/integration/sealed_generic_switch/sealed_generic.dart
Added SealedGeneric<T> with factories first(T) and second(T) to exercise generic pattern generation.
Code Generation Tests
packages/freezed/test/sealed_generic_switch_test.dart
Added tests that assert no analysis errors and verify generated switch cases and copyWith implementations include expected generics (e.g., case SealedGenericFirst<T>(), _$SealedGenericCopyWithImpl<T, $Res>).

Sequence Diagram(s)

(omitted)

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related PRs

Poem

🐇 I hopped through templates, tail a-fluff,
Tucked <T> and $Res in seams, not rough,
Switches now call with typed delight,
CopyWith wears its generics right,
A tiny hop — code glowing bright.

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title concisely captures the primary fix: adding type parameters to switch cases and copyWith factories for sealed generic classes.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 0

🧹 Nitpick comments (1)
packages/freezed/test/sealed_generic_switch_test.dart (1)

10-29: Consider defensive assertion before cast and const path extraction for code clarity.

The unchecked as ErrorsResult cast and hardcoded path follow the established pattern throughout the test suite, but adding a type assertion and extracting the path to a constant would improve readability and provide clearer error messages if the analyzer result type ever changes.

Suggested improvement
   test('sealed class with type parameters generates correct switch cases', () async {
-    final main = await resolveSources(
+    final library = await resolveSources(
       {'freezed|test/integration/sealed_generic_switch/sealed_generic.dart': useAssetReader},
       (r) => r.libraries.firstWhere(
         (element) =>
             element.firstFragment.source.toString().contains('sealed_generic'),
       ),
     );
 
-    final errorResult =
-        await main.session.getErrors(
-              '/freezed/test/integration/sealed_generic_switch/sealed_generic.freezed.dart',
-            )
-            as ErrorsResult;
+    const generatedPath =
+        '/freezed/test/integration/sealed_generic_switch/sealed_generic.freezed.dart';
+    final result = await library.session.getErrors(generatedPath);
+    expect(result, isA<ErrorsResult>(), reason: 'Expected ErrorsResult for $generatedPath');
+    final errorResult = result as ErrorsResult;
 
     expect(errorResult.errors, isEmpty);
   });
📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 821021c and ef95336.

📒 Files selected for processing (4)
  • packages/freezed/lib/src/templates/copy_with.dart
  • packages/freezed/lib/src/templates/pattern_template.dart
  • packages/freezed/test/integration/sealed_generic_switch/sealed_generic.dart
  • packages/freezed/test/sealed_generic_switch_test.dart
🔇 Additional comments (5)
packages/freezed/lib/src/templates/pattern_template.dart (2)

290-303: Good fix: propagate generic type args into switch case patterns (when variants).
Same rationale as _mapImpl; keeps pattern matching consistent for generic sealed unions.


218-226: Good fix: propagate generic type args into switch case patterns.

This correctly aligns case patterns with generic redirected types. The implementation uses genericsParameterTemplate, which renders empty string for non-generic classes (avoiding stray <>) and generates <T> or <T, U> as needed for generic types.

Dart 3 pattern matching supports this syntax: case SomeGeneric<T>(): is valid across stable Dart SDK versions. Integration tests confirm the generated patterns like case SealedGenericFirst<T>(): produce no analyzer errors.

packages/freezed/lib/src/templates/copy_with.dart (1)

63-71: Correct: include $Res in the factory redirect implementation type args.
This makes the factory target’s generic arguments match the generated impl class declaration and avoids type-resolution failures.

packages/freezed/test/integration/sealed_generic_switch/sealed_generic.dart (1)

1-9: Nice minimal fixture for exercising generic sealed codegen.
Covers the exact scenario that previously produced missing type args in switch cases and copyWith impl references.

packages/freezed/test/sealed_generic_switch_test.dart (1)

31-45: String-based assertions look good for guarding against regressions in generated output.
One thing to double-check is that the generated .freezed.dart is reliably present in the test environment (otherwise this becomes order/CI-step dependent).

@julienzarka julienzarka force-pushed the fix/sealed-generic-type-parameters branch from ef95336 to 24a1809 Compare January 9, 2026 09:08
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 0

🧹 Nitpick comments (1)
packages/freezed/test/sealed_generic_switch_test.dart (1)

46-60: Simplify: Redundant type check after explicit assertion.

Line 41 already asserts fileResult is a FileResult, so the conditional check on line 46 is redundant. If the assertion passes, the type is guaranteed.

♻️ Simplify by removing redundant check
-    if (fileResult is FileResult) {
-      final content = fileResult.content;
-      if (content.isNotEmpty) {
-        // Verify that switch cases include type parameters
-        expect(content, contains('case SealedGenericFirst<T>():'));
-        expect(content, contains('case SealedGenericSecond<T>():'));
-        
-        // Verify that copyWith factory includes type parameters
-        expect(content, contains('= _\$SealedGenericCopyWithImpl<T, \$Res>;'));
-        expect(content, contains('= _\$SealedGenericFirstCopyWithImpl<T, \$Res>;'));
-        expect(content, contains('= _\$SealedGenericSecondCopyWithImpl<T, \$Res>;'));
-      }
-      // If content is empty, the first test's error check is sufficient
-      // as it confirms the generated code compiles without errors
-    }
+    final content = fileResult.content;
+    if (content.isNotEmpty) {
+      // Verify that switch cases include type parameters
+      expect(content, contains('case SealedGenericFirst<T>():'));
+      expect(content, contains('case SealedGenericSecond<T>():'));
+      
+      // Verify that copyWith factory includes type parameters
+      expect(content, contains('= _\$SealedGenericCopyWithImpl<T, \$Res>;'));
+      expect(content, contains('= _\$SealedGenericFirstCopyWithImpl<T, \$Res>;'));
+      expect(content, contains('= _\$SealedGenericSecondCopyWithImpl<T, \$Res>;'));
+    }
+    // If content is empty, the first test's error check is sufficient
+    // as it confirms the generated code compiles without errors
📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between ef95336 and 24a1809.

📒 Files selected for processing (4)
  • packages/freezed/lib/src/templates/copy_with.dart
  • packages/freezed/lib/src/templates/pattern_template.dart
  • packages/freezed/test/integration/sealed_generic_switch/sealed_generic.dart
  • packages/freezed/test/sealed_generic_switch_test.dart
🚧 Files skipped from review as they are similar to previous changes (1)
  • packages/freezed/lib/src/templates/pattern_template.dart
🔇 Additional comments (4)
packages/freezed/lib/src/templates/copy_with.dart (1)

67-67: LGTM! Type parameter fix correctly applied.

The addition of ${genericsParameter.append('\$Res')} ensures the factory's implementation class reference includes all necessary type parameters, fixing compilation errors for generic sealed classes. This matches the pattern used elsewhere in the template (e.g., line 97, line 149).

packages/freezed/test/integration/sealed_generic_switch/sealed_generic.dart (1)

1-9: LGTM! Well-structured test fixture.

The test fixture appropriately exercises generic sealed class generation with:

  • Explicit type parameters on both the sealed class and factory redirections
  • Two factory constructors to test multiple switch cases
  • Minimal complexity, focused on the specific issue being fixed
packages/freezed/test/sealed_generic_switch_test.dart (2)

7-25: LGTM! Clear test for compilation correctness.

The test effectively verifies that generic sealed classes generate error-free code, which is the primary objective of this fix.


48-56: LGTM! Content assertions correctly verify the fix.

The string expectations properly validate both aspects of the PR fix:

  • Switch case patterns include type parameters (lines 50-51)
  • CopyWith factory implementations include type parameters (lines 54-56)

The defensive check for empty content with fallback to the first test is appropriate.

@julienzarka julienzarka force-pushed the fix/sealed-generic-type-parameters branch from 24a1809 to 7c28846 Compare January 9, 2026 09:26
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 0

🧹 Nitpick comments (2)
packages/freezed/test/sealed_generic_switch_test.dart (2)

7-25: Solid integration test for compilation errors.

The test correctly verifies that the generated code compiles without errors, which is the primary validation that generic switch cases are properly formed. The defensive check for ErrorsResult type is good practice.

One minor suggestion: the firstWhere predicate (line 11-12) uses string matching on the source path. While functional, this could match unintended files if the repo contains multiple files with "sealed_generic" in the path.

♻️ Optional: More specific library resolution
 (r) => r.libraries.firstWhere(
   (element) =>
-      element.firstFragment.source.toString().contains('sealed_generic'),
+      element.firstFragment.source.uri.path.endsWith('sealed_generic_switch/sealed_generic.dart'),
 ),

This ensures exact matching of the intended test fixture file.


27-59: Consider making the content verification more explicit.

The test performs valuable string-based verification of the generated code patterns. However, the conditional check at line 47 (if (content.isNotEmpty)) means the test passes silently when content cannot be retrieved, relying entirely on the first test's error checking.

While the comments explain this fallback, it could mask issues where:

  • The file isn't generated properly but has no analysis errors
  • The session can't access the file for other reasons
  • The generated code is syntactically valid but missing the expected patterns
♻️ Optional: Make content check more explicit

Consider either:

  1. Making content availability explicit:
 if (content.isNotEmpty) {
   // Verify that switch cases include type parameters
   expect(content, contains('case SealedGenericFirst<T>():'));
   // ... other checks
+} else {
+  // Document why empty content is acceptable or fail explicitly
+  fail('Generated file content is empty - cannot verify type parameters in generated code');
 }
  1. Or add a skip condition:
 if (content.isNotEmpty) {
   // Verify that switch cases include type parameters
   expect(content, contains('case SealedGenericFirst<T>():'));
   // ... other checks
+} else {
+  skip('File content not available in this test environment - relying on error check in first test');
 }

This makes the test's behavior more explicit and easier to debug if it starts failing.

📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 24a1809 and 7c28846.

📒 Files selected for processing (4)
  • packages/freezed/lib/src/templates/copy_with.dart
  • packages/freezed/lib/src/templates/pattern_template.dart
  • packages/freezed/test/integration/sealed_generic_switch/sealed_generic.dart
  • packages/freezed/test/sealed_generic_switch_test.dart
🚧 Files skipped from review as they are similar to previous changes (2)
  • packages/freezed/test/integration/sealed_generic_switch/sealed_generic.dart
  • packages/freezed/lib/src/templates/copy_with.dart
🔇 Additional comments (3)
packages/freezed/lib/src/templates/pattern_template.dart (2)

219-219: LGTM! Correct fix for generic switch case patterns.

The addition of ${data.genericsParameterTemplate} ensures that switch case patterns include type parameters (e.g., case ClassName<T>(): instead of case ClassName():), which is necessary for Dart's exhaustiveness checking with generic sealed classes. This is consistent with how generics are used elsewhere in the file (line 201).


291-291: LGTM! Parallel fix for when-style pattern matching.

This applies the same type parameter fix to the when-family of methods, ensuring consistency across all pattern-matching code generation paths.

packages/freezed/test/sealed_generic_switch_test.dart (1)

1-6: LGTM! Test setup is appropriate.

The imports and test structure are correct for integration testing with build_test and the analyzer.

@julienzarka julienzarka force-pushed the fix/sealed-generic-type-parameters branch 3 times, most recently from b1f089a to 60e643c Compare January 9, 2026 09:51
…or sealed classes

- Add type parameters to switch case patterns for generic sealed classes
- Include type parameters in copyWith factory implementations
- Add comprehensive tests for both generic and non-generic sealed classes
- Ensure backward compatibility with non-generic sealed classes

Tests:
- Verify generic sealed classes generate correct switch cases with type params
- Verify copyWith factories include type parameters for generic sealed classes
- Verify non-generic sealed classes continue to work correctly (backward compatibility)
@julienzarka julienzarka force-pushed the fix/sealed-generic-type-parameters branch from 60e643c to 0d0b28e Compare January 9, 2026 10:10
@antoniosb8891
Copy link

Please, merge this PR as soon as possible , it is a critical problem

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