diff --git a/CHANGELOG.md b/CHANGELOG.md index baef1f58..525dace2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -44,6 +44,34 @@ uses [Semantic Versioning](https://semver.org/). to directories through the module path in `go.mod` (and local `replace` directives), and standard-library or third-party calls are dropped. `_test.go` files and `vendor/` are skipped. +- **Zig analyzer.** 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. Edges cover calls (plain, `alias.fn()`, and method calls on + `Self`, parameters, and locals whose type can be inferred), `T{...}` + literals, and types named in parameters, return types, fields, and + annotated locals. `@import("x.zig")` aliases and re-exports + (`const T = @import("x.zig").T;`) resolve to the file that declares the + name, as do module names that 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, `build.zig` + is also a build artifact, and `build.zig.zon` is a manifest. +- **Vue single-file components.** Each `.vue` file becomes a component + (`ArticleList.vue::ArticleList`), and its options-API members (`methods`, + `computed`, `watch`, lifecycle hooks such as `mounted` or `setup`) become + its methods. ` + + +""" + +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 + + +# --------------------------------------------------------------------------- # +#