Skip to content

necodeIT/grumpy_lints

Repository files navigation

Grumpy Lints

Lint rules for using Grumpy architecture correctly.

Installation

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

Rules

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_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.

Codes

  • leaf_preview_must_not_use_injectables_or_navigation (WARNING)

Examples

❌ 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';
}

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).

Codes

  • 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)

Examples

❌ 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_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.

Codes

  • abstract_classes_should_set_log_group (INFO)

Examples

❌ 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_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.

Codes

  • concrete_classes_should_set_log_tag (INFO)

Examples

❌ 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';
}

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.

Codes

  • 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)

Examples

✅ 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 {}

domain_factory_from_di

Requires domain services and datasources (excluding base classes) to declare an unnamed factory constructor that retrieves the implementation from DI.

Codes

  • domain_factory_from_di_missing_factory (INFO)

Examples

❌ 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_domain_di_factory

Prefer using the domain contract factory constructor over direct DI access (Service.get/Datasource.get) outside the domain layer.

Codes

  • prefer_domain_di_factory (INFO)

Examples

❌ DON'T

final routing = Service.get<RoutingService>();

✅ DO

final routing = RoutingService();

lifecycle_mixin_requires_singleton

Classes that combine Injectable and LifecycleMixin must resolve as singletons by returning true from singelton.

Codes

  • lifecycle_mixin_requires_singleton (ERROR)

Examples

❌ 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;
}

About

Linting rules for using grumpy

Resources

License

Stars

Watchers

Forks

Contributors

Languages