Skip to content

Commit a5425c2

Browse files
committed
feat: add domain factory and prefer domain DI factory rules with associated fixes and documentation
1 parent d337317 commit a5425c2

10 files changed

+973
-0
lines changed

README.md

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@ Lint rules for using [Grumpy](https://github.com/necodeIT/grumpy) architecture c
1010
| [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 |
1111
| [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 |
1212
| [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 |
14+
| [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 |
1315

1416

1517

@@ -293,3 +295,53 @@ abstract class UserService {}
293295
294296
```
295297

298+
299+
300+
### domain_factory_from_di
301+
302+
Requires domain services and datasources (excluding base classes) to declare an unnamed factory constructor that resolves the implementation from DI.
303+
#### Codes
304+
- `domain_factory_from_di_missing_factory` (INFO)
305+
306+
#### Examples
307+
**❌ DON'T**
308+
```dart
309+
// Missing factory constructor.
310+
abstract class RoutingService<T, Config> extends Service {}
311+
312+
```
313+
314+
**✅ DO**
315+
```dart
316+
// Factory constructor resolves from DI.
317+
abstract class RoutingService<T, Config> extends Service {
318+
/// Returns the DI-registered implementation of [RoutingService].
319+
factory RoutingService() {
320+
return Service.get<RoutingService<T, Config>>();
321+
}
322+
}
323+
324+
```
325+
326+
327+
328+
### prefer_domain_di_factory
329+
330+
Prefer using the domain contract factory constructor over direct DI access (Service.get/Datasource.get) outside the domain layer.
331+
#### Codes
332+
- `prefer_domain_di_factory` (INFO)
333+
334+
#### Examples
335+
**❌ DON'T**
336+
```dart
337+
// Direct DI access from a non-domain layer.
338+
final routing = Service.get<RoutingService>();
339+
340+
```
341+
342+
**✅ DO**
343+
```dart
344+
// Use the domain factory constructor.
345+
final routing = RoutingService();
346+
347+
```

lib/main.dart

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@ import 'package:grumpy_lints/src/rule.dart';
44
import 'package:grumpy_lints/src/rules/abstract_classes_should_set_log_group_rule.dart';
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';
7+
import 'package:grumpy_lints/src/rules/domain_factory_from_di_rule.dart';
8+
import 'package:grumpy_lints/src/rules/prefer_domain_di_factory_rule.dart';
79
import 'package:grumpy_lints/src/rules/must_call_in_constructor_rule.dart';
810

911
final plugin = GrumpyLints();
@@ -18,6 +20,8 @@ class GrumpyLints extends Plugin {
1820
AbstractClassesShouldSetLogGroupRule(),
1921
ConcreteClassesShouldSetLogTagRule(),
2022
BaseClassRule(),
23+
DomainFactoryFromDiRule(),
24+
PreferDomainDiFactoryRule(),
2125
];
2226

2327
@override
Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
1+
import 'package:analysis_server_plugin/edit/dart/correction_producer.dart';
2+
import 'package:analysis_server_plugin/edit/dart/dart_fix_kind_priority.dart';
3+
import 'package:analyzer/dart/ast/ast.dart';
4+
import 'package:analyzer_plugin/utilities/change_builder/change_builder_core.dart';
5+
import 'package:analyzer_plugin/utilities/fixes/fixes.dart';
6+
7+
class AddDomainFactoryFromDiFix extends ResolvedCorrectionProducer {
8+
static const _fixKind = FixKind(
9+
'grumpy.fix.addDomainFactoryFromDi',
10+
DartFixKindPriority.standard,
11+
'Add DI factory constructor',
12+
);
13+
14+
AddDomainFactoryFromDiFix({required super.context});
15+
16+
@override
17+
CorrectionApplicability get applicability =>
18+
CorrectionApplicability.singleLocation;
19+
20+
@override
21+
FixKind get fixKind => _fixKind;
22+
23+
@override
24+
Future<void> compute(ChangeBuilder builder) async {
25+
final classDecl = node.thisOrAncestorOfType<ClassDeclaration>();
26+
if (classDecl == null) {
27+
return;
28+
}
29+
30+
final body = classDecl.body;
31+
if (body is! BlockClassBody) {
32+
return;
33+
}
34+
35+
final namePart = classDecl.namePart;
36+
final className = namePart.typeName.lexeme;
37+
final typeArgs = _typeArgumentList(namePart.typeParameters);
38+
final classType = '$className$typeArgs';
39+
final accessor = _accessorFromPath(file);
40+
final needsPrivateConstructor = !_hasGenerativeConstructor(body);
41+
42+
final eol = utils.endOfLine;
43+
final classIndent = utils.getLinePrefix(body.rightBracket.offset);
44+
final memberIndent = classIndent + utils.oneIndent;
45+
final bodyBetween = utils.getText(
46+
body.leftBracket.end,
47+
body.rightBracket.offset - body.leftBracket.end,
48+
);
49+
final leadingEol = bodyBetween.contains('\n') || bodyBetween.contains('\r')
50+
? ''
51+
: eol;
52+
53+
final source = StringBuffer()
54+
..write(leadingEol)
55+
..write(
56+
_privateConstructorSource(
57+
needsPrivateConstructor,
58+
className,
59+
memberIndent,
60+
eol,
61+
),
62+
)
63+
..write(
64+
'$memberIndent/// Returns the DI-registered implementation of [$className].',
65+
)
66+
..write(eol)
67+
..write('$memberIndent///')
68+
..write(eol)
69+
..write('$memberIndent/// Shorthand for [$accessor.get].')
70+
..write(eol)
71+
..write(
72+
'$memberIndent'
73+
'factory $className() {$eol',
74+
)
75+
..write(
76+
'$memberIndent${utils.oneIndent}return $accessor.get<$classType>();$eol',
77+
)
78+
..write('$memberIndent}$eol');
79+
80+
await builder.addDartFileEdit(file, (builder) {
81+
builder.addInsertion(body.rightBracket.offset, (builder) {
82+
builder.write(source.toString());
83+
});
84+
});
85+
}
86+
87+
String _typeArgumentList(TypeParameterList? typeParameters) {
88+
if (typeParameters == null || typeParameters.typeParameters.isEmpty) {
89+
return '';
90+
}
91+
final names = typeParameters.typeParameters
92+
.map((parameter) => parameter.name.lexeme)
93+
.join(', ');
94+
return '<$names>';
95+
}
96+
97+
String _accessorFromPath(String path) {
98+
final normalized = path.replaceAll('\\', '/');
99+
if (normalized.contains('/domain/datasources/')) {
100+
return 'Datasource';
101+
}
102+
return 'Service';
103+
}
104+
105+
String _privateConstructorSource(
106+
bool shouldAdd,
107+
String className,
108+
String indent,
109+
String eol,
110+
) {
111+
if (!shouldAdd) {
112+
return '';
113+
}
114+
return '$indent/// Internal constructor for subclasses.\n$indent$className.internal();$eol';
115+
}
116+
117+
bool _hasGenerativeConstructor(BlockClassBody body) {
118+
for (final member in body.members) {
119+
if (member is ConstructorDeclaration) {
120+
if (member.factoryKeyword == null) {
121+
return true;
122+
}
123+
}
124+
}
125+
return false;
126+
}
127+
}
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
import 'package:analysis_server_plugin/edit/dart/correction_producer.dart';
2+
import 'package:analysis_server_plugin/edit/dart/dart_fix_kind_priority.dart';
3+
import 'package:analyzer/dart/ast/ast.dart';
4+
import 'package:analyzer_plugin/utilities/change_builder/change_builder_core.dart';
5+
import 'package:analyzer_plugin/utilities/fixes/fixes.dart';
6+
import 'package:analyzer_plugin/utilities/range_factory.dart';
7+
8+
class PreferDomainDiFactoryFix extends ResolvedCorrectionProducer {
9+
static const _fixKind = FixKind(
10+
'grumpy.fix.preferDomainDiFactory',
11+
DartFixKindPriority.standard,
12+
'Use domain DI factory constructor',
13+
);
14+
15+
PreferDomainDiFactoryFix({required super.context});
16+
17+
@override
18+
CorrectionApplicability get applicability =>
19+
CorrectionApplicability.singleLocation;
20+
21+
@override
22+
FixKind get fixKind => _fixKind;
23+
24+
@override
25+
Future<void> compute(ChangeBuilder builder) async {
26+
final invocation = node.thisOrAncestorOfType<MethodInvocation>();
27+
if (invocation == null) {
28+
return;
29+
}
30+
31+
final typeName = _typeNameFrom(invocation.typeArguments);
32+
if (typeName == null) {
33+
return;
34+
}
35+
36+
final replacement = '$typeName()';
37+
38+
await builder.addDartFileEdit(file, (builder) {
39+
builder.addReplacement(range.node(invocation), (builder) {
40+
builder.write(replacement);
41+
});
42+
});
43+
}
44+
45+
String? _typeNameFrom(TypeArgumentList? typeArguments) {
46+
if (typeArguments == null || typeArguments.arguments.isEmpty) {
47+
return null;
48+
}
49+
final first = typeArguments.arguments.first;
50+
if (first is! NamedType) {
51+
return null;
52+
}
53+
54+
final prefix = first.importPrefix?.name.lexeme;
55+
final name = first.name.lexeme;
56+
final typeArgs = first.typeArguments;
57+
final typeArgText = typeArgs == null
58+
? ''
59+
: utils.getText(typeArgs.offset, typeArgs.length);
60+
61+
final buffer = StringBuffer();
62+
if (prefix != null && prefix.isNotEmpty) {
63+
buffer.write(prefix);
64+
buffer.write('.');
65+
}
66+
buffer.write(name);
67+
buffer.write(typeArgText);
68+
return buffer.toString();
69+
}
70+
}

0 commit comments

Comments
 (0)