prefer_do_notation
This rule flags flatMap callbacks nested three or more levels deep on an fpdart type.
Why use this rule
Section titled “Why use this rule”Do is sugar over flatMap with identical semantics — same short-circuit on the first None/Left, same order of effects. The difference is entirely shape.
Nested callbacks indent one level per step and push each step’s value into a closure, so by the third the reader is tracking which ) closes what, and the trailing ))) has to be counted rather than read. A Do block is flat regardless of step count, and every extracted value is an ordinary local that later steps just refer to by name.
See also: fpdart: Do notation
Three steps, each one wrapping the next in a callback. By the third the reader
is counting brackets to work out which ) closes what:
TaskEither<String, String> readConfig(String path) => TaskEither.of('{}');
TaskEither<String, int> parsePort(String config) => TaskEither.of(8080);
TaskEither<String, String> connect(int port) => TaskEither.of('ready');
TaskEither<String, String> start(String path) => readConfig(path).flatMap( (config) => parsePort(config).flatMap( (port) => connect(port).flatMap( (status) => TaskEither.of('$status on $port'), ), ), );Every extracted value is an ordinary local, and the block stays flat however many steps it grows to:
TaskEither<String, String> start(String path) => TaskEither.Do(($) async { final config = await $(readConfig(path)); final port = await $(parsePort(config)); final status = await $(connect(port)); return '$status on $port';});A synchronous Option pipeline is the same shape without the await:
Option<String> label(Option<String> first, Option<String> last) => Option.Do(($) { final given = $(first); final family = $(last); return '$given $family'; });Known limitations
Section titled “Known limitations”Only nesting is counted, not chaining. a.flatMap(f).flatMap(g).flatMap(h) is already flat and reads fine — it is a.flatMap((x) => b.flatMap((y) => ...)) that grows sideways.
Exactly one diagnostic is reported per nest, on the outermost call, because the whole nest is one shape with one fix. Reporting each level would produce a diagnostic per step.
There is no quick fix, but there is an assist: put the cursor on any flatMap in the nest and pick “Convert to Do notation”. It generates the block, takes each step’s name from that callback’s own parameter, and offers every generated name as a linked edit position — so accepting the assist drops the cursor on the first name with the rest reachable by Tab.
Going back the other way
Section titled “Going back the other way”The inverse assist, “Convert to flatMap chain”, is offered with the cursor anywhere in a Do block, for when this rule’s preference is the wrong call for a particular pipeline.
It converts only the straight-line shape: a run of final <name> = $(...) bindings followed by a single return. A block that branches, loops, or extracts inside a larger expression is declined rather than half-translated.
A plain return x becomes Type.of(x) on the way out, since Do lifts its own result and a chain does not; await $(...) in a TaskEither.Do loses the await, which belonged to the block rather than to the step.
Options
Section titled “Options”many_lints: rules: prefer_do_notation: max_flat_map_depth: 2rules: prefer_do_notation: max_flat_map_depth: 2| Option | Type | Default | Description |
|---|---|---|---|
max_flat_map_depth |
int | 3 |
How deeply flatMap callbacks may nest before the outermost is reported. 2 pushes a team toward Do almost immediately; 4 reserves it for genuinely long pipelines |
max_flatmap_depth |
int | 3 |
Deprecated compatibility alias for max_flat_map_depth |
Configuration
Section titled “Configuration”This rule is in the opinionated preset. With a lower preset, enable it by
name with prefer_do_notation: true.
To turn it off:
rules: prefer_do_notation: falseTo keep the rule on but skip certain paths, use per-rule exclude.
Related rules
Section titled “Related rules”avoid_nested_do_notation— A nested Do block short-circuits on its own instead of failing the outer pipeline.avoid_bare_await_in_do— Awaiting a raw Future inside a Do block escapes the block’s tracking.avoid_dollar_outside_do_frame— Calling a Do block’s extraction function from a nested callback unwinds through code that cannot handle it.avoid_ad_hoc_left_type— A pipeline only composes when every step shares one error type.