Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
72 changes: 45 additions & 27 deletions packages/freezed/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,34 +1,52 @@
## Unreleased 3.0.0

Freezed 3.0 is about supporting a "mixed mode".
From now on, Freezed supports both the usual syntax:

```dart
@freezed
sealed class Usual with _$Usual {
factory Usual({int a}) = _Usual;
}
```

But also:

```dart
@freezed
class Usual with _$Usual {
Usual({this.a});
final int a;
}
```

This has multiple benefits:

- Simple classes don't need Freezed's "weird" syntax and can stay simple
- Unions can keep using the usual `factory` syntax

It also has another benefit:
Complex Unions now have a way to use Inheritance and non-constant default values,
by relying on a non-factory `MyClass._()` constructor:

```dart
@freezed
sealed class Response<T> with _$Response<T> {
Response._({DateTime? time}) : time = time ?? DateTime.now();
// Constructors may enable passing parameters to ._();
factory Response.data(T value, {DateTime? time}) = ResponseData;
// If those parameters are named optionals, they are not required to be passed.
factory Response.error(Object error) = ResponseError;

@override
final DateTime time;
}
```

### Breaking changes:

- **Breaking**: Removed `map/when` and variants. These have been discouraged since Dart got pattern matching.
- **Breaking**: Freezed classes should now either be `abstract`, `sealed`, or manually implements `_$MyClass`.
- Inheritance and dynamic default values are now supported by specifying them in the `MyClass._()` constructor.
Inheritance example:
```dart
class BaseClass {
BaseClass.name(this.value);
final int value;
}
@freezed
abstract class Example extends BaseClass with _$Example {
// We can pass super values through the ._ constructor.
Example._(super.value): super.name();

factory Example(int value, String name) = _Example;
}
```
Dynamic default values example:
```dart
@freezed
abstract class Example with _$Example {
Example._(Duration? duration)
: duration ??= DateTime.now();

factory Example({Duration? duration}) = _Example;

final Duration? duration;
}
```

## 2.5.8 - 2025-01-06

Expand Down
6 changes: 3 additions & 3 deletions packages/freezed/build.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,11 @@ targets:
enabled: true
generate_for:
exclude:
- test
- example
- test/source_gen_src.dart
include:
- test/integration/*
- test/integration/**/*
- test/*
- test/**/*
source_gen|combining_builder:
options:
ignore_for_file:
Expand Down
4 changes: 3 additions & 1 deletion packages/freezed/lib/src/ast.dart
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import 'package:analyzer/dart/ast/ast.dart';
import 'package:analyzer/dart/ast/token.dart';

extension AstX on AstNode {
String get documentation {
String? get documentation {
final builder = StringBuffer();

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

if (builder.isEmpty) return null;

return builder.toString();
}
}
Expand Down
165 changes: 12 additions & 153 deletions packages/freezed/lib/src/freezed_generator.dart
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,6 @@ import 'package:analyzer/dart/ast/ast.dart';
import 'package:analyzer/dart/constant/value.dart';
import 'package:collection/collection.dart';
import 'package:freezed/src/templates/copy_with.dart';
import 'package:freezed/src/templates/properties.dart';
import 'package:freezed/src/tools/type.dart';
import 'package:freezed_annotation/freezed_annotation.dart' show Freezed;
import 'package:meta/meta.dart';
import 'package:source_gen/source_gen.dart';
Expand All @@ -21,14 +19,6 @@ extension StringX on String {
}
}

class CommonProperties {
/// Properties that have a getter in the abstract class
final List<Property> readableProperties = [];

/// Properties that are visible on `copyWith`
final List<Property> cloneableProperties = [];
}

@immutable
class FreezedGenerator extends ParserGenerator<Freezed> {
FreezedGenerator(this._buildYamlConfigs);
Expand Down Expand Up @@ -57,141 +47,14 @@ class FreezedGenerator extends ParserGenerator<Freezed> {
return Class.from(declaration, configs, globalConfigs: _buildYamlConfigs);
}

CommonProperties _commonParametersBetweenAllConstructors(Class data) {
final constructorsNeedsGeneration = data.constructors;

final result = CommonProperties();
if (constructorsNeedsGeneration case [final ctor]) {
result.cloneableProperties.addAll(
constructorsNeedsGeneration.first.parameters.allParameters
.map(Property.fromParameter),
);
result.readableProperties.addAll(result.cloneableProperties
.where((p) => ctor.isSynthetic(param: p.name)));
return result;
}

parameterLoop:
for (final parameter
in constructorsNeedsGeneration.first.parameters.allParameters) {
final isSynthetic =
constructorsNeedsGeneration.first.isSynthetic(param: parameter.name);

final library = parameter.parameterElement!.library!;

var commonTypeBetweenAllUnionConstructors =
parameter.parameterElement!.type;

for (final constructor in constructorsNeedsGeneration) {
final matchingParameter = constructor.parameters.allParameters
.firstWhereOrNull((p) => p.name == parameter.name);
// The property is not present in one of the union cases, so shouldn't
// be present in the abstract class.
if (matchingParameter == null) continue parameterLoop;

commonTypeBetweenAllUnionConstructors =
library.typeSystem.leastUpperBound(
commonTypeBetweenAllUnionConstructors,
matchingParameter.parameterElement!.type,
);
}

final matchingParameters = constructorsNeedsGeneration
.expand((element) => element.parameters.allParameters)
.where((element) => element.name == parameter.name)
.toList();

final isFinal = matchingParameters.any(
(element) =>
element.isFinal ||
element.parameterElement?.type !=
commonTypeBetweenAllUnionConstructors,
);

final nonNullableCommonType = library.typeSystem
.promoteToNonNull(commonTypeBetweenAllUnionConstructors);

final didDowncast = matchingParameters.any(
(element) =>
element.parameterElement?.type !=
commonTypeBetweenAllUnionConstructors,
);
final didNonNullDowncast = matchingParameters.any(
(element) =>
element.parameterElement?.type !=
commonTypeBetweenAllUnionConstructors &&
element.parameterElement?.type != nonNullableCommonType,
);
final didNullDowncast = !didNonNullDowncast && didDowncast;

final commonTypeString = resolveFullTypeStringFrom(
library,
commonTypeBetweenAllUnionConstructors,
);

final commonProperty = Property(
isFinal: isFinal,
type: commonTypeString,
isNullable: commonTypeBetweenAllUnionConstructors.isNullable,
isDartList: commonTypeBetweenAllUnionConstructors.isDartCoreList,
isDartMap: commonTypeBetweenAllUnionConstructors.isDartCoreMap,
isDartSet: commonTypeBetweenAllUnionConstructors.isDartCoreSet,
isPossiblyDartCollection:
commonTypeBetweenAllUnionConstructors.isPossiblyDartCollection,
name: parameter.name,
decorators: parameter.decorators,
defaultValueSource: parameter.defaultValueSource,
doc: parameter.doc,
// TODO support JsonKey
hasJsonKey: false,
);

if (isSynthetic) result.readableProperties.add(commonProperty);

// For {int a, int b, int c} | {int a, int? b, double c}, allows:
// copyWith({int a, int b})
// - int? b is not allowed because `null` is not compatible with the
// first union case.
// - num c is not allowed because num is not assignable int/double
if (!didNonNullDowncast) {
final copyWithType = didNullDowncast
? nonNullableCommonType
: commonTypeBetweenAllUnionConstructors;

result.cloneableProperties.add(
Property(
isFinal: isFinal,
type: resolveFullTypeStringFrom(
library,
copyWithType,
),
isNullable: copyWithType.isNullable,
isDartList: copyWithType.isDartCoreList,
isDartMap: copyWithType.isDartCoreMap,
isDartSet: copyWithType.isDartCoreSet,
isPossiblyDartCollection: copyWithType.isPossiblyDartCollection,
name: parameter.name,
decorators: parameter.decorators,
defaultValueSource: parameter.defaultValueSource,
doc: parameter.doc,
// TODO support JsonKey
hasJsonKey: false,
),
);
}
}

return result;
}

Iterable<DeepCloneableProperty> _getCommonDeepCloneableProperties(
List<ConstructorDetails> constructors,
CommonProperties commonProperties,
PropertyList commonProperties,
) sync* {
for (final commonProperty in commonProperties.cloneableProperties) {
final commonGetter = commonProperties.readableProperties
.firstWhereOrNull((e) => e.name == commonProperty.name);
final deepCopyProperty = constructors.first.deepCloneableProperties
final deepCopyProperty = constructors.firstOrNull?.deepCloneableProperties
.firstWhereOrNull((e) => e.name == commonProperty.name);

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

final commonProperties = _commonParametersBetweenAllConstructors(data);

final commonCopyWith = data.options.annotation.copyWith ??
commonProperties.cloneableProperties.isNotEmpty
data.properties.cloneableProperties.isNotEmpty
? CopyWith(
clonedClassName: data.name,
readableProperties: commonProperties.readableProperties,
cloneableProperties: commonProperties.cloneableProperties,
readableProperties: data.properties.readableProperties,
cloneableProperties: data.properties.cloneableProperties,
deepCloneableProperties: _getCommonDeepCloneableProperties(
data.constructors,
commonProperties,
data.properties,
).toList(),
genericsDefinition: data.genericsDefinitionTemplate,
genericsParameter: data.genericsParameterTemplate,
Expand All @@ -260,25 +121,23 @@ class FreezedGenerator extends ParserGenerator<Freezed> {
yield Abstract(
data: data,
copyWith: commonCopyWith,
commonProperties: commonProperties.readableProperties,
commonProperties: data.properties.readableProperties,
globalData: globalData,
);

for (final constructor in data.constructors) {
yield Concrete(
data: data,
constructor: constructor,
commonProperties: commonProperties.readableProperties,
commonProperties: data.properties.readableProperties,
globalData: globalData,
copyWith: data.options.annotation.copyWith ??
constructor.parameters.allParameters.isNotEmpty
? CopyWith(
clonedClassName: constructor.redirectedName,
cloneableProperties:
constructor.properties.map((e) => e.value).toList(),
readableProperties: constructor.properties
.where((e) => e.isSynthetic)
.map((e) => e.value)
.toList(),
cloneableProperties: constructor.properties.toList(),
readableProperties:
constructor.properties.where((e) => e.isSynthetic).toList(),
deepCloneableProperties: constructor.deepCloneableProperties,
genericsDefinition: data.genericsDefinitionTemplate,
genericsParameter: data.genericsParameterTemplate,
Expand Down
Loading