Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
68 commits
Select commit Hold shift + click to select a range
43cd7a2
feat(php): resolve $this->prop->method() to the property's declared t…
filipechagas Aug 5, 2026
ae95261
feat(php): resolve (new Service())->method() with FQN corroboration (#3)
filipechagas Aug 5, 2026
a707b42
feat(php): resolve typed locals and typed params with scope poisoning…
filipechagas Aug 5, 2026
6d33de1
fix(extract): skip language-tagged raw calls in the Swift, Python and…
filipechagas Aug 5, 2026
4f17dd8
feat(php): refuse interface-typed receivers, even under short-name co…
filipechagas Aug 5, 2026
f567a36
Merge branch 'feat/php-member-calls-1682-t6': mixed-corpus lang-tag h…
filipechagas Aug 5, 2026
2e68ec6
fix(extract): language-scope the PHP and ObjC receiver type indexes (#8)
filipechagas Aug 5, 2026
5de8f76
feat(php): refuse enum- and trait-typed receivers alongside interface…
filipechagas Aug 5, 2026
0cb78af
fix(php): poison receiver types rebound by `global` and `static` stat…
filipechagas Aug 5, 2026
8d051c6
fix(php): keep the interface refusal across incremental rebuilds (#11)
filipechagas Aug 5, 2026
49b665e
fix(php): corroborate an inline-`new` FQN against the declared namesp…
filipechagas Aug 5, 2026
c2c3e89
Merge branch 'feat/php-member-calls-1682-t6': interface refusal acros…
filipechagas Aug 5, 2026
85bb8df
fix(php): carry enum and trait names across incremental rebuilds too …
filipechagas Aug 5, 2026
db7c8f8
chore: bump to 0.9.34; changelog and docs for #1682 (#2/#3/#4/#5/#6/#…
filipechagas Aug 5, 2026
102ae79
Merge pull request #17 from lawnstarter/feat/php-member-calls-1682
filipechagas Aug 5, 2026
cf49aff
fix(php): emit indirect_call for first-class callables (#15)
filipechagas Aug 5, 2026
f310fb5
fix(php): refuse the same-file bare-name edge for union/intersection …
filipechagas Aug 5, 2026
fe25b51
fix(php): recognize PHP 8.2 DNF property and promoted-param types (#9)
filipechagas Aug 5, 2026
f8c58e6
feat(php): capture use FQN/alias/kind metadata on imports edges (#19)
filipechagas Aug 5, 2026
6e91dbd
Merge pull request #25 from lawnstarter/fix/php-union-intersection-9
filipechagas Aug 5, 2026
fbda3ec
fix(extract): scope member-call resolvers to the sources they own (#10)
filipechagas Aug 5, 2026
d97e37d
Merge branch 'v8' into fix/php-fcc-indirect-call-15
filipechagas Aug 5, 2026
2a9f9fe
Merge pull request #27 from lawnstarter/fix/php-fcc-indirect-call-15
filipechagas Aug 5, 2026
e147764
Merge branch 'v8' into fix/member-call-lang-scoping-10
filipechagas Aug 5, 2026
ee3a411
Merge pull request #28 from lawnstarter/fix/member-call-lang-scoping-10
filipechagas Aug 5, 2026
d40d006
Merge remote-tracking branch 'origin/v8' into feat/php-use-import-cap…
filipechagas Aug 5, 2026
e188ff6
Merge pull request #29 from lawnstarter/feat/php-use-import-capture-19
filipechagas Aug 5, 2026
45f7de1
fix(php): stop group-form `use function`/`use const` from claiming cl…
filipechagas Aug 5, 2026
7006988
Merge pull request #30 from lawnstarter/fix/php-group-use-kind-26
filipechagas Aug 5, 2026
68a3274
feat(extract): keep written qualified PHP receiver types (#20)
filipechagas Aug 5, 2026
64da0eb
Merge remote-tracking branch 'origin/v8' into feat/php-qualified-rece…
filipechagas Aug 5, 2026
e713e02
Merge pull request #31 from lawnstarter/feat/php-qualified-receiver-t…
filipechagas Aug 5, 2026
3e148ef
fix(php): refuse a member call whose receiver type the file claims (#21)
filipechagas Aug 5, 2026
cec9cdd
Merge pull request #32 from lawnstarter/feat/php-name-resolver-21
filipechagas Aug 5, 2026
49c25ff
fix(extract): scope the last four receiver-type indexes to their own …
filipechagas Aug 6, 2026
10cdee2
fix(extract): keep stem-named import edges alive across a same-stem s…
filipechagas Aug 6, 2026
d724156
Merge pull request #34 from lawnstarter/fix/cross-language-isolation-…
filipechagas Aug 6, 2026
bf0a92c
feat(extract): bind claimed PHP receiver types to their declared FQNs…
filipechagas Aug 6, 2026
520e58f
feat(extract): persist PHP declared FQNs for incremental-rebuild pari…
filipechagas Aug 6, 2026
5517334
Merge pull request #35 from lawnstarter/feat/php-positive-alias-bindi…
filipechagas Aug 6, 2026
725d082
Merge upstream/v8 (v0.9.34) into chore/sync-upstream-v8
filipechagas Aug 6, 2026
ff970bb
Merge pull request #36 from lawnstarter/chore/sync-upstream-v8
filipechagas Aug 6, 2026
87a7398
feat(serve): demote relational-intent terms from the seed guarantee (…
filipechagas Aug 6, 2026
cebf231
feat(serve): skip the seed guarantee for already-covered terms (#41)
filipechagas Aug 6, 2026
4e8ff46
feat(serve): relax a starving heuristic context filter (#42)
filipechagas Aug 6, 2026
635db73
fix(serve): judge covered-term coverage on the seed label, never its …
filipechagas Aug 6, 2026
ebe7b67
Merge feat/seeding-fix-37-c3: heuristic-filter starvation fallback (#42)
filipechagas Aug 6, 2026
457b31e
chore: bump to 0.9.36; changelog for the query seeding fix (#40/#41/#…
filipechagas Aug 6, 2026
2033c46
Merge pull request #44 from lawnstarter/feat/seeding-fix-37
filipechagas Aug 6, 2026
be80845
Merge upstream/v8 (v0.9.35) into chore/sync-upstream-0935
filipechagas Aug 6, 2026
acf29e1
fix(serve): only the top-ranked seed may declare a term covered (#37)
filipechagas Aug 6, 2026
a9adfc6
Merge pull request #45 from lawnstarter/chore/sync-upstream-0935
filipechagas Aug 6, 2026
075038e
fix(engine): refuse relative-scope names as PHP scoped-call callees (…
filipechagas Aug 7, 2026
313a168
fix(serve): find_node_ambiguity must not collapse sourceless rivals i…
filipechagas Aug 7, 2026
9c4db41
fix(extract): PHP interface/trait/enum declarations mint canonical no…
filipechagas Aug 7, 2026
258d8e3
fix(resolution): resolve imports edges from their own target_fqn meta…
filipechagas Aug 7, 2026
293d57e
fix(extract): bind PHP receiver type from ctor-body assignment of a t…
filipechagas Aug 7, 2026
1606462
chore: bump to 0.9.38; changelog for the stub-shadow + call-edge fixe…
filipechagas Aug 7, 2026
18c17d8
Merge pull request #51 from lawnstarter/fix/stub-shadowing-46
filipechagas Aug 7, 2026
9069df5
fix(extract): PHP function-call sites never bare-match method or clas…
filipechagas Aug 7, 2026
630072e
fix(serve): scored endpoint resolution prefers sourced nodes over sou…
filipechagas Aug 7, 2026
26f7b03
fix(extract): lift the redundant _php_non_class_types receiver refusa…
filipechagas Aug 7, 2026
ec4d97e
chore: bump to 0.9.39; changelog for the call-resolution + endpoint-s…
filipechagas Aug 7, 2026
93657ef
Merge pull request #57 from lawnstarter/fix/resolution-gaps-52-53-54
filipechagas Aug 7, 2026
5e9d95c
Merge upstream/v8 (v0.9.37) into chore/sync-upstream-0937
filipechagas Aug 9, 2026
4492f9f
Merge upstream/v8 (v0.9.39) into chore/sync-upstream-0939
filipechagas Aug 10, 2026
a79ebe0
chore: consolidate CHANGELOG under 0.9.40 for the single upstream PR …
filipechagas Aug 11, 2026
b69dab8
Merge upstream 'v8' (v0.9.40) into consolidated/php-support-1682
filipechagas Aug 11, 2026
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
50 changes: 50 additions & 0 deletions CHANGELOG.md

Large diffs are not rendered by default.

6 changes: 6 additions & 0 deletions docs/how-it-works.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,12 @@ EXTRACTED edges always have confidence 1.0. INFERRED edges use a discrete rubric
- **0.65** — weak (naming similarity only)
- **0.55** — speculative

That rubric describes edges Claude inferred. The per-language member-call resolvers are a separate, deterministic source of INFERRED edges: they read the receiver's declared type out of the AST and bind at a fixed **0.8**, reserving EXTRACTED (1.0) for a receiver whose type is named in the source at the call site.

**PHP member calls refuse rather than guess.** `$this->prop->method()`, `$obj?->method()`, a typed parameter and a `$var = new T()` local all bind to the receiver's declared type as INFERRED 0.8; `(new Service())->method()` is EXTRACTED 1.0, but only when the namespace written at the call site corroborates the class that was found. When the type is not provably one concrete in-corpus class, no edge is emitted at all — untyped, union- and intersection-typed receivers, a short name that matches two classes, a method the receiver's class does not declare, chained and array-element receivers, a local rebound or shadowed anywhere in the method, anonymous classes, and `self`/`static`/`parent`. A receiver typed by an `interface`, `enum` or `trait` binds to that declaration's own method only; implementations are never guessed — `MailNotifier implements Notifier` gets nothing from a `Notifier`-typed receiver. A Laravel corpus has many identically named `search()`/`log()`/`handle()` methods, so an absent edge is worth more than a guessed one.

A PHP 8.1 first-class callable — `$obj->method(...)`, including the nullsafe and `$this` forms — resolves by exactly those rules but is emitted as **`indirect_call`**, not `calls`: the syntax creates a `Closure` and names the method without invoking it, the same shape the resolver already labels `indirect_call` for a callback passed by name. `calls` therefore keeps meaning "control flow transfers here". The spread form `$obj->method(...$args)` is a real invocation and stays `calls`.

---

## Token benchmark
Expand Down
31 changes: 30 additions & 1 deletion graphify/affected.py
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,22 @@ def _prefer_file_node(
return None


def _is_sourced(graph: nx.Graph, node_id: str) -> bool:
return bool(str(graph.nodes[node_id].get("source_file") or ""))


def _prefer_sourced_node(graph: nx.Graph, node_ids: list[str]) -> str | None:
"""Return the one node with a source file among label rivals, else None.

Mirrors serve's `_find_node_tiers` exact-tier rule (#49): a sourceless node is
an unresolved-reference placeholder, so a real declaration sharing its label is
the answer rather than a tie. Without this, `explain` resolved the sourced node
while `affected` refused with "No unique node match" — #46's symptom.
"""
sourced = [node_id for node_id in node_ids if _is_sourced(graph, node_id)]
return sourced[0] if len(sourced) == 1 else None


def resolve_seed(graph: nx.Graph, query: str) -> str | None:

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Health regressionresolve_seed()

18 callers depend on it (afferent coupling).

Grounded coupling-delta finding (deterministic), not an LLM guess.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Health regressionresolve_seed()

18 callers depend on it (afferent coupling).

Grounded coupling-delta finding (deterministic), not an LLM guess.

# A trailing path separator must not change a source-file match — serve's
# _find_node tokenizes the path (which drops it), so strip it here for parity
Expand All @@ -115,8 +131,17 @@ def resolve_seed(graph: nx.Graph, query: str) -> str | None:
for node_id, data in graph.nodes(data=True)
if _normalize_label(str(data.get("label", ""))) == query_lower
]
if len(exact_label_matches) == 1:
if len(exact_label_matches) == 1 and _is_sourced(graph, exact_label_matches[0]):
return exact_label_matches[0]
if exact_label_matches:
sourced = _prefer_sourced_node(graph, exact_label_matches)
if sourced is not None:
return sourced
# A lone match that is a sourceless stub is NOT yet an answer: the real
# declaration may carry a decorated label ("handle()") that only the
# bare-name pass below reaches, and serve's exact tier — which matches
# both forms — would prefer it (#49). That pass is a superset of this
# one, so a stub with no sourced rival still resolves there.
# Callable labels are decorated ("name()"), so a bare "name" query falls
# through exact matching and then ties with any "name*" sibling in the
# contains pass. Match on the undecorated name before giving up.
Expand All @@ -128,6 +153,10 @@ def resolve_seed(graph: nx.Graph, query: str) -> str | None:
]
if len(bare_name_matches) == 1:
return bare_name_matches[0]
if bare_name_matches:
sourced = _prefer_sourced_node(graph, bare_name_matches)
if sourced is not None:
return sourced
exact_source_matches = [
str(node_id)
for node_id, data in graph.nodes(data=True)
Expand Down
15 changes: 14 additions & 1 deletion graphify/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -3373,7 +3373,20 @@ def _ctx_identity(source_file) -> str | None:
"file_type": _node.get("file_type"),
"type": _node.get("type"),
}
for _marker in ("_callable", "_callable_class"):
# `_php_non_class_types` (#11, #12) rides the same
# marker channel as the callability flags: it records
# which of an unchanged PHP file's declarations are
# interfaces, enums or traits rather than classes. It
# drove a receiver refusal until #53 lifted it, and is
# still carried; `_php_interfaces` is the pre-#12
# spelling, kept for older graphs. `_php_class_fqns`
# (#23) is what resolution reads today: the declared
# FQNs that let a claimed `use` import bind into an
# unchanged defining file (#22), and let the guard
# refuse a vendor import that only shares a short name.
for _marker in ("_callable", "_callable_class",
"_php_non_class_types", "_php_interfaces",
"_php_class_fqns"):
if _node.get(_marker):
_ctx_node[_marker] = _node[_marker]
_ctx_nodes.append(_ctx_node)
Expand Down
Loading