From 3b74b8ad3d195e4f582a233b35cd5a8ff6f66e7f Mon Sep 17 00:00:00 2001 From: Alex Tumanov Date: Thu, 1 Oct 2026 03:35:03 -0500 Subject: [PATCH] feat(extract): include Verilog header files Register .vh with the existing Verilog detection and extraction paths, including incremental and watch coverage. Refs #3749 AI-Assisted-By: OpenAI Codex --- README.md | 2 +- graphify/detect.py | 2 +- graphify/extract.py | 1 + tests/test_detect.py | 26 ++++++++++++++++++++++++++ tests/test_languages.py | 27 +++++++++++++++++++++++++++ tests/test_watch.py | 8 ++++++++ 6 files changed, 64 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 46d7291cb..0be5e5481 100644 --- a/README.md +++ b/README.md @@ -344,7 +344,7 @@ To remove graphify from all platforms at once: `graphify uninstall` (add `--purg | Type | Extensions | |------|-----------| -| Code (37 tree-sitter grammars) | `.py .ts .mts .cts .js .jsx .tsx .mjs .go .rs .java .c .cpp .cc .cxx .h .hpp .cu .cuh .metal .rb .cs .kt .kts .scala .php .swift .lua .luau .toc .zig .ps1 .psm1 .psd1 .ex .exs .m .mm .ml .mli .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]`, `.ml`/`.mli` requires `uv tool install graphifyy[ocaml]`; `.mts`/`.cts` reuse the TypeScript grammar, `.cc`/`.cxx` and CUDA `.cu`/`.cuh` and Metal `.metal` reuse the C++ grammar) | +| Code (37 tree-sitter grammars) | `.py .ts .mts .cts .js .jsx .tsx .mjs .go .rs .java .c .cpp .cc .cxx .h .hpp .cu .cuh .metal .rb .cs .kt .kts .scala .php .swift .lua .luau .toc .zig .ps1 .psm1 .psd1 .ex .exs .m .mm .ml .mli .jl .vue .svelte .astro .groovy .gradle .dart .v .sv .svh .vh .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]`, `.ml`/`.mli` requires `uv tool install graphifyy[ocaml]`; `.mts`/`.cts` reuse the TypeScript grammar, `.cc`/`.cxx` and 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]`) | | OCaml | `.ml .mli` (requires `uv tool install graphifyy[ocaml]`) | diff --git a/graphify/detect.py b/graphify/detect.py index 00fa7d5ae..483082670 100644 --- a/graphify/detect.py +++ b/graphify/detect.py @@ -44,7 +44,7 @@ _MANIFEST_PATH = str(out_path("manifest.json")) _MTIME_COARSE_S = 2.0 _MTIME_SUBSECOND_S = 0.05 -CODE_EXTENSIONS = {'.py', '.ts', '.tsx', '.mts', '.cts', '.js', '.jsx', '.mjs', '.cjs', '.ejs', '.ets', '.go', '.rs', '.vb', '.cbl', '.cob', '.cobol', '.cpy', '.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', '.ml', '.mli', '.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', '.lisp', '.cl', '.lsp', '.asd', '.robot', '.resource', '.sol', '.erl', '.hrl', '.escript'} +CODE_EXTENSIONS = {'.py', '.ts', '.tsx', '.mts', '.cts', '.js', '.jsx', '.mjs', '.cjs', '.ejs', '.ets', '.go', '.rs', '.vb', '.cbl', '.cob', '.cobol', '.cpy', '.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', '.ml', '.mli', '.jl', '.vue', '.svelte', '.astro', '.dart', '.v', '.sv', '.svh', '.vh', '.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', '.lisp', '.cl', '.lsp', '.asd', '.robot', '.resource', '.sol', '.erl', '.hrl', '.escript'} DOC_EXTENSIONS = {'.md', '.mdx', '.qmd', '.skill', '.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 b54889c8b..e21ef9a23 100644 --- a/graphify/extract.py +++ b/graphify/extract.py @@ -6767,6 +6767,7 @@ _DISPATCH: dict[str, Any] = { ".v": extract_verilog, ".sv": extract_verilog, ".svh": extract_verilog, + ".vh": extract_verilog, ".sql": extract_sql, ".md": extract_markdown, ".mdx": extract_markdown, diff --git a/tests/test_detect.py b/tests/test_detect.py index bba9361f0..bf1261619 100644 --- a/tests/test_detect.py +++ b/tests/test_detect.py @@ -34,6 +34,32 @@ def test_classify_python(): def test_classify_typescript(): assert classify_file(Path("bar.ts")) == FileType.CODE +@pytest.mark.parametrize("suffix", [".vh", ".VH"]) +def test_detect_verilog_headers(tmp_path, suffix): + header = tmp_path / f"defs{suffix}" + header.write_text("`define WIDTH 8\n", encoding="utf-8") + + assert classify_file(header) == FileType.CODE + result = detect(tmp_path) + assert header.resolve() in {Path(path).resolve() for path in result["files"]["code"]} + +def test_detect_incremental_requeues_changed_verilog_header(tmp_path): + header = tmp_path / "defs.vh" + header.write_text("`define WIDTH 8\n", encoding="utf-8") + manifest_path = tmp_path / "graphify-out" / "manifest.json" + + full = detect(tmp_path) + save_manifest(full["files"], manifest_path, root=tmp_path, kind="ast") + + warm = detect_incremental(tmp_path, manifest_path, kind="ast") + assert warm["new_total"] == 0 + + header.write_text("`define WIDTH 16\n", encoding="utf-8") + changed = detect_incremental(tmp_path, manifest_path, kind="ast") + assert header.resolve() in { + Path(path).resolve() for path in changed["new_files"]["code"] + } + def test_classify_powershell_module(): # #1315: .psm1 modules were never indexed (CODE_EXTENSIONS gap). assert classify_file(Path("Utils.psm1")) == FileType.CODE diff --git a/tests/test_languages.py b/tests/test_languages.py index ffe59b919..de6e91635 100644 --- a/tests/test_languages.py +++ b/tests/test_languages.py @@ -3875,6 +3875,33 @@ def test_apex_no_dangling_edges(): # -- SystemVerilog ------------------------------------------------------------- +@pytest.mark.parametrize("suffix", [".v", ".sv", ".svh", ".vh"]) +def test_verilog_family_dispatch(suffix): + from graphify.extract import _get_extractor + + assert _get_extractor(Path(f"defs{suffix}")) is extract_verilog + + +@pytest.mark.parametrize("with_module", [False, True]) +def test_verilog_header_collection_and_extraction(tmp_path, with_module): + from graphify.extract import collect_files, extract + + header = tmp_path / "defs.vh" + source = "`define WIDTH 8\n" + if with_module: + source += "module helper(input wire a, output wire b);\nassign b = a;\nendmodule\n" + header.write_text(source, encoding="utf-8") + + paths = collect_files(tmp_path) + assert header in paths + result = extract(paths, root=tmp_path, cache_root=tmp_path, parallel=False) + assert header.name in {node["label"] for node in result["nodes"]} + if with_module: + assert (header.name, "helper") in _edge_labels(result, "defines") + else: + assert {node["label"] for node in result["nodes"]} == {header.name} + + def test_systemverilog_no_error(): r = extract_verilog(FIXTURES / "sample.sv") assert "error" not in r diff --git a/tests/test_watch.py b/tests/test_watch.py index 4d88904d9..d13f4b362 100644 --- a/tests/test_watch.py +++ b/tests/test_watch.py @@ -47,6 +47,14 @@ def test_watched_extensions_includes_code(): assert ".go" in _WATCHED_EXTENSIONS assert ".rs" in _WATCHED_EXTENSIONS +def test_verilog_header_triggers_code_rebuild_without_llm(tmp_path): + header = tmp_path / "defs.vh" + header.write_text("`define WIDTH 8\n", encoding="utf-8") + + assert ".vh" in _WATCHED_EXTENSIONS + assert _batch_triggers_rebuild([header]) is True + assert _batch_needs_llm_flag([header]) is False + def test_watched_extensions_includes_docs(): assert ".md" in _WATCHED_EXTENSIONS assert ".txt" in _WATCHED_EXTENSIONS