Skip to content

Commit d6d501e

Browse files
committed
feat: allow making generated classes final
1 parent 9e78252 commit d6d501e

File tree

7 files changed

+372
-1
lines changed

7 files changed

+372
-1
lines changed

packages/freezed/lib/src/models.dart

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1109,6 +1109,9 @@ To fix, either:
11091109

11101110
return '$escapedElementName$generics';
11111111
}
1112+
1113+
bool get shouldBeFinal =>
1114+
options.annotation.makeGeneratedClassesFinal ?? false;
11121115
}
11131116

11141117
class PropertyList {
@@ -1299,6 +1302,11 @@ class ClassConfig {
12991302
},
13001303
orElse: () => globalConfigs.map,
13011304
),
1305+
makeGeneratedClassesFinal: annotation.decodeField(
1306+
'makeGeneratedClassesFinal',
1307+
decode: (obj) => obj.toBoolValue(),
1308+
orElse: () => globalConfigs.makeGeneratedClassesFinal,
1309+
),
13021310
);
13031311
}
13041312

packages/freezed/lib/src/templates/concrete_template.dart

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ class Concrete {
4343
/// @nodoc
4444
$jsonSerializable
4545
${constructor.decorators.join('\n')}
46-
class ${constructor.redirectedName}${data.genericsDefinitionTemplate} $_concreteSuper {
46+
${data.shouldBeFinal ? 'final ' : ''}class ${constructor.redirectedName}${data.genericsDefinitionTemplate} $_concreteSuper {
4747
$_concreteConstructor
4848
$_concreteFromJsonConstructor
4949
Lines changed: 223 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,223 @@
1+
import 'package:analyzer/dart/analysis/results.dart';
2+
import 'package:analyzer/error/error.dart';
3+
import 'package:build_test/build_test.dart';
4+
import 'package:test/test.dart';
5+
6+
void main() {
7+
group('marks generated classes as final', () {
8+
group('sealed with single constructor', () {
9+
test(
10+
'causes pattern_never_matches_value_type warning when trying to match on pattern that can never match',
11+
() async {
12+
final main = await resolveSources(
13+
{
14+
'freezed|test/integration/main.dart': r'''
15+
library main;
16+
import 'finalized.dart';
17+
18+
void main() {
19+
switch (SealedWithFinalFoo()) {
20+
case SealedWithFinalBar():
21+
break;
22+
23+
case SealedWithFinalFoo():
24+
break;
25+
}
26+
}
27+
''',
28+
},
29+
(r) => r.findLibraryByName('main'),
30+
readAllSourcesFromFilesystem: true,
31+
);
32+
33+
final errorResult =
34+
await main!.session.getErrors(
35+
'/freezed/test/integration/main.dart',
36+
)
37+
as ErrorsResult;
38+
39+
expect(errorResult.errors, hasLength(1));
40+
41+
final [error] = errorResult.errors;
42+
43+
expect(error.errorCode.errorSeverity, ErrorSeverity.WARNING);
44+
expect(error.errorCode.name, 'PATTERN_NEVER_MATCHES_VALUE_TYPE');
45+
},
46+
);
47+
});
48+
49+
group('sealed with single constructor and superclass', () {
50+
test(
51+
'causes pattern_never_matches_value_type warning when trying to match on pattern that can never match',
52+
() async {
53+
final main = await resolveSources(
54+
{
55+
'freezed|test/integration/main.dart': r'''
56+
library main;
57+
import 'finalized.dart';
58+
59+
void main() {
60+
final SuperFoo foo = CustomFoo();
61+
62+
switch (foo) {
63+
case SealedWithFinalSuperFoo():
64+
break;
65+
66+
case AbstractWithFinalSuperFoo():
67+
break;
68+
69+
case CustomFoo():
70+
break;
71+
72+
case SealedWithFinalBar():
73+
break;
74+
}
75+
}
76+
''',
77+
},
78+
(r) => r.findLibraryByName('main'),
79+
readAllSourcesFromFilesystem: true,
80+
);
81+
82+
final errorResult =
83+
await main!.session.getErrors(
84+
'/freezed/test/integration/main.dart',
85+
)
86+
as ErrorsResult;
87+
88+
expect(errorResult.errors, hasLength(1));
89+
90+
final [error] = errorResult.errors;
91+
92+
expect(error.errorCode.errorSeverity, ErrorSeverity.WARNING);
93+
expect(error.errorCode.name, 'PATTERN_NEVER_MATCHES_VALUE_TYPE');
94+
},
95+
);
96+
});
97+
98+
group('sealed with multiple constructors', () {
99+
test(
100+
'causes pattern_never_matches_value_type warning when trying to match on pattern that can never match',
101+
() async {
102+
final main = await resolveSources(
103+
{
104+
'freezed|test/integration/main.dart': r'''
105+
library main;
106+
import 'finalized.dart';
107+
108+
void main() {
109+
switch (SealedWithFinalAbc.b()) {
110+
case SealedWithFinalBar():
111+
break;
112+
113+
case SealedWithFinalAbcA():
114+
break;
115+
116+
case SealedWithFinalAbcB():
117+
break;
118+
119+
case SealedWithFinalAbcC():
120+
break;
121+
}
122+
}
123+
''',
124+
},
125+
(r) => r.findLibraryByName('main'),
126+
readAllSourcesFromFilesystem: true,
127+
);
128+
129+
final errorResult =
130+
await main!.session.getErrors(
131+
'/freezed/test/integration/main.dart',
132+
)
133+
as ErrorsResult;
134+
135+
expect(errorResult.errors, hasLength(1));
136+
137+
final [error] = errorResult.errors;
138+
139+
expect(error.errorCode.errorSeverity, ErrorSeverity.WARNING);
140+
expect(error.errorCode.name, 'PATTERN_NEVER_MATCHES_VALUE_TYPE');
141+
},
142+
);
143+
});
144+
145+
group('abstract with single constructor', () {
146+
test(
147+
'doesnt cause pattern_never_matches_value_type warning when trying to match on pattern that can never match',
148+
() async {
149+
final main = await resolveSources(
150+
{
151+
'freezed|test/integration/main.dart': r'''
152+
library main;
153+
import 'finalized.dart';
154+
155+
void main() {
156+
switch (AbstractWithFinalFoo()) {
157+
case AbstractWithFinalBar():
158+
break;
159+
160+
case AbstractWithFinalFoo():
161+
break;
162+
}
163+
}
164+
''',
165+
},
166+
(r) => r.findLibraryByName('main'),
167+
readAllSourcesFromFilesystem: true,
168+
);
169+
170+
final errorResult =
171+
await main!.session.getErrors(
172+
'/freezed/test/integration/main.dart',
173+
)
174+
as ErrorsResult;
175+
176+
expect(errorResult.errors, isEmpty);
177+
},
178+
);
179+
});
180+
181+
group('abstract with multiple constructors', () {
182+
test(
183+
'does not cause pattern_never_matches_value_type warning when trying to match on pattern that can never match',
184+
() async {
185+
final main = await resolveSources(
186+
{
187+
'freezed|test/integration/main.dart': r'''
188+
library main;
189+
import 'finalized.dart';
190+
191+
void main() {
192+
switch (AbstractWithFinalAbc.b()) {
193+
case AbstractWithFinalBar():
194+
break;
195+
196+
case AbstractWithFinalAbcA():
197+
break;
198+
199+
case AbstractWithFinalAbcB():
200+
break;
201+
202+
case AbstractWithFinalAbcC():
203+
break;
204+
}
205+
}
206+
''',
207+
},
208+
(r) => r.findLibraryByName('main'),
209+
readAllSourcesFromFilesystem: true,
210+
);
211+
212+
final errorResult =
213+
await main!.session.getErrors(
214+
'/freezed/test/integration/main.dart',
215+
)
216+
as ErrorsResult;
217+
218+
expect(errorResult.errors, isEmpty);
219+
},
220+
);
221+
});
222+
});
223+
}
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
import 'package:freezed_annotation/freezed_annotation.dart';
2+
3+
part 'finalized.freezed.dart';
4+
5+
@Freezed(makeGeneratedClassesFinal: true)
6+
sealed class SealedWithFinalFoo with _$SealedWithFinalFoo {
7+
factory SealedWithFinalFoo() = _SealedWithFinalFoo;
8+
}
9+
10+
@Freezed(makeGeneratedClassesFinal: true)
11+
sealed class SealedWithFinalBar with _$SealedWithFinalBar {
12+
factory SealedWithFinalBar() = _SealedWithFinalBar;
13+
}
14+
15+
@Freezed(makeGeneratedClassesFinal: true)
16+
sealed class SealedWithFinalAbc with _$SealedWithFinalAbc {
17+
factory SealedWithFinalAbc.a() = SealedWithFinalAbcA;
18+
factory SealedWithFinalAbc.b() = SealedWithFinalAbcB;
19+
factory SealedWithFinalAbc.c() = SealedWithFinalAbcC;
20+
}
21+
22+
@Freezed(makeGeneratedClassesFinal: true)
23+
abstract class AbstractWithFinalFoo with _$AbstractWithFinalFoo {
24+
factory AbstractWithFinalFoo() = _AbstractWithFinalFoo;
25+
}
26+
27+
@Freezed(makeGeneratedClassesFinal: true)
28+
abstract class AbstractWithFinalBar with _$AbstractWithFinalBar {
29+
factory AbstractWithFinalBar() = _AbstractWithFinalBar;
30+
}
31+
32+
@Freezed(makeGeneratedClassesFinal: true)
33+
abstract class AbstractWithFinalAbc with _$AbstractWithFinalAbc {
34+
factory AbstractWithFinalAbc.a() = AbstractWithFinalAbcA;
35+
factory AbstractWithFinalAbc.b() = AbstractWithFinalAbcB;
36+
factory AbstractWithFinalAbc.c() = AbstractWithFinalAbcC;
37+
}
38+
39+
sealed class SuperFoo {
40+
const SuperFoo();
41+
}
42+
43+
final class CustomFoo extends SuperFoo {}
44+
45+
@Freezed(makeGeneratedClassesFinal: true)
46+
sealed class SealedWithFinalSuperFoo extends SuperFoo
47+
with _$SealedWithFinalSuperFoo {
48+
const SealedWithFinalSuperFoo._() : super();
49+
50+
factory SealedWithFinalSuperFoo() = _SealedWithFinalSuperFoo;
51+
}
52+
53+
@Freezed(makeGeneratedClassesFinal: true)
54+
abstract class AbstractWithFinalSuperFoo extends SuperFoo
55+
with _$AbstractWithFinalSuperFoo {
56+
const AbstractWithFinalSuperFoo._() : super();
57+
58+
factory AbstractWithFinalSuperFoo() = _AbstractWithFinalSuperFoo;
59+
}
60+
61+
class SubFoo extends AbstractWithFinalSuperFoo {
62+
SubFoo() : super._();
63+
}

packages/freezed_annotation/lib/freezed_annotation.dart

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -219,6 +219,7 @@ class Freezed {
219219
this.makeCollectionsUnmodifiable,
220220
this.addImplicitFinal = true,
221221
this.genericArgumentFactories = false,
222+
this.makeGeneratedClassesFinal,
222223
});
223224

224225
/// Decode the options from a build.yaml
@@ -489,6 +490,43 @@ class Freezed {
489490
/// If that value is null too, defaults to [FreezedWhenOptions.all].
490491
@_FreezedWhenOptionsConverter()
491492
final FreezedWhenOptions? when;
493+
494+
/// Whether to add `final` modifiers to the generated classes.
495+
///
496+
/// Defaults to false.
497+
///
498+
/// This makes the generated classes `final` by default,
499+
/// so when using them in a switch statement, the analzyer will warn you
500+
/// if you try to match against a pattern that will never match the type.
501+
///
502+
/// ```dart
503+
/// @Freezed(makeGeneratedClassesFinal: true)
504+
/// sealed class Foo with _$Foo {
505+
/// const Foo._();
506+
/// const factory Foo() = _Foo;
507+
/// }
508+
///
509+
/// @Freezed(makeGeneratedClassesFinal: true)
510+
/// sealed class Bar with _$Bar {
511+
/// const Bar._();
512+
/// const factory Bar() = _Bar;
513+
/// }
514+
///
515+
/// void main() {
516+
/// switch (Foo()) {
517+
/// // The analyzer will yield a warning that this case can never match,
518+
/// // because all subclasses of Foo are sealed/final, so it is guaranteed
519+
/// // that instances of type Bar can never also be of type Foo.
520+
/// case Bar():
521+
/// // ...
522+
/// break;
523+
///
524+
/// case Foo():
525+
/// // ...
526+
/// break;
527+
/// }
528+
/// ```
529+
final bool? makeGeneratedClassesFinal;
492530
}
493531

494532
/// Defines an immutable data-class.

packages/freezed_annotation/lib/freezed_annotation.g.dart

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)