dartway_lints 0.3.1
dartway_lints: ^0.3.1 copied to clipboard
Custom lint rules enforcing DartWay conventions: the UI kit as the single source of styles, relative imports kept short, ProviderScope left to the app bootstrap and tests, and a stored model rebuilt w [...]
dartway_lints #
Custom lint rules enforcing DartWay conventions.
Rules #
-
forbidden_ui_style_usage (warning) — the UI kit is the single source of styles. Flagged everywhere outside
ui_kit/: rawColor(...),TextStyle(...),BorderRadius(...),Colors.*,Theme.of(context), and the kit's own theme shortcuts (context.theme,context.textTheme,context.colorScheme— recognised by the type of the target, soctxis caught as readily ascontext). Generated files are left alone.Feature code composes kit widgets and tokens instead. When Flutter insists on a style rather than a widget —
InputDecoration.labelStyle, aTextSpan, anIcon's colour — that widget belongs in the kit, and the feature composes it. -
deep_relative_import (warning) — a relative import may walk at most two levels up. One or two
../read as "the feature next door"; beyond that the path names nothing ('../../../../ui_kit/ui_kit.dart') and the destination belongs in the line:package:my_app/ui_kit/ui_kit.dart.Deliberately not
always_use_package_imports, which also forbids the legitimate neighbour. The limit doubles as a structure signal: a "sibling" four levels away is not a sibling — either the group fell apart, or the thing being imported belongs inshared/orcommon/. -
forbidden_provider_scope (warning) — the application does not write a
ProviderScope. The only one belongs toDwAppRunner, which wraps the whole app; files undertest/(and*_test.dart) build their own freely.A nested scope with
overrides:is the mistake that does not look like one: widgets under it do read the override, while a provider reaching the same provider through its ownRefresolves from the root container and silently gets the base value.riverpod_lint's rule for this skips providers it cannot prove scoped — which it can only do for generated ones, and DartWay writes them by hand. A value that must differ per subtree is a family key or a constructor argument. -
model_rebuild_by_constructor (warning) — a Serverpod model constructor called with a real
id:. A row being created never passes one (the id comes back from the database), so the call is rebuilding a row that already exists — and rebuilding iscopyWith's job.Serverpod makes a field with
default=, and any nullable field, an optional argument. A method that rebuilds a model by naming its fields therefore keeps compiling when the model grows a field, and quietly writes the default into it. One real project reset apriorityfield on every edit of the record for months; the compiler, the tests and the review all saw nothing. The method even carried a doc comment asking for the opposite — the field was added by a task that never opened that file.Nothing is lost by the ban. The one reason to rebuild by hand — "
copyWithcannot clear a nullable field" — is not true of the generated one: it takesObject? field = _Undefinedand testsfield is T? ? field : this.field, so passingnullclears and omitting keeps. Matched throughSerializableModelrather thanTableRow, because the client half of a generated model — the half an application is written against — implements only the former. Serverpod's own output (lib/src/protocol/,lib/src/generated/) is left alone, and so are the two ids that are not real:null, anddw.repo.mockModelId. The second is the sentinel a skeleton default carries —setupRepository(defaultModel: …)builds an instance from nothing to give a loading skeleton its shape, and there is no row there to rebuild. That exemption is whatcore/default_models.dartrests on; the file needs no// ignore_for_file:and neitherexample/nortemplate/carries one.
Testing #
example/ is the rules' test suite: files under lib/app/ carry
// expect_lint: <rule> above every line that must be reported, and the files
that must stay silent — lib/ui_kit/ writing styles freely, a feature importing
its neighbour one ../ away, test/ building its own ProviderScope,
lib/src/protocol/ rebuilding a model field by field the way the generator
does, lib/core/default_models.dart registering skeleton defaults on the
mockModelId sentinel — are there to catch an over-eager rule.
cd example && dart run custom_lint # fails on a missed or an unexpected lint
It runs the real analyzer, which is the point: this rule's one historical bug
was a visitor registered for the wrong AST node (context.textTheme is a
PrefixedIdentifier, not a PropertyAccess), so it matched nothing while every
unit test of its logic would still have passed.
Setup #
# pubspec.yaml
dev_dependencies:
custom_lint: ^0.8.0
dartway_lints: ^0.3.0
# analysis_options.yaml
analyzer:
plugins:
- custom_lint
Then run dart run custom_lint (or rely on the IDE integration).
Complements the dartway check command of
dartway_cli, which validates the
broader project structure. Part of the
DartWay monorepo.