Skip to content

Conversation

@Yegair
Copy link

@Yegair Yegair commented Aug 31, 2024

Adds a new configuration parameter finalize: bool that allows to mark generated classes as final in the case of concrete classes or sealed in the case of abstract classes. It is disabled by default.

Enabling it allows the Dart analyzer/compiler to report patterns that will never match the type of a freezed class at compile time.

For example:

@Freezed(finalize: true)
sealed class Foo with _$Foo {
  const Foo._();
  const factory Foo() = _Foo;
}

@Freezed(finalize: true)
sealed class Bar with _$Bar {
  const Bar._();
  const factory Bar() = Bar;
}

void main() {
  switch (Foo()) {
    // This will now cause an analyzer warning: pattern_never_matches_value_type.
    // Without finalize: true it would not cause any warning/error at all.
    case Bar():
      print("matched Bar()");
      return;

    case Foo():
      print("matched Foo()");
      return;
  }
}

Why do I think this is a good idea?

I have recently had an issue in one of my projects where I have a @freezed sealed class DataFromApi and I introduced a new @freezed sealed class DataFromDatabase that was intended to be used in many but not all cases where DataFromApi was being used so far.

It was a normal change, but I messed up due to the heavy use of pattern matching like

switch (data) {
  case DataFromApi(someCondition: true):
    // ...
    break;

  case DataFromApi(someCondition: false):
    // ...
    break;

  default:
    // ...
    break;
}

The changes mostly looked like this

- final data = await loadDataFromApi();
+ final data = await loadDataFromDatabase();

switch (data) {
  case DataFromApi(someCondition: true):
    // ...
    break;

    // ...
}

I was expecting the Dart analyzer/compiler to yell at me when changing the type of data in the example above to DataFromDatabase, but that was not the case. So I had to find all the now broken patterns by hand. Naturally I overlooked one case and the tests didn't catch it, so I got a Bug in production 😅.

IMHO the Dart analyzer/compiler should be able to help me in this case no matter if I use freezed or not, since the classes in question were marked as sealed. So I opened an issue over there: dart-lang/sdk#56616

However, since this might never be implemented or maybe is completely impossible, I thought it should be possible and rather simple to make freezed help me out in that case. All that needs to be done is mark the generated classes as final or sealed, and then the analyzer will step in and report any pattern that can never match at compile time.

What needs to be done before it might be merged?

Good question, I think first of all someone needs to decide if this even is a good idea or if it would mess things up. In case it should be merged, I assume a few more tests need to be written to make sure the finalize flag works well with other configuration options. However, so far I have a hard time figuring out what even might break due to this change, so I would need help to decide what special/edge cases to consider when writing tests.

Summary by CodeRabbit

  • New Features

    • Added an option to control whether generated classes are declared final by default.
    • Integration of final/sealed behavior into generated class patterns.
  • Tests

    • Added and expanded tests validating final/sealed class behavior and analyzer warnings for pattern matching.
    • Improved .fromJson test coverage to include the new option and related property checks.

@changeset-bot
Copy link

changeset-bot bot commented Aug 31, 2024

⚠️ No Changeset found

Latest commit: 4774262

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. If these changes should result in a version bump, you need to add a changeset.

Click here to learn what changesets are, and how to add one.

Click here if you're a maintainer who wants to add a changeset to this PR

@rrousselGit
Copy link
Owner

This is valuable.

Although I'm wondering whether we need a flag at all. Why not always make the generated class final if it helps?
Extending a Freezed class isn't supported anyway, as they only have factory constructors.

@Yegair
Copy link
Author

Yegair commented Aug 31, 2024

Happy to hear that 😊

If I would implement that change in the scope of my project I would certainly make final/sealed the default. However, I find it very hard to anticipate how freezed is being used out there in the wild and making it the default feels like doing a breaking change. So I would ask you to make that decision, but for me at least it would definitely work.

@Yegair
Copy link
Author

Yegair commented Sep 1, 2024

From my side the PR is now "ready", meaning I have no further actionable task on my list.

Would be happy to remove a whole bunch of code and just hardcode sealed and final when generating subclasses, but as I said before: I would 100% support this, but I don't think I am in a position to make that decision.

Tried to make the CI work, but I guess the reason it failed has nothing to do with my changes, so I just added a dependency override for frontend_server_client: ^4.0.0. However, I am unsure if this might have negative side effects, so would be happy to remove it if requested.

Also I was thinking if finalize is a good term to use (I'm not a native english speaker), but maybe makeGeneratedClassesFinal or something in this direction would be easier to understand for users?

And finally, I tried to add a changeset as the bot requested, but I was unable to make it work by following the guide it linked to, so I assume using this package is somewhat outdated?

@rrousselGit
Copy link
Owner

Would be happy to remove a whole bunch of code and just hardcode sealed and final when generating subclasses, but as I said before: I would 100% support this, but I don't think I am in a position to make that decision.

You can go ahead and do this change. Only _$Foo classes are sub-classable at the moment, and that's not something folks should be using anyway.

Removing the flag and making the generated classes final are beneficial to more people this way. Otherwise most folks won't even know that the option is there and won't benefit from the warning.

@rrousselGit
Copy link
Owner

You can ignore the changeset bot btw

@Yegair Yegair changed the title feat: add finalize parameter to Freezed config feat: make generated classes sealed/final Sep 2, 2024
@Yegair
Copy link
Author

Yegair commented Sep 2, 2024

Removed the configuration part, so basically it now just adds the final/sealed modifiers and of course left in some tests that make sure it works as intended.

I think it should be ready to merge.

If there is something else that should be changed, just let me know, but throughout the week it might take a while until I can get to it.


And since I get the chance to talk to you directly, just wanted to let you know how valuable Freezed and especially Riverpod are to the project I am working on (https://choosy.de if you want to know what people are building with your libs 😉). Since migrating from Bloc to Riverpod the ease of using providers from inside other providers made such a huge difference.

Cant wait to get a first glimpse of the wizardry you (hopefully) are going to do with macros once they are stable 😊.

@rrousselGit
Copy link
Owner

Cool! I'll leave this open for a few more days, but it should be good to go.
I just need to try it locally and evaluate the possible danger of this final change to be safe :)

@coderabbitai
Copy link

coderabbitai bot commented Nov 3, 2024

Walkthrough

The pull request introduces a new property for class modifiers in the Freezed package, allowing dynamic inclusion of modifiers like final in generated class declarations. This involves updates to the class structure and the addition of new fields and methods in the models and annotations. Comprehensive unit tests are added to validate the behavior of final and sealed classes, ensuring correct handling of patterns and JSON deserialization.

Changes

Cohort / File(s) Change Summary
Class Modifiers
packages/freezed/lib/src/templates/concrete_template.dart, packages/freezed/lib/src/models.dart, packages/freezed_annotation/lib/freezed_annotation.dart, packages/freezed_annotation/lib/freezed_annotation.g.dart
Added a new property for class modifiers in class declarations; introduced a getter and field in models, and updated JSON deserialization to include class modifiers.
Test Updates
packages/freezed/test/finalized_test.dart, packages/freezed/test/integration/finalized.dart, packages/freezed_annotation/test/freezed_test.dart
Added and updated tests to validate behavior of final/sealed classes, including warnings for pattern matches and assertions for JSON .fromJson properties.

Sequence Diagram(s)

No sequence diagram is provided, as the changes in this pull request do not introduce a new feature or significantly modify the control flow. The changes are focused on adding a new property to control class modifiers and updating the corresponding tests.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

The changes in this pull request have a moderate level of complexity. The addition of a new property to control class modifiers in the Freezed package requires updates to the class structure, models, and annotations. The comprehensive suite of tests added to validate the behavior of final and sealed classes adds further complexity to the review. While the changes are focused on a specific aspect of the Freezed package, the spread across multiple files and the introduction of new tests make the overall effort moderately complex.

Suggested reviewers

  • rrousselGit

Poem

I'm a little bunny, hopping through code so bright,
Adding a final touch to every class in sight.
With a skip and a jump, the tests now run clean,
Sealed and abstract, shining like a dream.
In fields and templates, the magic is spun –
CodeRabbit delights in the changes we've done!
🐇💻

Tip

🔌 Remote MCP (Model Context Protocol) integration is now available!

Pro plan users can now connect to remote MCP servers from the Integrations page. Connect with popular remote MCPs such as Notion and Linear to add more context to your reviews and chats.

✨ Finishing Touches
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment

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
🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.

Support

Need help? Create a ticket on our support page for assistance with any issues or questions.

CodeRabbit Commands (Invoked using PR/Issue comments)

Type @coderabbitai help to get the list of available commands.

Other keywords and placeholders

  • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

CodeRabbit Configuration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Status, Documentation and Community

  • Visit our Status Page to check the current availability of CodeRabbit.
  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

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

🧹 Outside diff range and nitpick comments (4)
packages/freezed/test/integration/finalized.dart (2)

10-13: Consider consolidating similar test cases

This class is structurally identical to FinalizedFoo. Consider whether having two identical test cases adds value, or if the test coverage could be achieved with a single class.


15-20: LGTM: Comprehensive test case for pattern matching

This class provides excellent coverage for testing pattern matching with multiple constructors, which is crucial for validating the compile-time warnings mentioned in the PR objectives.

Consider adding test cases for:

  1. Nested sealed classes
  2. Generic sealed classes
  3. Sealed classes with complex property types

These additional cases would help ensure the feature works correctly in more complex scenarios.

packages/freezed/test/finalized_test.dart (2)

8-42: Consider enhancing test coverage for single constructor case.

The test effectively verifies the warning for impossible pattern matches. Consider these enhancements:

  1. Add a positive test case to verify that valid pattern matches don't trigger warnings
  2. Verify the error message content for better diagnostic coverage
  3. Assert the error location (line/column) to ensure precise diagnostic reporting

Example enhancement:

 test('causes pattern_never_matches_value_type warning when trying to match on pattern that can never match',
   () async {
     final main = await resolveSources(
       {
         'freezed|test/integration/main.dart': r'''
           library main;
           import 'finalized.dart';

           void main() {
             switch (FinalizedFoo()) {
               case FinalizedBar():
                 break;
               case FinalizedFoo():
                 break;
             }
           }
           ''',
       },
       (r) => r.findLibraryByName('main'),
     );

     final errorResult = await main!.session
         .getErrors('/freezed/test/integration/main.dart') as ErrorsResult;

     expect(errorResult.errors, hasLength(1));

     final [error] = errorResult.errors;

     expect(error.errorCode.errorSeverity, ErrorSeverity.WARNING);
     expect(error.errorCode.name, 'PATTERN_NEVER_MATCHES_VALUE_TYPE');
+    expect(error.message, contains('pattern can never match'));
+    expect(error.location.lineNumber, 10);
+    expect(error.location.columnNumber, 10);
   });

+  test('does not warn for valid pattern matches', () async {
+    final main = await resolveSources(
+      {
+        'freezed|test/integration/main.dart': r'''
+          library main;
+          import 'finalized.dart';
+
+          void main() {
+            switch (FinalizedFoo()) {
+              case FinalizedFoo():
+                break;
+            }
+          }
+          ''',
+      },
+      (r) => r.findLibraryByName('main'),
+    );
+
+    final errorResult = await main!.session
+        .getErrors('/freezed/test/integration/main.dart') as ErrorsResult;
+
+    expect(errorResult.errors, isEmpty);
+  });

44-85: Consider refactoring multiple constructors test for better maintainability.

While the test effectively verifies the core functionality, consider these improvements:

  1. Split into separate test cases for each constructor variant
  2. Extract common error verification logic into a test utility
  3. Add positive test cases for valid pattern matches

Example refactoring:

+Future<void> expectPatternMatchWarning(String source) async {
+  final main = await resolveSources(
+    {'freezed|test/integration/main.dart': source},
+    (r) => r.findLibraryByName('main'),
+  );
+
+  final errorResult = await main!.session
+      .getErrors('/freezed/test/integration/main.dart') as ErrorsResult;
+
+  expect(errorResult.errors, hasLength(1));
+  final [error] = errorResult.errors;
+  expect(error.errorCode.errorSeverity, ErrorSeverity.WARNING);
+  expect(error.errorCode.name, 'PATTERN_NEVER_MATCHES_VALUE_TYPE');
+}

 group('multiple constructors', () {
-  test('causes pattern_never_matches_value_type warning when trying to match on pattern that can never match',
+  test('warns when matching FinalizedMultiple.b against FinalizedBar',
     () async {
-      final main = await resolveSources(
-        {
-          'freezed|test/integration/main.dart': r'''
+      await expectPatternMatchWarning(r'''
           library main;
           import 'finalized.dart';

           void main() {
             switch (FinalizedMultiple.b()) {
               case FinalizedBar():
                 break;
             }
           }
-          ''',
-        },
-        (r) => r.findLibraryByName('main'),
-      );
+          ''');
+    });

+  test('warns when matching FinalizedMultiple.b against FinalizedMultipleA',
+    () async {
+      await expectPatternMatchWarning(r'''
+          library main;
+          import 'finalized.dart';
+
+          void main() {
+            switch (FinalizedMultiple.b()) {
+              case FinalizedMultipleA():
+                break;
+            }
+          }
+          ''');
+    });

+  test('allows matching FinalizedMultiple.b against FinalizedMultipleB',
+    () async {
+      final main = await resolveSources(
+        {
+          'freezed|test/integration/main.dart': r'''
+            library main;
+            import 'finalized.dart';
+
+            void main() {
+              switch (FinalizedMultiple.b()) {
+                case FinalizedMultipleB():
+                  break;
+              }
+            }
+            ''',
+        },
+        (r) => r.findLibraryByName('main'),
+      );
+
+      final errorResult = await main!.session
+          .getErrors('/freezed/test/integration/main.dart') as ErrorsResult;
+
+      expect(errorResult.errors, isEmpty);
+    });
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL

📥 Commits

Reviewing files that changed from the base of the PR and between 382592c and 47f79b8.

📒 Files selected for processing (4)
  • packages/freezed/lib/src/templates/concrete_template.dart (2 hunks)
  • packages/freezed/pubspec_overrides.yaml (1 hunks)
  • packages/freezed/test/finalized_test.dart (1 hunks)
  • packages/freezed/test/integration/finalized.dart (1 hunks)
✅ Files skipped from review due to trivial changes (1)
  • packages/freezed/pubspec_overrides.yaml
🔇 Additional comments (4)
packages/freezed/test/integration/finalized.dart (2)

1-3: LGTM: Proper Freezed setup

The import and part directive follow the standard Freezed setup pattern.


5-8: LGTM: Simple sealed class test case

This provides a good test case for the simplest possible sealed class scenario with a single constructor.

Let's verify the test coverage for this class:

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

1-7: LGTM! Well-structured test setup.

The imports and main test group structure are appropriate for testing the sealed/final class behavior.

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

67-67: ⚠️ Potential issue

Update minimum Dart SDK constraint due to use of final class and sealed class

The use of final class (line 67) and sealed class (line 89) requires Dart SDK version 2.17.0 or higher. Please update the minimum SDK version in your pubspec.yaml file to >=2.17.0 to ensure compatibility and prevent build errors for users on earlier SDK versions.

Run the following script to verify the current SDK constraints:

Also applies to: 89-89

@rrousselGit
Copy link
Owner

My appologies, I kind of forgot about this PR. Let me see what we can do here.
Feel free to ping me if I forget again.

@Yegair
Copy link
Author

Yegair commented Jan 6, 2025

Friendly Reminder to take a look at this PR 😊

If there is a way I can help testing it, just let me know!

@EArminjon
Copy link

This is valuable.

Although I'm wondering whether we need a flag at all. Why not always make the generated class final if it helps? Extending a Freezed class isn't supported anyway, as they only have factory constructors.

Is this PR stil relevant as now freezed support "extends" ?

@Yegair
Copy link
Author

Yegair commented Feb 27, 2025

Is this PR stil relevant as now freezed support "extends" ?

At least the original way how it was implemented will no longer work. Just did a quick rebase from upstream, but have to write quite a few more tests to make sure it works in combination with the new features from release 3.x.x.

Will take a closer look on the weekend!

@rrousselGit
Copy link
Owner

Sorry as you can see, I got sidetracked.
I'm not sure how relevant it is anymore. To be honest, looking back at the motivation, I wonder if the iDE isn't to blame instead.

I'm not sure whether changing the generated code makes sense, when analyzer likely should know already that the case doesn't make sense for a sealed class.

After-all, sealed classes are some form of final classes

@rrousselGit
Copy link
Owner

Actually ignore me, I can see why the analyzer works this way.

if you don't mind fixing the conflicts, we can merge this.

For now, let's not enable it by default though. I'd rather not make a 4.0 right after a 3.0

@Yegair Yegair force-pushed the feature/finalized branch from c7dcdbb to df45cb2 Compare March 15, 2025 09:56
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: 1

🧹 Nitpick comments (1)
packages/freezed_annotation/lib/freezed_annotation.dart (1)

344-379: Fix minor spelling in documentation.

In the doc comment, “analzyer” should be spelled “analyzer” for clarity.

-  /// so when using them in a switch statement, the analzyer will warn you
+  /// so when using them in a switch statement, the analyzer will warn you
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between c7dcdbb and df45cb2.

📒 Files selected for processing (6)
  • .vscode/settings.json (1 hunks)
  • packages/freezed/lib/src/models.dart (2 hunks)
  • packages/freezed/lib/src/templates/concrete_template.dart (1 hunks)
  • packages/freezed/test/finalized_test.dart (1 hunks)
  • packages/freezed/test/integration/finalized.dart (1 hunks)
  • packages/freezed_annotation/lib/freezed_annotation.dart (2 hunks)
🚧 Files skipped from review as they are similar to previous changes (3)
  • packages/freezed/lib/src/models.dart
  • packages/freezed/test/finalized_test.dart
  • packages/freezed/lib/src/templates/concrete_template.dart
🧰 Additional context used
🪛 Biome (1.9.4)
.vscode/settings.json

[error] 9-9: Expected a property but instead found '}'.

Expected a property here.

(parse)

⏰ Context from checks skipped due to timeout of 90000ms (3)
  • GitHub Check: freezed (packages/freezed, master, get)
  • GitHub Check: freezed (packages/freezed_annotation, master, get)
  • GitHub Check: freezed (packages/freezed_lint, master, get)
🔇 Additional comments (2)
packages/freezed_annotation/lib/freezed_annotation.dart (1)

87-87: Property addition looks good.

Introducing makeGeneratedClassesFinal is coherent with the PR objectives, and defaulting it to false preserves backward compatibility.

packages/freezed/test/integration/finalized.dart (1)

1-63: All new sealed and abstract classes are well-structured.

The usage of @Freezed(makeGeneratedClassesFinal: true) and sealed/abstract class definitions align with the intended design. No issues found.

Comment on lines 5 to 9
"explorer.fileNesting.enabled": true,
"explorer.fileNesting.expand": false,
"explorer.fileNesting.patterns": {
"*.dart": "${capture}.g.dart, ${capture}.freezed.dart",
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

Remove trailing comma to ensure valid JSON.

The trailing comma after "*.dart": "${capture}.g.dart, ${capture}.freezed.dart", causes a parsing error. JSON does not allow trailing commas.

-    "*.dart": "${capture}.g.dart, ${capture}.freezed.dart",
+    "*.dart": "${capture}.g.dart, ${capture}.freezed.dart"
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
"explorer.fileNesting.enabled": true,
"explorer.fileNesting.expand": false,
"explorer.fileNesting.patterns": {
"*.dart": "${capture}.g.dart, ${capture}.freezed.dart",
}
{
"explorer.fileNesting.enabled": true,
"explorer.fileNesting.expand": false,
"explorer.fileNesting.patterns": {
"*.dart": "${capture}.g.dart, ${capture}.freezed.dart"
}
}
🧰 Tools
🪛 Biome (1.9.4)

[error] 9-9: Expected a property but instead found '}'.

Expected a property here.

(parse)

@Yegair Yegair force-pushed the feature/finalized branch 2 times, most recently from 9994dbc to 10dd80e Compare March 15, 2025 11:28
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/finalized_test.dart (2)

165-201: Test name appears inconsistent with the expected behavior.

The test name on line 167 states "causes pattern_never_matches_value_type warning" but the assertion on line 198 expects no errors (isEmpty). This seems inconsistent and could mislead readers.

-        'causes pattern_never_matches_value_type warning when trying to match on pattern that can never match',
+        'does not cause pattern_never_matches_value_type warning when trying to match on pattern that can never match',

6-7: Consider adding documentation about the feature being tested.

Adding a brief comment at the top of the test group to explain the purpose of making classes final/sealed would improve clarity for future maintainers.

 void main() {
+  // Tests for the feature that marks generated classes as 'final' for concrete classes
+  // or 'sealed' for abstract classes to enable compile-time warnings for impossible pattern matches.
   group('marks generated classes as final', () {
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between df45cb2 and 10dd80e.

📒 Files selected for processing (6)
  • .vscode/settings.json (1 hunks)
  • packages/freezed/lib/src/models.dart (2 hunks)
  • packages/freezed/lib/src/templates/concrete_template.dart (1 hunks)
  • packages/freezed/test/finalized_test.dart (1 hunks)
  • packages/freezed/test/integration/finalized.dart (1 hunks)
  • packages/freezed_annotation/lib/freezed_annotation.dart (2 hunks)
✅ Files skipped from review due to trivial changes (1)
  • .vscode/settings.json
🚧 Files skipped from review as they are similar to previous changes (4)
  • packages/freezed/lib/src/templates/concrete_template.dart
  • packages/freezed_annotation/lib/freezed_annotation.dart
  • packages/freezed/lib/src/models.dart
  • packages/freezed/test/integration/finalized.dart
⏰ Context from checks skipped due to timeout of 90000ms (3)
  • GitHub Check: freezed (packages/freezed_annotation, master, get)
  • GitHub Check: freezed (packages/freezed_lint, master, get)
  • GitHub Check: freezed (packages/freezed, master, get)
🔇 Additional comments (2)
packages/freezed/test/finalized_test.dart (2)

1-203: Well-structured and comprehensive test suite for the new feature.

This test file thoroughly validates the behavior of generated classes marked as final and sealed in various scenarios. The tests verify that the Dart analyzer correctly raises warnings when pattern matching with incompatible types, which is exactly what the PR aims to achieve. Good job covering multiple scenarios including sealed classes, abstract classes, and their interactions with single/multiple constructors and inheritance.


133-163: Interesting behavior difference between sealed and abstract classes.

This test confirms that for abstract classes with a single constructor, no warnings are raised even when matching patterns that cannot match. This differs from sealed classes where warnings are issued. This is important behavior to document as it may not be immediately obvious to users.

@Yegair Yegair force-pushed the feature/finalized branch from 10dd80e to 0bd569a Compare March 15, 2025 11:32
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/finalized_test.dart (1)

135-135: Minor typo in test description.

There's a missing apostrophe in "doesnt".

-        'doesnt cause pattern_never_matches_value_type warning when trying to match on pattern that can never match',
+        'doesn\'t cause pattern_never_matches_value_type warning when trying to match on pattern that can never match',
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 10dd80e and 0bd569a.

📒 Files selected for processing (6)
  • .vscode/settings.json (1 hunks)
  • packages/freezed/lib/src/models.dart (2 hunks)
  • packages/freezed/lib/src/templates/concrete_template.dart (1 hunks)
  • packages/freezed/test/finalized_test.dart (1 hunks)
  • packages/freezed/test/integration/finalized.dart (1 hunks)
  • packages/freezed_annotation/lib/freezed_annotation.dart (2 hunks)
🚧 Files skipped from review as they are similar to previous changes (5)
  • .vscode/settings.json
  • packages/freezed/lib/src/templates/concrete_template.dart
  • packages/freezed_annotation/lib/freezed_annotation.dart
  • packages/freezed/lib/src/models.dart
  • packages/freezed/test/integration/finalized.dart
⏰ Context from checks skipped due to timeout of 90000ms (3)
  • GitHub Check: freezed (packages/freezed_annotation, master, get)
  • GitHub Check: freezed (packages/freezed_lint, master, get)
  • GitHub Check: freezed (packages/freezed, master, get)
🔇 Additional comments (2)
packages/freezed/test/finalized_test.dart (2)

1-204: Well-structured tests for the new final/sealed class feature.

The test suite thoroughly validates the expected behavior of making generated Freezed classes either final (for concrete classes) or sealed (for abstract classes). The tests correctly verify that:

  1. Sealed classes with a single constructor raise warnings for non-matching patterns
  2. Sealed classes with superclasses raise warnings for non-matching patterns
  3. Sealed classes with multiple constructors raise warnings for non-matching patterns
  4. Abstract classes (both with single and multiple constructors) don't raise warnings for non-matching patterns

This comprehensive coverage ensures the feature works as intended and will help catch pattern matching issues at compile time.


1-5: Imports look good.

The test imports the necessary analyzer packages to check for compile-time errors and warnings, which is appropriate for testing this feature.

@Yegair Yegair force-pushed the feature/finalized branch 2 times, most recently from 650feec to 2bdcffb Compare March 16, 2025 09:37
@Yegair
Copy link
Author

Yegair commented Mar 16, 2025

Updated the implementation, should be ready to review

  • added new parameter @Freezed(makeGeneratedClassesFinal: true) which defaults to null.
  • if makeGeneratedClassesFinal is null, it should be treated as if it was false, to not introduce a breaking change
  • added a few more tests, mostly to see if it works with custom super-/sub-classes

To really test it, I integrated it with the project I am working on (which contains hundreds of freezed classes). However, due to RevenueCat/purchases-flutter#1288 I am currently stuck on version 2.4.x. Had to add a dependency override for freezed_annotation: 3.0.0. If I'm not mistaken, it should not matter which version of freezed_annotation is present at runtime, since everything happens at build time. So my reasoning was that I can safely ignore any transitive freezed_annotation dependencies, because naturally those dependencies have already been built, but TBH I do still don't fully understand how build_runner works, so I am not entirely sure.

@Yegair Yegair requested a review from rrousselGit March 16, 2025 09:54
@rrousselGit
Copy link
Owner

rrousselGit commented Mar 16, 2025

Thinking about it, I wonder if this should be slightly generalised. We could have @Freezed(classKeywords: ['final'])
This would support base/mixin/sealed/...
(we'd need an assert error if the keyword is invalid)

I won't really want to move the goalpost forever though. But it feels like this change makes sense. Thought?

@Yegair
Copy link
Author

Yegair commented Mar 16, 2025

I do like the idea of using a more general API. Of course we can just use a List<String>, but it should also be possible to do something like

@Freezed(classModifiers: [FreezedClassModifier.Final])
abstract class Foo with _$Foo {
  factory Foo() = _Foo;
}

Might be a little more developer friendly, because one would get autocomplete. However, due to all modifiers being keywords we either have to use UpperCamelCase (like in the example) or something like FreezedClassModifier.$final.

When it comes to which modifiers should be supported, I think everything that makes the class effectively abstract is problematic, for example:

@Freezed(classModifiers: [FreezedClassModifier.Sealed])
abstract class Foo with _$Foo {
  factory Foo() = _Foo;
               // ^^^^
               // The redirecting constructor 'Foo' can't redirect to a constructor of the abstract class '_Foo'.
}

So that would leave the modifiers final, mixin, base, unless there is a way of using abstract, sealed, interface on the generated classes that I am not aware of.


That being said, I do have one "concern" with that change. Allowing more modifiers might lead to people using Freezed in unintended ways, which in turn might make future features more difficult to implement without breaking stuff. However, that is just a gut feeling that I have, and I can not support it with examples.

If I would have to make the decision, I would probably start by allowing final and base only. Then I'd wait and see if anyone asks for more modifiers.

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/lib/src/models.dart (1)

1330-1358: Consider refactoring validation for better maintainability

The validation logic works correctly, but consider a more data-driven approach for defining valid modifier combinations.

- void throwIfInvalid(Element? element) {
-   switch (this) {
-     case []:
-     case [FreezedClassModifier.Base]:
-     case [FreezedClassModifier.Base, FreezedClassModifier.Mixin]:
-     case [FreezedClassModifier.Mixin]:
-     case [FreezedClassModifier.Final]:
-       return;
-
-     default:
-       throw InvalidGenerationSourceError(
-         'invalid combination of class modifiers: ${map((modifier) => modifier.lexeme).join(' ')}',
-         element: element,
-       );
-   }
- }
+ // Define valid combinations as a static const Set for better maintainability
+ static const Set<Set<FreezedClassModifier>> validCombinations = {
+   {}, // Empty set
+   {FreezedClassModifier.Base},
+   {FreezedClassModifier.Base, FreezedClassModifier.Mixin},
+   {FreezedClassModifier.Mixin},
+   {FreezedClassModifier.Final},
+ };
+
+ void throwIfInvalid(Element? element) {
+   final thisSet = toSet();
+   if (!validCombinations.contains(thisSet)) {
+     throw InvalidGenerationSourceError(
+       'invalid combination of class modifiers: ${map((modifier) => modifier.lexeme).join(' ')}',
+       element: element,
+     );
+   }
+ }
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 2bdcffb and 35910fe.

📒 Files selected for processing (6)
  • packages/freezed/lib/src/models.dart (6 hunks)
  • packages/freezed/lib/src/templates/concrete_template.dart (1 hunks)
  • packages/freezed/test/integration/finalized.dart (1 hunks)
  • packages/freezed_annotation/lib/freezed_annotation.dart (3 hunks)
  • packages/freezed_annotation/lib/freezed_annotation.g.dart (2 hunks)
  • packages/freezed_annotation/test/freezed_test.dart (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (3)
  • packages/freezed/lib/src/templates/concrete_template.dart
  • packages/freezed_annotation/test/freezed_test.dart
  • packages/freezed/test/integration/finalized.dart
🔇 Additional comments (10)
packages/freezed_annotation/lib/freezed_annotation.dart (3)

87-87: Well-designed API addition for class modifiers

The new classModifiers parameter in the Freezed constructor is properly initialized with a default empty list, ensuring backward compatibility while introducing new functionality.


344-381: Excellent documentation with practical examples

The documentation for the classModifiers property is comprehensive and includes clear examples demonstrating how adding the 'final' modifier can lead to compile-time warnings for impossible pattern matches. This addresses the main goal of the PR to enhance type safety through sealed/final classes.


575-580: Good enum design following Dart conventions

The FreezedClassModifier enum follows standard Dart conventions with proper annotation for field renaming. The available options (Final, Base, Mixin) cover the most common class modifiers in Dart.

packages/freezed_annotation/lib/freezed_annotation.g.dart (2)

26-29: Proper deserialization of the new classModifiers field

The deserialization logic correctly handles the nullable list, maps each entry to the enum value, and provides an empty list as default value, consistent with the declaration in the Freezed class.


40-44: Well-structured enum mapping for serialization

The mapping between enum values and their string representations uses correct Dart syntax for the modifiers ('final', 'base', 'mixin'). This ensures proper serialization/deserialization of the FreezedClassModifier enum.

packages/freezed/lib/src/models.dart (5)

1070-1078: Clean implementation of the classModifiers getter

The getter appropriately handles empty modifier lists and formats non-empty lists with proper spacing for inclusion in Dart class declarations. This approach ensures seamless integration with existing code generation patterns.


1116-1116: Good integration of modifiers into ClassConfig

The modifiers field is properly added to the ClassConfig constructor parameter list, ensuring it's initialized when the config is created.


1141-1144: Robust validation pipeline for class modifiers

The implementation correctly normalizes and validates the modifiers before storing them, preventing invalid combinations that would lead to compilation errors in the generated code.


1207-1235: Thorough implementation of modifier decoding from annotations

The decoding logic handles null values appropriately and correctly extracts enum values from the Dart object representation, maintaining consistency with existing patterns in the codebase.


1320-1328: Simple and effective lexeme conversion

The extension method provides a clean way to convert enum values to their string representations, following best practices for extension methods in Dart.

@Yegair
Copy link
Author

Yegair commented Mar 16, 2025

Did a quick experiment on it. Let me know what you think of it. Not sure why CI is currently failing (something with Dart Format 🤔), tests should still be passing though.

@rrousselGit
Copy link
Owner

Ah, I kind of forgot that we wouldn't be able to mark the generated class sealed. Even though Freezed has inheritance now, the factory constructor still wouldn't work.

So we'd only gain mixin/base. That's a bit limited, so probably not worth the trouble

Do you mind reverting your last commit? I'll merge this then. I've already reviewed the previous changes.

@Yegair Yegair force-pushed the feature/finalized branch 2 times, most recently from ad558e6 to 5653ad5 Compare March 17, 2025 09:27
@Yegair
Copy link
Author

Yegair commented Mar 24, 2025

Didn't explicitly mention it, but from my side this PR is ready. Did a revert commit for the last one, but if you want a clean history you can just squash everything into a single commit when merging

@Yegair Yegair force-pushed the feature/finalized branch from 5653ad5 to d6d501e Compare August 17, 2025 07:14
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_annotation/test/freezed_test.dart (1)

159-187: Add negative-type and round-trip tests for make_generated_classes_final

To strengthen our .fromJson coverage:

• Add a failure-path test to verify that passing a non-bool value for make_generated_classes_final throws a cast error:

test('.fromJson (invalid make_generated_classes_final) throws', () {
  expect(
    () => Freezed.fromJson(<Object?, Object?>{
      'make_generated_classes_final': 42,
    }),
    throwsA(isA<TypeError>()),
  );
});

Place this alongside the existing explicit-override tests.

• Optionally, add a round-trip serialization check to guard against regressions in your generated JSON API:

test('Freezed annotation round-trip (fromJson ⇄ toJson)', () {
  final original = Freezed.fromJson(<Object?, Object?>{
    'fallback_union': 'test',
    'make_generated_classes_final': true,
    // …other flags…
  });
  final json = original.toJson();
  final same = Freezed.fromJson(json);

  expect(same.makeGeneratedClassesFinal, isTrue);
  expect(same, equals(original));
});
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 5653ad5 and d6d501e.

📒 Files selected for processing (7)
  • packages/freezed/lib/src/models.dart (2 hunks)
  • packages/freezed/lib/src/templates/concrete_template.dart (1 hunks)
  • packages/freezed/test/finalized_test.dart (1 hunks)
  • packages/freezed/test/integration/finalized.dart (1 hunks)
  • packages/freezed_annotation/lib/freezed_annotation.dart (2 hunks)
  • packages/freezed_annotation/lib/freezed_annotation.g.dart (1 hunks)
  • packages/freezed_annotation/test/freezed_test.dart (2 hunks)
🚧 Files skipped from review as they are similar to previous changes (6)
  • packages/freezed/lib/src/templates/concrete_template.dart
  • packages/freezed/test/finalized_test.dart
  • packages/freezed/lib/src/models.dart
  • packages/freezed_annotation/lib/freezed_annotation.dart
  • packages/freezed_annotation/lib/freezed_annotation.g.dart
  • packages/freezed/test/integration/finalized.dart
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: freezed (packages/freezed, stable, get)
🔇 Additional comments (3)
packages/freezed_annotation/test/freezed_test.dart (3)

154-157: Defaults for new and existing switches look correct

The assertions for addImplicitFinal=true, genericArgumentFactories=false, and makeGeneratedClassesFinal=null align with the intended tri-state default (null) for the new option. LGTM.


262-265: freezed defaults: asserting null is appropriate

Asserting that the annotation constant freezed.makeGeneratedClassesFinal remains null by default is consistent with the tri-state behavior and backwards compatibility goals. LGTM.


270-270: unfreezed defaults: null expectation is consistent

Validates parity with the freezed defaults for the new flag. Looks good.

@Yegair
Copy link
Author

Yegair commented Aug 17, 2025

@rrousselGit Just rebased this one onto the latest master. Do you think it could be merged? Would love to get rid of my forked freezed 😅

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.

3 participants