Skip to content
Open
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
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ Full release notes with details on each version: [GitHub Releases](https://githu

## 0.9.32 (unreleased)

- Feat: R roxygen blocks and S3 dispatch are extracted. `@seealso` and `@inheritParams` name functions the documented one never calls, and `@template` names a `man-roxygen/` file nothing sources — neither is reachable from the AST, and both become `references` edges when the corpus defines the target exactly once. `@export` marks the public API on the node and `@family` is recorded. S3 methods link to their generic: a dotted name is treated as a method only where the corpus evidences the split (a `UseMethod` generic, or a site assigning that class), so `is.null` and `compute.stuff` fabricate nothing; when several sites assign one class, R's `new_<class>` constructor convention breaks the tie. Non-call records carry `ref_name` rather than `callee`, because the shared cross-file pass turns any `callee` into a `calls` edge by name — the trap Ruby's mixin markers hit in #1668 — which had `@seealso` shipping as a phantom call and blocking the real `references` edge as a duplicate.
- Feat: R (`.R`/`.r`, and `Rscript` shebangs) is extracted with tree-sitter instead of being counted as code and silently dropped — it was the language the #1689 no-AST-extractor warning named as its example. R has no named-function syntax (every definition is an assignment whose RHS is an anonymous `function_definition`), so this is a bespoke extractor rather than a `LanguageConfig`; it covers `<-`/`=`/`<<-`/right-assign bindings, `\(x)` lambdas, nested definitions, `library`/`require`/`requireNamespace` and `pkg::fn` imports, and `source()`. Calls resolve corpus-wide in a registered `LanguageResolver` rather than per file: R has one shared namespace and no name-binding import, so `paste0(x)` and a sibling file's `compute(x)` are indistinguishable within a file. Anything the corpus does not define is dropped, which keeps base R out of the graph with no hardcoded list of ~1,300 base names and emits no dangling edges. The r-lib grammar has no standalone PyPI wheel, so it comes from `tree-sitter-language-pack` under a new optional `[r]` extra.
- Fix: incremental extraction and `_rebuild_code` no longer drop a file's other tier (#2333, #2334, #2336). Node/edge ownership was keyed on `source_file` alone, so a semantic re-extract deleted a doc's AST headings and a full rebuild deleted document AST nodes. Merge is now tier-aware (an AST re-extract replaces only AST nodes and keeps the semantic layer, and vice versa), the `_origin` provenance marker is backfilled on load so old graphs self-heal, and the full-rebuild drop is scoped to sources actually regenerated.
- Fix: `graphify update` preserves the graph's `directed` flag instead of rebuilding it undirected (#2342, thanks @Rishet11), so God-node / path ranking keeps its direction on both the clustered and `--no-cluster` rebuild paths.
- Fix: a numeric or otherwise non-string node id from an LLM fragment no longer aborts the build with a TypeError (#2326, thanks @Rishet11); ids are coerced consistently across nodes, edges, and hyperedges.
Expand Down
22 changes: 20 additions & 2 deletions graphify/extract.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
)
from .ruby_resolution import resolve_ruby_member_calls
from .pascal_resolution import resolve_pascal_inherited_calls
from .r_resolution import resolve_r_calls

# --- migrated to graphify/extractors/ (see graphify/extractors/MIGRATION.md) ---
from graphify.extractors.base import ( # noqa: F401
Expand All @@ -48,6 +49,7 @@
from graphify.extractors.markdown import extract_markdown # noqa: F401
from graphify.extractors.pascal_forms import extract_delphi_form, extract_lazarus_form # noqa: F401
from graphify.extractors.powershell import extract_powershell, extract_powershell_manifest # noqa: F401
from graphify.extractors.r import extract_r # noqa: F401
from graphify.extractors.razor import extract_razor # noqa: F401
from graphify.extractors.rust import extract_rust # noqa: F401
from graphify.extractors.sln import extract_sln # noqa: F401
Expand Down Expand Up @@ -3111,6 +3113,14 @@ def _key(label: str) -> str:
)
)

# R resolves every call at runtime against one shared namespace and has no import
# statement binding a name to a file, so the per-file extractor cannot tell a base
# R call from a sibling-file one. Lives in graphify.r_resolution; registered here
# as a consumer of the framework, same as the Ruby and Pascal resolvers above.
register_language_resolver(
LanguageResolver("r_calls", frozenset({".r", ".R"}), resolve_r_calls)
)


# Inline markdown link: [text](target "optional title"). The negative lookbehind
# excludes images (![alt](src)). The target stops at whitespace/closing paren so
Expand Down Expand Up @@ -4163,6 +4173,11 @@ def add_existing_edge(edge: dict) -> None:
".m": extract_objc,
".mm": extract_objc,
".jl": extract_julia,
# Both cases are listed, as for Fortran below: R sources are conventionally
# `.R`, and _DISPATCH.keys() is read as the supported-suffix set (a case-fold
# only at _get_extractor lookup would leave `.R` out of it).
".r": extract_r,
".R": extract_r,
".f": extract_fortran,
".F": extract_fortran,
".f90": extract_fortran,
Expand Down Expand Up @@ -4223,6 +4238,8 @@ def add_existing_edge(edge: dict) -> None:
# rather than falling back like Pascal does. Used by the #1745 warning in
# extract() to tell the user which extra restores the language.
_EXTRA_FOR_EXTENSION = {
".r": "r",
".R": "r",
".sql": "sql",
".tf": "terraform",
".tfvars": "terraform",
Expand All @@ -4237,7 +4254,7 @@ def add_existing_edge(edge: dict) -> None:
# routes them to the CODE path via _shebang_interpreter; _get_extractor must
# honor the same signal or these files are classified as code and then silently
# dropped by extraction. Only interpreters with a real extractor are mapped —
# detect's wider set (perl, fish, tcsh, Rscript) stays unmapped and skipped.
# detect's wider set (perl, fish, tcsh) stays unmapped and skipped.
_SHEBANG_DISPATCH: dict[str, Any] = {
"python": extract_python,
"python2": extract_python,
Expand All @@ -4253,6 +4270,7 @@ def add_existing_edge(edge: dict) -> None:
"lua": extract_lua,
"php": extract_php,
"julia": extract_julia,
"Rscript": extract_r,
}


Expand Down Expand Up @@ -4711,7 +4729,7 @@ def extract(
)

# #1689: a file counted as code (extension in CODE_EXTENSIONS) but with no AST
# extractor wired up (e.g. .r/.R — there is no tree-sitter-r dispatch) silently
# extractor wired up (e.g. .ejs/.ets — there is no dispatch for either) silently
# contributes zero nodes. The #1666 warning above deliberately skips these (it
# only fires when an extractor exists), so surface them explicitly, grouped by
# extension, rather than reporting success as if the language were mapped.
Expand Down
2 changes: 2 additions & 0 deletions graphify/extractors/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
from graphify.extractors.pascal import extract_pascal
from graphify.extractors.pascal_forms import extract_delphi_form, extract_lazarus_form
from graphify.extractors.powershell import extract_powershell, extract_powershell_manifest
from graphify.extractors.r import extract_r
from graphify.extractors.razor import extract_razor
from graphify.extractors.rust import extract_rust
from graphify.extractors.sln import extract_sln
Expand Down Expand Up @@ -54,6 +55,7 @@
"pascal": extract_pascal,
"powershell": extract_powershell,
"powershell_manifest": extract_powershell_manifest,
"r": extract_r,
"razor": extract_razor,
"rust": extract_rust,
"sln": extract_sln,
Expand Down
Loading