Files
codebase-memory-mcp/tests/test_php_lsp.c
T
Martin Vogel 9adada0185 fix(php): index trait methods by receiver while flattening (#1265)
flatten_trait_into_class looked up each trait's methods by scanning every
function of the registry, and every flattened method appends more of them.
A file with many trait-using classes paid O(classes x functions), and the
scan grew with its own output.

The class-field pass now keeps a receiver -> function-id index, synced
incrementally as functions are registered, and flattening reads only the
trait's own entries registered before the current class. If the index
cannot be built, the previous full scan runs unchanged. Only entries
registered before flattening started are visited, so the output is
identical.

Tests: the work-counter tests for #1265 (100 -> 200 trait-using classes,
ratio gate 2.5) and #765 (trait collision shapes stay bounded) use the
existing test-only work ceiling.

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-10-19 04:39:09 +02:00

6136 lines
211 KiB
C

/*
* test_php_lsp.c — Tests for PHP Light Semantic Pass.
*
* Coverage focus matches docs/PLAN_PHP_LSP_INTEGRATION.md §9.1:
* - Param type binding (incl. arrow-function and anonymous closures)
* - Method call dispatch on typed receivers
* - Static call dispatch (Class::m, self::, parent::, static::)
* - new Class chains
* - Namespace + `use` clause resolution
* - Catch-clause exception type binding
* - PHPDoc @param / @var fallback
* - Magic-method (__call, __callStatic) emission
* - "Unindexed receiver" fallback (vendor types)
*
* Specific regression: (Prompt $p) => $p->value() must NOT route to a
* global helpers.value function — see docs/PHP_LSP_PRE_FLIGHT.md §4.2.
*/
#include "test_framework.h"
#include "cbm.h"
#include "../src/pipeline/lsp_resolve.h"
#include "lsp/php_lsp.h"
#include "lsp/lsp_work.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
/* ── Helpers ───────────────────────────────────────────────────── */
static CBMFileResult *extract_php(const char *source) {
return cbm_extract_file(source, (int)strlen(source), CBM_LANG_PHP, "test", "main.php", 0, NULL,
NULL);
}
static int find_resolved(const CBMFileResult *r, const char *callerSub, const char *calleeSub) {
for (int i = 0; i < r->resolved_calls.count; i++) {
const CBMResolvedCall *rc = &r->resolved_calls.items[i];
if (rc->caller_qn && strstr(rc->caller_qn, callerSub) && rc->callee_qn &&
strstr(rc->callee_qn, calleeSub))
return i;
}
return -1;
}
static int require_resolved(const CBMFileResult *r, const char *callerSub, const char *calleeSub) {
int idx = find_resolved(r, callerSub, calleeSub);
if (idx < 0) {
printf(" MISSING resolved call: caller~%s -> callee~%s (have %d)\n", callerSub, calleeSub,
r->resolved_calls.count);
for (int i = 0; i < r->resolved_calls.count; i++) {
const CBMResolvedCall *rc = &r->resolved_calls.items[i];
printf(" %s -> %s [%s %.2f]\n", rc->caller_qn ? rc->caller_qn : "(null)",
rc->callee_qn ? rc->callee_qn : "(null)", rc->strategy ? rc->strategy : "(null)",
rc->confidence);
}
}
return idx;
}
static const CBMResolvedCall *find_resolved_with_strategy(const CBMFileResult *r,
const char *callerSub,
const char *calleeSub,
const char *strategy) {
for (int i = 0; i < r->resolved_calls.count; i++) {
const CBMResolvedCall *rc = &r->resolved_calls.items[i];
if (!rc->caller_qn || !rc->callee_qn) continue;
if (!strstr(rc->caller_qn, callerSub)) continue;
if (!strstr(rc->callee_qn, calleeSub)) continue;
if (strategy && (!rc->strategy || strcmp(rc->strategy, strategy) != 0)) continue;
return rc;
}
return NULL;
}
/* ── 1. Local class, method dispatch via $x = new C() ──────────── */
TEST(phplsp_local_method_via_new_assignment) {
const char *src =
"<?php\n"
"class Greeter {\n"
" public function hello(): string { return 'hi'; }\n"
"}\n"
"class Caller {\n"
" public function go(): void {\n"
" $g = new Greeter();\n"
" $g->hello();\n"
" }\n"
"}\n";
CBMFileResult *r = extract_php(src);
ASSERT(r);
int idx = require_resolved(r, "Caller.go", "Greeter.hello");
ASSERT(idx >= 0);
cbm_free_result(r);
PASS();
}
/* ── 2. Local class, method dispatch via typed parameter ───────── */
TEST(phplsp_local_method_via_typed_param) {
const char *src =
"<?php\n"
"class P { public function value(): string { return 'x'; } }\n"
"class C {\n"
" public function run(P $p): void { $p->value(); }\n"
"}\n";
CBMFileResult *r = extract_php(src);
ASSERT(r);
int idx = require_resolved(r, "C.run", "P.value");
ASSERT(idx >= 0);
cbm_free_result(r);
PASS();
}
/* ── 3. Arrow function with typed parameter ───────────────────── */
TEST(phplsp_arrow_function_typed_param) {
const char *src =
"<?php\n"
"class P { public function value(): string { return 'x'; } }\n"
"class C {\n"
" public function run(): void {\n"
" $f = fn (P $p) => $p->value();\n"
" }\n"
"}\n";
CBMFileResult *r = extract_php(src);
ASSERT(r);
int idx = require_resolved(r, "C.run", "P.value");
ASSERT(idx >= 0);
cbm_free_result(r);
PASS();
}
/* ── 4. Static call ─────────────────────────────────────────────── */
TEST(phplsp_static_call_resolved) {
const char *src =
"<?php\n"
"class Util { public static function fmt(string $s): string { return $s; } }\n"
"class C {\n"
" public function run(): void { Util::fmt('hi'); }\n"
"}\n";
CBMFileResult *r = extract_php(src);
ASSERT(r);
int idx = require_resolved(r, "C.run", "Util.fmt");
ASSERT(idx >= 0);
cbm_free_result(r);
PASS();
}
/* ── 5. self:: and parent:: dispatch ──────────────────────────── */
TEST(phplsp_self_and_parent) {
const char *src =
"<?php\n"
"class Base { public function tag(): string { return 'b'; } }\n"
"class Child extends Base {\n"
" public function alt(): string { return 'c'; }\n"
" public function go(): void {\n"
" self::alt();\n"
" parent::tag();\n"
" }\n"
"}\n";
CBMFileResult *r = extract_php(src);
ASSERT(r);
ASSERT(require_resolved(r, "Child.go", "Child.alt") >= 0);
ASSERT(require_resolved(r, "Child.go", "Base.tag") >= 0);
cbm_free_result(r);
PASS();
}
/* ── 6. Function call with `use function` map ─────────────────── */
TEST(phplsp_use_function_clause) {
/* The aliased function isn't defined in this file; we just verify we
* don't crash and don't misroute. Resolved-call set is allowed to be
* empty for this case. */
const char *src =
"<?php\n"
"namespace A;\n"
"use function B\\helper;\n"
"function caller(): void { helper(); }\n";
CBMFileResult *r = extract_php(src);
ASSERT(r);
cbm_free_result(r);
PASS();
}
/* ── 7. PHPDoc @var binding ───────────────────────────────────── */
TEST(phplsp_phpdoc_var) {
const char *src =
"<?php\n"
"class Q { public function tap(): string { return 'q'; } }\n"
"class C {\n"
" public function run($x): void {\n"
" /** @var Q $x */\n"
" $x->tap();\n"
" }\n"
"}\n";
CBMFileResult *r = extract_php(src);
ASSERT(r);
int idx = require_resolved(r, "C.run", "Q.tap");
ASSERT(idx >= 0);
cbm_free_result(r);
PASS();
}
/* ── 8. Catch-clause typed binding ────────────────────────────── */
TEST(phplsp_catch_binding) {
const char *src =
"<?php\n"
"class MyExc { public function name(): string { return 'x'; } }\n"
"class C {\n"
" public function run(): void {\n"
" try { /* nop */ } catch (MyExc $e) { $e->name(); }\n"
" }\n"
"}\n";
CBMFileResult *r = extract_php(src);
ASSERT(r);
int idx = require_resolved(r, "C.run", "MyExc.name");
ASSERT(idx >= 0);
cbm_free_result(r);
PASS();
}
/* ── 9. $this-bound method dispatch ───────────────────────────── */
TEST(phplsp_this_method) {
const char *src =
"<?php\n"
"class C {\n"
" public function helper(): string { return 'h'; }\n"
" public function go(): void { $this->helper(); }\n"
"}\n";
CBMFileResult *r = extract_php(src);
ASSERT(r);
int idx = require_resolved(r, "C.go", "C.helper");
ASSERT(idx >= 0);
cbm_free_result(r);
PASS();
}
/* ── 10. Inheritance — method resolved via parent ──────────────── *
*
* Phase 1 limitation: when the receiver type is in-registry but the method
* lives on a parent class, the resolver currently emits the unindexed
* heuristic for the receiver class instead of walking the parent chain.
* This is a known gap tracked for Phase 2 — the unified extractor's
* d->parent_class string does not always match the registered type QN
* because of namespace stripping, so the receiver_type lookup misses.
* For the smoke-test pattern the override still suppresses the wrong
* helper edge, which is the value-correctness goal.
*/
TEST(phplsp_method_inherited_via_parent) {
const char *src =
"<?php\n"
"class Base { public function tag(): string { return 'b'; } }\n"
"class Mid extends Base {}\n"
"class C {\n"
" public function run(Mid $m): void { $m->tag(); }\n"
"}\n";
CBMFileResult *r = extract_php(src);
ASSERT(r);
/* Either resolved Base.tag OR emitted unindexed Mid.tag is acceptable —
* both block the misroute. */
int direct = find_resolved(r, "C.run", "Base.tag");
int unindexed_mid = find_resolved(r, "C.run", "Mid.tag");
ASSERT(direct >= 0 || unindexed_mid >= 0);
cbm_free_result(r);
PASS();
}
/* ── 11. Vendor / unindexed receiver — emit unindexed strategy ── */
TEST(phplsp_unindexed_receiver_emits_block) {
const char *src =
"<?php\n"
"namespace App;\n"
"use Vendor\\NotIndexed\\Foo;\n"
"class C {\n"
" public function run(Foo $x): void { $x->value(); }\n"
"}\n";
CBMFileResult *r = extract_php(src);
ASSERT(r);
/* The synthetic entry must exist so the pipeline bridge can suppress
* the unified extractor's name-based misroute. */
const CBMResolvedCall *rc =
find_resolved_with_strategy(r, "C.run", "Foo.value", "php_method_typed_unindexed");
if (!rc) {
printf(" expected php_method_typed_unindexed for Foo.value, got %d entries\n",
r->resolved_calls.count);
for (int i = 0; i < r->resolved_calls.count; i++) {
const CBMResolvedCall *rcd = &r->resolved_calls.items[i];
printf(" %s -> %s [%s %.2f]\n", rcd->caller_qn ? rcd->caller_qn : "(null)",
rcd->callee_qn ? rcd->callee_qn : "(null)",
rcd->strategy ? rcd->strategy : "(null)", rcd->confidence);
}
}
ASSERT(rc != NULL);
cbm_free_result(r);
PASS();
}
/* ── 12. Specific regression from PHP_LSP_PRE_FLIGHT §4.2 ──────── */
TEST(phplsp_regression_prompt_value_not_routed_to_helper) {
/* Mirrors the pattern from
* src/Illuminate/Console/Concerns/ConfiguresPrompts.php:34
* fn (Prompt $prompt) => $this->validatePrompt($prompt->value(), ...)
*
* With Prompt being a vendor type AND a global "value" function in the
* same project, the unified extractor misroutes $prompt->value() to
* the global helper. The LSP's php_method_typed_unindexed strategy
* exists to block this misroute downstream. */
const char *src =
"<?php\n"
"namespace App;\n"
"use Vendor\\Prompt;\n"
"class C {\n"
" public function configure(): void {\n"
" Bus::run(fn (Prompt $prompt) => $prompt->value());\n"
" }\n"
"}\n"
"class Bus { public static function run(callable $f): void {} }\n"
"function value($x) { return $x; }\n";
CBMFileResult *r = extract_php(src);
ASSERT(r);
const CBMResolvedCall *rc = find_resolved_with_strategy(r, "C.configure", "Prompt.value",
"php_method_typed_unindexed");
if (!rc) {
printf(" expected unindexed Prompt.value for ConfiguresPrompts pattern, got %d\n",
r->resolved_calls.count);
for (int i = 0; i < r->resolved_calls.count; i++) {
const CBMResolvedCall *rcd = &r->resolved_calls.items[i];
printf(" %s -> %s [%s %.2f]\n", rcd->caller_qn ? rcd->caller_qn : "(null)",
rcd->callee_qn ? rcd->callee_qn : "(null)",
rcd->strategy ? rcd->strategy : "(null)", rcd->confidence);
}
}
ASSERT(rc != NULL);
cbm_free_result(r);
PASS();
}
/* ── 13. Method chaining — type from earlier method's return ──── */
TEST(phplsp_method_chain_return_type) {
const char *src =
"<?php\n"
"class B { public function tap(): string { return 'b'; } }\n"
"class A { public function next(): B { return new B(); } }\n"
"class C {\n"
" public function run(A $a): void { $a->next()->tap(); }\n"
"}\n";
CBMFileResult *r = extract_php(src);
ASSERT(r);
/* First link: A.next */
ASSERT(require_resolved(r, "C.run", "A.next") >= 0);
/* Second link via return type */
ASSERT(require_resolved(r, "C.run", "B.tap") >= 0);
cbm_free_result(r);
PASS();
}
/* ── 14. Function global fallback (bare value() in same file) ─── */
TEST(phplsp_function_global_fallback) {
const char *src =
"<?php\n"
"function doit($x) { return $x; }\n"
"class C { public function go(): void { doit(1); } }\n";
CBMFileResult *r = extract_php(src);
ASSERT(r);
/* Either php_function_namespaced or php_function_global_fallback. */
ASSERT(find_resolved(r, "C.go", "doit") >= 0);
cbm_free_result(r);
PASS();
}
/* ── 15. namespace + use class alias ──────────────────────────── */
TEST(phplsp_use_alias_resolves_for_new) {
/* Alias points at a vendor type. Phase 1 may or may not bind the
* variable type from `new R()` depending on AST shape; the smoke
* confirmation is only that we don't crash. The Laravel benchmark
* exercises the same path with typed parameters (Test #12), which
* is the production-load-bearing case. */
const char *src =
"<?php\n"
"namespace App;\n"
"use Vendor\\Real as R;\n"
"class C {\n"
" public function run(): void { $r = new R(); $r->go(); }\n"
"}\n";
CBMFileResult *r = extract_php(src);
ASSERT(r);
cbm_free_result(r);
PASS();
}
/* ── 16. Constructor property promotion (Phase 4b) ─────────────── */
TEST(phplsp_constructor_property_promotion) {
const char *src =
"<?php\n"
"class P { public function value(): string { return 'p'; } }\n"
"class C {\n"
" public function __construct(public P $p) {}\n"
" public function run(): void { $this->p->value(); }\n"
"}\n";
CBMFileResult *r = extract_php(src);
ASSERT(r);
int idx = require_resolved(r, "C.run", "P.value");
ASSERT(idx >= 0);
cbm_free_result(r);
PASS();
}
/* ── 17. Typed property declaration ────────────────────────────── */
TEST(phplsp_typed_property_declaration) {
const char *src =
"<?php\n"
"class B { public function tap(): string { return 'b'; } }\n"
"class C {\n"
" private B $bar;\n"
" public function __construct(B $b) { $this->bar = $b; }\n"
" public function run(): void { $this->bar->tap(); }\n"
"}\n";
CBMFileResult *r = extract_php(src);
ASSERT(r);
ASSERT(require_resolved(r, "C.run", "B.tap") >= 0);
cbm_free_result(r);
PASS();
}
/* ── 18. Constructor-body inference (no typed declaration) ─────── */
TEST(phplsp_constructor_body_inference) {
const char *src =
"<?php\n"
"class W { public function ping(): string { return 'w'; } }\n"
"class C {\n"
" private $w;\n"
" public function __construct(W $w) { $this->w = $w; }\n"
" public function run(): void { $this->w->ping(); }\n"
"}\n";
CBMFileResult *r = extract_php(src);
ASSERT(r);
ASSERT(require_resolved(r, "C.run", "W.ping") >= 0);
cbm_free_result(r);
PASS();
}
/* ── 19. Type narrowing — instanceof ───────────────────────────── */
TEST(phplsp_narrow_instanceof) {
const char *src =
"<?php\n"
"class Foo { public function bar(): int { return 1; } }\n"
"class C {\n"
" public function run($x): void {\n"
" if ($x instanceof Foo) { $x->bar(); }\n"
" }\n"
"}\n";
CBMFileResult *r = extract_php(src);
ASSERT(r);
ASSERT(require_resolved(r, "C.run", "Foo.bar") >= 0);
cbm_free_result(r);
PASS();
}
/* ── 20. Type narrowing — assert(instanceof) ───────────────────── */
TEST(phplsp_narrow_assert_instanceof) {
const char *src =
"<?php\n"
"class Foo { public function bar(): int { return 1; } }\n"
"class C {\n"
" public function run($x): void {\n"
" assert($x instanceof Foo);\n"
" $x->bar();\n"
" }\n"
"}\n";
CBMFileResult *r = extract_php(src);
ASSERT(r);
ASSERT(require_resolved(r, "C.run", "Foo.bar") >= 0);
cbm_free_result(r);
PASS();
}
/* ── 21. PHPDoc @property tag binds field ──────────────────────── */
TEST(phplsp_phpdoc_property_class_tag) {
const char *src =
"<?php\n"
"class Q { public function tap(): string { return 'q'; } }\n"
"/**\n"
" * @property Q $q\n"
" */\n"
"class Model {\n"
" public function go(): void { $this->q->tap(); }\n"
"}\n";
CBMFileResult *r = extract_php(src);
ASSERT(r);
ASSERT(require_resolved(r, "Model.go", "Q.tap") >= 0);
cbm_free_result(r);
PASS();
}
/* ── 22. PHPDoc @method tag registers virtual method ───────────── */
TEST(phplsp_phpdoc_method_class_tag) {
const char *src =
"<?php\n"
"class Z { public function tap(): string { return 'z'; } }\n"
"/**\n"
" * @method Z fooBar()\n"
" */\n"
"class Model {\n"
" public function go(): void { $this->fooBar()->tap(); }\n"
"}\n";
CBMFileResult *r = extract_php(src);
ASSERT(r);
/* fooBar resolves on Model (the @method virtual), and chains to Z.tap. */
ASSERT(require_resolved(r, "Model.go", "Model.fooBar") >= 0);
ASSERT(require_resolved(r, "Model.go", "Z.tap") >= 0);
cbm_free_result(r);
PASS();
}
/* ── 23. Trait flattening — methods from used trait ────────────── */
TEST(phplsp_trait_method_flattened) {
const char *src =
"<?php\n"
"trait T { public function shared(): string { return 't'; } }\n"
"class C {\n"
" use T;\n"
" public function run(): void { $this->shared(); }\n"
"}\n";
CBMFileResult *r = extract_php(src);
ASSERT(r);
ASSERT(require_resolved(r, "C.run", "C.shared") >= 0 ||
require_resolved(r, "C.run", "T.shared") >= 0);
cbm_free_result(r);
PASS();
}
/* ── 23b. Regression: a trait that uses itself must terminate ─────
*
* `trait T { use T; ... }` — directly, or via an alias that resolves back
* to T by short name (`use Other\T as A; use A;`) — previously sent
* flatten_trait_into_class() into an unbounded loop: each copied method
* re-matched the loop filter while cbm_registry_add_func() grew the
* registry, exhausting all memory (40 GB+, freezing the host). The fix
* short-circuits self-flattening and snapshots the loop bound. The
* assertion is simply that extraction returns: pre-fix this never
* completed, and under the ASan test build a regression would surface as
* an OOM/abort here rather than a silent hang. */
TEST(phplsp_trait_self_use_terminates) {
const char *src =
"<?php\n"
"namespace App;\n"
"trait EnumTrait {\n"
" use EnumTrait;\n"
" public function getRandom(): int { return 1; }\n"
"}\n";
CBMFileResult *r = extract_php(src);
ASSERT(r);
cbm_free_result(r);
PASS();
}
/* ── 23c. Regression #765: trait name collides with an aliased import ──
*
* `use Vendor\Pkg\Auditable as AuditableBase;` + `trait Auditable { use
* AuditableBase; ... }` — the composed alias canonicalizes (via short-name
* fallback) onto the ENCLOSING trait itself, which is the same
* self-flattening loop as 23b arriving through the import alias: each
* copied method re-matched the flatten filter while the registry grew,
* OOM-ing the indexer (reported at 40+ GB). Guarded by the #920 pair
* (self-flatten short-circuit + snapshotted loop bound); this fixture is
* the exact reproducer from issue #765 and hung/OOM'd before those. */
TEST(phplsp_trait_aliased_import_name_collision_terminates) {
const char *src = "<?php\n"
"namespace App\\Demo;\n"
"\n"
"use Vendor\\Pkg\\Auditable as AuditableBase;\n"
"\n"
"trait Auditable {\n"
" use AuditableBase;\n"
"\n"
" function f() { $x = 1; return $x; }\n"
"}\n";
CBMFileResult *r = extract_php(src);
ASSERT(r);
cbm_free_result(r);
PASS();
}
/* ── 23d. Regression #951: class shares its short name with an aliased
* trait import ──
*
* `use App\Functions\Delivery as Delivery_Trait;` + `class Delivery { use
* Delivery_Trait; ... }` — resolving the composed alias falls back to the
* short name and lands on the LOCAL CLASS `Delivery`, so the class is
* flattened into itself (class_qn == canonical target QN): the same loop
* as 23b/23c reached through a class/trait short-name collision. Reported
* as an indexer OOM kill (exit 137) in issue #951. The two-file original
* reduces to this single-file form because the trait's definition being
* unavailable is exactly what forces the short-name fallback. */
TEST(phplsp_class_aliased_trait_name_collision_terminates) {
const char *src = "<?php\n"
"namespace App\\Modules\\Foo;\n"
"\n"
"use App\\Functions\\Delivery as Delivery_Trait;\n"
"\n"
"class Delivery {\n"
" use Delivery_Trait;\n"
"\n"
" public function run(): void {\n"
" }\n"
"}\n";
CBMFileResult *r = extract_php(src);
ASSERT(r);
cbm_free_result(r);
PASS();
}
/* ── 23e. #765 / #1265: trait flattening work is counted, bounded, linear ──
*
* The guards above prove only that extraction returns; a regression would hang
* the runner instead of failing it. These tests count the LSP work
* (CBM_LSP_WORK, which flatten_trait_into_class adds to once per registry
* function it visits) under a test-only ceiling (lsp_work.h): if a flatten
* bound is reverted, the runaway loop stops at the ceiling and the test is RED
* deterministically. No clock is read (O9). */
enum { PHPLSP_FLATTEN_CEILING = 50000000 };
static uint64_t phplsp_counted_work(const char *src, int *ceiling_hit, CBMFileResult **out) {
(void)cbm_lsp_work_take();
cbm_lsp_work_ceiling = PHPLSP_FLATTEN_CEILING;
CBMFileResult *r = extract_php(src);
uint64_t work = cbm_lsp_work_take();
cbm_lsp_work_ceiling = 0;
*ceiling_hit = work > PHPLSP_FLATTEN_CEILING;
if (out) {
*out = r;
} else if (r) {
cbm_free_result(r);
}
return work;
}
/* Every reported collision shape, in one file: #765's aliased import that
* shares the trait's name, the v0.9.0 variant where the alias TARGET's short
* name equals the local trait (owen-it/laravel-auditing wrapper), #951's
* class named like its aliased trait, a self-using trait and a mutual pair. A
* model using the wrapper trait still sees the flattened method. */
TEST(phplsp_trait_collision_shapes_bounded_work_issue765) {
const char *src = "<?php\n"
"namespace App\\Models\\Traits;\n"
"\n"
"use OwenIt\\Auditing\\Auditable as AuditableBase;\n"
"use App\\Functions\\Delivery as Delivery_Trait;\n"
"\n"
"trait Auditable {\n"
" use AuditableBase;\n"
" public function auditTag(): string { $x = 'a'; return $x; }\n"
"}\n"
"trait Selfish {\n"
" use Selfish;\n"
" public function me(): int { return 1; }\n"
"}\n"
"trait Ping {\n"
" use Pong;\n"
" public function ping(): int { return 1; }\n"
"}\n"
"trait Pong {\n"
" use Ping;\n"
" public function pong(): int { return 2; }\n"
"}\n"
"class Delivery {\n"
" use Delivery_Trait;\n"
" public function run(): void { }\n"
"}\n"
"class User {\n"
" use Auditable, Ping;\n"
" public function label(): string { return $this->auditTag(); }\n"
" public function both(): int { return $this->ping() + $this->pong(); }\n"
"}\n";
int hit = 0;
CBMFileResult *r = NULL;
uint64_t work = phplsp_counted_work(src, &hit, &r);
printf(" collision shapes: work=%llu\n", (unsigned long long)work);
ASSERT_NOT_NULL(r);
ASSERT_FALSE(hit);
/* The flattened trait method is still found through the using class. */
ASSERT(find_resolved(r, "label", "auditTag") >= 0);
cbm_free_result(r);
PASS();
}
/* n classes in one file, each composing the same two traits, and one probe
* class calling a flattened method through $this. */
static char *phplsp_trait_users_source(int n) {
size_t sz = (size_t)n * 256 + 1024;
char *src = malloc(sz);
if (!src) {
return NULL;
}
size_t pos = (size_t)snprintf(src, sz,
"<?php\nnamespace App\\Bulk;\n\n"
"trait Stamps {\n"
" public function stamp(): int { return 1; }\n"
" public function touch(): int { return 2; }\n"
" public function fresh(): int { return 3; }\n"
"}\n"
"trait Flags {\n"
" use Stamps;\n"
" public function flag(): int { return 4; }\n"
"}\n");
for (int i = 0; i < n; i++) {
pos += (size_t)snprintf(src + pos, sz - pos,
"class M%d {\n"
" use Flags;\n"
" public function own%d(): int { return %d; }\n"
"}\n",
i, i, i);
}
/* One call through a flattened method, so the lookup cost stays constant
* and the gate measures flattening (F12: the single-file registry is not
* finalized, so every lookup is a linear scan; that is its own defect). */
snprintf(src + pos, sz - pos,
"class Probe {\n"
" use Flags;\n"
" public function probe(): int { return $this->stamp(); }\n"
"}\n");
return src;
}
/* Deterministic complexity gate: twice the trait-using classes must cost about
* twice the counted work. Each flatten visits the registry; if that visit grows
* with the classes already flattened, the ratio drifts toward 4 (main before the
* per-trait method index: 3.22 at 150 -> 300). Both sizes stay below the 256
* top-level declarations the class-field walk visits (#2388), so every class
* is flattened. */
TEST(phplsp_trait_flatten_work_is_linear_issue1265) {
enum { N1 = 100, N2 = 200 };
char *s1 = phplsp_trait_users_source(N1);
char *s2 = phplsp_trait_users_source(N2);
ASSERT_NOT_NULL(s1);
ASSERT_NOT_NULL(s2);
int hit1 = 0;
int hit2 = 0;
CBMFileResult *r2 = NULL;
uint64_t w1 = phplsp_counted_work(s1, &hit1, NULL);
uint64_t w2 = phplsp_counted_work(s2, &hit2, &r2);
free(s1);
free(s2);
printf(" trait users %d -> %d: work %llu -> %llu (ratio %.2f)\n", N1, N2,
(unsigned long long)w1, (unsigned long long)w2, w1 ? (double)w2 / (double)w1 : 0.0);
ASSERT_NOT_NULL(r2);
ASSERT_FALSE(hit1);
ASSERT_FALSE(hit2);
ASSERT(w1 > 0);
/* The flattened method still resolves through the probe class. */
ASSERT(find_resolved(r2, "Probe.probe", "stamp") >= 0);
cbm_free_result(r2);
ASSERT(w2 * 10 <= w1 * 25); /* ratio <= 2.5 */
PASS();
}
/* F12: the single-file PHP LSP (cbm_run_php_lsp) never indexed its registry,
* so every type and method lookup was a linear scan over the stdlib plus every
* definition of the file. Each class here looks up the previous class and its
* method once: twice the classes must cost about twice the counted work; the
* linear scans drift toward four. */
static char *phplsp_chain_classes_source(int n) {
size_t sz = (size_t)n * 96 + 256;
char *src = malloc(sz);
if (!src) {
return NULL;
}
size_t pos = (size_t)snprintf(src, sz,
"<?php\nnamespace App\\Chain;\n\n"
"class C0 { public function m(): int { return 0; } }\n");
for (int i = 1; i < n; i++) {
pos += (size_t)snprintf(src + pos, sz - pos,
"class C%d { public function m(): int { return (new C%d())->m(); } }\n",
i, i - 1);
}
return src;
}
TEST(phplsp_single_file_lookup_work_is_linear_f12) {
enum { N1 = 1000, N2 = 2000 };
char *s1 = phplsp_chain_classes_source(N1);
char *s2 = phplsp_chain_classes_source(N2);
ASSERT_NOT_NULL(s1);
ASSERT_NOT_NULL(s2);
int hit1 = 0;
int hit2 = 0;
CBMFileResult *r2 = NULL;
uint64_t w1 = phplsp_counted_work(s1, &hit1, NULL);
uint64_t w2 = phplsp_counted_work(s2, &hit2, &r2);
free(s1);
free(s2);
printf(" chained classes %d -> %d: work %llu -> %llu (ratio %.2f)\n", N1, N2,
(unsigned long long)w1, (unsigned long long)w2, w1 ? (double)w2 / (double)w1 : 0.0);
ASSERT_NOT_NULL(r2);
ASSERT_FALSE(hit1);
ASSERT_FALSE(hit2);
ASSERT(w1 > 0);
/* The last class's call still resolves to the previous class's method. */
ASSERT(find_resolved(r2, "C1999.m", "C1998.m") >= 0);
cbm_free_result(r2);
ASSERT(w2 * 10 <= w1 * 25); /* ratio <= 2.5 */
PASS();
}
/* ── 24. Match expression result type ──────────────────────────── */
TEST(phplsp_match_result_type) {
const char *src =
"<?php\n"
"class A { public function go(): int { return 1; } }\n"
"class C {\n"
" public function run(int $i): void {\n"
" $a = match($i) { 1 => new A(), default => new A() };\n"
" $a->go();\n"
" }\n"
"}\n";
CBMFileResult *r = extract_php(src);
ASSERT(r);
ASSERT(require_resolved(r, "C.run", "A.go") >= 0);
cbm_free_result(r);
PASS();
}
/* ── 25. Ternary result type ───────────────────────────────────── */
TEST(phplsp_ternary_result_type) {
const char *src =
"<?php\n"
"class A { public function go(): int { return 1; } }\n"
"class C {\n"
" public function run(bool $b): void {\n"
" $a = $b ? new A() : new A();\n"
" $a->go();\n"
" }\n"
"}\n";
CBMFileResult *r = extract_php(src);
ASSERT(r);
ASSERT(require_resolved(r, "C.run", "A.go") >= 0);
cbm_free_result(r);
PASS();
}
/* ── 26. Method chain depth 3 ──────────────────────────────────── */
TEST(phplsp_method_chain_depth_three) {
const char *src =
"<?php\n"
"class C3 { public function tap(): string { return 'c3'; } }\n"
"class C2 { public function next(): C3 { return new C3(); } }\n"
"class C1 { public function next(): C2 { return new C2(); } }\n"
"class Caller {\n"
" public function run(C1 $c): void { $c->next()->next()->tap(); }\n"
"}\n";
CBMFileResult *r = extract_php(src);
ASSERT(r);
ASSERT(require_resolved(r, "Caller.run", "C1.next") >= 0);
ASSERT(require_resolved(r, "Caller.run", "C2.next") >= 0);
ASSERT(require_resolved(r, "Caller.run", "C3.tap") >= 0);
cbm_free_result(r);
PASS();
}
/* ── 27. Late static binding through 2-deep parent chain ───────── */
TEST(phplsp_lsb_two_deep_chain) {
const char *src =
"<?php\n"
"class GrandParent_ { public function tag(): string { return 'g'; } }\n"
"class Parent_ extends GrandParent_ {}\n"
"class Child extends Parent_ {}\n"
"class C {\n"
" public function run(Child $c): void { $c->tag(); }\n"
"}\n";
CBMFileResult *r = extract_php(src);
ASSERT(r);
/* Either resolves directly to GrandParent_.tag or emits unindexed for
* an intermediate — both block the misroute. */
ASSERT(find_resolved(r, "C.run", ".tag") >= 0);
cbm_free_result(r);
PASS();
}
/* ── 28. Nullsafe operator ─────────────────────────────────────── */
TEST(phplsp_nullsafe_operator) {
const char *src =
"<?php\n"
"class Foo { public function bar(): int { return 1; } }\n"
"class C {\n"
" public function run(?Foo $f): void { $f?->bar(); }\n"
"}\n";
CBMFileResult *r = extract_php(src);
ASSERT(r);
ASSERT(require_resolved(r, "C.run", "Foo.bar") >= 0);
cbm_free_result(r);
PASS();
}
/* ── 29. Static method through `static::` ─────────────────────── */
TEST(phplsp_static_keyword_dispatch) {
const char *src =
"<?php\n"
"class C {\n"
" public static function make(): self { return new self(); }\n"
" public function go(): void { static::make(); }\n"
"}\n";
CBMFileResult *r = extract_php(src);
ASSERT(r);
ASSERT(require_resolved(r, "C.go", "C.make") >= 0);
cbm_free_result(r);
PASS();
}
/* ── 30. self:: from inside a method ──────────────────────────── */
TEST(phplsp_self_in_method_body) {
const char *src =
"<?php\n"
"class C {\n"
" public function helper(): int { return 1; }\n"
" public function go(): void { self::helper(); }\n"
"}\n";
CBMFileResult *r = extract_php(src);
ASSERT(r);
ASSERT(require_resolved(r, "C.go", "C.helper") >= 0);
cbm_free_result(r);
PASS();
}
/* ── 31. Variadic parameter doesn't crash binding ──────────────── */
TEST(phplsp_variadic_parameter) {
const char *src =
"<?php\n"
"class C {\n"
" public function run(string ...$args): void {}\n"
"}\n";
CBMFileResult *r = extract_php(src);
ASSERT(r);
cbm_free_result(r);
PASS();
}
/* ── 32. Closure with use() clause ─────────────────────────────── */
TEST(phplsp_closure_with_use) {
const char *src =
"<?php\n"
"class P { public function value(): string { return 'p'; } }\n"
"class C {\n"
" public function run(P $p): void {\n"
" $f = function () use ($p) { return $p->value(); };\n"
" }\n"
"}\n";
CBMFileResult *r = extract_php(src);
ASSERT(r);
/* Closure use-imports aren't tracked in Phase 1; allow either resolve
* or no resolve, but test must not crash. */
cbm_free_result(r);
PASS();
}
/* ── 33. Anonymous function as argument ────────────────────────── */
TEST(phplsp_anonymous_function_arg) {
const char *src =
"<?php\n"
"class C {\n"
" public function run(): void {\n"
" Bus::run(function () { return 1; });\n"
" }\n"
"}\n"
"class Bus { public static function run(callable $f): void {} }\n";
CBMFileResult *r = extract_php(src);
ASSERT(r);
ASSERT(require_resolved(r, "C.run", "Bus.run") >= 0);
cbm_free_result(r);
PASS();
}
/* ── 34. Arrow function inside arrow function ──────────────────── */
TEST(phplsp_nested_arrow_functions) {
const char *src =
"<?php\n"
"class A { public function go(): int { return 1; } }\n"
"class C {\n"
" public function run(): void {\n"
" $f = fn () => fn (A $a) => $a->go();\n"
" }\n"
"}\n";
CBMFileResult *r = extract_php(src);
ASSERT(r);
ASSERT(require_resolved(r, "C.run", "A.go") >= 0);
cbm_free_result(r);
PASS();
}
/* ── 35. is_string narrowing ───────────────────────────────────── */
TEST(phplsp_narrow_is_string) {
const char *src =
"<?php\n"
"class C {\n"
" public function run($x): void {\n"
" if (is_string($x)) {}\n"
" }\n"
"}\n";
CBMFileResult *r = extract_php(src);
ASSERT(r);
cbm_free_result(r);
PASS();
}
/* ── 36. is_array narrowing ───────────────────────────────────── */
TEST(phplsp_narrow_is_array) {
const char *src =
"<?php\n"
"class C {\n"
" public function run($x): void {\n"
" if (is_array($x)) { count($x); }\n"
" }\n"
"}\n";
CBMFileResult *r = extract_php(src);
ASSERT(r);
cbm_free_result(r);
PASS();
}
/* ── 37. Multiple use clauses, multiple classes used ───────────── */
TEST(phplsp_multiple_use_clauses) {
const char *src =
"<?php\n"
"namespace App;\n"
"use Vendor\\Foo;\n"
"use Vendor\\Bar;\n"
"class C {\n"
" public function run(Foo $f, Bar $b): void {\n"
" $f->go();\n"
" $b->stop();\n"
" }\n"
"}\n";
CBMFileResult *r = extract_php(src);
ASSERT(r);
/* Both should emit unindexed override entries. */
ASSERT(find_resolved(r, "C.run", "Vendor.Foo.go") >= 0);
ASSERT(find_resolved(r, "C.run", "Vendor.Bar.stop") >= 0);
cbm_free_result(r);
PASS();
}
/* ── 38. Group use clause ──────────────────────────────────────── */
TEST(phplsp_group_use_clause) {
const char *src =
"<?php\n"
"namespace App;\n"
"use Vendor\\{ Foo, Bar };\n"
"class C {\n"
" public function run(Foo $f): void { $f->go(); }\n"
"}\n";
CBMFileResult *r = extract_php(src);
ASSERT(r);
/* Group form may or may not be parsed by tree-sitter-php — accept. */
cbm_free_result(r);
PASS();
}
/* ── 39. Fully-qualified class name in `new \\Vendor\\Foo()` ───── */
TEST(phplsp_fqn_new_expression) {
const char *src =
"<?php\n"
"class C {\n"
" public function run(): void {\n"
" $f = new \\Vendor\\Foo();\n"
" $f->go();\n"
" }\n"
"}\n";
CBMFileResult *r = extract_php(src);
ASSERT(r);
ASSERT(find_resolved(r, "C.run", "Vendor.Foo.go") >= 0);
cbm_free_result(r);
PASS();
}
/* ── 40. Foreach loop variable not crashed ─────────────────────── */
TEST(phplsp_foreach_iteration) {
const char *src =
"<?php\n"
"class C {\n"
" public function run(array $xs): void {\n"
" foreach ($xs as $x) { /* nop */ }\n"
" }\n"
"}\n";
CBMFileResult *r = extract_php(src);
ASSERT(r);
cbm_free_result(r);
PASS();
}
/* ── 41. Try / catch with multiple exception types ─────────────── */
TEST(phplsp_catch_multi_type) {
const char *src =
"<?php\n"
"class A { public function go(): int { return 1; } }\n"
"class B { public function go(): int { return 2; } }\n"
"class C {\n"
" public function run(): void {\n"
" try { /* nop */ } catch (A | B $e) { $e->go(); }\n"
" }\n"
"}\n";
CBMFileResult *r = extract_php(src);
ASSERT(r);
/* Phase 1 takes leftmost — A.go is the expected resolution. */
ASSERT(find_resolved(r, "C.run", ".go") >= 0);
cbm_free_result(r);
PASS();
}
/* ── 42. Exception in catch with PHP namespace ─────────────────── */
TEST(phplsp_namespaced_catch) {
const char *src =
"<?php\n"
"namespace App\\Errors;\n"
"class MyExc { public function reason(): string { return 'r'; } }\n"
"class C {\n"
" public function run(): void {\n"
" try { /* nop */ } catch (MyExc $e) { $e->reason(); }\n"
" }\n"
"}\n";
CBMFileResult *r = extract_php(src);
ASSERT(r);
ASSERT(require_resolved(r, "C.run", "MyExc.reason") >= 0);
cbm_free_result(r);
PASS();
}
/* ── 43. Multiple interfaces implemented ──────────────────────── */
TEST(phplsp_implements_multiple_interfaces) {
const char *src =
"<?php\n"
"interface A { public function aa(): int; }\n"
"interface B { public function bb(): int; }\n"
"class C implements A, B {\n"
" public function aa(): int { return 1; }\n"
" public function bb(): int { return 2; }\n"
" public function go(): void { $this->aa(); $this->bb(); }\n"
"}\n";
CBMFileResult *r = extract_php(src);
ASSERT(r);
ASSERT(require_resolved(r, "C.go", "C.aa") >= 0);
ASSERT(require_resolved(r, "C.go", "C.bb") >= 0);
cbm_free_result(r);
PASS();
}
/* ── 44. Method on field access ($this->x->method()) ───────────── */
TEST(phplsp_method_on_typed_field) {
const char *src =
"<?php\n"
"class Inner { public function go(): int { return 1; } }\n"
"class C {\n"
" private Inner $i;\n"
" public function __construct(Inner $i) { $this->i = $i; }\n"
" public function run(): void { $this->i->go(); }\n"
"}\n";
CBMFileResult *r = extract_php(src);
ASSERT(r);
ASSERT(require_resolved(r, "C.run", "Inner.go") >= 0);
cbm_free_result(r);
PASS();
}
/* ── 45. Inheritance: child overrides parent method ─────────────── */
TEST(phplsp_child_overrides_parent_method) {
const char *src =
"<?php\n"
"class Base { public function speak(): string { return 'b'; } }\n"
"class Kid extends Base {\n"
" public function speak(): string { return 'k'; }\n"
"}\n"
"class C {\n"
" public function run(Kid $k): void { $k->speak(); }\n"
"}\n";
CBMFileResult *r = extract_php(src);
ASSERT(r);
/* Child override is preferred over parent. */
ASSERT(require_resolved(r, "C.run", "Kid.speak") >= 0);
cbm_free_result(r);
PASS();
}
/* ── 46. PHPDoc on method body — @param type override ─────────── */
TEST(phplsp_phpdoc_param_method) {
const char *src =
"<?php\n"
"class T { public function tap(): string { return 't'; } }\n"
"class C {\n"
" /**\n"
" * @param T $arg\n"
" */\n"
" public function run($arg): void { $arg->tap(); }\n"
"}\n";
CBMFileResult *r = extract_php(src);
ASSERT(r);
ASSERT(require_resolved(r, "C.run", "T.tap") >= 0);
cbm_free_result(r);
PASS();
}
/* ── 47. Static field access not crashed ─────────────────────── */
TEST(phplsp_static_field_access) {
const char *src =
"<?php\n"
"class Cfg { public static $value = 1; }\n"
"class C {\n"
" public function run(): void { $x = Cfg::$value; }\n"
"}\n";
CBMFileResult *r = extract_php(src);
ASSERT(r);
cbm_free_result(r);
PASS();
}
/* ── 48. Class constant access ────────────────────────────────── */
TEST(phplsp_class_constant_access) {
const char *src =
"<?php\n"
"class Cfg { const KEY = 'k'; }\n"
"class C {\n"
" public function run(): void { $x = Cfg::KEY; }\n"
"}\n";
CBMFileResult *r = extract_php(src);
ASSERT(r);
cbm_free_result(r);
PASS();
}
/* ── 49. Anonymous class doesn't crash ───────────────────────── */
TEST(phplsp_anonymous_class) {
const char *src =
"<?php\n"
"class C {\n"
" public function run(): object {\n"
" return new class { public function go(): int { return 1; } };\n"
" }\n"
"}\n";
CBMFileResult *r = extract_php(src);
ASSERT(r);
cbm_free_result(r);
PASS();
}
/* ── 50. Enum with methods ────────────────────────────────────── */
TEST(phplsp_enum_method_dispatch) {
const char *src =
"<?php\n"
"enum Suit: string {\n"
" case Hearts = 'h';\n"
" case Spades = 's';\n"
" public function label(): string { return 'x'; }\n"
"}\n"
"class C {\n"
" public function run(Suit $s): void { $s->label(); }\n"
"}\n";
CBMFileResult *r = extract_php(src);
ASSERT(r);
ASSERT(require_resolved(r, "C.run", "Suit.label") >= 0);
cbm_free_result(r);
PASS();
}
/* ── 51. Readonly property ───────────────────────────────────── */
TEST(phplsp_readonly_property) {
const char *src =
"<?php\n"
"class V { public function get(): int { return 1; } }\n"
"class C {\n"
" public readonly V $v;\n"
" public function __construct(V $v) { $this->v = $v; }\n"
" public function run(): void { $this->v->get(); }\n"
"}\n";
CBMFileResult *r = extract_php(src);
ASSERT(r);
ASSERT(require_resolved(r, "C.run", "V.get") >= 0);
cbm_free_result(r);
PASS();
}
/* ── 52. Static factory + chained methods ─────────────────────── */
TEST(phplsp_static_factory_chain) {
const char *src =
"<?php\n"
"class B { public function tap(): int { return 1; } }\n"
"class A {\n"
" public static function make(): A { return new self(); }\n"
" public function next(): B { return new B(); }\n"
"}\n"
"class C {\n"
" public function run(): void { A::make()->next()->tap(); }\n"
"}\n";
CBMFileResult *r = extract_php(src);
ASSERT(r);
ASSERT(require_resolved(r, "C.run", "A.make") >= 0);
ASSERT(require_resolved(r, "C.run", "A.next") >= 0);
ASSERT(require_resolved(r, "C.run", "B.tap") >= 0);
cbm_free_result(r);
PASS();
}
/* ── 53. Builtin DateTime stdlib ──────────────────────────────── */
TEST(phplsp_stdlib_datetime_chain) {
const char *src =
"<?php\n"
"class C {\n"
" public function run(\\DateTime $d): void { $d->modify('+1 day')->format('Y'); }\n"
"}\n";
CBMFileResult *r = extract_php(src);
ASSERT(r);
ASSERT(require_resolved(r, "C.run", "DateTime.modify") >= 0);
cbm_free_result(r);
PASS();
}
/* ── 54. Builtin Throwable hierarchy ─────────────────────────── */
TEST(phplsp_stdlib_throwable_chain) {
const char *src =
"<?php\n"
"class C {\n"
" public function run(): void {\n"
" try { /* nop */ } catch (\\RuntimeException $e) { $e->getMessage(); }\n"
" }\n"
"}\n";
CBMFileResult *r = extract_php(src);
ASSERT(r);
/* getMessage is on Throwable; RuntimeException -> Exception -> Throwable */
ASSERT(find_resolved(r, "C.run", "Throwable.getMessage") >= 0 ||
find_resolved(r, "C.run", "RuntimeException.getMessage") >= 0 ||
find_resolved(r, "C.run", "Exception.getMessage") >= 0);
cbm_free_result(r);
PASS();
}
/* ── 55. Iterator stdlib method dispatch ─────────────────────── */
TEST(phplsp_stdlib_iterator) {
const char *src =
"<?php\n"
"class C {\n"
" public function run(\\Iterator $it): void { $it->current(); }\n"
"}\n";
CBMFileResult *r = extract_php(src);
ASSERT(r);
ASSERT(require_resolved(r, "C.run", "Iterator.current") >= 0);
cbm_free_result(r);
PASS();
}
/* ── 56. Abstract class method ─────────────────────────────── */
TEST(phplsp_abstract_class_method) {
const char *src =
"<?php\n"
"abstract class Base { abstract public function go(): int; public function tap(): int { return 1; } }\n"
"class Kid extends Base { public function go(): int { return 2; } }\n"
"class C {\n"
" public function run(Kid $k): void { $k->tap(); $k->go(); }\n"
"}\n";
CBMFileResult *r = extract_php(src);
ASSERT(r);
ASSERT(find_resolved(r, "C.run", ".tap") >= 0);
ASSERT(require_resolved(r, "C.run", "Kid.go") >= 0);
cbm_free_result(r);
PASS();
}
/* ── 57. Static call via aliased namespace ─────────────────── */
TEST(phplsp_static_call_via_alias) {
const char *src =
"<?php\n"
"namespace App;\n"
"use Vendor\\Helper as H;\n"
"class C {\n"
" public function run(): void { H::go(); }\n"
"}\n";
CBMFileResult *r = extract_php(src);
ASSERT(r);
/* Vendor.Helper unindexed; emit unindexed entry. */
ASSERT(find_resolved(r, "C.run", "Vendor.Helper.go") >= 0);
cbm_free_result(r);
PASS();
}
/* ── 58. Function call with namespace ────────────────────── */
TEST(phplsp_function_call_namespaced) {
const char *src =
"<?php\n"
"namespace App;\n"
"function helper(): int { return 1; }\n"
"class C {\n"
" public function run(): void { helper(); }\n"
"}\n";
CBMFileResult *r = extract_php(src);
ASSERT(r);
/* Either path works. */
ASSERT(find_resolved(r, "C.run", ".helper") >= 0);
cbm_free_result(r);
PASS();
}
/* ── 59. Reassignment changes type ─────────────────────── */
TEST(phplsp_reassignment_changes_type) {
const char *src =
"<?php\n"
"class A { public function aa(): int { return 1; } }\n"
"class B { public function bb(): int { return 2; } }\n"
"class C {\n"
" public function run(): void {\n"
" $x = new A();\n"
" $x->aa();\n"
" $x = new B();\n"
" $x->bb();\n"
" }\n"
"}\n";
CBMFileResult *r = extract_php(src);
ASSERT(r);
ASSERT(require_resolved(r, "C.run", "A.aa") >= 0);
ASSERT(require_resolved(r, "C.run", "B.bb") >= 0);
cbm_free_result(r);
PASS();
}
/* ── 60. Chained `use` then `new` resolution ─────────────── */
TEST(phplsp_use_then_new_aliased) {
const char *src =
"<?php\n"
"namespace App;\n"
"use Vendor\\Real as R;\n"
"class C {\n"
" public function run(): void {\n"
" $r = new R();\n"
" $r->go();\n"
" }\n"
"}\n";
CBMFileResult *r = extract_php(src);
ASSERT(r);
/* $r should bind to NAMED(Vendor.Real); $r->go() should emit override. */
ASSERT(find_resolved(r, "C.run", "Vendor.Real.go") >= 0);
cbm_free_result(r);
PASS();
}
/* ── 61. Type narrowing — multiple branches ──────────────── */
TEST(phplsp_narrow_branches) {
const char *src =
"<?php\n"
"class A { public function aa(): int { return 1; } }\n"
"class B { public function bb(): int { return 2; } }\n"
"class C {\n"
" public function run($x): void {\n"
" if ($x instanceof A) { $x->aa(); }\n"
" if ($x instanceof B) { $x->bb(); }\n"
" }\n"
"}\n";
CBMFileResult *r = extract_php(src);
ASSERT(r);
ASSERT(require_resolved(r, "C.run", "A.aa") >= 0);
ASSERT(require_resolved(r, "C.run", "B.bb") >= 0);
cbm_free_result(r);
PASS();
}
/* ── 62. Sequential statements maintain bindings ─────────── */
TEST(phplsp_sequential_bindings) {
const char *src =
"<?php\n"
"class A { public function next(): A { return $this; } public function tap(): int { return 1; } }\n"
"class C {\n"
" public function run(): void {\n"
" $a = new A();\n"
" $b = $a->next();\n"
" $b->tap();\n"
" }\n"
"}\n";
CBMFileResult *r = extract_php(src);
ASSERT(r);
ASSERT(require_resolved(r, "C.run", "A.tap") >= 0);
cbm_free_result(r);
PASS();
}
/* ── 63. Chained calls with intermediate variable ────────── */
TEST(phplsp_chained_with_intermediate_var) {
const char *src =
"<?php\n"
"class B { public function tap(): int { return 1; } }\n"
"class A { public function next(): B { return new B(); } }\n"
"class C {\n"
" public function run(): void {\n"
" $a = new A();\n"
" $b = $a->next();\n"
" $b->tap();\n"
" }\n"
"}\n";
CBMFileResult *r = extract_php(src);
ASSERT(r);
ASSERT(require_resolved(r, "C.run", "A.next") >= 0);
ASSERT(require_resolved(r, "C.run", "B.tap") >= 0);
cbm_free_result(r);
PASS();
}
/* ── 64. Trait with multiple methods, all flattened ──────── */
TEST(phplsp_trait_multiple_methods) {
const char *src =
"<?php\n"
"trait T {\n"
" public function aa(): int { return 1; }\n"
" public function bb(): int { return 2; }\n"
"}\n"
"class C {\n"
" use T;\n"
" public function run(): void { $this->aa(); $this->bb(); }\n"
"}\n";
CBMFileResult *r = extract_php(src);
ASSERT(r);
ASSERT(find_resolved(r, "C.run", "C.aa") >= 0 || find_resolved(r, "C.run", "T.aa") >= 0);
ASSERT(find_resolved(r, "C.run", "C.bb") >= 0 || find_resolved(r, "C.run", "T.bb") >= 0);
cbm_free_result(r);
PASS();
}
/* ── 65. Cast expression ─────────────────────────────────── */
TEST(phplsp_cast_expression) {
const char *src =
"<?php\n"
"class C {\n"
" public function run($x): void {\n"
" $i = (int) $x;\n"
" }\n"
"}\n";
CBMFileResult *r = extract_php(src);
ASSERT(r);
cbm_free_result(r);
PASS();
}
/* ── 66. Clone returns same type ─────────────────────────── */
TEST(phplsp_clone_preserves_type) {
const char *src =
"<?php\n"
"class A { public function go(): int { return 1; } }\n"
"class C {\n"
" public function run(A $a): void {\n"
" $b = clone $a;\n"
" $b->go();\n"
" }\n"
"}\n";
CBMFileResult *r = extract_php(src);
ASSERT(r);
ASSERT(require_resolved(r, "C.run", "A.go") >= 0);
cbm_free_result(r);
PASS();
}
/* ── 67. Empty class body ────────────────────────────────── */
TEST(phplsp_empty_class_body) {
const char *src = "<?php\nclass Empty_ {}\n";
CBMFileResult *r = extract_php(src);
ASSERT(r);
cbm_free_result(r);
PASS();
}
/* ── 68. PSR LoggerInterface ────────────────────────────── */
TEST(phplsp_stdlib_psr_logger) {
const char *src =
"<?php\n"
"class C {\n"
" public function run(\\Psr\\Log\\LoggerInterface $log): void {\n"
" $log->info('hi');\n"
" }\n"
"}\n";
CBMFileResult *r = extract_php(src);
ASSERT(r);
ASSERT(require_resolved(r, "C.run", "LoggerInterface.info") >= 0);
cbm_free_result(r);
PASS();
}
/* ── 69. Long method chain doesn't overflow ─────────────── */
TEST(phplsp_long_method_chain) {
const char *src =
"<?php\n"
"class A { public function n(): A { return $this; } public function t(): int { return 1; } }\n"
"class C {\n"
" public function run(A $a): void { $a->n()->n()->n()->n()->n()->t(); }\n"
"}\n";
CBMFileResult *r = extract_php(src);
ASSERT(r);
ASSERT(find_resolved(r, "C.run", "A.t") >= 0);
cbm_free_result(r);
PASS();
}
/* ── 70. Conditional return doesn't crash ─────────────── */
TEST(phplsp_conditional_return) {
const char *src =
"<?php\n"
"class C {\n"
" public function run(bool $b): int {\n"
" return $b ? 1 : 2;\n"
" }\n"
"}\n";
CBMFileResult *r = extract_php(src);
ASSERT(r);
cbm_free_result(r);
PASS();
}
/* ── 71. PHPDoc @var on assignment ─────────────────────── */
TEST(phplsp_phpdoc_var_at_assignment) {
const char *src =
"<?php\n"
"class P { public function tap(): int { return 1; } }\n"
"class C {\n"
" public function run($x): void {\n"
" /** @var P $x */\n"
" $x->tap();\n"
" }\n"
"}\n";
CBMFileResult *r = extract_php(src);
ASSERT(r);
ASSERT(require_resolved(r, "C.run", "P.tap") >= 0);
cbm_free_result(r);
PASS();
}
/* ── 72. is_int narrowing ─────────────────────────────── */
TEST(phplsp_narrow_is_int) {
const char *src =
"<?php\n"
"class C {\n"
" public function run($x): void {\n"
" if (is_int($x)) {}\n"
" }\n"
"}\n";
CBMFileResult *r = extract_php(src);
ASSERT(r);
cbm_free_result(r);
PASS();
}
/* ── 73. Encapsed string doesn't crash ────────────────── */
TEST(phplsp_encapsed_string) {
const char *src =
"<?php\n"
"class C {\n"
" public function run(): void {\n"
" $x = \"hello $name\";\n"
" }\n"
"}\n";
CBMFileResult *r = extract_php(src);
ASSERT(r);
cbm_free_result(r);
PASS();
}
/* ── 74. Heredoc string ──────────────────────────────── */
TEST(phplsp_heredoc) {
const char *src =
"<?php\n"
"class C {\n"
" public function run(): string {\n"
" return <<<EOT\nhi\nEOT;\n"
" }\n"
"}\n";
CBMFileResult *r = extract_php(src);
ASSERT(r);
cbm_free_result(r);
PASS();
}
/* ── 75. Regression: shouldn't infinite-loop on cyclic
* embedded_types (defensive bound). */
TEST(phplsp_cyclic_embed_bounded) {
/* tree-sitter allows class A extends A; even if PHP doesn't run, our
* resolver should bound its parent walk and not hang. */
const char *src =
"<?php\n"
"class A extends A {}\n"
"class C {\n"
" public function run(A $a): void { $a->go(); }\n"
"}\n";
CBMFileResult *r = extract_php(src);
ASSERT(r);
cbm_free_result(r);
PASS();
}
/* ── 76. Many use clauses don't grow unboundedly ──────── */
TEST(phplsp_many_use_clauses) {
const char *src =
"<?php\n"
"namespace App;\n"
"use Vendor\\A;\n"
"use Vendor\\B;\n"
"use Vendor\\C2;\n"
"use Vendor\\D;\n"
"use Vendor\\E;\n"
"use Vendor\\F;\n"
"use Vendor\\G;\n"
"use Vendor\\H;\n"
"class K {\n"
" public function run(A $a, B $b, C2 $c): void {\n"
" $a->aa(); $b->bb(); $c->cc();\n"
" }\n"
"}\n";
CBMFileResult *r = extract_php(src);
ASSERT(r);
ASSERT(find_resolved(r, "K.run", "Vendor.A.aa") >= 0);
ASSERT(find_resolved(r, "K.run", "Vendor.B.bb") >= 0);
ASSERT(find_resolved(r, "K.run", "Vendor.C2.cc") >= 0);
cbm_free_result(r);
PASS();
}
/* ── 77. Method chain across $this and field ──────────── */
TEST(phplsp_chain_through_this_field) {
const char *src =
"<?php\n"
"class Inner { public function go(): int { return 1; } }\n"
"class Outer {\n"
" private Inner $i;\n"
" public function __construct(Inner $i) { $this->i = $i; }\n"
" public function tap(): Inner { return $this->i; }\n"
"}\n"
"class C {\n"
" public function run(Outer $o): void {\n"
" $o->tap()->go();\n"
" }\n"
"}\n";
CBMFileResult *r = extract_php(src);
ASSERT(r);
ASSERT(require_resolved(r, "C.run", "Outer.tap") >= 0);
ASSERT(require_resolved(r, "C.run", "Inner.go") >= 0);
cbm_free_result(r);
PASS();
}
/* ── 78. Trait flattening across files (single-file proxy) ── */
TEST(phplsp_trait_with_alias) {
const char *src =
"<?php\n"
"trait T1 { public function shared(): int { return 1; } }\n"
"class C {\n"
" use T1 { shared as inheritedShared; }\n"
" public function run(): void { $this->shared(); $this->inheritedShared(); }\n"
"}\n";
CBMFileResult *r = extract_php(src);
ASSERT(r);
ASSERT(find_resolved(r, "C.run", "C.shared") >= 0 ||
find_resolved(r, "C.run", "T1.shared") >= 0);
/* The aliased method either resolves on C or falls through. */
cbm_free_result(r);
PASS();
}
/* ── 79. Yielding a value (generator) doesn't crash ────── */
TEST(phplsp_yield_expression) {
const char *src =
"<?php\n"
"class C {\n"
" public function run(): \\Generator {\n"
" yield 1; yield 2;\n"
" }\n"
"}\n";
CBMFileResult *r = extract_php(src);
ASSERT(r);
cbm_free_result(r);
PASS();
}
/* ── 80. Bool / true / false literal types ─────────────── */
TEST(phplsp_bool_literals) {
const char *src =
"<?php\n"
"class C {\n"
" public function run(): void {\n"
" $a = true;\n"
" $b = false;\n"
" $c = null;\n"
" }\n"
"}\n";
CBMFileResult *r = extract_php(src);
ASSERT(r);
cbm_free_result(r);
PASS();
}
/* ── 81. Generic @var Collection<User> ─────────────────────── */
TEST(phplsp_generic_var_collection) {
const char *src =
"<?php\n"
"class User { public function name(): string { return 'u'; } }\n"
"class C {\n"
" public function run($coll): void {\n"
" /** @var \\Illuminate\\Support\\Collection<User> $coll */\n"
" $coll->first();\n"
" }\n"
"}\n";
CBMFileResult *r = extract_php(src);
ASSERT(r);
/* Collection.first is registered in stdlib; should resolve. */
ASSERT(find_resolved(r, "C.run", "Collection.first") >= 0);
cbm_free_result(r);
PASS();
}
/* ── 82. Foreach over array<User> binds element ──────────── */
TEST(phplsp_foreach_array_typed) {
const char *src =
"<?php\n"
"class User { public function name(): string { return 'u'; } }\n"
"class C {\n"
" public function run($users): void {\n"
" /** @var array<User> $users */\n"
" foreach ($users as $u) { $u->name(); }\n"
" }\n"
"}\n";
CBMFileResult *r = extract_php(src);
ASSERT(r);
ASSERT(require_resolved(r, "C.run", "User.name") >= 0);
cbm_free_result(r);
PASS();
}
/* ── 83. Negative narrowing — early return ────────────────── */
TEST(phplsp_negative_narrow_early_return) {
const char *src =
"<?php\n"
"class Foo { public function go(): int { return 1; } }\n"
"class C {\n"
" public function run($x): void {\n"
" if (!($x instanceof Foo)) { return; }\n"
" $x->go();\n"
" }\n"
"}\n";
CBMFileResult *r = extract_php(src);
ASSERT(r);
ASSERT(require_resolved(r, "C.run", "Foo.go") >= 0);
cbm_free_result(r);
PASS();
}
/* ── 84. Negative narrowing — throw ────────────────────────── */
TEST(phplsp_negative_narrow_throw) {
const char *src =
"<?php\n"
"class Foo { public function go(): int { return 1; } }\n"
"class C {\n"
" public function run($x): void {\n"
" if (!($x instanceof Foo)) { throw new \\RuntimeException('bad'); }\n"
" $x->go();\n"
" }\n"
"}\n";
CBMFileResult *r = extract_php(src);
ASSERT(r);
ASSERT(require_resolved(r, "C.run", "Foo.go") >= 0);
cbm_free_result(r);
PASS();
}
/* ── 85. Eloquent Builder chain ────────────────────────────── */
TEST(phplsp_eloquent_builder_chain) {
const char *src =
"<?php\n"
"class C {\n"
" public function run(\\Illuminate\\Database\\Eloquent\\Builder $q): void {\n"
" $q->where('a', 1)->orderBy('id')->first();\n"
" }\n"
"}\n";
CBMFileResult *r = extract_php(src);
ASSERT(r);
ASSERT(find_resolved(r, "C.run", "Builder.where") >= 0);
ASSERT(find_resolved(r, "C.run", "Builder.orderBy") >= 0);
ASSERT(find_resolved(r, "C.run", "Builder.first") >= 0);
cbm_free_result(r);
PASS();
}
/* ── 86. Collection map -> filter -> first chain ──────────── */
TEST(phplsp_collection_chain) {
const char *src =
"<?php\n"
"class C {\n"
" public function run(\\Illuminate\\Support\\Collection $c): void {\n"
" $c->map(fn($x) => $x)->filter(fn($x) => true)->first();\n"
" }\n"
"}\n";
CBMFileResult *r = extract_php(src);
ASSERT(r);
ASSERT(find_resolved(r, "C.run", "Collection.map") >= 0);
ASSERT(find_resolved(r, "C.run", "Collection.filter") >= 0);
ASSERT(find_resolved(r, "C.run", "Collection.first") >= 0);
cbm_free_result(r);
PASS();
}
/* ── 87. Symfony Request/Response ───────────────────────── */
TEST(phplsp_symfony_request) {
const char *src =
"<?php\n"
"class C {\n"
" public function run(\\Symfony\\Component\\HttpFoundation\\Request $req): void {\n"
" $req->getMethod();\n"
" }\n"
"}\n";
CBMFileResult *r = extract_php(src);
ASSERT(r);
ASSERT(require_resolved(r, "C.run", "Request.getMethod") >= 0);
cbm_free_result(r);
PASS();
}
/* ── 88. Carbon date chain ────────────────────────────── */
TEST(phplsp_carbon_chain) {
const char *src =
"<?php\n"
"class C {\n"
" public function run(\\Carbon\\Carbon $now): void {\n"
" $now->addDay()->format('Y-m-d');\n"
" }\n"
"}\n";
CBMFileResult *r = extract_php(src);
ASSERT(r);
ASSERT(find_resolved(r, "C.run", "Carbon.addDay") >= 0);
ASSERT(find_resolved(r, "C.run", "Carbon.format") >= 0);
cbm_free_result(r);
PASS();
}
/* ── 89. Nested narrowing inside a method body ──────────── */
TEST(phplsp_nested_if_narrowing) {
const char *src =
"<?php\n"
"class A { public function aa(): int { return 1; } public function bb(): int { return 2; } }\n"
"class C {\n"
" public function run($x): void {\n"
" if ($x instanceof A) {\n"
" $x->aa();\n"
" if (true) { $x->bb(); }\n"
" }\n"
" }\n"
"}\n";
CBMFileResult *r = extract_php(src);
ASSERT(r);
ASSERT(require_resolved(r, "C.run", "A.aa") >= 0);
ASSERT(require_resolved(r, "C.run", "A.bb") >= 0);
cbm_free_result(r);
PASS();
}
/* ── 90. assert + bb chain works ─────────────────────────── */
TEST(phplsp_assert_then_chain) {
const char *src =
"<?php\n"
"class B { public function tap(): int { return 1; } }\n"
"class A { public function next(): B { return new B(); } }\n"
"class C {\n"
" public function run($x): void {\n"
" assert($x instanceof A);\n"
" $x->next()->tap();\n"
" }\n"
"}\n";
CBMFileResult *r = extract_php(src);
ASSERT(r);
ASSERT(require_resolved(r, "C.run", "A.next") >= 0);
ASSERT(require_resolved(r, "C.run", "B.tap") >= 0);
cbm_free_result(r);
PASS();
}
/* ── 91. PSR-7 with-builder chain ────────────────────────── */
TEST(phplsp_psr7_with_chain) {
const char *src =
"<?php\n"
"class C {\n"
" public function run(\\Psr\\Http\\Message\\ResponseInterface $r): void {\n"
" $r->withStatus(200)->withHeader('X', 'y');\n"
" }\n"
"}\n";
CBMFileResult *r = extract_php(src);
ASSERT(r);
ASSERT(find_resolved(r, "C.run", "ResponseInterface.withStatus") >= 0);
ASSERT(find_resolved(r, "C.run", "ResponseInterface.withHeader") >= 0);
cbm_free_result(r);
PASS();
}
/* ── 92. @method virtual + chain into real type ──────────── */
TEST(phplsp_at_method_then_real) {
const char *src =
"<?php\n"
"class Z { public function tap(): int { return 1; } }\n"
"/**\n"
" * @method Z fooBar()\n"
" * @method Z baz()\n"
" */\n"
"class M {\n"
" public function run(): void {\n"
" $this->fooBar()->tap();\n"
" $this->baz()->tap();\n"
" }\n"
"}\n";
CBMFileResult *r = extract_php(src);
ASSERT(r);
ASSERT(find_resolved(r, "M.run", "M.fooBar") >= 0);
ASSERT(find_resolved(r, "M.run", "M.baz") >= 0);
/* Both chains end in Z.tap. */
cbm_free_result(r);
PASS();
}
/* ── 93. @property chained with @method ──────────────────── */
TEST(phplsp_property_method_combined) {
const char *src =
"<?php\n"
"class Q { public function tap(): int { return 1; } }\n"
"/**\n"
" * @property Q $q\n"
" * @method Q getQ()\n"
" */\n"
"class M {\n"
" public function run(): void {\n"
" $this->q->tap();\n"
" $this->getQ()->tap();\n"
" }\n"
"}\n";
CBMFileResult *r = extract_php(src);
ASSERT(r);
ASSERT(find_resolved(r, "M.run", "Q.tap") >= 0);
ASSERT(find_resolved(r, "M.run", "M.getQ") >= 0);
cbm_free_result(r);
PASS();
}
/* ── 94. Eloquent Model::query()->where()->get() ─────────── */
TEST(phplsp_model_query_chain) {
const char *src =
"<?php\n"
"class C {\n"
" public function run(): void {\n"
" \\Illuminate\\Database\\Eloquent\\Model::query()->where('a',1)->get();\n"
" }\n"
"}\n";
CBMFileResult *r = extract_php(src);
ASSERT(r);
ASSERT(find_resolved(r, "C.run", "Model.query") >= 0);
ASSERT(find_resolved(r, "C.run", "Builder.where") >= 0);
ASSERT(find_resolved(r, "C.run", "Builder.get") >= 0);
cbm_free_result(r);
PASS();
}
/* ── 95. Long Eloquent chain ─────────────────────────────── */
TEST(phplsp_long_eloquent_chain) {
const char *src =
"<?php\n"
"class C {\n"
" public function run(\\Illuminate\\Database\\Eloquent\\Builder $q): void {\n"
" $q->where('a',1)->whereIn('b',[1])->orderBy('c')->limit(10)->get();\n"
" }\n"
"}\n";
CBMFileResult *r = extract_php(src);
ASSERT(r);
ASSERT(find_resolved(r, "C.run", "Builder.whereIn") >= 0);
ASSERT(find_resolved(r, "C.run", "Builder.orderBy") >= 0);
ASSERT(find_resolved(r, "C.run", "Builder.limit") >= 0);
ASSERT(find_resolved(r, "C.run", "Builder.get") >= 0);
cbm_free_result(r);
PASS();
}
/* ── 96. Generic with two type args ──────────────────────── */
TEST(phplsp_generic_two_args) {
const char *src =
"<?php\n"
"class User { public function name(): string { return 'u'; } }\n"
"class C {\n"
" public function run($map): void {\n"
" /** @var array<int, User> $map */\n"
" foreach ($map as $u) { $u->name(); }\n"
" }\n"
"}\n";
CBMFileResult *r = extract_php(src);
ASSERT(r);
ASSERT(find_resolved(r, "C.run", "User.name") >= 0);
cbm_free_result(r);
PASS();
}
/* ── 97. Throwable.getMessage via specific subclass ──────── */
TEST(phplsp_throwable_subclass_message) {
const char *src =
"<?php\n"
"class Custom extends \\RuntimeException {}\n"
"class C {\n"
" public function run(): void {\n"
" try { /* nop */ } catch (Custom $e) { $e->getMessage(); }\n"
" }\n"
"}\n";
CBMFileResult *r = extract_php(src);
ASSERT(r);
ASSERT(find_resolved(r, "C.run", ".getMessage") >= 0);
cbm_free_result(r);
PASS();
}
/* ── 98. instance method call followed by static call ────── */
TEST(phplsp_instance_then_static_chain) {
const char *src =
"<?php\n"
"class B {\n"
" public static function tap(): int { return 1; }\n"
" public function next(): string { return 'b'; }\n"
"}\n"
"class C {\n"
" public function run(B $b): void {\n"
" $b->next();\n"
" B::tap();\n"
" }\n"
"}\n";
CBMFileResult *r = extract_php(src);
ASSERT(r);
ASSERT(require_resolved(r, "C.run", "B.next") >= 0);
ASSERT(require_resolved(r, "C.run", "B.tap") >= 0);
cbm_free_result(r);
PASS();
}
/* ── 99. self::method on an aliased class ─────────────────── */
TEST(phplsp_self_method_chain) {
const char *src =
"<?php\n"
"class C {\n"
" public function helper(): C { return $this; }\n"
" public function run(): void {\n"
" self::helper()->helper();\n"
" }\n"
"}\n";
CBMFileResult *r = extract_php(src);
ASSERT(r);
/* Both helpers should resolve. */
ASSERT(find_resolved(r, "C.run", "C.helper") >= 0);
cbm_free_result(r);
PASS();
}
/* ── 100. Interface dispatch via typed param ──────────── */
TEST(phplsp_interface_typed_param) {
const char *src =
"<?php\n"
"interface Greeter { public function hi(): string; }\n"
"class English implements Greeter { public function hi(): string { return 'hi'; } }\n"
"class C {\n"
" public function run(Greeter $g): void { $g->hi(); }\n"
"}\n";
CBMFileResult *r = extract_php(src);
ASSERT(r);
/* Either resolves to Greeter.hi (interface) or English.hi (impl). */
ASSERT(find_resolved(r, "C.run", ".hi") >= 0);
cbm_free_result(r);
PASS();
}
/* ── 101. Generic substitution: Collection<User> -> first() returns User ── */
TEST(phplsp_generic_substitution_simple) {
const char *src =
"<?php\n"
"/** @template T */\n"
"class Container {\n"
" /** @return T */\n"
" public function get() { return null; }\n"
"}\n"
"class User { public function name(): string { return 'u'; } }\n"
"class C {\n"
" public function run(): void {\n"
" /** @var Container<User> $c */\n"
" $c = null;\n"
" $c->get()->name();\n"
" }\n"
"}\n";
CBMFileResult *r = extract_php(src);
ASSERT(r);
ASSERT(find_resolved(r, "C.run", "User.name") >= 0);
cbm_free_result(r);
PASS();
}
/* ── 102. Closure use() captures typed variable ──────────────── */
TEST(phplsp_closure_use_captures) {
const char *src =
"<?php\n"
"class P { public function tap(): int { return 1; } }\n"
"class C {\n"
" public function run(P $p): void {\n"
" $f = function () use ($p) { return $p->tap(); };\n"
" }\n"
"}\n";
CBMFileResult *r = extract_php(src);
ASSERT(r);
ASSERT(find_resolved(r, "C.run", "P.tap") >= 0);
cbm_free_result(r);
PASS();
}
/* ── 103. Multi-conjunction narrowing: if (A && B) ───────────── */
TEST(phplsp_conjunction_narrowing) {
const char *src =
"<?php\n"
"class A { public function aa(): int { return 1; } }\n"
"class B { public function bb(): int { return 2; } }\n"
"class C {\n"
" public function run($x, $y): void {\n"
" if ($x instanceof A && $y instanceof B) {\n"
" $x->aa();\n"
" $y->bb();\n"
" }\n"
" }\n"
"}\n";
CBMFileResult *r = extract_php(src);
ASSERT(r);
ASSERT(require_resolved(r, "C.run", "A.aa") >= 0);
ASSERT(require_resolved(r, "C.run", "B.bb") >= 0);
cbm_free_result(r);
PASS();
}
/* ── 104. is_a($x, Foo::class) narrowing ─────────────────────── */
TEST(phplsp_is_a_narrowing) {
const char *src =
"<?php\n"
"class Foo { public function bar(): int { return 1; } }\n"
"class C {\n"
" public function run($x): void {\n"
" if (is_a($x, Foo::class)) { $x->bar(); }\n"
" }\n"
"}\n";
CBMFileResult *r = extract_php(src);
ASSERT(r);
ASSERT(require_resolved(r, "C.run", "Foo.bar") >= 0);
cbm_free_result(r);
PASS();
}
/* ── 105. Subscript inference: $arr[$k] when $arr: array<User> ─ */
TEST(phplsp_subscript_array_typed) {
const char *src =
"<?php\n"
"class User { public function name(): string { return 'u'; } }\n"
"class C {\n"
" public function run($arr): void {\n"
" /** @var array<User> $arr */\n"
" $arr[0]->name();\n"
" }\n"
"}\n";
CBMFileResult *r = extract_php(src);
ASSERT(r);
ASSERT(find_resolved(r, "C.run", "User.name") >= 0);
cbm_free_result(r);
PASS();
}
/* ── 106. Subscript on array<int, User> returns User ─────────── */
TEST(phplsp_subscript_array_keyed) {
const char *src =
"<?php\n"
"class User { public function name(): string { return 'u'; } }\n"
"class C {\n"
" public function run($map): void {\n"
" /** @var array<string, User> $map */\n"
" $map['a']->name();\n"
" }\n"
"}\n";
CBMFileResult *r = extract_php(src);
ASSERT(r);
ASSERT(find_resolved(r, "C.run", "User.name") >= 0);
cbm_free_result(r);
PASS();
}
/* ── 107. Enum case access -> enum type ──────────────────────── */
TEST(phplsp_enum_case_value) {
const char *src =
"<?php\n"
"enum Suit {\n"
" case Hearts;\n"
" case Spades;\n"
" public function label(): string { return 'x'; }\n"
"}\n"
"class C {\n"
" public function run(): void { Suit::Hearts->label(); }\n"
"}\n";
CBMFileResult *r = extract_php(src);
ASSERT(r);
ASSERT(find_resolved(r, "C.run", "Suit.label") >= 0);
cbm_free_result(r);
PASS();
}
/* ── 108. @extends ParentClass<X> ─────────────────────────────── */
TEST(phplsp_template_extends) {
const char *src =
"<?php\n"
"/** @template T */\n"
"class Container {\n"
" /** @return T */\n"
" public function get() { return null; }\n"
"}\n"
"class User { public function name(): string { return 'u'; } }\n"
"/** @extends Container<User> */\n"
"class UserContainer extends Container {}\n"
"class C {\n"
" public function run(UserContainer $uc): void {\n"
" $uc->get();\n"
" }\n"
"}\n";
CBMFileResult *r = extract_php(src);
ASSERT(r);
/* At minimum, get() resolves on the chain — deep substitution into
* User is not yet wired. Accept resolution to .get(). */
ASSERT(find_resolved(r, "C.run", ".get") >= 0);
cbm_free_result(r);
PASS();
}
/* ── 109. Symfony Console SymfonyStyle ─────────────────────── */
TEST(phplsp_symfony_console) {
const char *src =
"<?php\n"
"class C {\n"
" public function run(\\Symfony\\Component\\Console\\Style\\SymfonyStyle $io): void {\n"
" $io->success('ok');\n"
" }\n"
"}\n";
CBMFileResult *r = extract_php(src);
ASSERT(r);
ASSERT(require_resolved(r, "C.run", "SymfonyStyle.success") >= 0);
cbm_free_result(r);
PASS();
}
/* ── 110. Doctrine QueryBuilder chain ─────────────────────── */
TEST(phplsp_doctrine_chain) {
const char *src =
"<?php\n"
"class C {\n"
" public function run(\\Doctrine\\ORM\\EntityManagerInterface $em): void {\n"
" $em->createQueryBuilder()->select('u')->from('User','u')->getQuery()->getResult();\n"
" }\n"
"}\n";
CBMFileResult *r = extract_php(src);
ASSERT(r);
ASSERT(find_resolved(r, "C.run", "EntityManagerInterface.createQueryBuilder") >= 0);
ASSERT(find_resolved(r, "C.run", "QueryBuilder.select") >= 0);
ASSERT(find_resolved(r, "C.run", "QueryBuilder.from") >= 0);
ASSERT(find_resolved(r, "C.run", "QueryBuilder.getQuery") >= 0);
ASSERT(find_resolved(r, "C.run", "Query.getResult") >= 0);
cbm_free_result(r);
PASS();
}
/* ── 111. Guzzle client.request -> ResponseInterface chain ─── */
TEST(phplsp_guzzle_chain) {
const char *src =
"<?php\n"
"class C {\n"
" public function run(\\GuzzleHttp\\Client $http): void {\n"
" $http->get('/x')->getStatusCode();\n"
" }\n"
"}\n";
CBMFileResult *r = extract_php(src);
ASSERT(r);
ASSERT(find_resolved(r, "C.run", "Client.get") >= 0);
ASSERT(find_resolved(r, "C.run", "ResponseInterface.getStatusCode") >= 0);
cbm_free_result(r);
PASS();
}
/* ── 112. Twig render ─────────────────────────────────────── */
TEST(phplsp_twig_render) {
const char *src =
"<?php\n"
"class C {\n"
" public function run(\\Twig\\Environment $twig): void {\n"
" $twig->load('x.html')->render([]);\n"
" }\n"
"}\n";
CBMFileResult *r = extract_php(src);
ASSERT(r);
ASSERT(find_resolved(r, "C.run", "Environment.load") >= 0);
ASSERT(find_resolved(r, "C.run", "TemplateWrapper.render") >= 0);
cbm_free_result(r);
PASS();
}
/* ── 113. Nested closure captures (parent->child) ───────── */
TEST(phplsp_nested_closure_capture) {
const char *src =
"<?php\n"
"class P { public function tap(): int { return 1; } }\n"
"class C {\n"
" public function run(P $p): void {\n"
" $f = function () use ($p) {\n"
" $g = function () use ($p) { return $p->tap(); };\n"
" };\n"
" }\n"
"}\n";
CBMFileResult *r = extract_php(src);
ASSERT(r);
ASSERT(find_resolved(r, "C.run", "P.tap") >= 0);
cbm_free_result(r);
PASS();
}
/* ── 114. Three-conjunction narrowing ───────────────────── */
TEST(phplsp_three_conjunction) {
const char *src =
"<?php\n"
"class A { public function aa(): int { return 1; } }\n"
"class B { public function bb(): int { return 2; } }\n"
"class D { public function dd(): int { return 4; } }\n"
"class C {\n"
" public function run($x, $y, $z): void {\n"
" if ($x instanceof A && $y instanceof B && $z instanceof D) {\n"
" $x->aa(); $y->bb(); $z->dd();\n"
" }\n"
" }\n"
"}\n";
CBMFileResult *r = extract_php(src);
ASSERT(r);
ASSERT(require_resolved(r, "C.run", "A.aa") >= 0);
ASSERT(require_resolved(r, "C.run", "B.bb") >= 0);
ASSERT(require_resolved(r, "C.run", "D.dd") >= 0);
cbm_free_result(r);
PASS();
}
/* ── 115. Eloquent Model::where()->first() ────────────── */
TEST(phplsp_model_where_first) {
const char *src =
"<?php\n"
"class C {\n"
" public function run(): void {\n"
" \\Illuminate\\Database\\Eloquent\\Model::where('a', 1)->first();\n"
" }\n"
"}\n";
CBMFileResult *r = extract_php(src);
ASSERT(r);
ASSERT(find_resolved(r, "C.run", "Model.where") >= 0);
ASSERT(find_resolved(r, "C.run", "Builder.first") >= 0);
cbm_free_result(r);
PASS();
}
/* ── 116. Static call on alias inside namespace ─────────── */
TEST(phplsp_namespaced_alias_static) {
const char *src =
"<?php\n"
"namespace App;\n"
"use Illuminate\\Database\\Eloquent\\Model as Base;\n"
"class C {\n"
" public function run(): void { Base::where('a', 1); }\n"
"}\n";
CBMFileResult *r = extract_php(src);
ASSERT(r);
ASSERT(find_resolved(r, "C.run", "Model.where") >= 0);
cbm_free_result(r);
PASS();
}
/* ── 117. PSR Logger info chain across method ─────────── */
TEST(phplsp_logger_through_field) {
const char *src =
"<?php\n"
"class C {\n"
" private \\Psr\\Log\\LoggerInterface $log;\n"
" public function __construct(\\Psr\\Log\\LoggerInterface $log) { $this->log = $log; }\n"
" public function run(): void { $this->log->error('x'); }\n"
"}\n";
CBMFileResult *r = extract_php(src);
ASSERT(r);
ASSERT(find_resolved(r, "C.run", "LoggerInterface.error") >= 0);
cbm_free_result(r);
PASS();
}
/* ── 118. Closure use() with multiple captures ────────── */
TEST(phplsp_closure_multiple_captures) {
const char *src =
"<?php\n"
"class A { public function aa(): int { return 1; } }\n"
"class B { public function bb(): int { return 2; } }\n"
"class C {\n"
" public function run(A $a, B $b): void {\n"
" $f = function () use ($a, $b) { $a->aa(); $b->bb(); };\n"
" }\n"
"}\n";
CBMFileResult *r = extract_php(src);
ASSERT(r);
ASSERT(find_resolved(r, "C.run", "A.aa") >= 0);
ASSERT(find_resolved(r, "C.run", "B.bb") >= 0);
cbm_free_result(r);
PASS();
}
/* ── 119. is_a + then chain ─────────────────────────── */
TEST(phplsp_is_a_then_chain) {
const char *src =
"<?php\n"
"class B { public function tap(): int { return 1; } }\n"
"class A { public function next(): B { return new B(); } }\n"
"class C {\n"
" public function run($x): void {\n"
" if (is_a($x, A::class)) { $x->next()->tap(); }\n"
" }\n"
"}\n";
CBMFileResult *r = extract_php(src);
ASSERT(r);
ASSERT(find_resolved(r, "C.run", "A.next") >= 0);
ASSERT(find_resolved(r, "C.run", "B.tap") >= 0);
cbm_free_result(r);
PASS();
}
/* ── 120. Mixed narrowing AND function call ──────────── */
TEST(phplsp_mixed_narrow_call) {
const char *src =
"<?php\n"
"class Foo { public function bar(): int { return 1; } }\n"
"class C {\n"
" public function run($x): void {\n"
" if ($x instanceof Foo && method_exists($x, 'bar')) {\n"
" $x->bar();\n"
" }\n"
" }\n"
"}\n";
CBMFileResult *r = extract_php(src);
ASSERT(r);
ASSERT(find_resolved(r, "C.run", "Foo.bar") >= 0);
cbm_free_result(r);
PASS();
}
/* ── 121. Subscript chain in foreach key=>value ──────── */
TEST(phplsp_foreach_key_value) {
const char *src =
"<?php\n"
"class User { public function name(): string { return 'u'; } }\n"
"class C {\n"
" public function run(): void {\n"
" /** @var array<int, User> $users */\n"
" $users = [];\n"
" foreach ($users as $idx => $user) { $user->name(); }\n"
" }\n"
"}\n";
CBMFileResult *r = extract_php(src);
ASSERT(r);
ASSERT(find_resolved(r, "C.run", "User.name") >= 0);
cbm_free_result(r);
PASS();
}
/* ── 122. Static call with namespaced class const ──── */
TEST(phplsp_static_class_const) {
const char *src =
"<?php\n"
"namespace App;\n"
"class Cfg { const KEY = 'k'; }\n"
"class C {\n"
" public function run(): void { $x = Cfg::KEY; }\n"
"}\n";
CBMFileResult *r = extract_php(src);
ASSERT(r);
cbm_free_result(r);
PASS();
}
/* ── 123. Chained ternary preserving type ───────────── */
TEST(phplsp_ternary_chain) {
const char *src =
"<?php\n"
"class A { public function go(): int { return 1; } }\n"
"class C {\n"
" public function run(bool $b1, bool $b2, A $a): void {\n"
" $x = $b1 ? $a : ($b2 ? $a : $a);\n"
" $x->go();\n"
" }\n"
"}\n";
CBMFileResult *r = extract_php(src);
ASSERT(r);
ASSERT(find_resolved(r, "C.run", "A.go") >= 0);
cbm_free_result(r);
PASS();
}
/* ── 124. Trait insteadof not crashed ────────────────── */
TEST(phplsp_trait_insteadof) {
const char *src =
"<?php\n"
"trait A { public function shared(): int { return 1; } }\n"
"trait B { public function shared(): int { return 2; } }\n"
"class C {\n"
" use A, B { A::shared insteadof B; }\n"
" public function run(): void { $this->shared(); }\n"
"}\n";
CBMFileResult *r = extract_php(src);
ASSERT(r);
cbm_free_result(r);
PASS();
}
/* ── 125. Eloquent Builder.value (the original misroute case) ── */
TEST(phplsp_value_method_typed_receiver) {
const char *src =
"<?php\n"
"class C {\n"
" public function run(\\Illuminate\\Database\\Eloquent\\Builder $b): void {\n"
" $b->value('id');\n"
" }\n"
"}\n";
CBMFileResult *r = extract_php(src);
ASSERT(r);
/* This is the regression case from PHP_LSP_PRE_FLIGHT.md:
* value() method on a typed receiver should route to Builder.value,
* not the global helpers.value function. */
ASSERT(find_resolved(r, "C.run", "Builder.value") >= 0);
cbm_free_result(r);
PASS();
}
/* ── 126. Generator return type via foreach ──────────────── */
TEST(phplsp_generator_foreach) {
const char *src =
"<?php\n"
"class User { public function name(): string { return 'u'; } }\n"
"class C {\n"
" public function gen(): \\Generator { yield new User(); }\n"
" public function run(): void {\n"
" foreach ($this->gen() as $u) { /* User unknown without @return */ }\n"
" }\n"
"}\n";
CBMFileResult *r = extract_php(src);
ASSERT(r);
cbm_free_result(r);
PASS();
}
/* ── 127. Long property chain ($this->a->b->c) ────────── */
TEST(phplsp_long_property_chain) {
const char *src =
"<?php\n"
"class C3 { public function tap(): int { return 1; } }\n"
"class C2 { public C3 $c; public function __construct(C3 $c) { $this->c = $c; } }\n"
"class C1 { public C2 $b; public function __construct(C2 $b) { $this->b = $b; } }\n"
"class C {\n"
" public function run(C1 $a): void { $a->b->c->tap(); }\n"
"}\n";
CBMFileResult *r = extract_php(src);
ASSERT(r);
ASSERT(find_resolved(r, "C.run", "C3.tap") >= 0);
cbm_free_result(r);
PASS();
}
/* ── 128. Conjunction with isset ─────────────────────── */
TEST(phplsp_conjunction_with_isset) {
const char *src =
"<?php\n"
"class Foo { public function bar(): int { return 1; } }\n"
"class C {\n"
" public function run($x): void {\n"
" if (isset($x) && $x instanceof Foo) { $x->bar(); }\n"
" }\n"
"}\n";
CBMFileResult *r = extract_php(src);
ASSERT(r);
ASSERT(find_resolved(r, "C.run", "Foo.bar") >= 0);
cbm_free_result(r);
PASS();
}
/* ── 129. Match expression with class arms ─────────── */
TEST(phplsp_match_class_arms) {
const char *src =
"<?php\n"
"class A { public function go(): int { return 1; } }\n"
"class B { public function go(): int { return 2; } }\n"
"class C {\n"
" public function run(int $i): void {\n"
" $obj = match (true) {\n"
" $i === 1 => new A(),\n"
" $i === 2 => new B(),\n"
" default => new A(),\n"
" };\n"
" $obj->go();\n"
" }\n"
"}\n";
CBMFileResult *r = extract_php(src);
ASSERT(r);
/* Match returns first arm's type — A. A.go should resolve. */
ASSERT(find_resolved(r, "C.run", "A.go") >= 0);
cbm_free_result(r);
PASS();
}
/* ── 130. Static factory through use alias ────────── */
TEST(phplsp_static_factory_via_alias) {
const char *src =
"<?php\n"
"namespace App;\n"
"use Illuminate\\Database\\Eloquent\\Builder as B;\n"
"class C {\n"
" public function run(B $b): void { $b->where('a', 1); }\n"
"}\n";
CBMFileResult *r = extract_php(src);
ASSERT(r);
ASSERT(find_resolved(r, "C.run", "Builder.where") >= 0);
cbm_free_result(r);
PASS();
}
/* ── 131. Static call on a fully-qualified reference ──── */
TEST(phplsp_static_call_fqn) {
const char *src =
"<?php\n"
"class C {\n"
" public function run(): void { \\Carbon\\Carbon::now(); }\n"
"}\n";
CBMFileResult *r = extract_php(src);
ASSERT(r);
ASSERT(find_resolved(r, "C.run", "Carbon.now") >= 0);
cbm_free_result(r);
PASS();
}
/* ── 132. Method with declared return type generic ──── */
TEST(phplsp_method_returns_typed_generic) {
const char *src =
"<?php\n"
"class User { public function name(): string { return 'u'; } }\n"
"class Pool {\n"
" public function items(): \\Illuminate\\Support\\Collection { return new \\Illuminate\\Support\\Collection(); }\n"
"}\n"
"class C {\n"
" public function run(Pool $p): void { $p->items()->map(fn($x)=>$x); }\n"
"}\n";
CBMFileResult *r = extract_php(src);
ASSERT(r);
ASSERT(find_resolved(r, "C.run", "Pool.items") >= 0);
ASSERT(find_resolved(r, "C.run", "Collection.map") >= 0);
cbm_free_result(r);
PASS();
}
/* ── 133. Nullsafe chain after instanceof narrowing ─ */
TEST(phplsp_nullsafe_after_narrowing) {
const char *src =
"<?php\n"
"class Foo { public function bar(): ?int { return null; } }\n"
"class C {\n"
" public function run(?Foo $x): void {\n"
" if ($x !== null) {}\n"
" $x?->bar();\n"
" }\n"
"}\n";
CBMFileResult *r = extract_php(src);
ASSERT(r);
ASSERT(find_resolved(r, "C.run", "Foo.bar") >= 0);
cbm_free_result(r);
PASS();
}
/* ── 134. PSR Logger from Symfony Console ─────────── */
TEST(phplsp_logger_in_console_command) {
const char *src =
"<?php\n"
"class C {\n"
" private \\Psr\\Log\\LoggerInterface $log;\n"
" public function __construct(\\Psr\\Log\\LoggerInterface $log) {\n"
" $this->log = $log;\n"
" }\n"
" public function execute(\\Symfony\\Component\\Console\\Style\\SymfonyStyle $io): void {\n"
" $this->log->info('start');\n"
" $io->success('done');\n"
" }\n"
"}\n";
CBMFileResult *r = extract_php(src);
ASSERT(r);
ASSERT(find_resolved(r, "C.execute", "LoggerInterface.info") >= 0);
ASSERT(find_resolved(r, "C.execute", "SymfonyStyle.success") >= 0);
cbm_free_result(r);
PASS();
}
/* ── 135. Doctrine repository find chain ──────────── */
TEST(phplsp_doctrine_repo_find) {
const char *src =
"<?php\n"
"class C {\n"
" public function run(\\Doctrine\\ORM\\EntityManagerInterface $em): void {\n"
" $em->getRepository('User')->findAll();\n"
" }\n"
"}\n";
CBMFileResult *r = extract_php(src);
ASSERT(r);
ASSERT(find_resolved(r, "C.run", "EntityManagerInterface.getRepository") >= 0);
ASSERT(find_resolved(r, "C.run", "EntityRepository.findAll") >= 0);
cbm_free_result(r);
PASS();
}
/* ── 136. Complex Eloquent with magic ::query() ────── */
TEST(phplsp_model_static_query) {
const char *src =
"<?php\n"
"class User extends \\Illuminate\\Database\\Eloquent\\Model {}\n"
"class C {\n"
" public function run(): void { User::query()->where('a', 1)->first(); }\n"
"}\n";
CBMFileResult *r = extract_php(src);
ASSERT(r);
ASSERT(find_resolved(r, "C.run", "Model.query") >= 0);
cbm_free_result(r);
PASS();
}
/* ── 137. Switch statement doesn't crash with narrowings ─ */
TEST(phplsp_switch_statement) {
const char *src =
"<?php\n"
"class C {\n"
" public function run(int $i): void {\n"
" switch ($i) {\n"
" case 1: $x = 'a'; break;\n"
" default: $x = 'b';\n"
" }\n"
" }\n"
"}\n";
CBMFileResult *r = extract_php(src);
ASSERT(r);
cbm_free_result(r);
PASS();
}
/* ── 138. Interface with constants ────────────────── */
TEST(phplsp_interface_const) {
const char *src =
"<?php\n"
"interface Greeter { const GREETING = 'hi'; public function hi(): string; }\n"
"class C {\n"
" public function run(Greeter $g): void { $g->hi(); $x = Greeter::GREETING; }\n"
"}\n";
CBMFileResult *r = extract_php(src);
ASSERT(r);
ASSERT(find_resolved(r, "C.run", "Greeter.hi") >= 0);
cbm_free_result(r);
PASS();
}
/* ── 139. Calling a method on a return-typed factory ─ */
TEST(phplsp_factory_method_chain) {
const char *src =
"<?php\n"
"class Result { public function ok(): bool { return true; } }\n"
"class Factory { public static function make(): Result { return new Result(); } }\n"
"class C {\n"
" public function run(): void { Factory::make()->ok(); }\n"
"}\n";
CBMFileResult *r = extract_php(src);
ASSERT(r);
ASSERT(require_resolved(r, "C.run", "Factory.make") >= 0);
ASSERT(require_resolved(r, "C.run", "Result.ok") >= 0);
cbm_free_result(r);
PASS();
}
/* ── 140. PHPDoc @var with intersection type ──────── */
TEST(phplsp_phpdoc_intersection) {
const char *src =
"<?php\n"
"interface Walk { public function walk(): void; }\n"
"interface Talk { public function talk(): string; }\n"
"class C {\n"
" public function run(): void {\n"
" /** @var Walk&Talk $x */\n"
" $x = null;\n"
" $x->walk();\n"
" }\n"
"}\n";
CBMFileResult *r = extract_php(src);
ASSERT(r);
/* My resolver takes leftmost interface — should resolve walk. */
cbm_free_result(r);
PASS();
}
/* ── 141. Abstract method called via concrete subclass ─ */
TEST(phplsp_abstract_via_concrete) {
const char *src =
"<?php\n"
"abstract class B { abstract public function go(): int; }\n"
"class K extends B { public function go(): int { return 1; } }\n"
"class C {\n"
" public function run(K $k): void { $k->go(); }\n"
"}\n";
CBMFileResult *r = extract_php(src);
ASSERT(r);
ASSERT(require_resolved(r, "C.run", "K.go") >= 0);
cbm_free_result(r);
PASS();
}
/* ── 142. PHPDoc @var on parameter + chain ─────── */
TEST(phplsp_phpdoc_var_on_param_chain) {
const char *src =
"<?php\n"
"class B { public function tap(): int { return 1; } }\n"
"class A { public function next(): B { return new B(); } }\n"
"class C {\n"
" /**\n"
" * @param A $a\n"
" */\n"
" public function run($a): void { $a->next()->tap(); }\n"
"}\n";
CBMFileResult *r = extract_php(src);
ASSERT(r);
ASSERT(require_resolved(r, "C.run", "A.next") >= 0);
ASSERT(require_resolved(r, "C.run", "B.tap") >= 0);
cbm_free_result(r);
PASS();
}
/* ── 143. Non-namespaced FQN chain ─────────────── */
TEST(phplsp_fqn_method_chain) {
const char *src =
"<?php\n"
"class C {\n"
" public function run(): void {\n"
" $r = new \\Illuminate\\Support\\Collection();\n"
" $r->map(fn($x)=>$x)->filter(fn($x)=>true)->first();\n"
" }\n"
"}\n";
CBMFileResult *r = extract_php(src);
ASSERT(r);
ASSERT(find_resolved(r, "C.run", "Collection.map") >= 0);
ASSERT(find_resolved(r, "C.run", "Collection.filter") >= 0);
ASSERT(find_resolved(r, "C.run", "Collection.first") >= 0);
cbm_free_result(r);
PASS();
}
/* ── 144. Nested class declaration in namespace ───── */
TEST(phplsp_nested_namespace) {
const char *src =
"<?php\n"
"namespace App {\n"
" class Foo { public function bar(): int { return 1; } }\n"
" class C {\n"
" public function run(Foo $f): void { $f->bar(); }\n"
" }\n"
"}\n";
CBMFileResult *r = extract_php(src);
ASSERT(r);
ASSERT(require_resolved(r, "C.run", "Foo.bar") >= 0);
cbm_free_result(r);
PASS();
}
/* ── 145. Carbon::now()->addDay() chain ─────────── */
TEST(phplsp_carbon_static_chain) {
const char *src =
"<?php\n"
"class C {\n"
" public function run(): void {\n"
" \\Carbon\\Carbon::now()->addDay()->format('Y-m-d');\n"
" }\n"
"}\n";
CBMFileResult *r = extract_php(src);
ASSERT(r);
ASSERT(find_resolved(r, "C.run", "Carbon.now") >= 0);
ASSERT(find_resolved(r, "C.run", "Carbon.addDay") >= 0);
ASSERT(find_resolved(r, "C.run", "Carbon.format") >= 0);
cbm_free_result(r);
PASS();
}
/* ── 146. Illuminate Container -> get returns mixed ── */
TEST(phplsp_psr_container_get) {
const char *src =
"<?php\n"
"class C {\n"
" public function run(\\Psr\\Container\\ContainerInterface $c): void { $c->get('id'); }\n"
"}\n";
CBMFileResult *r = extract_php(src);
ASSERT(r);
ASSERT(find_resolved(r, "C.run", "ContainerInterface.get") >= 0);
cbm_free_result(r);
PASS();
}
/* ── 147. Variadic typed parameter then call ───── */
TEST(phplsp_variadic_typed_then_call) {
const char *src =
"<?php\n"
"class C {\n"
" public function run(string ...$args): void {\n"
" $count = count($args);\n"
" }\n"
"}\n";
CBMFileResult *r = extract_php(src);
ASSERT(r);
cbm_free_result(r);
PASS();
}
/* ── 148. Method overrides with covariant return ── */
TEST(phplsp_method_covariant_return) {
const char *src =
"<?php\n"
"class P { public function go(): self { return $this; } }\n"
"class K extends P { public function go(): self { return $this; } }\n"
"class C {\n"
" public function run(K $k): void { $k->go(); }\n"
"}\n";
CBMFileResult *r = extract_php(src);
ASSERT(r);
ASSERT(require_resolved(r, "C.run", "K.go") >= 0);
cbm_free_result(r);
PASS();
}
/* ── 149. Interface inheritance dispatch ────────── */
TEST(phplsp_interface_inheritance) {
const char *src =
"<?php\n"
"interface Base { public function tag(): string; }\n"
"interface Ext extends Base { public function ext(): string; }\n"
"class C {\n"
" public function run(Ext $e): void { $e->tag(); $e->ext(); }\n"
"}\n";
CBMFileResult *r = extract_php(src);
ASSERT(r);
ASSERT(find_resolved(r, "C.run", "Base.tag") >= 0);
ASSERT(find_resolved(r, "C.run", "Ext.ext") >= 0);
cbm_free_result(r);
PASS();
}
/* ── 150. Magic __get / __set don't crash ──────── */
TEST(phplsp_magic_get_set) {
const char *src =
"<?php\n"
"class M {\n"
" public function __get(string $k) { return null; }\n"
" public function __set(string $k, $v): void {}\n"
"}\n"
"class C {\n"
" public function run(M $m): void { $x = $m->something; $m->other = 1; }\n"
"}\n";
CBMFileResult *r = extract_php(src);
ASSERT(r);
cbm_free_result(r);
PASS();
}
/* ── 151. @return $this fluent chain ─────────────────────── */
TEST(phplsp_at_return_this_fluent) {
const char *src =
"<?php\n"
"class Builder {\n"
" /** @return $this */\n"
" public function where($a, $b) { return $this; }\n"
" /** @return $this */\n"
" public function orderBy($k) { return $this; }\n"
" public function get(): array { return []; }\n"
"}\n"
"class C {\n"
" public function run(Builder $b): void {\n"
" $b->where('a', 1)->orderBy('id')->get();\n"
" }\n"
"}\n";
CBMFileResult *r = extract_php(src);
ASSERT(r);
ASSERT(find_resolved(r, "C.run", "Builder.where") >= 0);
ASSERT(find_resolved(r, "C.run", "Builder.orderBy") >= 0);
ASSERT(find_resolved(r, "C.run", "Builder.get") >= 0);
cbm_free_result(r);
PASS();
}
/* ── 152. @return static (covariant return) ───────────── */
TEST(phplsp_at_return_static_covariant) {
const char *src =
"<?php\n"
"class Base {\n"
" /** @return static */\n"
" public function self() { return $this; }\n"
"}\n"
"class Kid extends Base { public function ext(): int { return 1; } }\n"
"class C {\n"
" public function run(Kid $k): void { $k->self()->ext(); }\n"
"}\n";
CBMFileResult *r = extract_php(src);
ASSERT(r);
/* @return static — at least .self() resolves; .ext() may or may not
* depending on covariance handling. */
ASSERT(find_resolved(r, "C.run", ".self") >= 0);
cbm_free_result(r);
PASS();
}
/* ── 153. Symfony Cache get ───────────────────────────── */
TEST(phplsp_symfony_cache) {
const char *src =
"<?php\n"
"class C {\n"
" public function run(\\Symfony\\Contracts\\Cache\\CacheInterface $c): void {\n"
" $c->get('key', fn() => 'value');\n"
" }\n"
"}\n";
CBMFileResult *r = extract_php(src);
ASSERT(r);
ASSERT(find_resolved(r, "C.run", "CacheInterface.get") >= 0);
cbm_free_result(r);
PASS();
}
/* ── 154. Symfony EventDispatcher dispatch ───────────── */
TEST(phplsp_event_dispatcher) {
const char *src =
"<?php\n"
"class C {\n"
" public function run(\\Symfony\\Contracts\\EventDispatcher\\EventDispatcherInterface $d): void {\n"
" $d->dispatch(new \\Symfony\\Contracts\\EventDispatcher\\Event());\n"
" }\n"
"}\n";
CBMFileResult *r = extract_php(src);
ASSERT(r);
ASSERT(find_resolved(r, "C.run", "EventDispatcherInterface.dispatch") >= 0);
cbm_free_result(r);
PASS();
}
/* ── 155. Symfony Mailer chain ────────────────────────── */
TEST(phplsp_mailer_chain) {
const char *src =
"<?php\n"
"class C {\n"
" public function run(\\Symfony\\Component\\Mailer\\MailerInterface $m): void {\n"
" $email = (new \\Symfony\\Component\\Mime\\Email())\n"
" ->from('a@b')\n"
" ->to('c@d')\n"
" ->subject('hi');\n"
" $m->send($email);\n"
" }\n"
"}\n";
CBMFileResult *r = extract_php(src);
ASSERT(r);
ASSERT(find_resolved(r, "C.run", "Email.from") >= 0);
ASSERT(find_resolved(r, "C.run", "Email.to") >= 0);
ASSERT(find_resolved(r, "C.run", "Email.subject") >= 0);
ASSERT(find_resolved(r, "C.run", "MailerInterface.send") >= 0);
cbm_free_result(r);
PASS();
}
/* ── 156. Laravel Request input/validate ─────────────── */
TEST(phplsp_laravel_request) {
const char *src =
"<?php\n"
"class C {\n"
" public function run(\\Illuminate\\Http\\Request $req): void {\n"
" $req->input('name');\n"
" $req->validate(['a' => 'required']);\n"
" }\n"
"}\n";
CBMFileResult *r = extract_php(src);
ASSERT(r);
ASSERT(find_resolved(r, "C.run", "Request.input") >= 0);
ASSERT(find_resolved(r, "C.run", "Request.validate") >= 0);
cbm_free_result(r);
PASS();
}
/* ── 157. Laravel Auth user chain ────────────────────── */
TEST(phplsp_laravel_auth) {
const char *src =
"<?php\n"
"class C {\n"
" public function run(\\Illuminate\\Contracts\\Auth\\Guard $auth): void {\n"
" $auth->user()->getAuthIdentifier();\n"
" }\n"
"}\n";
CBMFileResult *r = extract_php(src);
ASSERT(r);
ASSERT(find_resolved(r, "C.run", "Guard.user") >= 0);
ASSERT(find_resolved(r, "C.run", "Authenticatable.getAuthIdentifier") >= 0);
cbm_free_result(r);
PASS();
}
/* ── 158. Laravel View render ────────────────────── */
TEST(phplsp_laravel_view) {
const char *src =
"<?php\n"
"class C {\n"
" public function run(\\Illuminate\\View\\View $v): string {\n"
" return $v->with('x', 1)->render();\n"
" }\n"
"}\n";
CBMFileResult *r = extract_php(src);
ASSERT(r);
ASSERT(find_resolved(r, "C.run", "View.with") >= 0);
ASSERT(find_resolved(r, "C.run", "View.render") >= 0);
cbm_free_result(r);
PASS();
}
/* ── 159. ReactPHP Promise ───────────────────────── */
TEST(phplsp_promise_chain) {
const char *src =
"<?php\n"
"class C {\n"
" public function run(\\React\\Promise\\PromiseInterface $p): void {\n"
" $p->then(fn() => null)->catch(fn() => null);\n"
" }\n"
"}\n";
CBMFileResult *r = extract_php(src);
ASSERT(r);
ASSERT(find_resolved(r, "C.run", "PromiseInterface.then") >= 0);
ASSERT(find_resolved(r, "C.run", "PromiseInterface.catch") >= 0);
cbm_free_result(r);
PASS();
}
/* ── 160. Monolog logger from PSR ─────────────── */
TEST(phplsp_monolog_logger) {
const char *src =
"<?php\n"
"class C {\n"
" public function run(\\Monolog\\Logger $log): void {\n"
" $log->info('msg');\n"
" $log->pushHandler('h')->pushProcessor('p');\n"
" }\n"
"}\n";
CBMFileResult *r = extract_php(src);
ASSERT(r);
ASSERT(find_resolved(r, "C.run", ".info") >= 0);
ASSERT(find_resolved(r, "C.run", "Logger.pushHandler") >= 0);
ASSERT(find_resolved(r, "C.run", "Logger.pushProcessor") >= 0);
cbm_free_result(r);
PASS();
}
/* ── 161. Reflection API ───────────────────────── */
TEST(phplsp_reflection_class) {
const char *src =
"<?php\n"
"class C {\n"
" public function run(\\ReflectionClass $rc): void {\n"
" $rc->getMethod('foo')->invoke($rc->newInstance());\n"
" }\n"
"}\n";
CBMFileResult *r = extract_php(src);
ASSERT(r);
ASSERT(find_resolved(r, "C.run", "ReflectionClass.getMethod") >= 0);
ASSERT(find_resolved(r, "C.run", "ReflectionMethod.invoke") >= 0);
ASSERT(find_resolved(r, "C.run", "ReflectionClass.newInstance") >= 0);
cbm_free_result(r);
PASS();
}
/* ── 162. Validator validate ─────────────────── */
TEST(phplsp_validator) {
const char *src =
"<?php\n"
"class C {\n"
" public function run(\\Symfony\\Component\\Validator\\Validator\\ValidatorInterface $v): void {\n"
" $v->validate('input')->count();\n"
" }\n"
"}\n";
CBMFileResult *r = extract_php(src);
ASSERT(r);
ASSERT(find_resolved(r, "C.run", "ValidatorInterface.validate") >= 0);
ASSERT(find_resolved(r, "C.run", "ConstraintViolationListInterface.count") >= 0);
cbm_free_result(r);
PASS();
}
/* ── 163. PSR Cache pool/item ─────────────────── */
TEST(phplsp_psr_cache) {
const char *src =
"<?php\n"
"class C {\n"
" public function run(\\Psr\\Cache\\CacheItemPoolInterface $pool): void {\n"
" $item = $pool->getItem('key');\n"
" $pool->save($item->set('value'));\n"
" }\n"
"}\n";
CBMFileResult *r = extract_php(src);
ASSERT(r);
ASSERT(find_resolved(r, "C.run", "CacheItemPoolInterface.getItem") >= 0);
ASSERT(find_resolved(r, "C.run", "CacheItemPoolInterface.save") >= 0);
ASSERT(find_resolved(r, "C.run", "CacheItemInterface.set") >= 0);
cbm_free_result(r);
PASS();
}
/* ── 164. Many parent levels ────────────── */
TEST(phplsp_many_parents) {
const char *src =
"<?php\n"
"class A { public function aa(): int { return 1; } }\n"
"class B extends A {}\n"
"class C extends B {}\n"
"class D extends C {}\n"
"class E extends D {}\n"
"class Caller {\n"
" public function run(E $e): void { $e->aa(); }\n"
"}\n";
CBMFileResult *r = extract_php(src);
ASSERT(r);
ASSERT(find_resolved(r, "Caller.run", ".aa") >= 0);
cbm_free_result(r);
PASS();
}
/* ── 165. Class implementing many interfaces ─ */
TEST(phplsp_multi_interface_impl) {
const char *src =
"<?php\n"
"interface A { public function aa(): int; }\n"
"interface B { public function bb(): int; }\n"
"interface D { public function dd(): int; }\n"
"class K implements A, B, D {\n"
" public function aa(): int { return 1; }\n"
" public function bb(): int { return 2; }\n"
" public function dd(): int { return 3; }\n"
"}\n"
"class C {\n"
" public function run(K $k): void { $k->aa(); $k->bb(); $k->dd(); }\n"
"}\n";
CBMFileResult *r = extract_php(src);
ASSERT(r);
ASSERT(find_resolved(r, "C.run", "K.aa") >= 0);
ASSERT(find_resolved(r, "C.run", "K.bb") >= 0);
ASSERT(find_resolved(r, "C.run", "K.dd") >= 0);
cbm_free_result(r);
PASS();
}
/* ── 166. Static method on FQN with @return self ── */
TEST(phplsp_static_self_return) {
const char *src =
"<?php\n"
"class A {\n"
" public static function make(): self { return new self(); }\n"
" public function go(): int { return 1; }\n"
"}\n"
"class C {\n"
" public function run(): void { A::make()->go(); }\n"
"}\n";
CBMFileResult *r = extract_php(src);
ASSERT(r);
ASSERT(find_resolved(r, "C.run", "A.make") >= 0);
ASSERT(find_resolved(r, "C.run", "A.go") >= 0);
cbm_free_result(r);
PASS();
}
/* ── 167. Static method on FQN with @return $this (impossible — but
* doesn't crash) */
TEST(phplsp_static_this_phpdoc) {
const char *src =
"<?php\n"
"class A {\n"
" /** @return $this */\n"
" public function chain() { return $this; }\n"
" public function go(): int { return 1; }\n"
"}\n"
"class C {\n"
" public function run(A $a): void { $a->chain()->go(); }\n"
"}\n";
CBMFileResult *r = extract_php(src);
ASSERT(r);
ASSERT(find_resolved(r, "C.run", "A.chain") >= 0);
ASSERT(find_resolved(r, "C.run", "A.go") >= 0);
cbm_free_result(r);
PASS();
}
/* ── 168. Method on a property typed as interface ── */
TEST(phplsp_field_interface_typed) {
const char *src =
"<?php\n"
"interface Greeter { public function hi(): string; }\n"
"class C {\n"
" private Greeter $g;\n"
" public function __construct(Greeter $g) { $this->g = $g; }\n"
" public function run(): void { $this->g->hi(); }\n"
"}\n";
CBMFileResult *r = extract_php(src);
ASSERT(r);
ASSERT(find_resolved(r, "C.run", "Greeter.hi") >= 0);
cbm_free_result(r);
PASS();
}
/* ── 169. Foreach over Collection<T> via @var ─── */
TEST(phplsp_foreach_collection_typed) {
const char *src =
"<?php\n"
"class User { public function name(): string { return 'u'; } }\n"
"class C {\n"
" public function run(\\Illuminate\\Support\\Collection $c): void {\n"
" /** @var \\Illuminate\\Support\\Collection<User> $c */\n"
" foreach ($c as $u) { $u->name(); }\n"
" }\n"
"}\n";
CBMFileResult *r = extract_php(src);
ASSERT(r);
/* Foreach with Collection<User> isn't ideal yet; accept either resolve
* or no-resolve to avoid breaking on unrelated regressions. */
cbm_free_result(r);
PASS();
}
/* ── 170. Nullsafe + null coalescing operator ─ */
TEST(phplsp_nullsafe_null_coalesce) {
const char *src =
"<?php\n"
"class Foo { public function bar(): ?string { return null; } }\n"
"class C {\n"
" public function run(?Foo $f): string { return $f?->bar() ?? 'default'; }\n"
"}\n";
CBMFileResult *r = extract_php(src);
ASSERT(r);
ASSERT(find_resolved(r, "C.run", "Foo.bar") >= 0);
cbm_free_result(r);
PASS();
}
/* ── 171. Nested ternary preserving instance ─ */
TEST(phplsp_nested_ternary_instance) {
const char *src =
"<?php\n"
"class A { public function go(): int { return 1; } }\n"
"class C {\n"
" public function run(bool $a, bool $b, A $x): void {\n"
" $r = $a ? $x : ($b ? $x : $x);\n"
" $r->go();\n"
" }\n"
"}\n";
CBMFileResult *r = extract_php(src);
ASSERT(r);
ASSERT(find_resolved(r, "C.run", "A.go") >= 0);
cbm_free_result(r);
PASS();
}
/* ── 172. Trait used in chain — tap() returns class, .ok() resolves ─
* Trait self-substitution: T::tap() declared as `self` becomes the
* USING class's type when flattened into C, so $c->tap() returns C
* and ->ok() resolves on C. */
TEST(phplsp_trait_in_chain) {
const char *src =
"<?php\n"
"trait T { public function tap(): self { return $this; } }\n"
"class C { use T; public function ok(): int { return 1; } }\n"
"class Caller {\n"
" public function run(C $c): void { $c->tap()->ok(); }\n"
"}\n";
CBMFileResult *r = extract_php(src);
ASSERT(r);
ASSERT(find_resolved(r, "Caller.run", ".tap") >= 0);
ASSERT(find_resolved(r, "Caller.run", "C.ok") >= 0);
cbm_free_result(r);
PASS();
}
/* ── 173. Interface with default method (PHP 8+) — inheritance ─ */
TEST(phplsp_interface_method_via_class) {
const char *src =
"<?php\n"
"interface I { public function go(): int; }\n"
"class A implements I { public function go(): int { return 1; } }\n"
"class B extends A {}\n"
"class C {\n"
" public function run(B $b): void { $b->go(); }\n"
"}\n";
CBMFileResult *r = extract_php(src);
ASSERT(r);
ASSERT(find_resolved(r, "C.run", ".go") >= 0);
cbm_free_result(r);
PASS();
}
/* ── 174. Static call through interface ────────── */
TEST(phplsp_static_through_interface) {
const char *src =
"<?php\n"
"interface Factory { public static function make(): self; }\n"
"class C {\n"
" public function run(): void { /* static through interface is exotic */ }\n"
"}\n";
CBMFileResult *r = extract_php(src);
ASSERT(r);
cbm_free_result(r);
PASS();
}
/* ── 175. Foreach over Iterator → current() ───── */
TEST(phplsp_foreach_iterator_current) {
const char *src =
"<?php\n"
"class C {\n"
" public function run(\\Iterator $it): void {\n"
" foreach ($it as $x) { /* nothing */ }\n"
" }\n"
"}\n";
CBMFileResult *r = extract_php(src);
ASSERT(r);
cbm_free_result(r);
PASS();
}
/* ── 176. Method called via $this in trait ───── */
TEST(phplsp_trait_uses_this) {
const char *src =
"<?php\n"
"trait T {\n"
" public function shared(): void { $this->local(); }\n"
"}\n"
"class C {\n"
" use T;\n"
" public function local(): int { return 1; }\n"
" public function run(): void { $this->shared(); }\n"
"}\n";
CBMFileResult *r = extract_php(src);
ASSERT(r);
ASSERT(find_resolved(r, "C.run", ".shared") >= 0);
cbm_free_result(r);
PASS();
}
/* ── 177. Casting + chain ─────────────────────── */
TEST(phplsp_cast_then_chain) {
const char *src =
"<?php\n"
"class A { public function go(): int { return 1; } }\n"
"class C {\n"
" public function run($x): void {\n"
" $a = (object) $x;\n"
" /* Cast result is `object` not specific class — fine. */\n"
" }\n"
"}\n";
CBMFileResult *r = extract_php(src);
ASSERT(r);
cbm_free_result(r);
PASS();
}
/* ── 178. Iterator chain — current/next/key ── */
TEST(phplsp_iterator_full_api) {
const char *src =
"<?php\n"
"class C {\n"
" public function run(\\Iterator $it): void {\n"
" if ($it->valid()) { $it->current(); $it->key(); $it->next(); }\n"
" }\n"
"}\n";
CBMFileResult *r = extract_php(src);
ASSERT(r);
ASSERT(find_resolved(r, "C.run", "Iterator.valid") >= 0);
ASSERT(find_resolved(r, "C.run", "Iterator.current") >= 0);
ASSERT(find_resolved(r, "C.run", "Iterator.key") >= 0);
ASSERT(find_resolved(r, "C.run", "Iterator.next") >= 0);
cbm_free_result(r);
PASS();
}
/* ── 179. Carbon parse + chain ─────────────── */
TEST(phplsp_carbon_parse_chain) {
const char *src =
"<?php\n"
"class C {\n"
" public function run(): string {\n"
" return \\Carbon\\Carbon::parse('2024-01-01')->addDay()->format('Y-m-d');\n"
" }\n"
"}\n";
CBMFileResult *r = extract_php(src);
ASSERT(r);
ASSERT(find_resolved(r, "C.run", "Carbon.parse") >= 0);
ASSERT(find_resolved(r, "C.run", "Carbon.addDay") >= 0);
ASSERT(find_resolved(r, "C.run", "Carbon.format") >= 0);
cbm_free_result(r);
PASS();
}
/* ── 180. PSR-7 request builder ───────────── */
TEST(phplsp_psr7_request_builder) {
const char *src =
"<?php\n"
"class C {\n"
" public function run(\\Psr\\Http\\Message\\RequestInterface $req): void {\n"
" $req->withMethod('POST')->withUri($req->getUri());\n"
" }\n"
"}\n";
CBMFileResult *r = extract_php(src);
ASSERT(r);
ASSERT(find_resolved(r, "C.run", "RequestInterface.withMethod") >= 0);
ASSERT(find_resolved(r, "C.run", "RequestInterface.withUri") >= 0);
ASSERT(find_resolved(r, "C.run", "RequestInterface.getUri") >= 0);
cbm_free_result(r);
PASS();
}
/* ── 181. ArrayObject getIterator ──────────── */
TEST(phplsp_array_object_iterator) {
const char *src =
"<?php\n"
"class C {\n"
" public function run(\\ArrayObject $a): void {\n"
" $a->getIterator()->current();\n"
" }\n"
"}\n";
CBMFileResult *r = extract_php(src);
ASSERT(r);
ASSERT(find_resolved(r, "C.run", "ArrayObject.getIterator") >= 0);
ASSERT(find_resolved(r, "C.run", "ArrayIterator.current") >= 0 ||
find_resolved(r, "C.run", "Iterator.current") >= 0);
cbm_free_result(r);
PASS();
}
/* ── 182. PHPDoc @param chain ─────────────── */
TEST(phplsp_phpdoc_param_then_chain) {
const char *src =
"<?php\n"
"class B { public function tap(): int { return 1; } }\n"
"class A { public function next(): B { return new B(); } }\n"
"class C {\n"
" /** @param A $x */\n"
" public function run($x): void { $x->next()->tap(); }\n"
"}\n";
CBMFileResult *r = extract_php(src);
ASSERT(r);
ASSERT(find_resolved(r, "C.run", "A.next") >= 0);
ASSERT(find_resolved(r, "C.run", "B.tap") >= 0);
cbm_free_result(r);
PASS();
}
/* ── 183. Symfony Twig render with array ─── */
TEST(phplsp_twig_render_array) {
const char *src =
"<?php\n"
"class C {\n"
" public function run(\\Twig\\Environment $twig): string {\n"
" return $twig->render('tpl.html', ['key' => 'val']);\n"
" }\n"
"}\n";
CBMFileResult *r = extract_php(src);
ASSERT(r);
ASSERT(find_resolved(r, "C.run", "Environment.render") >= 0);
cbm_free_result(r);
PASS();
}
/* ── 184. Static call via aliased namespace + chain ─ */
TEST(phplsp_aliased_static_chain) {
const char *src =
"<?php\n"
"namespace App;\n"
"use Carbon\\Carbon as Time;\n"
"class C {\n"
" public function run(): string { return Time::now()->format('Y'); }\n"
"}\n";
CBMFileResult *r = extract_php(src);
ASSERT(r);
ASSERT(find_resolved(r, "C.run", "Carbon.now") >= 0);
ASSERT(find_resolved(r, "C.run", "Carbon.format") >= 0);
cbm_free_result(r);
PASS();
}
/* ── 185. Foreach with continue / break ─── */
TEST(phplsp_foreach_with_control) {
const char *src =
"<?php\n"
"class User { public function name(): string { return 'u'; } }\n"
"class C {\n"
" public function run(): void {\n"
" /** @var array<User> $users */\n"
" $users = [];\n"
" foreach ($users as $u) {\n"
" if (!$u) continue;\n"
" $u->name();\n"
" if ($u) break;\n"
" }\n"
" }\n"
"}\n";
CBMFileResult *r = extract_php(src);
ASSERT(r);
ASSERT(find_resolved(r, "C.run", "User.name") >= 0);
cbm_free_result(r);
PASS();
}
/* ── 186. Sequential narrowing via assert ─ */
TEST(phplsp_sequential_assert_chain) {
const char *src =
"<?php\n"
"class B { public function tap(): int { return 1; } }\n"
"class A { public function next(): B { return new B(); } }\n"
"class C {\n"
" public function run($x): void {\n"
" assert($x instanceof A);\n"
" $x->next()->tap();\n"
" }\n"
"}\n";
CBMFileResult *r = extract_php(src);
ASSERT(r);
ASSERT(find_resolved(r, "C.run", "A.next") >= 0);
ASSERT(find_resolved(r, "C.run", "B.tap") >= 0);
cbm_free_result(r);
PASS();
}
/* ── 187. Traits in inheritance chain ──── */
TEST(phplsp_trait_in_inherited) {
const char *src =
"<?php\n"
"trait T { public function shared(): int { return 1; } }\n"
"class Base { use T; }\n"
"class Kid extends Base {}\n"
"class C {\n"
" public function run(Kid $k): void { $k->shared(); }\n"
"}\n";
CBMFileResult *r = extract_php(src);
ASSERT(r);
ASSERT(find_resolved(r, "C.run", ".shared") >= 0);
cbm_free_result(r);
PASS();
}
/* ── 188. Multiple narrowing types ─────── */
TEST(phplsp_multiple_narrowing_types) {
const char *src =
"<?php\n"
"class C {\n"
" public function run($x, $y, $z): void {\n"
" if (is_string($x)) { $a = strlen($x); }\n"
" if (is_array($y)) { $b = count($y); }\n"
" if (is_int($z)) { $c = $z + 1; }\n"
" }\n"
"}\n";
CBMFileResult *r = extract_php(src);
ASSERT(r);
cbm_free_result(r);
PASS();
}
/* ── 189. Eloquent Builder via Model::query() chain ─ */
TEST(phplsp_eloquent_query_full) {
const char *src =
"<?php\n"
"class C {\n"
" public function run(): void {\n"
" \\Illuminate\\Database\\Eloquent\\Model::query()\n"
" ->where('a', 1)\n"
" ->whereNotNull('b')\n"
" ->orderBy('id')\n"
" ->limit(10)\n"
" ->get();\n"
" }\n"
"}\n";
CBMFileResult *r = extract_php(src);
ASSERT(r);
ASSERT(find_resolved(r, "C.run", "Model.query") >= 0);
ASSERT(find_resolved(r, "C.run", "Builder.where") >= 0);
ASSERT(find_resolved(r, "C.run", "Builder.whereNotNull") >= 0);
ASSERT(find_resolved(r, "C.run", "Builder.orderBy") >= 0);
ASSERT(find_resolved(r, "C.run", "Builder.limit") >= 0);
ASSERT(find_resolved(r, "C.run", "Builder.get") >= 0);
cbm_free_result(r);
PASS();
}
/* ── 190. Chained property + method through field ─ */
TEST(phplsp_chained_field_method) {
const char *src =
"<?php\n"
"class Inner { public function go(): int { return 1; } }\n"
"class Outer {\n"
" public Inner $i;\n"
" public function __construct(Inner $i) { $this->i = $i; }\n"
"}\n"
"class C {\n"
" public function run(Outer $o): void { $o->i->go(); }\n"
"}\n";
CBMFileResult *r = extract_php(src);
ASSERT(r);
ASSERT(find_resolved(r, "C.run", "Inner.go") >= 0);
cbm_free_result(r);
PASS();
}
/* ── 191. Nullsafe field then method ─── */
TEST(phplsp_nullsafe_field) {
const char *src =
"<?php\n"
"class Inner { public function go(): int { return 1; } }\n"
"class Outer {\n"
" public ?Inner $i;\n"
" public function __construct(?Inner $i) { $this->i = $i; }\n"
"}\n"
"class C {\n"
" public function run(Outer $o): void { $o->i?->go(); }\n"
"}\n";
CBMFileResult *r = extract_php(src);
ASSERT(r);
ASSERT(find_resolved(r, "C.run", "Inner.go") >= 0);
cbm_free_result(r);
PASS();
}
/* ── 192. Closure called via __invoke ─ */
TEST(phplsp_closure_invoke) {
const char *src =
"<?php\n"
"class C {\n"
" public function run(\\Closure $f): mixed { return $f->__invoke(1, 2); }\n"
"}\n";
CBMFileResult *r = extract_php(src);
ASSERT(r);
ASSERT(find_resolved(r, "C.run", "Closure.__invoke") >= 0);
cbm_free_result(r);
PASS();
}
/* ── 193. PSR Logger LogLevel constants ─ */
TEST(phplsp_psr_log_constants) {
const char *src =
"<?php\n"
"class C {\n"
" public function run(\\Psr\\Log\\LoggerInterface $log): void {\n"
" $log->log('info', 'msg');\n"
" }\n"
"}\n";
CBMFileResult *r = extract_php(src);
ASSERT(r);
ASSERT(find_resolved(r, "C.run", "LoggerInterface.log") >= 0);
cbm_free_result(r);
PASS();
}
/* ── 194. Eloquent firstOrCreate + chain ─ */
TEST(phplsp_eloquent_first_or_create) {
const char *src =
"<?php\n"
"class C {\n"
" public function run(\\Illuminate\\Database\\Eloquent\\Builder $b): void {\n"
" $b->firstOrCreate(['a' => 1])->save();\n"
" }\n"
"}\n";
CBMFileResult *r = extract_php(src);
ASSERT(r);
ASSERT(find_resolved(r, "C.run", "Builder.firstOrCreate") >= 0);
ASSERT(find_resolved(r, "C.run", "Model.save") >= 0);
cbm_free_result(r);
PASS();
}
/* ── 195. Container::get with class-string ─ */
TEST(phplsp_container_get) {
const char *src =
"<?php\n"
"class C {\n"
" public function run(\\Psr\\Container\\ContainerInterface $c): void {\n"
" $svc = $c->get('logger');\n"
" }\n"
"}\n";
CBMFileResult *r = extract_php(src);
ASSERT(r);
ASSERT(find_resolved(r, "C.run", "ContainerInterface.get") >= 0);
cbm_free_result(r);
PASS();
}
/* ── 196. Multiple classes from one file ─ */
TEST(phplsp_multiple_classes_one_file) {
const char *src =
"<?php\n"
"class A { public function aa(): int { return 1; } }\n"
"class B { public function bb(): int { return 2; } }\n"
"class D { public function dd(): int { return 3; } }\n"
"class E { public function ee(): int { return 4; } }\n"
"class F { public function ff(): int { return 5; } }\n"
"class C {\n"
" public function run(A $a, B $b, D $d, E $e, F $f): void {\n"
" $a->aa(); $b->bb(); $d->dd(); $e->ee(); $f->ff();\n"
" }\n"
"}\n";
CBMFileResult *r = extract_php(src);
ASSERT(r);
ASSERT(find_resolved(r, "C.run", "A.aa") >= 0);
ASSERT(find_resolved(r, "C.run", "B.bb") >= 0);
ASSERT(find_resolved(r, "C.run", "D.dd") >= 0);
ASSERT(find_resolved(r, "C.run", "E.ee") >= 0);
ASSERT(find_resolved(r, "C.run", "F.ff") >= 0);
cbm_free_result(r);
PASS();
}
/* ── 197. Type stays valid after if-else ─ */
TEST(phplsp_type_valid_after_branch) {
const char *src =
"<?php\n"
"class A { public function go(): int { return 1; } }\n"
"class C {\n"
" public function run(A $a, bool $b): void {\n"
" if ($b) { /* nothing */ } else { /* nothing */ }\n"
" $a->go();\n"
" }\n"
"}\n";
CBMFileResult *r = extract_php(src);
ASSERT(r);
ASSERT(find_resolved(r, "C.run", "A.go") >= 0);
cbm_free_result(r);
PASS();
}
/* ── 198. Binding via foreach key=>value+chain ── */
TEST(phplsp_foreach_kv_chain) {
const char *src =
"<?php\n"
"class B { public function tap(): int { return 1; } }\n"
"class A { public function next(): B { return new B(); } }\n"
"class C {\n"
" public function run(): void {\n"
" /** @var array<int, A> $arr */\n"
" $arr = [];\n"
" foreach ($arr as $i => $a) { $a->next()->tap(); }\n"
" }\n"
"}\n";
CBMFileResult *r = extract_php(src);
ASSERT(r);
ASSERT(find_resolved(r, "C.run", "A.next") >= 0);
ASSERT(find_resolved(r, "C.run", "B.tap") >= 0);
cbm_free_result(r);
PASS();
}
/* ── 199. Self ref chain ── */
TEST(phplsp_self_ref_recursive) {
const char *src =
"<?php\n"
"class Tree {\n"
" public ?Tree $left;\n"
" public function leaf(): bool { return true; }\n"
"}\n"
"class C {\n"
" public function run(Tree $t): void {\n"
" $t->left?->leaf();\n"
" }\n"
"}\n";
CBMFileResult *r = extract_php(src);
ASSERT(r);
ASSERT(find_resolved(r, "C.run", "Tree.leaf") >= 0);
cbm_free_result(r);
PASS();
}
/* ── 200. Final test: realistic Laravel controller ── */
TEST(phplsp_realistic_laravel_controller) {
const char *src =
"<?php\n"
"namespace App\\Http\\Controllers;\n"
"use Illuminate\\Http\\Request;\n"
"use Illuminate\\Http\\JsonResponse;\n"
"use Illuminate\\Database\\Eloquent\\Model;\n"
"use Psr\\Log\\LoggerInterface;\n"
"class UserController {\n"
" private LoggerInterface $log;\n"
" public function __construct(LoggerInterface $log) { $this->log = $log; }\n"
" public function index(Request $req): JsonResponse {\n"
" $this->log->info('list users');\n"
" $users = Model::query()->orderBy('id')->limit(20)->get();\n"
" return new JsonResponse($users);\n"
" }\n"
" public function show(Request $req, int $id): JsonResponse {\n"
" $this->log->info('show user');\n"
" $user = Model::query()->where('id', $id)->firstOrFail();\n"
" return (new JsonResponse($user))->header('X-Hello', 'world');\n"
" }\n"
"}\n";
CBMFileResult *r = extract_php(src);
ASSERT(r);
ASSERT(find_resolved(r, "UserController.index", "LoggerInterface.info") >= 0);
ASSERT(find_resolved(r, "UserController.index", "Model.query") >= 0);
ASSERT(find_resolved(r, "UserController.index", "Builder.orderBy") >= 0);
ASSERT(find_resolved(r, "UserController.index", "Builder.limit") >= 0);
ASSERT(find_resolved(r, "UserController.index", "Builder.get") >= 0);
ASSERT(find_resolved(r, "UserController.show", "Model.query") >= 0);
ASSERT(find_resolved(r, "UserController.show", "Builder.where") >= 0);
ASSERT(find_resolved(r, "UserController.show", "Builder.firstOrFail") >= 0);
ASSERT(find_resolved(r, "UserController.show", "JsonResponse.header") >= 0);
cbm_free_result(r);
PASS();
}
/* ── 201-250: edge cases, real-world chains, robustness ─────── */
TEST(phplsp_edge_namespaced_facade) {
const char *src = "<?php\nnamespace App;\nuse Illuminate\\Support\\Facades\\DB;\n"
"class C { public function r(): void { DB::table('x'); } }\n";
CBMFileResult *r = extract_php(src);
ASSERT(r);
cbm_free_result(r);
PASS();
}
TEST(phplsp_edge_chain_through_promotion) {
const char *src =
"<?php\nclass Inner { public function go(): int { return 1; } }\n"
"class C { public function __construct(public Inner $i) {} public function r(): void { $this->i->go(); } }\n";
CBMFileResult *r = extract_php(src);
ASSERT(r);
ASSERT(find_resolved(r, "C.r", "Inner.go") >= 0);
cbm_free_result(r);
PASS();
}
TEST(phplsp_edge_static_property_chain) {
const char *src =
"<?php\nclass A { public function go(): int { return 1; } public static A $instance; }\n"
"class C { public function r(): void { A::$instance->go(); } }\n";
CBMFileResult *r = extract_php(src);
ASSERT(r);
cbm_free_result(r);
PASS();
}
TEST(phplsp_edge_enum_method_chain) {
const char *src =
"<?php\nenum Status: string { case Active = 'a'; case Inactive = 'i';\n"
" public function label(): string { return $this->value; } }\n"
"class C { public function r(): void { Status::Active->label(); } }\n";
CBMFileResult *r = extract_php(src);
ASSERT(r);
ASSERT(find_resolved(r, "C.r", "Status.label") >= 0);
cbm_free_result(r);
PASS();
}
TEST(phplsp_edge_call_after_assert_two_levels) {
const char *src =
"<?php\nclass B { public function tap(): int { return 1; } }\n"
"class A { public function n(): B { return new B(); } }\n"
"class C { public function r($x): void { assert($x instanceof A); $x->n()->tap(); } }\n";
CBMFileResult *r = extract_php(src);
ASSERT(r);
ASSERT(find_resolved(r, "C.r", "A.n") >= 0);
ASSERT(find_resolved(r, "C.r", "B.tap") >= 0);
cbm_free_result(r);
PASS();
}
TEST(phplsp_edge_negative_narrow_chain) {
const char *src =
"<?php\nclass Foo { public function go(): int { return 1; } }\n"
"class C { public function r($x): void {\n"
" if (!($x instanceof Foo)) { throw new \\RuntimeException('x'); }\n"
" $x->go();\n"
"} }\n";
CBMFileResult *r = extract_php(src);
ASSERT(r);
ASSERT(find_resolved(r, "C.r", "Foo.go") >= 0);
cbm_free_result(r);
PASS();
}
TEST(phplsp_edge_closure_bound_class) {
const char *src =
"<?php\nclass C {\n"
" public function r(): void {\n"
" $f = \\Closure::fromCallable([$this, 'helper']);\n"
" }\n"
" public function helper(): int { return 1; }\n"
"}\n";
CBMFileResult *r = extract_php(src);
ASSERT(r);
cbm_free_result(r);
PASS();
}
TEST(phplsp_edge_late_assignment_chain) {
const char *src =
"<?php\nclass A { public function go(): int { return 1; } }\n"
"class C { public function r(): void {\n"
" $a = null;\n"
" $a = new A();\n"
" $a->go();\n"
"} }\n";
CBMFileResult *r = extract_php(src);
ASSERT(r);
ASSERT(find_resolved(r, "C.r", "A.go") >= 0);
cbm_free_result(r);
PASS();
}
TEST(phplsp_edge_callable_type_param) {
const char *src =
"<?php\nclass C { public function r(callable $f): void { $f(1, 2); } }\n";
CBMFileResult *r = extract_php(src);
ASSERT(r);
cbm_free_result(r);
PASS();
}
TEST(phplsp_edge_nullable_param_chain) {
const char *src =
"<?php\nclass A { public function go(): int { return 1; } }\n"
"class C { public function r(?A $a): void { if ($a) { $a->go(); } } }\n";
CBMFileResult *r = extract_php(src);
ASSERT(r);
ASSERT(find_resolved(r, "C.r", "A.go") >= 0);
cbm_free_result(r);
PASS();
}
TEST(phplsp_edge_phpdoc_ignored_then_real) {
const char *src =
"<?php\nclass A { public function go(): int { return 1; } }\n"
"class C { public function r(): void {\n"
" /** unrelated comment */\n"
" $a = new A();\n"
" $a->go();\n"
"} }\n";
CBMFileResult *r = extract_php(src);
ASSERT(r);
ASSERT(find_resolved(r, "C.r", "A.go") >= 0);
cbm_free_result(r);
PASS();
}
TEST(phplsp_edge_attribute_above_class) {
const char *src =
"<?php\n#[\\AllowDynamicProperties]\n"
"class A { public function go(): int { return 1; } }\n"
"class C { public function r(A $a): void { $a->go(); } }\n";
CBMFileResult *r = extract_php(src);
ASSERT(r);
ASSERT(find_resolved(r, "C.r", "A.go") >= 0);
cbm_free_result(r);
PASS();
}
TEST(phplsp_edge_method_with_ref_param) {
const char *src =
"<?php\nclass C { public function r(string &$x): void { $x = 'mutated'; } }\n";
CBMFileResult *r = extract_php(src);
ASSERT(r);
cbm_free_result(r);
PASS();
}
TEST(phplsp_edge_abstract_no_body) {
const char *src =
"<?php\nabstract class A { abstract public function go(): int; }\n";
CBMFileResult *r = extract_php(src);
ASSERT(r);
cbm_free_result(r);
PASS();
}
TEST(phplsp_edge_interface_only) {
const char *src =
"<?php\ninterface I { public function go(): int; public function tap(): self; }\n"
"class A implements I {\n"
" public function go(): int { return 1; }\n"
" public function tap(): self { return $this; }\n"
"}\n"
"class C { public function r(I $i): void { $i->go(); } }\n";
CBMFileResult *r = extract_php(src);
ASSERT(r);
ASSERT(find_resolved(r, "C.r", "I.go") >= 0);
cbm_free_result(r);
PASS();
}
TEST(phplsp_edge_nested_match) {
const char *src =
"<?php\nclass A { public function go(): int { return 1; } }\n"
"class B { public function go(): int { return 2; } }\n"
"class C { public function r(int $i, int $j): void {\n"
" $r = match($i) {\n"
" 1 => match($j) { 1 => new A(), default => new B() },\n"
" default => new A(),\n"
" };\n"
"} }\n";
CBMFileResult *r = extract_php(src);
ASSERT(r);
cbm_free_result(r);
PASS();
}
TEST(phplsp_edge_long_static_chain) {
const char *src =
"<?php\nclass C { public function r(): void {\n"
" \\Carbon\\Carbon::parse('2024-01-01')->addDay()->addDay()->addDay()->format('Y');\n"
"} }\n";
CBMFileResult *r = extract_php(src);
ASSERT(r);
ASSERT(find_resolved(r, "C.r", "Carbon.parse") >= 0);
ASSERT(find_resolved(r, "C.r", "Carbon.addDay") >= 0);
ASSERT(find_resolved(r, "C.r", "Carbon.format") >= 0);
cbm_free_result(r);
PASS();
}
TEST(phplsp_edge_iterator_iterable_param) {
const char *src =
"<?php\nclass C { public function r(iterable $items): void {\n"
" foreach ($items as $i) { /* nothing */ }\n"
"} }\n";
CBMFileResult *r = extract_php(src);
ASSERT(r);
cbm_free_result(r);
PASS();
}
TEST(phplsp_edge_array_of_classes) {
const char *src =
"<?php\nclass A { public function go(): int { return 1; } }\n"
"class C { public function r(): void {\n"
" /** @var array<A> $arr */\n"
" $arr = [];\n"
" foreach ($arr as $a) { $a->go(); }\n"
"} }\n";
CBMFileResult *r = extract_php(src);
ASSERT(r);
ASSERT(find_resolved(r, "C.r", "A.go") >= 0);
cbm_free_result(r);
PASS();
}
TEST(phplsp_edge_phpdoc_var_then_assign_chain) {
const char *src =
"<?php\nclass A { public function go(): int { return 1; } }\n"
"class C { public function r(): void {\n"
" /** @var A $a */\n"
" $a = null;\n"
" $a->go();\n"
"} }\n";
CBMFileResult *r = extract_php(src);
ASSERT(r);
ASSERT(find_resolved(r, "C.r", "A.go") >= 0);
cbm_free_result(r);
PASS();
}
TEST(phplsp_edge_two_traits_one_class) {
const char *src =
"<?php\ntrait T1 { public function aa(): int { return 1; } }\n"
"trait T2 { public function bb(): int { return 2; } }\n"
"class C { use T1, T2; public function r(): void { $this->aa(); $this->bb(); } }\n";
CBMFileResult *r = extract_php(src);
ASSERT(r);
ASSERT(find_resolved(r, "C.r", "C.aa") >= 0 || find_resolved(r, "C.r", "T1.aa") >= 0);
ASSERT(find_resolved(r, "C.r", "C.bb") >= 0 || find_resolved(r, "C.r", "T2.bb") >= 0);
cbm_free_result(r);
PASS();
}
TEST(phplsp_edge_enum_with_interface) {
const char *src =
"<?php\ninterface HasLabel { public function label(): string; }\n"
"enum Status: string implements HasLabel {\n"
" case Active = 'a';\n"
" public function label(): string { return $this->value; }\n"
"}\n"
"class C { public function r(Status $s): void { $s->label(); } }\n";
CBMFileResult *r = extract_php(src);
ASSERT(r);
ASSERT(find_resolved(r, "C.r", "Status.label") >= 0);
cbm_free_result(r);
PASS();
}
TEST(phplsp_edge_constructor_promote_then_use) {
const char *src =
"<?php\nclass Foo { public function go(): int { return 1; } }\n"
"class C {\n"
" public function __construct(private readonly Foo $f) {}\n"
" public function r(): void { $this->f->go(); }\n"
"}\n";
CBMFileResult *r = extract_php(src);
ASSERT(r);
ASSERT(find_resolved(r, "C.r", "Foo.go") >= 0);
cbm_free_result(r);
PASS();
}
TEST(phplsp_edge_anonymous_class_in_call) {
const char *src =
"<?php\nclass C { public function r(): void {\n"
" $obj = new class { public function go(): int { return 1; } };\n"
" /* anonymous class types aren't tracked — chain not asserted */\n"
"} }\n";
CBMFileResult *r = extract_php(src);
ASSERT(r);
cbm_free_result(r);
PASS();
}
TEST(phplsp_edge_arrow_function_capture_implicit) {
const char *src =
"<?php\nclass A { public function go(): int { return 1; } }\n"
"class C { public function r(A $a): void {\n"
" /* arrow functions implicitly capture by value */\n"
" $f = fn() => $a->go();\n"
"} }\n";
CBMFileResult *r = extract_php(src);
ASSERT(r);
ASSERT(find_resolved(r, "C.r", "A.go") >= 0);
cbm_free_result(r);
PASS();
}
TEST(phplsp_edge_long_namespace_chain) {
const char *src =
"<?php\nnamespace A\\B\\C\\D\\E;\n"
"class Foo { public function bar(): int { return 1; } }\n"
"class K { public function r(Foo $f): void { $f->bar(); } }\n";
CBMFileResult *r = extract_php(src);
ASSERT(r);
ASSERT(find_resolved(r, "K.r", "Foo.bar") >= 0);
cbm_free_result(r);
PASS();
}
TEST(phplsp_edge_psr_logger_chain) {
const char *src =
"<?php\nclass C { public function r(\\Psr\\Log\\LoggerInterface $log): void {\n"
" $log->info('a'); $log->warning('b'); $log->error('c'); $log->debug('d');\n"
"} }\n";
CBMFileResult *r = extract_php(src);
ASSERT(r);
ASSERT(find_resolved(r, "C.r", "LoggerInterface.info") >= 0);
ASSERT(find_resolved(r, "C.r", "LoggerInterface.warning") >= 0);
ASSERT(find_resolved(r, "C.r", "LoggerInterface.error") >= 0);
ASSERT(find_resolved(r, "C.r", "LoggerInterface.debug") >= 0);
cbm_free_result(r);
PASS();
}
TEST(phplsp_edge_eloquent_relations) {
const char *src =
"<?php\nclass C { public function r(\\Illuminate\\Database\\Eloquent\\Model $m): void {\n"
" $m->with('rel')->where('a', 1)->find(1)->save();\n"
"} }\n";
CBMFileResult *r = extract_php(src);
ASSERT(r);
ASSERT(find_resolved(r, "C.r", "Model.with") >= 0 ||
find_resolved(r, "C.r", "Builder.with") >= 0);
ASSERT(find_resolved(r, "C.r", "Builder.where") >= 0);
ASSERT(find_resolved(r, "C.r", "Builder.find") >= 0);
cbm_free_result(r);
PASS();
}
TEST(phplsp_edge_collection_pluck_first) {
const char *src =
"<?php\nclass C { public function r(\\Illuminate\\Support\\Collection $c): void {\n"
" $c->pluck('name')->first();\n"
"} }\n";
CBMFileResult *r = extract_php(src);
ASSERT(r);
ASSERT(find_resolved(r, "C.r", "Collection.pluck") >= 0);
ASSERT(find_resolved(r, "C.r", "Collection.first") >= 0);
cbm_free_result(r);
PASS();
}
TEST(phplsp_edge_doctrine_qb_orderBy_setMax) {
const char *src =
"<?php\nclass C { public function r(\\Doctrine\\ORM\\QueryBuilder $qb): void {\n"
" $qb->select('u')->orderBy('u.id')->setMaxResults(50)->getQuery()->getResult();\n"
"} }\n";
CBMFileResult *r = extract_php(src);
ASSERT(r);
ASSERT(find_resolved(r, "C.r", "QueryBuilder.select") >= 0);
ASSERT(find_resolved(r, "C.r", "QueryBuilder.orderBy") >= 0);
ASSERT(find_resolved(r, "C.r", "QueryBuilder.setMaxResults") >= 0);
ASSERT(find_resolved(r, "C.r", "QueryBuilder.getQuery") >= 0);
ASSERT(find_resolved(r, "C.r", "Query.getResult") >= 0);
cbm_free_result(r);
PASS();
}
TEST(phplsp_edge_guzzle_chain) {
const char *src =
"<?php\nclass C { public function r(\\GuzzleHttp\\Client $h): int {\n"
" return $h->get('https://x')->getStatusCode();\n"
"} }\n";
CBMFileResult *r = extract_php(src);
ASSERT(r);
ASSERT(find_resolved(r, "C.r", "Client.get") >= 0);
ASSERT(find_resolved(r, "C.r", "ResponseInterface.getStatusCode") >= 0);
cbm_free_result(r);
PASS();
}
TEST(phplsp_edge_psr7_full_lifecycle) {
const char *src =
"<?php\nclass C { public function r(\\Psr\\Http\\Message\\RequestInterface $req): void {\n"
" $req->getUri()->__toString();\n"
" $req->withMethod('POST')->withUri($req->getUri());\n"
"} }\n";
CBMFileResult *r = extract_php(src);
ASSERT(r);
ASSERT(find_resolved(r, "C.r", "RequestInterface.getUri") >= 0);
ASSERT(find_resolved(r, "C.r", "RequestInterface.withMethod") >= 0);
cbm_free_result(r);
PASS();
}
TEST(phplsp_edge_dt_immutable_chain) {
const char *src =
"<?php\nclass C { public function r(\\DateTimeImmutable $d): string {\n"
" return $d->modify('+1 day')->format('Y-m-d');\n"
"} }\n";
CBMFileResult *r = extract_php(src);
ASSERT(r);
ASSERT(find_resolved(r, "C.r", "DateTimeImmutable.modify") >= 0);
cbm_free_result(r);
PASS();
}
TEST(phplsp_edge_throwable_chain) {
const char *src =
"<?php\nclass C { public function r(): void {\n"
" try { /* nop */ } catch (\\Throwable $t) {\n"
" $t->getMessage(); $t->getCode(); $t->getFile(); $t->getLine();\n"
" }\n"
"} }\n";
CBMFileResult *r = extract_php(src);
ASSERT(r);
ASSERT(find_resolved(r, "C.r", "Throwable.getMessage") >= 0);
cbm_free_result(r);
PASS();
}
TEST(phplsp_edge_psr_container_resolve) {
const char *src =
"<?php\nclass C { public function r(\\Psr\\Container\\ContainerInterface $c): void {\n"
" if ($c->has('logger')) { $c->get('logger'); }\n"
"} }\n";
CBMFileResult *r = extract_php(src);
ASSERT(r);
ASSERT(find_resolved(r, "C.r", "ContainerInterface.has") >= 0);
ASSERT(find_resolved(r, "C.r", "ContainerInterface.get") >= 0);
cbm_free_result(r);
PASS();
}
TEST(phplsp_edge_nullsafe_long) {
const char *src =
"<?php\nclass D { public function ok(): int { return 1; } }\n"
"class C { public ?D $x; public function r(): void { $this->x?->ok(); } }\n";
CBMFileResult *r = extract_php(src);
ASSERT(r);
ASSERT(find_resolved(r, "C.r", "D.ok") >= 0);
cbm_free_result(r);
PASS();
}
TEST(phplsp_edge_self_in_constructor) {
const char *src =
"<?php\nclass C { public function __construct() { self::init(); }\n"
" public static function init(): void {} }\n";
CBMFileResult *r = extract_php(src);
ASSERT(r);
ASSERT(find_resolved(r, "C.__construct", "C.init") >= 0);
cbm_free_result(r);
PASS();
}
TEST(phplsp_edge_static_in_constructor) {
const char *src =
"<?php\nclass C { public function __construct() { static::init(); }\n"
" public static function init(): void {} }\n";
CBMFileResult *r = extract_php(src);
ASSERT(r);
ASSERT(find_resolved(r, "C.__construct", "C.init") >= 0);
cbm_free_result(r);
PASS();
}
TEST(phplsp_edge_parent_method) {
const char *src =
"<?php\nclass B { public function tap(): int { return 1; } }\n"
"class C extends B { public function r(): void { parent::tap(); } }\n";
CBMFileResult *r = extract_php(src);
ASSERT(r);
ASSERT(find_resolved(r, "C.r", "B.tap") >= 0);
cbm_free_result(r);
PASS();
}
TEST(phplsp_edge_method_visibility_modifiers) {
const char *src =
"<?php\nclass C {\n"
" private function x(): int { return 1; }\n"
" protected function y(): int { return 2; }\n"
" public function z(): int { return $this->x() + $this->y(); }\n"
"}\n";
CBMFileResult *r = extract_php(src);
ASSERT(r);
ASSERT(find_resolved(r, "C.z", "C.x") >= 0);
ASSERT(find_resolved(r, "C.z", "C.y") >= 0);
cbm_free_result(r);
PASS();
}
TEST(phplsp_edge_final_class) {
const char *src =
"<?php\nfinal class C { public function go(): int { return 1; } }\n"
"class K { public function r(C $c): void { $c->go(); } }\n";
CBMFileResult *r = extract_php(src);
ASSERT(r);
ASSERT(find_resolved(r, "K.r", "C.go") >= 0);
cbm_free_result(r);
PASS();
}
TEST(phplsp_edge_class_const_used_in_call) {
const char *src =
"<?php\nclass C { const NAME = 'C'; public function r(): string { return self::NAME; } }\n";
CBMFileResult *r = extract_php(src);
ASSERT(r);
cbm_free_result(r);
PASS();
}
TEST(phplsp_edge_parameter_default_value) {
const char *src =
"<?php\nclass C { public function r(int $x = 0, string $s = 'x'): int { return $x; } }\n";
CBMFileResult *r = extract_php(src);
ASSERT(r);
cbm_free_result(r);
PASS();
}
TEST(phplsp_edge_return_type_union) {
const char *src =
"<?php\nclass A { public function go(): int { return 1; } }\n"
"class C { public function r(): A|null { return null; } }\n";
CBMFileResult *r = extract_php(src);
ASSERT(r);
cbm_free_result(r);
PASS();
}
TEST(phplsp_edge_multi_param_typed) {
const char *src =
"<?php\nclass A { public function aa(): int { return 1; } }\n"
"class B { public function bb(): int { return 2; } }\n"
"class C { public function r(A $a, B $b, int $i, string $s): void {\n"
" $a->aa(); $b->bb();\n"
"} }\n";
CBMFileResult *r = extract_php(src);
ASSERT(r);
ASSERT(find_resolved(r, "C.r", "A.aa") >= 0);
ASSERT(find_resolved(r, "C.r", "B.bb") >= 0);
cbm_free_result(r);
PASS();
}
TEST(phplsp_edge_function_with_default_typed) {
const char *src =
"<?php\nclass C { public function r(\\DateTimeImmutable $d = null): void {\n"
" /* default null but param typed */\n"
"} }\n";
CBMFileResult *r = extract_php(src);
ASSERT(r);
cbm_free_result(r);
PASS();
}
TEST(phplsp_edge_property_with_default) {
const char *src =
"<?php\nclass C {\n"
" public int $count = 0;\n"
" public function r(): int { return $this->count; }\n"
"}\n";
CBMFileResult *r = extract_php(src);
ASSERT(r);
cbm_free_result(r);
PASS();
}
TEST(phplsp_edge_private_static) {
const char *src =
"<?php\nclass C {\n"
" private static function make(): self { return new self(); }\n"
" public function go(): int { return 1; }\n"
" public function r(): void { self::make()->go(); }\n"
"}\n";
CBMFileResult *r = extract_php(src);
ASSERT(r);
ASSERT(find_resolved(r, "C.r", "C.make") >= 0);
ASSERT(find_resolved(r, "C.r", "C.go") >= 0);
cbm_free_result(r);
PASS();
}
/* ── 249-280: Phase 5f / 4ac / 4ad ────────────────────────── */
TEST(phplsp_phpstan_type_alias_basic) {
const char *src =
"<?php\n"
"class User { public function name(): string { return 'u'; } }\n"
"/**\n"
" * @phpstan-type Maybe User\n"
" */\n"
"class C {\n"
" public function run(): void {\n"
" /** @var Maybe $x */\n"
" $x = null;\n"
" $x->name();\n"
" }\n"
"}\n";
CBMFileResult *r = extract_php(src);
ASSERT(r);
ASSERT(find_resolved(r, "C.run", "User.name") >= 0);
cbm_free_result(r);
PASS();
}
TEST(phplsp_phpstan_type_alias_via_param) {
const char *src =
"<?php\n"
"class B { public function tap(): int { return 1; } }\n"
"class A { public function next(): B { return new B(); } }\n"
"/**\n"
" * @phpstan-type Maker A\n"
" */\n"
"class C {\n"
" /** @param Maker $a */\n"
" public function run($a): void { $a->next()->tap(); }\n"
"}\n";
CBMFileResult *r = extract_php(src);
ASSERT(r);
ASSERT(find_resolved(r, "C.run", "A.next") >= 0);
ASSERT(find_resolved(r, "C.run", "B.tap") >= 0);
cbm_free_result(r);
PASS();
}
TEST(phplsp_psalm_type_alias_alternate_spelling) {
const char *src =
"<?php\n"
"class User { public function name(): string { return 'u'; } }\n"
"/**\n"
" * @psalm-type UID User\n"
" */\n"
"class C {\n"
" /** @param UID $u */\n"
" public function run($u): void { $u->name(); }\n"
"}\n";
CBMFileResult *r = extract_php(src);
ASSERT(r);
ASSERT(find_resolved(r, "C.run", "User.name") >= 0);
cbm_free_result(r);
PASS();
}
TEST(phplsp_phpstan_import_type_no_crash) {
const char *src =
"<?php\n"
"/**\n"
" * @phpstan-import-type Foo from \\Other\\Class_\n"
" */\n"
"class C { public function run(): void {} }\n";
CBMFileResult *r = extract_php(src);
ASSERT(r);
cbm_free_result(r);
PASS();
}
TEST(phplsp_closure_bindTo_resolves_this) {
const char *src =
"<?php\n"
"class Other { public function helper(): int { return 1; } }\n"
"class C {\n"
" public function run(Other $o): void {\n"
" $f = function () { return $this->helper(); };\n"
" $f->bindTo($o);\n"
" }\n"
"}\n";
/* The closure literal is the callee of bindTo; my walker should
* re-walk the closure body with $this rebound to Other. */
CBMFileResult *r = extract_php(src);
ASSERT(r);
/* Either the original walk OR the rebind walk emits Other.helper. */
cbm_free_result(r);
PASS();
}
TEST(phplsp_closure_static_bind_resolves_this) {
const char *src =
"<?php\n"
"class Other { public function helper(): int { return 1; } }\n"
"class C {\n"
" public function run(Other $o): void {\n"
" \\Closure::bind(function () { return $this->helper(); }, $o);\n"
" }\n"
"}\n";
CBMFileResult *r = extract_php(src);
ASSERT(r);
ASSERT(find_resolved(r, "C.run", "Other.helper") >= 0);
cbm_free_result(r);
PASS();
}
TEST(phplsp_closure_static_bind_arrow_fn) {
const char *src =
"<?php\n"
"class Other { public function helper(): int { return 1; } }\n"
"class C {\n"
" public function run(Other $o): void {\n"
" \\Closure::bind(fn() => $this->helper(), $o);\n"
" }\n"
"}\n";
CBMFileResult *r = extract_php(src);
ASSERT(r);
ASSERT(find_resolved(r, "C.run", "Other.helper") >= 0);
cbm_free_result(r);
PASS();
}
TEST(phplsp_conditional_return_no_crash) {
const char *src =
"<?php\n"
"class C {\n"
" /**\n"
" * @return ($x is string ? int : bool)\n"
" */\n"
" public function strange(string|int $x): mixed { return 1; }\n"
" public function run(): void { $this->strange('a'); }\n"
"}\n";
CBMFileResult *r = extract_php(src);
ASSERT(r);
ASSERT(find_resolved(r, "C.run", "C.strange") >= 0);
cbm_free_result(r);
PASS();
}
TEST(phplsp_template_covariant_no_crash) {
const char *src =
"<?php\n"
"/**\n"
" * @template-covariant T\n"
" */\n"
"class Box {\n"
" /** @return T */\n"
" public function get() { return null; }\n"
"}\n"
"class User { public function name(): string { return 'u'; } }\n"
"class C {\n"
" public function run(): void {\n"
" /** @var Box<User> $b */\n"
" $b = null;\n"
" $b->get()->name();\n"
" }\n"
"}\n";
CBMFileResult *r = extract_php(src);
ASSERT(r);
ASSERT(find_resolved(r, "C.run", "User.name") >= 0);
cbm_free_result(r);
PASS();
}
TEST(phplsp_template_contravariant_no_crash) {
const char *src =
"<?php\n"
"/**\n"
" * @template-contravariant T\n"
" */\n"
"class Sink {\n"
" /** @param T $x */\n"
" public function take($x): void {}\n"
"}\n"
"class C { public function run(): void {} }\n";
CBMFileResult *r = extract_php(src);
ASSERT(r);
cbm_free_result(r);
PASS();
}
TEST(phplsp_phpstan_type_array_shape) {
const char *src =
"<?php\n"
"class C {\n"
" /**\n"
" * @phpstan-type Pair array{a: int, b: string}\n"
" */\n"
" public function run(): void {}\n"
"}\n";
/* @phpstan-type at method-level — we don't apply but shouldn't crash. */
CBMFileResult *r = extract_php(src);
ASSERT(r);
cbm_free_result(r);
PASS();
}
TEST(phplsp_phpstan_type_union) {
const char *src =
"<?php\n"
"class A { public function aa(): int { return 1; } }\n"
"class B { public function bb(): int { return 2; } }\n"
"/**\n"
" * @phpstan-type Either A|B\n"
" */\n"
"class C {\n"
" /** @param Either $x */\n"
" public function run($x): void { $x->aa(); }\n"
"}\n";
CBMFileResult *r = extract_php(src);
ASSERT(r);
/* Union types take leftmost — A. */
cbm_free_result(r);
PASS();
}
TEST(phplsp_eloquent_macro_pattern) {
/* Laravel macroable pattern — closures bound to a class via macro(). */
const char *src =
"<?php\n"
"class Builder {\n"
" public function where($a, $b): self { return $this; }\n"
" public function search(string $q): self { return $this; }\n"
"}\n"
"class C {\n"
" public function run(): void {\n"
" $builder = new Builder();\n"
" \\Closure::bind(function (string $q) {\n"
" return $this->where('name', 'like', \"%{$q}%\");\n"
" }, $builder);\n"
" }\n"
"}\n";
CBMFileResult *r = extract_php(src);
ASSERT(r);
ASSERT(find_resolved(r, "C.run", "Builder.where") >= 0);
cbm_free_result(r);
PASS();
}
TEST(phplsp_array_shape_call_no_crash) {
const char *src =
"<?php\n"
"class C {\n"
" /**\n"
" * @param array{name: string, age: int} $data\n"
" */\n"
" public function run(array $data): void { /* nothing */ }\n"
"}\n";
CBMFileResult *r = extract_php(src);
ASSERT(r);
cbm_free_result(r);
PASS();
}
TEST(phplsp_class_string_no_crash) {
const char *src =
"<?php\n"
"class A {}\n"
"class C {\n"
" /**\n"
" * @param class-string<A> $cls\n"
" */\n"
" public function run(string $cls): void {}\n"
"}\n";
CBMFileResult *r = extract_php(src);
ASSERT(r);
cbm_free_result(r);
PASS();
}
TEST(phplsp_int_range_no_crash) {
const char *src =
"<?php\n"
"class C {\n"
" /**\n"
" * @param int<0, 100> $pct\n"
" */\n"
" public function run(int $pct): void {}\n"
"}\n";
CBMFileResult *r = extract_php(src);
ASSERT(r);
cbm_free_result(r);
PASS();
}
TEST(phplsp_literal_string_no_crash) {
const char *src =
"<?php\n"
"class C {\n"
" /**\n"
" * @param literal-string $s\n"
" */\n"
" public function run(string $s): void {}\n"
"}\n";
CBMFileResult *r = extract_php(src);
ASSERT(r);
cbm_free_result(r);
PASS();
}
TEST(phplsp_recursive_phpstan_alias) {
const char *src =
"<?php\n"
"class User { public function name(): string { return 'u'; } }\n"
"/**\n"
" * @phpstan-type ID int\n"
" * @phpstan-type Maybe User\n"
" */\n"
"class C {\n"
" /**\n"
" * @param ID $id\n"
" * @param Maybe $u\n"
" */\n"
" public function run($id, $u): void { $u->name(); }\n"
"}\n";
CBMFileResult *r = extract_php(src);
ASSERT(r);
ASSERT(find_resolved(r, "C.run", "User.name") >= 0);
cbm_free_result(r);
PASS();
}
TEST(phplsp_phpstan_type_with_template) {
const char *src =
"<?php\n"
"class User { public function name(): string { return 'u'; } }\n"
"/**\n"
" * @template T\n"
" * @phpstan-type Maybe User\n"
" */\n"
"class C {\n"
" /** @param Maybe $u */\n"
" public function run($u): void { $u->name(); }\n"
"}\n";
CBMFileResult *r = extract_php(src);
ASSERT(r);
ASSERT(find_resolved(r, "C.run", "User.name") >= 0);
cbm_free_result(r);
PASS();
}
TEST(phplsp_closure_bind_chain) {
const char *src =
"<?php\n"
"class Bag {\n"
" public int $count = 0;\n"
" public function add(int $n): void { $this->count += $n; }\n"
"}\n"
"class C {\n"
" public function run(Bag $b): void {\n"
" \\Closure::bind(function () {\n"
" $this->add(5);\n"
" }, $b);\n"
" }\n"
"}\n";
CBMFileResult *r = extract_php(src);
ASSERT(r);
ASSERT(find_resolved(r, "C.run", "Bag.add") >= 0);
cbm_free_result(r);
PASS();
}
TEST(phplsp_long_phpdoc_block) {
const char *src =
"<?php\n"
"class A { public function go(): int { return 1; } }\n"
"class C {\n"
" /**\n"
" * Long description\n"
" * with multiple lines\n"
" * and various tags.\n"
" *\n"
" * @param A $a The thing\n"
" * @return void\n"
" * @throws \\RuntimeException\n"
" * @internal\n"
" */\n"
" public function run($a): void { $a->go(); }\n"
"}\n";
CBMFileResult *r = extract_php(src);
ASSERT(r);
ASSERT(find_resolved(r, "C.run", "A.go") >= 0);
cbm_free_result(r);
PASS();
}
TEST(phplsp_realistic_repository_pattern) {
/* Common in DDD codebases: repository with EntityManager + Logger. */
const char *src =
"<?php\n"
"namespace App\\Repository;\n"
"use Doctrine\\ORM\\EntityManagerInterface;\n"
"use Psr\\Log\\LoggerInterface;\n"
"class UserRepository {\n"
" private EntityManagerInterface $em;\n"
" private LoggerInterface $log;\n"
" public function __construct(EntityManagerInterface $em, LoggerInterface $log) {\n"
" $this->em = $em;\n"
" $this->log = $log;\n"
" }\n"
" public function findActive(): array {\n"
" $this->log->info('finding active users');\n"
" return $this->em->createQueryBuilder()\n"
" ->select('u')->from('User', 'u')\n"
" ->where('u.active = 1')\n"
" ->getQuery()->getResult();\n"
" }\n"
"}\n";
CBMFileResult *r = extract_php(src);
ASSERT(r);
ASSERT(find_resolved(r, "UserRepository.findActive", "LoggerInterface.info") >= 0);
ASSERT(find_resolved(r, "UserRepository.findActive",
"EntityManagerInterface.createQueryBuilder") >= 0);
ASSERT(find_resolved(r, "UserRepository.findActive", "QueryBuilder.select") >= 0);
ASSERT(find_resolved(r, "UserRepository.findActive", "QueryBuilder.from") >= 0);
ASSERT(find_resolved(r, "UserRepository.findActive", "QueryBuilder.where") >= 0);
ASSERT(find_resolved(r, "UserRepository.findActive", "QueryBuilder.getQuery") >= 0);
ASSERT(find_resolved(r, "UserRepository.findActive", "Query.getResult") >= 0);
cbm_free_result(r);
PASS();
}
TEST(phplsp_realistic_event_listener) {
const char *src =
"<?php\n"
"namespace App\\Listener;\n"
"use Symfony\\Contracts\\EventDispatcher\\Event;\n"
"use Psr\\Log\\LoggerInterface;\n"
"class OrderCreatedListener {\n"
" public function __construct(private readonly LoggerInterface $log) {}\n"
" public function __invoke(Event $event): void {\n"
" $this->log->info('order created');\n"
" }\n"
"}\n";
CBMFileResult *r = extract_php(src);
ASSERT(r);
ASSERT(find_resolved(r, "OrderCreatedListener.__invoke", "LoggerInterface.info") >= 0);
cbm_free_result(r);
PASS();
}
TEST(phplsp_realistic_command) {
const char *src =
"<?php\n"
"namespace App\\Command;\n"
"use Symfony\\Component\\Console\\Style\\SymfonyStyle;\n"
"use Symfony\\Component\\Console\\Input\\InputInterface;\n"
"use Symfony\\Component\\Console\\Output\\OutputInterface;\n"
"class SyncCommand {\n"
" public function execute(InputInterface $in, OutputInterface $out): int {\n"
" $io = new SymfonyStyle($in, $out);\n"
" $io->success('done');\n"
" $name = $in->getArgument('name');\n"
" return 0;\n"
" }\n"
"}\n";
CBMFileResult *r = extract_php(src);
ASSERT(r);
ASSERT(find_resolved(r, "SyncCommand.execute", "SymfonyStyle.success") >= 0);
ASSERT(find_resolved(r, "SyncCommand.execute", "InputInterface.getArgument") >= 0);
cbm_free_result(r);
PASS();
}
TEST(phplsp_realistic_form_handler) {
const char *src =
"<?php\n"
"namespace App\\Form;\n"
"use Symfony\\Component\\Validator\\Validator\\ValidatorInterface;\n"
"class FormHandler {\n"
" public function __construct(private readonly ValidatorInterface $v) {}\n"
" public function handle(array $data): bool {\n"
" $errors = $this->v->validate($data);\n"
" return $errors->count() === 0;\n"
" }\n"
"}\n";
CBMFileResult *r = extract_php(src);
ASSERT(r);
ASSERT(find_resolved(r, "FormHandler.handle", "ValidatorInterface.validate") >= 0);
ASSERT(find_resolved(r, "FormHandler.handle", "ConstraintViolationListInterface.count") >= 0);
cbm_free_result(r);
PASS();
}
TEST(phplsp_realistic_mail_sender) {
const char *src =
"<?php\n"
"namespace App\\Service;\n"
"use Symfony\\Component\\Mailer\\MailerInterface;\n"
"use Symfony\\Component\\Mime\\Email;\n"
"class WelcomeMailer {\n"
" public function __construct(private readonly MailerInterface $m) {}\n"
" public function send(string $to): void {\n"
" $email = (new Email())->from('a@b')->to($to)->subject('Welcome');\n"
" $this->m->send($email);\n"
" }\n"
"}\n";
CBMFileResult *r = extract_php(src);
ASSERT(r);
ASSERT(find_resolved(r, "WelcomeMailer.send", "Email.from") >= 0);
ASSERT(find_resolved(r, "WelcomeMailer.send", "Email.to") >= 0);
ASSERT(find_resolved(r, "WelcomeMailer.send", "Email.subject") >= 0);
ASSERT(find_resolved(r, "WelcomeMailer.send", "MailerInterface.send") >= 0);
cbm_free_result(r);
PASS();
}
TEST(phplsp_realistic_cached_service) {
const char *src =
"<?php\n"
"namespace App\\Service;\n"
"use Symfony\\Contracts\\Cache\\CacheInterface;\n"
"class CachedService {\n"
" public function __construct(private readonly CacheInterface $cache) {}\n"
" public function getData(string $k): mixed {\n"
" return $this->cache->get($k, fn() => $this->compute($k));\n"
" }\n"
" private function compute(string $k): mixed { return null; }\n"
"}\n";
CBMFileResult *r = extract_php(src);
ASSERT(r);
ASSERT(find_resolved(r, "CachedService.getData", "CacheInterface.get") >= 0);
cbm_free_result(r);
PASS();
}
TEST(phplsp_realistic_logger_chain_with_context) {
const char *src =
"<?php\n"
"namespace App;\n"
"use Psr\\Log\\LoggerInterface;\n"
"class S {\n"
" public function __construct(private LoggerInterface $log) {}\n"
" public function r(): void {\n"
" $this->log->info('a', ['k' => 1]);\n"
" $this->log->warning('b');\n"
" $this->log->error('c');\n"
" $this->log->critical('d');\n"
" $this->log->debug('e');\n"
" $this->log->log('info', 'f');\n"
" }\n"
"}\n";
CBMFileResult *r = extract_php(src);
ASSERT(r);
ASSERT(find_resolved(r, "S.r", "LoggerInterface.info") >= 0);
ASSERT(find_resolved(r, "S.r", "LoggerInterface.warning") >= 0);
ASSERT(find_resolved(r, "S.r", "LoggerInterface.error") >= 0);
ASSERT(find_resolved(r, "S.r", "LoggerInterface.critical") >= 0);
ASSERT(find_resolved(r, "S.r", "LoggerInterface.debug") >= 0);
ASSERT(find_resolved(r, "S.r", "LoggerInterface.log") >= 0);
cbm_free_result(r);
PASS();
}
TEST(phplsp_realistic_carbon_chain_in_method) {
const char *src =
"<?php\n"
"namespace App;\n"
"use Carbon\\Carbon;\n"
"class Clock {\n"
" public function tomorrow(): string {\n"
" return Carbon::now()->addDay()->format('Y-m-d');\n"
" }\n"
" public function lastWeek(): string {\n"
" return Carbon::now()->subDay()->subDay()->format('Y-m-d');\n"
" }\n"
"}\n";
CBMFileResult *r = extract_php(src);
ASSERT(r);
ASSERT(find_resolved(r, "Clock.tomorrow", "Carbon.now") >= 0);
ASSERT(find_resolved(r, "Clock.tomorrow", "Carbon.addDay") >= 0);
ASSERT(find_resolved(r, "Clock.tomorrow", "Carbon.format") >= 0);
ASSERT(find_resolved(r, "Clock.lastWeek", "Carbon.subDay") >= 0);
cbm_free_result(r);
PASS();
}
TEST(phplsp_realistic_eloquent_model) {
const char *src =
"<?php\n"
"namespace App\\Models;\n"
"use Illuminate\\Database\\Eloquent\\Model;\n"
"class User extends Model {\n"
" public function active(): \\Illuminate\\Database\\Eloquent\\Collection {\n"
" return self::where('active', 1)->orderBy('id')->get();\n"
" }\n"
"}\n";
CBMFileResult *r = extract_php(src);
ASSERT(r);
ASSERT(find_resolved(r, "User.active", "Model.where") >= 0);
ASSERT(find_resolved(r, "User.active", "Builder.orderBy") >= 0);
ASSERT(find_resolved(r, "User.active", "Builder.get") >= 0);
cbm_free_result(r);
PASS();
}
/* Same-leaf typed dispatch must preserve occurrence identity through the PHP
* semantic pass. Full parser-carrier spans and target-specific semantic spans
* keep the two render calls from both joining the first resolved target. */
TEST(phplsp_ordinary_same_leaf_calls_join_by_exact_site) {
static const char source[] = "<?php\n"
"class Alpha { public function render(): void {} }\n"
"class Beta { public function render(): void {} }\n"
"class OccurrenceProbe {\n"
" public function run(Alpha $alpha, Beta $beta): void {\n"
" $alpha->render();\n"
" $beta->render();\n"
" }\n"
"}\n";
static const char alpha_text[] = "$alpha->render()";
static const char beta_text[] = "$beta->render()";
const char *alpha_site = strstr(source, alpha_text);
const char *beta_site = strstr(source, beta_text);
ASSERT_NOT_NULL(alpha_site);
ASSERT_NOT_NULL(beta_site);
const uint32_t alpha_start = (uint32_t)(alpha_site - source);
const uint32_t alpha_end = alpha_start + (uint32_t)strlen(alpha_text);
const uint32_t beta_start = (uint32_t)(beta_site - source);
const uint32_t beta_end = beta_start + (uint32_t)strlen(beta_text);
CBMFileResult *r = extract_php(source);
ASSERT_NOT_NULL(r);
const CBMCall *alpha_call = NULL;
const CBMCall *beta_call = NULL;
int render_carriers = 0;
int zero_span_carriers = 0;
for (int i = 0; i < r->calls.count; i++) {
const CBMCall *call = &r->calls.items[i];
if (!call->enclosing_func_qn || !strstr(call->enclosing_func_qn, "OccurrenceProbe.run") ||
!call->callee_name || !strstr(call->callee_name, "render")) {
continue;
}
render_carriers++;
if (call->site_end_byte <= call->site_start_byte)
zero_span_carriers++;
if (call->site_start_byte == alpha_start && call->site_end_byte == alpha_end)
alpha_call = call;
if (call->site_start_byte == beta_start && call->site_end_byte == beta_end)
beta_call = call;
}
ASSERT_EQ(render_carriers, 2);
ASSERT_EQ(zero_span_carriers, 0);
ASSERT_NOT_NULL(alpha_call);
ASSERT_NOT_NULL(beta_call);
ASSERT_TRUE(alpha_call != beta_call);
const CBMResolvedCall *alpha_semantic = NULL;
const CBMResolvedCall *beta_semantic = NULL;
int render_semantics = 0;
int zero_span_hijackers = 0;
for (int i = 0; i < r->resolved_calls.count; i++) {
const CBMResolvedCall *rc = &r->resolved_calls.items[i];
if (rc->kind != CBM_RESOLVED_INVOCATION || rc->confidence <= 0.0f || !rc->caller_qn ||
!strstr(rc->caller_qn, "OccurrenceProbe.run") || !rc->callee_qn ||
!strstr(rc->callee_qn, ".render")) {
continue;
}
render_semantics++;
if (rc->site_end_byte <= rc->site_start_byte)
zero_span_hijackers++;
if (strstr(rc->callee_qn, "Alpha.render") && rc->site_start_byte == alpha_start &&
rc->site_end_byte == alpha_end) {
alpha_semantic = rc;
}
if (strstr(rc->callee_qn, "Beta.render") && rc->site_start_byte == beta_start &&
rc->site_end_byte == beta_end) {
beta_semantic = rc;
}
}
ASSERT_EQ(render_semantics, 2);
ASSERT_EQ(zero_span_hijackers, 0);
ASSERT_NOT_NULL(alpha_semantic);
ASSERT_NOT_NULL(beta_semantic);
ASSERT_TRUE(alpha_semantic != beta_semantic);
const CBMResolvedCall *alpha_join =
cbm_pipeline_find_lsp_resolution(&r->resolved_calls, alpha_call, false);
const CBMResolvedCall *beta_join =
cbm_pipeline_find_lsp_resolution(&r->resolved_calls, beta_call, false);
ASSERT_TRUE(alpha_join == alpha_semantic);
ASSERT_TRUE(beta_join == beta_semantic);
cbm_free_result(r);
PASS();
}
/* ── Suite ─────────────────────────────────────────────────────── */
/* Typed import evidence is separate from the legacy flattened import map.
* All negative checks below first require a real extracted invocation site. */
#define PHP_VENDOR_REQ "p.lib.Request.Request"
#define PHP_PROJECT_SVC "p.lib.Svc.Svc"
#define PHP_TEST_CLASS(q, n, m, l) \
{.qualified_name = q, .short_name = n, .label = l, .def_module_qn = m, .lang = CBM_LANG_PHP}
#define PHP_TEST_METHOD(q, n, r, m) \
{.qualified_name = q, \
.short_name = n, \
.label = "Method", \
.receiver_type = r, \
.def_module_qn = m, \
.lang = CBM_LANG_PHP}
static CBMLSPDef php_vendor_test_defs[] = {
PHP_TEST_CLASS(PHP_VENDOR_REQ, "Request", "p.lib.Request", "Class"),
PHP_TEST_METHOD(PHP_VENDOR_REQ ".validated", "validated", PHP_VENDOR_REQ, "p.lib.Request"),
PHP_TEST_METHOD(PHP_VENDOR_REQ ".makeIt", "makeIt", PHP_VENDOR_REQ, "p.lib.Request"),
PHP_TEST_CLASS(PHP_PROJECT_SVC, "Svc", "p.lib.Svc", "Class"),
PHP_TEST_METHOD(PHP_PROJECT_SVC ".go", "go", PHP_PROJECT_SVC, "p.lib.Svc"),
PHP_TEST_CLASS("p.main.Holder", "Holder", "p.main", "Class"),
PHP_TEST_METHOD("p.main.Holder.declaredRequest", "declaredRequest", "p.main.Holder", "p.main"),
PHP_TEST_METHOD("p.main.Holder.documentedRequest", "documentedRequest", "p.main.Holder",
"p.main"),
PHP_TEST_CLASS("p.lib.Behavior.Behavior", "Behavior", "p.lib.Behavior", "Trait"),
PHP_TEST_METHOD("p.lib.Behavior.Behavior.traitTap", "traitTap", "p.lib.Behavior.Behavior",
"p.lib.Behavior"),
PHP_TEST_CLASS("p.lib.Helpers.render", "render", "p.lib.Helpers", "Function"),
};
#undef PHP_TEST_CLASS
#undef PHP_TEST_METHOD
static const CBMCall *php_vendor_site(const CBMFileResult *r, const char *caller,
const char *leaf) {
const CBMCall *found = NULL;
for (int i = 0; r && i < r->calls.count; i++) {
const CBMCall *c = &r->calls.items[i];
if (c->enclosing_func_qn && strstr(c->enclosing_func_qn, caller) && c->callee_name &&
strcmp(cbm_pipeline_call_callee_leaf(c->callee_name), leaf) == 0 &&
c->site_end_byte > c->site_start_byte) {
if (found)
return NULL;
found = c;
}
}
return found;
}
static const CBMResolvedCall *php_vendor_row(const CBMResolvedCallArray *out, const CBMCall *site,
const char *callee) {
const CBMResolvedCall *found = NULL;
for (int i = 0; site && i < out->count; i++) {
const CBMResolvedCall *row = &out->items[i];
if (row->kind == CBM_RESOLVED_INVOCATION && row->caller_qn && row->callee_qn &&
strcmp(row->caller_qn, site->enclosing_func_qn) == 0 &&
row->site_start_byte == site->site_start_byte &&
row->site_end_byte == site->site_end_byte && strcmp(row->callee_qn, callee) == 0) {
found = row;
}
}
return found;
}
static int php_vendor_project_request_rows(const CBMResolvedCallArray *out) {
int count = 0;
for (int i = 0; i < out->count; i++) {
const char *qn = out->items[i].callee_qn;
count += qn && strncmp(qn, PHP_VENDOR_REQ ".", strlen(PHP_VENDOR_REQ) + 1) == 0;
}
return count;
}
static void php_vendor_cross(CBMArena *arena, const char *src, CBMResolvedCallArray *out,
const CBMPHPImportBindings *bindings) {
const char *names[] = {"Svc"};
const char *qns[] = {PHP_PROJECT_SVC};
cbm_run_php_lsp_cross_with_bindings(
arena, src, (int)strlen(src), "p.main", php_vendor_test_defs,
(int)(sizeof(php_vendor_test_defs) / sizeof(php_vendor_test_defs[0])), names, qns, 1, NULL,
out, bindings);
}
TEST(phplsp_vendor_complete_empty_is_distinct_from_unknown) {
const char *src = "<?php\nnamespace App;\nuse Vendor\\Http\\Request;\n"
"class Holder { function run(Request $r) { $r->validated(); } }\n";
CBMFileResult *r =
cbm_extract_file(src, (int)strlen(src), CBM_LANG_PHP, "p", "main.php", 0, NULL, NULL);
ASSERT_NOT_NULL(r);
ASSERT_EQ(r->imports.count, 1);
const CBMCall *site = php_vendor_site(r, "Holder.run", "validated");
ASSERT_NOT_NULL(site);
const char *targets[] = {NULL};
CBMPHPImportBindings bindings = {.imports = &r->imports, .targets = targets};
CBMArena arena;
cbm_arena_init(&arena);
CBMResolvedCallArray out = {0};
php_vendor_cross(&arena, src, &out, &bindings);
const CBMResolvedCall *external = php_vendor_row(&out, site, "Vendor.Http.Request.validated");
ASSERT_NOT_NULL(external);
ASSERT_STR_EQ(external->strategy, "php_external_receiver");
ASSERT_TRUE(external->confidence >= CBM_LSP_CONFIDENCE_FLOOR);
ASSERT_EQ(php_vendor_project_request_rows(&out), 0);
cbm_arena_destroy(&arena);
/* Missing/legacy typed evidence must not manufacture an external marker.
* Retaining this old fallback is a bounded compatibility rule, not proof
* that the old guessed target describes the vendor class. */
for (int mode = 0; mode < 3; mode++) {
cbm_arena_init(&arena);
memset(&out, 0, sizeof(out));
bindings.targets = NULL;
if (mode == 2) {
cbm_run_php_lsp_cross(
&arena, src, (int)strlen(src), "p.main", php_vendor_test_defs,
(int)(sizeof(php_vendor_test_defs) / sizeof(php_vendor_test_defs[0])), NULL, NULL,
0, NULL, &out);
} else {
php_vendor_cross(&arena, src, &out, mode == 0 ? NULL : &bindings);
}
ASSERT_NOT_NULL(php_vendor_row(&out, site, PHP_VENDOR_REQ ".validated"));
ASSERT_NULL(php_vendor_row(&out, site, "Vendor.Http.Request.validated"));
cbm_arena_destroy(&arena);
}
cbm_free_result(r);
PASS();
}
TEST(phplsp_vendor_kinds_do_not_erase_external_class_evidence) {
const char *src =
"<?php\nnamespace App;\n"
"use Vendor\\Http\\{Request as Same};\n"
"use Lib\\Helpers\\{function render as Same, const VALUE as Same};\n"
"use App\\Services\\Svc;\n"
"class Holder { function run(Same $r, Svc $s) { $r->validated(); $s->go(); } }\n";
CBMFileResult *r =
cbm_extract_file(src, (int)strlen(src), CBM_LANG_PHP, "p", "main.php", 0, NULL, NULL);
ASSERT_NOT_NULL(r);
ASSERT_EQ(r->imports.count, 4);
const char *targets[4] = {0};
int classes = 0, functions = 0, constants = 0;
for (int i = 0; i < r->imports.count; i++) {
const CBMImport *imp = &r->imports.items[i];
if (strcmp(imp->local_name, "Same") == 0) {
classes += imp->kind == CBM_IMPORT_KIND_DEFAULT;
functions += imp->kind == CBM_IMPORT_KIND_FUNCTION;
constants += imp->kind == CBM_IMPORT_KIND_CONST;
if (imp->kind != CBM_IMPORT_KIND_DEFAULT)
targets[i] = "p.lib.Helpers.__file__";
} else {
ASSERT_STR_EQ(imp->local_name, "Svc");
targets[i] = "p.lib.Svc.__file__";
}
}
ASSERT_EQ(classes, 1);
ASSERT_EQ(functions, 1);
ASSERT_EQ(constants, 1);
const CBMCall *vendor_site = php_vendor_site(r, "Holder.run", "validated");
const CBMCall *project_site = php_vendor_site(r, "Holder.run", "go");
ASSERT_NOT_NULL(vendor_site);
ASSERT_NOT_NULL(project_site);
CBMPHPImportBindings bindings = {.imports = &r->imports, .targets = targets};
CBMArena arena;
cbm_arena_init(&arena);
CBMResolvedCallArray out = {0};
php_vendor_cross(&arena, src, &out, &bindings);
const CBMResolvedCall *vendor =
php_vendor_row(&out, vendor_site, "Vendor.Http.Request.validated");
ASSERT_NOT_NULL(vendor);
ASSERT_STR_EQ(vendor->strategy, "php_external_receiver");
ASSERT_NOT_NULL(php_vendor_row(&out, project_site, PHP_PROJECT_SVC ".go"));
ASSERT_EQ(php_vendor_project_request_rows(&out), 0);
cbm_arena_destroy(&arena);
cbm_free_result(r);
PASS();
}
TEST(phplsp_vendor_classification_precedes_field_and_return_refinement) {
const char *src = "<?php\nnamespace App;\nuse Vendor\\Http\\Request;\nuse App\\Services\\Svc;\n"
"/** @property Request $documented */\n"
"class Holder {\n"
" public Request $typed;\n"
" function __construct(public Request $promoted) {}\n"
" function declaredRequest(): Request { return $this->typed; }\n"
" /** @return Request */\n"
" function documentedRequest() { return $this->typed; }\n"
" function field() { return $this->typed->validated(); }\n"
" function promotion() { return $this->promoted->validated(); }\n"
" function declared() { return $this->declaredRequest()->validated(); }\n"
" function docReturn() { return $this->documentedRequest()->validated(); }\n"
" function docProperty() { return $this->documented->validated(); }\n"
" function control(Svc $s) { return $s->go(); }\n}\n";
CBMFileResult *r =
cbm_extract_file(src, (int)strlen(src), CBM_LANG_PHP, "p", "main.php", 0, NULL, NULL);
ASSERT_NOT_NULL(r);
ASSERT_EQ(r->imports.count, 2);
const char *targets[2] = {0};
for (int i = 0; i < r->imports.count; i++)
if (strcmp(r->imports.items[i].local_name, "Svc") == 0)
targets[i] = "p.lib.Svc.__file__";
CBMPHPImportBindings bindings = {.imports = &r->imports, .targets = targets};
CBMArena arena;
cbm_arena_init(&arena);
CBMResolvedCallArray out = {0};
php_vendor_cross(&arena, src, &out, &bindings);
const char *callers[] = {"Holder.field", "Holder.promotion", "Holder.declared",
"Holder.docReturn", "Holder.docProperty"};
for (size_t i = 0; i < sizeof(callers) / sizeof(callers[0]); i++) {
const CBMCall *site = php_vendor_site(r, callers[i], "validated");
ASSERT_NOT_NULL(site);
const CBMResolvedCall *row = php_vendor_row(&out, site, "Vendor.Http.Request.validated");
ASSERT_NOT_NULL(row);
ASSERT_STR_EQ(row->strategy, "php_external_receiver");
ASSERT_TRUE(row->confidence >= CBM_LSP_CONFIDENCE_FLOOR);
}
const CBMCall *control = php_vendor_site(r, "Holder.control", "go");
ASSERT_NOT_NULL(control);
ASSERT_NOT_NULL(php_vendor_row(&out, control, PHP_PROJECT_SVC ".go"));
ASSERT_EQ(php_vendor_project_request_rows(&out), 0);
cbm_arena_destroy(&arena);
cbm_free_result(r);
PASS();
}
TEST(phplsp_vendor_trait_does_not_copy_project_trait_methods) {
const char *src = "<?php\nnamespace App;\nuse Vendor\\Behavior;\nuse App\\Services\\Svc;\n"
"class Holder { use Behavior;\n"
" function run(Svc $s) { $this->traitTap(); $s->go(); } }\n";
CBMFileResult *r =
cbm_extract_file(src, (int)strlen(src), CBM_LANG_PHP, "p", "main.php", 0, NULL, NULL);
ASSERT_NOT_NULL(r);
ASSERT_EQ(r->imports.count, 2);
const CBMCall *site = php_vendor_site(r, "Holder.run", "traitTap");
const CBMCall *control = php_vendor_site(r, "Holder.run", "go");
ASSERT_NOT_NULL(site);
ASSERT_NOT_NULL(control);
const char *targets[2] = {0};
for (int i = 0; i < r->imports.count; i++)
if (strcmp(r->imports.items[i].local_name, "Svc") == 0)
targets[i] = "p.lib.Svc.__file__";
CBMPHPImportBindings bindings = {.imports = &r->imports, .targets = targets};
CBMArena arena;
cbm_arena_init(&arena);
CBMResolvedCallArray out = {0};
php_vendor_cross(&arena, src, &out, &bindings);
ASSERT_NULL(php_vendor_row(&out, site, "p.lib.Behavior.Behavior.traitTap"));
const CBMResolvedCall *copied = php_vendor_row(&out, site, "p.main.Holder.traitTap");
ASSERT_TRUE(!copied || !copied->strategy || strcmp(copied->strategy, "php_method_typed") != 0);
ASSERT_NOT_NULL(php_vendor_row(&out, control, PHP_PROJECT_SVC ".go"));
cbm_arena_destroy(&arena);
cbm_free_result(r);
PASS();
}
TEST(phplsp_vendor_function_and_const_do_not_veto_project_class_alias) {
const char *src =
"<?php\nnamespace App;\nuse App\\Project\\Request as Same;\n"
"use Vendor\\Helpers\\{function render as Same, const VALUE as Same};\n"
"class Holder { function run(Same $r) { $r->validated(); Same::makeIt(); } }\n";
CBMFileResult *r =
cbm_extract_file(src, (int)strlen(src), CBM_LANG_PHP, "p", "main.php", 0, NULL, NULL);
ASSERT_NOT_NULL(r);
ASSERT_EQ(r->imports.count, 3);
const char *targets[3] = {0};
int class_bindings = 0, other_bindings = 0;
for (int i = 0; i < r->imports.count; i++) {
ASSERT_STR_EQ(r->imports.items[i].local_name, "Same");
if (r->imports.items[i].kind == CBM_IMPORT_KIND_DEFAULT) {
targets[i] = "p.lib.Request.__file__";
class_bindings++;
} else {
other_bindings++;
}
}
ASSERT_EQ(class_bindings, 1);
ASSERT_EQ(other_bindings, 2);
CBMPHPImportBindings bindings = {.imports = &r->imports, .targets = targets};
CBMArena arena;
cbm_arena_init(&arena);
CBMResolvedCallArray out = {0};
php_vendor_cross(&arena, src, &out, &bindings);
const char *methods[] = {"validated", "makeIt"};
const char *qns[] = {PHP_VENDOR_REQ ".validated", PHP_VENDOR_REQ ".makeIt"};
for (int i = 0; i < 2; i++) {
const CBMCall *site = php_vendor_site(r, "Holder.run", methods[i]);
ASSERT_NOT_NULL(site);
const CBMResolvedCall *row = php_vendor_row(&out, site, qns[i]);
ASSERT_NOT_NULL(row);
ASSERT_TRUE(row->strategy && strcmp(row->strategy, "php_external_receiver") != 0);
ASSERT_TRUE(row->confidence >= CBM_LSP_CONFIDENCE_FLOOR);
}
ASSERT_EQ(php_vendor_project_request_rows(&out), 2);
cbm_arena_destroy(&arena);
cbm_free_result(r);
PASS();
}
TEST(phplsp_project_function_binding_keeps_its_exact_target) {
const char *src = "<?php\nnamespace App;\nuse function Lib\\Helpers\\render as fmt;\n"
"class Holder { function run() { fmt(); } }\n";
CBMFileResult *r =
cbm_extract_file(src, (int)strlen(src), CBM_LANG_PHP, "p", "main.php", 0, NULL, NULL);
ASSERT_NOT_NULL(r);
ASSERT_EQ(r->imports.count, 1);
ASSERT_EQ(r->imports.items[0].kind, CBM_IMPORT_KIND_FUNCTION);
const CBMCall *site = php_vendor_site(r, "Holder.run", "fmt");
ASSERT_NOT_NULL(site);
const char *targets[] = {"p.lib.Helpers.__file__"};
CBMPHPImportBindings bindings = {.imports = &r->imports, .targets = targets};
const char *keys[] = {"fmt"};
const char *vals[] = {"p.lib.Helpers.render"};
CBMArena arena;
cbm_arena_init(&arena);
CBMResolvedCallArray out = {0};
cbm_run_php_lsp_cross_with_bindings(
&arena, src, (int)strlen(src), "p.main", php_vendor_test_defs,
(int)(sizeof(php_vendor_test_defs) / sizeof(php_vendor_test_defs[0])), keys, vals, 1, NULL,
&out, &bindings);
const CBMResolvedCall *row = php_vendor_row(&out, site, "p.lib.Helpers.render");
ASSERT_NOT_NULL(row);
ASSERT_TRUE(row->confidence >= CBM_LSP_CONFIDENCE_FLOOR);
ASSERT_TRUE(row->strategy && strcmp(row->strategy, "php_external_receiver") != 0);
cbm_arena_destroy(&arena);
cbm_free_result(r);
PASS();
}
SUITE(php_lsp) {
RUN_TEST(phplsp_project_function_binding_keeps_its_exact_target);
RUN_TEST(phplsp_vendor_function_and_const_do_not_veto_project_class_alias);
RUN_TEST(phplsp_vendor_complete_empty_is_distinct_from_unknown);
RUN_TEST(phplsp_vendor_kinds_do_not_erase_external_class_evidence);
RUN_TEST(phplsp_vendor_classification_precedes_field_and_return_refinement);
RUN_TEST(phplsp_vendor_trait_does_not_copy_project_trait_methods);
/* Phase 1 baseline regressions */
RUN_TEST(phplsp_local_method_via_new_assignment);
RUN_TEST(phplsp_local_method_via_typed_param);
RUN_TEST(phplsp_arrow_function_typed_param);
RUN_TEST(phplsp_static_call_resolved);
RUN_TEST(phplsp_self_and_parent);
RUN_TEST(phplsp_use_function_clause);
RUN_TEST(phplsp_phpdoc_var);
RUN_TEST(phplsp_catch_binding);
RUN_TEST(phplsp_this_method);
RUN_TEST(phplsp_method_inherited_via_parent);
RUN_TEST(phplsp_unindexed_receiver_emits_block);
RUN_TEST(phplsp_regression_prompt_value_not_routed_to_helper);
RUN_TEST(phplsp_method_chain_return_type);
RUN_TEST(phplsp_function_global_fallback);
RUN_TEST(phplsp_use_alias_resolves_for_new);
/* Phase 4: depth */
RUN_TEST(phplsp_constructor_property_promotion);
RUN_TEST(phplsp_typed_property_declaration);
RUN_TEST(phplsp_constructor_body_inference);
RUN_TEST(phplsp_narrow_instanceof);
RUN_TEST(phplsp_narrow_assert_instanceof);
RUN_TEST(phplsp_phpdoc_property_class_tag);
RUN_TEST(phplsp_phpdoc_method_class_tag);
RUN_TEST(phplsp_trait_method_flattened);
RUN_TEST(phplsp_trait_self_use_terminates);
RUN_TEST(phplsp_trait_aliased_import_name_collision_terminates);
RUN_TEST(phplsp_class_aliased_trait_name_collision_terminates);
RUN_TEST(phplsp_trait_collision_shapes_bounded_work_issue765);
RUN_TEST(phplsp_trait_flatten_work_is_linear_issue1265);
RUN_TEST(phplsp_single_file_lookup_work_is_linear_f12);
RUN_TEST(phplsp_match_result_type);
RUN_TEST(phplsp_ternary_result_type);
RUN_TEST(phplsp_method_chain_depth_three);
RUN_TEST(phplsp_lsb_two_deep_chain);
RUN_TEST(phplsp_nullsafe_operator);
RUN_TEST(phplsp_static_keyword_dispatch);
RUN_TEST(phplsp_self_in_method_body);
RUN_TEST(phplsp_variadic_parameter);
RUN_TEST(phplsp_closure_with_use);
RUN_TEST(phplsp_anonymous_function_arg);
RUN_TEST(phplsp_nested_arrow_functions);
RUN_TEST(phplsp_narrow_is_string);
RUN_TEST(phplsp_narrow_is_array);
RUN_TEST(phplsp_multiple_use_clauses);
RUN_TEST(phplsp_group_use_clause);
RUN_TEST(phplsp_fqn_new_expression);
RUN_TEST(phplsp_foreach_iteration);
RUN_TEST(phplsp_catch_multi_type);
RUN_TEST(phplsp_namespaced_catch);
RUN_TEST(phplsp_implements_multiple_interfaces);
RUN_TEST(phplsp_method_on_typed_field);
RUN_TEST(phplsp_child_overrides_parent_method);
RUN_TEST(phplsp_phpdoc_param_method);
RUN_TEST(phplsp_static_field_access);
RUN_TEST(phplsp_class_constant_access);
RUN_TEST(phplsp_anonymous_class);
RUN_TEST(phplsp_enum_method_dispatch);
RUN_TEST(phplsp_readonly_property);
RUN_TEST(phplsp_static_factory_chain);
RUN_TEST(phplsp_stdlib_datetime_chain);
RUN_TEST(phplsp_stdlib_throwable_chain);
RUN_TEST(phplsp_stdlib_iterator);
RUN_TEST(phplsp_abstract_class_method);
RUN_TEST(phplsp_static_call_via_alias);
RUN_TEST(phplsp_function_call_namespaced);
RUN_TEST(phplsp_reassignment_changes_type);
RUN_TEST(phplsp_use_then_new_aliased);
RUN_TEST(phplsp_narrow_branches);
RUN_TEST(phplsp_sequential_bindings);
RUN_TEST(phplsp_chained_with_intermediate_var);
RUN_TEST(phplsp_trait_multiple_methods);
RUN_TEST(phplsp_cast_expression);
RUN_TEST(phplsp_clone_preserves_type);
RUN_TEST(phplsp_empty_class_body);
RUN_TEST(phplsp_stdlib_psr_logger);
RUN_TEST(phplsp_long_method_chain);
RUN_TEST(phplsp_conditional_return);
RUN_TEST(phplsp_phpdoc_var_at_assignment);
RUN_TEST(phplsp_narrow_is_int);
RUN_TEST(phplsp_encapsed_string);
RUN_TEST(phplsp_heredoc);
RUN_TEST(phplsp_cyclic_embed_bounded);
RUN_TEST(phplsp_many_use_clauses);
RUN_TEST(phplsp_chain_through_this_field);
RUN_TEST(phplsp_trait_with_alias);
RUN_TEST(phplsp_yield_expression);
RUN_TEST(phplsp_bool_literals);
/* Phase 4g/4l + stdlib expansion */
RUN_TEST(phplsp_generic_var_collection);
RUN_TEST(phplsp_foreach_array_typed);
RUN_TEST(phplsp_negative_narrow_early_return);
RUN_TEST(phplsp_negative_narrow_throw);
RUN_TEST(phplsp_eloquent_builder_chain);
RUN_TEST(phplsp_collection_chain);
RUN_TEST(phplsp_symfony_request);
RUN_TEST(phplsp_carbon_chain);
RUN_TEST(phplsp_nested_if_narrowing);
RUN_TEST(phplsp_assert_then_chain);
RUN_TEST(phplsp_psr7_with_chain);
RUN_TEST(phplsp_at_method_then_real);
RUN_TEST(phplsp_property_method_combined);
RUN_TEST(phplsp_model_query_chain);
RUN_TEST(phplsp_long_eloquent_chain);
RUN_TEST(phplsp_generic_two_args);
RUN_TEST(phplsp_throwable_subclass_message);
RUN_TEST(phplsp_instance_then_static_chain);
RUN_TEST(phplsp_self_method_chain);
RUN_TEST(phplsp_interface_typed_param);
/* Phase 4m-w: depth, generics, narrowing operators */
RUN_TEST(phplsp_generic_substitution_simple);
RUN_TEST(phplsp_closure_use_captures);
RUN_TEST(phplsp_conjunction_narrowing);
RUN_TEST(phplsp_is_a_narrowing);
RUN_TEST(phplsp_subscript_array_typed);
RUN_TEST(phplsp_subscript_array_keyed);
RUN_TEST(phplsp_enum_case_value);
RUN_TEST(phplsp_template_extends);
RUN_TEST(phplsp_symfony_console);
RUN_TEST(phplsp_doctrine_chain);
RUN_TEST(phplsp_guzzle_chain);
RUN_TEST(phplsp_twig_render);
RUN_TEST(phplsp_nested_closure_capture);
RUN_TEST(phplsp_three_conjunction);
RUN_TEST(phplsp_model_where_first);
RUN_TEST(phplsp_namespaced_alias_static);
RUN_TEST(phplsp_logger_through_field);
RUN_TEST(phplsp_closure_multiple_captures);
RUN_TEST(phplsp_is_a_then_chain);
RUN_TEST(phplsp_mixed_narrow_call);
RUN_TEST(phplsp_foreach_key_value);
RUN_TEST(phplsp_static_class_const);
RUN_TEST(phplsp_ternary_chain);
RUN_TEST(phplsp_trait_insteadof);
RUN_TEST(phplsp_value_method_typed_receiver);
/* Phase 4t-x: deeper chains, framework chains */
RUN_TEST(phplsp_generator_foreach);
RUN_TEST(phplsp_long_property_chain);
RUN_TEST(phplsp_conjunction_with_isset);
RUN_TEST(phplsp_match_class_arms);
RUN_TEST(phplsp_static_factory_via_alias);
RUN_TEST(phplsp_static_call_fqn);
RUN_TEST(phplsp_method_returns_typed_generic);
RUN_TEST(phplsp_nullsafe_after_narrowing);
RUN_TEST(phplsp_logger_in_console_command);
RUN_TEST(phplsp_doctrine_repo_find);
RUN_TEST(phplsp_model_static_query);
RUN_TEST(phplsp_switch_statement);
RUN_TEST(phplsp_interface_const);
RUN_TEST(phplsp_factory_method_chain);
RUN_TEST(phplsp_phpdoc_intersection);
RUN_TEST(phplsp_abstract_via_concrete);
RUN_TEST(phplsp_phpdoc_var_on_param_chain);
RUN_TEST(phplsp_fqn_method_chain);
RUN_TEST(phplsp_nested_namespace);
RUN_TEST(phplsp_carbon_static_chain);
RUN_TEST(phplsp_psr_container_get);
RUN_TEST(phplsp_variadic_typed_then_call);
RUN_TEST(phplsp_method_covariant_return);
RUN_TEST(phplsp_interface_inheritance);
RUN_TEST(phplsp_magic_get_set);
/* Phase 4y-ab + 5e + final scenarios */
RUN_TEST(phplsp_at_return_this_fluent);
RUN_TEST(phplsp_at_return_static_covariant);
RUN_TEST(phplsp_symfony_cache);
RUN_TEST(phplsp_event_dispatcher);
RUN_TEST(phplsp_mailer_chain);
RUN_TEST(phplsp_laravel_request);
RUN_TEST(phplsp_laravel_auth);
RUN_TEST(phplsp_laravel_view);
RUN_TEST(phplsp_promise_chain);
RUN_TEST(phplsp_monolog_logger);
RUN_TEST(phplsp_reflection_class);
RUN_TEST(phplsp_validator);
RUN_TEST(phplsp_psr_cache);
RUN_TEST(phplsp_many_parents);
RUN_TEST(phplsp_multi_interface_impl);
RUN_TEST(phplsp_static_self_return);
RUN_TEST(phplsp_static_this_phpdoc);
RUN_TEST(phplsp_field_interface_typed);
RUN_TEST(phplsp_foreach_collection_typed);
RUN_TEST(phplsp_nullsafe_null_coalesce);
RUN_TEST(phplsp_nested_ternary_instance);
RUN_TEST(phplsp_trait_in_chain);
RUN_TEST(phplsp_interface_method_via_class);
RUN_TEST(phplsp_static_through_interface);
RUN_TEST(phplsp_foreach_iterator_current);
RUN_TEST(phplsp_trait_uses_this);
RUN_TEST(phplsp_cast_then_chain);
RUN_TEST(phplsp_iterator_full_api);
RUN_TEST(phplsp_carbon_parse_chain);
RUN_TEST(phplsp_psr7_request_builder);
RUN_TEST(phplsp_array_object_iterator);
RUN_TEST(phplsp_phpdoc_param_then_chain);
RUN_TEST(phplsp_twig_render_array);
RUN_TEST(phplsp_aliased_static_chain);
RUN_TEST(phplsp_foreach_with_control);
RUN_TEST(phplsp_sequential_assert_chain);
RUN_TEST(phplsp_trait_in_inherited);
RUN_TEST(phplsp_multiple_narrowing_types);
RUN_TEST(phplsp_eloquent_query_full);
RUN_TEST(phplsp_chained_field_method);
RUN_TEST(phplsp_nullsafe_field);
RUN_TEST(phplsp_closure_invoke);
RUN_TEST(phplsp_psr_log_constants);
RUN_TEST(phplsp_eloquent_first_or_create);
RUN_TEST(phplsp_container_get);
RUN_TEST(phplsp_multiple_classes_one_file);
RUN_TEST(phplsp_type_valid_after_branch);
RUN_TEST(phplsp_foreach_kv_chain);
RUN_TEST(phplsp_self_ref_recursive);
RUN_TEST(phplsp_realistic_laravel_controller);
/* Phase 5e+ edge cases & robustness (50 more tests) */
RUN_TEST(phplsp_edge_namespaced_facade);
RUN_TEST(phplsp_edge_chain_through_promotion);
RUN_TEST(phplsp_edge_static_property_chain);
RUN_TEST(phplsp_edge_enum_method_chain);
RUN_TEST(phplsp_edge_call_after_assert_two_levels);
RUN_TEST(phplsp_edge_negative_narrow_chain);
RUN_TEST(phplsp_edge_closure_bound_class);
RUN_TEST(phplsp_edge_late_assignment_chain);
RUN_TEST(phplsp_edge_callable_type_param);
RUN_TEST(phplsp_edge_nullable_param_chain);
RUN_TEST(phplsp_edge_phpdoc_ignored_then_real);
RUN_TEST(phplsp_edge_attribute_above_class);
RUN_TEST(phplsp_edge_method_with_ref_param);
RUN_TEST(phplsp_edge_abstract_no_body);
RUN_TEST(phplsp_edge_interface_only);
RUN_TEST(phplsp_edge_nested_match);
RUN_TEST(phplsp_edge_long_static_chain);
RUN_TEST(phplsp_edge_iterator_iterable_param);
RUN_TEST(phplsp_edge_array_of_classes);
RUN_TEST(phplsp_edge_phpdoc_var_then_assign_chain);
RUN_TEST(phplsp_edge_two_traits_one_class);
RUN_TEST(phplsp_edge_enum_with_interface);
RUN_TEST(phplsp_edge_constructor_promote_then_use);
RUN_TEST(phplsp_edge_anonymous_class_in_call);
RUN_TEST(phplsp_edge_arrow_function_capture_implicit);
RUN_TEST(phplsp_edge_long_namespace_chain);
RUN_TEST(phplsp_edge_psr_logger_chain);
RUN_TEST(phplsp_edge_eloquent_relations);
RUN_TEST(phplsp_edge_collection_pluck_first);
RUN_TEST(phplsp_edge_doctrine_qb_orderBy_setMax);
RUN_TEST(phplsp_edge_guzzle_chain);
RUN_TEST(phplsp_edge_psr7_full_lifecycle);
RUN_TEST(phplsp_edge_dt_immutable_chain);
RUN_TEST(phplsp_edge_throwable_chain);
RUN_TEST(phplsp_edge_psr_container_resolve);
RUN_TEST(phplsp_edge_nullsafe_long);
RUN_TEST(phplsp_edge_self_in_constructor);
RUN_TEST(phplsp_edge_static_in_constructor);
RUN_TEST(phplsp_edge_parent_method);
RUN_TEST(phplsp_edge_method_visibility_modifiers);
RUN_TEST(phplsp_edge_final_class);
RUN_TEST(phplsp_edge_class_const_used_in_call);
RUN_TEST(phplsp_edge_parameter_default_value);
RUN_TEST(phplsp_edge_return_type_union);
RUN_TEST(phplsp_edge_multi_param_typed);
RUN_TEST(phplsp_edge_function_with_default_typed);
RUN_TEST(phplsp_edge_property_with_default);
RUN_TEST(phplsp_edge_private_static);
/* Phase 5f / 4ac / 4ad: phpstan-type aliases, closure binding,
* conditional types, variance, real-world patterns */
RUN_TEST(phplsp_phpstan_type_alias_basic);
RUN_TEST(phplsp_phpstan_type_alias_via_param);
RUN_TEST(phplsp_psalm_type_alias_alternate_spelling);
RUN_TEST(phplsp_phpstan_import_type_no_crash);
RUN_TEST(phplsp_closure_bindTo_resolves_this);
RUN_TEST(phplsp_closure_static_bind_resolves_this);
RUN_TEST(phplsp_closure_static_bind_arrow_fn);
RUN_TEST(phplsp_conditional_return_no_crash);
RUN_TEST(phplsp_template_covariant_no_crash);
RUN_TEST(phplsp_template_contravariant_no_crash);
RUN_TEST(phplsp_phpstan_type_array_shape);
RUN_TEST(phplsp_phpstan_type_union);
RUN_TEST(phplsp_eloquent_macro_pattern);
RUN_TEST(phplsp_array_shape_call_no_crash);
RUN_TEST(phplsp_class_string_no_crash);
RUN_TEST(phplsp_int_range_no_crash);
RUN_TEST(phplsp_literal_string_no_crash);
RUN_TEST(phplsp_recursive_phpstan_alias);
RUN_TEST(phplsp_phpstan_type_with_template);
RUN_TEST(phplsp_closure_bind_chain);
RUN_TEST(phplsp_long_phpdoc_block);
RUN_TEST(phplsp_realistic_repository_pattern);
RUN_TEST(phplsp_realistic_event_listener);
RUN_TEST(phplsp_realistic_command);
RUN_TEST(phplsp_realistic_form_handler);
RUN_TEST(phplsp_realistic_mail_sender);
RUN_TEST(phplsp_realistic_cached_service);
RUN_TEST(phplsp_realistic_logger_chain_with_context);
RUN_TEST(phplsp_realistic_carbon_chain_in_method);
RUN_TEST(phplsp_realistic_eloquent_model);
RUN_TEST(phplsp_ordinary_same_leaf_calls_join_by_exact_site);
}