Lint rules for using Grumpy architecture correctly.
To use the recommended lint rules drop the following into your analysis_options.yaml
plugins:
grumpy_lints:
diagnostics:
leaf_preview_must_not_use_injectables_or_navigation: true
must_call_in_constructor: true
abstract_classes_should_set_log_group: true
concrete_classes_should_set_log_tag: true
base_class: true
domain_factory_from_di: true
prefer_domain_di_factory: true
lifecycle_mixin_requires_singleton: true| Rule | Overview | Severity | Fix Available | Codes |
|---|---|---|---|---|
| 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 |
| must_call_in_constructor | Requires constructors to call methods annotated with @mustCallInConstructor from supertypes or mixins | ERROR | ✅ | 6 |
| abstract_classes_should_set_log_group | Abstract classes that mix in LogMixin must override group to return their class name |
INFO | ✅ | 1 |
| concrete_classes_should_set_log_tag | Concrete (non-abstract) classes that mix in LogMixin must override logTag to return their own class name |
INFO | ✅ | 1 |
| 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 | INFO | ❌ | 6 |
| 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 |
| 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 |
| lifecycle_mixin_requires_singleton | Classes that combine Injectable and LifecycleMixin must resolve as singletons by returning true from singelton. | ERROR | ❌ | 1 |
Leaf.preview must remain side-effect free: do not resolve/use injectables or navigation APIs in preview, including through reachable helper calls.
leaf_preview_must_not_use_injectables_or_navigation(WARNING)
❌ DON'T
class HomeLeaf extends Leaf<String> {
@override
String preview(RouteContext ctx) {
final api = Service.get<NetworkService>();
return api.toString();
}
}
✅ DO
class HomeLeaf extends Leaf<String> {
@override
String preview(RouteContext ctx) => 'loading';
}
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).
missing_required_constructor_call(ERROR)missing_required_initializer_call(ERROR)avoid_abstract_constructor_calls(ERROR)avoid_abstract_initializer_calls(ERROR)avoid_exempt_constructor_calls(INFO)avoid_exempt_initializer_calls(INFO)
❌ DON'T
// Missing required call in the constructor.
mixin InitMixin {
@mustCallInConstructor
void init() {}
}
class Widget with InitMixin {
Widget();
}
✅ DO
// Required call is present.
mixin InitMixin {
@mustCallInConstructor
void init() {}
}
class Widget with InitMixin {
Widget() {
init();
}
}
❌ DON'T
// Abstract classes must not call methods that are concreteOnly.
mixin InitMixin {
@MustCallInConstructor(concreteOnly: true)
void init() {}
}
abstract class BaseWidget with InitMixin {
BaseWidget() {
init();
}
}
❌ DON'T
// Exempt types must not call the annotated method.
mixin InitMixin {
@MustCallInConstructor(exempt: [NoopWidget])
void init() {}
}
class NoopWidget with InitMixin {
NoopWidget() {
init();
}
}
✅ DO
// Exempt types can omit the call entirely.
mixin InitMixin {
@MustCallInConstructor(exempt: [NoopWidget])
void init() {}
}
class NoopWidget with InitMixin {
NoopWidget();
}
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.
abstract_classes_should_set_log_group(INFO)
❌ DON'T
// Missing group override on an abstract LogMixin class.
abstract class MyAbstractClass with LogMixin {
// ...
}
❌ DON'T
// Missing group override when extending another LogMixin class.
abstract class BaseAbstractClass with LogMixin {
@override
String get group => 'BaseAbstractClass';
}
abstract class DerivedAbstractClass extends BaseAbstractClass {
// ...
}
✅ DO
// Group must match the abstract class name.
abstract class MyAbstractClass with LogMixin {
@override
String get group => 'MyAbstractClass';
}
✅ DO
// Derived abstract classes must append to super.group.
abstract class BaseAbstractClass with LogMixin {
@override
String get group => 'BaseAbstractClass';
}
abstract class DerivedAbstractClass extends BaseAbstractClass {
@override
String get group => '${super.group}.DerivedAbstractClass';
}
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.
concrete_classes_should_set_log_tag(INFO)
❌ DON'T
// Missing logTag override on a concrete LogMixin class.
class MyConcreteClass with LogMixin {
// ...
}
✅ DO
// Concrete class must use its own class name as logTag.
class MyConcreteClass with LogMixin {
@override
String get logTag => 'MyConcreteClass';
}
❌ DON'T
// Missing logTag override when extending another LogMixin class.
abstract class BaseClass with LogMixin {
@override
String get group => 'BaseClass';
}
class DerivedConcreteClass extends BaseClass {
// ...
}
✅ DO
// Derived concrete classes must override logTag too.
abstract class BaseClass with LogMixin {
@override
String get group => 'BaseClass';
}
class DerivedConcreteClass extends BaseClass {
@override
String get logTag => 'DerivedConcreteClass';
}
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.
base_class_invalid_layer(INFO)base_class_missing_suffix(INFO)base_class_wrong_directory(INFO)base_class_wrong_file_name(INFO)base_class_extra_class(INFO)base_class_missing_extension(INFO)
✅ DO
// Base class:
@BaseClass(allowedLayers: {LayerType.domain}, typeDirectory: 'services')
abstract class Service {}
// File: lib/src/module/domain/services/user_service.dart
abstract class UserService extends Service {}
❌ DON'T
// Wrong layer (presentation is not allowed).
// File: lib/src/module/presentation/services/user_service.dart
abstract class UserService extends Service {}
❌ DON'T
// Missing suffix when forceSuffix is true.
// File: lib/src/module/domain/services/user_manager.dart
abstract class UserManager extends Service {}
❌ DON'T
// Wrong directory (should be services/).
// File: lib/src/module/domain/user_service.dart
abstract class UserService extends Service {}
❌ DON'T
// Wrong file name (should be user_service.dart).
// File: lib/src/module/domain/services/userService.dart
abstract class UserService extends Service {}
❌ DON'T
// Extra class in the same file.
// File: lib/src/module/domain/services/user_service.dart
abstract class UserService extends Service {}
class Helper {}
❌ DON'T
// Class in the services/ directory must extend Service.
// File: lib/src/module/domain/services/user_service.dart
abstract class UserService {}
Requires domain services and datasources (excluding base classes) to declare an unnamed factory constructor that retrieves the implementation from DI.
domain_factory_from_di_missing_factory(INFO)
❌ DON'T
// BAD: missing factory constructor
abstract class RoutingService<T, Config> extends Service {}
✅ DO
// GOOD: factory constructor resolves from DI
abstract class RoutingService<T, Config> extends Service {
/// Returns the DI-registered implementation of [RoutingService].
///
/// Shorthand for [Service.get]
factory RoutingService() {
return Service.get<RoutingService<T, Config>>();
}
}
Prefer using the domain contract factory constructor over direct DI access (Service.get/Datasource.get) outside the domain layer.
prefer_domain_di_factory(INFO)
❌ DON'T
final routing = Service.get<RoutingService>();
✅ DO
final routing = RoutingService();
Classes that combine Injectable and LifecycleMixin must resolve as singletons by returning true from singelton.
lifecycle_mixin_requires_singleton(ERROR)
❌ DON'T
class RoutingService with LifecycleMixin implements Injectable {
@override
bool get singelton => false;
}
✅ DO
class RoutingService with LifecycleMixin implements Injectable {
@override
bool get singelton => true;
}