mirror of
https://github.com/safishamsi/graphify.git
synced 2026-10-15 08:29:26 +00:00
Index JavaScript in inline script blocks of a PHP file
The PHP tree sitter grammar treats a span that is not PHP, such as an inline script block, as opaque markup, so any JavaScript declared there was entirely absent from the graph: functions, calls, and everything else in that region. extract_php now runs a second pass after the PHP extraction. Every part of the source that is not a script body is blanked to spaces, preserving newlines so line numbers stay accurate, and the masked source is parsed with the JS grammar the same way the Vue extractor already masks everything outside its own script block. The two node sets are merged onto the one file node, so a symbol declared in the script block looks like any other symbol in the file. A script tag with only a src attribute and no body masks to an empty region and contributes nothing, and a file with no script tag at all skips the second pass entirely. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
26b02b5e34
commit
2114766c91
1 file changed
+65
-2
+65
-2
@@ -2546,9 +2546,72 @@ def extract_scala(path: Path) -> dict:
|
||||
return _extract_generic(path, _SCALA_CONFIG)
|
||||
|
||||
|
||||
_PHP_SCRIPT_RE = re.compile(
|
||||
r"<script\b(?:\"[^\"]*\"|'[^']*'|[^>\"'])*>([\s\S]*?)</script\s*>",
|
||||
re.IGNORECASE,
|
||||
)
|
||||
|
||||
|
||||
def _php_mask_to_script_blocks(src: str) -> tuple[str, bool]:
|
||||
"""Blank everything outside inline ``<script>`` bodies, keeping line numbers.
|
||||
|
||||
tree-sitter-php treats non-PHP spans (including inline ``<script>`` blocks)
|
||||
as opaque text, so JS declared there is invisible to the PHP grammar (#2320).
|
||||
Mirrors :func:`_vue_mask_non_script`'s approach: replace every character
|
||||
outside a script body with a space (preserving ``\\r``/``\\n`` so source
|
||||
locations still line up), leaving only the script content for a JS parse.
|
||||
A ``<script src="…"></script>`` with no body masks to an empty region,
|
||||
which parses as empty JS and contributes nothing — no special-casing needed.
|
||||
Returns ``(masked_source, had_script)``; the caller skips the JS pass
|
||||
entirely when ``had_script`` is False.
|
||||
"""
|
||||
def _blank(s: str) -> str:
|
||||
return re.sub(r"[^\r\n]", " ", s)
|
||||
|
||||
out: list[str] = []
|
||||
pos = 0
|
||||
had_script = False
|
||||
for m in _PHP_SCRIPT_RE.finditer(src):
|
||||
had_script = True
|
||||
out.append(_blank(src[pos:m.start(1)])) # everything up to and including <script ...>
|
||||
out.append(m.group(1)) # script body, verbatim
|
||||
out.append(_blank(src[m.end(1):m.end()])) # </script> and any trailing part of the match
|
||||
pos = m.end()
|
||||
if not had_script:
|
||||
return src, False
|
||||
out.append(_blank(src[pos:]))
|
||||
return "".join(out), True
|
||||
|
||||
|
||||
def extract_php(path: Path) -> dict:
|
||||
"""Extract classes, functions, methods, namespace uses, and calls from a .php file."""
|
||||
return _extract_generic(path, _PHP_CONFIG)
|
||||
"""Extract classes, functions, methods, namespace uses, and calls from a .php file.
|
||||
|
||||
Also parses any inline ``<script>`` blocks as JavaScript (#2320): the PHP
|
||||
grammar sees those spans as opaque markup, so a page mixing PHP with
|
||||
client-side JS previously indexed only its PHP half. The JS pass masks
|
||||
everything else to blanks (preserving line numbers) and reuses the same
|
||||
file node id, so JS symbols land under the one file node like any other
|
||||
PHP symbol; a same-name PHP/JS collision (rare — different languages,
|
||||
same file) is resolved by keeping the PHP node, since PHP is the file's
|
||||
primary language.
|
||||
"""
|
||||
result = _extract_generic(path, _PHP_CONFIG)
|
||||
try:
|
||||
src = path.read_text(encoding="utf-8", errors="replace")
|
||||
masked, had_script = _php_mask_to_script_blocks(src)
|
||||
if not had_script:
|
||||
return result
|
||||
js_result = _extract_generic(path, _JS_CONFIG, source_override=masked.encode("utf-8"))
|
||||
existing_ids = {n["id"] for n in result.get("nodes", [])}
|
||||
for n in js_result.get("nodes", []):
|
||||
if n["id"] not in existing_ids:
|
||||
result.setdefault("nodes", []).append(n)
|
||||
existing_ids.add(n["id"])
|
||||
for e in js_result.get("edges", []):
|
||||
result.setdefault("edges", []).append(e)
|
||||
except Exception:
|
||||
pass
|
||||
return result
|
||||
|
||||
|
||||
# One level of balanced parens (e.g. `Foo #(Bar #(int))`) — bounded so malformed
|
||||
|
||||
Reference in new issue
Block a user