Skip to content

Invalid code removes entire function #233

@nojaf

Description

@nojaf

Problem

When formatting a file that contains invalid GDScript syntax, the formatter can silently delete entire function definitions instead of leaving the file untouched.

Reproducer

func jump(force: float) -> void:
    self.velocity.y = force
    is_shooting = false


func shoot() -> void:
    var pos: Vector2 = muzzle_right.global_position
    var direction: Vector2 = Vector2.RIGHT
    if is_facing_left:
        pos = muzzle_left.global_position
        direction = Vector2.LEFT

    var lazer: Projectile = Instancer.instance_scene_to_level(Instancer.laster_scene, pos) as Projectile
    if lazer != null:
        lazer.launch(direction, lazer_speed)

    is_shooting = false
    var yozora: bool. 

Run with --reorder-code --use-spaces. After formatting, the entire shoot function disappears from the output.

Root cause

The bug is a three-step failure chain:

1. Tree-sitter error recovery is too aggressive.
The last line var yozora: bool. is invalid — the . after the type is unexpected. When tree-sitter tries to recover, it backtracks far enough to wrap the entire shoot() function in an ERROR node, not just the bad line:

(source
  (function_definition ...)   ← jump() parses correctly
  (ERROR
    (name)                    ← "shoot"
    (parameters)
    return_type: (type ...)
    (variable_statement ...)
    (if_statement ...)
    ...                       ← entire body of shoot()
  )
)

2. The tree-sitter query silently skips ERROR nodes.
The reorder module collects top-level tokens using this query (reorder.rs):

(source (_) @element)

In tree-sitter, the _ wildcard matches any named node but explicitly excludes ERROR nodes. So jump() is captured, and the ERROR node containing shoot() is invisibly dropped.

3. The reorder pass outputs only what it collected.
build_reordered_code rebuilds the file from the token list. Because shoot() was never added to that list, it simply isn't written to the output.

Expected behavior

If the file cannot be fully parsed (i.e. tree.root_node().has_error() is true), the formatter should leave the file completely untouched — or at minimum skip the reorder step — rather than silently losing declarations.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions