|
|
@@ -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.
|