mirror of
https://github.com/safishamsi/graphify.git
synced 2026-10-16 17:09:17 +00:00
fix(extract): parse only Astro frontmatter and scripts in the AST pass
extract_astro fed the whole .astro file to the JS grammar. The template is not JS, so every page reported a syntax error (usually at line 1-2) and symbols tree-sitter could not recover past the template were dropped. Blank everything outside the frontmatter and JS <script> bodies (keeping newlines so locations still match, as _vue_mask_non_script does) and parse with the TS grammar, Astro's default. Scripts with a non-JS type (application/ld+json, ...) are blanked too. The existing regex import rescue is unchanged. Fixes #2788. Covers the unmasked-script half of #3337. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
d6eaa8aae8
commit
fe07657fe1
2 files changed
+104
-1
No files matched your search
+48
-1
@@ -2405,6 +2405,42 @@ def extract_svelte(path: Path) -> dict:
|
||||
return result
|
||||
|
||||
|
||||
_ASTRO_FRONTMATTER_RE = re.compile(r"\A\s*---[^\S\r\n]*\r?\n([\s\S]*?)\r?\n---")
|
||||
_ASTRO_SCRIPT_RE = re.compile(
|
||||
r"<script\b((?:\"[^\"]*\"|'[^']*'|[^>\"'])*)>([\s\S]*?)</script\s*>",
|
||||
re.IGNORECASE,
|
||||
)
|
||||
_ASTRO_NON_JS_TYPE_RE = re.compile(
|
||||
r"""\btype\s*=\s*["']?(?!module\b|text/javascript\b|application/javascript\b)""",
|
||||
re.IGNORECASE,
|
||||
)
|
||||
|
||||
|
||||
def _astro_mask_non_script(src: str) -> str:
|
||||
"""Blank everything in a ``.astro`` file except frontmatter and JS ``<script>`` bodies.
|
||||
|
||||
Every character outside those regions becomes a space (``\\r``/``\\n`` are kept),
|
||||
so AST locations match the original file. Scripts with a non-JS ``type``
|
||||
(``application/ld+json`` and the like) are blanked too: their bodies are not
|
||||
statements and would only add parse errors.
|
||||
"""
|
||||
keep: list[tuple[int, int]] = []
|
||||
fm = _ASTRO_FRONTMATTER_RE.match(src)
|
||||
if fm:
|
||||
keep.append((fm.start(1), fm.end(1)))
|
||||
for m in _ASTRO_SCRIPT_RE.finditer(src, fm.end() if fm else 0):
|
||||
if not _ASTRO_NON_JS_TYPE_RE.search(m.group(1)):
|
||||
keep.append((m.start(2), m.end(2)))
|
||||
chars = [c if c in "\r\n" else " " for c in src]
|
||||
for start, end in keep:
|
||||
chars[start:end] = src[start:end]
|
||||
# Terminate the region in place of the following `<`, so two scripts on
|
||||
# one line don't run together into a single statement.
|
||||
if end < len(chars) and chars[end] == " ":
|
||||
chars[end] = ";"
|
||||
return "".join(chars)
|
||||
|
||||
|
||||
def extract_astro(path: Path) -> dict:
|
||||
"""Extract imports from .astro files: frontmatter (TS) + template regex fallback.
|
||||
|
||||
@@ -2417,8 +2453,19 @@ def extract_astro(path: Path) -> dict:
|
||||
silently dropped (#850). Mirrors :func:`extract_svelte` — same regex-rescue
|
||||
approach, scanning the frontmatter block and any client-side ``<script>`` blocks
|
||||
for static and dynamic imports.
|
||||
|
||||
The AST pass parses only the frontmatter and JS ``<script>`` bodies with the TS
|
||||
grammar (Astro's default), blanking everything else so line numbers still
|
||||
line up — the same masking as :func:`_vue_mask_non_script`. Parsing the whole
|
||||
file flagged every template as a syntax error and dropped frontmatter symbols.
|
||||
"""
|
||||
result = _extract_generic(path, _JS_CONFIG)
|
||||
try:
|
||||
src = path.read_text(encoding="utf-8", errors="replace")
|
||||
except OSError:
|
||||
return {"nodes": [], "edges": []}
|
||||
masked = _astro_mask_non_script(src).encode("utf-8")
|
||||
masked = _normalize_ts_import_types(masked) or masked
|
||||
result = _extract_generic(path, _TS_CONFIG, source_override=masked)
|
||||
try:
|
||||
import re as _re
|
||||
src = path.read_text(encoding="utf-8", errors="replace")
|
||||
|
||||
@@ -141,3 +141,59 @@ import Hero from '@components/Hero.astro';
|
||||
result = extract_astro(page)
|
||||
targets = _import_targets(result, relation="imports_from")
|
||||
assert _make_id(str(hero)) in targets
|
||||
|
||||
|
||||
def _labels(result: dict) -> dict[str, str]:
|
||||
"""Label -> source_location for every node except the file node."""
|
||||
return {
|
||||
str(n.get("label")): str(n.get("source_location"))
|
||||
for n in result.get("nodes", [])
|
||||
if n.get("id") != result.get("nodes", [{}])[0].get("id")
|
||||
}
|
||||
|
||||
|
||||
def test_extract_astro_template_is_not_a_parse_error(tmp_path):
|
||||
"""The template is not TS; only frontmatter and <script> bodies are parsed (#2788).
|
||||
|
||||
Parsing the whole file as JS reported every page as a syntax error, and
|
||||
symbols the parser could not recover past the template were dropped.
|
||||
"""
|
||||
page = _write(
|
||||
tmp_path / "src/pages/results.astro",
|
||||
"""---
|
||||
import Layout from '../layouts/Layout.astro';
|
||||
interface Props { year: number }
|
||||
const { year } = Astro.props;
|
||||
function rank(scores: number[]): number[] {
|
||||
return [...scores].sort((a, b) => b - a);
|
||||
}
|
||||
---
|
||||
|
||||
<Layout title={`Results ${year}`}>
|
||||
<ol>{rank([3, 1, 2]).map((s) => <li class="score">{s}</li>)}</ol>
|
||||
</Layout>
|
||||
<script type="application/ld+json">{ "@type": "Event", "name": "x" }</script>
|
||||
<script>
|
||||
function toggle(el: HTMLElement) { el.classList.toggle('open'); }
|
||||
document.querySelectorAll('ol').forEach((el) => toggle(el));
|
||||
</script>
|
||||
""",
|
||||
)
|
||||
result = extract_astro(page)
|
||||
assert result.get("parse_errors") is None
|
||||
labels = _labels(result)
|
||||
assert "rank()" in labels and "toggle()" in labels
|
||||
# Masking keeps offsets, so locations still point at the original lines.
|
||||
assert labels["rank()"] == "L5"
|
||||
assert labels["toggle()"] == "L15"
|
||||
|
||||
|
||||
def test_extract_astro_scripts_on_one_line_do_not_merge(tmp_path):
|
||||
"""Two script bodies on one line must not parse as a single statement."""
|
||||
page = _write(
|
||||
tmp_path / "src/pages/inline.astro",
|
||||
"<script>const a = 1</script><script>function b() {}</script>\n",
|
||||
)
|
||||
result = extract_astro(page)
|
||||
assert result.get("parse_errors") is None
|
||||
assert "b()" in _labels(result)
|
||||
Reference in new issue
Block a user