Covers a plain data literal getting named and marked as likely data,
a real function staying silent, no overlap with the existing syntax
error warning on a genuinely broken file, and the total size count
across more than one file. Confirmed to fail against pre fix code.
(cherry picked from commit 32ad059ebc)
A code file that parses with zero errors but still produces nothing
beyond its own bare file node is usually a plain data literal the AST
extractor has nothing to model (a const array, a module export of
plain data), not a code file that legitimately has no symbols. Those
two cases looked identical in the output: the file silently entered
the graph as an isolated, zero edge node, and nothing reported it. A
real corpus measured in the issue had 88 such nodes covering roughly
40 percent of the corpus, with not one edge among them.
Add a complementary check next to the existing syntax error warning,
gated on the opposite condition (no parse_errors and no extractor
error) so the two can never double report the same file: a file
already explained by a genuine parse error or a missing optional
dependency is excluded here.
Fixes#3946.
(cherry picked from commit 3b3ee82b1a)
The prune counter included a stub created in this run, so it could
excuse a dedup shrink. Count only nodes that were already saved.
Strip both counters inside to_json so other writers cannot persist them.
Co-Authored-By: Grok 4.7 <noreply@x.ai>
(cherry picked from commit 294946d4ce)
A finished extract force-writes, so the node-count check never sees a
graph that dedup made smaller. Refuse that overwrite and name
--allow-dedup-shrink. A deleted file still writes.
Fixes#3774
Co-Authored-By: Grok 4.7 <noreply@x.ai>
(cherry picked from commit 686bef5855)
Follow-up on #4007: is_interpreter_variable fired for any bare-$VAR
command word, so `"$DIR/run.sh" stage.py` could mint a spurious invokes
edge to the argument on top of the real .sh calls edge. A bare
interpreter variable ($PYTHON) has no path separator; gate on
`"/" not in cmd_word`.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
A configurable directive such as
export 'service_stub.dart' if (dart.library.io) 'service_io.dart';
was matched only up to its first quoted string, so the `if` alternatives
were dropped. In the usual Flutter facade the stub is an empty placeholder
and the real implementation lives in the conditional branch, which left
the implementation unreachable from the facade and its importers.
Parse the whole directive and emit one edge per URI: the default URI keeps
its plain edge (unchanged), each alternative gets the same relation tagged
context="conditional_uri" with its own source line. A string literal inside
the condition (`if (app.flavor == 'prod')`) is part of the URI test and is
not mistaken for a URI.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
(cherry picked from commit 2b965f23fc)
Since #1098 a `part of` file's declarations are redirected to the parent
library so one library has one id namespace. The redirect also dropped
the part's own file node, so a scanned source file could not be found by
name (`explain "<part>.dart"` -> no node).
Keep the redirect and keep the node: the part gets its regular file node,
`library --includes--> part` anchored on the `part of` line, and
`part --contains--> declaration` mirrors every `defines` edge the library
gets from this part. Declaration ids and the library's `defines` edges are
unchanged. test_roadmap_bug_fixes asserted the old "no part node"
behaviour and now asserts the part's own node instead.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
(cherry picked from commit e32f49dbad)
Covers python3/python plus a bare interpreter variable, a non shell
interpreter running a dot sh target landing on its entrypoint node,
and two negative cases: a non interpreter command with a script
looking argument, and a missing target. Confirmed to fail against
pre fix code except for the two negative cases, which hold either way.
(cherry picked from commit 97d4c9207c)
The bash extractor recorded functions, source imports, and calls
between functions, but nothing for the programs a script actually
runs. In a pipeline driven by a shell script that is most of what the
script does, so an orchestrator looked disconnected from every stage
it runs: path between the runner and a stage it invokes found
nothing.
Add an invokes edge when a command's first word is a known
interpreter (python, python3, python2, node, nodejs, ruby, perl) or a
bare variable standing in for one, and an argument resolves to an
existing file in the repo with a recognized script extension. Kept
entirely separate from the existing bash to bash runner handling
(bash/sh/zsh/ksh/dash plus direct dot slash invocation), which still
emits its own calls edge unchanged, so this only adds coverage rather
than risking the existing path.
Gated on the target actually existing on disk, same as every other
heuristic edge in this file, so a plain command that merely happens to
take a dot py argument (cp, a custom tool) is never mistaken for an
interpreter invocation.
Fixes#3802.
(cherry picked from commit 52fe75be93)
The shared cross-file call pass bound an unqualified call to any
same-named def in the corpus. In Elixir an unqualified call can only
reach the caller's own module, Kernel, or a module the file imports or
uses, so DSL calls landed on unrelated project functions: every
migration's `table(:users)` (Ecto.Migration, brought in by `use`) became
a calls edge to a Phoenix component's `table/1`, making it the top god
node.
The Elixir extractor now records, on each raw call, the modules its file
names in `import` or `use` (`alias` and `require` do not bring functions
into scope). The shared pass keeps only candidates whose file defines one
of those modules, the same way Go selectors are already filtered by
import path, and leaves the call unresolved otherwise. The module index
is read from the resolution nodes, so an unchanged file's modules still
count on an incremental rebuild.
Calls into a module that a `use` macro imports indirectly (for example
`use MyAppWeb, :live_view` importing CoreComponents) are no longer
resolved, since that import is not visible in the source.
Refs #4001
(cherry picked from commit 98d4ae9878)
_merge_csharp_partial_class_nodes rewrote every edge in the corpus and
dropped every self-loop, so one partial class split across two files
erased recursive calls in every language of the repo. It also deduped
edges it never rewrote, on a key that ignores confidence and context.
Keep edges the merge does not remap verbatim (the Swift extension merge
rule, #2538) and drop a self-loop only when two distinct endpoints
collapse into one node (the deduplicate_entities rule, #3809), so a
pre-existing self-loop on a merged-away half is rewired, not lost.
Co-Authored-By: Claude <noreply@anthropic.com>
(cherry picked from commit 5992044f32)
Regression tests for #4040: read, stale-read and search reminders name the effective graph.json under a custom GRAPHIFY_OUT, and the default output dir stays byte-identical.
Co-Authored-By: Claude <noreply@anthropic.com>
(cherry picked from commit 31cb3bb1c0)
The read, stale-read and search reminders always named graphify-out/graph.json, even when GRAPHIFY_OUT pointed elsewhere. Rewrite the path at emit time from out_path("graph.json"); the default output dir stays byte-identical.
Closes#4040
Co-Authored-By: Claude <noreply@anthropic.com>
(cherry picked from commit 654aefb49f)
A call through an injected `Store` resolves to `Store.fetch()` — the
protocol requirement (#3673) — and the conforming `RemoteStore.fetch()`
is a separate node with nothing joining the two, so every directed walk
through an injected dependency stops at the protocol. Link the two with
the member-level `dispatches_to` edge C# already gets (#3003), under the
same single-conformer / single-owner guard: a protocol with two
conformers, a conformer with two same-named methods, or a default
implementation nobody overrides emits nothing.
The mechanism lives in graphify.interface_dispatch, lifted out of the
C# pass in the preceding refactor; swift_dispatch.py is the `.swift`
gate over it, registered right after the C# pass. Both ends of a pair
must be declared in a Swift file, so a Kotlin class naming the same
supertype never receives a Swift edge.
Known limit, pinned by test: a `class C: P` whose protocol is declared
in another file is classified `inherits` by the per-file classifier and
is not dispatched; struct/enum/actor conformers and `extension C: P`
are.
AI disclosure: Claude Code assisted with this change.
(cherry picked from commit 0ca5c71b1c)
Move the single-implementer dispatch mechanism from csharp_dispatch.py
into interface_dispatch.py with the file-suffix gate as a keyword
parameter. csharp_dispatch.py keeps its public names
(resolve_csharp_interface_dispatch, _method_label, DISPATCH_RELATION)
and becomes the `.cs` gate over the core. No behaviour change: the 15
C# dispatch tests pass untouched, and the extractor registration is
not modified.
Swift protocols have the same shape (#3673 resolves a call through an
injected protocol to the requirement, and the conformer's method sits
unreachable from it). With the core shared, that language can add a
gate instead of a second copy of the algorithm.
AI disclosure: Claude Code assisted with this change.
(cherry picked from commit 6aade32806)
`def sort[A: Ordering]` desugars to an implicit `Ordering[A]`, so a context
bound is a real dependency the function has on that typeclass -- and the
idiomatic form is more common in modern Scala than the desugared
`(implicit ...)` it compiles to. `context_bound` was not referenced anywhere
in the extractor, so the dependency was invisible wherever it appeared.
Two grammar shapes drive the implementation, both verified against
tree-sitter-scala 0.26.0 before writing the fix:
- `context_bound` is a direct child of `type_parameters`, one per bound, so
the walk belongs in the existing per-function Scala block and needs no new
handler.
- A higher-kinded parameter nests a further `type_parameters` for its own arity
brackets (`def program[F[_]: Async: Temporal]`), so the walk recurses rather
than reading direct children only -- otherwise both bounds on `program` are
missed.
`upper_bound` (`[T <: Comparable[T]]`) is a distinct node type and is
deliberately not matched: a subtype constraint is not a dependency, so an edge
there would assert something the source does not say. Bounds also get their
own `type_bound` context rather than reusing `parameter_type`, so a consumer
can tell a typeclass constraint from a parameter annotation.
One trap worth recording, because the neighbouring code invites it:
`type_parameters` and `parameters` carry the *same* field name on
`function_definition`, so `child_by_field_name("parameters")` returns whichever
comes first. The bounds are therefore located by node type, never by field
name -- a field lookup would read the function's real parameters as bounds. A
regression test pins that `xs: List[A]` still resolves under `parameter_type`
and never under `type_bound`.
Verified:
- uv run pytest tests/test_scala_context_bounds.py -q (10 passed)
- 6 of the 10 new tests fail on the pre-fix tree
- uv run pytest tests/ -q -> 28 failed, 6164 passed, 101 skipped; the 28
failures are byte-identical to the clean-tree baseline and none touch Scala
- uv run ruff check . -> clean
- uv run pyright -> 635 errors, identical to the clean-tree baseline
Co-Authored-By: Claude <noreply@anthropic.com>
(cherry picked from commit e293b64f6e)
A type alias (`type Alias = List[Int]`), an `opaque type`, and a `match type`
all declare a dependency on the types they name, but the type-reference
dispatch only fired for `val_definition`/`var_definition`, so these produced
zero `references` edges in any context -- in contrast to a `val`/`var`, whose
annotation is walked.
`match_type` needed a second, independent change: the type-reference walker's
recursion whitelist did not know `match_type`/`type_case_clause`, so even a
correctly dispatched call on a match type's right-hand side would have
resolved nothing. Both are now handled, recursing over named children exactly
as `compound_type` already is.
Verified field names against tree-sitter-scala 0.26.0 before writing the fix:
`type_definition` carries its RHS under the `type` field for all three forms
(`generic_type`, `type_identifier`, `match_type`), so the existing
`child_by_field_name("type")` walk applies verbatim and no new handler is
needed. File-scope aliases inherit the #2054 anchor fallback.
Scope: only the edges are added. The alias still gets no node of its own, which
is a separate already-accepted tradeoff shared with `val`/`given` bindings.
Verified:
- uv run pytest tests/test_scala_type_definition.py -q (10 passed)
- 8 of the 10 new tests fail on the pre-fix tree
- uv run pytest tests/ -q -> 28 failed, 6154 passed, 101 skipped; the 28
failures are byte-identical to the clean-tree baseline and none touch Scala
- uv run ruff check . -> clean
- uv run pyright -> 635 errors, identical to the clean-tree baseline
Co-Authored-By: Claude <noreply@anthropic.com>
(cherry picked from commit 879dfd330d)
Scala 3 allows `val`/`var` to be written directly at file scope with no
enclosing object/class/trait. The `val_definition`/`var_definition`
type-reference dispatch was gated on `and parent_class_nid`, so a file-scope
binding produced no node and no edge of any kind, while the byte-identical
definition nested one level inside any container resolved correctly. The
missing container was the only difference: both positions parse to the same
node type carrying the same `type` field.
This is the Scala instance of a generic defect in the walker's default
recursion path that had already caused and been fixed once for Python
(#1050) -- a position that legitimately has no container must fall back to
the file node rather than be skipped. The fix reuses the `owner_nid =
parent_class_nid or file_nid` idiom already present in the property branch
above, and moves the self-reference guard onto the same anchor so a
file-scope binding cannot mint an edge from the file node to itself.
Behaviour is unchanged for class-nested bindings; a regression test pins
that case explicitly.
Verified:
- uv run pytest tests/test_scala_top_level_binding.py -q (8 passed)
- 6 of the 8 new tests fail on the pre-fix tree, 2 pass either way
(the in-class control, and the self-edge guard which is trivially empty
before the fix and meaningful after it)
- uv run pytest tests/ -q -> 28 failed, 6144 passed, 101 skipped; the 28
failures are byte-identical to the clean-tree baseline (missing
tree-sitter grammars for erlang/R/solidity/vbnet, plus ollama retry
tests) and none touch Scala
- uv run ruff check . -> clean
- uv run pyright -> 635 errors, identical to the clean-tree baseline
(pre-existing debt; this change adds none)
Co-Authored-By: Claude <noreply@anthropic.com>
(cherry picked from commit 2d61ef7e68)
Two small follow-ups on the 0.9.75 batch:
- merge-graphs --previous now reports a clear error and exits 1 on a
malformed/unreadable JSON file instead of crashing with an uncaught
traceback (follow-up to #4006).
- JS/TS super.m() fails closed (no edge) instead of falling back to the
file-wide name map, which bound it to the overriding method as a wrong
self-loop. The extends chain is unknown at this pass, so super can
never be resolved here; inherited this.m() still keeps the plain
lookup (follow-up to #4012).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Covers the exact shape from the issue through the real CLI command
(an imports edge and a calls edge between the same pair surviving a
cluster only run), plus a direct unit test on the export function
confirming only a dangling endpoint is dropped, never a real pair.
Confirmed to fail against pre fix code.
(cherry picked from commit 5bde5b5a06)
cluster only and label reload an existing graph.json into a simple
Graph to re cluster, which keeps one edge per node pair. Writing that
same graph back out then silently dropped a second edge on any pair
that stayed connected by another, for example an imports edge and a
calls edge between the same two nodes. A real corpus measured in the
issue lost 322 edges this way across one run, every one of them a
second edge on a pair that stayed connected.
Community is purely a node attribute; re clustering never needs to
add or remove an edge. Add an original links parameter to to_json
that, when given, writes back every original link verbatim instead
of re deriving the link list from the simple graph, dropping only one
whose endpoint no longer exists in the graph. Wire cluster only and
label to pass the links already loaded from the input file. This also
sidesteps any endpoint order canonicalization undirected storage
would otherwise apply when deriving the link list from the graph,
since the original links keep their exact stored endpoint order.
The parameter is opt in and defaults to the previous behavior, so a
fresh extraction build (which has no prior links to preserve, since
the graph it just built is the only authoritative source) is
unaffected.
Fixes#3999.
(cherry picked from commit 576d03d89c)
Covers the exact shape from the issue: a file imports a symbol that a
second file contains, with no direct file to file edge between them.
Confirmed to fail against pre fix code (reports no path found despite
both halves of the route existing).
(cherry picked from commit 6a7a720894)
A contains edge only runs file to symbol. Reaching a symbol via an
imports/calls/references hop had no edge back out to the file that
defines it, so a file to file dependency that must route through a
shared symbol (the common case for cross file dependencies) reported
no path at all even though both halves of the route existed.
Add the implied reverse hop to path's own directed traversal graph
only, for routing purposes. The printed segment still recovers the
real stored contains edge and its true direction from the graph, same
as any other hop, so the output stays honest about what is actually
stored.
Fixes#3878.
(cherry picked from commit 1201149cb6)
The ollama branch of _call_openai_compat assigned extra_body outright,
so the {"thinking": {"type": "disabled"}} body that
GRAPHIFY_DISABLE_THINKING sets a few lines earlier was discarded and the
flag was silently inert on that backend. Merge the num_ctx / keep_alive
defaults into whatever is already there.
A provider-supplied extra_body is unaffected: that case already skips
the ollama defaults entirely.
Part of #3988.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
(cherry picked from commit f5cc53cf12)
Covers the exact shape from the issue: a real previous merged
clustering restored by node id instead of merge graphs' own fresh
per source offset, plus a new node getting no community rather than
an unrelated offset. Confirmed to fail against pre fix code.
(cherry picked from commit b9f21e9bec)
merge graphs offsets each input's own communities into a shared id
space so they do not fuse (number 3014), but that per source
partition has no relation to the previous MERGED clustering a labels
sidecar was built against. A later cluster only run's label reuse
remap read merge graphs' output as though its community field were
that previous clustering, and silently mismatched almost every
community since it was actually per source ids.
Add a previous flag that reads a prior merged graph.json and restores
its real community value onto each node that survives the merge, by
node id, before the per source offset is ever written out. A node
with no match (new since the previous merge) gets no community rather
than keeping an unrelated per source offset, since an invented value
is worse than none here. The per source partition stays available
regardless, in local community.
Fixes#3858.
(cherry picked from commit 60e153176c)
Covers the exact shape from the issue: two projects, each with their
own absolute cache directory, touched in one process. Confirmed to
fail against pre fix code: shared, poisoned index, no warning on
switch, and a dropped reload when revisiting the first root.
(cherry picked from commit 2429ce1b0e)
_ensure_stat_index bound to the first root/cache_root a process ever
saw and returned immediately for every later call, for the life of
the process. A read for a later root silently came from the first
root's file, and at exit the accumulated in memory index, now holding
entries for every root ever touched, was written back into the first
root's file alone, poisoning it with paths from unrelated projects.
Detect a switch to a different cache root, flush the outgoing root's
own pending entries to its own file before switching, then load the
new root fresh, so each root's file only ever holds that root's own
entries. Also warn on the switch, so a library caller crossing project
roots in one process sees it rather than losing the fastpath silently.
Capture the resolved stat index path at bind time rather than
deriving it fresh from the live out directory at flush time, since a
caller following the documented pattern of setting the out directory
then calling has already pointed it at the new root by the time the
switch is noticed, and deriving it fresh then would flush the outgoing
root's entries into the new root's file instead of its own.
Fixes#3989.
(cherry picked from commit 6175248175)
A `macro_rules! name { ... }` is a named, invocable item, but its node
type (macro_definition) had no branch, so the macro was dropped entirely:
it never became a node, and a `name!(...)` invocation of it (a
macro_invocation, never a call_expression) had nothing to resolve to.
Emit a node per macro_definition and resolve a bare `name!` invocation to
it with a calls edge. Macros are tracked in their own name registry, not
the function/type label map, because a macro and a function can share a
name (`vec!` vs `vec`); the node id is macro-qualified for the same
reason and the label carries a `!`. A scoped invocation (`log::info!`)
is cross-module/crate and stays unresolved (fail-closed).
Adds regression tests for the dropped definition, the resolved local
invocation, name coexistence with a same-named function, and the
unresolved scoped case.
(cherry picked from commit 7010322250)
defmacro/defmacrop (macros) and defguard/defguardp (guard macros) define
named, invocable members with the same head shape as def/defp — a call
head, optionally wrapped in a when binary_operator. Only def/defp were
handled, so these fell through to the generic recursion and were dropped
entirely: the member was never a node, and a call to a locally-defined
macro had nothing to resolve to.
Handle them in the same branch as def/defp. They are already listed in
the call-pass skip keywords, so their defining keyword is never mistaken
for a call.
Adds a regression test proving the macro, private macro, and guard were
previously dropped and that a call to a local macro now resolves.
(cherry picked from commit 237d358e39)
A PHP 8.1 `enum` is in the PHP class_types, so the enum type and its
methods were captured, but the cases (`case Hearts = 'H';`) nest in the
`enum_declaration_list` body as `enum_case` nodes that no handler
visited. They were dropped, leaving the enum a caseless leaf.
Add a PHP extra-walk that emits a node plus a case_of edge per case,
keying the name off the case's `name` field. This brings PHP to parity
with Java #1719 (enum_constant), Scala, Swift, and C++. Backed enums
(`: string`) and pure enums are both handled, and enum methods are
untouched.
Adds a regression test proving the cases were previously dropped.
(cherry picked from commit 3a9d305277)
A Zig error set (`const E = error{ A, B };`) parses as a
variable_declaration whose value is an error_set_declaration. That value
node type was not among the recognised forms, so the declaration fell
through the variable_declaration handler and both the error type and its
members were dropped entirely.
An error set is a named enumeration of error values, the direct parallel
of a Zig enum. Recognise error_set_declaration and emit the type node
plus a node and a case_of edge per member identifier, matching the
existing enum-member handling. Struct and union fields are untouched.
Adds a regression test proving the whole declaration was previously
dropped.
(cherry picked from commit 3e7f4a5905)
self.save, a paren-less save and save(x) with no receiver all send to
the current object, but they went through the file-wide name map,
which keeps only the last definition of each name, so they landed on
whichever class the file declares last, as EXTRACTED. In sinatra's
base.rb, Helpers#not_found was wired to Base#error and
AcceptEntry#to_s to MimeTypeEntry#to_str.
Route them through _self_call_target with the in-file inherits walk.
A top-level def and a mixed-in module method are not methods of an
unrelated class, so implicit-self sends to them resolve as before.
(cherry picked from commit 0175b64783)
self.save(), a bare save() (implicit self) and super.ping() in a Swift
method went through the file-wide name map, which keeps only the last
definition of each name, so they landed on whichever class the file
declares last, as EXTRACTED. In Alamofire, DataRequest.init calling
super.init() was recorded as DataRequest.init calling itself, and
URLEncodedFormEncoder's keyed container calls went to the unkeyed one.
Route them through _self_call_target with the in-file inherits walk,
which Swift supports because its inherits edges exist at this point.
Free functions, constructors and calls from an extension of the same
type resolve exactly as before.
(cherry picked from commit a8333ea8a1)
this.save() in one JS/TS class was resolved through the file-wide name
map, which keeps only the last definition of each name, so a class that
calls this.save() was wired to the save() of whichever class the file
declared last, as EXTRACTED. In zod's v3 types.ts every ZodString,
ZodNumber and ZodBigInt check method landed on ZodDate._addCheck.
Route this.m() and super.m() through the self-call lookup the Python
fix added, renamed _self_call_target. It binds to the caller's own
class when that class defines m(). JS/TS extends edges are only emitted
by the later symbol pass, so the base chain is unknown at this point;
anything the own class does not define keeps the plain lookup, which
leaves inherited this.m() edges exactly as they were.
(cherry picked from commit 33184e7b06)
self.m(), cls.m() and super().m() are exempt from the receiver-typed
defer because the receiver is the caller's own instance, but the lookup
they fell through to was the file-wide name map, which keeps only the
last definition of each name. Two classes in one file that both define
m() sent every self-call to whichever was declared last, as EXTRACTED.
On httpx that wires the sync Client's request/send chain into
AsyncClient, and every super().__init__() in _exceptions.py into
RequestNotRead.__init__.
Resolve these against the enclosing class, then its in-file bases
nearest first (super skips the class itself), the same order the ObjC
resolver uses for self. A tie between two bases on one level binds
nothing, and a method of a class outside the chain is never the target.
A module-level callable stored on the instance still binds as before.
(cherry picked from commit 02e1ed4e10)
Add a one-click "Install in VS Code" button (uvx --from graphifyy[mcp]
graphify-mcp, no API key) and a Smithery install command for the MCP
server, plus a Smithery badge linking to the server page as the
ownership backlink Smithery verifies.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
build_merge carries forward the hyperedges of files that were not
re-extracted, but it skipped any carried hyperedge whose id also appeared
in the new chunks, whichever file the new one came from. Hyperedge ids
are chosen per extraction, so two documents can emit the same id: a full
build keeps both, yet re-extracting only one of them silently dropped the
other file's hyperedge.
Match a re-emission on (id, source_file) instead of id alone. The new
side's source_file is normalized the same way the carried side already
is, and an empty source_file counts as missing. A re-emission from the
same file, or a source-less hyperedge re-emitted without one, still
replaces the carried copy. merge_raw_extraction already replaces
hyperedges per source with no id-only check, so the two paths now agree.
Refs #3981
(cherry picked from commit 0c72e04c8a)
Register .vh with the existing Verilog detection and extraction paths,
including incremental and watch coverage.
Refs #3749
AI-Assisted-By: OpenAI Codex
(cherry picked from commit 3b74b8ad3d)
A wikilink such as [[note.en]], [[v1.2 release]] or [[Mr. Smith]] was
dropped: Path("note.en").suffix is ".en", so the target skipped the
extension-less .md completion and was rejected as a non-document link,
although note.en.md exists and is a node. The docstring already promised
that extension-less wikilink targets resolve to sibling .md.
For wikilinks only, a target whose suffix is not a document extension is
now retried as <name>.md through the same sibling-then-vault resolution,
and kept only when that document exists. A literal target that graphify
indexes itself keeps precedence: [[pic.png]] beside pic.png and
pic.png.md still yields no edge. [[missing.en]] and inline/reference
links keep their current behavior.
Assisted-by: assayer harness on Claude Code (https://github.com/Yyunozor/assayer-memory-mcp)
(cherry picked from commit 3d281a674a)
When a class is only referenced in a type annotation (Python) or a generic
argument (C#) earlier in the file, that reference mints a source-less stub
with the class name. A later `Parser()` / `new Widget()` in the same file
found the stub in the local lookup, was recognised as an external stub, and
was dropped: it was neither emitted as a local edge nor queued for
cross-file resolution. Since 0.9.62 these constructor edges were missing.
Queue those calls for cross-file resolution as well. That pass already
refuses source-less targets (#3156), so the stub itself still never becomes
a calls hub.
These calls did not bind to anything before, so a rescued call is marked
and only binds with evidence: an import of the target, or for C# the
namespace / `using` scope via CsharpNameResolver. Without that, a file that
imports `com.lib.Client` or uses `System.Collections.Generic` would bind
`new Client()` / `new List<int>()` to an unrelated same-named class.
For C#, `new X()` also drops `case_of` / `defines` targets from its
candidates, the same exclusion the C# type-definition index uses (#3815),
so it cannot bind to an enum member or property named X.
Fixes#3888
(cherry picked from commit e4a9270f45)
Move _RUST_BUILTIN_TYPES from extractors/base.py next to the Rust extractor,
matching _GO_PREDECLARED_FUNCS and _RUST_TRAIT_METHOD_BLOCKLIST. The shared
_LANGUAGE_BUILTIN_GLOBALS is consulted with no language gate, so a Rust-only
set belongs with the language that reads it; this also keeps base.py's public
surface unchanged.
(cherry picked from commit 2d766780bc)
Rust's standard-library types were absent from _LANGUAGE_BUILTIN_GLOBALS, so
every Option/Vec/String/Result annotation was extracted as a domain type and
resolved to a few canonical nodes with corpus-wide degree (String led a
1,200-file workspace with ~1,700 edges).
Add a Rust-only _RUST_BUILTIN_TYPES set, kept separate so the same names still
resolve as user types in other languages, and skip those names in the Rust
type-reference and call collectors. The filter is definition-aware: a file that
defines its own `struct Result<T>` keeps its references.
A tuple-struct construction (`ClientId(id)`) targets a data definition, not a
callable, so it is now emitted as `references[constructor]` instead of `calls`
in both the in-file and cross-file paths; a real function call still emits
`calls`.
(cherry picked from commit 88cb2a8cd1)
Ids are casefolded, so `fn handle`, `const HANDLE`, `struct HANDLE` or an
`impl` for `HANDLE` share the id of `struct Handle`, and walk() keeps
whichever item comes first. The pre-scan registered the type id
regardless, so a reference placed above the declarations was bound to
that shared id and could end on the function or on the other item, where
v8's extractor left a sourceless stub. For the same reason a `HANDLE`
reference (a `type HANDLE` alias, or an imported name) placed above a
local `struct Handle` was bound to the struct.
Only pre-register a type when every file-level item behind its id is
that same type (`#[cfg]` twins count once), and match it by its exact
name. An `impl` block counts as the type it names, since walk() gives it
the node of its type text; methods keep impl-qualified ids and are not
counted. Otherwise the forward reference keeps its stub. A reference
placed after an item that already owns the id still resolves through
seen_ids, unchanged from v8.
Adds regression cases for a type colliding with a function (also in an
`extern` block), a const, a static, another type, an `impl` for another
type and a type alias, and controls where the id is not shared: another
function name, `#[cfg]` twins, an `impl` for the type itself, a method
named like the type, and a generic type only an `impl` names.
Assisted-by: assayer harness on Claude Code (https://github.com/Yyunozor/assayer-memory-mcp)
(cherry picked from commit 93fa872e2c)
ensure_named_node only resolved names already in seen_ids, so a struct,
enum or trait used above its declaration in the same file got a
sourceless stub:
fn before(s: &Sink) {}
struct Sink { n: u32 }
The corpus-level rewire folds that stub back onto the real struct only
when the name is unique across the corpus. Once another file declares a
`Sink` as well, the reference stays on the stub, and `affected` or
`explain` on the local struct miss that user.
Pre-scan the struct/enum/trait names the file declares, as the Verilog,
SQL and Bash extractors already pre-scan their definitions, and let
ensure_named_node resolve against them, so a use above a declaration
resolves exactly as a use below it already does. Only these three items
are pre-registered: walk() never makes a node of a `type` alias or
`union`, so registering one would leave its references to be dropped by
the final dangling-edge filter. The scan never descends further than
walk(), so every registered id ends up as a node.
Adds regression coverage for a struct, enum and trait used above their
declarations, a type declared in an inline module, the two-file
ambiguous case (cold and warm cache), and the items that must not
resolve a forward reference (type alias, union, function body, const
and static initializers).
Assisted-by: assayer harness on Claude Code (https://github.com/Yyunozor/assayer-memory-mcp)
(cherry picked from commit d73c4adf59)