From 652ba42e61b63689aeac660a214d164fcfe10516 Mon Sep 17 00:00:00 2001 From: jiangyq9 Date: Sat, 27 Jun 2026 10:07:11 +0100 Subject: [PATCH] feat(extract): index Metal (.metal) shader files (#1480) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Metal Shading Language is C++14, so .metal files are classified as code and routed through the existing C++ extractor, mirroring the CUDA .cu/.cuh reuse. Also adds .cu/.cuh/.metal to build.py's _LANG_FAMILY map (they were all missing), so the cross-language phantom-`calls`-edge filter treats them as C++. README language table updated. Ported from PR #1480 by @jiangyq9. This supersedes the earlier #1450 by @GoodOlClint (same .metal -> C++ approach and fixture) — credit to @GoodOlClint for the original; #1480 additionally closes the CUDA family-map gap and updates the docs, and merges clean against current v8. Co-Authored-By: Claude Opus 4.8 (1M context) --- README.md | 2 +- graphify/build.py | 1 + graphify/detect.py | 2 +- graphify/extract.py | 1 + tests/fixtures/sample.metal | 21 +++++++++++++++++++++ tests/test_languages.py | 21 +++++++++++++++++++++ 6 files changed, 46 insertions(+), 2 deletions(-) create mode 100644 tests/fixtures/sample.metal diff --git a/README.md b/README.md index 85fb7383..89a32616 100644 --- a/README.md +++ b/README.md @@ -242,7 +242,7 @@ To remove graphify from all platforms at once: `graphify uninstall` (add `--purg | Type | Extensions | |------|-----------| -| Code (36 tree-sitter grammars) | `.py .ts .js .jsx .tsx .mjs .go .rs .java .c .cpp .h .hpp .cu .cuh .rb .cs .kt .scala .php .swift .lua .luau .zig .ps1 .psm1 .ex .exs .m .mm .jl .vue .svelte .astro .groovy .gradle .dart .v .sv .svh .sql .f .f90 .f95 .f03 .f08 .pas .pp .dpr .dpk .lpr .inc .dfm .lfm .lpk .sh .bash .json .dm .dme .dmi .dmm .dmf .sln .slnx .csproj .fsproj .vbproj .xaml .razor .cshtml` (`.dm`/`.dme` requires `uv tool install graphifyy[dm]`; CUDA `.cu`/`.cuh` reuse the C++ grammar) | +| Code (36 tree-sitter grammars) | `.py .ts .js .jsx .tsx .mjs .go .rs .java .c .cpp .h .hpp .cu .cuh .metal .rb .cs .kt .scala .php .swift .lua .luau .zig .ps1 .psm1 .ex .exs .m .mm .jl .vue .svelte .astro .groovy .gradle .dart .v .sv .svh .sql .f .f90 .f95 .f03 .f08 .pas .pp .dpr .dpk .lpr .inc .dfm .lfm .lpk .sh .bash .json .dm .dme .dmi .dmm .dmf .sln .slnx .csproj .fsproj .vbproj .xaml .razor .cshtml` (`.dm`/`.dme` requires `uv tool install graphifyy[dm]`; CUDA `.cu`/`.cuh` and Metal `.metal` reuse the C++ grammar) | | Salesforce Apex | `.cls .trigger` (regex-based; classes, interfaces, enums, methods, triggers, SOQL/DML edges) | | Terraform / HCL | `.tf .tfvars .hcl` (requires `uv tool install graphifyy[terraform]`) | | MCP configs | `.mcp.json` `mcp.json` `mcp_servers.json` `claude_desktop_config.json` — extracts server nodes, package refs, env var requirements | diff --git a/graphify/build.py b/graphify/build.py index 75e2d33d..4495c0bf 100644 --- a/graphify/build.py +++ b/graphify/build.py @@ -336,6 +336,7 @@ def build_from_json(extraction: dict, *, directed: bool = False, root: str | Pat ".go": "go", ".rs": "rs", ".java": "jvm", ".kt": "jvm", ".scala": "jvm", ".groovy": "jvm", ".c": "c", ".h": "c", ".cc": "cpp", ".cpp": "cpp", ".hpp": "cpp", + ".cu": "cpp", ".cuh": "cpp", ".metal": "cpp", ".rb": "rb", ".php": "php", ".cs": "cs", ".swift": "swift", ".lua": "lua", } src_ext = Path(G.nodes[src].get("source_file") or "").suffix.lower() diff --git a/graphify/detect.py b/graphify/detect.py index 593c2f5d..92b773f7 100644 --- a/graphify/detect.py +++ b/graphify/detect.py @@ -27,7 +27,7 @@ class FileType(str, Enum): _MANIFEST_PATH = str(out_path("manifest.json")) -CODE_EXTENSIONS = {'.py', '.ts', '.tsx', '.js', '.jsx', '.mjs', '.ejs', '.ets', '.go', '.rs', '.java', '.groovy', '.gradle', '.cpp', '.cc', '.cxx', '.c', '.h', '.hpp', '.cu', '.cuh', '.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', '.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'} DOC_EXTENSIONS = {'.md', '.mdx', '.qmd', '.txt', '.rst', '.html', '.yaml', '.yml'} PAPER_EXTENSIONS = {'.pdf'} IMAGE_EXTENSIONS = {'.png', '.jpg', '.jpeg', '.gif', '.webp', '.svg'} diff --git a/graphify/extract.py b/graphify/extract.py index 5a80bf42..75efb65b 100644 --- a/graphify/extract.py +++ b/graphify/extract.py @@ -12750,6 +12750,7 @@ _DISPATCH: dict[str, Any] = { ".hpp": extract_cpp, ".cu": extract_cpp, ".cuh": extract_cpp, + ".metal": extract_cpp, ".rb": extract_ruby, ".cs": extract_csharp, ".kt": extract_kotlin, diff --git a/tests/fixtures/sample.metal b/tests/fixtures/sample.metal new file mode 100644 index 00000000..6ab3322e --- /dev/null +++ b/tests/fixtures/sample.metal @@ -0,0 +1,21 @@ +#include +using namespace metal; + +struct Vec3 { + float x; + float y; + float z; +}; + +float dot3(Vec3 a, Vec3 b) { + return a.x * b.x + a.y * b.y + a.z * b.z; +} + +kernel void saxpy( + device const float* x [[buffer(0)]], + device float* y [[buffer(1)]], + constant float& a [[buffer(2)]], + uint id [[thread_position_in_grid]] +) { + y[id] = a * x[id] + y[id]; +} diff --git a/tests/test_languages.py b/tests/test_languages.py index d16353e2..d2fd4390 100644 --- a/tests/test_languages.py +++ b/tests/test_languages.py @@ -257,6 +257,27 @@ def test_cuda_host_call_edges(): assert ("main()", "host_norm()") in calls +# Metal Shading Language is a C++14-derived language, so .metal files route +# through the C++ extractor just like CUDA does. + +def test_metal_is_code_extension(): + from graphify.detect import CODE_EXTENSIONS + assert ".metal" in CODE_EXTENSIONS + + +def test_metal_no_error(): + r = extract_cpp(FIXTURES / "sample.metal") + assert "error" not in r + + +def test_metal_finds_kernel_function_and_struct(): + r = extract_cpp(FIXTURES / "sample.metal") + labels = _labels(r) + assert any("Vec3" in l for l in labels) + assert any("dot3" in l for l in labels) + assert any("saxpy" in l for l in labels) + + # ── Ruby ───────────────────────────────────────────────────────────────────── def test_ruby_no_error():