Skip to content

avoid_banned_annotations

v1.0.0WarningConfigurableArchitecture

Flags annotations you ban, optionally only inside the directories you name. The motivating case is scope: @visibleForTesting is reasonable on a helper and wrong in a production directory.

@visibleForTesting says “this is private, except to tests”. That is a fair trade in a utility, but in a core production type it is how encapsulation erodes — the annotation makes each widening explicit without making it rare.

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:
avoid_banned_annotations:
banned:
- deny: ['visibleForTesting']
in: ['lib/production/**']
message: 'Production code must not widen visibility for tests.'

Write the annotation name without the @. Both @visibleForTesting and the prefixed @meta.visibleForTesting match a single visibleForTesting entry.

Stop production code opening itself up for tests

Section titled “Stop production code opening itself up for tests”
- deny: ['visibleForTesting']
in: ['lib/production/**']
message: 'Production code must not widen visibility for tests.'
// Don't — in lib/production/payment_service.dart
class PaymentService {
@visibleForTesting // LINT: widens visibility in production code
void resetLedger() {}
}
// Do — in lib/production/payment_service.dart: inject the seam instead
abstract class Ledger {
void reset();
}
class PaymentService {
const PaymentService(this._ledger);
final Ledger _ledger;
void refundAll() => _ledger.reset();
}

The same annotation under lib/testing/** is left alone — only the directories named in in: are checked.

Keep new code off a deprecated API surface

Section titled “Keep new code off a deprecated API surface”
- deny: ['deprecated']
in: ['lib/features/**']
message: 'Delete it or promote it; a feature module keeps no deprecations.'
// Don't — in lib/features/checkout/coupon.dart
class Coupon {
@deprecated // LINT
String get label => code;
final String code = '';
}
// Do
class Coupon {
const Coupon(this.code);
final String code;
}

deny_pattern is anchored to the whole name, so .*Override matches JsonKeyOverride but not OverrideBehaviour:

- deny_pattern: ['.*Override']
in: ['lib/domain/**']
message: 'Codegen overrides belong on the DTO, not the entity.'
// Don't — in lib/domain/order.dart
class JsonKeyOverride {
const JsonKeyOverride();
}
class Order {
@JsonKeyOverride() // LINT
final int total = 0;
}

Omit in: and the entry applies to the whole package:

- deny: ['experimental']
message: 'Experimental APIs must not be used outside a spike branch.'
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 Annotation names banned by exact match, written without the @
deny_pattern string or list one of deny / deny_pattern Regular expressions, anchored to the whole name
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

Names, not libraries. An entry matches by annotation name alone, so banning deprecated also catches an unrelated @deprecated you declared yourself. There is no package:uri#Name form here as there is on avoid_banned_types.

Constructor arguments are ignored. @Deprecated('use X') matches a Deprecated entry whatever it is passed; you cannot ban only some of its arguments.

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.