avoid_exit_outside_entrypoint
Warns when dart:io’s exit() is called outside the program’s entrypoint. By
default only bin/** is allowed.
exit() in domain code destroys testability outright: the test process
disappears mid-assertion. There is nothing to catch, nothing to assert on, and
a runner that reports nothing reads as a passing suite far more often than it
should.
This rule is in the recommended preset and works with its default
allow_in: ['bin/**']; configuration is only needed if your entrypoints live
somewhere else.
import 'dart:io';
Future<void> upload(File artifact, HttpClient http) async { final request = await http.putUrl(Uri.https('example.com', '/artifact')); final response = await request.close(); if (response.statusCode == 403) { stderr.writeln('Permission denied'); exit(3); }}A test of upload() cannot assert on that 403 branch — reaching it kills the
test runner.
Throw a typed error from the domain code…
import 'dart:io';
class AuthFailure implements Exception { const AuthFailure(this.message);
final String message;}
Future<void> upload(File artifact, HttpClient http) async { final request = await http.putUrl(Uri.https('example.com', '/artifact')); final response = await request.close(); if (response.statusCode == 403) { throw const AuthFailure('Permission denied'); }}…and map it to an exit code in the one file that is allowed to:
// bin/tool.dart — not reported, bin/** is allowed by default.import 'dart:io';
Future<void> main(List<String> args) async { try { await upload(File(args.first), HttpClient()); } on AuthFailure catch (e) { stderr.writeln(e.message); exit(3); }}The branch is now testable:
test('a 403 is an auth failure', () { expect(() => upload(artifact, http), throwsA(isA<AuthFailure>()));});More examples
Section titled “More examples”A tear-off is reported too
Section titled “A tear-off is reported too”Passing exit as a callback reaches the same place, one call later, so the
bare name is flagged as well as the call:
import 'dart:io';
void install(void Function(int) onFatal) {}
void wire() { install(exit); // reported — same process death, one hop away}Entrypoints outside bin/
Section titled “Entrypoints outside bin/”A repo that keeps its scripts in tool/ or its dev entrypoints in example/
lists them. allow_in replaces the default, so restate bin/** if you
still want it:
rules: avoid_exit_outside_entrypoint: allow_in: - 'bin/**' - 'tool/**'Or add to whichever list won, without restating it:
rules: avoid_exit_outside_entrypoint: additional_allow_in: - 'tool/**'Known limitations
Section titled “Known limitations”Only dart:io’s top-level exit is reported. The element is resolved, so a
Terminal().exit(3) of your own, Process.exit, or a local function named
exit are never flagged.
A file the analyzer cannot place relative to the package root is left alone rather than guessed at — reporting on an unclassifiable path risks flagging the entrypoint itself.
Options
Section titled “Options”many_lints: rules: avoid_exit_outside_entrypoint: allow_in: - 'bin/**' - 'tool/**'rules: avoid_exit_outside_entrypoint: allow_in: - 'bin/**' - 'tool/**'| Option | Type | Default | Description |
|---|---|---|---|
allow_in |
list of globs | ['bin/**'] |
Paths where exit() is permitted. Replaces the default |
additional_allow_in |
list of globs | [] |
Adds to whichever list won, without restating the default |
Globs are matched against the path relative to the package root, with / as
the separator on every platform.
Turning this rule off
Section titled “Turning this rule off”This rule is in the recommended preset, so it is on with
preset: recommended, preset: opinionated or preset: pedantic. Add it to
preset: core with avoid_exit_outside_entrypoint: true.
To turn it off:
rules: avoid_exit_outside_entrypoint: falseTo keep the rule on but skip certain paths, use per-rule exclude.
Related rules
Section titled “Related rules”avoid_accessing_other_classes_private_members— Make the underscore mean what everyone reads it as.avoid_commented_out_code— Detect and flag commented-out code.avoid_complex_conditions— Keep boolean conditions within an operand budget.avoid_deep_nesting— Keep control flow within a nesting budget.