feat: add PHP language support - #64
Conversation
|
Tested this branch against a large Laravel 11 codebase (2145 indexed files, 1353 of them PHP). The PHP support itself works well — sharing both the positive results and one reproducible defect. What worksBuilt at commit 11 seconds for the full build. Coverage of Call-edge accuracy — I diffed Zero misses, zero false edges on that symbol. Signatures parse correctly too, including promoted constructor properties, Ambiguity handling is appropriately conservative.
That's the right call, and the message says so plainly. Defect:
|
phpClosureName decided whether a closure was an assignment's right-hand
side with `parent.childForFieldName("right") === node`. tree-sitter's
binding does not guarantee that two traversals to the same underlying
node return the same JS wrapper, so `===` could be false for the same
node — collapsing a variable-assigned closure to the anonymous
`{closure}` name on some passes but not others.
The stored graph then held `…#itemGroupCallback` while a later
recomputation produced `…#{closure}`, so `graft check` reported the
graph STALE (added/removed pair) immediately after a clean build, with
the affected file set varying between runs — making `check` unusable as
a CI freshness gate on closure-heavy PHP.
Compare tree-sitter node `.id` (stable per node within a tree) instead
of wrapper identity, in both extract.ts and the duplicated copy in
bindings.ts so the two scope stacks stay in lockstep.
Adds regression tests: variable-assigned (incl. `static`) closures keep
their variable name, closure-node ids are stable across repeated
extraction, and `graft check` is OK immediately after a build.
Reported by @CarlLee1983 in NanoNets#64.
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
7de0bcd to
980d76e
Compare
|
@CarlLee1983 thank you so much for this — a genuinely excellent report. The reproduction, the file-by-file I've pushed a fix taking your suggestion: You were right that reference identity was the fragile part. I couldn't force the non-deterministic path in a small isolated fixture (there
Full suite is green. Offer to run further probes very much appreciated — if |
|
✅👌 please update |
phpClosureName decided whether a closure was an assignment's right-hand
side with `parent.childForFieldName("right") === node`. tree-sitter's
binding does not guarantee that two traversals to the same underlying
node return the same JS wrapper, so `===` could be false for the same
node — collapsing a variable-assigned closure to the anonymous
`{closure}` name on some passes but not others.
The stored graph then held `…#itemGroupCallback` while a later
recomputation produced `…#{closure}`, so `graft check` reported the
graph STALE (added/removed pair) immediately after a clean build, with
the affected file set varying between runs — making `check` unusable as
a CI freshness gate on closure-heavy PHP.
Compare tree-sitter node `.id` (stable per node within a tree) instead
of wrapper identity, in both extract.ts and the duplicated copy in
bindings.ts so the two scope stacks stay in lockstep.
Adds regression tests: variable-assigned (incl. `static`) closures keep
their variable name, closure-node ids are stable across repeated
extraction, and `graft check` is OK immediately after a build.
Reported by @CarlLee1983 in NanoNets#64.
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
980d76e to
1aacb5a
Compare
Adds a tree-sitter-php grammar and first-class PHP tier-1 extraction: definitions (classes, methods, interfaces, traits, enums, functions), the four PHP call shapes, extends/implements heritage, use-imports, trait composition, typed-parameter receiver binding, and closures-as-nodes. Closes NanoNets#63. Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
phpClosureName decided whether a closure was an assignment's right-hand
side with `parent.childForFieldName("right") === node`. tree-sitter's
binding does not guarantee that two traversals to the same underlying
node return the same JS wrapper, so `===` could be false for the same
node — collapsing a variable-assigned closure to the anonymous
`{closure}` name on some passes but not others.
The stored graph then held `…#itemGroupCallback` while a later
recomputation produced `…#{closure}`, so `graft check` reported the
graph STALE (added/removed pair) immediately after a clean build, with
the affected file set varying between runs — making `check` unusable as
a CI freshness gate on closure-heavy PHP.
Compare tree-sitter node `.id` (stable per node within a tree) instead
of wrapper identity, in both extract.ts and the duplicated copy in
bindings.ts so the two scope stacks stay in lockstep.
Adds regression tests: variable-assigned (incl. `static`) closures keep
their variable name, closure-node ids are stable across repeated
extraction, and `graft check` is OK immediately after a build.
Reported by CarlLee1983 in NanoNets#64.
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1aacb5a to
4c37c54
Compare
Add PHP language support
Closes #63.
Problem
Graft ships tree-sitter grammars for Go, Python, and TypeScript/JS, but not
PHP. On a PHP project,
graft buildindexes 0.phpfiles and reports only theother languages;
--extensions .phpparses nothing because no PHP grammar isregistered. The entire PHP codebase is invisible to the graph.
What this adds
PHP as a first-class Tier-1 language, following the existing per-language
pattern (
extract.tsfor the walk,bindings.tsfor receiver types), with asmall resolver tweak and a project marker:
tree-sitter-php(.php-> the tag-awarePHP.phpgrammar),registered in
EXTENSIONS,GRAMMARS,KINDS_BY_LANG.a PHP-only
traitKind(mirrors the existing Go-onlystruct).phpExported()— public unless avisibility_modifiermarksit private/protected; top-level defs are always visible.
isCallNode()handles PHP's four call shapes(function / member / nullsafe-member / scoped).
$this/self/static/parentresolve to the enclosing class; a staticFoo::bar()resolves totype
Foo.extends(base clause) andimplements(interface clause),names de-qualified for name-based resolution.
use SomeTrait;inside a class body emits animplementsedge to the trait;implementsresolution now also accepts atraittarget (resolve.ts).bindings.ts): type-hinted parameters(
function f(Foo $x)) and$x = new Foo()bind$xtoFoo, so$x->method()resolves to the right class instead of by bare method name.anonymous_function/arrow_functionbecome functionnodes — named after the variable they're assigned to (
$h = fn(...)->h,like TS arrow-consts), else an anonymous
{closure}. This keeps aclosure-only file (a routing table, a DI container) structured, so the calls
inside a callback attribute to the callback rather than vanishing into the
file node.
importsedge peruseclause (like Go package paths, thesestay unresolved-to-file by design).
composer.jsonadded to project-rootMARKERS.Every PHP tree-sitter node type/field used here was confirmed against a real
tree-sitter-phpAST before implementation.Validation
Beyond the unit tests, this was dogfooded on three real-world PHP codebases —
4,479 source files parsed in total, with zero parse failures and no tree-sitter
ERROR nodes:
symbol-free (config-array returns, bootstrap/entry scripts, DI definitions).
58,167 edges; class heritage,
$this->/static calls, typed-parameter membercalls, and trait composition all resolve across files.
0 symbols now yields 33 closure nodes, and on the largest codebase
1,556 resolved calls are now owned by closure nodes that were previously
attributed to the file (or dropped).
Tests
test/graph-php.test.ts(mirrorstest/graph-go.test.ts) — four tests coveringnode kinds + visibility,
extends/implements/$this->/Cls::edges, traitcomposition, typed-parameter binding, and closures-as-nodes.
test/graph-languages.test.tsgets the.phpextension.npm testis green(pre-existing unrelated failures aside);
tsc --noEmitclean.Limitations (deliberate, documented in code)
#[...]are not emitted. Unlike TS/Python decorators(which are call expressions and so leave
callsedges incidentally), a PHPattribute is metadata, not a call — a natural follow-up is a
referencesedgeto the attribute class.
.blade.php) templates are out of scope.$var->m()with no type hint /newstill resolves by method nameonly (no full data-flow analysis) — same conservative stance as the other
languages.
Files changed
package.json,src/graph/extract.ts,src/graph/bindings.ts,src/graph/resolve.ts,src/graph/scopes.ts,src/graph/types.ts,test/graph-php.test.ts,test/graph-languages.test.ts.Applying