diff --git a/packages/freezed/CHANGELOG.md b/packages/freezed/CHANGELOG.md index da3c2458..501322c3 100644 --- a/packages/freezed/CHANGELOG.md +++ b/packages/freezed/CHANGELOG.md @@ -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 with _$Response { + 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 diff --git a/packages/freezed/build.yaml b/packages/freezed/build.yaml index bb7dec1e..79e38688 100644 --- a/packages/freezed/build.yaml +++ b/packages/freezed/build.yaml @@ -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: diff --git a/packages/freezed/lib/src/ast.dart b/packages/freezed/lib/src/ast.dart index 53d82866..2b18d35d 100644 --- a/packages/freezed/lib/src/ast.dart +++ b/packages/freezed/lib/src/ast.dart @@ -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; @@ -11,6 +11,8 @@ extension AstX on AstNode { builder.writeln(token); } + if (builder.isEmpty) return null; + return builder.toString(); } } diff --git a/packages/freezed/lib/src/freezed_generator.dart b/packages/freezed/lib/src/freezed_generator.dart index a4b7592b..7ec0e2ce 100644 --- a/packages/freezed/lib/src/freezed_generator.dart +++ b/packages/freezed/lib/src/freezed_generator.dart @@ -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'; @@ -21,14 +19,6 @@ extension StringX on String { } } -class CommonProperties { - /// Properties that have a getter in the abstract class - final List readableProperties = []; - - /// Properties that are visible on `copyWith` - final List cloneableProperties = []; -} - @immutable class FreezedGenerator extends ParserGenerator { FreezedGenerator(this._buildYamlConfigs); @@ -57,141 +47,14 @@ class FreezedGenerator extends ParserGenerator { 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 _getCommonDeepCloneableProperties( List 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; @@ -239,17 +102,15 @@ class FreezedGenerator extends ParserGenerator { ) 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, @@ -260,25 +121,23 @@ class FreezedGenerator extends ParserGenerator { 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, diff --git a/packages/freezed/lib/src/models.dart b/packages/freezed/lib/src/models.dart index 6fc34963..a93bba23 100644 --- a/packages/freezed/lib/src/models.dart +++ b/packages/freezed/lib/src/models.dart @@ -145,6 +145,21 @@ class DeepCloneableProperty { } } +extension on FormalParameter { + TypeAnnotation? typeAnnotation() { + final that = this; + return switch (that) { + DefaultFormalParameter() => that.parameter.typeAnnotation(), + FieldFormalParameter() => that.type, + FunctionTypedFormalParameter() => throw UnsupportedError( + 'Parameters of format `T name()` are not supported. Use `T Function()` name.', + ), + SimpleFormalParameter() => that.type, + SuperFormalParameter() => that.type, + }; + } +} + /// The information of a specific constructor of a class tagged with ``. /// /// This only includes constructors where Freezed needs to generate something. @@ -173,10 +188,15 @@ class ConstructorDetails { ClassDeclaration declaration, ConstructorDeclaration constructor, ) { - if (constructor.factoryKeyword == null && constructor.name?.lexeme != '_') { + final freezedCtors = declaration.constructors.where( + (e) => e.factoryKeyword != null && e.redirectedConstructor != null, + ); + + if (constructor.factoryKeyword == null && + constructor.name?.lexeme != '_' && + freezedCtors.isNotEmpty) { throw InvalidGenerationSourceError( - 'Classes decorated with @freezed can only have a single non-factory' - ', and must be named MyClass._()', + 'Classes decorated with @freezed can only have a single non-factory constructor.', element: constructor.declaredElement, ); } @@ -184,20 +204,17 @@ class ConstructorDetails { if (constructor.name?.lexeme == '_') { for (final param in constructor.parameters.parameters) { if (param.isPositional) { - final otherFreezedCtors = declaration.constructors.where((e) => - e.factoryKeyword != null && e.redirectedConstructor != null); - - for (final ctor in otherFreezedCtors) { + for (final ctor in freezedCtors) { final hasMatchingParam = ctor.parameters.parameters .any((e) => e.name?.lexeme == param.name?.lexeme); if (hasMatchingParam) continue; throw InvalidGenerationSourceError( ''' -The constructor MyClass._() specified a positional parameter named ${param.name}, +A non-factory constructor specified a positional parameter named ${param.name}, but at least one constructor does not have a matching parameter. -When specifying fields in MyClass._(), either: +When specifying fields in non-factory constructor then specifying factory constructors, either: - the parameter should be named - or all constructors in the class should specify that parameter. ''', @@ -270,12 +287,10 @@ When specifying fields in MyClass._(), either: final allProperties = [ for (final parameter in constructor.parameters.parameters) - ( + Property.fromFormalParameter( + parameter, + addImplicitFinal: configs.annotation.addImplicitFinal, isSynthetic: !excludedProperties.contains(parameter.name!.lexeme), - value: Property.fromFormalParameter( - parameter, - addImplicitFinal: configs.annotation.addImplicitFinal, - ), ), ]; @@ -320,7 +335,7 @@ When specifying fields in MyClass._(), either: globalConfigs, ).toList(), parameters: ParametersTemplate.fromParameterList( - constructor.parameters, + constructor.parameters.parameters, addImplicitFinal: configs.annotation.addImplicitFinal, ), redirectedName: redirectedName, @@ -328,13 +343,6 @@ When specifying fields in MyClass._(), either: ); } - if (result.isEmpty) { - throw InvalidGenerationSourceError( - 'Marked ${declaration.name} with @freezed, but freezed has nothing to generate', - element: declaration.declaredElement, - ); - } - if (result.length > 1 && result.any((c) => c.name.startsWith('_'))) { throw InvalidGenerationSourceError( 'A freezed union cannot have private constructors', @@ -358,7 +366,7 @@ When specifying fields in MyClass._(), either: final bool isConst; final String redirectedName; final ParametersTemplate parameters; - final List<({Property value, bool isSynthetic})> properties; + final List properties; final bool isDefault; final bool isFallback; final bool hasJsonSerializable; @@ -374,7 +382,7 @@ When specifying fields in MyClass._(), either: bool isSynthetic({required String param}) { return properties - .where((element) => element.value.name == param) + .where((element) => element.name == param) .first .isSynthetic; } @@ -459,8 +467,8 @@ class AssertAnnotation { } } -class SuperInvocation { - SuperInvocation({ +class ConstructorInvocation { + ConstructorInvocation({ required this.name, required this.positional, required this.named, @@ -470,6 +478,12 @@ class SuperInvocation { final List named; } +class CopyWithTarget { + CopyWithTarget({required this.parameters, required this.name}); + final ParametersTemplate parameters; + final String? name; +} + class Class { Class({ required this.name, @@ -479,6 +493,8 @@ class Class { required this.genericsDefinitionTemplate, required this.genericsParameterTemplate, required this.superCall, + required this.properties, + required this.copyWithTarget, }) : assert(constructors.isNotEmpty); final String name; @@ -487,7 +503,9 @@ class Class { final List constructors; final GenericsDefinitionTemplate genericsDefinitionTemplate; final GenericsParameterTemplate genericsParameterTemplate; - final SuperInvocation? superCall; + final ConstructorInvocation? superCall; + final CopyWithTarget? copyWithTarget; + final PropertyList properties; static Class from( ClassDeclaration declaration, @@ -508,9 +526,38 @@ class Class { globalConfigs: globalConfigs, ); + final properties = PropertyList() + ..readableProperties.addAll( + _computeReadableProperties(declaration, constructors), + ) + ..cloneableProperties.addAll( + _computeCloneableProperties( + declaration, + constructors, + configs, + ), + ); + + final copyWithTarget = + constructors.isNotEmpty ? null : declaration.copyWithTarget; + final copyWithInvocation = copyWithTarget == null + ? null + : CopyWithTarget( + name: copyWithTarget.name?.lexeme, + parameters: ParametersTemplate.fromParameterList( + // Only include parameters that are cloneable + copyWithTarget.parameters.parameters.where((e) { + return properties.cloneableProperties + .map((e) => e.name) + .contains(e.name!.lexeme); + }), + addImplicitFinal: configs.annotation.addImplicitFinal, + ), + ); + final superCall = privateCtor == null ? null - : SuperInvocation( + : ConstructorInvocation( name: '_', positional: privateCtor.parameters.parameters .where((e) => e.isPositional) @@ -524,6 +571,8 @@ class Class { return Class( name: declaration.name.lexeme, + copyWithTarget: copyWithInvocation, + properties: properties, superCall: superCall, options: configs, constructors: constructors, @@ -540,6 +589,312 @@ class Class { ); } + static Iterable _computeCloneableProperties( + ClassDeclaration declaration, + List constructorsNeedsGeneration, + ClassConfig configs, + ) sync* { + if (constructorsNeedsGeneration.isNotEmpty) { + yield* _commonParametersBetweenAllConstructors( + constructorsNeedsGeneration, + ).cloneableProperties; + return; + } + + // Pick `(default ?? _)` constructor + final targetConstructor = declaration.copyWithTarget; + if (targetConstructor == null) return; + + for (final parameter in targetConstructor.parameters.parameters) { + yield Property.fromFormalParameter( + parameter, + addImplicitFinal: configs.annotation.addImplicitFinal, + isSynthetic: false, + ); + } + } + + static Iterable _computeReadableProperties( + ClassDeclaration declaration, + List constructorsNeedsGeneration, + ) sync* { + final typesMap = decorators, + })?>>{}; + void setForName({ + required String name, + required TypeAnnotation? type, + required int index, + required String? doc, + required bool isFinal, + required bool isSynthetic, + required List decorators, + }) { + final list = typesMap.putIfAbsent( + name, + () => List.filled(constructorsNeedsGeneration.length + 1, null), + ); + list[index] = ( + type: type, + doc: doc, + isFinal: isFinal, + isSynthetic: isSynthetic, + decorators: decorators, + ); + } + + final properties = declaration.properties; + + for (final property in properties) { + setForName( + name: property.$2.name.lexeme, + type: property.$1.fields.type, + index: 0, + doc: property.$1.documentation, + isFinal: property.$1.fields.isFinal, + isSynthetic: false, + decorators: switch (property.$1.declaredElement) { + final e? => parseDecorators(e.metadata), + null => [], + }, + ); + } + + for (final (index, freezedCtor) in constructorsNeedsGeneration.indexed) { + final ctor = declaration.constructors + .where((e) => (e.name?.lexeme ?? '') == freezedCtor.name) + .first; + + for (final parameter in ctor.parameters.parameters) { + final freezedParameter = freezedCtor.parameters.allParameters + .where((e) => e.name == parameter.name?.lexeme) + .first; + + setForName( + name: parameter.name!.lexeme, + type: parameter.typeAnnotation(), + index: index + 1, + isSynthetic: true, + doc: parameter.documentation, + isFinal: freezedParameter.isFinal, + decorators: freezedParameter.decorators, + ); + } + } + + late final typeSystem = declaration.declaredElement!.library.typeSystem; + late final typeProvider = declaration.declaredElement!.library.typeProvider; + + fieldLoop: + for (final entry in typesMap.entries) { + final name = entry.key; + + // late for: https://github.com/dart-lang/language/issues/4272 + late String typeString; + String? doc; + late bool isFinal; + late DartType type; + late List decorators; + late bool isSynthetic; + switch (entry.value) { + case []: + throw AssertionError('Unreachable'); + // Only fields + case [final fieldType?, ...]: + // Only a single constructor and no field + case [null, final fieldType?]: + type = fieldType.type?.type ?? typeProvider.dynamicType; + typeString = fieldType.type?.toSource() ?? type.toString(); + doc = fieldType.doc; + isFinal = fieldType.isFinal; + decorators = fieldType.decorators; + isSynthetic = fieldType.isSynthetic; + case [...]: + final fields = entry.value.skip(1); + // Field not present in all constructors, so skip it as we can't read it from the interface. + if (fields.contains(null)) continue fieldLoop; + + doc = fields.map((e) => e!.doc).nonNulls.firstOrNull; + decorators = fields.expand((e) => e!.decorators).toSet().toList(); + isSynthetic = true; + + final typeSources = fields.map((e) => e?.type?.toSource()).toSet(); + if (typeSources.length == 1) { + type = fields + .map((e) => e!.type?.type ?? typeProvider.dynamicType) + .first; + // All constructors use the exact same type. No need to check lower-bounds, + // and we can paste the type in the generated source directly. + typeString = typeSources.single ?? type.toString(); + isFinal = fields.any((e) => e!.isFinal); + + break; + } + + type = fields + .map((e) => e!.type?.type ?? typeProvider.dynamicType) + .reduce((a, b) => typeSystem.leastUpperBound(a, b)); + isFinal = true; + + typeString = resolveFullTypeStringFrom( + declaration.declaredElement!.library, + type, + ); + } + + yield Property( + name: name, + type: typeString, + isNullable: type.isNullable, + isDartList: type.isDartCoreList, + isDartMap: type.isDartCoreMap, + isDartSet: type.isDartCoreSet, + isPossiblyDartCollection: type.isPossiblyDartCollection, + isFinal: isFinal, + isSynthetic: isSynthetic, + decorators: decorators, + defaultValueSource: null, + doc: doc ?? '', + hasJsonKey: false, + ); + } + } + + static PropertyList _commonParametersBetweenAllConstructors( + List constructorsNeedsGeneration, + ) { + final result = PropertyList(); + if (constructorsNeedsGeneration.isEmpty) return result; + + if (constructorsNeedsGeneration case [final ctor]) { + result.cloneableProperties.addAll( + constructorsNeedsGeneration.first.parameters.allParameters + .map((e) => Property.fromParameter(e, isSynthetic: true)), + ); + 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, + hasJsonKey: false, + isSynthetic: true, + ); + + 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, + ), + isSynthetic: true, + 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, + hasJsonKey: false, + ), + ); + } + } + + return result; + } + static void _assertValidFieldUsage( FieldElement field, { required bool shouldUseExtends, @@ -572,6 +927,26 @@ class Class { ); } } + + String get escapedName { + var generics = + genericsParameterTemplate.typeParameters.map((e) => '\$$e').join(', '); + if (generics.isNotEmpty) { + generics = '<$generics>'; + } + + final escapedElementName = name.replaceAll(r'$', r'\$'); + + return '$escapedElementName$generics'; + } +} + +class PropertyList { + /// Properties that have a getter in the abstract class + final List readableProperties = []; + + /// Properties that are visible on `copyWith` + final List cloneableProperties = []; } class Library { @@ -706,10 +1081,29 @@ class ClassConfig { } extension ClassDeclarationX on ClassDeclaration { + /// Pick either Class(), Class._() or the first constructor found, in that order. + ConstructorDeclaration? get copyWithTarget { + return constructors.fold(null, (acc, ctor) { + if (ctor.name == null) return ctor; + if (ctor.name!.lexeme == '_') return acc ?? ctor; + return acc; + }) ?? + constructors.firstOrNull; + } + Iterable get constructors { return members.whereType(); } + Iterable<(FieldDeclaration, VariableDeclaration)> get properties { + return members + .whereType() + .where((e) => !e.isStatic) + .expand( + (e) => e.fields.variables.map((f) => (e, f)), + ); + } + bool needsJsonSerializable(Library library) { if (!library.hasJson) return false; diff --git a/packages/freezed/lib/src/templates/abstract_template.dart b/packages/freezed/lib/src/templates/abstract_template.dart index 239037e6..f0c79036 100644 --- a/packages/freezed/lib/src/templates/abstract_template.dart +++ b/packages/freezed/lib/src/templates/abstract_template.dart @@ -1,24 +1,35 @@ import 'package:freezed/src/freezed_generator.dart'; import 'package:freezed/src/models.dart'; +import 'concrete_template.dart'; import 'copy_with.dart'; import 'properties.dart'; -import 'prototypes.dart'; class Abstract { Abstract({ required this.data, required this.copyWith, required this.commonProperties, + required this.globalData, }); final Class data; final CopyWith? copyWith; final List commonProperties; + final Library globalData; @override String toString() { + final needsAbstractGetters = data.options.toJson || + copyWith != null || + data.options.asString || + data.options.equal; + final abstractProperties = commonProperties + // If toJson/==/toString/copyWith are enabled, always generate a `T get field`, + // to enable those methods to use all properties. + // Otherwise only do so for fields generated by factory constructors. + .where((e) => needsAbstractGetters || e.isSynthetic) .expand((e) => [ e.abstractGetter, if (!e.isFinal) e.abstractSetter, @@ -30,25 +41,19 @@ class Abstract { mixin _\$${data.name.public}${data.genericsDefinitionTemplate} { $abstractProperties -$_toJson -${copyWith?.abstractCopyWithGetter ?? ''} +${copyWith?.copyWithGetter(needsCast: true) ?? ''} +${methods( + data, + globalData, + properties: commonProperties, + name: data.name, + escapedName: data.escapedName, + source: Source.mixin, + )} } ${copyWith?.commonInterface ?? ''} ${copyWith?.commonConcreteImpl ?? ''} -'''; - } - - String get _toJsonParams => toJsonParameters( - data.genericsParameterTemplate, - data.options.genericArgumentFactories, - ); - String get _toJson { - if (!data.options.toJson) return ''; - - return ''' - /// Serializes this ${data.name} to a JSON map. - Map toJson($_toJsonParams); '''; } } diff --git a/packages/freezed/lib/src/templates/concrete_template.dart b/packages/freezed/lib/src/templates/concrete_template.dart index 72111caf..ade783e2 100644 --- a/packages/freezed/lib/src/templates/concrete_template.dart +++ b/packages/freezed/lib/src/templates/concrete_template.dart @@ -11,6 +11,12 @@ import 'copy_with.dart'; import 'parameter_template.dart'; import 'prototypes.dart'; +sealed class Foo {} + +class Bar extends Foo {} + +class Baz extends Foo {} + class Concrete { Concrete({ required this.constructor, @@ -30,16 +36,13 @@ class Concrete { (data.options.toJson || data.options.fromJson) && data.constructors.length > 1 && constructor.properties - .every((e) => e.value.name != data.options.annotation.unionKey); + .every((e) => e.name != data.options.annotation.unionKey); @override String toString() { final jsonSerializable = _jsonSerializable(); return ''' -${copyWith?.interface ?? ''} -${copyWith?.concreteImpl(constructor.parameters) ?? ''} - /// @nodoc $jsonSerializable ${constructor.decorators.join('\n')} @@ -49,13 +52,19 @@ class ${constructor.redirectedName}${data.genericsDefinitionTemplate} $_concrete $_properties -${copyWith?.concreteCopyWithGetter ?? ''} -$_toJson -$_debugFillProperties -$_operatorEqualMethod -$_hashCodeMethod -$_toStringMethod +${copyWith?.copyWithGetter(needsCast: false) ?? ''} +${methods( + data, + globalData, + properties: constructor.properties, + name: constructor.redirectedName, + escapedName: constructor.escapedName, + source: Source.syntheticClass, + )} } + +${copyWith?.interface ?? ''} +${copyWith?.concreteImpl(constructor.parameters) ?? ''} '''; } @@ -96,7 +105,7 @@ $_toStringMethod } final correspondingProperty = constructor.properties - .where((element) => element.value.name == p.name) + .where((element) => element.name == p.name) .first; if (correspondingProperty.isSynthetic) { return ( @@ -135,7 +144,6 @@ $_toStringMethod if (data.options.asUnmodifiableCollections) ...constructor.properties .where((e) => e.isSynthetic) - .map((e) => e.value) .where((e) => e.isDartList || e.isDartMap || e.isDartSet) .map((e) => '_${e.name} = ${e.name}'), if (_hasUnionKeyProperty) @@ -222,10 +230,8 @@ $_toStringMethod } String get _properties { - final classProperties = constructor.properties - .where((e) => e.isSynthetic) - .map((e) => e.value) - .expand((p) { + final classProperties = + constructor.properties.where((e) => e.isSynthetic).expand((p) { final annotatedProperty = p.copyWith( decorators: [ if (commonProperties.any((element) => element.name == p.name)) @@ -303,135 +309,215 @@ final String \$type; return 'factory ${constructor.redirectedName}.fromJson(Map json$_fromJsonParams)' ' => _\$${constructor.redirectedName.public}FromJson(json$_fromJsonArgs);'; } +} - String get _toJsonParams => toJsonParameters( - data.genericsParameterTemplate, data.options.genericArgumentFactories); +enum Source { + mixin, + syntheticClass, +} - String get _toJsonArgs => toJsonArguments( - data.genericsParameterTemplate, data.options.genericArgumentFactories); +String methods( + Class data, + Library globalData, { + required List properties, + required String name, + required String escapedName, + required Source source, +}) { + return ''' +${toJson(data, name: name, source: source)} +${debugFillProperties( + data, + globalData, + properties, + escapedClassName: escapedName, + )} +${operatorEqualMethod(data, properties, className: name, source: source)} +${hashCodeMethod(data, properties, source: source)} +${toStringMethod( + data, + globalData, + escapedClassName: escapedName, + properties: properties, + )} +'''; +} - String get _toJson { - if (!data.options.toJson) return ''; +String toJson( + Class data, { + required String name, + required Source source, +}) { + if (!data.options.toJson) return ''; + + switch ((source, data.constructors)) { + // Manual classes have no toJson generated. + // This is due to the inability for parts to add `@JsonSerializable` + // on behalf of the user. + case (Source.mixin, []): + return ''; + case (Source.mixin, [_, ...]): + final _toJsonParams = toJsonParameters( + data.genericsParameterTemplate, + data.options.genericArgumentFactories, + ); - return ''' + return ''' + /// Serializes this ${data.name} to a JSON map. + Map toJson($_toJsonParams); +'''; + case _: + final _toJsonParams = toJsonParameters( + data.genericsParameterTemplate, + data.options.genericArgumentFactories, + ); + + final _toJsonArgs = toJsonArguments( + data.genericsParameterTemplate, + data.options.genericArgumentFactories, + ); + + return ''' @override Map toJson($_toJsonParams) { - return _\$${constructor.redirectedName.public}ToJson${data.genericsParameterTemplate}(this, $_toJsonArgs); + return _\$${name.public}ToJson${data.genericsParameterTemplate}(this, $_toJsonArgs); }'''; } +} - String get _debugFillProperties { - if (!globalData.hasDiagnostics || !data.options.asString) return ''; +String debugFillProperties( + Class data, + Library globalData, + List properties, { + required String escapedClassName, +}) { + if (!globalData.hasDiagnostics || !data.options.asString) return ''; - final diagnostics = [ - for (final e in constructor.properties.map((e) => e.value)) - "..add(DiagnosticsProperty('${e.name}', ${e.name}))", - ].join(); + final diagnostics = [ + for (final e in properties) + "..add(DiagnosticsProperty('${e.name}', ${e.name}))", + ].join(); - return ''' + return ''' @override void debugFillProperties(DiagnosticPropertiesBuilder properties) { super.debugFillProperties(properties); properties - ..add(DiagnosticsProperty('type', '${constructor.escapedName}')) + ..add(DiagnosticsProperty('type', '$escapedClassName')) $diagnostics; } '''; - } +} - String get _toStringMethod { - if (!data.options.asString) return ''; +String toStringMethod( + Class data, + Library globalData, { + required String escapedClassName, + required List properties, +}) { + if (!data.options.asString) return ''; - final parameters = globalData.hasDiagnostics - ? '{ DiagnosticLevel minLevel = DiagnosticLevel.info }' - : ''; + final parameters = globalData.hasDiagnostics + ? '{ DiagnosticLevel minLevel = DiagnosticLevel.info }' + : ''; - final properties = [ - for (final p in constructor.properties.map((e) => e.value)) - '${p.name.replaceAll(r'$', r'\$')}: ${wrapClassField(p.name)}', - ]; + final propertiesDisplayString = [ + for (final p in properties) + '${p.name.replaceAll(r'$', r'\$')}: ${wrapClassField(p.name)}', + ]; - return ''' + return ''' @override String toString($parameters) { - return '${constructor.escapedName}(${properties.join(', ')})'; + return '$escapedClassName(${propertiesDisplayString.join(', ')})'; } '''; - } +} - String get _operatorEqualMethod { - if (!data.options.equal) return ''; - - final comparisons = [ - 'other.runtimeType == runtimeType', - 'other is ${constructor.redirectedName}${data.genericsParameterTemplate}', - ...constructor.properties.map((e) => e.value).map((p) { - var name = p.name; - if (p.isPossiblyDartCollection) { - if (data.options.asUnmodifiableCollections && - (p.isDartList || p.isDartMap || p.isDartSet)) { - name = '_$name'; - } - } - final target = p.name == 'other' ? 'this.' : ''; +String operatorEqualMethod( + Class data, + List properties, { + required String className, + required Source source, +}) { + if (!data.options.equal) return ''; - if (p.isPossiblyDartCollection) { - // no need to check `identical` as `DeepCollectionEquality` already does it - return 'const DeepCollectionEquality().equals(other.$name, $target$name)'; - } - return '(identical(other.${p.name}, $target$name) || other.$name == $target$name)'; - }), - ]; + final comparisons = [ + 'other.runtimeType == runtimeType', + 'other is $className${data.genericsParameterTemplate}', + ...properties.map((p) { + var name = p.name; - return ''' + if (data.options.asUnmodifiableCollections && + source == Source.syntheticClass && + p.isPossiblyDartCollection && + (p.isDartList || p.isDartMap || p.isDartSet)) { + name = '_$name'; + } + + final target = p.name == 'other' ? 'this.' : ''; + + if (p.isPossiblyDartCollection) { + // no need to check `identical` as `DeepCollectionEquality` already does it + return 'const DeepCollectionEquality().equals(other.$name, $target$name)'; + } + return '(identical(other.${p.name}, $target$name) || other.$name == $target$name)'; + }), + ]; + + return ''' @override bool operator ==(Object other) { return identical(this, other) || (${comparisons.join('&&')}); } '''; - } +} - String get _hashCodeMethod { - if (!data.options.equal) return ''; - - final jsonKey = data.options.fromJson || data.options.toJson - ? '@JsonKey(includeFromJson: false, includeToJson: false)' - : ''; - - final hashedProperties = [ - 'runtimeType', - for (final property in constructor.properties.map((e) => e.value)) - if (property.isPossiblyDartCollection) - if (data.options.asUnmodifiableCollections && - (property.isDartList || property.isDartMap || property.isDartSet)) - 'const DeepCollectionEquality().hash(_${property.name})' - else - 'const DeepCollectionEquality().hash(${property.name})' +String hashCodeMethod( + Class data, + List properties, { + required Source source, +}) { + if (!data.options.equal) return ''; + + final jsonKey = data.options.fromJson || data.options.toJson + ? '@JsonKey(includeFromJson: false, includeToJson: false)' + : ''; + + final hashedProperties = [ + 'runtimeType', + for (final property in properties) + if (property.isPossiblyDartCollection) + if (data.options.asUnmodifiableCollections && + source == Source.syntheticClass && + (property.isDartList || property.isDartMap || property.isDartSet)) + 'const DeepCollectionEquality().hash(_${property.name})' else - property.name, - ]; + 'const DeepCollectionEquality().hash(${property.name})' + else + property.name, + ]; - if (hashedProperties.length == 1) { - return ''' + if (hashedProperties.length == 1) { + return ''' $jsonKey @override int get hashCode => ${hashedProperties.first}.hashCode; '''; - } - if (hashedProperties.length >= 20) { - return ''' + } + if (hashedProperties.length >= 20) { + return ''' $jsonKey @override int get hashCode => Object.hashAll([${hashedProperties.join(',')}]); '''; - } + } - return ''' + return ''' $jsonKey @override int get hashCode => Object.hash(${hashedProperties.join(',')}); '''; - } } extension DefaultValue on ParameterElement { diff --git a/packages/freezed/lib/src/templates/copy_with.dart b/packages/freezed/lib/src/templates/copy_with.dart index 1a381ed6..d9309a61 100644 --- a/packages/freezed/lib/src/templates/copy_with.dart +++ b/packages/freezed/lib/src/templates/copy_with.dart @@ -61,22 +61,10 @@ ${_abstractDeepCopyMethods().join()} }'''; } - String get abstractCopyWithGetter { + String copyWithGetter({required bool needsCast}) { if (cloneableProperties.isEmpty) return ''; - return _maybeOverride( - doc: ''' -${_copyWithDocs(data.name)} -''', - ''' -@JsonKey(includeFromJson: false, includeToJson: false) -$_abstractClassName${genericsParameter.append('$clonedClassName$genericsParameter')} get copyWith; -''', - ); - } - - String get concreteCopyWithGetter { - if (cloneableProperties.isEmpty) return ''; + final cast = needsCast ? ' as $clonedClassName$genericsParameter' : ''; return _maybeOverride( doc: ''' ${_copyWithDocs(data.name)} @@ -84,7 +72,7 @@ ${_copyWithDocs(data.name)} ''' @JsonKey(includeFromJson: false, includeToJson: false) @pragma('vm:prefer-inline') -$_abstractClassName${genericsParameter.append('$clonedClassName$genericsParameter')} get copyWith => $_implClassName${genericsParameter.append('$clonedClassName$genericsParameter')}(this, _\$identity); +$_abstractClassName${genericsParameter.append('$clonedClassName$genericsParameter')} get copyWith => $_implClassName${genericsParameter.append('$clonedClassName$genericsParameter')}(this$cast, _\$identity); ''', ); } @@ -92,36 +80,47 @@ $_abstractClassName${genericsParameter.append('$clonedClassName$genericsParamete String get commonConcreteImpl { var copyWith = ''; - if (cloneableProperties.isNotEmpty) { + if (cloneableProperties.isNotEmpty || data.copyWithTarget != null) { final prototype = _concreteCopyWithPrototype( properties: cloneableProperties, methodName: 'call', ); - final body = _copyWithMethodBody( - parametersTemplate: ParametersTemplate( - const [], - namedParameters: cloneableProperties.map((e) { - return Parameter( - decorators: e.decorators, - name: e.name, - isNullable: e.isNullable, - isFinal: false, - isDartList: false, - isDartMap: false, - isDartSet: false, - showDefaultValue: false, - isRequired: false, - defaultValueSource: '', - type: e.type, - doc: e.doc, - isPossiblyDartCollection: e.isPossiblyDartCollection, - parameterElement: null, - ); - }).toList(), - ), - returnType: '_self.copyWith', - ); + String body; + if (data.copyWithTarget case final target?) { + body = _copyWithMethodBody( + parametersTemplate: target.parameters, + returnType: switch (target.name) { + final name? => '${data.name}.$name', + null => data.name, + }, + ); + } else { + body = _copyWithMethodBody( + parametersTemplate: ParametersTemplate( + const [], + namedParameters: cloneableProperties.map((e) { + return Parameter( + decorators: e.decorators, + name: e.name, + isNullable: e.isNullable, + isFinal: false, + isDartList: false, + isDartMap: false, + isDartSet: false, + showDefaultValue: false, + isRequired: false, + defaultValueSource: '', + type: e.type, + doc: e.doc, + isPossiblyDartCollection: e.isPossiblyDartCollection, + parameterElement: null, + ); + }).toList(), + ), + returnType: '_self.copyWith', + ); + } copyWith = '@pragma(\'vm:prefer-inline\') @override $prototype $body'; } diff --git a/packages/freezed/lib/src/templates/from_json_template.dart b/packages/freezed/lib/src/templates/from_json_template.dart index b40cfe27..094e7bee 100644 --- a/packages/freezed/lib/src/templates/from_json_template.dart +++ b/packages/freezed/lib/src/templates/from_json_template.dart @@ -12,6 +12,10 @@ class FromJson { @override String toString() { + // For manual classes, we don't handle from/toJson. This is because parts + // cannot add annotations on user's behalf. + if (clazz.constructors.isEmpty) return ''; + final conflictCtor = clazz.constructors .where((c) => c.redirectedName.public == clazz.name.public) .firstOrNull; diff --git a/packages/freezed/lib/src/templates/parameter_template.dart b/packages/freezed/lib/src/templates/parameter_template.dart index dee9a0d7..5186a93c 100644 --- a/packages/freezed/lib/src/templates/parameter_template.dart +++ b/packages/freezed/lib/src/templates/parameter_template.dart @@ -63,7 +63,7 @@ class ParametersTemplate { }); static ParametersTemplate fromParameterList( - FormalParameterList parameters, { + Iterable parameters, { bool isAssignedToThis = false, required bool addImplicitFinal, }) { @@ -81,7 +81,7 @@ class ParametersTemplate { isFinal: addImplicitFinal || e.isFinal, type: parseTypeSource(e), decorators: parseDecorators(e.metadata), - doc: p.documentation, + doc: p.documentation ?? '', isPossiblyDartCollection: e.type.isPossiblyDartCollection, showDefaultValue: true, parameterElement: e, @@ -92,18 +92,16 @@ class ParametersTemplate { } return ParametersTemplate( - parameters.parameters + parameters .where((p) => p.isRequiredPositional) .map(asParameter) .toList(), - optionalPositionalParameters: parameters.parameters + optionalPositionalParameters: parameters .where((p) => p.isOptionalPositional) .map(asParameter) .toList(), - namedParameters: parameters.parameters - .where((p) => p.isNamed) - .map(asParameter) - .toList()); + namedParameters: + parameters.where((p) => p.isNamed).map(asParameter).toList()); } final List requiredPositionalParameters; diff --git a/packages/freezed/lib/src/templates/properties.dart b/packages/freezed/lib/src/templates/properties.dart index 6cb7267f..e594ff09 100644 --- a/packages/freezed/lib/src/templates/properties.dart +++ b/packages/freezed/lib/src/templates/properties.dart @@ -17,6 +17,7 @@ class Property { required this.defaultValueSource, required this.hasJsonKey, required this.doc, + required this.isSynthetic, required this.isFinal, required this.isNullable, required this.isDartList, @@ -25,26 +26,29 @@ class Property { required this.isPossiblyDartCollection, }) : type = type ?? 'dynamic'; - Property.fromParameter(Parameter p) - : this( + Property.fromParameter( + Parameter p, { + required bool isSynthetic, + }) : this( decorators: p.decorators, name: p.name, isFinal: p.isFinal, doc: p.doc, type: p.type, defaultValueSource: p.defaultValueSource, + isSynthetic: isSynthetic, isNullable: p.isNullable, isDartList: p.isDartList, isDartMap: p.isDartMap, isDartSet: p.isDartSet, isPossiblyDartCollection: p.isPossiblyDartCollection, - // TODO: support hasJsonKey hasJsonKey: false, ); static Property fromFormalParameter( FormalParameter parameter, { required bool addImplicitFinal, + required bool isSynthetic, }) { final element = parameter.declaredElement!; @@ -64,7 +68,8 @@ class Property { isDartMap: element.type.isDartCoreMap, isDartSet: element.type.isDartCoreSet, isFinal: addImplicitFinal || element.isFinal, - doc: parameter.documentation, + isSynthetic: isSynthetic, + doc: parameter.documentation ?? '', type: parseTypeSource(element), decorators: parseDecorators(element.metadata), defaultValueSource: defaultValue, @@ -80,6 +85,7 @@ class Property { final bool isDartMap; final bool isDartSet; final bool isFinal; + final bool isSynthetic; final List decorators; final String? defaultValueSource; final bool hasJsonKey; @@ -134,6 +140,7 @@ class Property { return Property( type: type ?? this.type, name: name ?? this.name, + isSynthetic: isSynthetic, isNullable: isNullable ?? this.isNullable, decorators: decorators ?? this.decorators, defaultValueSource: defaultValueSource ?? this.defaultValueSource, diff --git a/packages/freezed/test/concrete_test.dart b/packages/freezed/test/concrete_test.dart index 49b9b464..63223810 100644 --- a/packages/freezed/test/concrete_test.dart +++ b/packages/freezed/test/concrete_test.dart @@ -1,5 +1,3 @@ -// TODO: debugFillProperties still works if base class is Diagnosticable - import 'package:test/test.dart'; import 'integration/concrete.dart'; diff --git a/packages/freezed/test/decorator_test.dart b/packages/freezed/test/decorator_test.dart index 99d5721d..2b9985f6 100644 --- a/packages/freezed/test/decorator_test.dart +++ b/packages/freezed/test/decorator_test.dart @@ -4,7 +4,6 @@ import 'package:build_test/build_test.dart'; import 'package:test/test.dart'; void main() { - // TODO: Tear off deprecated test('has no issue', () async { final main = await resolveSources( { @@ -91,9 +90,6 @@ void main() { 'DEPRECATED_MEMBER_USE_FROM_SAME_PACKAGE', 'DEPRECATED_MEMBER_USE_FROM_SAME_PACKAGE', 'DEPRECATED_MEMBER_USE_FROM_SAME_PACKAGE', - // TODO: find out why copyWith doesn't warn even if deprecated - // 'DEPRECATED_MEMBER_USE_FROM_SAME_PACKAGE', - // 'DEPRECATED_MEMBER_USE_FROM_SAME_PACKAGE', ], [ 'UNUSED_RESULT', @@ -102,9 +98,6 @@ void main() { 'DEPRECATED_MEMBER_USE', 'DEPRECATED_MEMBER_USE', 'DEPRECATED_MEMBER_USE', - // TODO: find out why copyWith doesn't warn even if deprecated - // 'DEPRECATED_MEMBER_USE', - // 'DEPRECATED_MEMBER_USE', ], ])); }); diff --git a/packages/freezed/test/deep_copy_test.dart b/packages/freezed/test/deep_copy_test.dart index 374132eb..0a1e192d 100644 --- a/packages/freezed/test/deep_copy_test.dart +++ b/packages/freezed/test/deep_copy_test.dart @@ -10,8 +10,6 @@ import 'integration/generic.dart' import 'integration/single_class_constructor.dart' show Dynamic; void main() { - // TODO: copyWith is identical to itself if don't have descendants - test('has no issue', () async { final main = await resolveSources( { diff --git a/packages/freezed/test/integration/deep_copy.dart b/packages/freezed/test/integration/deep_copy.dart index 8d9e00cc..7a2c9433 100644 --- a/packages/freezed/test/integration/deep_copy.dart +++ b/packages/freezed/test/integration/deep_copy.dart @@ -12,6 +12,7 @@ abstract class ManualField with _$ManualField { const ManualField._({required this.value}); const factory ManualField(int value) = _ManualField; + @override final int value; } diff --git a/packages/freezed/test/integration/extend.dart b/packages/freezed/test/integration/extend.dart new file mode 100644 index 00000000..b98a6f73 --- /dev/null +++ b/packages/freezed/test/integration/extend.dart @@ -0,0 +1,14 @@ +import 'package:freezed_annotation/freezed_annotation.dart'; + +part 'extend.freezed.dart'; + +class Base { + const Base.named(this.value); + final int value; +} + +@freezed +abstract class Subclass extends Base with _$Subclass { + const Subclass._(super.value) : super.named(); + const factory Subclass(int value) = _Subclass; +} diff --git a/packages/freezed/test/integration/multiple_constructors.dart b/packages/freezed/test/integration/multiple_constructors.dart index 882ffde6..e91b1bcc 100644 --- a/packages/freezed/test/integration/multiple_constructors.dart +++ b/packages/freezed/test/integration/multiple_constructors.dart @@ -2,6 +2,18 @@ import 'package:freezed_annotation/freezed_annotation.dart'; part 'multiple_constructors.freezed.dart'; +@freezed +sealed class Response with _$Response { + Response._({DateTime? time}) : time = time ?? DateTime(0, 0, 0); + // 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; +} + @freezed abstract class ManualPositionInAnyOrder with _$ManualPositionInAnyOrder { const ManualPositionInAnyOrder._(this.a, this.b); @@ -10,7 +22,9 @@ abstract class ManualPositionInAnyOrder with _$ManualPositionInAnyOrder { const factory ManualPositionInAnyOrder.other(int b, String a) = _ManualPositionInAnyOrder2; + @override final String a; + @override final int b; } @@ -20,6 +34,7 @@ abstract class ManualNamedOptionalProperty with _$ManualNamedOptionalProperty { const factory ManualNamedOptionalProperty(int value) = _ManualNamedProperty; const factory ManualNamedOptionalProperty.a() = _ManualNamedPropertyA; + @override final int value; } @@ -30,6 +45,7 @@ abstract class Subclass with _$Subclass { const factory Subclass.b(int value) = _SubclassB; // Check that no @override is nu + @override final int value; } diff --git a/packages/freezed/test/integration/single_class_constructor.dart b/packages/freezed/test/integration/single_class_constructor.dart index a624249e..fddba3ba 100644 --- a/packages/freezed/test/integration/single_class_constructor.dart +++ b/packages/freezed/test/integration/single_class_constructor.dart @@ -377,6 +377,7 @@ abstract class Late with _$Late { Late._(); factory Late(int value) = _Late; + @override late final container = LateContainer(value); } @@ -397,7 +398,9 @@ abstract class AllProperties with _$AllProperties { factory AllProperties(int value) = _AllProperties; int get a => 1; + @override late final b = 2; + @override final c = 3; } @@ -406,6 +409,7 @@ abstract class Late2 with _$Late2 { Late2._(); factory Late2(int? Function() cb) = _Late2; + @override late final int? first = cb(); } @@ -414,6 +418,7 @@ abstract class ComplexLate with _$ComplexLate { ComplexLate._(); factory ComplexLate(List values) = _ComplexLate; + @override late final List odd = values.where((value) { if (value.isOdd) return true; diff --git a/packages/freezed/test/manual_test.dart b/packages/freezed/test/manual_test.dart new file mode 100644 index 00000000..baa8bace --- /dev/null +++ b/packages/freezed/test/manual_test.dart @@ -0,0 +1,135 @@ +import 'package:freezed_annotation/freezed_annotation.dart'; +import 'package:test/test.dart'; + +part 'manual_test.freezed.dart'; +part 'manual_test.g.dart'; + +@freezed +@JsonSerializable() +class JsonManual with _$JsonManual { + JsonManual(this.a); + factory JsonManual.fromJson(Map json) => + _$JsonManualFromJson(json); + + @override + final int a; + + Map toJson() => _$JsonManualToJson(this); +} + +@freezed +class ManualWithBothDefault with _$ManualWithBothDefault { + ManualWithBothDefault._(String this.b) : a = null; + ManualWithBothDefault(int this.a) : b = null; + + @override + final int? a; + @override + final String? b; +} + +@freezed +class ManualWithPrivateDefault with _$ManualWithPrivateDefault { + ManualWithPrivateDefault.b(String this.b) : a = null; + ManualWithPrivateDefault._(int this.a) : b = null; + + @override + final int? a; + @override + final String? b; +} + +@freezed +class ManualWithDefault with _$ManualWithDefault { + ManualWithDefault.b(String this.b) : a = null; + ManualWithDefault(int this.a) : b = null; + + @override + final int? a; + @override + final String? b; +} + +@freezed +class ManualWithoutDefault with _$ManualWithoutDefault { + ManualWithoutDefault.a(int this.a) : b = null; + ManualWithoutDefault.b(String this.b) : a = null; + + @override + final int? a; + @override + final String? b; +} + +@freezed +class ManualWithoutDefault2 with _$ManualWithoutDefault2 { + ManualWithoutDefault2.b(String this.b) : a = null; + ManualWithoutDefault2.a(int this.a) : b = null; + + @override + final int? a; + @override + final String? b; +} + +void main() { + group('ManualWithBothDefault', () { + test('has copyWith use default', () { + expect( + ManualWithBothDefault(42).copyWith(a: 21), + ManualWithBothDefault(21), + ); + }); + test('overrides ==/hashCode/toString', () { + final manual = ManualWithBothDefault(42); + final manual2 = ManualWithBothDefault._('foo'); + + expect(manual, ManualWithBothDefault(42)); + expect(manual, isNot(ManualWithBothDefault(21))); + expect(manual.hashCode, ManualWithBothDefault(42).hashCode); + expect(manual.hashCode, isNot(ManualWithBothDefault(21).hashCode)); + expect(manual.toString(), 'ManualWithBothDefault(a: 42, b: null)'); + + expect(manual2, ManualWithBothDefault._('foo')); + expect(manual2, isNot(ManualWithBothDefault._('bar'))); + expect(manual2.hashCode, ManualWithBothDefault._('foo').hashCode); + expect(manual2.hashCode, isNot(ManualWithBothDefault._('bar').hashCode)); + expect(manual2.toString(), 'ManualWithBothDefault(a: null, b: foo)'); + }); + }); + + group('ManualWithPrivateDefault', () { + test('has copyWith use _', () { + expect( + ManualWithPrivateDefault._(42).copyWith(a: 21), + ManualWithPrivateDefault._(21), + ); + }); + }); + + group('ManualWithDefault', () { + test('has copyWith use default', () { + expect( + ManualWithDefault(42).copyWith(a: 21), + ManualWithDefault(21), + ); + }); + }); + + group('ManualWithoutDefault', () { + test('has copyWith target the first ctor', () { + expect( + ManualWithoutDefault.a(42).copyWith(a: 21), + ManualWithoutDefault.a(21), + ); + }); + }); + group('ManualWithoutDefault2', () { + test('has copyWith target the first ctor', () { + expect( + ManualWithoutDefault2.a(42).copyWith(b: 'foo'), + ManualWithoutDefault2.b('foo'), + ); + }); + }); +} diff --git a/packages/freezed/test/multiple_constructors_test.dart b/packages/freezed/test/multiple_constructors_test.dart index 36eb424c..7dd85f2b 100644 --- a/packages/freezed/test/multiple_constructors_test.dart +++ b/packages/freezed/test/multiple_constructors_test.dart @@ -26,6 +26,16 @@ Future main() async { .firstWhere((element) => element.name == elementName); } + test('Response', () { + expect(Response.data('a').time, DateTime(0, 0, 0)); + expect( + Response.data('a', time: DateTime(1, 0, 0)).time, + DateTime(1, 0, 0), + ); + + expect(Response.error('err').time, DateTime(0, 0, 0)); + }); + test('recursive class does not generate dynamic', () async { final recursiveClass = _getClassElement('_RecursiveNext'); diff --git a/packages/freezed/test/single_class_constructor_test.dart b/packages/freezed/test/single_class_constructor_test.dart index aae8a9aa..0c926afe 100644 --- a/packages/freezed/test/single_class_constructor_test.dart +++ b/packages/freezed/test/single_class_constructor_test.dart @@ -233,21 +233,24 @@ Future main() async { final doc = singleClassLibrary.topLevelElements .firstWhere((e) => e.name == 'Doc') as ClassElement; - expect(doc.mixins.first.accessors.where((e) => e.name != 'copyWith'), [ - isA() - .having((e) => e.name, 'name', 'positional') - .having((e) => e.documentationComment, 'doc', ''' + expect( + doc.mixins.first.accessors + .where((e) => e.name != 'copyWith' && e.name != 'hashCode'), + [ + isA() + .having((e) => e.name, 'name', 'positional') + .having((e) => e.documentationComment, 'doc', ''' /// Multi /// line /// positional'''), - isA() // - .having((e) => e.name, 'name', 'named') - .having( - (e) => e.documentationComment, 'doc', '/// Single line named'), - isA() // - .having((e) => e.name, 'name', 'simple') - .having((e) => e.documentationComment, 'doc', null), - ]); + isA() // + .having((e) => e.name, 'name', 'named') + .having((e) => e.documentationComment, 'doc', + '/// Single line named'), + isA() // + .having((e) => e.name, 'name', 'simple') + .having((e) => e.documentationComment, 'doc', null), + ]); }); test('Assertion', () { diff --git a/packages/freezed/test/source_gen_src.dart b/packages/freezed/test/source_gen_src.dart index e557a0ef..0ed7f8b8 100644 --- a/packages/freezed/test/source_gen_src.dart +++ b/packages/freezed/test/source_gen_src.dart @@ -103,69 +103,6 @@ abstract class Mixed { factory Mixed.named(String b) = Mixed1; } -@ShouldThrow( - 'Classes decorated with @freezed can only have a single non-factory' - ', and must be named MyClass._()', -) -@freezed -abstract class MultipleConcreteConstructors { - MultipleConcreteConstructors._(); - MultipleConcreteConstructors(); -} - -@ShouldThrow( - 'Classes decorated with @freezed can only have a single non-factory' - ', and must be named MyClass._()', -) -@freezed -abstract class SingleConcreteConstructorInvalidName { - SingleConcreteConstructorInvalidName(); -} - -@ShouldThrow(''' -The constructor MyClass._() specified a positional parameter named a, -but at least one constructor does not have a matching parameter. - -When specifying fields in MyClass._(), either: -- the parameter should be named -- or all constructors in the class should specify that parameter. -''') -@freezed -abstract class ConcreteConstructorWithParameters { - ConcreteConstructorWithParameters._(int a); - - factory ConcreteConstructorWithParameters() = - _ConcreteConstructorWithParameters; -} - -class _ConcreteConstructorWithParameters - implements ConcreteConstructorWithParameters { - _ConcreteConstructorWithParameters(); -} - -@ShouldThrow( - 'Marked NothingToDo with @freezed, but freezed has nothing to generate', -) -@freezed -abstract class NothingToDo { - NothingToDo._(); -} - -@ShouldThrow( - 'Marked ManualFactory with @freezed, but freezed has nothing to generate', -) -@freezed -abstract class ManualFactory { - factory ManualFactory() => _Manual(); -} - -class _Manual implements ManualFactory { - @override - dynamic noSuchMethod(Invocation invocation) { - return super.noSuchMethod(invocation); - } -} - @ShouldThrow('Fallback union was specified but no fallback constructor exists.') @Freezed(fallbackUnion: 'fallback') class FallbackUnionMissing { @@ -181,23 +118,6 @@ class _Second implements FallbackUnionMissing { _Second(); } -@ShouldThrow( - 'Marked ManualFactory2 with @freezed, but freezed has nothing to generate', -) -@freezed -abstract class ManualFactory2 { - factory ManualFactory2({int? a}) => _Manual2(a: a ??= 42); -} - -class _Manual2 implements ManualFactory2 { - _Manual2({int? a}); - - @override - dynamic noSuchMethod(Invocation invocation) { - return super.noSuchMethod(invocation); - } -} - @ShouldThrow( 'Classes decorated with @freezed cannot have mutable properties', )