Skip to content

Commit 2bdcffb

Browse files
committed
feat: allow making generated classes final
1 parent 9274c98 commit 2bdcffb

File tree

7 files changed

+365
-12
lines changed

7 files changed

+365
-12
lines changed

packages/freezed/lib/src/models.dart

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1066,6 +1066,9 @@ class Class {
10661066

10671067
return '$escapedElementName$generics';
10681068
}
1069+
1070+
bool get shouldBeFinal =>
1071+
options.annotation.makeGeneratedClassesFinal ?? false;
10691072
}
10701073

10711074
class PropertyList {
@@ -1190,6 +1193,11 @@ class ClassConfig {
11901193
},
11911194
orElse: () => globalConfigs.unionValueCase,
11921195
),
1196+
makeGeneratedClassesFinal: annotation.decodeField(
1197+
'makeGeneratedClassesFinal',
1198+
decode: (obj) => obj.toBoolValue(),
1199+
orElse: () => globalConfigs.makeGeneratedClassesFinal,
1200+
),
11931201
);
11941202
}
11951203

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

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

8990
/// Decode the options from a build.yaml
@@ -339,6 +340,43 @@ class Freezed {
339340
/// }
340341
/// ```
341342
final bool genericArgumentFactories;
343+
344+
/// Whether to add `final` modifiers to the generated classes.
345+
///
346+
/// Defaults to false.
347+
///
348+
/// This makes the generated classes `final` by default,
349+
/// so when using them in a switch statement, the analzyer will warn you
350+
/// if you try to match against a pattern that will never match the type.
351+
///
352+
/// ```dart
353+
/// @Freezed(makeGeneratedClassesFinal: true)
354+
/// sealed class Foo with _$Foo {
355+
/// const Foo._();
356+
/// const factory Foo() = _Foo;
357+
/// }
358+
///
359+
/// @Freezed(makeGeneratedClassesFinal: true)
360+
/// sealed class Bar with _$Bar {
361+
/// const Bar._();
362+
/// const factory Bar() = _Bar;
363+
/// }
364+
///
365+
/// void main() {
366+
/// switch (Foo()) {
367+
/// // The analyzer will yield a warning that this case can never match,
368+
/// // because all subclasses of Foo are sealed/final, so it is guaranteed
369+
/// // that instances of type Bar can never also be of type Foo.
370+
/// case Bar():
371+
/// // ...
372+
/// break;
373+
///
374+
/// case Foo():
375+
/// // ...
376+
/// break;
377+
/// }
378+
/// ```
379+
final bool? makeGeneratedClassesFinal;
342380
}
343381

344382
/// 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)