Skip to content

fix(php): keep proven vendor types out of project name fallbacks - #2520

Merged
DeusData merged 15 commits into
mainfrom
fix/issue-1186-vendor-types-v2
Oct 12, 2026
Merged

DeusData merged 15 commits into
mainfrom
fix/issue-1186-vendor-types-v2

Conversation

@DeusData

@DeusData DeusData commented Oct 3, 2026 •

Copy link
Copy Markdown
Owner

PHP imports whose declared namespaces are absent from the indexed project can currently resolve constructors, inherited types, usages, and typed receiver calls to unrelated project symbols sharing a short name. This adds typed, per-file import evidence and uses it before PHP refinement and generic name fallback in both pipeline paths. Exact semantic targets and HTTP/ASYNC classification retain precedence.

Both PHP IMPORTS writers retain class/function/constant provenance, including same-alias bindings that share an edge. File evidence is accepted only when the full current import manifest and stored edges agree. Rebuilds invalidate the old completion marker before allocating replacement metadata. Namespace-map construction fails closed on detectable allocation or insertion failures so a partial map cannot prove absence. Borrowed evidence is released before result disposal or spill/reload. Binding arrays and namespace keys use tracked memory-core allocation and matching cleanup; existing FQN values and JSON writer buffers retain their allocator ownership. Sequential and parallel import paths share cleanup without increasing the raw-allocation baseline.

The absence proof is intentionally bounded. Missing namespace maps, overlong names, unresolved global/path imports, and covered-but-missing or empty-prefix PSR-4 imports leave the whole file unknown; resolved IMPORTS remain. Ordinary usage sites with genuinely ambiguous import kinds may remain unresolved. This does not claim full PHP import-kind coverage or complete resolution of #1186.

Adds regressions for known-empty versus unknown evidence, grouped import kinds and project positives, pre-refinement property/return/trait cases, same-occurrence stale results, service calls, and sequential/parallel spill parity.

Validation: UNRUN by explicit user waiver: local builds, tests, compiler/syntax checks, lint, sanitizers, benchmarks, revert proof, and CI. Changed-range formatting with clang-format 20, source/ownership review, and diff/byte/literal/hash integrity checks were performed. Runtime and lint confirmation are deferred to PR CI. Existing graphs need reindexing to obtain the new PHP import evidence.

Base: main. Depends on #2515 (the import-resolution prerequisite); this branch includes that prerequisite. Refs #1186.

@DeusData

DeusData commented Oct 3, 2026

Copy link
Copy Markdown
Owner Author

Checkpoint and handover (2026-10-03 UTC)

Published head: 6e2c18bfbff87d0611dceac419bc235a5e686169. The remote head was verified.

PHP vendor type handling is published with the allocator/ownership follow-up. Review covered target filtering, namespace keys tracked as OTHER, borrowed values and libc-owned allocations; the raw-allocation baseline was not relaxed. The earlier lint failure requires a fresh hosted result on this corrected head. Local lint/runtime verification was not run. Import prerequisites are #2509 and #2515.

Hosted snapshot at 2026-10-03 21:54:18 UTC: 3 queued. Confirm the required checks on this exact head before treating it as ready.

No additional local build, test, lint, sanitizer, benchmark or CI runs were performed at this checkpoint, as requested. Earlier executed evidence remains historical; prepared tests and the newer source-reviewed changes must still be validated by the hosted gate.

The campaign is paused at the maintainer’s request. Local monitoring has stopped; hosted jobs remain running. No merge was performed. Thanks for reviewing this change.

Preserve the arena-owned raw Module caller name so top-level resolutions
join the extractor in both pipeline drivers. Add occurrence and ownership
controls, including package modules and named lambdas.

Signed-off-by: Martin Vogel <[email protected]>
Resolve symbols declared in __init__.py under their package while retaining
the raw module identity for file and top-level caller nodes. Carry explicit
language through registry callers, select Python package imports, preserve
long exact names, and report scope allocation failures without partial output.

Signed-off-by: Martin Vogel <[email protected]>
Use tracked zeroed storage for the parallel LSP failure array and
release it with the matching memory class after worker joins.
Share import-filter failure cleanup without changing fallback state.
Keep existing raw-allocation allowances unchanged.

Signed-off-by: Martin Vogel <[email protected]>
cbm_scope_lookup_local() returns the frame binding (CBMVarBinding), not
its type, so passing it to pylsp_init_named_type() did not compile
(-Werror=incompatible-pointer-types). Add pylsp_local_type(), which
returns the binding's type field or NULL when the name is unbound, and
assert on that type as the test intended.

Signed-off-by: Martin Vogel <[email protected]>
…alk call

The cross and register scope-failure cases defined S and called s.m()
inside pkg/__init__.py. The per-file LSP already resolves those calls,
and the parallel driver skips the cross-file walk for a file whose call
sites are all resolved, so CBM_TEST_PY_LSP_SCOPE_FAIL_STAGE=cross and
=register never fired on the parallel path and both parallel cases saw
no file error.

For those two stages, move S into memo_def.py (imported by
pkg/__init__.py) so run() holds calls only the cross walk resolves, and
count that module's File node. The raw stage keeps its single-file
fixture; the error assertions are unchanged.

With the corrected fixture all eight package-scope pipeline cases pass;
removing the CBM_PY_LSP_SCOPE_ERROR capture in pass_parallel.c turns
exactly the two parallel cross/register cases RED again.

Signed-off-by: Martin Vogel <[email protected]>
…vice_edge

The call now carries both the route mount and the caller language;
wrap its arguments the way clang-format lays them out so lint-format
passes. No code change.

Signed-off-by: Martin Vogel <[email protected]>
@DeusData
DeusData force-pushed the fix/issue-1186-vendor-types-v2 branch from 6e2c18b to d3d1a67 Compare October 12, 2026 20:37
Use tracked allocations for binding arrays and namespace keys, share
IMPORTS cleanup, and consolidate namespace construction failure cleanup.
Preserve libc ownership for existing FQN values and JSON writer buffers.
Remove the redundant condition in binding validation.

Signed-off-by: Martin Vogel <[email protected]>
collect_use_declaration dropped the group prefix of `use A\B\{C as D}`
and read the function/const keyword only from the declaration, so a
grouped alias mapped to the bare `C` with the declaration's kind. The
cross-file LSP then could not recognise a grouped vendor class import as
external, and a grouped `function`/`const` alias was registered as a
class. Prefix each grouped clause with the group namespace and take the
keyword from the clause when the declaration has none, mirroring
emit_php_use_decl in extract_imports.c.

Refs #1186

Signed-off-by: Martin Vogel <[email protected]>
When a cross-file lsp_cross_crate or php_external_receiver row
supersedes the per-file records of its exact parser occurrence, every
matching record was rewritten in place. Two earlier records at one
occurrence therefore became two identical rows, which the dedup table
only indexes and never collapses, so a single call site carried a
duplicated resolution. Rewrite the first matching record and drop the
others at that occurrence; records at other occurrences are untouched.

Refs #1186

Signed-off-by: Martin Vogel <[email protected]>
Two gaps let a call on a vendor class escape the #1186 evidence:

- A vendor class the PHP LSP knows from its seeded library data (for
  example GuzzleHttp.Client) resolved as php_static_resolved or
  php_method_typed. Its target has no graph node either, but without the
  php_external_receiver strategy the pipeline fell through to a
  name-only registry binding. Methods found on a class reached through a
  vendor `use` now carry php_external_receiver as well.

- A vendor call skips the registry, and the extractor names a scoped
  call by its method alone (only a literal `Route::` scope is kept), so
  `GuzzleClient::get('/x')` reached the route-verb fallback and became a
  route registration instead of an HTTP call. When the callee text names
  no service, the service patterns now also see the vendor method the
  LSP proved (`GuzzleHttp.Client.get`, `Kafka.Producer.publish`), in
  both the sequential and the parallel pass.

Refs #1186

Signed-off-by: Martin Vogel <[email protected]>
The vendor-types test never got past its extraction precondition, so
two of its assertions had never run against production:

- The precondition looked for the import alias in the callee text of
  `GuzzleClient::get` and `KafkaProducer::publish`. The extractor names
  a scoped call by its method alone and keeps only a literal `Route::`
  scope (#952). The aliased scope is now proven on the occurrence text
  at the call's site span, which identifies the same call just as
  strictly.

- The sequential/parallel parity fingerprint hashed the HTTP_CALLS and
  ASYNC_CALLS properties whole. The parallel emitter appends the call's
  "args" member last; the sequential pass leaves route/service
  properties without it by design (calls_emit_edge). Those two edge
  types are now compared with only that trailing member removed; their
  type, endpoints and every other property must still agree.

Refs #1186

Signed-off-by: Martin Vogel <[email protected]>
The parallel pass appends the call's ,"args":[...] member to every
HTTP_CALLS and ASYNC_CALLS edge (finalize_and_emit). The sequential pass
appended args to CALLS only, so the same service call produced
different edge properties depending on which pass indexed the repo, and
create_data_flows had no caller_args for small (sequential) repos.

Split the args-and-closing-brace step out of finalize_and_emit as
cbm_pipeline_close_call_props and let the sequential
emit_http_async_edge build its props in the same CBM_SZ_2K buffer and
finish them with that function. Field order, expression sanitizing,
escaping and truncation are therefore one implementation; CALLS edges
and their "line" member are unchanged in both passes.

Refs #1186

Signed-off-by: Martin Vogel <[email protected]>
Both passes now emit byte-identical HTTP_CALLS and ASYNC_CALLS
properties, so the vendor-types parity fingerprint hashes them whole
again instead of trimming the "args" member. The precondition change
from the previous fixture correction stays.

Refs #1186

Signed-off-by: Martin Vogel <[email protected]>
@DeusData
DeusData force-pushed the fix/issue-1186-vendor-types-v2 branch from d3d1a67 to 87a815a Compare October 12, 2026 22:13
@DeusData
DeusData merged commit 4d26a25 into main Oct 12, 2026
15 of 17 checks passed
thugrock7 pushed a commit to thugrock7/codebase-memory-mcp that referenced this pull request Oct 19, 2026
A PHP file's import evidence is written as complete only when every `use`
either resolved or is proven absent. A global-namespace class use such as
`use Exception;` had no absence proof, so one such line left the whole
file's bindings unknown, and its project imports reached the PHP LSP as
File QNs again (the DeusData#2520 path).

An unaliased global class use is now proven absent when no PHP file
declares a class-like symbol of that name (Class, Interface, Trait, Enum,
File or Module; namespaced ones included, so the proof stays
conservative). Aliased global uses keep the old, unknown verdict.

Signed-off-by: Martin Vogel <[email protected]>
DeusData pushed a commit that referenced this pull request Oct 20, 2026
When the PHP LSP knows a receiver's class but the class is not in the
index, it emits php_method_typed_unindexed at 0.55, below the LSP
confidence floor. The resolver skipped that row and the weak short-name
fallback bound the call to a same-named project method: `$file->getUid()`
on a vendor File became a CALLS edge to App\Item::getUid.

#2520 turns this row into php_external_receiver when a vendor `use`
proves the class external, but two cases still reach the fallback. A
global function import (`use function sprintf;`) leaves the file's import
bindings unproven, since #2705 proves only class imports absent. And a
vendor type that arrives through a return type
(`$repository->findFile()->getUid()`) has no `use` in the calling file
at all.

Both resolvers now drop a unique_name or suffix_match edge when the LSP
row names a receiver class that no project class-like symbol shares by
simple name. A project class with that name keeps the fallback, because
the PHP LSP does not follow every cross-file type.

The test pair covers both cases, through pass_calls.c and pass_parallel.c.
Each case is RED on main on its own (vendorCall, chainedVendorCall);
calls on a project-typed receiver and on $this still resolve. On a
211-file TYPO3 project PHP CALLS edges go from 1083 to 1059
(-18 suffix_match, -6 unique_name; typed php_* edges unchanged): 25
removed, and 1 that a removed edge on the same caller had absorbed.

Signed-off-by: Wolfgang Klinger <[email protected]>
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.

1 participant