Skip to content

Commit b51e44f

Browse files
authoredFeb 23, 2025
Support using Freezed on normal Dart classes (#1157)
1 parent a58c4f6 commit b51e44f

22 files changed

+943
-478
lines changed
 

‎packages/freezed/CHANGELOG.md

+45-27
Original file line numberDiff line numberDiff line change
@@ -1,34 +1,52 @@
11
## Unreleased 3.0.0
22

3+
Freezed 3.0 is about supporting a "mixed mode".
4+
From now on, Freezed supports both the usual syntax:
5+
6+
```dart
7+
@freezed
8+
sealed class Usual with _$Usual {
9+
factory Usual({int a}) = _Usual;
10+
}
11+
```
12+
13+
But also:
14+
15+
```dart
16+
@freezed
17+
class Usual with _$Usual {
18+
Usual({this.a});
19+
final int a;
20+
}
21+
```
22+
23+
This has multiple benefits:
24+
25+
- Simple classes don't need Freezed's "weird" syntax and can stay simple
26+
- Unions can keep using the usual `factory` syntax
27+
28+
It also has another benefit:
29+
Complex Unions now have a way to use Inheritance and non-constant default values,
30+
by relying on a non-factory `MyClass._()` constructor:
31+
32+
```dart
33+
@freezed
34+
sealed class Response<T> with _$Response<T> {
35+
Response._({DateTime? time}) : time = time ?? DateTime.now();
36+
// Constructors may enable passing parameters to ._();
37+
factory Response.data(T value, {DateTime? time}) = ResponseData;
38+
// If those parameters are named optionals, they are not required to be passed.
39+
factory Response.error(Object error) = ResponseError;
40+
41+
@override
42+
final DateTime time;
43+
}
44+
```
45+
46+
### Breaking changes:
47+
348
- **Breaking**: Removed `map/when` and variants. These have been discouraged since Dart got pattern matching.
449
- **Breaking**: Freezed classes should now either be `abstract`, `sealed`, or manually implements `_$MyClass`.
5-
- Inheritance and dynamic default values are now supported by specifying them in the `MyClass._()` constructor.
6-
Inheritance example:
7-
```dart
8-
class BaseClass {
9-
BaseClass.name(this.value);
10-
final int value;
11-
}
12-
@freezed
13-
abstract class Example extends BaseClass with _$Example {
14-
// We can pass super values through the ._ constructor.
15-
Example._(super.value): super.name();
16-
17-
factory Example(int value, String name) = _Example;
18-
}
19-
```
20-
Dynamic default values example:
21-
```dart
22-
@freezed
23-
abstract class Example with _$Example {
24-
Example._(Duration? duration)
25-
: duration ??= DateTime.now();
26-
27-
factory Example({Duration? duration}) = _Example;
28-
29-
final Duration? duration;
30-
}
31-
```
3250

3351
## 2.5.8 - 2025-01-06
3452

‎packages/freezed/build.yaml

+3-3
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,11 @@ targets:
55
enabled: true
66
generate_for:
77
exclude:
8-
- test
98
- example
9+
- test/source_gen_src.dart
1010
include:
11-
- test/integration/*
12-
- test/integration/**/*
11+
- test/*
12+
- test/**/*
1313
source_gen|combining_builder:
1414
options:
1515
ignore_for_file:

‎packages/freezed/lib/src/ast.dart

+3-1
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import 'package:analyzer/dart/ast/ast.dart';
22
import 'package:analyzer/dart/ast/token.dart';
33

44
extension AstX on AstNode {
5-
String get documentation {
5+
String? get documentation {
66
final builder = StringBuffer();
77

88
for (Token? token = beginToken.precedingComments;
@@ -11,6 +11,8 @@ extension AstX on AstNode {
1111
builder.writeln(token);
1212
}
1313

14+
if (builder.isEmpty) return null;
15+
1416
return builder.toString();
1517
}
1618
}

‎packages/freezed/lib/src/freezed_generator.dart

+12-153
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,6 @@ import 'package:analyzer/dart/ast/ast.dart';
22
import 'package:analyzer/dart/constant/value.dart';
33
import 'package:collection/collection.dart';
44
import 'package:freezed/src/templates/copy_with.dart';
5-
import 'package:freezed/src/templates/properties.dart';
6-
import 'package:freezed/src/tools/type.dart';
75
import 'package:freezed_annotation/freezed_annotation.dart' show Freezed;
86
import 'package:meta/meta.dart';
97
import 'package:source_gen/source_gen.dart';
@@ -21,14 +19,6 @@ extension StringX on String {
2119
}
2220
}
2321

24-
class CommonProperties {
25-
/// Properties that have a getter in the abstract class
26-
final List<Property> readableProperties = [];
27-
28-
/// Properties that are visible on `copyWith`
29-
final List<Property> cloneableProperties = [];
30-
}
31-
3222
@immutable
3323
class FreezedGenerator extends ParserGenerator<Freezed> {
3424
FreezedGenerator(this._buildYamlConfigs);
@@ -57,141 +47,14 @@ class FreezedGenerator extends ParserGenerator<Freezed> {
5747
return Class.from(declaration, configs, globalConfigs: _buildYamlConfigs);
5848
}
5949

60-
CommonProperties _commonParametersBetweenAllConstructors(Class data) {
61-
final constructorsNeedsGeneration = data.constructors;
62-
63-
final result = CommonProperties();
64-
if (constructorsNeedsGeneration case [final ctor]) {
65-
result.cloneableProperties.addAll(
66-
constructorsNeedsGeneration.first.parameters.allParameters
67-
.map(Property.fromParameter),
68-
);
69-
result.readableProperties.addAll(result.cloneableProperties
70-
.where((p) => ctor.isSynthetic(param: p.name)));
71-
return result;
72-
}
73-
74-
parameterLoop:
75-
for (final parameter
76-
in constructorsNeedsGeneration.first.parameters.allParameters) {
77-
final isSynthetic =
78-
constructorsNeedsGeneration.first.isSynthetic(param: parameter.name);
79-
80-
final library = parameter.parameterElement!.library!;
81-
82-
var commonTypeBetweenAllUnionConstructors =
83-
parameter.parameterElement!.type;
84-
85-
for (final constructor in constructorsNeedsGeneration) {
86-
final matchingParameter = constructor.parameters.allParameters
87-
.firstWhereOrNull((p) => p.name == parameter.name);
88-
// The property is not present in one of the union cases, so shouldn't
89-
// be present in the abstract class.
90-
if (matchingParameter == null) continue parameterLoop;
91-
92-
commonTypeBetweenAllUnionConstructors =
93-
library.typeSystem.leastUpperBound(
94-
commonTypeBetweenAllUnionConstructors,
95-
matchingParameter.parameterElement!.type,
96-
);
97-
}
98-
99-
final matchingParameters = constructorsNeedsGeneration
100-
.expand((element) => element.parameters.allParameters)
101-
.where((element) => element.name == parameter.name)
102-
.toList();
103-
104-
final isFinal = matchingParameters.any(
105-
(element) =>
106-
element.isFinal ||
107-
element.parameterElement?.type !=
108-
commonTypeBetweenAllUnionConstructors,
109-
);
110-
111-
final nonNullableCommonType = library.typeSystem
112-
.promoteToNonNull(commonTypeBetweenAllUnionConstructors);
113-
114-
final didDowncast = matchingParameters.any(
115-
(element) =>
116-
element.parameterElement?.type !=
117-
commonTypeBetweenAllUnionConstructors,
118-
);
119-
final didNonNullDowncast = matchingParameters.any(
120-
(element) =>
121-
element.parameterElement?.type !=
122-
commonTypeBetweenAllUnionConstructors &&
123-
element.parameterElement?.type != nonNullableCommonType,
124-
);
125-
final didNullDowncast = !didNonNullDowncast && didDowncast;
126-
127-
final commonTypeString = resolveFullTypeStringFrom(
128-
library,
129-
commonTypeBetweenAllUnionConstructors,
130-
);
131-
132-
final commonProperty = Property(
133-
isFinal: isFinal,
134-
type: commonTypeString,
135-
isNullable: commonTypeBetweenAllUnionConstructors.isNullable,
136-
isDartList: commonTypeBetweenAllUnionConstructors.isDartCoreList,
137-
isDartMap: commonTypeBetweenAllUnionConstructors.isDartCoreMap,
138-
isDartSet: commonTypeBetweenAllUnionConstructors.isDartCoreSet,
139-
isPossiblyDartCollection:
140-
commonTypeBetweenAllUnionConstructors.isPossiblyDartCollection,
141-
name: parameter.name,
142-
decorators: parameter.decorators,
143-
defaultValueSource: parameter.defaultValueSource,
144-
doc: parameter.doc,
145-
// TODO support JsonKey
146-
hasJsonKey: false,
147-
);
148-
149-
if (isSynthetic) result.readableProperties.add(commonProperty);
150-
151-
// For {int a, int b, int c} | {int a, int? b, double c}, allows:
152-
// copyWith({int a, int b})
153-
// - int? b is not allowed because `null` is not compatible with the
154-
// first union case.
155-
// - num c is not allowed because num is not assignable int/double
156-
if (!didNonNullDowncast) {
157-
final copyWithType = didNullDowncast
158-
? nonNullableCommonType
159-
: commonTypeBetweenAllUnionConstructors;
160-
161-
result.cloneableProperties.add(
162-
Property(
163-
isFinal: isFinal,
164-
type: resolveFullTypeStringFrom(
165-
library,
166-
copyWithType,
167-
),
168-
isNullable: copyWithType.isNullable,
169-
isDartList: copyWithType.isDartCoreList,
170-
isDartMap: copyWithType.isDartCoreMap,
171-
isDartSet: copyWithType.isDartCoreSet,
172-
isPossiblyDartCollection: copyWithType.isPossiblyDartCollection,
173-
name: parameter.name,
174-
decorators: parameter.decorators,
175-
defaultValueSource: parameter.defaultValueSource,
176-
doc: parameter.doc,
177-
// TODO support JsonKey
178-
hasJsonKey: false,
179-
),
180-
);
181-
}
182-
}
183-
184-
return result;
185-
}
186-
18750
Iterable<DeepCloneableProperty> _getCommonDeepCloneableProperties(
18851
List<ConstructorDetails> constructors,
189-
CommonProperties commonProperties,
52+
PropertyList commonProperties,
19053
) sync* {
19154
for (final commonProperty in commonProperties.cloneableProperties) {
19255
final commonGetter = commonProperties.readableProperties
19356
.firstWhereOrNull((e) => e.name == commonProperty.name);
194-
final deepCopyProperty = constructors.first.deepCloneableProperties
57+
final deepCopyProperty = constructors.firstOrNull?.deepCloneableProperties
19558
.firstWhereOrNull((e) => e.name == commonProperty.name);
19659

19760
if (deepCopyProperty == null || commonGetter == null) continue;
@@ -239,17 +102,15 @@ class FreezedGenerator extends ParserGenerator<Freezed> {
239102
) sync* {
240103
if (data.options.fromJson) yield FromJson(data);
241104

242-
final commonProperties = _commonParametersBetweenAllConstructors(data);
243-
244105
final commonCopyWith = data.options.annotation.copyWith ??
245-
commonProperties.cloneableProperties.isNotEmpty
106+
data.properties.cloneableProperties.isNotEmpty
246107
? CopyWith(
247108
clonedClassName: data.name,
248-
readableProperties: commonProperties.readableProperties,
249-
cloneableProperties: commonProperties.cloneableProperties,
109+
readableProperties: data.properties.readableProperties,
110+
cloneableProperties: data.properties.cloneableProperties,
250111
deepCloneableProperties: _getCommonDeepCloneableProperties(
251112
data.constructors,
252-
commonProperties,
113+
data.properties,
253114
).toList(),
254115
genericsDefinition: data.genericsDefinitionTemplate,
255116
genericsParameter: data.genericsParameterTemplate,
@@ -260,25 +121,23 @@ class FreezedGenerator extends ParserGenerator<Freezed> {
260121
yield Abstract(
261122
data: data,
262123
copyWith: commonCopyWith,
263-
commonProperties: commonProperties.readableProperties,
124+
commonProperties: data.properties.readableProperties,
125+
globalData: globalData,
264126
);
265127

266128
for (final constructor in data.constructors) {
267129
yield Concrete(
268130
data: data,
269131
constructor: constructor,
270-
commonProperties: commonProperties.readableProperties,
132+
commonProperties: data.properties.readableProperties,
271133
globalData: globalData,
272134
copyWith: data.options.annotation.copyWith ??
273135
constructor.parameters.allParameters.isNotEmpty
274136
? CopyWith(
275137
clonedClassName: constructor.redirectedName,
276-
cloneableProperties:
277-
constructor.properties.map((e) => e.value).toList(),
278-
readableProperties: constructor.properties
279-
.where((e) => e.isSynthetic)
280-
.map((e) => e.value)
281-
.toList(),
138+
cloneableProperties: constructor.properties.toList(),
139+
readableProperties:
140+
constructor.properties.where((e) => e.isSynthetic).toList(),
282141
deepCloneableProperties: constructor.deepCloneableProperties,
283142
genericsDefinition: data.genericsDefinitionTemplate,
284143
genericsParameter: data.genericsParameterTemplate,

0 commit comments

Comments
 (0)