Skip to content

feat(engine): extract php symbols with structure awareness - #369

Merged
egolearner merged 5 commits into
zvec-ai:mainfrom
GrzegorzDrozd:feat/php-rust-support
Nov 11, 2026
Merged

egolearner merged 5 commits into
zvec-ai:mainfrom
GrzegorzDrozd:feat/php-rust-support

Conversation

@GrzegorzDrozd

@GrzegorzDrozd GrzegorzDrozd commented Nov 4, 2026 •

Copy link
Copy Markdown
Contributor

PHP files were discovered but fell through to plain-text chunks. They now parse with the full tree-sitter-php 0.25.1 grammar, so .phtml templates that mix HTML and PHP work too, and index classes, interfaces, traits, enums, enum cases, functions, methods, properties, constants and braced namespaces with scopes, signatures and docblocks.

  • Source outside declarations, such as template markup, imports and top-level statements, is kept as plain-text chunks so it stays searchable. Docblocks already stored as documentation are not indexed twice, and sections holding only open or close tags are skipped.
  • Property and constant statements that declare several elements split into one entity per element, each with its own signature built from the shared modifiers and type. A lone element keeps the whole statement, as Java fields do.
  • Promoted constructor parameters, including by-reference ones, are indexed as properties on the class, since the walk never descends into a method.
  • Signatures drop attributes and PHP 8.4 property hook bodies but keep ternary values.
  • Traits classify as interfaces, matching Rust traits.
  • Outlines parse stored entities with the full grammar behind a synthetic open tag, so ?>text<?php switches stay valid. Member, nullsafe and static calls keep their receiver or class, so Audit::record and Ledger::record stay distinct.
  • zg --help file-types lists php (.php, .phtml) under structured code.

Statement-form namespaces (namespace App;) have no body, so their declarations are not scoped under them. Visibility stays unset, matching the other languages. PHP has no import or heritage vocabulary in the code graph yet, so it uses the generic resolution.

Existing workspaces already hold PHP files as plain-text chunks, and unchanged files are not re-extracted. Run zg --index --rebuild to pick up PHP symbols.

The supported-content guide update will follow in a zvec-web PR.

@GrzegorzDrozd

Copy link
Copy Markdown
Contributor Author

Implements #88

@github-actions

github-actions Bot commented Nov 4, 2026 •

Copy link
Copy Markdown

ZG Retrieval Benchmark

Retrieval-only · PR base vs head

✅ Complete

Base: zvec-ai/zvec-grep@e4082245bcc861ed92a25203f7e3f2f4cfdbb063

Head: GrzegorzDrozd/zvec-grep@c833298ef1381b4abce2a9bd60b42da96ae838ce

Each cell is base / head / Δ. Δ = head − base; all five metrics use the 0–1 scale and higher is better. Both versions use the same frozen harness, queries, gold labels and local embedding models, with fresh indexes and five calls per query/mode. Quality is report-only; no regression threshold is applied.

Suite Mode File Hit@1 File Hit@5 File Hit@10 MRR@10 nDCG@10
SWE-QA20 hybrid 0.3500 / 0.3500 / 0.0000 0.6000 / 0.6000 / 0.0000 0.7500 / 0.7500 / 0.0000 0.4793 / 0.4793 / 0.0000 0.4374 / 0.4374 / 0.0000
SWE-QA20 fts 0.4500 / 0.4500 / 0.0000 0.6500 / 0.6500 / 0.0000 0.8500 / 0.8500 / 0.0000 0.5347 / 0.5347 / 0.0000 0.4184 / 0.4184 / 0.0000
SWE-QA20 vector 0.3500 / 0.3500 / 0.0000 0.6500 / 0.6500 / 0.0000 0.7000 / 0.7000 / 0.0000 0.4905 / 0.4905 / 0.0000 0.4650 / 0.4650 / 0.0000
BEIR / four datasets hybrid 0.3500 / 0.3500 / 0.0000 0.6500 / 0.6500 / 0.0000 0.7000 / 0.7000 / 0.0000 0.5056 / 0.5056 / 0.0000 0.4784 / 0.4784 / 0.0000
BEIR / four datasets fts 0.5500 / 0.5500 / 0.0000 0.8000 / 0.8000 / 0.0000 0.8500 / 0.8500 / 0.0000 0.6633 / 0.6633 / 0.0000 0.5879 / 0.5879 / 0.0000
BEIR / four datasets vector 0.2000 / 0.2000 / 0.0000 0.6000 / 0.6000 / 0.0000 0.6500 / 0.6500 / 0.0000 0.3221 / 0.3221 / 0.0000 0.3675 / 0.3675 / 0.0000
DuRetrieval / Chinese web search hybrid 0.6000 / 0.6000 / 0.0000 1.0000 / 1.0000 / 0.0000 1.0000 / 1.0000 / 0.0000 0.7583 / 0.7583 / 0.0000 0.5090 / 0.5090 / 0.0000
DuRetrieval / Chinese web search fts 0.5000 / 0.5000 / 0.0000 0.9000 / 0.9000 / 0.0000 0.9000 / 0.9000 / 0.0000 0.6833 / 0.6833 / 0.0000 0.5589 / 0.5589 / 0.0000
DuRetrieval / Chinese web search vector 0.3000 / 0.3000 / 0.0000 0.9000 / 0.9000 / 0.0000 0.9000 / 0.9000 / 0.0000 0.4783 / 0.4783 / 0.0000 0.3872 / 0.3872 / 0.0000
Quarry / eight languages hybrid 0.6500 / 0.6500 / 0.0000 0.8000 / 0.8000 / 0.0000 0.8000 / 0.8000 / 0.0000 0.6950 / 0.6950 / 0.0000 0.6630 / 0.6630 / 0.0000
Quarry / eight languages fts 0.5500 / 0.5500 / 0.0000 0.7000 / 0.7000 / 0.0000 0.7500 / 0.7500 / 0.0000 0.6321 / 0.6321 / 0.0000 0.5940 / 0.5940 / 0.0000
Quarry / eight languages vector 0.7000 / 0.7000 / 0.0000 0.8000 / 0.8000 / 0.0000 0.9000 / 0.9000 / 0.0000 0.7538 / 0.7538 / 0.0000 0.7398 / 0.7398 / 0.0000

SWE-QA nDCG uses repository macro averaging; the other suites use query means. Suites are reported separately. Full base/head reports and per-call evidence are available in the run artifacts.

View run and artifacts

Embedding: local. e2e: skipped. This comment is updated by automatic PR Retrieval runs.

@egolearner egolearner left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reusing the existing language adapter fits the PHP support requested in #88. The main issue is loss of previously searchable template and top-level PHP content once a declaration is found. The inline comments also cover existing-index upgrades, declaration metadata, and outlines.

Please include a companion update to both language versions of the supported-content guide, as required by CONTRIBUTING.md. The published guides still classify PHP as plain-text chunks. English guide · Chinese guide

GrzegorzDrozd added a commit to GrzegorzDrozd/zvec-grep that referenced this pull request Nov 6, 2026
Address review on zvec-ai#369.

- Index source between PHP declarations as plain-text chunks, so
templates, imports and top-level statements stay searchable.
- Give each split constant or property element its own signature.
- Keep ternary true branches in signatures.
- Name by-reference promoted parameters without the & and $.
- Keep the receiver or class in PHP call targets in outlines.
- Parse stored PHP entities with the full grammar behind a synthetic
open tag, so ?>text<?php switches stay valid.
PHP files were discovered but fell through to plain-text chunks. They now
parse with tree-sitter-php 0.25.1 and index classes, interfaces, traits,
enums, enum cases, functions, methods, properties, constants and braced
namespaces with scopes, signatures and docblocks.

- Property and constant statements that declare several elements split
into one entity per element. A lone element keeps the whole statement,
as Java fields do.
- Promoted constructor parameters are indexed as properties on the
class, since the walk never descends into a method.
- Signatures drop attributes and PHP 8.4 property hook bodies.
- Traits classify as interfaces, matching Rust traits.
- Outlines parse stored entities with the PHP-only grammar, because they
lack an open tag, and recognise member, nullsafe and static calls.
- zg --help file-types lists php under structured code.

Statement-form namespaces (namespace App;) have no body, so their
declarations are not scoped under them. Visibility stays unset, matching
the other languages.
Address review on zvec-ai#369.

- Index source between PHP declarations as plain-text chunks, so
templates, imports and top-level statements stay searchable.
- Give each split constant or property element its own signature.
- Keep ternary true branches in signatures.
- Name by-reference promoted parameters without the & and $.
- Keep the receiver or class in PHP call targets in outlines.
- Parse stored PHP entities with the full grammar behind a synthetic
open tag, so ?>text<?php switches stay valid.
@GrzegorzDrozd

Copy link
Copy Markdown
Contributor Author

Addressed all inline comments in the latest commit. For the companion docs, I'll open a zvec-web PR updating the supported-content guide: PHP moves to structured code with .phtml listed, plus a note that existing workspaces need zg --index --rebuild to get PHP symbols. I'll link it here once it's up.

@GrzegorzDrozd

Copy link
Copy Markdown
Contributor Author

Doc update: zvec-ai/zvec-web#82

@egolearner egolearner left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The previous review threads are resolved. Two additional P2 issues remain in comment coverage and callable summaries.

Address second review on zvec-ai#369 and findings from adversarial local tests.

- Cover a declaration's docblock from the declaration, so comments
between split constant or property elements stay searchable.
- Skip calls that run nothing at the call site: first-class callables
(record(...)), anonymous classes, immediately invoked closures and
new static, new self or new parent.
- Skip attribute arguments, which run on reflection.
- Build receiver call names from tokens, so layout and comments no
longer leak into them and dynamic names keep their closing brace.
- Record PHP member, nullsafe and static calls as graph edges with
their receiver, as Java does. Outlines and graph share one rule for
what counts as a call.
- Scan every element of a split PHP statement for calls, so each
initializer is attributed to its own element.
- Let complete top-level PHP functions skip the method wrapper in
outlines, which the synthetic open tag used to prevent.
- Stop joining receiver tokens once a name exceeds the line limit.

The graph does not yet resolve receiver-qualified or cross-file
references in any language, so PHP method edges stay pending until the
resolver supports them.
@GrzegorzDrozd

Copy link
Copy Markdown
Contributor Author

Hi @egolearner :)

Pushed fixes for both comments. While testing this round I also ran a larger local corpus of unusual PHP through the real zg binary. It found a few more outline issues, which this push fixes as well:

  • Anonymous classes, immediately invoked closures and new static, new self or new parent are no longer listed as calls.
  • Calls inside attribute arguments are skipped, since they run on reflection.
  • Receiver call names are built from tokens, so line breaks, indentation and comments inside a chain no longer leak into the name. Dynamic names such as $this->{'dyn'}() keep their closing brace.
  • PHP member, nullsafe and static calls now reach the call graph with their receiver, the same way Java's do. Outlines and graph share one rule for what counts as a call.

Known limitations I left alone: declarations nested inside a function body aren't extracted, as in the other languages. isset, empty and eval show up as calls because the grammar models them that way.

@egolearner egolearner left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One P2 issue remains in PHP call-graph resolution.

Copy link
Copy Markdown
Contributor

Thanks for the fixes and the additional PHP coverage. I've reviewed the updates and resolved the previous review threads. The documented rebuild requirement and companion docs update work for this PR, and the known limitations you listed can stay outside its scope.

I left one additional comment on the call-graph fallback: dropping an unsupported receiver can bind a method call to an unrelated global function. Preserving the receiver and leaving the target unresolved, or omitting an unsupported edge, would address this without requiring broader PHP name or type resolution. Please also add a regression test covering the final partitioned graph with a same-named global function.

Address third review on zvec-ai#369.

- Keep the receiver on every PHP member, nullsafe and static call edge.
A receiver the resolver cannot read, such as \Audit or
$this->factory(), keeps its source spelling, so the call stays pending
instead of binding to a same-named global function.
- Drop edges for dynamic method names such as $this->{'record'}().
- Use the bare method name in outlines when the receiver defines a class
or closure inline, so its source no longer leaks into the call name.
- Skip calls of another call's result, such as make()(). The inner call
is still reported.
@GrzegorzDrozd

Copy link
Copy Markdown
Contributor Author

@egolearner ready :)

egolearner
egolearner previously approved these changes Nov 10, 2026

Copy link
Copy Markdown
Contributor

Thanks for adding PHP support and for working through the review feedback. I appreciate the extra regression coverage and the companion documentation update. The review is complete, and I'm preparing to merge this. Thanks again for the contribution!

Copy link
Copy Markdown
Contributor

CI is currently failing in rust quality and the Linux/macOS/Windows test jobs. The quality job reports:

error[E0063]: missing field `package` in initializer of `domain::metadata::CodeMetadata`
  --> crates/zg-engine/src/extraction/code.rs:1027:43

CI is testing the merge with the latest main, where CodeMetadata now has a package field, but the new PHP test initializer does not set it. The platform test jobs also stop during compilation, before running the tests.

Could you merge the latest main from zvec-ai/zvec-grep into this branch, update the initializer (e.g. package: None for PHP), and rerun CI? main has also received call-graph and cross-file resolution updates, so syncing it before we merge would be helpful. Thanks!

Set package: None in the PHP metadata test, since CodeMetadata gained a
package field on main.

🤖 Generated with Claude Code

Co-Authored-By: Claude <noreply@anthropic.com>
@GrzegorzDrozd

Copy link
Copy Markdown
Contributor Author

Merged the latest main into the branch and set package: None in the PHP metadata test. fmt, clippy and the engine and CLI tests pass locally on the merged tree. I also checked the PHP call edges against the new resolver: self::record() inside a class now resolves to that class's method, and $this, static and parent receivers stay pending, so nothing binds to a same-named global function.

@egolearner
egolearner merged commit cc283e6 into zvec-ai:main Nov 11, 2026
26 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants