mirror of
https://github.com/safishamsi/graphify.git
synced 2026-09-29 00:45:58 +00:00
fix(extract): route .rake files to the Ruby extractor + resolution (#1784)
.rake files are plain Ruby (Rake's task DSL is ordinary method calls), but the extension was gated out everywhere, so rake tasks were classified as unsupported, skipped, and their calls invisible. Add `.rake` to all seven `.rb` gates the reporter mapped: - detect.CODE_EXTENSIONS (classification) - extract._DISPATCH (extractor dispatch) - extract._LANG_FAMILY_BY_EXT-adjacent language-name map (.rake -> ruby) - the ruby_member_calls LanguageResolver suffix set - both `.rb`-suffix filters in ruby_resolution.py (raw-call gather + class-def index) - analyze language-stats map - build repo-tag map The extractor already parsed the content; this is purely extension routing. Regression test: a `.rake` task's `Widget.tally` resolves cross-file to the `.rb` definition. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
committed by
safishamsi
co-authored by
Claude Opus 4.8
parent
df74ab4481
commit
5777fec8c1
@@ -4,6 +4,8 @@ Full release notes with details on each version: [GitHub Releases](https://githu
|
||||
|
||||
## 0.9.13 (unreleased)
|
||||
|
||||
- Fix: Ruby `.rake` files are now extracted and participate in Ruby cross-file resolution like `.rb` (#1784, thanks @krishnateja7). `.rake` is plain Ruby but the extension was gated out of seven places (classification, extractor dispatch, the language-name/family maps, the `ruby_member_calls` resolver's suffix set, both `.rb`-suffix filters in `ruby_resolution.py`, and the build repo-tag map), so every rake task was skipped and its calls were invisible. All seven now include `.rake`; `Widget.tally` from a `.rake` task resolves to its `.rb` definition.
|
||||
|
||||
- Fix: cross-module references to a function now resolve to its definition instead of dangling on a name-only stub (#1781, thanks @EmilNyg). `_rewire_unique_stub_nodes` gated merge targets through `_is_type_like_definition`, which rejects any label ending in `)` — so function/method defs could never absorb their reference stubs, and "who references this function" returned nothing on the definition node while a sourceless stub held all the edges. Top-level function defs are now eligible rewire targets when the label match is globally unique, gated by a language-family match with the referrers (a Python `get_db` reference can't bind to a unique Go `get_db()`) and excluding stubs used as a supertype (`inherits`/`implements`/`extends` — you don't inherit from a function). Types are unchanged.
|
||||
|
||||
- Fix: live PostgreSQL introspection (`--postgres`) now emits foreign-key `references` edges under a read-only role (#1746, thanks @rithyKabir). The FK query read `information_schema.referential_constraints`, which is privilege-filtered — a role with only SELECT sees zero FK rows while tables/views/routines still appear, so every `references` edge silently vanished. It now reads the world-readable `pg_catalog.pg_constraint` (keyed by oid, which also fixes same-named constraints on sibling tables cross-matching in the old name-based joins), preserving composite-FK column order via `UNNEST ... WITH ORDINALITY`.
|
||||
|
||||
+1
-1
@@ -28,7 +28,7 @@ _LANG_FAMILY: dict[str, str] = {
|
||||
**{e: "rust" for e in (".rs",)},
|
||||
**{e: "jvm" for e in (".java", ".kt", ".kts", ".scala")},
|
||||
**{e: "c" for e in (".c", ".h", ".cpp", ".cc", ".cxx", ".hpp")},
|
||||
**{e: "ruby" for e in (".rb",)},
|
||||
**{e: "ruby" for e in (".rb", ".rake")},
|
||||
**{e: "swift" for e in (".swift",)},
|
||||
**{e: "dotnet" for e in (".cs",)},
|
||||
**{e: "php" for e in (".php",)},
|
||||
|
||||
+1
-1
@@ -49,7 +49,7 @@ _EDGE_LANG_FAMILY: dict[str, str] = {
|
||||
".c": "c", ".h": "c", ".cc": "c", ".cpp": "c", ".hpp": "c",
|
||||
".cxx": "c", ".hh": "c", ".hxx": "c",
|
||||
".cu": "c", ".cuh": "c", ".metal": "c", ".m": "c", ".mm": "c",
|
||||
".rb": "rb", ".php": "php", ".cs": "cs", ".swift": "swift", ".lua": "lua",
|
||||
".rb": "rb", ".rake": "rb", ".php": "php", ".cs": "cs", ".swift": "swift", ".lua": "lua",
|
||||
}
|
||||
|
||||
|
||||
|
||||
+1
-1
@@ -27,7 +27,7 @@ class FileType(str, Enum):
|
||||
|
||||
_MANIFEST_PATH = str(out_path("manifest.json"))
|
||||
|
||||
CODE_EXTENSIONS = {'.py', '.ts', '.tsx', '.mts', '.cts', '.js', '.jsx', '.mjs', '.ejs', '.ets', '.go', '.rs', '.java', '.groovy', '.gradle', '.cpp', '.cc', '.cxx', '.c', '.h', '.hpp', '.cu', '.cuh', '.metal', '.rb', '.swift', '.kt', '.kts', '.cs', '.scala', '.php', '.lua', '.luau', '.toc', '.zig', '.ps1', '.psm1', '.psd1', '.ex', '.exs', '.m', '.mm', '.jl', '.vue', '.svelte', '.astro', '.dart', '.v', '.sv', '.svh', '.sql', '.r', '.f', '.F', '.f90', '.F90', '.f95', '.F95', '.f03', '.F03', '.f08', '.F08', '.pas', '.pp', '.dpr', '.dpk', '.lpr', '.inc', '.dfm', '.lfm', '.lpk', '.sh', '.bash', '.json', '.tf', '.tfvars', '.hcl', '.dm', '.dme', '.dmi', '.dmm', '.dmf', '.sln', '.slnx', '.csproj', '.fsproj', '.vbproj', '.xaml', '.razor', '.cshtml', '.cls', '.trigger'}
|
||||
CODE_EXTENSIONS = {'.py', '.ts', '.tsx', '.mts', '.cts', '.js', '.jsx', '.mjs', '.ejs', '.ets', '.go', '.rs', '.java', '.groovy', '.gradle', '.cpp', '.cc', '.cxx', '.c', '.h', '.hpp', '.cu', '.cuh', '.metal', '.rb', '.rake', '.swift', '.kt', '.kts', '.cs', '.scala', '.php', '.lua', '.luau', '.toc', '.zig', '.ps1', '.psm1', '.psd1', '.ex', '.exs', '.m', '.mm', '.jl', '.vue', '.svelte', '.astro', '.dart', '.v', '.sv', '.svh', '.sql', '.r', '.f', '.F', '.f90', '.F90', '.f95', '.F95', '.f03', '.F03', '.f08', '.F08', '.pas', '.pp', '.dpr', '.dpk', '.lpr', '.inc', '.dfm', '.lfm', '.lpk', '.sh', '.bash', '.json', '.tf', '.tfvars', '.hcl', '.dm', '.dme', '.dmi', '.dmm', '.dmf', '.sln', '.slnx', '.csproj', '.fsproj', '.vbproj', '.xaml', '.razor', '.cshtml', '.cls', '.trigger'}
|
||||
DOC_EXTENSIONS = {'.md', '.mdx', '.qmd', '.txt', '.rst', '.html', '.yaml', '.yml'}
|
||||
PAPER_EXTENSIONS = {'.pdf'}
|
||||
IMAGE_EXTENSIONS = {'.png', '.jpg', '.jpeg', '.gif', '.webp', '.svg'}
|
||||
|
||||
+3
-3
@@ -1778,7 +1778,7 @@ _LANG_FAMILY_BY_EXT: dict[str, str] = {
|
||||
".py": "python",
|
||||
".go": "go",
|
||||
".rs": "rust",
|
||||
".rb": "ruby",
|
||||
".rb": "ruby", ".rake": "ruby",
|
||||
".php": "php", ".phtml": "php", ".php3": "php", ".php4": "php",
|
||||
".php5": "php", ".php7": "php", ".phps": "php",
|
||||
".cs": "dotnet", ".razor": "dotnet", ".cshtml": "dotnet", ".xaml": "dotnet",
|
||||
@@ -2757,7 +2757,7 @@ register_language_resolver(
|
||||
# Ruby type-aware member-call resolution (Class.new + typed var.method). Lives in
|
||||
# graphify.ruby_resolution; registered here as a second consumer of the framework.
|
||||
register_language_resolver(
|
||||
LanguageResolver("ruby_member_calls", frozenset({".rb"}), resolve_ruby_member_calls)
|
||||
LanguageResolver("ruby_member_calls", frozenset({".rb", ".rake"}), resolve_ruby_member_calls)
|
||||
)
|
||||
register_language_resolver(
|
||||
LanguageResolver("typescript_member_calls", frozenset({".ts", ".tsx", ".mts", ".cts", ".js", ".jsx"}), _resolve_typescript_member_calls)
|
||||
@@ -3813,7 +3813,7 @@ _DISPATCH: dict[str, Any] = {
|
||||
".cu": extract_cpp,
|
||||
".cuh": extract_cpp,
|
||||
".metal": extract_cpp,
|
||||
".rb": extract_ruby,
|
||||
".rb": extract_ruby, ".rake": extract_ruby,
|
||||
".cs": extract_csharp,
|
||||
".kt": extract_kotlin,
|
||||
".kts": extract_kotlin,
|
||||
|
||||
@@ -44,7 +44,7 @@ def _ruby_raw_calls(per_file: list[dict]) -> list[dict]:
|
||||
if not isinstance(rc, dict):
|
||||
continue
|
||||
sf = str(rc.get("source_file", ""))
|
||||
if sf.endswith(".rb"):
|
||||
if sf.endswith((".rb", ".rake")):
|
||||
calls.append(rc)
|
||||
return calls
|
||||
|
||||
@@ -83,7 +83,7 @@ def resolve_ruby_member_calls(
|
||||
for n in all_nodes:
|
||||
nid = n.get("id")
|
||||
sf = str(n.get("source_file", ""))
|
||||
if nid and sf.endswith(".rb") and _BARE_CONST_RE.match(str(n.get("label", ""))):
|
||||
if nid and sf.endswith((".rb", ".rake")) and _BARE_CONST_RE.match(str(n.get("label", ""))):
|
||||
class_def_nids.setdefault(_key(n.get("label", "")), []).append(str(nid))
|
||||
for k in list(class_def_nids):
|
||||
class_def_nids[k] = sorted(set(class_def_nids[k]))
|
||||
|
||||
@@ -348,3 +348,21 @@ def test_mixin_is_not_emitted_as_calls_edge(tmp_path: Path) -> None:
|
||||
for e in g["edges"] if e.get("relation") == "calls"}
|
||||
assert ("K", "C") not in calls
|
||||
assert ("K", "C") in _mixes_in(g)
|
||||
|
||||
|
||||
def test_rake_files_extract_and_resolve_like_rb(tmp_path):
|
||||
"""#1784: `.rake` files are plain Ruby and must route to the Ruby extractor
|
||||
and participate in Ruby cross-file resolution exactly like `.rb`."""
|
||||
rake = _write(tmp_path, "ops.rake",
|
||||
"class RakeHelper\n def self.run\n Widget.tally\n end\nend\n")
|
||||
rb = _write(tmp_path, "widget.rb",
|
||||
"class Widget\n def self.tally\n 42\n end\nend\n")
|
||||
result = extract([rake, rb], cache_root=tmp_path / ".cache")
|
||||
label = {n["id"]: n.get("label") for n in result["nodes"]}
|
||||
labels = set(label.values())
|
||||
# the .rake file's symbols are extracted
|
||||
assert "RakeHelper" in labels and ".run()" in labels
|
||||
# and the cross-file member call resolves .rake -> .rb
|
||||
calls = {(label.get(e["source"]), label.get(e["target"]))
|
||||
for e in result["edges"] if e["relation"] == "calls"}
|
||||
assert (".run()", ".tally()") in calls
|
||||
|
||||
Reference in New Issue
Block a user