Skip to content

fix(scanner): clear heredoc stack in deserialize - #304

Merged
calebdw merged 7 commits into
tree-sitter:masterfrom
NathanEvryg:fix/heredoc-stack-deserialize
Sep 24, 2026
Merged

calebdw merged 7 commits into
tree-sitter:masterfrom
NathanEvryg:fix/heredoc-stack-deserialize

Conversation

@NathanEvryg

Copy link
Copy Markdown

A file with many sequential heredocs stops parsing at some point: the closing identifier is lexed as string_content, the rest of the file ends up inside the heredoc body and the root becomes ERROR. php -l accepts the file.

Cause: deserialize in common/scanner.h resets every entry of scanner->heredocs but never shrinks the array. Each re-lex of heredoc_start leaves one empty entry behind, and serialize writes all of them, 5 bytes each. Once the 1024-byte serialization buffer would overflow, serialize returns 0 and the open heredoc's identifier is lost.

Where it fails depends on the tag length. A stale entry costs 5 bytes, the live one 5 + 4 * N for a tag of N characters (each character is stored on 4 bytes), plus 1 byte for the entry count. So the n-th heredoc of a file fails as soon as its tag reaches ceil((1018 - 5 * (n - 1)) / 4) characters: a 3-character tag fails from the 203rd heredoc, a 240-character tag from the 13th.

The test uses a long tag to keep the corpus short, but the file still grows a lot; I can move it to a separate heredoc.txt next to nowdoc.txt if you prefer. It is in its own commit, so you can check it out and see it fail before the fix. With the fix, tree-sitter test passes for both grammars.

@calebdw

calebdw commented Sep 24, 2026

Copy link
Copy Markdown
Collaborator

Thanks! I'll take it from here 👍

`deserialize` reset every entry of `heredocs` but kept the array at
its previous size, so leftover scanner-state entries were serialized
again, 5 bytes each. One stale entry accumulated per heredoc. Once
the 1024-byte serialization buffer filled up, `serialize` returned 0,
the open heredoc's identifier was lost, and its closer could no
longer match.

Clear the array after resetting its entries. The reuse branch in the
fill loop is then dead and is removed.

Cover the failure with generated binding tests: 20 long-tag
heredocs (the original overflow) and 250 short-tag ones.
Widen the Node peer range to tree-sitter ^0.25.0 so the package
installs alongside 0.25.x. Bump tree-sitter-cli to 0.27 to match
CI, refresh Cargo/PyPI versions, and ignore Python bytecode.

Closes tree-sitter#301
`use \Foo\{Bar}` is valid PHP and already works for non-group
forms. `_namespace_use_group` required a bare namespace_name, so
the leading separator became an ERROR that swallowed the next
statement.

Closes tree-sitter#303
@calebdw
calebdw force-pushed the fix/heredoc-stack-deserialize branch from c920cca to 54f3194 Compare September 24, 2026 19:55
PHP allows continuing the expression after a heredoc/nowdoc
closer, e.g. SQL.PHP_EOL. The body scanners only stopped for
whitespace, ';', ',', or ')', so a following '.' was consumed as
string content and the rest of the file was swallowed.

Closes the Laravel DatabaseSqliteSchemaStateTest.php example
parse failure.
Laravel renamed Foundation/fixtures to Fixtures, so the known
bad-syntax fixture was no longer excluded. Also skip PHPUnit's
intentional parse-error fixture.
@calebdw
calebdw force-pushed the fix/heredoc-stack-deserialize branch from 54f3194 to 9e651d6 Compare September 24, 2026 20:07
@calebdw
calebdw force-pushed the fix/heredoc-stack-deserialize branch from de485b9 to b78c60c Compare September 24, 2026 20:26
Peer tree-sitter is now ^0.25.0, which cannot install with 0.22.x.
That is a breaking change for Node consumers, so this is a minor
bump rather than 0.24.3. ABI remains 15.
@calebdw
calebdw force-pushed the fix/heredoc-stack-deserialize branch from b78c60c to 12bf00f Compare September 24, 2026 20:29
@calebdw
calebdw merged commit 92b5271 into tree-sitter:master Sep 24, 2026
6 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants