A Lint Rule That Matches Nothing Is Not Green

A custom lint rule can compile, execute, report zero diagnostics, and be completely broken.

That is the unpleasant lesson behind many static-analysis migrations. A removed API causes a useful red build. The obvious repair makes the project compile again. Then a changed query, stale type name, or weakened library check turns the rule into a silent spectator.

Green tooling is not evidence that the tool still detects anything.

The standard I should have applied is simple: every rule migration needs a positive fixture that must be flagged and a near-miss fixture that must not. Compilation is only the entrance exam.

A changed question changes the expected answer

In an analyzer 8.x migration, code that once reached from a SimpleIdentifier to staticElement can no longer ask the same question at the same position.

For a method invocation, one known-good route is to inspect the invocation’s static return type and its originating library. That shifts the subject from the receiver class to the returned value.

A check that previously expected MediaQuery may therefore need to expect MediaQueryData:

final returnElement = invocation.staticType?.element;
final libraryUri = returnElement?.library?.uri.toString();

if (returnElement?.name != 'MediaQueryData' ||
    libraryUri == null ||
    !libraryUri.startsWith('package:flutter/')) {
  return;
}

The dangerous migration is one that updates the access path but keeps the old expected name. It compiles. It runs. It returns early forever.

This is why API migration by autocomplete is not enough. When the subject changes, write down the semantic question in a sentence: “Does this invocation return Flutter’s MediaQueryData?” The code and fixtures can then be reviewed against that sentence.

Positive fixtures defend the reason the rule exists

A lint test suite often grows around exclusions because false positives are noisy. That pressure is understandable. Developers notice a bad diagnostic immediately.

False negatives are quieter. Nobody opens an issue saying, “The tool failed to warn me about the thing I did not know it should warn me about.”

For each rule, keep the smallest source snippet that must trigger it:

// expect_lint: example_rule
final width = MediaQuery.of(context).size.width;

The exact assertion mechanism depends on the lint harness. The invariant does not: a known violation must produce the expected diagnostic after every dependency migration.

Then add a near-miss with a locally defined type carrying a similar name. That fixture proves the library-origin check still matters and prevents the migrated rule from widening into name matching.

When I first checked the prefer_media_query_partial_methods suite, it had the positive fixture and excluded copyWith, but not that locally defined lookalike. The gap has since been closed by test_ignoresNonFlutterMediaQueryData, which declares its own MediaQueryData and asserts that no diagnostic fires. That it had to be added deliberately is the point: the pair is a standard to apply, not something a suite acquires on its own.

Together, the pair verifies both halves of usefulness:

real target → diagnostic
lookalike target → no diagnostic

Zero findings is a result that needs calibration

This principle extends beyond linting.

A search command can match nothing because its regex dialect changed. A test runner can report success after discovering zero tests. A compiler can emit no warnings because no source entered the target. Absence becomes evidence only after the observation path is calibrated.

For a lint rule, calibration is the positive fixture. It proves the visitor reaches the node, the semantic query resolves the intended symbol, and the reporter emits a diagnostic.

Without that fixture, “zero findings” is compatible with two opposite realities:

  • the codebase contains no violations;
  • the detector has stopped detecting.

A green check cannot choose between them on its own.

Treat rule semantics as the stable interface

Analyzer internals and element access paths will change. The intent of a good lint rule should move less often.

Name that intent in fixtures and comments rather than encoding it only through a chain of API calls. During the next migration, begin from the examples the rule must recognize, then find the supported semantic query that preserves those results.

Do not ask only, “What replaced this property?” Ask, “What fact was this property helping the rule establish?”

Then force the test suite to answer with at least one diagnostic.

If you maintain custom lint rules, open the rule with the cleanest CI output and deliberately insert one known violation into its fixture. If the suite stays green, you have not found a wonderfully clean codebase. You have found the next bug.