A Source Transformer Should Be Allowed to Say No

The most reassuring response from a source-editing tool is not always a successful edit.

Sometimes it is: “There is no safe value at this cursor position.”

That sounds like a disappointing feature. A keyboard shortcut was pressed; surely the extension should do something. But automatic edits have an awkward property: a confident wrong insertion can compile, run, and change the program being debugged.

Consider a cursor on prefs here:

await prefs.reload();

A logging tool could widen the selection to prefs.reload() and print it. That would execute reload() again. The log would no longer observe the program; it would participate in it.

The safer product direction is not “support every cursor.” It is recognize value positions, preserve statement boundaries, and refuse the rest.

Selection is a semantic decision

Text editors give extensions offsets, selections, and document text. Those primitives make it tempting to solve the problem with nearby characters: scan left, scan right, stop at punctuation, insert a line.

Source code is less cooperative.

A dot may belong to a member chain. A comma may separate arguments, a map entry, or collection elements. A line may contain part of a multiline statement. A token under the cursor may be a declaration keyword rather than a runtime value.

The tool therefore needs a narrow question:

Can this position be mapped to an expression that is safe to evaluate exactly once in a log?

“Exactly once” is the important part. Selecting the receiver prefs may be safe. Selecting the invocation prefs.reload() is not, because logging it repeats behavior.

This is where refusing an edit becomes a feature. The tool has detected that convenience would violate its observation contract.

Line boundaries are not statement boundaries

Even after selecting a safe expression, insertion is not “add text after the current line.” Dart statements can span several lines, and one line can participate in a larger expression.

The insertion point belongs after the containing statement, not after whichever line happens to contain the cursor.

final result = await client.fetch(
  endpoint,
  headers: headers,
);

A log inserted after endpoint, would split the invocation. A log inserted after the semicolon observes the completed assignment.

That difference forces a useful architecture: expression targeting and statement placement are separate decisions. One identifies what may be observed; the other identifies where a new statement may exist without changing syntax or evaluation order.

When those responsibilities are separate, each can be tested with plain source strings. The extension host does not need to launch for every edge case, which is fortunate because source transformers manufacture edge cases for sport. (They get bored otherwise.)

Generated code must survive the formatter

A source transformer can find the right value and still produce a poor edit through quoting, interpolation, or line length.

The generated label may contain quotes. The expression may sit inside a string interpolation. The log function itself may be configurable. A long statement may exceed the formatter’s page width.

A robust source-editing pipeline can use this sequence:

validate position
→ select a non-evaluating value
→ find the containing statement
→ generate escaped log text
→ insert
→ format

Formatting comes last because it should normalize a semantically valid edit, not rescue a malformed one.

Turbo Flutter Log currently takes a narrower approach. It reads the configured page width and generates formatter-stable text, but it does not run the formatter after insertion. Either design must prove that formatting will not expose a malformed edit.

Tests should follow the same contract. Include reserved words, declaration recovery, method chains, named arguments, map entries, nested brackets, multiline statements, quote escaping, and ambiguous positions that must be rejected. The negative cases matter as much as the successful ones.

Refusal keeps automation small

There is pressure in developer tools to turn every limitation into another heuristic. If a cursor is ambiguous, scan farther. If the expression might have side effects, guess the receiver. If parsing fails, insert at the next newline.

Each fallback increases apparent coverage while weakening the promise.

A narrow transformer can make a stronger claim: when it edits, the instrumentation will not alter control flow, evaluation order, or evaluation count. When it cannot establish that claim, it leaves the file untouched and tells the developer why.

That is not less helpful. It converts an invisible semantic risk into a visible manual decision.

I built this boundary into Turbo Flutter Log. If you maintain an editor extension, try writing the refusal tests before adding the next selection heuristic. The shortest path to a trustworthy automatic edit may be one more deliberate “no.”