Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,10 @@ Full release notes with details on each version: [GitHub Releases](https://githu
- Feature: PHP instance-method calls on a typed receiver now resolve to the method they really reach, instead of a bare same-name match (#1682). `$this->prop->method()` binds to the type declared on the property — including a constructor-promoted param — and the same typing covers nullsafe receivers (`$obj?->method()`), natively typed parameters, and `$var = new T()` locals. `(new Service())->method()` resolves too and is the one form tagged EXTRACTED (1.0), since the class is named right there in the source; the promotion applies only when the written namespace corroborates the class node found for it, compared against the namespace the defining file DECLARES rather than against its PSR-4 path (a file at `app/Services/Client.php` may well declare `namespace App\Vendor;`). Every other typed receiver is INFERRED (0.8). The motivating case is a Laravel corpus: `$this->leadHunter->search(...)` used to reach nothing, and now reaches `App\Services\LeadHunterService::search`.
- Refuse-don't-guess is the policy wherever the receiver's type is not provably one concrete in-corpus class, so all of these deliberately emit NO edge: untyped, union-typed (`A|B`) and intersection-typed (`A&B`) receivers; receivers typed by an `interface`, `enum` or `trait` — none of which mints a definition node, so binding one would pick a same-short-named stranger (the `App\Contracts\Notifier` vs `App\Support\Notifier` collision, or `App\Enums\Status` beside an Eloquent `App\Models\Status`), and that refusal now survives an incremental rebuild by persisting those names on the declaring file's node; a short type name that does not match exactly one class in the corpus; a method the receiver's own class does not declare, so `__call` magic dispatch fabricates nothing; chained (`$this->factory()->method()`) and array-element (`$bag['k']->method()`) receivers; a local rebound to anything but a matching `new`, or rebound to other storage by `global`/`static`; a name shadowed by a closure or arrow-function parameter, a `foreach` target, or list destructuring; anonymous classes (`new class { ... }`); and `self`/`static`/`parent` in type position, which need inheritance context the raw-call facts do not carry.
- Behavior change: a same-file call through a typed receiver moves from EXTRACTED to INFERRED (0.8). Those calls used to be minted by the in-file bare-name matcher, which cannot tell the property's declared type apart from any other class in the file; they are now routed through the receiver-typed resolver, which is right more often but no longer claims to be certain. Untyped receivers keep their existing in-file behavior, so this is a confidence change on typed receivers only, not a drop in edge count. One asymmetry is visible in the output and worth knowing about: a fully qualified `(new \App\Services\Client())->method()` is EXTRACTED, while the same name written as a local (`$c = new \App\Services\Client(); $c->method();`) stays INFERRED — the inline form is corroborated against the declared namespace, the local form is typed through the method-scoped table and is not.
- Fix: the PHP and Objective-C member-call resolvers no longer match a receiver's type against class definitions written in ANY language; each index is scoped to its own source suffixes. This cut both ways in a polyglot corpus, so it is two fixes: a Python `class Lead` could be bound as a PHP or ObjC receiver's type and mint a cross-language edge, and a foreign class merely SHARING a short name pushed the single-definition guard to 2 and silently suppressed the correct same-language edge. Polyglot corpora therefore also GAIN PHP and ObjC edges that a name collision previously deleted. The ObjC half is a pre-existing defect of the same shape that rides along with the PHP work; the Java and C# halves are fixed by the next bullet, and the same exposure in the C++, Swift, TypeScript and Python resolvers is untouched and left as a follow-up (`lawnstarter/graphify#24`).
- Fix: a member-call resolver no longer mints edges out of another language's data (`lawnstarter/graphify#10`), which is again two fixes. First, the Swift, Python and TypeScript resolvers claimed a raw call by skipping anything carrying a `lang` tag — but only the cpp, csharp, java, objc and php extractors stamp one, and those three resolvers are themselves untagged, so they consumed each other's raw calls: a TypeScript `Lead.search({})` reached the Python resolver's capitalized-receiver class arm and minted an EXTRACTED edge into a Python method with no TypeScript `Lead` anywhere in the corpus. Each now consumes only raw calls written in the source files it owns, a positive suffix filter that is closed by construction rather than a hardcoded list of languages to exclude; the tagged languages keep matching on `lang`, because C++ and ObjC share `.h` and a suffix cannot tell their raw calls apart. Second, the Java and C# receiver-type indexes are now scoped to their own sources, the last copies of the shape the previous bullet fixed for PHP and ObjC: a Java `Lead lead; lead.search()` bound to a Python `class Lead` at INFERRED. Polyglot corpora therefore LOSE cross-language member-call edges that were always wrong — including some labelled EXTRACTED, the strongest confidence — and GAIN Java and C# edges that a foreign class merely sharing a short name previously deleted. Single-language corpora are unaffected: every filter added here admits everything such a corpus contains. Both defects predate the receiver-typed PHP work; the same index exposure in the C++, Swift, TypeScript and Python resolvers is tracked as `lawnstarter/graphify#24`.
- Fix: the PHP and Objective-C member-call resolvers no longer match a receiver's type against class definitions written in ANY language; each index is scoped to its own source suffixes. This cut both ways in a polyglot corpus, so it is two fixes: a Python `class Lead` could be bound as a PHP or ObjC receiver's type and mint a cross-language edge, and a foreign class merely SHARING a short name pushed the single-definition guard to 2 and silently suppressed the correct same-language edge. Polyglot corpora therefore also GAIN PHP and ObjC edges that a name collision previously deleted. The ObjC half is a pre-existing defect of the same shape that rides along with the PHP work; the Java and C# halves are fixed by the next bullet, and the C++, Swift, TypeScript and Python halves by the one after it.
- Fix: a member-call resolver no longer mints edges out of another language's data (`lawnstarter/graphify#10`), which is again two fixes. First, the Swift, Python and TypeScript resolvers claimed a raw call by skipping anything carrying a `lang` tag — but only the cpp, csharp, java, objc and php extractors stamp one, and those three resolvers are themselves untagged, so they consumed each other's raw calls: a TypeScript `Lead.search({})` reached the Python resolver's capitalized-receiver class arm and minted an EXTRACTED edge into a Python method with no TypeScript `Lead` anywhere in the corpus. Each now consumes only raw calls written in the source files it owns, a positive suffix filter that is closed by construction rather than a hardcoded list of languages to exclude; the tagged languages keep matching on `lang`, because C++ and ObjC share `.h` and a suffix cannot tell their raw calls apart. Second, the Java and C# receiver-type indexes are now scoped to their own sources, the last copies of the shape the previous bullet fixed for PHP and ObjC: a Java `Lead lead; lead.search()` bound to a Python `class Lead` at INFERRED. Polyglot corpora therefore LOSE cross-language member-call edges that were always wrong — including some labelled EXTRACTED, the strongest confidence — and GAIN Java and C# edges that a foreign class merely sharing a short name previously deleted. Single-language corpora are unaffected: every filter added here admits everything such a corpus contains. Both defects predate the receiver-typed PHP work; the same index exposure in the C++, Swift, TypeScript and Python resolvers is closed by the next bullet.
- Fix: the last four receiver-type indexes — C++, Swift, TypeScript and Python — are now scoped to their own sources too, so no member-call resolver matches a receiver's declared type against class definitions written in another language any more (`lawnstarter/graphify#24`). This is the same two-way defect the PHP/ObjC and Java/C# bullets above describe, in its remaining copies: a C++ `Lead lead; lead.search()` bound to a Python `class Lead` at INFERRED, a Swift `let lead: Lead` and a TypeScript `private lead: Lead` did the same, and — in the other direction — a foreign class merely SHARING a short name pushed the single-definition guard to 2 and silently suppressed the correct same-language edge. Polyglot corpora therefore LOSE these cross-language edges, which were always wrong, and GAIN C++, Swift, TypeScript and Python edges that a foreign short-name collision previously deleted. Single-language corpora are unaffected: the filter admits everything such a corpus contains. Two notes on the edges of this change. `.h` is scoped into BOTH the C++ and the Objective-C index, because it routes to either extractor by content and a C++ class and an ObjC `@interface` both live in a header — the two languages are therefore isolated from every other language but not from each other, which no suffix can fix, and raw-call ownership for them stays on the extractor-stamped `lang` for exactly that reason. And Python is scoped on both of its arms, not just the class index: its `module.func()` arm matched any corpus file whose stem equalled the receiver, so `import lead` beside a `lead.ts` bound the call to a TypeScript function at EXTRACTED. All ten resolvers now share one `_is_owned_definition` predicate keyed off the same per-resolver suffix tuple that registers the resolver, so the registration and the scoping cannot drift. Pre-existing, not a regression.
- Fix: an `imports` edge no longer vanishes when any same-stem file sits beside its target, in Python, Rust, Zig, Elixir, PowerShell, Pascal and Bash (`lawnstarter/graphify#33`). Each of these names an import's target by the imported file's bare stem id (`import lead` -> `lead`), which resolves only while that id is unique: add a `lead.md` and the two file nodes collide, so id-disambiguation salts them into `lead_py_lead` and `lead_md_lead` while the edge — keyed by the importer's own file rather than the target's — was left pointing at an id that no longer named anything, and was dropped along with everything downstream of it (Python's `module.func()` call resolution among them). These edges now stamp the `target_file` hint the disambiguator already accepts for this (#1814), so the salt lands on the right file; an import written in one language can only mean a file of that language, so the choice stays unambiguous whatever the collider is. The hint is stamped centrally rather than in each extractor, because an extractor sees one file and cannot know which of the corpus's same-stem candidates the id will end up naming; it is transient and popped by its only reader, so it never reaches `graph.json`. An id claimed by more than one importable file of the same language is left dangling, as before, and a corpus with no collision is bit-identical — the hint only ever selects among the salted variants of an id the edge already named, so it cannot change WHICH node an edge resolves to. Bash additionally emitted the edge twice under a collision — once correct, once dangling — because its second producer in `resolve_bash_source_edges` derives ids from the path after disambiguation has already renamed them; that pass now reads the ids as they actually stand, which also repairs the source-backed `calls` edges it resolves. TypeScript/JavaScript and C/C++/Objective-C already had equivalent protection; Julia, Fortran and Verilog target an importer-scoped node and were never exposed; Dart mints its own stub nodes and Ruby/PHP emit no file-targeting import edges. Pre-existing, not a regression.
- Known recall gaps in PHP member-call resolution, all consequences of refusing rather than guessing: a method reached through a `trait` the receiver's class `use`s gets no edge (traits mint no definition node, so the class carries no `method` edge for it); a method inherited from a cross-file parent class gets no edge (the `inherits` chain is not walked — C# is currently the only resolver that does); an `enum`'s methods are unreachable as call targets for the same reason the enum-typed receiver is refused; and typed parameters are read only inside class methods, so a top-level `function helper(Service $s) { $s->method(); }` resolves nothing. One residual false-positive risk was named here — a property typed through a `use` alias that points OUTSIDE the corpus, while exactly one unrelated class of that short name exists INSIDE it, satisfying the single-definition guard and minting a wrong INFERRED edge — and is closed by the `use`-map fix below. Java still has the identical exposure.
- Fix: a union- or intersection-typed PHP receiver no longer mints a bare-name `calls` edge when the candidate methods live in the SAME file as the call (`lawnstarter/graphify#9`). The refusal above already held across files, but the legacy in-file matcher derived its decision from whether a type had been STAMPED, which made "annotation refused" indistinguishable from "no annotation" — so `private Alpha|Beta $svc; $this->svc->run();` bound to whichever `run()` the file's label index saw last, by file order, at EXTRACTED confidence. The receiver table now tells the two apart, and a refused multi-class annotation defers to the receiver-typed resolver, which emits nothing for an unstamped receiver. **Deletion scope**, stated deliberately because deferring removes edges that exist today: the ONLY edges removed are same-file bare-name edges whose receiver is declared as a union (`A|B`) or an intersection (`A&B`) — including `A|null`, which is semantically `?A` but is a union node, and so loses its same-file edge rather than resolving as one concrete type. Everything else the concrete-type policy also refuses is deliberately left on the in-file arm, because none of it declares MULTIPLE candidate classes: `self`/`static`/`parent` (which name the calling class, whose methods usually ARE the in-file match), primitives, and `mixed`/`object`/`iterable`/`callable`. Genuinely untyped receivers and `$this->method()` are untouched.
- Fix: a PHP 8.2 disjunctive-normal-form property type (`private (A&B)|C $x;`) is no longer skipped outright (`lawnstarter/graphify#9`). DNF parses as its own AST node, which the property and promoted-param scanners did not name among the type shapes they accept, so such a property was invisible twice over: it minted the same-file bare-name `calls` edge the fix above removes (a DNF type is a union at top level, so it has no single receiver class either), and its classes got no `references` edge at all. It now refuses like a union, and references A, B and C like one.
Expand Down
Loading