Skip to content

Index JavaScript in inline <script> blocks of a PHP file (#2320) - #3627

Open
ayushcodes10 wants to merge 6 commits into
Graphify-Labs:v8from
ayushcodes10:fix-2320-php-inline-script
Open

ayushcodes10 wants to merge 6 commits into
Graphify-Labs:v8from
ayushcodes10:fix-2320-php-inline-script

Conversation

@ayushcodes10

Copy link
Copy Markdown
Contributor

Summary

Fixes #2320. `.php` files that mix PHP with an inline `<script>` block only had their PHP half indexed — any JavaScript declared inside `<script>…</script>` was silently absent from the graph, so `query`/`explain`/`path` couldn't see it. This matters for classic server-rendered PHP apps, where a large share of behavior lives in inline `<script>` blocks.

Root cause

`extract_php` delegates straight to the generic tree-sitter path with no handling for embedded regions. `tree-sitter-php`'s grammar treats non-PHP spans as opaque text, so JS inside them is never parsed.

Fix

Reuses the masking pattern `extract_vue` already established for the same class of problem (an embedded `<script>` region a host grammar can't see into): after the normal PHP extraction, everything outside `<script>` bodies is blanked to spaces (preserving `\r`/`\n` so line numbers stay accurate), and the masked full source is parsed with the JS grammar via `_extract_generic(path, _JS_CONFIG, source_override=...)`. The two node sets merge onto the one file node — a symbol declared in the script block looks like any other symbol in the file.

Two edge cases handled:

  • `<script src="…"></script>` with no body masks to an empty region, which parses as empty JS and contributes nothing — no special-casing needed.
  • A file with no `<script>` tag at all skips the second pass entirely (no wasted parse).

A PHP echo tag inside the JS region (`const MODE = ;`) doesn't crash the JS parser and the rest of the block still extracts correctly, so the placeholder-substitution refinement the issue mentioned as a nice-to-have wasn't needed.

Test plan

  • New tests in `tests/test_languages.py`: the issue's exact reproduction (two functions plus the call edge between them), line numbers surviving the masking pass, a `src`-only script tag contributing nothing without crashing, and a plain PHP file with no script tag being unaffected (same node/edge shape as before).
  • Verified the full 3-file scenario from the issue (`page.php` + `plain.js` + `plain.php`) produces all 5 expected symbols plus the cross-block call edge.
  • All 23 existing PHP extraction tests still pass unmodified.
  • Full suite: `python3 -m pytest -q` — 5625 passed, 68 skipped, no regressions.

🤖 Generated with Claude Code

ayushcodes10 and others added 3 commits September 17, 2026 15:34
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>
Covers the issue's exact reproduction (two functions plus the call
edge between them), that line numbers survive the masking pass, that
a script tag with only a src attribute contributes nothing without
crashing, and that a plain PHP file with no script tag is unaffected.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

@graphify-labs graphify-labs Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Graphify reviewed this change.

Worth a look — the grounded gate found no coupling regressions or blocking issues, but 4 advisory finding(s) below merit a look before merge.

Formal verification. No changes could be formally verified in this run.


Graphify review — findings

Indexes JavaScript declared inside inline <script> blocks of .php files, which tree-sitter-php otherwise treats as opaque markup and skips entirely. extract_php runs a second JS parse over _php_mask_to_script_blocks, which blanks everything outside script bodies while preserving line numbers, then merges the JS symbols onto the same file node; PHP wins on same-name collisions. Files with no <script> tag short-circuit before the JS pass and any extraction error is swallowed rather than failing the file.

Worth a look

  • JS edges appended without checking node id existence may reference dropped nodesgraphify/extract.py · Escalate · medium
    • agreed by 2 of 2 members but NOT verified (no proof, no reproducing execution) — consensus is not a verdict; needs human review
  • Script tags inside PHP strings are parsed as inline JSgraphify/extract.py:2572 · Escalate · medium
    • agreed by 2 of 2 members but NOT verified (no proof, no reproducing execution) — consensus is not a verdict; needs human review
  • Merged JS edges can attach to retained PHP node after node-id collisiongraphify/extract.py:2611 · Escalate · medium
    • agreed by 2 of 2 members but NOT verified (no proof, no reproducing execution) — consensus is not a verdict; needs human review
  • Bare except silently discards JS extraction failuresgraphify/extract.py:2612 · Escalate · medium
    • agreed by 2 of 2 members but NOT verified (no proof, no reproducing execution) — consensus is not a verdict; needs human review
Analysis details — impact, health, verification

Impact & health

Graphify review

Impact — 2568 functions depend on the 1014 functions this change touches.

Health — this change adds coupling hotspots:

  • new: extract() — 645 callers, 45 callees
  • new: _rebuild_code() — 129 callers, 54 callees
  • new: extract_js() — 85 callers, 4 callees
  • new: extract_xaml() — 19 callers, 17 callees
  • new: dispatch_command() — 2 callers, 125 callees
  • new: _get_extractor() — 26 callers, 6 callees
  • new: run_pipeline() — 8 callers, 13 callees
  • new: collect_files() — 17 callers, 6 callees
  • …and 30 more — each is listed as a finding

Verification — 2568 functions in the blast radius were not formally verified this run (proofs are advisory here).

Gate & verification

graphify gate

PASS — objectively clean (no health regressions, tests not run — proofs not run this pass (advisory)). Grounded, not self-assessed.

Advisory (not blocking):

  • verification_scope: 2393 function(s) in the blast radius were not formally verified this run

Test selection

Test selection

286 of 286 test file(s) selected (100%) via static blast radius.

Escalated to a full run for safety — the selection is not trustworthy on its own (see below). CI should run the whole suite.

  • tests/test_affected_cli.py — full-run-safety
  • tests/test_affected_member_seed.py — full-run-safety
  • tests/test_agents_platform.py — full-run-safety
  • tests/test_analyze.py — full-run-safety
  • tests/test_anthropic_custom_endpoint.py — full-run-safety
  • tests/test_antigravity_install.py — full-run-safety
  • tests/test_apm_fallback_version.py — full-run-safety
  • tests/test_architecture_doc.py — full-run-safety
  • tests/test_astro_extraction.py — impact, full-run-safety
  • tests/test_astro_import_ids.py — impact, full-run-safety
  • tests/test_atomic_canvas_export.py — full-run-safety
  • tests/test_atomic_version_stamp.py — full-run-safety
  • tests/test_atomic_writes.py — full-run-safety
  • tests/test_backend_env_isolation.py — full-run-safety
  • tests/test_backend_extras.py — full-run-safety
  • tests/test_benchmark.py — full-run-safety
  • tests/test_benchmark_raw_graph.py — full-run-safety
  • tests/test_build.py — impact, full-run-safety
  • tests/test_build_merge_dedup_scope.py — full-run-safety
  • tests/test_build_merge_hyperedges_and_prune.py — full-run-safety
  • tests/test_build_merge_shrink_guard.py — full-run-safety
  • tests/test_builtin_global_type_refs.py — impact, full-run-safety
  • tests/test_cache.py — full-run-safety
  • tests/test_callflow_html.py — full-run-safety
  • tests/test_cargo_introspect.py — full-run-safety
  • tests/test_carried_hyperedge_remap.py — full-run-safety
  • tests/test_case_sensitive_resolution.py — impact, full-run-safety
  • tests/test_charmap_encoding.py — full-run-safety
  • tests/test_chunking.py — full-run-safety
  • tests/test_cjs_module_extension.py — impact, full-run-safety
  • tests/test_claude_cli_backend.py — full-run-safety
  • tests/test_claude_md.py — full-run-safety
  • tests/test_cli_broken_pipe.py — full-run-safety
  • tests/test_cli_export.py — full-run-safety
  • tests/test_cli_help.py — full-run-safety
  • tests/test_cluster.py — full-run-safety
  • tests/test_codebuddy.py — full-run-safety
  • tests/test_community_hub_labels.py — full-run-safety
  • tests/test_community_labels_skill.py — full-run-safety
  • tests/test_confidence.py — full-run-safety
  • tests/test_corrupt_graph_json.py — full-run-safety
  • tests/test_cpp_nested_and_cli.py — impact, full-run-safety
  • tests/test_cpp_objc_cross_file_calls.py — impact, full-run-safety
  • tests/test_cpp_preprocess.py — full-run-safety
  • tests/test_cross_extension_reexport_self_cycle.py — impact, full-run-safety
  • tests/test_cross_language_call_resolution.py — impact, full-run-safety
  • tests/test_cross_repo_external_call_guards.py — impact, full-run-safety
  • tests/test_cross_repo_member_calls.py — impact, full-run-safety
  • tests/test_cross_repo_shared_types.py — full-run-safety
  • tests/test_csharp_call_site_generic_args.py — impact, full-run-safety
  • … and 236 more

non-code file(s) changed (CHANGELOG.md) → running the full suite for safety (a code graph can't see config/fixture/data deps)

changed code file(s) with no mapped test (CHANGELOG.md) — a coverage gap or a missing link — running the full suite rather than only the selected tests

Selection is safe under the controlled-regression assumption; always-run tests + a periodic full run are the backstops. Advisory — it never changes the check verdict.

Formal verification

Could not verify: Could not verify extract\_php.

The verifier did not have enough to check extract\_php, so it is saying so rather than guessing. No false assurance is the whole point.

Guarantee: No guarantee either way, this is an honest abstention, not a pass.

Note: Reason: no capturable inputs from the test suite; property tier: parameter `path` is annotated `Path` — outside the synthesizable primitive/collection set

· 38 more finding(s) on lines outside this diff (see the check run).

ayushcodes10 and others added 3 commits September 17, 2026 21:11
A reviewer on this PR pointed out that when a JS symbol's id collides
with an existing PHP node (same name, one file, resolved in favor of
the PHP node), the merge still appended every JS edge unconditionally.
An edge meant for the discarded JS symbol silently attached to the
unrelated PHP node sharing its id instead of vanishing with it. The
file node's own always on collision (both passes deliberately reuse
its id) is excluded from this so an unrelated JS symbol's own edges
sourced from the file node are unaffected.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Covers a same name PHP/JS function pair in one file: exactly one
surviving node, the JS call to the colliding name absent as a calls
edge entirely rather than misattached, and the unrelated JS symbol's
own contains edge from the shared file node still present.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@ayushcodes10

Copy link
Copy Markdown
Contributor Author

Good catch — fixed. When a JS symbol's id collided with an existing PHP node (same name, resolved in favor of the PHP node), every JS edge was still being merged in unconditionally, so an edge meant for the discarded JS symbol silently attached to the unrelated PHP node sharing its id. Now edges touching a dropped JS node's id are skipped too, with the file node's own always on collision (both passes deliberately reuse its id, by design) excluded from that so unrelated JS symbols' edges from the shared file node are unaffected. Added a regression test with a same name PHP/JS pair.

@graphify-labs graphify-labs Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Graphify reviewed this change.

Worth a look — the grounded gate found no coupling regressions or blocking issues, but 2 advisory finding(s) below merit a look before merge.

Formal verification. No changes could be formally verified in this run.


Graphify review — findings

Indexes JavaScript declared in inline <script> blocks of .php files: extract_php now runs a second JS pass over the source with everything outside script bodies blanked to spaces (line numbers preserved) via _php_mask_to_script_blocks, and merges those symbols onto the same file node, skipping the pass entirely when no <script> is present. On a same-name PHP/JS id collision it keeps the PHP node and drops every edge touching the discarded JS id, so no stray edge misattaches to the unrelated PHP node — while preserving the shared file node's own contains edges to surviving JS symbols. A <script src=...> with no body masks to empty JS and contributes nothing.

Worth a look

  • Bare except swallows all errors, hiding JS extraction failuresgraphify/extract.py · Escalate · medium
    • agreed by 2 of 2 members but NOT verified (no proof, no reproducing execution) — consensus is not a verdict; needs human review
  • PHP extractor treats script tags in PHP comments as executable JSgraphify/extract.py:2550 · Escalate · medium
    • agreed by 2 of 2 members but NOT verified (no proof, no reproducing execution) — consensus is not a verdict; needs human review
Analysis details — impact, health, verification

Impact & health

Graphify review

Impact — 2570 functions depend on the 1016 functions this change touches.

Health — this change adds coupling hotspots:

  • new: extract() — 645 callers, 45 callees
  • new: _rebuild_code() — 129 callers, 54 callees
  • new: extract_js() — 85 callers, 4 callees
  • new: extract_xaml() — 19 callers, 17 callees
  • new: dispatch_command() — 2 callers, 125 callees
  • new: _get_extractor() — 26 callers, 6 callees
  • new: run_pipeline() — 8 callers, 13 callees
  • new: collect_files() — 17 callers, 6 callees
  • …and 30 more — each is listed as a finding

Verification — 2570 functions in the blast radius were not formally verified this run (proofs are advisory here).

Gate & verification

graphify gate

PASS — objectively clean (no health regressions, tests not run — proofs not run this pass (advisory)). Grounded, not self-assessed.

Advisory (not blocking):

  • verification_scope: 2395 function(s) in the blast radius were not formally verified this run

Test selection

Test selection

286 of 286 test file(s) selected (100%) via static blast radius.

Escalated to a full run for safety — the selection is not trustworthy on its own (see below). CI should run the whole suite.

  • tests/test_affected_cli.py — full-run-safety
  • tests/test_affected_member_seed.py — full-run-safety
  • tests/test_agents_platform.py — full-run-safety
  • tests/test_analyze.py — full-run-safety
  • tests/test_anthropic_custom_endpoint.py — full-run-safety
  • tests/test_antigravity_install.py — full-run-safety
  • tests/test_apm_fallback_version.py — full-run-safety
  • tests/test_architecture_doc.py — full-run-safety
  • tests/test_astro_extraction.py — impact, full-run-safety
  • tests/test_astro_import_ids.py — impact, full-run-safety
  • tests/test_atomic_canvas_export.py — full-run-safety
  • tests/test_atomic_version_stamp.py — full-run-safety
  • tests/test_atomic_writes.py — full-run-safety
  • tests/test_backend_env_isolation.py — full-run-safety
  • tests/test_backend_extras.py — full-run-safety
  • tests/test_benchmark.py — full-run-safety
  • tests/test_benchmark_raw_graph.py — full-run-safety
  • tests/test_build.py — impact, full-run-safety
  • tests/test_build_merge_dedup_scope.py — full-run-safety
  • tests/test_build_merge_hyperedges_and_prune.py — full-run-safety
  • tests/test_build_merge_shrink_guard.py — full-run-safety
  • tests/test_builtin_global_type_refs.py — impact, full-run-safety
  • tests/test_cache.py — full-run-safety
  • tests/test_callflow_html.py — full-run-safety
  • tests/test_cargo_introspect.py — full-run-safety
  • tests/test_carried_hyperedge_remap.py — full-run-safety
  • tests/test_case_sensitive_resolution.py — impact, full-run-safety
  • tests/test_charmap_encoding.py — full-run-safety
  • tests/test_chunking.py — full-run-safety
  • tests/test_cjs_module_extension.py — impact, full-run-safety
  • tests/test_claude_cli_backend.py — full-run-safety
  • tests/test_claude_md.py — full-run-safety
  • tests/test_cli_broken_pipe.py — full-run-safety
  • tests/test_cli_export.py — full-run-safety
  • tests/test_cli_help.py — full-run-safety
  • tests/test_cluster.py — full-run-safety
  • tests/test_codebuddy.py — full-run-safety
  • tests/test_community_hub_labels.py — full-run-safety
  • tests/test_community_labels_skill.py — full-run-safety
  • tests/test_confidence.py — full-run-safety
  • tests/test_corrupt_graph_json.py — full-run-safety
  • tests/test_cpp_nested_and_cli.py — impact, full-run-safety
  • tests/test_cpp_objc_cross_file_calls.py — impact, full-run-safety
  • tests/test_cpp_preprocess.py — full-run-safety
  • tests/test_cross_extension_reexport_self_cycle.py — impact, full-run-safety
  • tests/test_cross_language_call_resolution.py — impact, full-run-safety
  • tests/test_cross_repo_external_call_guards.py — impact, full-run-safety
  • tests/test_cross_repo_member_calls.py — impact, full-run-safety
  • tests/test_cross_repo_shared_types.py — full-run-safety
  • tests/test_csharp_call_site_generic_args.py — impact, full-run-safety
  • … and 236 more

non-code file(s) changed (CHANGELOG.md) → running the full suite for safety (a code graph can't see config/fixture/data deps)

changed code file(s) with no mapped test (CHANGELOG.md) — a coverage gap or a missing link — running the full suite rather than only the selected tests

Selection is safe under the controlled-regression assumption; always-run tests + a periodic full run are the backstops. Advisory — it never changes the check verdict.

Formal verification

Could not verify: Could not verify extract\_php.

The verifier did not have enough to check extract\_php, so it is saying so rather than guessing. No false assurance is the whole point.

Guarantee: No guarantee either way, this is an honest abstention, not a pass.

Note: Reason: no capturable inputs from the test suite; property tier: parameter `path` is annotated `Path` — outside the synthesizable primitive/collection set

· 38 more finding(s) on lines outside this diff (see the check run).

@ayushcodes10

Copy link
Copy Markdown
Contributor Author

Re: the two remaining advisory findings.

"Bare except swallows all errors, hiding JS extraction failures": checked, and this matches an existing, deliberate convention already in this codebase — extract_vue's own secondary best effort pass (the dynamic import rescue) uses the identical bare except Exception: pass shape, so a failure in the optional JS side pass degrades to PHP only extraction rather than losing the whole file's result. Not changing this to stay consistent with that established pattern.

"PHP extractor treats script tags in PHP comments as executable JS" (graphify/extract.py:2550): correct as an observation, but the same accepted limitation the masking approach already carries elsewhere — the regex based Vue script masker this PR explicitly mirrors has the identical gap (it can't distinguish a real script tag from one mentioned inside a comment without a full parse). Given it mirrors an existing, accepted tradeoff rather than introducing a new one, leaving it as is.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Inline <script> JS in .php files is not indexed (extract_php skips embedded script blocks, unlike extract_svelte/astro/vue)

1 participant