better_effect_analyzer 0.1.2
better_effect_analyzer: ^0.1.2 copied to clipboard
Static analysis and whole-project Module graph validation for better_effect and better_effect_flutter.
better_effect_analyzer #
Static analysis for better_effect and better_effect_flutter.
The package contains two complementary tools:
- An official Dart Analysis Server plugin for immediate diagnostics in the IDE,
dart analyze, andflutter analyze. - A whole-project Module graph checker for validations that need information from multiple libraries, such as missing services and dependency cycles.
There is intentionally a single analyzer package. Core Effect correctness and optional Flutter MVVM architecture rules use the same resolved AST and the same plugin configuration, while the runtime packages stay free of analyzer dependencies.
Compatibility #
better_effect_analyzer 0.1.2 intentionally targets the analyzer line used by Flutter SDKs that pin meta to 1.18.0:
dependencies:
analysis_server_plugin: '>=0.3.14 <0.3.18'
analyzer: ^12.1.0
dev_dependencies:
analyzer_testing: 0.2.5
The dependency range is intentionally capped below 0.3.18: later 0.3.x
releases move to newer analyzer lines and can conflict with Flutter's
SDK-pinned meta version.
For IDE-only usage, the plugin can be configured under the top-level plugins: section of analysis_options.yaml; it does not need to be a normal Flutter runtime dependency. Add it to dev_dependencies only when you also want to run the project-wide graph CLI with dart run better_effect_analyzer.
Requirements #
- Dart 3.10 or newer for this tooling package.
- A Flutter SDK that bundles Dart 3.10 or newer when used in a Flutter app.
The official plugin system starts in Dart 3.10 / Flutter 3.38. This release keeps
a Dart 3.10 lower bound because the official analyzer plugin system starts at
Dart 3.10. That tooling constraint does not change the lower bound of better_effect or
better_effect_flutter.
Local workspace #
A simple local layout is:
workspace/
├── better_effect/
├── better_effect_flutter/
├── better_effect_analyzer/
└── my_app/
Enable the plugin in the top-level analysis_options.yaml of the app:
include: package:flutter_lints/flutter.yaml
plugins:
better_effect_analyzer:
path: /absolute/path/to/workspace/better_effect_analyzer
diagnostics:
repository_requests_repository: true
viewmodel_requests_service: true
widget_requests_business_dependency: true
singleton_viewmodel: true
Restart the Dart Analysis Server after changing the plugins section. The
current plugin resolver expects an absolute local path. Once the package is
published, the path can be replaced with a normal version constraint:
plugins:
better_effect_analyzer: ^0.1.2
The plugin is resolved directly from analysis_options.yaml; it does not need
to be added to the application's runtime dependencies. Add it as an optional
dev_dependency only when the graph CLI should be invokable from the app root:
dev_dependencies:
better_effect_analyzer:
path: ../better_effect_analyzer
Default correctness warnings #
These warnings are enabled as soon as the plugin is enabled.
discarded_effect #
Effects are lazy. Creating one as an expression statement does not run it:
repository.save(user);
// ^ Effect created but never executed.
Use an explicit execution or composition boundary:
await use.unwrap(repository.save(user));
return repository.save(user);
await runtime.run(repository.save(user));
unawaited_effect_context_operation #
Operations such as use.unwrap, use.result, use.tryAsync, and
use.acquire return Futures. Ignoring them also ignores the value and the
intended failure propagation:
use.unwrap(repository.save(user));
// ^ await or return this operation
await use.unwrap(repository.save(user));
missing_binding_type_argument #
Constructor-backed Bindings must identify the service they register. The
constructor parameter is intentionally typed as Function, so Dart cannot
infer T from a tear-off alone and otherwise falls back to Object:
Module([
.provide(DatabaseLive.new),
// ^ add the service contract
]);
Use an explicit contract:
Module([
.provide<Database>(DatabaseLive.new),
]);
The explicit type argument is optional when a typed ServiceKey<T> already
provides the inference constraint:
.provide(DatabaseLive.new, key: primaryDatabase)
incompatible_provider #
The constructor, instance, or resource registered by a Binding must satisfy the service contract:
Module([
.provide<UserRepository>(AnalyticsService.new),
// ^ not a UserRepository
]);
The rule supports both regular invocations and Dart dot shorthands.
duplicate_service_binding #
A directly declared Module cannot accidentally register the same service identity twice:
final appModule = Module([
.provide<Database>(SqliteDatabase.new),
.provide<Database>(MemoryDatabase.new),
// ^ duplicate default Database binding
]);
Bindings using different ServiceKey<T> values remain distinct.
Flutter MVVM architecture lints #
These lints are opt-in because not every project adopts the same boundaries.
They implement the architecture direction used by better_effect_flutter:
Views observe ViewModels, ViewModels coordinate repositories or use cases, and
repositories access low-level services.
repository_requests_repository #
final class BookingRepositoryLive implements BookingRepository {
AppEffect<Booking> create() => .result((use) async {
final users = use<UserRepository>();
// ^ move cross-repository composition to a UseCase or ViewModel
// ...
});
}
viewmodel_requests_service #
final class HomeViewModel extends EffectViewModel {
AppEffect<HomeData> load() => .result((use) async {
final api = use<HomeApiClient>();
// ^ request a Repository or UseCase instead
// ...
});
}
The rule recognizes common low-level names such as Service, Client, Api,
Database, DataSource, and Storage, plus conventional infrastructure paths.
widget_requests_business_dependency #
final class HomePage extends StatelessWidget {
@override
Widget build(BuildContext context) {
final repository = context.readEffectService<UserRepository>();
// ^ expose this through the ViewModel
// ...
}
}
readEffectService remains valid at composition boundaries, such as route and
Provider factories. The rule only reports direct business dependency reads
inside Widget or State classes.
singleton_viewmodel #
Module([
.singleton<HomeViewModel>(HomeViewModel.new),
// ^ ViewModels should normally follow their View or feature lifecycle
]);
The rule also recognizes lazy application lifetime registrations, including the
default .provide lifetime:
.provide<HomeViewModel>(HomeViewModel.new)
.provide<HomeViewModel>(
HomeViewModel.new,
lifetime: .singleton,
)
Whole-project Module graph #
The IDE rules are intentionally local and fast. Module completeness is a
whole-project property: a provider can be declared in one file, request a
service through use<T>() in another, and be composed into an application
Module in a third file.
With the optional dev dependency configured, run the graph checker from the app root:
dart run better_effect_analyzer
Without that dev dependency, run the executable from the analyzer package and pass the application path:
cd ../better_effect_analyzer
dart run better_effect_analyzer ../my_app
Explicit project path:
dart run better_effect_analyzer ../my_app
Check named root Modules:
dart run better_effect_analyzer \
--module appModule \
--module backgroundModule
Include test Modules:
dart run better_effect_analyzer --include-tests
CI-friendly output:
dart run better_effect_analyzer --format machine
JSON output:
dart run better_effect_analyzer --format json
missing_service #
The checker combines:
- required constructor parameters used by AutoInjector;
- contextual callable requests through
use<T>()andServices<T>(); - method requests through
use.service<T>()andservices.get<T>(); - static
Effect.service<T>()requests, including.service<T>()dot shorthands; - dependencies requested by
.resource(acquire: ...); - Module composition with spreads and
Module.merge; Module.overrideWith.
Example:
error lib/config/app_module.dart:12:3 [missing_service]
Provider 'UserRepository' requires 'Database', but Module 'appModule' doesn't provide it.
dependency_cycle #
Database -> SessionRepository -> Database
The checker reports cycles across both constructor-injected and contextual services.
Other graph diagnostics #
duplicate_service_bindingincompatible_providermodule_composition_cyclemodule_not_found
Root Module selection #
When no --module option is supplied, reusable Modules included by another
Module are treated as partial environments. Modules that are not composed into
another Module are treated as roots.
Use repeated --module options when root selection must be explicit or when
Module composition contains dynamic runtime control flow.
CI #
A typical Flutter pipeline runs both analysis layers:
flutter analyze --fatal-infos
dart run ../better_effect_analyzer --format machine
flutter test
Suppression #
Plugin diagnostics use the normal Dart suppression syntax:
// ignore: better_effect_analyzer/discarded_effect
repository.save(user);
Or for a file:
// ignore_for_file: better_effect_analyzer/repository_requests_repository
Architecture rules should preferably be disabled in analysis_options.yaml
when a project deliberately follows different boundaries.
Library API #
The graph checker can also be embedded in tooling:
import 'package:better_effect_analyzer/better_effect_analyzer.dart';
final result = await BetterEffectGraphChecker(
'/path/to/app',
).check(
options: const GraphCheckOptions(
moduleNames: {'appModule'},
),
);
Development #
dart pub get
./tool/check.sh
Static-analysis boundary #
The graph checker follows statically visible Module declarations, list spreads,
Module.merge, overrideWith, constructor tear-offs, and contextual service
requests. It cannot reconstruct arbitrary dependency lists assembled through
runtime reflection or complex dynamic control flow. Prefer declarative Modules
when compile-time graph validation matters.
Flutter / analyzer compatibility #
This release intentionally targets the analyzer 12.1 toolchain so it can coexist
with Flutter SDKs that pin meta to 1.18.0.
dependencies:
analysis_server_plugin: 0.3.14
analyzer: 12.1.0
dev_dependencies:
analyzer_testing: 0.2.5
test: 1.31.1
NamedArgument is an analyzer 13+ AST type. This package uses
NamedExpression, which is the corresponding API in analyzer 12.1.