Repository: carthage-software/mago (Rust)

Issue:
Mago format removes closing part of echo tag
### 🐞 Describe the Bug

Mago format incorrectly removes the closing part of an echo tag, which creates invalid PHP syntax. For example, `<?= 'hello' ?>` is re-formatted to `<?= 'hello'`

### 🔄 Steps to Reproduce

1. Run `mago format`
2. The command succeeds, but mis-formats files ending with echo tags
3. Mago format should produce valid PHP code

### ⚙️ Configuration (mago.toml)

```toml
[source]
paths = ["src", "test"]
```

### 📜 Command Output

```shell
$ MAGO_LOG=trace vendor/bin/mago format test.php
DEBUG Configuration specifies 14 threads.
DEBUG Configuration specifies a stack size of 37748736 bytes.
 INFO Formatted 1 source files successfully.
```

### 📂 PHP Code Sample (If Applicable)

```php
<?= 'hello' ?>
```

### 🖥️ Operating System

macOS

### 📦 How did you install Mago?

Composer (`composer require carthage-software/mago`)

### 📝 Additional Context

_No response_


Discussion:

Patch:
diff --git a/crates/formatter/src/internal/format/mod.rs b/crates/formatter/src/internal/format/mod.rs
index 872227b9e..45bf3c90c 100644
--- a/crates/formatter/src/internal/format/mod.rs
+++ b/crates/formatter/src/internal/format/mod.rs
@@ -96,8 +96,15 @@ impl<'a> Format<'a> for Statement {
     fn format(&'a self, f: &mut FormatterState<'a>) -> Document<'a> {
         wrap!(f, self, Statement, {
             match self {
-                Statement::OpeningTag(t) => t.format(f),
-                Statement::ClosingTag(t) => t.format(f),
+                Statement::OpeningTag(t) => {
+                    f.in_echo_tag = matches!(t, OpeningTag::Echo(_));
+                    t.format(f)
+                }
+                Statement::ClosingTag(t) => {
+                    let tag = t.format(f);
+                    f.in_echo_tag = false;
+                    tag
+                }
                 Statement::Inline(i) => i.format(f),
                 Statement::Namespace(n) => n.format(f),
                 Statement::Use(u) => u.format(f),
@@ -171,6 +178,7 @@ impl<'a> Format<'a> for ClosingTag {
 
         wrap!(f, self, ClosingTag, {
             if f.settings.remove_trailing_close_tag
+                && !f.in_echo_tag
                 && f.skip_spaces_and_new_lines(Some(self.span.end.offset), false).is_none()
             {
                 f.scripting_mode = true;
diff --git a/crates/formatter/src/internal/mod.rs b/crates/formatter/src/internal/mod.rs
index 1bd34745a..5851ee885 100644
--- a/crates/formatter/src/internal/mod.rs
+++ b/crates/formatter/src/internal/mod.rs
@@ -45,6 +45,7 @@ pub struct FormatterState<'a> {
     id_builder: GroupIdentifierBuilder,
     argument_state: ArgumentState,
     parameter_state: ParameterState,
+    in_echo_tag: bool,
     in_pipe_chain_arrow_segment: bool,
     in_condition: bool,
     halted_compilation: bool,
@@ -69,6 +70,7 @@ impl<'a> FormatterState<'a> {
             id_builder: GroupIdentifierBuilder::new(),
             argument_state: ArgumentState::default(),
             parameter_state: ParameterState::default(),
+            in_echo_tag: false,
             in_pipe_chain_arrow_segment: false,
             in_condition: false,
             halted_compilation: false,


Test patch:
diff --git a/crates/formatter/tests/cases/closing_echo_tag_preserved/after.php b/crates/formatter/tests/cases/closing_echo_tag_preserved/after.php
new file mode 100644
index 000000000..e859d069c
--- /dev/null
+++ b/crates/formatter/tests/cases/closing_echo_tag_preserved/after.php
@@ -0,0 +1,1 @@
+<?= 'Hello, world!' ?>
diff --git a/crates/formatter/tests/cases/closing_echo_tag_preserved/before.php b/crates/formatter/tests/cases/closing_echo_tag_preserved/before.php
new file mode 100644
index 000000000..e859d069c
--- /dev/null
+++ b/crates/formatter/tests/cases/closing_echo_tag_preserved/before.php
@@ -0,0 +1,1 @@
+<?= 'Hello, world!' ?>
diff --git a/crates/formatter/tests/cases/closing_echo_tag_preserved/settings.inc b/crates/formatter/tests/cases/closing_echo_tag_preserved/settings.inc
new file mode 100644
index 000000000..5ae62fbda
--- /dev/null
+++ b/crates/formatter/tests/cases/closing_echo_tag_preserved/settings.inc
@@ -0,0 +1,1 @@
+FormatSettings { remove_trailing_close_tag: true, ..Default::default() }
diff --git a/crates/formatter/tests/mod.rs b/crates/formatter/tests/mod.rs
index eb0bde316..eee6a628a 100644
--- a/crates/formatter/tests/mod.rs
+++ b/crates/formatter/tests/mod.rs
@@ -76,6 +76,7 @@ test_case!(inline_echo);
 test_case!(parameter_attributes);
 test_case!(space_before_enum_backing_type_colon);
 test_case!(no_space_before_enum_backing_type_colon);
+test_case!(closing_echo_tag_preserved);
 test_case!(closing_tag_removed);
 test_case!(closing_tag_preserved);
 test_case!(single_quote_string);