Skip to content

v0.2.0

Latest

Choose a tag to compare

@github-actions github-actions released this 31 Aug 10:39
· 8 commits to main since this release
v0.2.0
7561a11

Coding agents no longer write PHP directly. They describe syntax; php-ast-edit parses it, mutates the AST, and only the AST produces a .php file — new files, empty containers and deletions included.

The engine

0.1.0 could mutate the constructs it had modelled by hand, which left holes of a predictable shape: adding the first method to an empty class, the first statement to an empty body, or the first parameter to an empty signature was not expressible at all. 0.2.0 replaces that with a small complete algebra — replace_node, delete_node, insert_into, replace_child, delete_child, move_node — and keeps the typed operations as shorthands over it. insert_into addresses a container by node, property and position, so it needs no existing sibling as an anchor.

Snippets are parsed inside a synthetic host construct, so the grammar comes from nikic/php-parser rather than being modelled a second time here: seventeen contexts from expr and member to match_arm and static_var. New PHP syntax arrives with the parser.

inspect now returns a structural reference — stmts[1].stmts[0].params[0] — alongside the byte coordinates, and the node's slots. A file entry carries "mode": "edit", "create" or "delete".

Transactions became real: every file is read, guarded, resolved, mutated, printed and re-parsed before the first byte is written, each is re-compared against its snapshot immediately before that write, and a failure during the write phase rolls back. In 0.1.0 a failure in the third file could leave the first two changed.

The formatting contract

An AST write reprints the file from the tree, so a repository whose code is not already written that way pays for every edit. Measured on a 63-file TYPO3 extension: a one-identifier rename changed 105 lines where the repository was not set up, and 2 where it was — and running that project's own formatter on both sides did not close the gap.

Line breaking turned out to be nobody's job: of php-cs-fixer's 303 fixers not one has a concept of line width, and PHP_CodeSniffer's Generic.Files.LineLength only reports. So the printer does it. At a budget of 80 the same extension printed at 318 characters against its authors' 294, where the unmodified printer reached 1745.

normalize writes .php-ast-edit.json; apply reads it. Whether a file may be rewritten canonically cannot be measured from the file — the fixed point belongs to the printer and the project's formatter together, and the formatter runs last — so it is declared, never inferred. Without that declaration apply prints format-preserving and returns a NOT_CANONICAL warning. doctor reports whether a repository can hold the contract at all.

hooks/php-ast-only.py denies text mutation of .php before the write, rather than asking for it in prose.

Fixed

Writing through a symlink replaced the link with a regular file instead of updating its target — present since 0.1.0, so an edit to a symlinked file landed where nobody reads. Also: two spellings of one path silently discarded each other's edits, a sub node name was resolved without regard to the node holding it, three raw TypeErrors replaced by typed refusals, move_node re-attaching a node below itself, snippets rejected for containing <? inside a string literal, remove_doc_comment deleting every comment on the node, and inspect failing outright on files with umlauts near its excerpt boundary.

Verifying

tests/corpus.php round-trips php-parser's own 270 source files and requires the AST back identical, comments compared as a set — which is how two genuine comment losses surfaced, both upstream behaviour and both now recorded. Alongside it: 61 grammar and failure-mode cases, 53 formatting checks, 41 enforcement-gate cases, a CLI surface test and a catalog-drift gate.

Thanks to @CybotTM for the reviews on #5, #6 and #7.

Full Changelog: v0.1.0...v0.2.0