Skip to content

Formatting is not idempotent: await (call(lambda).chain(...)) statements keep changing on the second pass #281

Description

@minami110

Impact

Running the formatter twice on the same file produces two different outputs for statements of the shape await (call(func() -> void: body()).chain("long argument")) when the line exceeds max-line-length. The first pass only expands the lambda; the second pass additionally wraps the outer parenthesized group and re-indents the lambda body one level deeper. Output converges from the second pass on.

Minimal repro

repro.gd (the statement line exceeds the default limit):

extends Node


func f() -> void:
	await (check(func() -> void: do_work()).report("some quite long message that pushes this line well past the limit"))
gdscript-formatter repro.gd
gdscript-formatter --check repro.gd   # exit 1 — the file it just wrote is "not formatted"
gdscript-formatter repro.gd           # changes the file again

Actual output

After pass 1:

func f() -> void:
	await (check(func() -> void:
			do_work()).report("some quite long message that pushes this line well past the limit"))

After pass 2 (differs from pass 1 — outer group and call arguments now wrapped, lambda body indented one level deeper):

func f() -> void:
	await (
		check(func() -> void:
				do_work()).report(
			"some quite long message that pushes this line well past the limit"
		)
	)

Pass 3 output equals pass 2 (converged).

Expected

Pass 1 should directly produce the fixpoint (whatever shape that is) — format followed by --check on the same file should always exit 0.

Boundary (verified individually on 0.22.0)

Idempotent:

  • The same statement without the outer parentheses, or short enough to fit the limit
  • call(func() -> void: body()).chain("...") without await (...) wrapping (converges in one pass)

Not idempotent (two passes needed):

  • Overlong await ( call(<single-line lambda>).chain(...) ) statements — the single-line lambda's expansion in pass 1 seems to happen after the wrapping decisions, so the extra wrapping that the now-taller statement needs is only applied on the next run

Metadata

Metadata

Assignees

Labels

No labels
No labels

Type

Fields

No fields configured for Bug.

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions