banned_usage
Flags uses of a member you ban — one method, getter or constructor of a type rather than the whole type. The common need is DateTime.now() and Random() in code that should take an injected clock or seed.
DateTime.now() is a hidden input: a type that calls it cannot be tested at a chosen moment. Banning it inside the domain layer while leaving it available in the composition root puts the constraint where it belongs — the code that decides when may know the time; the code that decides what may not.
This rule is in no preset and reports nothing until you add banned:. Every example below shows the configuration that produces it.
many_lints: rules: banned_usage: banned: - deny: ['DateTime.now', 'Random.new'] in: ['lib/domain/**'] message: 'Inject a clock or seed so this stays testable.'rules: banned_usage: banned: - deny: ['DateTime.now', 'Random.new'] in: ['lib/domain/**'] message: 'Inject a clock or seed so this stays testable.'Members are written Type.member, matched on the type that declares the member. The unnamed constructor is spelled new.
Examples
Section titled “Examples”Force an injected clock
Section titled “Force an injected clock”- deny: ['DateTime.now'] in: ['lib/domain/**'] message: 'Inject a clock so this stays testable.'// Don't — in lib/domain/session.dartclass Session { const Session(this.expiresAt);
final DateTime expiresAt;
// LINT: cannot be tested without waiting for real time to pass. bool get isExpired => DateTime.now().isAfter(expiresAt);}// Do — in lib/domain/session.dartabstract class Clock { DateTime now();}
class Session { const Session({required this.expiresAt, required Clock clock}) : _clock = clock;
final DateTime expiresAt; final Clock _clock;
bool get isExpired => _clock.now().isAfter(expiresAt);}Ban the unnamed constructor, keep the named one
Section titled “Ban the unnamed constructor, keep the named one”Type.new names the unnamed constructor, so Random() is reported while
Random.secure() is left alone:
- deny: ['Random.new'] message: 'Pass a seeded Random so failures reproduce.'// Don'tint roll() => Random().nextInt(6); // LINT
// Doint roll(Random random) => random.nextInt(6);Ban a member declared on a supertype
Section titled “Ban a member declared on a supertype”The declaring type is what matches, so an Iterable.first entry also catches a
List receiver — a subclass cannot slip past:
- deny: ['Iterable.first'] message: 'first throws on empty; use firstOrNull and handle the null.'// Don'tint? firstScore(List<int> scores) => scores.first; // LINT
// Doint? firstScore(List<int> scores) => scores.firstOrNull;Writing List.first instead would report nothing here: first is declared on
Iterable, not on List.
Ban a member on every type
Section titled “Ban a member on every type”A bare member name, with no Type. prefix, bans it wherever it appears:
- deny: ['toString'] in: ['lib/api/**'] message: 'Serialize through toJson; toString is for debugging.'// Don't — in lib/api/payload.dartString encode(Object value) => value.toString(); // LINT
// DoString encode(Map<String, Object?> json) => jsonEncode(json);Prefer the qualified form unless you really mean every type — deny: ['now']
catches DateTime.now() and anything else named now.
Scope a layer, then carve out one exception
Section titled “Scope a layer, then carve out one exception”in: and exclude: do different jobs and compose. in: scopes a banned
entry to the paths where the policy applies; exclude: (available on every
rule) skips paths for the whole rule:
many_lints: rules: banned_usage: exclude: ['lib/src/cli/output/**'] banned: - deny: ['print', 'Stdout.write', 'Stdout.writeln', 'Stdout.add'] in: ['lib/**'] message: 'Return the value and let the renderer emit it.'rules: banned_usage: exclude: ['lib/src/cli/output/**'] banned: - deny: ['print', 'Stdout.write', 'Stdout.writeln', 'Stdout.add'] in: ['lib/**'] message: 'Return the value and let the renderer emit it.'Writing to stdout is now reported everywhere under lib/ except the one
renderer that owns the terminal.
Options
Section titled “Options”| Option | Type | Default | Description |
|---|---|---|---|
banned |
list of entries | [] |
The entries to enforce. With none, the rule reports nothing |
Per entry:
| Key | Type | Required | Description |
|---|---|---|---|
deny |
string or list | one of deny / deny_pattern |
Members banned by exact match, written Type.member or as a bare member |
deny_pattern |
string or list | one of deny / deny_pattern |
Regular expressions, anchored to the whole value and tried against both Type.member and the bare member |
in |
list of globs | no | Paths, relative to the package root, where the entry applies. Omit to apply everywhere |
message |
string | no | A project-specific explanation appended to the diagnostic |
Known limitations
Section titled “Known limitations”Write the declaring type. Iterable.first catches a List receiver, but List.first catches nothing — the member is declared once, on Iterable. When an entry unexpectedly reports nothing, this is usually why.
A pattern is tested against both spellings. deny_pattern is tried against the qualified Type.member and against the bare member, so DateTime\.now and now both report DateTime.now() — and an unanchored-looking pattern like .* bans every member it reaches.
Calls, reads and tear-offs, not declarations. The rule reports where a member is used. Declaring a method named now is avoid_banned_names’s job.
A malformed entry is skipped silently. An invalid regular expression costs you that pattern and nothing else — a plugin cannot report problems against a YAML file.
Related rules
Section titled “Related rules”avoid_banned_annotations— Ban specific annotations, optionally scoped by directory.avoid_banned_exports— Ban re-exports of specific libraries, optionally scoped by directory.avoid_banned_imports— Ban imports of specific libraries, optionally scoped by directory.avoid_banned_names— Ban specific identifiers from being used as declaration names.