Lints, Analyzer & Code Review
analysis_options, lint rulesets, dart fix, dart format, custom_lint, annotations, PR review
- 01
dart analyzeprints lines tagged error, warning and info — which of those does analysis_options.yaml control, and which would fail the build anyway?EasyOnly the analyzer reads
analysis_options.yaml: it decides which diagnostics are produced and at what severity, and the compiler never opens the file — so a genuine type error fails the build whatever you write there.# analysis_options.yaml — everything the analyzer reads about this package include: package:flutter_lints/flutter.yaml # inherit a published ruleset analyzer: exclude: # not analysed at all: no lints, no errors - "**/*.g.dart" … - 02
The team wants to swap flutter_lints for very_good_analysis — what actually changes in the repo the morning you edit that include line?
EasyYou go from roughly a hundred rules to a hundred and ninety-three,
dart fixclears most of the new findings mechanically, and what it cannot fix is the style argument you are really having.# --- What flutter create gives you: lints/core + lints/recommended + 10 rules include: package:flutter_lints/flutter.yaml # --- The strict end: 193 rules, docs and line length included # include: package:very_good_analysis/analysis_options.yaml … - 03
Which lints have earned
errors: <rule>: errorin your analysis_options, and which promotion just teaches the team to write ignore comments?EasyPromote the rules whose violations are bugs a reviewer cannot see — the async and context ones — and leave naming, layout and documentation at info, because a style rule that breaks the build gets an ignore comment rather than a fix.
include: package:flutter_lints/flutter.yaml analyzer: errors: # --- Promoted: each one is a bug class review cannot see --- use_build_context_synchronously: error # crash after an await … - 04
A PR silences a lint with
// ignore_for_file:at the top of a 900-line file — what do you ask for instead, and what does each escape hatch actually hide?MediumAsk for a line-level
// ignore:with a comment saying why, because the file-level form also silences code nobody has written yet, andexcluderemoves the file from analysis entirely — errors included.// ignore_for_file: type=lint // ❌ every lint in this file, including // lints on code written next year class Repo { // ✅ narrowest form: one line, one rule, and a reason a reviewer can check // ignore: avoid_dynamic_calls -- payload shape is asserted in decode() … - 05
CI fails
dart analyzeon a file your editor has called clean all afternoon — it is the same analyzer, so what actually differs?MediumBoth sides run the same
package:analyzer, so the difference is never the tool — it is the inputs: the SDK, the resolved dependency versions, what is actually on disk, and which folders are in scope.# Editor green, CI red. Make the inputs identical, in this order. # 1. Same SDK the job uses — the IDE must point at the same one fvm use 3.35.0 && fvm flutter --version dart --version … - 06
dart format .on a newer SDK rewrites 700 files nobody touched — what changed, and how do you land that without destroying git blame?MediumDart 3.7 replaced the formatter's style, and the new one is chosen by the package's language version rather than by a flag — so raising your SDK constraint reformats the repository, which is exactly why it has to be a commit of its own listed in
.git-blame-ignore-revs.# What actually changed: the tall style ships in Dart 3.7 and is selected by # the package's language version, so bumping `environment: sdk:` reformats # the repo. No flag turns it off. cat > lib/f.dart <<'DART' int add(int a, int b,) => a + b; void main() { … - 07
A reviewer calls the lint set cosmetic and asks to turn most of it off — which Flutter lints actually change what the app does at runtime?
MediumA minority do, and they are worth naming: the const family decides whether whole subtrees rebuild, and a small correctness group catches comparisons and overrides that are silently wrong at runtime.
// ── const is a rebuild boundary, not a style preference ── class Header extends StatelessWidget { const Header({super.key}); @override Widget build(BuildContext context) => const Text('Inbox'); } … - 08
What production bug does each of avoid_print, unawaited_futures and use_build_context_synchronously actually prevent?
MediumA token in the device log, an exception nobody ever sees, and a crash on a screen the user has already left.
// ── avoid_print: this line ships to users ── print('auth response: $token'); // ❌ release build, logcat, forever if (kDebugMode) log('auth ok', name: 'auth'); // ✅ dart:developer, debug only // ── unawaited_futures: the error has nowhere to go ── Future<void> save(BuildContext context) async { … - 09
Your team keeps leaving the same review comment — "a widget must not build a repository". How do you turn that into a lint?
MediumWrite it as a
custom_lintrule: a small Dart package the analysis server loads, which is handed the resolved AST of each file and whose diagnostics show up in the IDE and in CI.// analysis_options.yaml // analyzer: // plugins: // - custom_lint // // CI needs its own step — `dart analyze` will not run any of this: … - 10
The bundle carries files nobody opens and pubspec lists packages nobody imports — what actually finds them, and what can no tool find?
MediumThe analyzer only reports what it can prove inside one library — private declarations and unreachable statements — so everything public, every dependency and every asset needs a separate whole-repo pass.
# 1. What the analyzer already knows — private declarations, unreachable code. dart analyze --fatal-infos # 2. Dependencies nothing imports, and imports with no dependency. dart pub global activate dependency_validator dart run dependency_validator … - 11
A reviewer wants the build to fail when a file passes 300 lines or a function's cyclomatic complexity passes 10. Would you take that deal?
MediumTake it as a signal on the diff, never as a gate on the repository — the number tells you which file is worth reading and nothing at all about whether the code is good.
# analysis_options.yaml include: package:flutter_lints/flutter.yaml analyzer: exclude: - "**/*.g.dart" … - 12
You need to rename a method in a package five apps depend on. How do you retire the old name without breaking anyone's build on Monday?
MediumAdd the new name, mark the old one
@Deprecatedwith a message that names the replacement, keep both through at least one release, and delete it only in a major version.class UserApi { // The new name. The only implementation lives here. Future<User> parseUser(String body) async => User.fromJson(jsonDecode(body)); @Deprecated('Use parseUser instead. Will be removed in 3.0.0.') Future<User> decodeUser(String body) => parseUser(body); // forwards, never copies … - 13
@visibleForTesting, @protected, @immutable and @mustCallSuper — which of those does the analyzer actually enforce, and how far does each go?
MediumAll four are enforced, but only by the analyzer and only as warnings — nothing stops the code compiling, running or shipping.
import 'package:meta/meta.dart'; @immutable class Filter { const Filter(this.tags); final List<String> tags; // ✅ satisfies must_be_immutable … … - 14
Half the team's commits arrive unformatted despite a pre-commit hook. What belongs in a hook, what belongs in CI, and how do you stop the hook being skipped?
HardCI is the gate and the hook is only a convenience, because hooks live in
.git/hooks, which is never cloned, andgit commit --no-verifyskips them by design.# lefthook.yml — committed. `lefthook install` wires .git/hooks on every clone. pre-commit: parallel: true commands: format: glob: "*.dart" … - 15
Turning on a strict ruleset in a 200 000-line app reports 4 000 issues. How do you land it without one unreviewable diff?
HardTurn the whole ruleset on and demote every rule it lights up to
ignorein the same commit, then delete entries from that ignore list one pull request at a time.# 1. The machine-fixable half, in its own commit, with zero manual edits. dart fix --dry-run # what would change, and under which rule dart fix --apply # or: --code=prefer_const_constructors git commit -am "chore: dart fix --apply (machine-generated, no manual edits)" git rev-parse HEAD >> .git-blame-ignore-revs git config blame.ignoreRevsFile .git-blame-ignore-revs … - 16
The analyzer is clean and the tests pass. What are you still looking for in a Flutter pull request, and how do you raise it without stalling the author?
HardEverything that depends on conditions the diff does not show — lifecycle, states the code can reach but the tests never do, the failure path, and whether the change belongs in the layer it landed in.
// The diff as it arrives: analyzer clean, one green widget test. class _OrderPageState extends State<OrderPage> { final _note = TextEditingController(); Order? _order; bool _loading = false; String? _error; // blocking: _loading && _error != null is reachable. …