Commit Graph
3 Commits
Author SHA1 Message Date
rikurunico c7bc5fe4c0 fix(php): route ALL scoped calls through the PHP resolver, not just uppercase receivers
engine.py's defer guard only deferred a scoped_call_expression into raw_calls
when member_receiver[:1].isupper() (or is_this_field_call/_csharp_defer), so
self::, static::, parent::, $var::, and lowercase-first class receivers fell
through to the pre-existing per-file bare-name path instead of reaching
_resolve_php_member_calls. That path is last-write-wins across the whole file
and can mis-bind a same-named method on an unrelated class.

Tag scoped_call_expression with is_scoped_call and add it to the defer
condition unconditionally, so every `::` call is deferred to raw_calls and
resolved by the dedicated resolver regardless of receiver casing or sigil.

Also tighten the self::/static:: and $var:: fixtures with a same-named decoy
method on another class, so they actually prove enclosing-class scoping
and no-fabrication instead of passing on a bare-name coincidence.

Addresses review feedback on PR #3874.

(cherry picked from commit 801c2a2dfd)
2026-10-12 14:50:04 +01:00
rikurunico b0fd76a61c fix(php): handle case-insensitivity and namespace qualification in static-call resolution
Addresses 3 findings from the automated review on PR #3874:

- Fully-qualified scopes (\App\Models\User::method() or
  App\Models\User::method()) were left parked as unresolved: the class
  lookup keyed on the full scope text, which never matches a class
  node's own unqualified label. Now normalizes to the last namespace
  segment before lookup.
- Method names were matched case-sensitively even though PHP method
  calls are case-insensitive; a call spelled with different case than
  the declaration silently produced no edge. The lookup key is now
  lowercased on both sides.
- A lowercase-first class name (legal PHP, just non-PSR) fell through
  the `receiver[:1].isupper()` gate into "dynamic scope" and was
  dropped entirely. The gate is now `not receiver.startswith("$")`,
  since a `$var::` scope is the only case actually requiring the
  declared-type table this resolver doesn't have.

(cherry picked from commit 7ed0e1d70a)
2026-10-12 14:49:34 +01:00
rikurunico 2d37da18c9 fix(php): resolve Class::method() static calls to the actual method
scoped_call_expression (`Helper::format()`) used the scope text as the
callee name instead of the method name, so the shared cross-file
bare-name pass bound the call to whatever node shared the class's
label - almost always the class definition - never the actual method.
It also skipped member-call handling entirely, so the edge went
through the import-evidence confidence gate: same-namespace PHP
classes never need a `use` import for each other, so every
same-namespace static call was stuck at INFERRED 0.85 even though the
receiver class is named explicitly in source.

Capture the real method name as the callee and the scope as the
receiver, mark it a member call, and add a dedicated PHP resolver
(mirroring the existing Java/Swift/C++ member-call resolvers) that
binds self::/static:: to the enclosing class, parent:: through a
single inherits edge, and Class:: to the class's own method - all
EXTRACTED, since the receiver is named explicitly in source. A
dynamic scope ($var::method()) has no declared-type table to resolve
against and is left unresolved rather than guessed.

Fixes #3872

(cherry picked from commit 2ec460168b)
2026-10-12 14:49:34 +01:00