Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: make generated classes sealed/final #1121

Open
wants to merge 8 commits into
base: master
Choose a base branch
from

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
    • Introduced sealed classes with immutability in the Freezed package.
    • Added unit tests for validating behavior of final and sealed classes.
  • Bug Fixes
    • Updated dependency overrides for compatibility with Freezed requirements.
  • Documentation
    • Enhanced internal logic for JSON serialization and class structure.

finalize: true allows marking generated classes
final (or sealed if it would otherwise be an
abstract class).

This then causes the Dart analyzer to yield
pattern_never_matches_value_type warnings when
trying to match such classes against patterns that
can never match.
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
Removes [finalize: bool] configuration parameter
and instead makes it the default.
@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 :)

Copy link

coderabbitai bot commented Nov 3, 2024

Walkthrough

The changes include significant modifications to the Concrete class in concrete_template.dart, transitioning it to a final class and changing its superclass to a sealed class. This enforces immutability and restricts inheritance. Additionally, the pubspec_overrides.yaml file has updated dependency specifications for compatibility with the freezed package. Two new test files have been introduced: finalized_test.dart, which contains unit tests for sealed classes, and finalized.dart, which defines several new sealed classes with factory constructors.

Changes

File Path Change Summary
packages/freezed/lib/src/templates/concrete_template.dart Class declaration changed from class to final class and abstract to sealed. Updated internal logic and methods.
packages/freezed/pubspec_overrides.yaml Dependency overrides updated for compatibility; no new or removed dependencies.
packages/freezed/test/finalized_test.dart New unit tests added to validate behavior of final and sealed classes, checking for specific warnings.
packages/freezed/test/integration/finalized.dart New sealed classes (FinalizedFoo, FinalizedBar, FinalizedMultiple) defined with factory constructors.

Sequence Diagram(s)

sequenceDiagram
    participant User
    participant FinalizedTest
    participant FinalizedClasses

    User->>FinalizedClasses: Create FinalizedFoo
    FinalizedClasses-->>User: Return instance
    User->>FinalizedTest: Run tests on FinalizedFoo
    FinalizedTest-->>User: Return test results
Loading

🐰 "In the meadow where changes bloom,
Final classes rise, dispelling gloom.
Sealed and safe, they stand so tall,
With tests in place to catch the fall.
A hop, a skip, through code we glide,
Celebrating structure, with joy and pride!" 🌼


Thank you for using CodeRabbit. We offer it for free to the OSS community and would appreciate your support in helping us grow. If you find it useful, would you consider giving us a shout-out on your favorite social media?

❤️ 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.
    • Generate unit testing code for this file.
    • 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. Examples:
    • @coderabbitai generate unit testing code for this file.
    • @coderabbitai modularize this function.
  • 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 src/utils.ts and generate unit testing code.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.
    • @coderabbitai help me debug CodeRabbit configuration file.

Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments.

CodeRabbit Commands (Invoked using PR comments)

  • @coderabbitai pause to pause the reviews on a PR.
  • @coderabbitai resume to resume the paused reviews.
  • @coderabbitai review to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
  • @coderabbitai full review to do a full review from scratch and review all the files again.
  • @coderabbitai summary to regenerate the summary of the PR.
  • @coderabbitai resolve resolve all the CodeRabbit review comments.
  • @coderabbitai configuration to show the current CodeRabbit configuration for the repository.
  • @coderabbitai help to get help.

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

Documentation and Community

  • 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.

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