Skip to content

Commit b6993a7

Browse files
committed
feat: add rules for leaf preview and lifecycle mixin singleton requirements
1 parent a5425c2 commit b6993a7

10 files changed

+1128
-10
lines changed

README.md

Lines changed: 89 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,16 +2,68 @@
22

33
Lint rules for using [Grumpy](https://github.com/necodeIT/grumpy) architecture correctly.
44

5+
## Installation
6+
7+
To use the recommended lint rules drop the following into your `analysis_options.yaml`
8+
9+
```yaml
10+
plugins:
11+
grumpy_lints:
12+
diagnostics:
13+
leaf_preview_must_not_use_injectables_or_navigation: true
14+
must_call_in_constructor: true
15+
abstract_classes_should_set_log_group: true
16+
concrete_classes_should_set_log_tag: true
17+
base_class: true
18+
domain_factory_from_di: true
19+
prefer_domain_di_factory: true
20+
lifecycle_mixin_requires_singleton: true
21+
```
22+
23+
524
## Rules
625

726
| Rule | Overview | Severity | Fix Available | Codes |
827
| --- | --- | --- | --- | --- |
28+
| [leaf_preview_must_not_use_injectables_or_navigation](#leaf_preview_must_not_use_injectables_or_navigation) | Leaf.preview must remain side-effect free: do not resolve/use injectables or navigation APIs in preview, including through reachable helper calls. | WARNING || 1 |
929
| [must_call_in_constructor](#must_call_in_constructor) | Requires constructors to call methods annotated with @mustCallInConstructor from supertypes or mixins. It respects concreteOnly (abstract classes must not call those methods) and exempt (subtypes listed as exempt must not call the method at all). | ERROR || 6 |
1030
| [abstract_classes_should_set_log_group](#abstract_classes_should_set_log_group) | Abstract classes that mix in LogMixin must override `group` to return their class name. If they extend another abstract LogMixin class, they must append their class name to `super.group` to keep group names hierarchical. | INFO || 1 |
1131
| [concrete_classes_should_set_log_tag](#concrete_classes_should_set_log_tag) | Concrete (non-abstract) classes that mix in LogMixin must override `logTag` to return their own class name. This applies even when inheriting from another LogMixin class so each class logs with a specific tag. | INFO || 1 |
1232
| [base_class](#base_class) | Enforces the BaseClass contract: subclasses must live in allowed layers, use the base class name as a suffix when forceSuffix is true, reside in the configured type directory with a snake_case filename, be the only class in the file, and any class inside the type directory must extend the base class. Test files are exempt. | INFO || 6 |
13-
| [domain_factory_from_di](#domain_factory_from_di) | Requires domain services and datasources (excluding base classes) to declare an unnamed factory constructor that resolves the implementation from DI. | INFO || 1 |
33+
| [domain_factory_from_di](#domain_factory_from_di) | Requires domain services and datasources (excluding base classes) to declare an unnamed factory constructor that retrieves the implementation from DI. | INFO || 1 |
1434
| [prefer_domain_di_factory](#prefer_domain_di_factory) | Prefer using the domain contract factory constructor over direct DI access (Service.get/Datasource.get) outside the domain layer. | INFO || 1 |
35+
| [lifecycle_mixin_requires_singleton](#lifecycle_mixin_requires_singleton) | Classes that combine Injectable and LifecycleMixin must resolve as singletons by returning true from singelton. | ERROR || 1 |
36+
37+
38+
39+
### leaf_preview_must_not_use_injectables_or_navigation
40+
41+
Leaf.preview must remain side-effect free: do not resolve/use injectables or navigation APIs in preview, including through reachable helper calls.
42+
#### Codes
43+
- `leaf_preview_must_not_use_injectables_or_navigation` (WARNING)
44+
45+
#### Examples
46+
**❌ DON'T**
47+
```dart
48+
class HomeLeaf extends Leaf<String> {
49+
@override
50+
String preview(RouteContext ctx) {
51+
final api = Service.get<NetworkService>();
52+
return api.toString();
53+
}
54+
}
55+
56+
```
57+
58+
**✅ DO**
59+
```dart
60+
class HomeLeaf extends Leaf<String> {
61+
@override
62+
String preview(RouteContext ctx) => 'loading';
63+
}
64+
65+
```
66+
1567

1668

1769

@@ -297,25 +349,28 @@ abstract class UserService {}
297349

298350

299351

352+
300353
### domain_factory_from_di
301354

302-
Requires domain services and datasources (excluding base classes) to declare an unnamed factory constructor that resolves the implementation from DI.
355+
Requires domain services and datasources (excluding base classes) to declare an unnamed factory constructor that retrieves the implementation from DI.
303356
#### Codes
304357
- `domain_factory_from_di_missing_factory` (INFO)
305358

306359
#### Examples
307360
**❌ DON'T**
308361
```dart
309-
// Missing factory constructor.
362+
// BAD: missing factory constructor
310363
abstract class RoutingService<T, Config> extends Service {}
311364
312365
```
313366

314367
**✅ DO**
315368
```dart
316-
// Factory constructor resolves from DI.
369+
// GOOD: factory constructor resolves from DI
317370
abstract class RoutingService<T, Config> extends Service {
318371
/// Returns the DI-registered implementation of [RoutingService].
372+
///
373+
/// Shorthand for [Service.get]
319374
factory RoutingService() {
320375
return Service.get<RoutingService<T, Config>>();
321376
}
@@ -325,6 +380,7 @@ abstract class RoutingService<T, Config> extends Service {
325380

326381

327382

383+
328384
### prefer_domain_di_factory
329385

330386
Prefer using the domain contract factory constructor over direct DI access (Service.get/Datasource.get) outside the domain layer.
@@ -334,14 +390,41 @@ Prefer using the domain contract factory constructor over direct DI access (Serv
334390
#### Examples
335391
**❌ DON'T**
336392
```dart
337-
// Direct DI access from a non-domain layer.
338393
final routing = Service.get<RoutingService>();
339394
340395
```
341396

342397
**✅ DO**
343398
```dart
344-
// Use the domain factory constructor.
345399
final routing = RoutingService();
346400
347401
```
402+
403+
404+
405+
406+
### lifecycle_mixin_requires_singleton
407+
408+
Classes that combine Injectable and LifecycleMixin must resolve as singletons by returning true from singelton.
409+
#### Codes
410+
- `lifecycle_mixin_requires_singleton` (ERROR)
411+
412+
#### Examples
413+
**❌ DON'T**
414+
```dart
415+
class RoutingService with LifecycleMixin implements Injectable {
416+
@override
417+
bool get singelton => false;
418+
}
419+
420+
```
421+
422+
**✅ DO**
423+
```dart
424+
class RoutingService with LifecycleMixin implements Injectable {
425+
@override
426+
bool get singelton => true;
427+
}
428+
429+
```
430+

README.template

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,18 @@
22

33
Lint rules for using [Grumpy](https://github.com/necodeIT/grumpy) architecture correctly.
44

5+
## Installation
6+
7+
To use the recommended lint rules drop the following into your `analysis_options.yaml`
8+
9+
```yaml
10+
plugins:
11+
grumpy_lints:
12+
diagnostics:
13+
{{yaml_rules(4)}}
14+
```
15+
16+
517
## Rules
618

719
{{rules(3)}}

bin/readme.dart

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,9 @@ void main(List<String> args) {
77
final f = File('README.template');
88
var template = f.readAsStringSync();
99

10-
final regex = RegExp(r"{{rules\((\d*)\)}}");
10+
final rulesRegex = RegExp(r"{{rules\((\d*)\)}}");
1111

12-
final match = regex.firstMatch(template)!;
12+
final match = rulesRegex.firstMatch(template)!;
1313

1414
final count = int.parse(match.group(1) ?? '0');
1515

@@ -65,7 +65,17 @@ void main(List<String> args) {
6565
rulesBuffer.writeln('$subHeaderLevel Examples\n$examples\n');
6666
}
6767

68-
template = template.replaceFirst(regex, rulesBuffer.toString());
68+
template = template.replaceFirst(rulesRegex, rulesBuffer.toString());
69+
70+
final yamlRulesRegex = RegExp(r"{{yaml_rules\((\d*)\)}}");
71+
72+
final yamlMatch = yamlRulesRegex.firstMatch(template)!;
73+
74+
final indent = '\t' * int.parse(match.group(1) ?? '0');
75+
76+
final yamlRules = rules.map((r) => '$indent${r.name}: true').join('\n');
77+
78+
template = template.replaceFirst(yamlRulesRegex, yamlRules);
6979

7080
final outputFile = File('README.md');
7181
outputFile.writeAsStringSync(template);

lib/main.dart

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@ import 'package:grumpy_lints/src/rules/abstract_classes_should_set_log_group_rul
55
import 'package:grumpy_lints/src/rules/base_class_rule.dart';
66
import 'package:grumpy_lints/src/rules/concrete_classes_should_set_log_tag_rule.dart';
77
import 'package:grumpy_lints/src/rules/domain_factory_from_di_rule.dart';
8+
import 'package:grumpy_lints/src/rules/leaf_preview_must_not_use_injectables_or_navigation_rule.dart';
9+
import 'package:grumpy_lints/src/rules/lifecycle_mixin_requires_singleton_rule.dart';
810
import 'package:grumpy_lints/src/rules/prefer_domain_di_factory_rule.dart';
911
import 'package:grumpy_lints/src/rules/must_call_in_constructor_rule.dart';
1012

@@ -14,14 +16,17 @@ class GrumpyLints extends Plugin {
1416
@override
1517
String get name => 'grumpy';
1618

17-
static List<GrumpyRule> warnings = [];
19+
static List<GrumpyRule> warnings = [
20+
LeafPreviewMustNotUseInjectablesOrNavigationRule(),
21+
];
1822
static List<GrumpyRule> errors = [
1923
MustCallInConstructorRule(),
2024
AbstractClassesShouldSetLogGroupRule(),
2125
ConcreteClassesShouldSetLogTagRule(),
2226
BaseClassRule(),
2327
DomainFactoryFromDiRule(),
2428
PreferDomainDiFactoryRule(),
29+
LifecycleMixinRequiresSingletonRule(),
2530
];
2631

2732
@override

0 commit comments

Comments
 (0)