Skip to content

banned_usage

v1.0.0WarningConfigurableArchitecture

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.

analysis_options.yaml
many_lints:
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.

- deny: ['DateTime.now']
in: ['lib/domain/**']
message: 'Inject a clock so this stays testable.'
// Don't — in lib/domain/session.dart
class 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.dart
abstract 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't
int roll() => Random().nextInt(6); // LINT
// Do
int roll(Random random) => random.nextInt(6);

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't
int? firstScore(List<int> scores) => scores.first; // LINT
// Do
int? firstScore(List<int> scores) => scores.firstOrNull;

Writing List.first instead would report nothing here: first is declared on Iterable, not on List.

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.dart
String encode(Object value) => value.toString(); // LINT
// Do
String 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:

analysis_options.yaml
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.'

Writing to stdout is now reported everywhere under lib/ except the one renderer that owns the terminal.

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

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.