Skip to content

Commit 6a1eac7

Browse files
committed
feat: add BaseClass and Initializer annotations
1 parent 784a245 commit 6a1eac7

File tree

5 files changed

+111
-4
lines changed

5 files changed

+111
-4
lines changed

lib/grumpy_annotations.dart

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,3 @@
11
export 'src/must_call_in_constructor.dart';
2+
export 'src/base_class.dart';
3+
export 'src/initializer.dart';

lib/src/base_class.dart

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
import 'package:meta/meta.dart';
2+
import 'package:meta/meta_meta.dart';
3+
4+
/// {@template layer_class}
5+
/// Marks a class as a base class.
6+
///
7+
/// Subclasses of this class have to follow the following rules:
8+
/// - They are only allowed to be defined in the layers specified in [allowedLayers].
9+
/// - They must have the name of the base class as a suffix.
10+
/// - The file must be in a subdirectory named after the base class in plural snake_case ([typeDirectory]).
11+
/// - The file the class is defined in must have the same name as the class in snake_case.
12+
/// - The file must not contain any other classes (except for extensions or mixins).
13+
/// - Classes defined in the [typeDirectory] must extend this base class.
14+
///
15+
/// Unit tests are exempt from these rules.
16+
/// {@endtemplate}
17+
@Target({.classType})
18+
@immutable
19+
class BaseClass {
20+
/// The layers in which subclasses are allowed to be defined.
21+
final Set<LayerType> allowedLayers;
22+
23+
/// The directory name where subclasses must be defined.
24+
///
25+
/// If not provided, it is inferred from the class name
26+
/// by converting it to plural snake_case.
27+
final String? typeDirectory;
28+
29+
/// If true, subclasses must have the name of the base class as a suffix.
30+
final bool forceSuffix;
31+
32+
/// {@macro layer_class}
33+
const BaseClass({
34+
this.allowedLayers = const {.domain, .infra, .presentation},
35+
this.typeDirectory,
36+
this.forceSuffix = true,
37+
});
38+
}
39+
40+
enum LayerType { infra, domain, presentation }
41+
42+
/// {@macro layer_class}
43+
const base = BaseClass();

lib/src/initializer.dart

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
import 'package:meta/meta.dart';
2+
import 'package:meta/meta_meta.dart';
3+
import 'package:grumpy_annotations/grumpy_annotations.dart';
4+
5+
/// {@template initializer}
6+
/// Marks a method as the *initialization entrypoint* of a class.
7+
///
8+
/// This annotation is meant for frameworks or patterns where the constructor
9+
/// is not the right place to run setup logic (e.g. Flutter `State.initState`).
10+
///
11+
/// If a class declares a method annotated with `@initializer`, then that method
12+
/// becomes the place where all [MustCallInConstructor]-annotated methods must
13+
/// be invoked.
14+
///
15+
/// In other words:
16+
///
17+
/// - **With an initializer:** required calls move from the constructor to the
18+
/// `@initializer` method.
19+
/// - **Without an initializer:** required calls must still happen in the
20+
/// constructor (default behavior of [MustCallInConstructor]).
21+
///
22+
/// The “called anywhere in the inheritance chain satisfies subclasses” rule
23+
/// still applies: if a superclass initializer performs the required calls,
24+
/// subclasses do not need to repeat them.
25+
///
26+
/// Example usage in a Flutter `State` class:
27+
/// ```dart
28+
/// class MyWidgetState extends State<MyWidget> with SomeMixin {
29+
/// @override
30+
/// @initializer
31+
/// void initState() {
32+
/// super.initState();
33+
///
34+
/// someMixinSetup(); // This method is annotated with [MustCallInConstructor] in SomeMixin
35+
/// }
36+
/// }
37+
/// ```
38+
///
39+
/// Flutter `State` objects often require setup in `initState()` rather than
40+
/// constructors. Annotate `initState()` with `@initializer`, and annotate the
41+
/// required setup hooks with [MustCallInConstructor] to enforce they are
42+
/// invoked from `initState()`.
43+
///
44+
/// See also:
45+
/// - [MustCallInConstructor] for marking required setup methods.
46+
///
47+
/// {@endtemplate}
48+
@immutable
49+
@Target({TargetKind.method})
50+
class Initializer {
51+
/// {@macro initializer}
52+
const Initializer();
53+
}
54+
55+
/// {@macro initializer}
56+
const initializer = Initializer();

lib/src/must_call_in_constructor.dart

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import 'package:meta/meta.dart';
22
import 'package:meta/meta_meta.dart';
3+
import 'package:grumpy_annotations/grumpy_annotations.dart';
34

45
/// {@template hook_installer}
56
/// An annotation indicating that the annotated method
@@ -14,6 +15,9 @@ import 'package:meta/meta_meta.dart';
1415
/// Note: If any class in the inheritance chain calls the
1516
/// annotated method in its constructor, the requirement
1617
/// is considered satisfied for subclasses.
18+
///
19+
/// See also:
20+
/// - [Initializer] for marking alternative initialization entrypoints.
1721
/// {@endtemplate}
1822
@Target({TargetKind.method})
1923
@immutable
@@ -25,14 +29,16 @@ class MustCallInConstructor {
2529
final bool concreteOnly;
2630

2731
/// A list of types that are exempt from this requirement.
32+
///
2833
/// If the class using the mixin is a subtype of any of these types,
29-
/// the requirement is considered satisfied.
30-
final List<Type> exceptions;
34+
/// the requirement is considered satisfied,
35+
/// and the method **must not** be called in the constructor.
36+
final List<Type> exempt;
3137

3238
/// {@macro hook_installer}
3339
const MustCallInConstructor({
3440
this.concreteOnly = true,
35-
this.exceptions = const [],
41+
this.exempt = const [],
3642
});
3743
}
3844

pubspec.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ version: 1.0.0
44
# repository: https://github.com/my_org/my_repo
55

66
environment:
7-
sdk: ^3.9.2
7+
sdk: ^3.10.0
88

99
# Add regular dependencies here.
1010
dependencies:

0 commit comments

Comments
 (0)