From 89047409f3259b9a6de6a0df3f9a22ad366cb4dc Mon Sep 17 00:00:00 2001 From: anhnh2002 Date: Thu, 22 Oct 2026 13:52:47 +0700 Subject: [PATCH] analyzers: add Zig and Vue SFC analyzers, Autotools build artifacts Zig (analyzers/zig.py): functions, containers (struct, enum, union, opaque), their member functions (Point.add), and generic type functions (fn List(comptime T: type) type { return struct {...}; }) become components, with call, T{...} instantiation, and type-reference edges. @import aliases and re-exports resolve to the file that declares the name, as do module names the root build.zig registers with addModule; std, builtin, and third-party packages are dropped. test blocks and zig-cache/, .zig-cache/, zig-out/ are skipped. Vue (analyzers/vue.py): the + + +""" + +SETUP_SFC = """\ + + + + + +""" + + +def _write(root: Path, rel: str, text: str) -> Path: + path = root / rel + path.parent.mkdir(parents=True, exist_ok=True) + path.write_text(text, encoding="utf-8") + return path + + +@pytest.fixture +def vue_repo(tmp_path: Path) -> Path: + _write(tmp_path, "src/components/ArticleList.vue", OPTIONS_SFC) + _write( + tmp_path, + "src/components/ArticlePreview.vue", + "\n" + "\n", + ) + _write(tmp_path, "src/components/VPagination.vue", "\n") + _write( + tmp_path, + "src/api/articles.js", + "export function fetchArticles() {\n return fetch('/api/articles');\n}\n", + ) + _write(tmp_path, "src/views/Home.vue", SETUP_SFC) + _write(tmp_path, "src/components/TagList.vue", "\n") + return tmp_path + + +def _analyze(repo: Path, rel: str): + path = repo / rel + nodes, rels = analyze_vue_file(str(path), path.read_text(), repo_path=str(repo)) + return {n.id: n for n in nodes}, {(r.caller, r.callee): r for r in rels} + + +# --------------------------------------------------------------------------- # +# names +# --------------------------------------------------------------------------- # + + +@pytest.mark.parametrize( + "value, expected", + [ + ("article-preview", "ArticlePreview"), + ("ArticlePreview", "ArticlePreview"), + ("article_list", "ArticleList"), + ("app", "App"), + ], +) +def test_pascal_case(value: str, expected: str) -> None: + assert pascal_case(value) == expected + + +def test_component_name_from_path() -> None: + assert vue_component_name("src/components/article-list.vue") == "ArticleList" + assert vue_component_name("src/views/Settings/index.vue") == "Settings" + assert vue_component_name("App.vue") == "App" + + +# --------------------------------------------------------------------------- # +# options API +# --------------------------------------------------------------------------- # + + +def test_component_and_members(vue_repo: Path) -> None: + nodes, _ = _analyze(vue_repo, "src/components/ArticleList.vue") + base = "src/components/ArticleList.vue::" + component = nodes[base + "ArticleList"] + assert component.component_type == "class" + assert component.node_type == "component" + assert component.language == "vue" + assert component.start_line == 1 and component.end_line == OPTIONS_SFC.count("\n") + + for member in ("load", "setPage", "pages", "type", "mounted"): + node = nodes[f"{base}ArticleList.{member}"] + assert node.component_type == "method" + assert node.class_name == "ArticleList" + # spreads and non-function options are not members + assert not any(i.endswith(("ArticleList.articles", "ArticleList.name")) for i in nodes) + # script-level functions still come from the JavaScript analyzer + assert base + "format" in nodes + + +def test_line_numbers_follow_the_vue_file(vue_repo: Path) -> None: + nodes, _ = _analyze(vue_repo, "src/components/ArticleList.vue") + lines = OPTIONS_SFC.splitlines() + fmt = nodes["src/components/ArticleList.vue::format"] + assert lines[fmt.start_line - 1].startswith("function format(n)") + load = nodes["src/components/ArticleList.vue::ArticleList.load"] + assert lines[load.start_line - 1].strip() == "load() {" + assert load.source_code.strip().startswith("load() {") + + +def test_template_and_import_edges(vue_repo: Path) -> None: + _, rels = _analyze(vue_repo, "src/components/ArticleList.vue") + base = "src/components/ArticleList.vue::" + component = base + "ArticleList" + + # kebab-case tag -> import resolved through the extension-less path + edge = rels[(component, "src/components/ArticlePreview.vue::ArticlePreview")] + assert edge.is_resolved + # PascalCase tag -> `@/` import + assert (component, "src/components/VPagination.vue::VPagination") in rels + # globally registered component: bare name for CallGraphAnalyzer + assert (component, "GlobalWidget") in rels + # built-in tags and package imports are not edges + assert not any(callee in ("RouterLink", "mapGetters") for _, callee in rels) + # template handlers / bindings / interpolations -> members and script functions + assert (component, base + "ArticleList.setPage") in rels + assert (component, base + "ArticleList.load") in rels + assert (component, base + "format") in rels + # named import keeps the exported name + assert (component, "src/api/articles.js::fetchArticles") in rels + + +def test_member_call_edges(vue_repo: Path) -> None: + _, rels = _analyze(vue_repo, "src/components/ArticleList.vue") + base = "src/components/ArticleList.vue::ArticleList." + assert (base + "setPage", base + "load") in rels # this.load() + assert (base + "type", base + "load") in rels + assert (base + "mounted", base + "load") in rels + assert (base + "load", "src/components/ArticleList.vue::format") in rels + assert (base + "load", "src/api/articles.js::fetchArticles") in rels # aliased import + + +# --------------------------------------------------------------------------- # +#