feat(kernel): R7b PHP walker — php module, tree-sitter-php 0.24.2 bump, php default-routed (#1380)
Fourth and final R7b batch-2 port, checklist-first recipe (docs/design/php-kernel-port-checklist.md). Grammar bump first, validated standalone with the diff ENUMERATED + CLASSIFIED (unlike rust/ruby the php bump is NOT graph-neutral): tree-sitter-php ^0.22 (tree-sitter-wasms, 2023) → v0.24.2, the full HTML-interleaving `php` variant (the walker calls LANGUAGE_PHP, never PHP_ONLY) — crate pinned =0.24.2, wasm built from tag 5b5627f's checked-in php/src/parser.c + scanner.c + shared common/scanner.h (all sha-matched against the crates.io tarball, ABI 14→15). Old-vs-new full-init diffs decompose completely into: (1) the anonymous_class wrapper shape (anon-class nodes/methods re-shape — 2,532 rows), (2) grouped nested-clause skip (absent in the gate repos, fixture-pinned), (3) 32 formerly-erroring files parsing clean (monolog Level.php, symfony Request/Response with 8.4 property hooks), (4) a survey-missed category found at gate time: the 8.4 parenthesis-free `new X()->m()` chaining misparse fix (86 garbage instantiates refs disappear, precision-positive), plus resolution RIPPLE proven mechanically (every remaining ref-table flip pairs 1:1 with a resolved edge on the opposite side; node rows byte-stable outside 1/3/4). Walker (java.rs chassis + the php specifics) preserves bug-for-bug: the visitNode hook (const_declaration at ANY scope → bare `constant` nodes, values never walked; trait-use → implements refs WITH filePath via the ruby port's REF_FLAG_FILE_PATH wire slot), FIRST-namespace whole-file scoping (braced namespaces scope nothing; namespaced files DROP top-level const value-ref targets), the import trio (single/aliased/grouped incl. the nested-clause skip, include/require static-literal-only, `Foo\Bar::Baz` use refs), the call-encoding zoo (DOT-joined scoped calls, `this->prop.m` #1251 encoding, `Cls::factory().m` fluent with inner args dropped, nullsafe `?->` emitting nothing, unsuppressed literal receivers), interface multi-extends first-base-only drop, anon-class methods as file-level functions (top) or vanishing (in-body), property type-hints emitting no field refs, the final-modifier-as-type signature quirk, HOF-gated string callables (skipGate) + array callables, and the `name`-node value-ref reader. Gates: sweeps 0-diff monolog 217/217, laravel-framework 3007/3008, symfony 10726/10737 (13,950 files byte-parity; 12 deferrals = exactly the predicted genuinely-broken fixtures, ≈0–0.1%); full-init dumps byte-identical ×3 (16.1k/354.2k/702.8k lines); kernel-php-parity suite (torture + drupal .module + leading-HTML fixtures, CRLF variants, wire-flag pin, defer) + php grammar-parity row; full suite 2,622 green ×2 under CODEGRAPH_KERNEL_EXPECT=1. DEFAULT_ROUTED += php (13 languages). Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5
parent
1909931238
commit
a6c62d77df
@@ -0,0 +1,978 @@
|
||||
# PHP kernel port (R7b) — the bug-for-bug checklist
|
||||
|
||||
**Status: PORT COMPLETE (2026-07-20)** — walker `codegraph-kernel/src/php.rs`,
|
||||
all gates passed (grammar bump validated standalone with the diff enumerated +
|
||||
classified — see §Grammar-bump deltas incl. the bump-gate-found category 4 and
|
||||
the ripple-proof note; parity sweeps 0-diff monolog 217/217 /
|
||||
laravel-framework 3007/3008 / symfony 10726/10737 with only the predicted
|
||||
broken-fixture deferrals; full-init dump gates byte-identical ×3;
|
||||
kernel-php-parity suite; DEFAULT_ROUTED += php — 13 languages). Trait-use
|
||||
implements refs carry filePath via the v2 REF_FLAG_FILE_PATH wire slot
|
||||
(shipped with the ruby port). Survey basis:
|
||||
every TS-side branch a php-routed file exercises, with file:line anchors as of
|
||||
`f1ca991` (HEAD at survey time, clean main). Every grammar-shape claim below
|
||||
was **probed against both the current production wasm (tree-sitter-wasms
|
||||
0.1.13 build of tree-sitter-php ^0.22, ABI 14) and a fresh v0.24.2 build**
|
||||
(probe scripts + dumps in the session scratchpad `svy-php/` — see §Probe
|
||||
artifacts), not assumed. Read WITH `docs/design/rust-kernel-migration-plan.md`
|
||||
(§0a recipe, §2 boundary, §5 gates) and the two format precedents
|
||||
(`rust-lang-kernel-port-checklist.md`, `ccpp-kernel-port-checklist.md`).
|
||||
|
||||
**Blocking findings: none.** Two eyes-open notes, neither blocking: (1) the
|
||||
grammar bump is **NOT graph-neutral** — unlike rust, the old→new wasm diff has
|
||||
three known behavior-changing deltas (anonymous classes, one grouped-import
|
||||
clause shape, enum-const files parsing clean), so the bump's standalone gate is
|
||||
"enumerate + classify the diff", not "expect zero" (§Grammar-bump deltas); (2)
|
||||
Laravel/Drupal-detected repos force the decoded path via framework `extract()`
|
||||
hooks, but none of the three gate repos triggers detection, so raw-path sweeps
|
||||
are representative (§Architecture decisions #2).
|
||||
|
||||
## Grammar prep (NOT staged — land FIRST, before any walker exists)
|
||||
|
||||
php is **not** in `VENDORED_WASM_LANGS` (grammars.ts:291) — production loads
|
||||
`require.resolve('tree-sitter-wasms/out/tree-sitter-php.wasm')`
|
||||
(grammars.ts:307-312; mapping `php: 'tree-sitter-php.wasm'` at grammars.ts:32),
|
||||
a 2023-era **ABI-14** build of npm tree-sitter-php ^0.22 (sha256 `55bb617b…`,
|
||||
812,594 bytes).
|
||||
|
||||
- **Variant: the full `php` grammar, NOT `php_only`.** Probed: the current
|
||||
wasm parses a mixed HTML+PHP file with root-level `text` / `php_tag` /
|
||||
`text_interpolation` nodes and no errors — that is the `php/` grammar of the
|
||||
two-grammar repo. The bump MUST keep this variant and the kernel walker MUST
|
||||
call **`tree_sitter_php::LANGUAGE_PHP`** (the crate also exports
|
||||
`LANGUAGE_PHP_ONLY` — wrong one; a php_only build ERRORs on any leading HTML,
|
||||
which is a routine Drupal/legacy shape).
|
||||
- **Version: crate `tree-sitter-php` 0.24.2** (crates.io max_stable) = repo tag
|
||||
`v0.24.2` = commit `5b5627faaa290d89eb3d01b9bf47c3bb9e797dea`
|
||||
("fix: publishing, 0.24.2"). sha256-matched tag ↔ crate tarball:
|
||||
- `php/src/parser.c` `59ad8e5e4fde3fe60687a488ab8420612840cc966b83739af1b3a4317ed27ec6`
|
||||
- `php/src/scanner.c` `58c92cafe4ebda509c3ad3864fa6fc0e9877bbac26e17a03d23ea2101c291ad5`
|
||||
— a thin wrapper: **the real external scanner is the SHARED
|
||||
`common/scanner.h`** (`de8eb36bc8f517ab9f3eaf82e3825d7b3e11b62e9471545f4c906100cfce0e07`),
|
||||
`#include`d by both variants. External scanner: YES (heredoc/nowdoc,
|
||||
encapsed strings, `?>`/text interleaving live there) — the crate build
|
||||
compiles it automatically; the wasm build picks it up from `src/`.
|
||||
- **Build (from the tag's CHECKED-IN parser.c — never `tree-sitter generate`):**
|
||||
```
|
||||
git clone --depth 1 --branch v0.24.2 https://github.com/tree-sitter/tree-sitter-php
|
||||
cd tree-sitter-php/php # the variant subdir — NOT the repo root
|
||||
npx tree-sitter-cli@0.25.10 build --wasm -o tree-sitter-php.wasm .
|
||||
```
|
||||
(brew emcc present; survey artifact: ABI 15, 1,058,082 bytes, sha256
|
||||
`6545a9a110bc878e26ed329950147e190c83da038bb17e999de646fe6c4d6c82`, left at
|
||||
scratchpad `svy-php/tree-sitter-php.wasm`.)
|
||||
- **Staging plan:** vendor to `src/extraction/wasm/tree-sitter-php.wasm`, add
|
||||
`'php'` to `VENDORED_WASM_LANGS` (grammars.ts:291), pin
|
||||
`tree-sitter-php = "=0.24.2"` in codegraph-kernel/Cargo.toml (crate + wasm
|
||||
move TOGETHER), add `'php'` to `GRAMMAR_LANGUAGES` in
|
||||
`__tests__/kernel-grammar-parity.test.ts:39` and to `grammar_for` in
|
||||
`codegraph-kernel/src/langs.rs` (+ the `LANGUAGES` const). MIT license, same
|
||||
family as the other vendored grammars. `copy-assets` already globs
|
||||
`src/extraction/wasm/*.wasm`.
|
||||
- **Bump lands FIRST with the full suite green and the old-vs-new full-init
|
||||
dump diff on the gate repos enumerated + classified** (see §Gates — for php
|
||||
this diff is expected NON-empty; every hunk must fall into a §Grammar-bump
|
||||
deltas category).
|
||||
|
||||
### Error incidence (probed, full php-routed file sets, 1 MiB skip applied)
|
||||
|
||||
| Repo | files | OLD (ABI-14 ^0.22) | NEW (v0.24.2) |
|
||||
|---|---|---|---|
|
||||
| monolog | 217 | 1 (0.46%) — `Level.php` (enum const) | **0 (0.00%)** |
|
||||
| laravel/framework | 2,999 | 3 (0.10%) | **1 (0.03%)** — a deliberately-broken test fixture |
|
||||
| symfony | 10,736 | 40 (0.37%) | **11 (0.10%)** — broken/8.4+ fixtures |
|
||||
|
||||
Both arms sit inside the ts/java/py/go norm (0–0.42%). **Deferral guard stays
|
||||
at the default `--max-deferral 0.1`** — the c/cpp 0.5 exemption does NOT apply;
|
||||
double-digit deferral on a php sweep means a broken walker. Old-grammar-only
|
||||
failures (fixed by the bump, probed construct-by-construct): `const` inside an
|
||||
enum body, property hooks (8.4), asymmetric visibility (8.4). Everything else
|
||||
(8.0–8.3: enums, readonly, promotion, DNF/intersection types, first-class
|
||||
callables, nullsafe, match, attributes, named args, typed class consts) parses
|
||||
clean on BOTH.
|
||||
|
||||
## Grammar-bump deltas (old → v0.24.2), every one classified
|
||||
|
||||
Full-tree diff on a clean-parsing torture file = 278 lines, all accounted for:
|
||||
|
||||
**Behavior-changing (the bump gate must show exactly these, nothing else):**
|
||||
|
||||
1. **Anonymous classes get a wrapper node.** OLD: `new class … { }` puts
|
||||
`base_clause`/`class_interface_clause`/`declaration_list` DIRECTLY under
|
||||
`object_creation_expression`; NEW nests them in an **`anonymous_class`**
|
||||
child (`body:` field on the list). Consequences (branches:
|
||||
`findAnonymousClassBody` tree-sitter.ts:4815 and `extractInstantiation`
|
||||
:4610):
|
||||
- OLD behavior: `declaration_list` is a direct child → anon-CLASS node
|
||||
`<T$anon@line>` + `extends` ref + method nodes (extractAnonymousClass
|
||||
:4837). NEW behavior (what the WALKER implements): `findAnonymousClassBody`
|
||||
finds nothing → **no anon class node, no extends ref**; the walker
|
||||
descends instead — at top level the inner `method_declaration`s hit the
|
||||
methodTypes branch, fail `isInsideClassLikeNode`, and extract as
|
||||
file-level **`function` nodes**; inside a body, `visitForCallsAndStructure`
|
||||
has no methodTypes branch, so anon-class methods **vanish** and their
|
||||
inner calls attribute to the enclosing function.
|
||||
- extractInstantiation's ctor (`namedChild(0)`, no field): OLD = the
|
||||
`base_clause` (ref text `extends B`) or `declaration_list`; NEW = the
|
||||
whole `anonymous_class` → className = the ENTIRE class text run through
|
||||
the `<`-strip + lastIndexOf('.'/'::') suffix logic (:4669-4686) —
|
||||
**garbage either way, differently-shaped garbage**. Reproduce the NEW
|
||||
shape exactly; pin both in the fixture.
|
||||
2. **Grouped-import nested clause drops.** OLD `use A\{Sub\Deep}` clause is
|
||||
`namespace_use_group_clause > namespace_name > name…` — the inline branch
|
||||
(tree-sitter.ts:3322-3347) finds `namespace_name` and emits import node +
|
||||
ref for `A\Sub` (wrong, but old behavior). NEW clause is
|
||||
`namespace_use_clause > qualified_name` — the branch's
|
||||
`find(c => c.type === 'name')` finds nothing → **that clause is silently
|
||||
skipped** (no import node, no ref). Simple (`Mailer`) and aliased
|
||||
(`Cache as CacheAlias` — children `name`, `as`, `alias: name`; the find
|
||||
returns the FIRST `name`, i.e. the source name) group members behave
|
||||
identically on both. The 3329 predicate already accepts both clause type
|
||||
names.
|
||||
3. **Old-grammar parse-error files now parse.** Enum-const files (monolog's
|
||||
`Level.php` class) go from mangled/error extraction to clean — node/edge
|
||||
diffs on such files are the bump working as intended.
|
||||
4. **(Found at bump-gate time, survey-missed.) PHP 8.4 parenthesis-free
|
||||
`new X()->m()` chaining misparse fix.** OLD parses the whole chain as ONE
|
||||
`object_creation_expression` with NO error flag (which is why the survey's
|
||||
error matrix missed it) → extractInstantiation emitted a garbage
|
||||
`X()->m`-shaped instantiates ref; NEW parses correctly as
|
||||
`member_call_expression(object_creation_expression(X), m)` → proper
|
||||
`instantiates X` + call refs. 86 such refs across symfony; probe:
|
||||
`probe-newchain.mjs`. Precision-positive, same nature as ruby's `&.!=`.
|
||||
|
||||
**Bump-gate ripple note (measured 2026-07-20):** beyond the four categories,
|
||||
the full-init dump diff carries RESOLUTION ripple — refs that flip between the
|
||||
parked unresolved_refs table and resolved edges because the graph gained
|
||||
symbols (category 3 recovering Request.php/Response.php re-resolves refs in
|
||||
hundreds of OTHER files). Ripple is provable mechanically: every side-only
|
||||
parked ref outside category-1/3/4 files pairs 1:1 with a resolved edge (same
|
||||
source/refName/line/col) on the opposite side, and node rows are byte-stable
|
||||
outside those files (ripple-proof.mjs — monolog 3/0 unpaired, framework 26/0,
|
||||
symfony 2,132/86-unpaired-all-category-4). Don't re-litigate ripple hunks
|
||||
per-file.
|
||||
|
||||
**Inert (verified against every consuming branch):**
|
||||
|
||||
- `qualified_name` internals: `namespace_name_as_prefix` wrapper →
|
||||
`prefix:`-fielded children. Every consumer reads `getNodeText` of the whole
|
||||
`qualified_name` or `find(type === 'name'/'namespace_name')` on OTHER nodes —
|
||||
no TS code references `namespace_name_as_prefix` (grepped). Text identical.
|
||||
- `namespace_use_clause` gains a `type:` field (`use function`/`use const` —
|
||||
the keyword moves inside the clause); the hook's `find('namespace_use_clause')`
|
||||
+ `find('qualified_name')` path is shape-independent. Same result.
|
||||
- Single aliased `use X as Y`: `namespace_aliasing_clause` → flat
|
||||
`as` + `alias: name`. Hook and emitPhpUseRefs read the `qualified_name`
|
||||
(source name) only. Same.
|
||||
- `property_element`: `variable_name` gains a `name:` field;
|
||||
`property_initializer` wrapper → `= (anon)` + `default_value:` field. The
|
||||
extractField php branch finds by TYPE (`variable_name`, then its `name`
|
||||
child) and the property_declaration-level type scan excludes only modifier +
|
||||
property_element types — direct children unchanged. Same.
|
||||
- `anonymous_function_creation_expression` → **`anonymous_function`**: NO TS
|
||||
code names either type (closures aren't extracted, see §Closures). Inert.
|
||||
- `primitive_type` becomes a leaf (anon keyword children like `void`,
|
||||
`mixed`, `iterable`, `false` dropped). All reads are node-type + text. Inert.
|
||||
- `text_interpolation`'s `?>` token → named `php_end_tag` child. No branch
|
||||
touches either; visitNode recursion over it is a no-op. Inert.
|
||||
- `namespace_definition` gains `name:` field on `namespace_name` — extractPackage
|
||||
finds by type. Inert.
|
||||
- `new static()`/`new self()`/`new parent()`: both grammars produce
|
||||
`object_creation_expression > name`; OLD prints an anon keyword child under
|
||||
`name`, NEW is a leaf — text identical (`static`/`self`/`parent`). Inert
|
||||
(the instantiates ref is that literal text — see §extractInstantiation).
|
||||
- `enum_case` gains `value:` field — never read (extractEnumMembers returns
|
||||
after the name-field path). Inert.
|
||||
- attributes `#[…]`: identical `attributes: attribute_list > attribute_group >
|
||||
attribute` shape on both; extraction ignores them entirely (§Attributes).
|
||||
|
||||
## Architecture decisions
|
||||
|
||||
1. **No preParse.** `phpExtractor` has no `preParse` hook (languages/php.ts —
|
||||
whole file, no such key), so `preParsedSource` (kernel/index.ts:82) is a
|
||||
no-op for php — both arms parse raw bytes. Nothing to hoist.
|
||||
2. **Laravel/Drupal repos take the DECODED path; the three gate repos do NOT.**
|
||||
`laravelResolver` (resolution/frameworks/laravel.ts:38, `languages:['php']`,
|
||||
detect = `artisan` file or `app/Http/Kernel.php` exists) and
|
||||
`drupalResolver` (drupal.ts:296, `languages:['php','yaml']`, detect =
|
||||
composer.json `drupal/*` deps/name/type, else `.info.yml` + drupal file)
|
||||
BOTH have `extract()` hooks, and parse-worker.ts:93-99 forces any language
|
||||
with an applicable framework `extract()` onto the decoded
|
||||
`extractFromSource` path. monolog / laravel-framework / symfony trip
|
||||
NEITHER detector (no `artisan`, no drupal composer manifest) → their php
|
||||
files ride the raw-buffers transport. Don't conclude the raw path is broken
|
||||
from a Laravel APP repo, and don't conclude framework hooks are dead from
|
||||
the gate repos.
|
||||
3. **Framework extractors themselves need NO port** (regex-over-raw-source TS,
|
||||
run in extractFromSource:6736-6758 after either arm) — but they pin parts of
|
||||
the walker's output contract (§Frameworks): drupal reconstructs extraction
|
||||
node IDs with `generateNodeId(filePath,'function',name,line)`.
|
||||
4. **One walker module** (suggest `codegraph-kernel/src/php.rs`), registered in
|
||||
`langs.rs` (`grammar_for` → `tree_sitter_php::LANGUAGE_PHP.into()`,
|
||||
`LANGUAGES` const += "php"); per-file `has_error()` → `defer:` like every
|
||||
walker. Skeleton mapping: **java.rs is the closest crib** (class-like scope
|
||||
stack, fields, enums, imports-with-hook, static-member refs, decorators
|
||||
no-op, value refs) — php adds the visitNode-hook branches, the
|
||||
package-namespace capture (java.rs has the same `extractFilePackage`
|
||||
mechanic), the php import trio, and the php type-ref walker; rustlang.rs is
|
||||
the crib for hook-suppressed import fallbacks and the `node_ids` dedupe
|
||||
pattern.
|
||||
5. **Extensions:** `.php`, and the Drupal set `.module`/`.install`/`.theme`/
|
||||
`.inc` all map to `php` at detectLanguage (grammars.ts:92-97) — no content
|
||||
sniffing, no dialect. Sweeps and fixtures must include a non-`.php`
|
||||
extension file. MAX_FILE_SIZE (1 MiB, extraction/index.ts:132) and
|
||||
generated-file skips are orchestrator/TS-side and shared.
|
||||
6. **No POST_PASSES entry** (kernel/index.ts:67 — none for php), so
|
||||
`tryKernelExtractRaw` stays eligible.
|
||||
|
||||
## Extractor config (languages/php.ts — 189 lines, read it whole)
|
||||
|
||||
Types: functionTypes=[`function_definition`];
|
||||
classTypes=[`class_declaration`, `trait_declaration`] with
|
||||
**classifyClassNode → 'trait' for trait_declaration** (php.ts:86) — a trait is
|
||||
kind `trait` via extractClass(node,'trait') (tree-sitter.ts:1014-1015);
|
||||
methodTypes=[`method_declaration`]; interfaceTypes=[`interface_declaration`]
|
||||
(kind `interface` — no interfaceKind override); structTypes=[];
|
||||
enumTypes=[`enum_declaration`]; enumMemberTypes=[`enum_case`];
|
||||
typeAliasTypes=[]; importTypes=[`namespace_use_declaration`,
|
||||
`include_expression`, `include_once_expression`, `require_expression`,
|
||||
`require_once_expression`]; callTypes=[`function_call_expression`,
|
||||
`member_call_expression`, `scoped_call_expression`] — **NOT
|
||||
`nullsafe_member_call_expression`** (see §extractCall);
|
||||
variableTypes=[`const_declaration`] (DEAD for dispatch — the visitNode hook
|
||||
consumes const_declaration first, see §visitNode hook);
|
||||
fieldTypes=[`property_declaration`]. nameField=`name`, bodyField=`body`,
|
||||
paramsField=`parameters`, returnField=`return_type`.
|
||||
|
||||
Hooks PRESENT (port each exactly):
|
||||
|
||||
- **getReturnType = extractPhpReturnType (php.ts:50)** — `return_type` field;
|
||||
`optional_type` unwraps to `namedChild(0) ?? rt`; then `primitive_type` →
|
||||
**undefined**. nameNode = `named_type` ? `namedChild(0) ?? rt` : rt; text =
|
||||
trim + strip leading `\`; empty → undefined; last = last `\`-segment;
|
||||
lowercase ∈ {self, static, this, $this} → the marker **`'self'`**
|
||||
(chained-call #608 resolves it to the declaring class); lowercase ∈
|
||||
PHP_NON_CLASS_RETURN (php.ts:37 — array string int integer float double bool
|
||||
boolean void mixed never null false true object callable iterable resource)
|
||||
→ undefined; must match `/^[A-Za-z_]\w*$/` else undefined (kills
|
||||
`A|B` unions — union_type is neither optional nor named_type, so nameNode =
|
||||
the union node, text = `A|B`, regex fails). PROBED shapes: `: self` and
|
||||
`: static` are **named_type > name** on v0.24.2 → marker `'self'` LIVE for
|
||||
both; `: void`/`: mixed`/`: string` are primitive_type → undefined;
|
||||
`: ?Foo` → optional_type > named_type → `Foo`; `: \App\Models\User` →
|
||||
qualified_name (not named_type) → nameNode = rt → text strips lead `\` →
|
||||
last segment `User`.
|
||||
- **classifyClassNode (php.ts:86)** — trait_declaration → 'trait', else 'class'.
|
||||
- **getVisibility (php.ts:89)** — scan ALL children (`child(i)`, anonymous
|
||||
included) for `visibility_modifier`; its text exactly
|
||||
`public`/`private`/`protected` → that; **no modifier → `'public'`** (php
|
||||
default). Called for functions, methods, classes, enums, structs(n/a),
|
||||
properties via extractField. Note `final_modifier`/`abstract_modifier`/
|
||||
`readonly_modifier` children are skipped by type.
|
||||
- **isStatic (php.ts:101)** — any child of type `static_modifier` → true, else
|
||||
false.
|
||||
- **visitNode hook (php.ts:108)** — see §visitNode hook. Fires for EVERY node
|
||||
visited by the main walker (tree-sitter.ts:943-953), NOT by
|
||||
visitFunctionBody's walker.
|
||||
- **packageTypes=[`namespace_definition`] + extractPackage (php.ts:149-156)** —
|
||||
see §Namespace capture.
|
||||
- **extractImport (php.ts:157-188)** — see §Imports.
|
||||
|
||||
Hooks ABSENT (the walker must NOT do these): `preParse`, `getSignature` (**php
|
||||
function/method nodes have NO signature — undefined**), `isAsync` (undefined,
|
||||
not false), `isConst`, `isExported` (undefined on every php node except the
|
||||
file node's literal `false`), `resolveName`, `recoverMangledName`,
|
||||
`isMisparsedFunction`, `resolveBody`, `getReceiverType` (**methods only via
|
||||
class-like scope; receiverType is always undefined** → no
|
||||
composeReceiverQualifiedName, no owner-contains fallback at
|
||||
tree-sitter.ts:1799), `classifyMethodNode`, `extractPropertyName`,
|
||||
`propertyTypes`, `extraClassNodeTypes`, `extractModifiers`,
|
||||
`synthesizeMembers`, `extractBareCall`, `skipBodilessClass` (**a bodiless
|
||||
`class_declaration` still mints a node** — doesn't occur in valid php),
|
||||
`methodsAreTopLevel`, `interfaceKind`.
|
||||
|
||||
## tree-sitter.ts branches (anchors as of `f1ca991`)
|
||||
|
||||
### visitNode dispatch — what each php node hits
|
||||
|
||||
| Node | Branch | Behavior |
|
||||
|---|---|---|
|
||||
| any node, first | visitNode hook, tree-sitter.ts:943-953 | php hook consumes `const_declaration` + `use_declaration` (§visitNode hook); on `true`: `scanFnRefSubtree(node,0)` then return (no descent) |
|
||||
| `text` / `php_tag` / `text_interpolation` (+ its `php_end_tag`) | no branch | recursed, nothing extracted. Positions of later nodes are absolute file coordinates — a file with leading HTML has its first symbol at the real (post-HTML) row |
|
||||
| `namespace_definition` | NOT dispatched in visitNode | consumed once by `extractFilePackage` (:1397, root's direct children scan) BEFORE the walk; the walk then recurses through it finding nothing (namespace_name/name have no branches). Braced form: extractPackage returns null (body check) → **no namespace node, contents index at file scope, bare QNs** — probed identical both grammars (`body: compound_statement`) |
|
||||
| `function_definition` (top level / inside namespace) | functionTypes:994 → extractFunction:1517 | never methodTypes (php methodTypes lacks function_definition) → always extractFunction at top level |
|
||||
| `class_declaration` | classTypes:1005 → classify → extractClass:1679 | kind `class`. `trait_declaration` → classify 'trait' → extractClass(node,'trait'):1015 → kind `trait` |
|
||||
| `interface_declaration` | interfaceTypes:1054 → extractInterface:1834 | kind `interface`; body walked with interface pushed → method_declarations become methods (bodiless — `;` — still nodes, no body walk) |
|
||||
| `enum_declaration` | enumTypes:1064 → extractEnum:1914 | `body:` field = enum_declaration_list; the backing type (`: string`, an unfielded `primitive_type` child) is never read; `class_interface_clause` child → implements refs via extractInheritance; `enum_case` children → extractEnumMembers; `method_declaration`/`const_declaration`/`use_declaration` children → visitNode (methods extract, consts + trait-uses via the hook) |
|
||||
| `property_declaration` | fieldTypes:1084 (gated `isInsideClassLikeNode`) → extractField:2046 | §Fields. Outside a class-like (invalid php) → falls through, children recursed |
|
||||
| `const_declaration` | **visitNode hook** (BEFORE the ladder) | §visitNode hook — the variableTypes:1098 branch is UNREACHABLE for php; **extractVariable is never called** |
|
||||
| `use_declaration` (trait use, inside class/trait/enum body) | **visitNode hook** | §visitNode hook |
|
||||
| `namespace_use_declaration`, include/require ×4 | importTypes:1209 → extractImport:3170 | §Imports |
|
||||
| `function_call_expression` / `member_call_expression` / `scoped_call_expression` | callTypes:1248 → extractCall:3684 | §extractCall. Top-level calls attribute to the FILE node (nodeStack=[file]) |
|
||||
| `nullsafe_member_call_expression` | **no branch** | recursed — **`?->` calls emit NOTHING** (#1251 follow-up, deliberately unshipped; pin CURRENT behavior). Inner argument calls still extract via recursion |
|
||||
| `object_creation_expression` | INSTANTIATION_KINDS:354(`object_creation_expression`), visitNode:1255 + body walker:5145 | extractInstantiation + findAnonymousClassBody (§extractInstantiation) |
|
||||
| `expression_statement`, `echo_statement`, `global_declaration`, `function_static_declaration`, `match_expression`, `anonymous_function`, `arrow_function`, attribute machinery, … | no branch | recursed. Calls/instantiations inside top-level closures attribute to the file node |
|
||||
|
||||
### visitNode hook (php.ts:108-144) — const + trait-use
|
||||
|
||||
Runs from tree-sitter.ts:943 with the ExtractorContext (:1465). Two branches:
|
||||
|
||||
- **`const_declaration` (ANY scope — top level, class, interface, trait,
|
||||
enum):** for each namedChild of type `const_element`: nameNode = its
|
||||
namedChildren `find(type==='name')` (the FIRST `name` — which IS the const
|
||||
name; the value of `const A = OTHER_CONST` is also a `name` node but comes
|
||||
second); skip if none; `ctx.createNode('constant', name, elem, {})` —
|
||||
**position = the const_element**, one node per element (`const A = 1, B = 2`
|
||||
→ two `constant` nodes), extra = {} so **no docstring, no signature, no
|
||||
visibility, no isStatic** — a `final public const int X = 5` typed const
|
||||
carries none of that. Returns true → hook-consumed →
|
||||
`scanFnRefSubtree(node,0)` (capture-only; php's dispatch is
|
||||
`arguments`-only so const initializers essentially never capture) → **no
|
||||
descent: const VALUES are never walked** (no calls/instantiates from const
|
||||
initializers). Contains edge from nodeStack top (file/class/interface/trait/
|
||||
enum). captureValueRefScope runs inside createNode (§Value refs).
|
||||
- **`use_declaration` (trait use inside a class-like body):** names =
|
||||
namedChildren filtered `type === 'name' || type === 'qualified_name'` — the
|
||||
used trait names ONLY (the `use_list` conflict block `{ A::g insteadof B;
|
||||
B::g as protected h; }` is type `use_list`, filtered out; its inner
|
||||
class_constant_access/name nodes are not direct children — probed). parentId
|
||||
= nodeStack top (the class); if none, nothing. Per name: unresolved ref
|
||||
{fromNodeId: parentId, referenceName: trait text (qualified_name keeps full
|
||||
`Foo\Bar` text), referenceKind: **`implements`**, line/column of the
|
||||
**use_declaration node** (same position for every name in `use A, B;`)}.
|
||||
Returns true → scanFnRefSubtree → no descent (insteadof/as clauses never
|
||||
extracted — **no aliased-method nodes, no conflict-resolution edges**).
|
||||
|
||||
### Namespace capture — extractFilePackage (:1397) + extractPackage (php.ts:150)
|
||||
|
||||
Before the walk: scan the ROOT's direct namedChildren for the FIRST
|
||||
`namespace_definition` (break at :1407 — **a file with multiple namespaces
|
||||
scopes everything under the first**). extractPackage: nsName = namedChildren
|
||||
`find(type==='namespace_name')`; hasBody = any namedChild of type
|
||||
`compound_statement` | `declaration_list`; `!nsName || hasBody` → null (braced
|
||||
namespaces make NO node and NO scoping); else the namespace_name text
|
||||
(`App\Services`). createNode('namespace', 'App\Services', the
|
||||
namespace_definition node) → **node #2 after the file node, regardless of
|
||||
where the declaration sits** (e.g. after `declare(strict_types=1)`); pushed on
|
||||
the nodeStack for the WHOLE walk → every top-level symbol's qualifiedName =
|
||||
`App\Services::Name` (buildQualifiedName :1447 joins stack names with `::`;
|
||||
namespacePrefix is always empty outside C/C++) — this is what
|
||||
`pushPhpUseRef`'s `Foo\Bar::Baz` refs resolve against. Methods:
|
||||
`App\Services::UserService::run`.
|
||||
|
||||
### Node creation, IDs, order
|
||||
|
||||
- createNode (:1308): id = `generateNodeId(filePath, kind, name, startRow+1)`
|
||||
= `` `${kind}:${sha256(`${filePath}:${kind}:${name}:${line}`).hex.slice(0,32)}` ``
|
||||
(tree-sitter-helpers.ts:18). File node id = literal `file:${filePath}`
|
||||
(:509), name = basename, qualifiedName = filePath, endLine =
|
||||
`source.split('\n').length`, isExported false. Dedupe/self-checks compare ID
|
||||
STRINGS (`node_ids` vec pattern).
|
||||
- endLine extension via resolveBody (:1329) — no hook → no-op for php.
|
||||
- contains edge from nodeStack top for every created node (:1363).
|
||||
- **A declaration with attributes STARTS at the attribute** — `#[Registry]\n
|
||||
class UserService` mints the class node at the `#[` row (node position =
|
||||
declaration node = attribute_list start). Affects generateNodeId's line AND
|
||||
drupal's function-id reconstruction (§Frameworks).
|
||||
- Emission order = TS walk order: file node → namespace node (if any) → source
|
||||
order (per construct: node + contains edge → its refs in extractor order) →
|
||||
fn-ref refs (flushFnRefCandidates :538) → value-ref EDGES (flushValueRefs
|
||||
:539). Store/harness are rowid-order-sensitive.
|
||||
|
||||
### extractFunction / extractMethod (:1517 / :1737)
|
||||
|
||||
- extractFunction: no getReceiverType → never diverts (:1522 no-op). Name via
|
||||
extractName (:90) → nameField `name`. `<anonymous>` never occurs for
|
||||
function_definition (grammar requires a name). Node: docstring (§Docstrings),
|
||||
signature **undefined**, visibility (hook — `'public'` for a bare function),
|
||||
isExported undefined, isAsync undefined, isStatic false (hook returns false
|
||||
when no static_modifier), returnType (hook). Then extractTypeAnnotations
|
||||
(§Type refs), extractDecoratorsFor (§Attributes — no-op), push, walk `body`
|
||||
field (compound_statement) via visitFunctionBody, pop.
|
||||
- extractMethod (method_declaration inside class/trait/interface/enum): gate
|
||||
:1747 passes via isInsideClassLikeNode (:1486 — parent kind ∈ class, struct,
|
||||
interface, trait, enum, module). Same extras as function (visibility from
|
||||
modifiers, isStatic real). receiverType undefined → no QN override, no
|
||||
:1799 owner-edge. **Bodiless method (interface/abstract):** `body` field
|
||||
missing → no body walk, node still minted.
|
||||
- **Nested named function inside a body** (`function inner() {}` in a method):
|
||||
visitFunctionBody:5245 → functionTypes + named → extractFunction → a
|
||||
`function` node contained by the enclosing method.
|
||||
- **Body-level class/interface/enum/trait declarations** (the polyfill idiom
|
||||
`if (!class_exists('X')) { class X {} }`): visitForCallsAndStructure
|
||||
:5255-5275 dispatches classTypes (incl. the trait classification) /
|
||||
enumTypes / interfaceTypes → full extraction, contained by the enclosing
|
||||
function. NOTE the body walker does NOT run the extractor's visitNode hook —
|
||||
but extractClass's own body walk uses visitNode, so consts/trait-uses INSIDE
|
||||
a body-level class still extract via the hook.
|
||||
- Closures (`anonymous_function`, renamed from
|
||||
`anonymous_function_creation_expression` — both untyped in TS) and
|
||||
`arrow_function`: **no nodes ever** — not in functionTypes; body walker
|
||||
recurses through them so their calls attribute to the ENCLOSING
|
||||
function/method/file. `scanFnRefSubtree`'s halt list (:606-612) includes
|
||||
`arrow_function` (halts scans at php arrow fns) but NOT
|
||||
`anonymous_function` (scan descends into closures — capture-only).
|
||||
- First-class callable `foo(...)` / `$x->m(...)` / `Cls::m(...)`: an ordinary
|
||||
call node with a `variadic_placeholder` argument → **plain `calls` ref** via
|
||||
extractCall (the function-ref spec deliberately leans on this — see
|
||||
function-ref.ts:361 comment).
|
||||
|
||||
### extractClass / extractInterface / extractEnum for php
|
||||
|
||||
- extractClass (:1679): resolvedBody = `body` field (declaration_list); no
|
||||
skipBodilessClass. Node kind class/trait: docstring, visibility (hook →
|
||||
bare class = 'public'), isExported undefined. extractInheritance (§below),
|
||||
extractCsharpPrimaryCtorParamRefs (no-op — needs `parameter_list` child
|
||||
type, php has none), extractDecoratorsFor (no-op), push, visit BODY
|
||||
namedChildren (hook first → consts/trait-uses; method_declaration →
|
||||
extractMethod; property_declaration → extractField; nested
|
||||
class_declaration → extractClass), no synthesizeMembers, pop.
|
||||
- extractInterface (:1834): kind `interface`; docstring, isExported undefined
|
||||
(NO visibility read — extractInterface never calls getVisibility);
|
||||
extractInheritance sees the interface's `base_clause`; body children visited
|
||||
with the interface pushed (methods, consts via hook).
|
||||
- extractEnum (:1914): body required (`body` field). docstring, visibility
|
||||
('public'), isExported undefined. extractInheritance → class_interface_clause
|
||||
→ implements. Body loop: `enum_case` ∈ enumMemberTypes → extractEnumMembers
|
||||
(:1958): **`name` field path → ONE `enum_member` node from
|
||||
`getChildByField(node,'name')`, positioned at the enum_case, then return** —
|
||||
backed-case values (`= 'H'`) never walked. Other children → visitNode
|
||||
(methods/consts/use).
|
||||
|
||||
### Fields — extractField php branch (:2077-2104)
|
||||
|
||||
property_declaration inside a class-like: docstring = preceding comment of the
|
||||
DECLARATION; visibility (hook); isStatic (hook). Java/C# `variable_declarator`
|
||||
finds miss → php branch: propElements = namedChildren of type
|
||||
`property_element` (≥1 in any valid property_declaration). typeNode = FIRST
|
||||
namedChild NOT of type {visibility_modifier, static_modifier,
|
||||
readonly_modifier, property_element, var_modifier} — i.e. the type node
|
||||
(primitive_type / named_type / optional_type / union_type / …) when present;
|
||||
**QUIRK: `final_modifier`/`abstract_modifier` are NOT excluded** — a
|
||||
`final public Foo $x` (php 8.4 final props; parses on 0.24.2) would take the
|
||||
final_modifier as the "type" (typeText = `final`). typeText = raw node text
|
||||
(`?Logger`, `iterable|CacheAlias`). Per element: varName = namedChildren
|
||||
`find(type==='variable_name')`; nameNode = varName's `find(type==='name')`;
|
||||
name = `name` (NO `$`); signature = `` typeText ? `${typeText} $${name}` :
|
||||
`$${name}` `` (the `$` is re-added in the signature only); one **`field`** node
|
||||
per element positioned at the property_element (multi: `private ?Logger
|
||||
$logger, $fallback;` → two nodes, same typeText), THEN RETURN — **the php
|
||||
branch skips extractDecoratorsFor AND extractTypeAnnotations** (both are only
|
||||
on the declarators path :2118-2141) → **property type-hints emit NO
|
||||
`references` from the field node** (the class's METHODS carry php type refs;
|
||||
properties don't). `var $legacy;` → var_modifier excluded → no type →
|
||||
signature `$legacy`. Untyped default (`default_value`) never walked — no refs
|
||||
from initializers. Promoted constructor params (`property_promotion_parameter`)
|
||||
are NOT fields — no node anywhere (§Type refs covers their type hints).
|
||||
Value-const kind upgrade (:2058) is java/csharp-gated — php fields stay `field`.
|
||||
|
||||
### Imports (:3170-3356 + :3508-3574)
|
||||
|
||||
extractImport, hook-first (:3176). Four php shapes:
|
||||
|
||||
1. **include/require (+_once)** (php.ts:163): phpStaticIncludePath — arg =
|
||||
namedChild(0); `parenthesized_expression` unwraps one level; must be
|
||||
`string` | `encapsed_string`; ALL namedChildren must be `string_content`
|
||||
(any interpolation/escape → null); content = the string_content text.
|
||||
Static → `{moduleName: path text, signature: trimmed full expression
|
||||
text}` → import node named the PATH + (no handledRefs) an `imports` ref
|
||||
{fromNodeId: **nodeStack top — the NAMESPACE node when a file-level
|
||||
namespace exists, else the file node** (validated on the built extractor:
|
||||
`from=namespace:…`), referenceName: the path, line/col of the include node}
|
||||
(:3183-3194). Import NODES likewise get their contains edge from the
|
||||
namespace and a namespace-prefixed qualifiedName
|
||||
(`App::App\Contracts\Logger`). Dynamic (`require __DIR__ . '/x'`, variables)
|
||||
→ hook null → falls THROUGH the php grouped branch (include nodes never
|
||||
match it) → `if (this.extractor.extractImport) return;` (:3350) → **nothing
|
||||
emitted**. Consumed by resolveIncludePath (import-resolver.ts:682-758) —
|
||||
suffix/relative file matching, `.php` appended if missing.
|
||||
2. **Single `use`** (incl. `use function`/`use const`/aliased): hook finds
|
||||
`namespace_use_clause` → its `qualified_name` (full text, e.g.
|
||||
`App\Contracts\Logger` — alias NOT included) else its `name` (bare
|
||||
single-segment import, e.g. `use Countable;`) → import node named that +
|
||||
the generic `imports` ref (same shape as includes). THEN the php-only
|
||||
:3224-3227 adds **emitPhpUseRefs** (:3515): clause → qualified_name ?? name
|
||||
→ pushPhpUseRef (:3563): strip leading `\`; **no `\` left → RETURN (bare
|
||||
`use Countable;` emits ONLY the generic ref, no `::` ref)**; else ref
|
||||
{fromNodeId = the same nodeStack top (namespace-or-file, per #1),
|
||||
referenceName: `` `${prefix}::${leaf}` `` (LAST `\` → `::`, e.g.
|
||||
`App\Contracts::Logger`), referenceKind: `imports`, line/col of the
|
||||
**declaration node**}. `use function App\Helpers\format_id` →
|
||||
`App\Helpers::format_id` (function imports ride the same path).
|
||||
3. **Grouped `use A\{B, C as D, Sub\E}`**: hook sees namespace_name +
|
||||
namespace_use_group → returns **null** (php.ts:171) → inline branch
|
||||
:3322-3347: prefix = namespace_name text; clauses = group's namedChildren of
|
||||
type `namespace_use_group_clause` | `namespace_use_clause` (v0.24.2:
|
||||
namespace_use_clause); per clause: nsName = clause's
|
||||
`find('namespace_name')` (v0.24.2: never present) → name = nsName ? its
|
||||
`find('name')` : clause's `find('name')` — FIRST `name` = the SOURCE name
|
||||
(aliases skipped); found → fullPath = `` `${prefix}\\${name}` `` → import
|
||||
node named fullPath (positioned at the whole DECLARATION, signature = full
|
||||
text) + pushPhpUseRef(fullPath) → `A::B` refs. **Nested `Sub\E` clause:
|
||||
qualified_name child → find('name') misses → clause SKIPPED entirely**
|
||||
(§Grammar-bump deltas #2). Multiple import NODES share the declaration's
|
||||
position → **same-(kind,name-differs) but same-line ids; `use A\{B, B}`
|
||||
would collide — id-string dedupe territory**.
|
||||
4. Any other hook-null case (malformed): :3350 → nothing (no generic fallback).
|
||||
|
||||
QUALIFIED_IMPORT (flushFnRefCandidates :665) admits `\`-separated import refs —
|
||||
**php `use` refs DO feed the fn-ref gate their last segment** (unlike rust's
|
||||
`::` paths): `App\Contracts::Logger` matches (`.`/`\` class) → contributes
|
||||
`Logger`… CAREFUL: the ref text contains BOTH `\` and `::` — the regex
|
||||
`^[A-Za-z_$][A-Za-z0-9_$.\\]*[.\\]([A-Za-z_$][A-Za-z0-9_$]*)$` REJECTS `:`
|
||||
characters entirely → `App\Contracts::Logger` does **NOT** match → contributes
|
||||
nothing. The include-path refs (`lib/plain.php`) contain `/` → also rejected.
|
||||
**Net: only bare single-segment `use X;` refs (SIMPLE_NAME) reach
|
||||
importedNames** — the php fn-ref gate is effectively "defined in this file ∪
|
||||
bare use imports ∪ skipGate candidates". Verify against the fixture.
|
||||
|
||||
### extractCall (:3684) — the php paths
|
||||
|
||||
php never hits the vbnet/erlang/ruby/arkts branches. Entry: nameField =
|
||||
`name` field, objectField = `object` ?? `scope` (:4137-4138).
|
||||
|
||||
**Branch A (:4140)** — `member_call_expression` / `scoped_call_expression`
|
||||
(both have name + object/scope):
|
||||
|
||||
1. **php fluent static-factory** (:4155-4173): objectField.type ===
|
||||
`scoped_call_expression` (i.e. `Cls::factory(...)->method()`):
|
||||
innerScope/innerName = the inner call's scope/name fields → calleeName =
|
||||
`` `${scopeText}::${nameText}().${methodName}` `` (inner ARGS dropped —
|
||||
`UserModel::query().where`); either missing → bare methodName. Emit +
|
||||
RETURN. (Inner scoped_call is ALSO visited by the walker's recursion →
|
||||
`UserModel.query` ref too — both emitted, like rust chains.) Consumed by
|
||||
the resolution chain matcher (`().` marker); scope text can be
|
||||
`self`/`static`/qualified — emitted verbatim (`self::make().x`).
|
||||
2. Java this-field unwrap (:4203) — `field_access` only, never php.
|
||||
receiverName = **raw objectField text** with ONE leading `$` stripped
|
||||
(:4215 `replace(/^\$/,'')`):
|
||||
- `$x->m()` → object variable_name `$x` → `x` → callee `x.m` (feeds
|
||||
local-receiver inference #1108 / typed-param #1125 — resolution-side,
|
||||
name-matcher.ts:1210-1217 php patterns).
|
||||
- `$this->m()` → `this` ∈ SKIP_RECEIVERS (:4219 {self, this, cls, super,
|
||||
parent, static}) → bare `m`.
|
||||
- **#1251/#1220 property receiver `$this->prop->m()`** → object =
|
||||
member_access_expression, raw text `$this->prop` → `this->prop` → callee
|
||||
**`this->prop.m`**. The ENTIRE #1251 machinery is RESOLUTION-side
|
||||
(name-matcher.ts:1333-1340 strips `this->`, phpPropertyTypePatterns
|
||||
:1418-1425 — modifier-prefixed typed property/promoted param OR
|
||||
`$this->prop = new Foo()`; the hardened SHADOWING GUARD: property-shaped
|
||||
patterns ONLY, so a plain `$prop` local/param elsewhere can never type
|
||||
the property; second chance inferPhpAssignedPropertyType :1438 follows
|
||||
`$this->prop = $var`; matchMethodCall :1533-1549 routes
|
||||
`^(this->\w+)\.(\w+)$` EXCLUSIVELY through declared-type inference —
|
||||
unresolvable stays unlinked, never name-matched). Extraction's ONLY job:
|
||||
the exact `this->prop.m` encoding + line/col.
|
||||
- Deeper `$this->a->b->m()` → `this->a->b.m` (resolver won't match — stays
|
||||
unresolved). `$obj->prop->m()` → `obj->prop.m` (same).
|
||||
- Instance-chain `$this->factory()->m()` → object =
|
||||
member_call_expression → raw text incl. ARGS → `this->factory().m` /
|
||||
`this->factory($cfg).m` (args KEPT — only the scoped fluent branch
|
||||
normalizes; the "fluent 2nd hop" gap, unshipped). `foo()->m()` →
|
||||
`foo().m`.
|
||||
- Nullsafe INNER receiver `$a?->b()->c()` → outer is member_call (object =
|
||||
nullsafe_member_call) → `a?->b().c`.
|
||||
- **LITERAL receivers are NOT suppressed** (#1230's
|
||||
LITERAL_RECEIVER_TYPES check lives in the generic Branch B :4397 only) —
|
||||
`"chain"->upper()` emits callee `"chain".upper` (garbage ref, never
|
||||
resolves; PRESERVE).
|
||||
- `self::m()` / `static::m()` / `parent::m()` → scope = relative_scope,
|
||||
text ∈ SKIP → bare `m`. `$var::m()` → scope variable_name → `var.m`.
|
||||
`\App\Util::go()` → scope qualified_name → callee `\App\Util.go`
|
||||
(leading `\` kept, `.`-joined — PRESERVE). **NOTE: scoped calls are
|
||||
DOT-joined** (`UserModel.query`, never `UserModel::query`) — laravel's
|
||||
`Model::method` resolve() pattern only ever sees `::` refs from OTHER
|
||||
emitters (fn-ref string callables, use refs).
|
||||
3. methodName empty (never in practice — grammar requires name) → fallthrough
|
||||
to no emission.
|
||||
|
||||
**Branch B (generic, :4312)** — `function_call_expression`: func = `function`
|
||||
field. Not a member/scoped shape → else :4518: calleeName = **raw func text**:
|
||||
bare `helper`; qualified `\App\Helpers\format_id` / `App\Helpers\other`
|
||||
(backslashes verbatim, unresolvable downstream — PRESERVE); variable callee
|
||||
`$fn()` → `$fn`; parenthesized/complex → raw text. FCC `format_id(...)` →
|
||||
`format_id`. Post-processing: parenthesized-conversion regex (:4529) can fire
|
||||
on parenthesized callees — `(\s*\*?\s*[A-Za-z_][\w.]*\s*)` shapes; php
|
||||
`($x)('a')` → func text `($x)` → regex needs `[A-Za-z_]` start after optional
|
||||
`*` → `$x` fails (`$`) → no rewrite (probe in fixture). Template strip (:4542)
|
||||
+ cpp fn-ptr fan-out (:4556) are c/cpp-gated. Final: one `calls` ref
|
||||
{callerId = nodeStack top, name, line = startRow+1, column = startColumn
|
||||
(UTF-16)}. extractCall returns immediately when the nodeStack is empty (never —
|
||||
file node pushed).
|
||||
|
||||
### extractInstantiation (:4610) + anonymous classes
|
||||
|
||||
`object_creation_expression`, from visitNode:1255 AND body walker:5145. ctor =
|
||||
`constructor`/`type`/`name` FIELDS (php has NONE — probed, the class child is
|
||||
unfielded) → `namedChild(0)`:
|
||||
|
||||
- `new UserModel()` → name → `UserModel`.
|
||||
- `new \App\Models\User()` / `new Models\User()` → qualified_name → full text
|
||||
`\App\Models\User`; `<`-strip no-op; **lastDot = max(lastIndexOf('.'),
|
||||
lastIndexOf('::')) — BACKSLASHES NOT HANDLED** → ref keeps the FULL
|
||||
qualified text incl. leading `\` (PRESERVE; resolution handles or drops).
|
||||
- `new static()` / `new self()` / `new parent()` → name (text
|
||||
`static`/`self`/`parent`) → instantiates refs literally named
|
||||
`static`/`self`/`parent` — unresolvable, PRESERVE.
|
||||
- `new $cls()` → variable_name → ref `$cls` (the `$` survives — only
|
||||
extractCall strips receiver `$`). PRESERVE.
|
||||
- `new class … {}` → **anonymous_class** (v0.24.2) → className = the WHOLE
|
||||
anon-class source text → `<`-strip at first `<` if the body contains one,
|
||||
then the `.`/`::` suffix logic on what remains, trim → one garbage
|
||||
instantiates ref (PRESERVE — pin exact bytes in the fixture). Then
|
||||
findAnonymousClassBody (:4815 — direct `class_body`/`declaration_list` child)
|
||||
→ **null on v0.24.2** (list nested in anonymous_class) → no
|
||||
extractAnonymousClass. Descent behavior (§Grammar-bump deltas #1): top-level
|
||||
→ methods extract as file-level `function` nodes (extractMethod :1747 gate →
|
||||
extractFunction; the object-literal parent check :1751 doesn't match
|
||||
declaration_list); in-body → **no nodes**, inner calls attribute to the
|
||||
enclosing symbol; base_clause/class_interface_clause of the anon class emit
|
||||
NOTHING either way (extractInheritance runs only from extract{Class,…}).
|
||||
- Ref position = the object_creation_expression. Children still recursed
|
||||
(visitNode :1255 leaves skipChildren false when no anonBody; body walker
|
||||
:5145 continues) → ctor-argument calls get their own refs.
|
||||
- **Param-default `new NullMailer()` inside a signature emits NOTHING** — the
|
||||
method walk covers the `body` field only; formal_parameters are walked
|
||||
exclusively by extractPhpTypeRefs (type nodes only). PRESERVE.
|
||||
|
||||
### Static-member / value-read refs (:4750-4808) — php IS in STATIC_MEMBER_LANGS (:345)
|
||||
|
||||
Called ONLY from the body walker (:5218) — top-level reads emit nothing.
|
||||
MEMBER_ACCESS_TYPES (:323) php rows: `class_constant_access_expression`
|
||||
(:328), `scoped_property_access_expression` (:329). NOTE
|
||||
`member_access_expression` (:325, listed for C#) ALSO matches php's `$x->y` —
|
||||
recv = object field = variable_name → not an accepted recv type → no-op, but
|
||||
the walker must still evaluate it (and any `name`-object member access —
|
||||
`FOO->x` — WOULD emit if capitalized; not expressible in valid php).
|
||||
Mechanics: callee-of-call skip (:4771-4779 — parent ∈ callTypes and its
|
||||
function/method/first-child starts at this node; scoped_call callees are
|
||||
scope+name directly, so this fires rarely for php); recv =
|
||||
`object`/`expression`/`scope` field ?? namedChild(0):
|
||||
|
||||
- `UserModel::class` / `Foo::CONST` / `Suit::Hearts` →
|
||||
class_constant_access_expression has NO fields → namedChild(0) = `name` ∈
|
||||
accepted types (:4791-4794) → capitalized regex `^[A-Z][A-Za-z0-9_]*$` →
|
||||
`references` ref to the class name at the RECEIVER's position.
|
||||
- `self::CONST` / `static::X` / `parent::Y` → namedChild(0) = relative_scope →
|
||||
not accepted → nothing.
|
||||
- `UserModel::$conn` → scoped_property_access_expression HAS `scope:` field =
|
||||
name → capitalized → references `UserModel`.
|
||||
- `\App\Models\User::class` → namedChild(0) = qualified_name → not accepted →
|
||||
nothing (PRESERVE).
|
||||
- lowercase receivers (`self`, `$x`) → nothing.
|
||||
|
||||
### Inheritance — extractInheritance (:5291) for php
|
||||
|
||||
Child-type scan on class/interface/enum nodes:
|
||||
|
||||
- **`base_clause`** (:5336, extends): no `type_list` child → targets =
|
||||
`[child.namedChild(0)]` — **ONLY THE FIRST base**. Classes are fine (single
|
||||
inheritance) but `interface I extends A, B, C` **drops B and C** (probed:
|
||||
base_clause children = [name, qualified_name, name]); a qualified first base
|
||||
keeps full text (`\Foo\Bar`). One `extends` ref, position = the target node.
|
||||
PRESERVE the drop.
|
||||
- **`class_interface_clause`** (:5437, implements): targets =
|
||||
child.namedChildren (ALL) → one `implements` ref per name/qualified_name —
|
||||
full text each (`HasColor`, `\JsonSerializable` with the backslash).
|
||||
Enum implements ride the same clause.
|
||||
- No other case matches php (`field_declaration` Go-shape absent, etc.).
|
||||
- The trait-`use` implements refs come from the visitNode hook (§above), NOT
|
||||
from extractInheritance.
|
||||
|
||||
### Type-annotation references (:5752-6069) — php IS in TYPE_ANNOTATION_LANGUAGES (:5753)
|
||||
|
||||
extractTypeAnnotations dispatches php (:5809-5811) to **extractPhpTypeRefs**
|
||||
(:6022) — for every FUNCTION and METHOD node (called at :1594/:1816; the
|
||||
property path :2037 is unreachable for php — §Fields):
|
||||
|
||||
- params: namedChildren `find(type==='formal_parameters')` → per parameter
|
||||
child (`simple_parameter` / `property_promotion_parameter` /
|
||||
`variadic_parameter`) → per namedChild ∈ PHP_TYPE_NODES (:310 — named_type,
|
||||
optional_type, nullable_type, union_type, intersection_type,
|
||||
disjunctive_normal_form_type, primitive_type) → walkPhpTypePosition.
|
||||
- return/direct: per namedChild of the DECLARATION ∈ PHP_TYPE_NODES →
|
||||
walkPhpTypePosition (catches the `return_type:` child; also a
|
||||
const_declaration's `type:` — but consts never reach here).
|
||||
- walkPhpTypePosition (:6040): `primitive_type` → nothing; `name` → text not
|
||||
∈ PHP_PSEUDO_TYPES (:5760 — self static parent mixed object iterable
|
||||
callable void null false true never array int float string bool) → one
|
||||
`references` ref at the name's position; `qualified_name` → **last
|
||||
`\`-segment** (not-pseudo) → ref at the qualified_name's position; wrapper
|
||||
types → recurse namedChildren. So `?Logger` → `Logger`;
|
||||
`Mailer|NullMailer` → both; `Logger&Deep ...$v` → both;
|
||||
`(A&B)|C` → A, B, C; `\App\Contracts\Logger $x` → `Logger`.
|
||||
- extractVariableTypeAnnotation (:6074, body `variable_declarator`s :5230)
|
||||
needs node type `variable_declarator`/`type_annotation` — php has neither →
|
||||
dead for php. property_signature/method_signature (:1282) — TS-only types.
|
||||
extractTypeRefsFromSubtree/BUILTIN_TYPES — never reached for php.
|
||||
|
||||
### Attributes `#[…]` — NO decorates refs, ever
|
||||
|
||||
extractDecoratorsFor (:4897) runs for functions/methods/classes but: the
|
||||
`attributes: attribute_list` direct child is type `attribute_list` — consider()
|
||||
accepts only decorator/annotation/marker_annotation/attribute/
|
||||
modifier_invocation → skipped, and only `modifiers`-typed children are
|
||||
descended (:4983 — php has none). Preceding-sibling scan (:5013) stops at the
|
||||
first non-decorator sibling immediately. The inner `attribute` nodes are never
|
||||
reached; attribute ARGUMENTS (`#[Deep(param: Logger::class)]`) are never
|
||||
walked. **php attributes emit nothing at all** — and (probed) the declaration
|
||||
node's position starts at `#[`, which is the ONLY observable effect. PRESERVE.
|
||||
|
||||
### Docstrings (tree-sitter-helpers.ts:95)
|
||||
|
||||
php comments (`//`, `#`, `/* */`, `/** */`) are all node type `comment` —
|
||||
accepted by the sibling scan. Consecutive preceding named siblings accumulate
|
||||
(unshift → source order). DOCSTRING_WRAPPER_TYPES (:55) — none apply to php
|
||||
(no climbing). **Attributes do NOT break the chain** (they're INSIDE the
|
||||
declaration node — contrast rust's attribute_item quirk): `/** doc */
|
||||
#[Attr] class C` keeps its docstring. cleanCommentMarkers (:77): `/**` open →
|
||||
strip `^\/\*+!?` + `\*+\/$`, then the `gm` per-line strips — `^\/\/[/!]?\s?`,
|
||||
`^#\s?` (php `#` comments), `^\s*\*\s?` (block continuation) — **all
|
||||
multiline: the #1329 CRLF `^`-after-`\r` semantics apply; use
|
||||
`js_multiline_strip` in docstring.rs** (the ONLY `(?m)`-class regexes in the
|
||||
php path — php.ts itself has none, and `\s*` in `^\s*\*\s?` is the classic
|
||||
CRLF `\n`-eater). Docstrings attach to functions/methods/classes/interfaces/
|
||||
enums/structs/properties(fields) — NOT to hook-created constants, NOT to
|
||||
enum_members, NOT to import nodes.
|
||||
|
||||
### Value-reference edges (:398-931) — php IS in VALUE_REF_LANGS (:401)
|
||||
|
||||
Port the full machinery (crib go.rs/java.rs): `CODEGRAPH_VALUE_REFS=0` kill;
|
||||
MAX_VALUE_REF_NODES = 20,000 caps the prune DFS and each reader scan;
|
||||
isGeneratedFile skip.
|
||||
|
||||
- **Targets** (captureValueRefScope :735, runs inside createNode): kind
|
||||
constant|variable — php mints ONLY `constant` (hook) — name length ≥3 AND
|
||||
`/[A-Z_]/`, parent id prefix ∈ {file:, class:, module:, struct:, enum:} —
|
||||
**top-level consts (under file: — or the file even when a namespace node
|
||||
exists? NO: with a namespace pushed, parent = `namespace:…` → NOT accepted →
|
||||
QUIRK: in a namespaced file, top-level `const` targets are DROPPED** (the
|
||||
namespace node id prefix `namespace:` is not in the list); un-namespaced
|
||||
files (drupal `.module`s, scripts) keep them. Class consts (class:) and enum
|
||||
consts (enum:) qualify; interface/trait consts (interface:/trait:) do NOT.
|
||||
fileScopeValueCounts bumps per name.
|
||||
- **Reader scopes**: every function/method/constant node (+variable — none).
|
||||
- **Shadow prune** (:803-878): the declarator switch has NO php cases that
|
||||
resolve — `assignment` (:829) is Python's node (php uses
|
||||
assignment_expression), `property_declaration` (:856) matches php's node
|
||||
type but its Kotlin/Swift extraction path (`variable_declaration` child /
|
||||
`name` field / value_binding_pattern) yields null → bump(null) no-op.
|
||||
declCounts stays empty → **no php target is ever pruned** (matches the :899
|
||||
comment — `$var` lives in another namespace). The walker still must bound
|
||||
the (no-op) DFS identically or skip it — either is byte-identical since it
|
||||
emits nothing.
|
||||
- **Emission** (:880-930): per reader scope DFS (php bodies are children — the
|
||||
Dart/Pascal sibling pull :891 is inert); match node type `name` (the
|
||||
php-specific reader type, :908; `identifier`/`constant`/`simple_identifier`
|
||||
never occur in php) whose text maps to a target, target ≠ self, name ≠
|
||||
scope's own name, deduped per (scope,target) → EDGE {source: scopeId,
|
||||
target, kind:'references', metadata:{valueRef:true}}. Because EVERY php
|
||||
`name` node matches — const reads (`MAX_RETRIES`), the const half of
|
||||
`self::MAX`, but ALSO the `name` INSIDE `variable_name` (`$MAX_RETRIES`),
|
||||
member names (`->MAX_RETRIES`), call names, interpolated `$X` in strings —
|
||||
**any textual occurrence of a target name inside a reader's subtree emits
|
||||
the edge**. PRESERVE (precision leans on the [A-Z_]-ish target-name gate).
|
||||
Const-element readers: `const A = OTHER;` — reader scope is the
|
||||
const_element; its own `name` (A) is skipped via target==self/name==scope
|
||||
checks; `OTHER` emits if a target.
|
||||
|
||||
### Function-as-value capture (#756) — PHP_SPEC (function-ref.ts:360)
|
||||
|
||||
idTypes = ∅ (**bare identifiers/`name`s are NEVER candidates**); dispatch:
|
||||
`arguments` → args; layers: `argument` → null (descend named children);
|
||||
special: {encapsed_string, string, array_creation_expression}. No
|
||||
unwrap/ungatedModes/addressOfOnly. Capture fires from visitNode:990, body
|
||||
walker:5137, and scanFnRefSubtree (hook-consumed subtrees). Rules
|
||||
(function-ref.ts:753-834):
|
||||
|
||||
- **String callable** (`'cmp_items'` / `"cmp"`): only when
|
||||
phpEnclosingCallName (:822 — ≤4 parent hops to a `function_call_expression`,
|
||||
aborting at member/scoped calls: **method-call HOFs never qualify**) is ∈
|
||||
PHP_CALLABLE_HOFS (:347 — array_map, array_filter, array_walk[_recursive],
|
||||
array_reduce, usort, uasort, uksort, array_udiff[_assoc],
|
||||
array_uintersect[_assoc], call_user_func[_array],
|
||||
forward_static_call[_array], preg_replace_callback[_array],
|
||||
register_shutdown_function, register_tick_function, set_error_handler,
|
||||
set_exception_handler, spl_autoload_register, ob_start, iterator_apply,
|
||||
header_register_callback, is_callable). Content = the `string_content`
|
||||
child's trimmed text; `^[A-Za-z_][A-Za-z0-9_]*$` → bare candidate,
|
||||
`^\w+::\w+$`-shaped (`Cls::method`) → qualified candidate — both
|
||||
**skipGate: true** (flush :712 — bypasses definedHere/imports). QUIRK:
|
||||
namespaced strings (`'App\Svc\fn'`) match neither regex → dropped.
|
||||
Note the qualified form ALSO always-flushes via the `::` rule (:709).
|
||||
- **Array callable** (ANY call's arguments, no HOF gate): exactly-2-element
|
||||
`array_creation_expression`; el0 = namedChild(0).namedChild(0), el1
|
||||
likewise; el1 must be string/encapsed_string with simple-name content;
|
||||
el0 = variable_name with text `$this` → candidate `this.<m>` (always
|
||||
flushes, :709); el0 = class_constant_access_expression whose namedChild(1)
|
||||
text === `class` (`[Foo::class, 'm']`) → `Foo::m` (always flushes).
|
||||
`['Cls', 'm']` (string receiver) → nothing. Positions: string-callable refs
|
||||
at the STRING node; array-callable refs at the el1 string node.
|
||||
- explicitRef = true for every php candidate (idTypes empty) — irrelevant at
|
||||
flush (no addressOfOnly). Flush dedupe `${fromNodeId}|${name}` →
|
||||
referenceKind `function_ref`.
|
||||
|
||||
### Closure-collection pass & other non-players
|
||||
|
||||
- CC_LANGUAGES (resolution/callback-synthesizer.ts:77) = {swift, kotlin} —
|
||||
**php is OUT** of closure-collection (synthesis-side anyway; nothing to port).
|
||||
- Chained-call #750 languages (the :4408 call-receiver re-encode list — cpp,
|
||||
c, kotlin, swift, rust, go, scala): **php is NOT in it**; php's only chain
|
||||
re-encode is the :4155 scoped fluent (`Cls::f().m`), plus the accidental
|
||||
raw-text shapes (`foo().m`, `this->factory().m`) documented above.
|
||||
- Value-ref shadow prune, csharp/dart/scala/etc. branches: inert as noted.
|
||||
- STATIC-member: in (§above). LITERAL_RECEIVER_TYPES: php-inert (Branch A has
|
||||
no literal check).
|
||||
|
||||
## Frameworks (stay TS-side — pin the walker's output contract)
|
||||
|
||||
- **laravelResolver** (laravel.ts): detect `artisan`/`app/Http/Kernel.php`.
|
||||
extract() (`.php` files only) regexes `Route::METHOD(...)`/`Route::resource`
|
||||
over stripCommentsForRegex'd source → `route` nodes with LITERAL ids
|
||||
`` `route:${filePath}:${line}:${METHOD}:${path}` `` (NOT hashed) + handler
|
||||
refs (`Cls@method`/`Cls`) — framework refs carry filePath+language (unlike
|
||||
extraction refs). resolve() consumes `Model::method` (only ever produced by
|
||||
fn-ref string callables / use refs — extraction scoped calls are DOT-joined)
|
||||
and `Controller@method`. No walker dependency beyond method/class node
|
||||
names + kinds.
|
||||
- **drupalResolver** (drupal.ts): languages ['php','yaml']. extract() on
|
||||
`.routing.yml` → route nodes; on hook files (`.module`/`.install`/`.theme`/
|
||||
`.inc`) AND every `.php` → hook refs whose fromNodeId is **RECONSTRUCTED as
|
||||
`generateNodeId(filePath, 'function', funcName, lineNum)`** (drupal.ts:248)
|
||||
with lineNum = the line of the `^function\s+(\w+)\s*\(` regex match
|
||||
(drupal.ts:236) — **the walker's function-node ids/lines must match
|
||||
byte-for-byte or every Drupal hook edge dangles** (attribute-prefixed
|
||||
functions already mismatch today — the regex finds the `function` line, the
|
||||
node starts at `#[` — preserved wire truth). Known latent perf bug at
|
||||
drupal.ts:387 (`getNodesByKind('function')` per hook ref, the #1180 class) —
|
||||
context only, do NOT fix in this arc.
|
||||
- Resolution-side consumers of extraction shapes (never ported, listed for
|
||||
the wire contract): resolveIncludePath (import-resolver.ts:682-758,
|
||||
path-shaped `imports` refs), the `Foo\Bar::Baz` use-ref resolution +
|
||||
PHP_PROP_SHAPE / `().`-chain handling (resolution/index.ts:935/1183,
|
||||
name-matcher.ts:1525), inferLocalReceiverType php patterns
|
||||
(name-matcher.ts:1210-1217).
|
||||
|
||||
## Parity mechanics (all have bitten before)
|
||||
|
||||
- **Emission order** per §Node creation — file → namespace → source-order walk
|
||||
→ fn-refs → value-ref edges. Refs interleave with nodes exactly as the TS
|
||||
call sites do (inheritance refs BEFORE the body's; a method's type-refs
|
||||
before its body's calls).
|
||||
- **generateNodeId inputs**: (filePath, kind, name, startRow+1) — name has NO
|
||||
`$` for fields, IS the full `App\Contracts\Logger` for import nodes, the
|
||||
package name for the namespace node; line = declaration start (=
|
||||
attribute_list start when attributes present; = const_element line for
|
||||
consts; = property_element line for fields; = enum_case line for members;
|
||||
= whole-declaration line for grouped-import nodes).
|
||||
- **UTF-16 columns + slices** (textutil::col16/slice_utf16): every
|
||||
ref/node column, `startIndex/endIndex` substrings (getNodeText), and the
|
||||
include-path/type/signature texts. php sources are full of multibyte
|
||||
strings — the torture fixture needs a non-ASCII line before a symbol.
|
||||
- **CRLF**: probed — the v0.24.2 scanner parses CRLF heredocs/nowdocs/
|
||||
docblocks cleanly and identically to old. The only CRLF-sensitive TS logic
|
||||
is cleanCommentMarkers' `gm` strips (§Docstrings) → `js_multiline_strip`.
|
||||
CRLF variants of the torture fixture derived in-memory, per the tsjs
|
||||
pattern.
|
||||
- **Defer policy**: per-file `has_error()` → `defer:` — wasm recovery is
|
||||
canonical. Expected incidence ≈0.0–0.1% on the NEW grammar (§table);
|
||||
`--max-deferral 0.1` default stands.
|
||||
- MAX_FILE_SIZE / generated-file skips: shared, nothing php-specific.
|
||||
- No php POST_PASS; no preParse; `sourceIsPreParsed` never set for php.
|
||||
|
||||
## Gates (per plan §5, no exceptions)
|
||||
|
||||
- **Grammar bump lands FIRST, standalone** (the rust pattern, with a php
|
||||
twist): vendor wasm + `=0.24.2` crate pin + VENDORED_WASM_LANGS +
|
||||
kernel-grammar-parity `GRAMMAR_LANGUAGES += 'php'` in one change, full suite
|
||||
green, **before any walker exists**. Old-wasm vs new-wasm full-init dump
|
||||
diff (`scripts/dump-graph.mjs`, cmp) on all three gate repos: the diff is
|
||||
expected NON-EMPTY — every hunk must classify into §Grammar-bump deltas
|
||||
(anon-class shapes, grouped nested clause, formerly-erroring files e.g.
|
||||
monolog `Level.php`); any OTHER category blocks the bump.
|
||||
- **Torture fixtures** per `## Fixtures to build` below (+ CRLF variants
|
||||
derived in-memory), exercised by the new parity suite.
|
||||
- **Parity sweeps** (`scripts/kernel-parity.mjs <dir>`, order-sensitive
|
||||
full-object, `--max-deferral 0.1`):
|
||||
- `/private/tmp/claude-501/-Users-colby-Development-CodeGraph-codegraph/765a9532-0a92-43de-8d50-7c8ca1cb345c/scratchpad/monolog` (small, 217 files)
|
||||
- `…/scratchpad/framework` (laravel/framework, medium, 2,999 files)
|
||||
- `…/scratchpad/symfony` (large, 10,736 files)
|
||||
(already cloned; re-clone fresh if gone). Then **full-init dump-diffs
|
||||
byte-identical** (kernel arm vs `CODEGRAPH_KERNEL=0`, `dump-graph.mjs`,
|
||||
cmp) on the same three.
|
||||
- **Suite**: new `__tests__/kernel-php-parity.test.ts` — torture + CRLF
|
||||
variants + leading-HTML fixture + an intentionally-erroring defer fixture
|
||||
(genuinely broken syntax — e.g. an unclosed `function f( {` — NOT an
|
||||
8.4 feature, those parse clean on v0.24.2) asserting the kernel defers and
|
||||
wasm output is served; full suite ×2 green with `CODEGRAPH_KERNEL_EXPECT=1`.
|
||||
- **`DEFAULT_ROUTED += 'php'`** (kernel/index.ts:37) only after ALL of the
|
||||
above; changelog rides the existing kernel entry.
|
||||
- Post-route sanity: remember §arch-2 — gate repos ride the raw path; a
|
||||
Laravel APP (artisan present) and a Drupal module are the decoded-path
|
||||
smoke checks (drupal hook-id reconstruction must still land — one
|
||||
`.module` fixture with a hook docblock).
|
||||
|
||||
## Fixtures to build
|
||||
|
||||
**`torture.php`** (the survey's `svy-php/torture.php` is the seed; every line
|
||||
below names the branch it pins), **a CRLF variant of each fixture derived
|
||||
in-memory** (normalization-proof, per the tsjs pattern), **one leading-HTML
|
||||
mixed file** (HTML text + `<?php` + `?>` more HTML + `<?=` short echo —
|
||||
absolute row positions of post-HTML symbols, text/text_interpolation
|
||||
recursion), **one intentionally-erroring defer fixture** (genuinely broken
|
||||
syntax — an unclosed `function f( {` — NOT an 8.4 feature, those parse clean
|
||||
on v0.24.2; asserts kernel `defer:` + wasm-served output), and **one
|
||||
`.module`-named fixture** (drupal extension routing + a `@Implements
|
||||
hook_x().` docblocked function whose reconstructed node id must match).
|
||||
|
||||
torture.php inventory: file-level namespace (+ a second namespace_definition
|
||||
ignored; braced form → no node); use forms: single, aliased, bare
|
||||
single-segment (no `::` ref), `use function`, `use const`, grouped incl.
|
||||
aliased member AND the nested `Sub\Deep` SKIP; include/require ×4 incl.
|
||||
parenthesized + dynamic (nothing); interface multi-extends (first-only);
|
||||
class extends + implements (qualified `\JsonSerializable` text); trait decl
|
||||
+ `use A, B { insteadof / as }` (2 implements refs at the use line, nothing
|
||||
else); enum backed + pure + implements + method + const-in-enum + enum_case
|
||||
positions; class consts (multi-element, typed, final) + top-level const
|
||||
(value-ref target only when un-namespaced!); properties: typed, nullable,
|
||||
union, readonly, `var`, multi-element, static; promotion ctor (type refs
|
||||
only, no field nodes, `new` default emits nothing); methods: visibility
|
||||
default 'public', static, abstract/bodiless, `: self`/`: static` → 'self',
|
||||
`: ?Foo`, `: Foo|Bar` → undefined, `: void` → undefined; nested named
|
||||
function in a body; a body-level conditional class (polyfill idiom); closures
|
||||
(`function() use (&$x)`) + arrow fns (calls attribute to encloser, no nodes);
|
||||
FCC `f(...)`/`$this->m(...)`/`Cls::m(...)` (plain calls refs); call shapes:
|
||||
bare, qualified `\A\B\f()` (verbatim), `$x->m()`, `$this->m()` (bare),
|
||||
`$this->prop->m()` (**`this->prop.m`**), 2-hop `$this->a->b->m()`,
|
||||
`$obj->prop->m()`, `Cls::m()` (**`Cls.m` dot-joined**),
|
||||
`self::`/`static::`/`parent::` (bare), `$var::m()`, `\Qual\Cls::m()`, fluent
|
||||
`Cls::factory($a)->m()` (**`Cls::factory().m`** + inner `Cls.factory`),
|
||||
`$this->factory()->m()` (`this->factory().m` args-kept variant too), nullsafe
|
||||
`?->` (NOTHING), literal `"x"->upper()` (`"x".upper`); instantiation:
|
||||
`new Cls`, `new \Q\Cls` (full text), `new static/self/parent` (literal),
|
||||
`new $cls` (`$cls`), ctor-arg call recursion; anonymous class top-level
|
||||
(file-level `function` nodes + the garbage instantiates ref) AND in-body
|
||||
(nothing but attributed calls); static-member reads `Cls::CONST`,
|
||||
`Cls::class`, `Cls::$prop`, `self::CONST` (nothing), `\Q\Cls::CONST`
|
||||
(nothing), enum `Suit::Hearts`; match expression; `$$var`; interpolation
|
||||
`"{$this->x} $y"` + heredoc with interpolation + nowdoc; fn-refs:
|
||||
`usort($a,'cmp')`, `array_map('A\B\f',…)` (dropped),
|
||||
`call_user_func([$this,'m'])`, `[Foo::class,'m']`, `['Cls','m']` (dropped),
|
||||
`register_shutdown_function('Cls::m')`, a method-call HOF (`$x->map('cb')` —
|
||||
dropped), non-HOF string arg (dropped); value refs: un-namespaced const +
|
||||
reader incl. a `$CONST_NAME` variable occurrence and an interpolated read;
|
||||
docblocks: `/** */` multi-line, `//` + `#` runs,
|
||||
attribute-does-NOT-break-docstring, docstring-position class WITH attributes
|
||||
(node line = `#[` line); a non-ASCII (UTF-16) line before a symbol.
|
||||
|
||||
## Probe artifacts (session scratchpad `svy-php/`)
|
||||
|
||||
`variant-probe.cjs` (variant/ABI), `construct-errors.cjs` (old-vs-new
|
||||
per-construct error matrix), `shape-probe-php.cjs` + `torture.php` /
|
||||
`torture-clean.php` (full-tree OLD/NEW dumps + `shape-torture-clean.diff`, the
|
||||
278-line classified diff), `mini-probes.cjs` + `mini-probes.out` (new
|
||||
static/self, `: self`, braced namespace, `<?=`, anon-class both scopes,
|
||||
qualified calls/new, static locals, trait/interface consts), CRLF inline
|
||||
probe, `error-incidence.cjs` (the §incidence table), `tree-sitter-php.wasm`
|
||||
(the staged-candidate build), `tree-sitter-php/` (tag clone) +
|
||||
`crate-extract/` (tarball) with matching shas.
|
||||
@@ -605,7 +605,7 @@ parity before porting the language.
|
||||
| python | `languages/python.ts` | T1 | crates.io | Third. Decorator extraction feeds framework route detection — parity required. **PORTED + DEFAULT-ON (§4e).** | ✅ |
|
||||
| go | `languages/go.ts` | T1 | crates.io | Third (tie). Value-reference edges ship here too (#897). **PORTED + DEFAULT-ON (§4e).** | ✅ |
|
||||
| ruby | `languages/ruby.ts` | T1 | crates.io | **DONE (R7b #3, 2026-07-20)** — `ruby.rs` walker; grammar bumped to v0.23.1 (crate + vendored wasm together; content bump, ABI stays 14; standalone gate: old-vs-new dumps byte-identical on sinatra/jekyll, rails = exactly the one classified `&.!=` misparse-fix hunk). Parity 0-diff on sinatra/jekyll/rails (3,763 files, 0 deferrals) + dump byte-identical ×3. Introduced the **v2 ref-flag wire slot** (REF_FLAG_FILE_PATH): the visitNode hook's mixin `implements` refs carry `filePath: ctx.filePath` — the one extraction-ref denormalized field; php's trait-use refs need the same bit. Quirk list: docs/design/ruby-kernel-port-checklist.md. | ☑ |
|
||||
| php | `languages/php.ts` | T1 | crates.io | PHP property-receiver shapes (#1220/#1251) are RESOLUTION-side, unaffected. Trait-use `implements` refs carry filePath → use REF_FLAG_FILE_PATH (shipped with ruby). Grammar bump to 0.24.2 required (NOT graph-neutral — see the php checklist's classified delta). | ☐ |
|
||||
| php | `languages/php.ts` | T1 | crates.io | **DONE (R7b #4, 2026-07-20)** — `php.rs` walker (LANGUAGE_PHP, never PHP_ONLY); grammar bumped to v0.24.2 (crate + vendored wasm together; NOT graph-neutral — bump gate = enumerate+classify: anon-class wrapper, grouped nested-clause skip, old-error files, the survey-missed 8.4 `new X()->m()` misparse fix, everything else proven resolution ripple via ref↔edge pairing). Parity 0-diff monolog/laravel-framework/symfony (13,950 files) + dump byte-identical ×3. Trait-use implements refs ride REF_FLAG_FILE_PATH. Quirk list: docs/design/php-kernel-port-checklist.md. | ☑ |
|
||||
| csharp | `languages/csharp.ts` | T1 | crates.io | **DONE (R7b #2, 2026-07-20)** — `csharp.rs` walker; NO grammar bump (the #717 vendored wasm verified table-identical to crate 0.23.5 — first port with no grammar-prep step); the #237 `#if` preParse stays TS-side via the route-point hoist. Parity 0-diff on serilog/Newtonsoft.Json/jellyfin (3,229 files) + dump byte-identical ×3; deferral 0.05–3.3% = both-arm `#if` damage. Quirk list: docs/design/csharp-kernel-port-checklist.md. | ☑ |
|
||||
| rust | `languages/rust.ts` | T1 | crates.io | **DONE (R7b #1, 2026-07-20)** — `rustlang.rs` walker; grammar bumped to v0.24.2 (crate + vendored wasm together). Parity 0-diff on ripgrep/tokio/rust-analyzer + dump byte-identical ×3; rust-analyzer's parser crates defer 18% (token-macro tables, both-arm parse errors — grammar-inherent). Quirk list: docs/design/rust-lang-kernel-port-checklist.md. | ☑ |
|
||||
| dart, scala, lua, luau, r | dedicated files | T1 | crates.io (luau/r/scala: verify crate freshness vs our wasm) | Long-tail T1; port opportunistically after the big five. | ☐ |
|
||||
|
||||
Reference in New Issue
Block a user