Two gaps in the PowerShell extractor dropped edges from real-world
scripts:
- Dot-source only read the path when tree-sitter parsed it as
command_name. `. "$PSScriptRoot\lib.ps1"` and `. '.\lib.ps1'` parse as
string_literal, and unquoted `. $PSScriptRoot\lib.ps1` as
path_command_name, so no imports_from edge was emitted. Accept all
three and strip surrounding quotes; the existing basename logic
already handles the rest.
- Calls were collected only from function/method bodies, so commands at
script scope (the main body of most .ps1 scripts) produced no calls
edge or raw_call. Walk the file root too, with the file node as the
caller. walk_calls stops at function_statement/class_statement, so
function bodies are not walked twice (covered by a control test).
`. (Join-Path $PSScriptRoot 'lib.ps1')` stays unresolved: the path is
only known at runtime.
AST cache schema 8 -> 9 so cached results for unchanged .ps1 files are
re-extracted.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013LcXGsdFdpLjYVHemLogkE
(cherry picked from commit 69e20c70e3)
The functor branch picked the type from the typed expression's identifier and
parametrized-type children. A module-qualified type (`Mod.Box`) is a
`field_expression`, so `(b::Mod.Box)(x)` fell back to the variable `b`, and
`(::Mod.Box)(x)` / `(b::Mod.Box{T})(x)` were dropped. Read the last child (the
type) through a new `_functor_type_name`, which unwraps parametric and
qualified names and returns None for anything else.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
`(b::Box{T})(x)` puts the type in a `parametrized_type_expression`, so taking
the last identifier in the typed expression picked the variable `b` and
emitted a bogus `b#call` node, split from `Box#call`. Read the last operand
through `_type_name_of`, which already unwraps parametric heads.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Unwrap `::RetType` (alongside `where`) around a definition's call head, so
`function f(x)::Float64` is no longer dropped.
- Name method definitions via `_def_name`: `Base.show`, `Base.:+`, bare
operators, and functors `(c::Circle)(x)`. The id uses the bare name so all
methods of a generic function share one node; the label keeps the qualifier.
- Emit `calls` edges for broadcast calls (`f.(xs)`) and pipes (`x |> f`, `.|>`).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
(cherry picked from commit cf3648e762)
A named generic interface (`interface area; module procedure area_circle,
area_square; end interface`) names a callable that dispatches to its
specific procedures by argument type. The interface block was never turned
into a node, so a call to the generic name dangled — a Fortran function
call_expression only resolves to an in-file node — and the dispatch set
was invisible, even though the specific procedures were extracted. Generic
interfaces (and operator overload sets) are routine in scientific Fortran.
Emit a node for the generic name, link it to the procedures it groups with
`dispatches_to` edges (resolved after the walk, like type-bound
procedures, so a procedure from another module becomes a collapsible
sourceless stub), and the existing call resolution then binds `area(x)`
to it. Unnamed abstract/explicit interfaces keep recursing so their
procedure declarations are still extracted.
(cherry picked from commit ccfb4018fb)
My #4141 fix used a one-off calls/generic_dispatch relation; PR #4142
(@rajatnagda45) implements the same fix with the codebase-standard
dispatches_to relation and earns author-preserving credit. Reverting the
Fortran hunk here so #4142 can be cherry-picked; the Julia fixes from the
same original commit are unaffected.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- Julia: a `where` clause wraps the call head in a `where_expression` and a
parametric type name sits inside a `parametrized_type_expression`, so the
name-lookup helpers missed both and dropped the method/type entirely. Unwrap
the wrapper nodes in `_func_name_from_signature`, the short-function assignment
branch, and `_type_head_names`. Declared type parameters (`value::T`) no longer
leak as phantom field-type reference nodes (#4126, #4128).
- Fortran: a named generic `interface` had no walk branch, so the generic name
was dropped and a call to it dangled. Emit a node for the generic name and
defer-resolve a `generic_dispatch` edge to each listed specific procedure,
mirroring the type-bound-procedure pattern; unnamed/operator interfaces are
untouched and their bodies still extracted (#4141).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
File-scope begin/process/end/dynamicparam/clean parse as pseudo-commands
(command_name 'begin'), not named_block, so the command branch returned before
the child walk and any function/class defined inside was dropped. Descend into
those five block keywords' children; ids stay name-based and stable.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Refuse a read-only destination in _atomic_replace on Windows (nt-only guard;
os.replace there raises PermissionError and the fallback would silently clobber
it), plus test-only guards: utf-8 encoding kwargs, posix-shape path assertions,
FIFO/AF_UNIX/symlink skips, and a replace-retry for AV/indexer locks. No POSIX
behavior change.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
CommonMark writes a link destination with a space either in angle
brackets ([x](<My Note.md>)) or percent-encoded ([x](My%20Note.md)).
The inline and reference-definition regexes stopped the capture at the
first space inside <...>, and the destination was never decoded, so
both forms kept an id derived from a nonexistent path and the
references edge dropped at build.
Capture the whole bracketed destination in both regexes; an unclosed <
falls back to the bare form, so it does not swallow the links after it.
Percent-decode inline and reference destinations, keep a file whose name
literally contains the escape (with or without the .md completion), and
re-run the external-link guard on the decoded form. Wikilinks stay
verbatim.
Assisted-by: assayer harness on Claude Code (https://github.com/Yyunozor/assayer-memory-mcp)
(cherry picked from commit 49ba8aafc0)
Replaces the name-only member-call resolver with receiver-proven binding:
$this.M() binds to the enclosing class only when its name is unique, and
[Type]::M() only to a single unambiguous local class; value/cast/dynamic/
duplicate/external receivers fall through to raw_calls. Fixes the same-name-
method caveat from the 0.9.77 member-call work (#3992).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
extract_dmf minted each element id from its window's node id, which
already embeds the file stem. The stem is absolute at extraction time and
extract()'s id remap only rewrites the leading copy, so the nested copy
kept the checkout path and the OS username inside every element id.
Use the window name instead. Element ids no longer depend on where the
repo is checked out.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01B8gwEpjajiWHVW3c9smuKf
(cherry picked from commit 3d9021fab7)
The call walker only visited `command` nodes, so every method invocation
was dropped from the call graph. A method call parses as an
invokation_expression (callee in the member_name child, args in
argument_list); a bare property read ($this.Name) is a separate
member_access node and correctly produces no call.
Handle invokation_expression and resolve the member name against a
method-only label map so a member call binds to a method, never to a free
function that merely shares the name (fail-closed on unknown receivers).
Fixes#3992
(cherry picked from commit 1e6a9029e4)
#4027 taught the Elixir extractor defmacro, defmacrop, defguard and
defguardp, but the regression test only exercised the first three. Add a
defguardp clause to the fixture and assert its node is extracted, so
dropping defguardp from the extractor's keyword list fails this test.
Closes#4076
Co-Authored-By: Claude <noreply@anthropic.com>
(cherry picked from commit 345b66b01f)
#4050 emits a node + case_of edge per `union(enum)` variant but
deliberately does not recurse into a variant's nested-struct payload.
Extend the tagged-union test to pin that: `rectangle: struct { w, h }`
is minted as a variant, while its `w`/`h` fields are not, and
`rectangle` owns no case_of edges of its own.
Fixes#4074
Co-Authored-By: Claude <noreply@anthropic.com>
(cherry picked from commit 994bb2e534)
A PowerShell enum member is a discriminant case, not a declared field, so it
should hang off its enum with a case_of edge like every other language that
has enums (Java #1719, C#, Swift, Rust, VB.NET). PowerShell was routing enum
members through the same contains path used for class members.
The relation is not cosmetic: case_of targets are excluded from constructor
binding, so an enum member named like a type could previously be mistaken for
one.
Update the existing enum test to expect case_of and add a focused regression
test asserting enum members get case_of (and not contains) while the file
still contains the enum type via contains.
(cherry picked from commit 011dda9b0f)
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)
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)
Inside a class body a `virtual double area() = 0;` (pure virtual), a
`virtual void draw();` and a plain prototype `int run();` parse as a
`field_declaration` whose declarator is a `function_declarator`. The field
branch recognised that shape only to skip emitting a type reference — it still
emitted a node for every declarator as a DATA MEMBER (a bare `area` label with
a `defines`/field edge).
So a pure-virtual interface class had no method nodes, only phantom fields; a
header-declared method and its out-of-line/overriding definition did not share
one node (`area` field vs `.area()` method); and a call to such a method
resolved to a field-shaped node.
Fix: in the field-declaration branch, emit a function_declarator with the same
shape a defined method uses — a `.name()` label, a `method` edge, and callable
— so the declaration and its definition collapse to one method node (parity
with the Java/TypeScript/Scala abstract-method contract). A genuine data member
still gets its `defines` field edge.
This corrects the label of every header-declared method, so a few existing
cross-file tests that asserted the old bare `bar`/`run` field label now assert
`.bar()`/`.run()`; the resolved call edges (source, target, confidence) are
unchanged, and the decl/def merge still produces exactly one node carrying the
definition site.
Added tests/test_cpp_method_declarations.py.
(cherry picked from commit 0038af9394)
`enum_definition` was missing from the Scala class_types, so a Scala 3
`enum` produced no nodes at all: the enum type, its cases, and any methods
declared in its body all vanished from the graph, and references to the
enum resolved to nothing.
Add `enum_definition` to the Scala class_types and `enum_body` to the
body fallbacks so the type gets a node and its body is walked, and add
`_scala_extra_walk` to emit a node plus a `case_of` edge per case. Cases
nest under an `enum_case_definitions` wrapper as `simple_enum_case`
(`case Red`) or `full_enum_case` (`case Add(x: Int)`); both carry the case
name as an `identifier` child. This is the Scala parity of Java's
enum_constant (#1719), Kotlin's enum_entry (#1738), and Swift.
Case constructor parameter types and per-case `extends` arguments are not
linked; the case node and its case_of edge are the fix for the dropped
members.
(cherry picked from commit 175af52419)
The enum body walk only emitted an enum's methods; its members — the
`container_field` nodes (`red`, `north = 0`) — were dropped, so an
`enum { red, green, blue }` produced a node for the enum with no members at
all, leaving it a leaf.
Emit a node plus a `case_of` edge per enum member. The handler is gated on
the `enum_declaration` parent so struct and union fields, which share the
`container_field` node shape, stay untouched. This brings Zig to parity with
the other languages that have enums: Java's enum_constant (#1719), Swift,
Scala, and Rust.
(cherry picked from commit 1e44311e22)
@code is the Blazor (.razor) spelling of an embedded C# member block;
@functions is the equivalent Razor Pages / MVC (.cshtml) spelling. Both
compile to class members, and this extractor serves both file types, but
only @code was matched. A .cshtml page that declared its methods inside an
@functions block lost every one of them.
Match @functions alongside @code in the block regex; the existing
brace-balanced body scan and method extraction are reused unchanged.
(cherry picked from commit e3693430b3)
enum_declaration was missing from the Groovy config's class_types, so a Groovy
enum and every constant it declares were dropped from the graph — a consumer
could not see which value a branch selects. tree-sitter-groovy exposes the same
enum_constant shape (a name field, optional class_body) that Java uses, so the
enum type now joins class_types and the existing Java enum-constant handler is
reused to emit a node per member with a case_of edge.
Added a regression test asserting the enum type and its constants are present,
attached to the enum rather than the file.
(cherry picked from commit 2c386de21a)
Two core Julia constructs were dropped by the extractor:
- macro name(...) ... end produced no node, even though macros are first-class,
heavily used definitions. The grammar gives a macro the same signature shape
as a function, so it now mints a definition (labelled @name) and its body is
walked for calls -- with the signature skipped so the macro does not call
itself.
- @enum Name a b c (and the @enum Name begin ... end block form) is a
macrocall_expression that was ignored entirely. The first identifier in the
argument list is the enum type; the rest, including members inside a begin/end
block, are contained by it. Other macro calls still fall through so
definitions nested in e.g. an @testset block are walked.
Added tests covering macro extraction with body-call attribution (no self-loop)
and both @enum forms with members contained by their type.
(cherry picked from commit 87edb5a278)
Polish on #3848: remove the unused mid-file tree_sitter imports (F401)
and add a negative test asserting a bare constructor param without
val/var is not emitted as a field property.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
A derived type's contains section binds procedures as the type's methods
(procedure :: area => circle_area, or plain procedure :: scale), but the
derived_type_definition branch only minted the type node and ignored the
binding list entirely. The type ended up in the graph with no connection to
its own methods, and calls dispatched through the type were unrepresented.
Collect the bindings while walking the type and, after the whole tree is
walked (so the module's internal procedures already have nodes), add a method
edge from the type to each implementing procedure. A binding to a procedure
from another module resolves to a sourceless stub via ensure_named_node, the
same treatment parameter and return types already get, so the corpus rewire
can collapse it and no dangling edge is produced.
(cherry picked from commit 75f8fff533)
defprotocol and defimpl were not recognized as definition keywords, so the
walker fell through to generic recursion with no parent module. The protocol
and implementation nodes were never created, and their functions were attached
to the file node instead of their container (two orphaned size() nodes hanging
off the file rather than off the protocol and its List implementation).
Handle both like module-like containers: mint the protocol/impl node, contain
it in its enclosing scope, and walk the do-block so each def becomes a method
of the protocol or implementation. The implementation also gets an implements
edge to its protocol; a cross-file protocol target is filtered by the existing
dangling-edge guard rather than left hanging.
(cherry picked from commit 5f4e979693)
A module instantiation minted its target id with a bare, unscoped
_make_id(name), while a module definition uses _make_id(stem, name). The
two never matched, so instantiating a module defined in the same file:
module counter(...); endmodule
module top;
counter u1(...);
endmodule
split `counter` into a real definition node and a second, sourced phantom
that carried the `instantiates` edge. `top`'s dependency on the module you
actually defined was invisible, and because the phantom was sourced it also
baked this file's path into the id and blocked the corpus-level rewire that
would otherwise collapse it (the #1402 duplicate shape the other
extractors guard against).
Pre-scan module names so a same-file instantiation (Verilog allows
forward/any-order instantiation) resolves to the definition id. A module
defined in another file becomes a SOURCELESS stub instead, so the
corpus-level rewire collapses it onto the real definition rather than a
per-file duplicate — mirroring the Go extractor's ensure_named_node.
Adds regression coverage for both the local-definition link and the
cross-file sourceless stub.
(cherry picked from commit bca8bbac72)
Covers a same name PHP/JS function pair in one file: exactly one
surviving node, the JS call to the colliding name absent as a calls
edge entirely rather than misattached, and the unrelated JS symbol's
own contains edge from the shared file node still present.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
(cherry picked from commit f59ce06c8c)
Covers the issue's exact reproduction (two functions plus the call
edge between them), that line numbers survive the masking pass, that
a script tag with only a src attribute contributes nothing without
crashing, and that a plain PHP file with no script tag is unaffected.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
(cherry picked from commit 2d408de529)
Companion to #3243: extend definition_file portability to the paths build_from_json does not
cover — the watch/incremental relativize + cache-root rebase and the direct extract() return
normalization — via a shared _PORTABLE_PATH_KEYS list so source_file and definition_file stay
in lockstep. Together with #3243 this closes all four persistence paths (graph.json, AST
cache, watch, direct extract).
A qualified Kotlin supertype (`class Foo : com.example.Base()`) resolved to the first
identifier (`com`), collapsing distinct bases onto a bogus node. Take the last segment of the
user type (handling generics like `Container<String>` -> `Container`), so the edge targets
the real base; simple and delegated supertypes are unaffected.
A qualified Scala base (`extends pkg.Base with other.Trait`) parses as a
stable_type_identifier (or generic_type wrapping one), which the heritage handler did not
match, so the edge was dropped or targeted the full qualified string. Add a helper that
takes the tail type name across all three shapes, emitting inherits for the first parent and
mixes_in for the rest, with no package/generic-arg false edges.
Only class_declaration was treated as a class type, so PHP interfaces, enums, and traits
produced no node and no heritage edges. Add them as class-like containers (and walk the
enum body) so their extends/implements/use heritage is captured as inherits/implements/
mixes_in edges to the right targets.
Add a Robot Framework extractor using the official robot.api parser (pure-Python, optional
[robot] extra), emitting suite/test/keyword nodes plus keyword-call, resource-import, and
library edges. Keyword resolution is case/space/underscore-insensitive (RF's real semantics)
and handles BDD prefixes and ${CURDIR}/${EXECDIR} path resolution; degrades to an actionable
error when robotframework is absent.
A guarded function head (def foo(x) when is_integer(x), do: ...) nests the head call under a
binary_operator, so a def whose only clause carries a guard was not extracted. Unwrap the
guard to the head call (the head is always the first child, so the guard predicate is never
selected) and emit the real function name; multi-clause, multi-condition guards, and defp
are handled.
The Common Lisp extractor minted its id prefix from the bare path.stem instead of the
canonical _file_stem used by every other extractor, so two same-basename .lisp files in
different directories collided (one file's nodes vanished on merge) and nested .lisp files
tripped a spurious legacy-id nudge. Use _make_id(_file_stem(path)) to match the shared
convention; cross-file rewire is unaffected (it matches on label, not id).
normalize_id strips !/?/= from Ruby method names, so save and save! (or foo and foo=) minted
the same node id and the second was dropped as a seen_ids collision. Add a Ruby symbol
sanitizer (!->_bang, ?->_pred, =->_eq) applied to the id only (the label keeps the raw
spelling), and rekey method_index on the raw name so member-call resolution still matches.
Ids stay stable across incremental re-extraction.
When a C/C++/ObjC declaration (header prototype) and its definition (implementation body)
merge into one node, the survivor kept the declaration's site, so the node pointed at the
prototype instead of the body. Record the definition's file/location on the survivor and
surface it as a Defined in: line; the pick is deterministic regardless of file order, and a
declaration with no definition keeps its own site.
A PowerShell enum definition and its members were not extracted, so a [Color] type
reference resolved to a sourceless phantom stub instead of the real enum. Emit the enum as
a real sourced node with its members as contained children (mirroring class_statement), so
references resolve to it; no builtins are fabricated.