diff --git a/graphify/extract.py b/graphify/extract.py index a11e4261d..654bef59e 100644 --- a/graphify/extract.py +++ b/graphify/extract.py @@ -154,6 +154,7 @@ from graphify.extractors.resolution import ( # noqa: E402,F401 _ts_collect_type_refs, _ts_heritage_clause_entries, _ts_walk_class_members, + _VUE_SCRIPT_LANG_RE as _SCRIPT_LANG_RE, _vue_mask_non_script, _walk_js_tree, _walk_python_tree, @@ -2380,17 +2381,79 @@ def _emit_rescued_import( existing_ids.add(node_id) -def extract_svelte(path: Path) -> dict: - """Extract imports from .svelte files: script-block via JS AST + template regex fallback. +_HTML_SCRIPT_TAG_RE = re.compile( + r"\"'])*)>([\s\S]*?)", + re.IGNORECASE, +) +_NON_JS_SCRIPT_TYPE_RE = re.compile( + r"""\btype\s*=\s*["']?(?!module\b|text/javascript\b|application/javascript\b)""", + re.IGNORECASE, +) - Tree-sitter only sees the + +""", + ) + result = extract_svelte(component) + assert result.get("parse_errors") is None + labels = _labels(result) + assert "bump()" in labels + # Masking keeps offsets, so locations still point at the original lines. + assert labels["bump()"] == "L4" + + +def test_extract_svelte_parses_module_and_instance_scripts(tmp_path): + """Svelte 5 ` + +

hi

+""", + ) + result = extract_svelte(component) + assert result.get("parse_errors") is None + labels = _labels(result) + assert "shared()" in labels and "local()" in labels + assert labels["shared()"] == "L2" + assert labels["local()"] == "L5" + + +def test_extract_svelte_lang_ts_parses_type_syntax(tmp_path): + """` + +""", + ) + result = extract_svelte(component) + assert result.get("parse_errors") is None + labels = _labels(result) + assert "Props" in labels + assert "bump()" in labels + + +def test_extract_svelte_non_js_script_type_is_not_parsed_as_code(tmp_path): + """` + +

hi

+""", + ) + result = extract_svelte(component) + assert result.get("parse_errors") is None + assert "go()" in _labels(result) + + +def test_extract_svelte_static_import_still_resolves(tmp_path): + """Masking must not cost the imports the regex rescue already recovered.""" + component = _write( + tmp_path / "src/Imports.svelte", + """ +

hi

+""", + ) + helper = _write(tmp_path / "src/helper.ts", "export function helper(){}\n") + + result = extract_svelte(component) + assert _make_id(str(helper)) in _import_targets(result, relation="imports_from") + + +def test_extract_svelte_dynamic_import_in_template(tmp_path): + """`{#await import('./X.svelte')}` lives in markup, so the regex pass owns it.""" + component = _write( + tmp_path / "src/Lazy.svelte", + """ +{#await import('./Other.svelte') then Mod} + +{/await} +""", + ) + other = _write(tmp_path / "src/Other.svelte", "

o

\n") + + result = extract_svelte(component) + assert _make_id(str(other)) in _import_targets(result, relation="dynamic_import") + + +def test_extract_svelte_scripts_on_one_line_do_not_merge(tmp_path): + """Two script bodies on one line must not parse as a single statement.""" + component = _write( + tmp_path / "src/Inline.svelte", + "\n", + ) + result = extract_svelte(component) + assert result.get("parse_errors") is None + assert "b()" in _labels(result) + + +def test_extract_svelte_markup_only_component_does_not_crash(tmp_path): + """A `.svelte` file need not have a `