Skip to content
Open
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
8 changes: 8 additions & 0 deletions packages/freezed/lib/src/models.dart
Original file line number Diff line number Diff line change
Expand Up @@ -1109,6 +1109,9 @@ To fix, either:

return '$escapedElementName$generics';
}

bool get shouldBeFinal =>
options.annotation.makeGeneratedClassesFinal ?? false;
}

class PropertyList {
Expand Down Expand Up @@ -1299,6 +1302,11 @@ class ClassConfig {
},
orElse: () => globalConfigs.map,
),
makeGeneratedClassesFinal: annotation.decodeField(
'makeGeneratedClassesFinal',
decode: (obj) => obj.toBoolValue(),
orElse: () => globalConfigs.makeGeneratedClassesFinal,
),
);
}

Expand Down
2 changes: 1 addition & 1 deletion packages/freezed/lib/src/templates/concrete_template.dart
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ class Concrete {
/// @nodoc
$jsonSerializable
${constructor.decorators.join('\n')}
class ${constructor.redirectedName}${data.genericsDefinitionTemplate} $_concreteSuper {
${data.shouldBeFinal ? 'final ' : ''}class ${constructor.redirectedName}${data.genericsDefinitionTemplate} $_concreteSuper {
$_concreteConstructor
$_concreteFromJsonConstructor
Expand Down
223 changes: 223 additions & 0 deletions packages/freezed/test/finalized_test.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,223 @@
import 'package:analyzer/dart/analysis/results.dart';
import 'package:analyzer/error/error.dart';
import 'package:build_test/build_test.dart';
import 'package:test/test.dart';

void main() {
group('marks generated classes as final', () {
group('sealed with single constructor', () {
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 (SealedWithFinalFoo()) {
case SealedWithFinalBar():
break;

case SealedWithFinalFoo():
break;
}
}
''',
},
(r) => r.findLibraryByName('main'),
readAllSourcesFromFilesystem: true,
);

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('sealed with single constructor and superclass', () {
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() {
final SuperFoo foo = CustomFoo();

switch (foo) {
case SealedWithFinalSuperFoo():
break;

case AbstractWithFinalSuperFoo():
break;

case CustomFoo():
break;

case SealedWithFinalBar():
break;
}
}
''',
},
(r) => r.findLibraryByName('main'),
readAllSourcesFromFilesystem: true,
);

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('sealed with multiple constructors', () {
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 (SealedWithFinalAbc.b()) {
case SealedWithFinalBar():
break;

case SealedWithFinalAbcA():
break;

case SealedWithFinalAbcB():
break;

case SealedWithFinalAbcC():
break;
}
}
''',
},
(r) => r.findLibraryByName('main'),
readAllSourcesFromFilesystem: true,
);

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('abstract with single constructor', () {
test(
'doesnt cause 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 (AbstractWithFinalFoo()) {
case AbstractWithFinalBar():
break;

case AbstractWithFinalFoo():
break;
}
}
''',
},
(r) => r.findLibraryByName('main'),
readAllSourcesFromFilesystem: true,
);

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

expect(errorResult.errors, isEmpty);
},
);
});

group('abstract with multiple constructors', () {
test(
'does not cause 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 (AbstractWithFinalAbc.b()) {
case AbstractWithFinalBar():
break;

case AbstractWithFinalAbcA():
break;

case AbstractWithFinalAbcB():
break;

case AbstractWithFinalAbcC():
break;
}
}
''',
},
(r) => r.findLibraryByName('main'),
readAllSourcesFromFilesystem: true,
);

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

expect(errorResult.errors, isEmpty);
},
);
});
});
}
63 changes: 63 additions & 0 deletions packages/freezed/test/integration/finalized.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import 'package:freezed_annotation/freezed_annotation.dart';

part 'finalized.freezed.dart';

@Freezed(makeGeneratedClassesFinal: true)
sealed class SealedWithFinalFoo with _$SealedWithFinalFoo {
factory SealedWithFinalFoo() = _SealedWithFinalFoo;
}

@Freezed(makeGeneratedClassesFinal: true)
sealed class SealedWithFinalBar with _$SealedWithFinalBar {
factory SealedWithFinalBar() = _SealedWithFinalBar;
}

@Freezed(makeGeneratedClassesFinal: true)
sealed class SealedWithFinalAbc with _$SealedWithFinalAbc {
factory SealedWithFinalAbc.a() = SealedWithFinalAbcA;
factory SealedWithFinalAbc.b() = SealedWithFinalAbcB;
factory SealedWithFinalAbc.c() = SealedWithFinalAbcC;
}

@Freezed(makeGeneratedClassesFinal: true)
abstract class AbstractWithFinalFoo with _$AbstractWithFinalFoo {
factory AbstractWithFinalFoo() = _AbstractWithFinalFoo;
}

@Freezed(makeGeneratedClassesFinal: true)
abstract class AbstractWithFinalBar with _$AbstractWithFinalBar {
factory AbstractWithFinalBar() = _AbstractWithFinalBar;
}

@Freezed(makeGeneratedClassesFinal: true)
abstract class AbstractWithFinalAbc with _$AbstractWithFinalAbc {
factory AbstractWithFinalAbc.a() = AbstractWithFinalAbcA;
factory AbstractWithFinalAbc.b() = AbstractWithFinalAbcB;
factory AbstractWithFinalAbc.c() = AbstractWithFinalAbcC;
}

sealed class SuperFoo {
const SuperFoo();
}

final class CustomFoo extends SuperFoo {}

@Freezed(makeGeneratedClassesFinal: true)
sealed class SealedWithFinalSuperFoo extends SuperFoo
with _$SealedWithFinalSuperFoo {
const SealedWithFinalSuperFoo._() : super();

factory SealedWithFinalSuperFoo() = _SealedWithFinalSuperFoo;
}

@Freezed(makeGeneratedClassesFinal: true)
abstract class AbstractWithFinalSuperFoo extends SuperFoo
with _$AbstractWithFinalSuperFoo {
const AbstractWithFinalSuperFoo._() : super();

factory AbstractWithFinalSuperFoo() = _AbstractWithFinalSuperFoo;
}

class SubFoo extends AbstractWithFinalSuperFoo {
SubFoo() : super._();
}
38 changes: 38 additions & 0 deletions packages/freezed_annotation/lib/freezed_annotation.dart
Original file line number Diff line number Diff line change
Expand Up @@ -219,6 +219,7 @@ class Freezed {
this.makeCollectionsUnmodifiable,
this.addImplicitFinal = true,
this.genericArgumentFactories = false,
this.makeGeneratedClassesFinal,
});

/// Decode the options from a build.yaml
Expand Down Expand Up @@ -489,6 +490,43 @@ class Freezed {
/// If that value is null too, defaults to [FreezedWhenOptions.all].
@_FreezedWhenOptionsConverter()
final FreezedWhenOptions? when;

/// Whether to add `final` modifiers to the generated classes.
///
/// Defaults to false.
///
/// This makes the generated classes `final` by default,
/// so when using them in a switch statement, the analzyer will warn you
/// if you try to match against a pattern that will never match the type.
///
/// ```dart
/// @Freezed(makeGeneratedClassesFinal: true)
/// sealed class Foo with _$Foo {
/// const Foo._();
/// const factory Foo() = _Foo;
/// }
///
/// @Freezed(makeGeneratedClassesFinal: true)
/// sealed class Bar with _$Bar {
/// const Bar._();
/// const factory Bar() = _Bar;
/// }
///
/// void main() {
/// switch (Foo()) {
/// // The analyzer will yield a warning that this case can never match,
/// // because all subclasses of Foo are sealed/final, so it is guaranteed
/// // that instances of type Bar can never also be of type Foo.
/// case Bar():
/// // ...
/// break;
///
/// case Foo():
/// // ...
/// break;
/// }
/// ```
final bool? makeGeneratedClassesFinal;
}

/// Defines an immutable data-class.
Expand Down
1 change: 1 addition & 0 deletions packages/freezed_annotation/lib/freezed_annotation.g.dart

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading