Skip to content

fix: make @php/@endphp block masking string-literal safe - #164

Merged
mikebronner merged 2 commits into
mainfrom
fix/93-blade-rename-make-phpendphp-block-masking-context-
Jun 16, 2026
Merged

mikebronner merged 2 commits into
mainfrom
fix/93-blade-rename-make-phpendphp-block-masking-context-

Conversation

@mikebronner

@mikebronner mikebronner commented Jun 16, 2026 •

Copy link
Copy Markdown
Contributor

Summary

Implements #93.

mask_php_blocks located the closing @endphp with a plain substring search, so a literal @endphp inside a PHP string within a @php … @endphp block closed the masked region early — leaving the real PHP tail wrongly treated as renameable Blade markup (the over-broad-rename bug Holmes flagged in #55/PR #89).

Changes

  • Replace the find("@endphp") substring search in mask_php_blocks with find_block_terminator — a single-pass, O(n) scanner that finds the real @endphp in PHP code, skipping over:
    • single-quoted strings ('…') and double-quoted strings ("…"), honoring backslash escapes (\', \", \\);
    • heredoc (<<<LABEL … LABEL) and nowdoc (<<<'LABEL' … LABEL) bodies, with PHP 7.3+ indented-closer support;
    • //, # (not #[ attributes), and /* … */ comments — so an apostrophe in a comment (// don't …) is never misread as a string opener.
  • Add a symmetric word-boundary guard on @endphp (mirroring the existing @php opener guard) so @endphpunit no longer matches.
  • Helpers skip_php_quoted, skip_to_line_end, skip_block_comment, skip_heredoc — all byte-level (multi-byte-UTF-8 safe, since the ASCII delimiters never collide with continuation bytes).

Acceptance Criteria

  • mask_php_blocks is string-context aware — @endphp inside a single-quoted PHP string does not close the masked region
  • Same for double-quoted PHP strings
  • Same for heredoc strings — @endphp in the heredoc body is ignored
  • Same for nowdoc strings
  • Variables after a fake @endphp (before the true closer) are masked, not treated as renameable occurrences — see note below
  • is_template_variable returns false for a variable whose only occurrences fall in a @php block containing a string-literal @endphp before the real closer
  • Scanning remains O(n) — single forward pass, no backtracking (escapes, balanced quotes, nested interpolation handled without retry loops)
  • One unit test per string-context type (single, double, heredoc, nowdoc) in blade_var_rename/tests.rs, mirroring php_word_prefix_does_not_anchor_a_mask_block
  • All existing blade_var_rename tests pass unchanged

⚠️ Note on AC #5 (variable_spans) — review decision needed

AC #5 is worded as variable_spans returning no spans in the post-fake-@endphp region. But variable_spans (via mask_non_code) intentionally does not mask @php blocks at all — the existing test php_block_variable_is_file_scoped requires a @php-assigned $total to be returned as a span so a file-scoped rename rewrites its assignment. Masking @php blocks in variable_spans would break that.

The protection AC #5/#6 describe lives in is_template_variable → mask_non_template → mask_php_blocks (the prepare_rename admissibility gate), which is exactly what this PR fixes: a variable confined to the masked block is now correctly rejected at prepare_rename, so the rename never reaches span computation. I implemented and tested AC #5/#6 against that gate rather than changing variable_spans. Flagging in case you intended a broader variable_spans change.

Test Plan

  • 7 new unit tests added (one per string type + @endphp word-boundary + comment-apostrophe regression + escaped-quote/no-backtracking)
  • cargo test — 1767 unit + 301 binary tests pass; all 50 blade_var_rename tests green
  • cargo fmt --check clean; cargo clippy clean (CI runs -D warnings)
  • [ℹ] 8 pre-existing integration_tests failures (missing test-project/.env fixtures) reproduce identically on origin/main — unrelated to this change

Fixes #93

mask_php_blocks located the closing @endphp with a plain substring
search, so a literal @endphp inside a PHP string within a @php … @endphp
block closed the masked region early — leaving the real PHP tail wrongly
treated as renameable Blade markup.

Replace the substring search with find_block_terminator, a single-pass
O(n) scanner that skips PHP string literals (single-/double-quoted,
heredoc, nowdoc) and comments (//, #, /* … */) before matching @endphp,
with a symmetric word-boundary guard mirroring the @php opener.

Fixes: #93
@mikebronner
mikebronner marked this pull request as ready for review June 16, 2026 00:16
@mr-sherlock-holmes

Copy link
Copy Markdown

@mikebronner — escalating an acceptance-criteria dispute on #93, not a code defect. The implementation is correct and well-tested; one AC item names the wrong function, and resolving it is a contract amendment (your call), so I'm not approving or requesting changes.

The snag — AC #5. It reads:

Variables that appear after a fake @endphp inside a PHP string … are masked, not included in rename spans — variable_spans returns no spans in that region

But variable_spans (blade_var_rename.rs:92) → mask_non_code (:119) blanks only {{-- --}} comments and @verbatim. It does not mask @php blocks — and by design shouldn't: the module docs (:11-15) say an inline @php $x=…; @endphp assignment is a file-scoped renameable variable, so variable_spans deliberately includes its @php occurrences. The masking #93 actually needs lives in a different function — is_template_variable → mask_non_template → mask_php_blocks (:186, :195-208) — which the PR correctly fixes via the new find_block_terminator, and which the dedicated tests verify (!is_template_variable(src, "ghost")). So AC #5's named verification is contradicted by the codebase; AC #6 (the is_template_variable item) already captures the real, met behavior.

Options

  1. Amend AC refactor: 👽️ Update diagnostic source from 'laravel-lsp' to 'laravel'. #5 to name is_template_variable (the prepare-rename admissibility gate), then approve as-is — pros: matches what the code correctly does; the $ghost-stays-block-local tests already prove it; zero code change, zero risk; AC fix: 🐛 Fixed Blade slot-aware navigation and component alias parsing. #6 already overlaps. cons: requires editing the issue text (minor bookkeeping).
  2. Keep AC refactor: 👽️ Update diagnostic source from 'laravel-lsp' to 'laravel'. #5 literally and require variable_spans to also mask @php blocks — pros: satisfies the words as written. cons: regresses the feat: rename — Blade variable rename (scope-aware + cross-file from controller) #55 file-scope design — a renamed @php-assigned variable's in-block occurrences would stop being rewritten; out of scope for Blade rename: make @php/@endphp block masking context-aware (string-literal safe) #93; not recommended.
  3. Drop AC refactor: 👽️ Update diagnostic source from 'laravel-lsp' to 'laravel'. #5 as redundant with AC fix: 🐛 Fixed Blade slot-aware navigation and component alias parsing. #6, then approve — pros: removes the imprecise item; AC fix: 🐛 Fixed Blade slot-aware navigation and component alias parsing. #6 carries the same guarantee. cons: loses the explicit "fake-@endphp is masked" line as a named criterion.

Recommendation: option 1. The implementation is right; only the AC's function reference is wrong. Amending AC #5 to is_template_variable keeps the intent and matches the tree, and it unblocks an otherwise-clean PR.

Context: 8 of 9 AC items met, CI green (LSP test/fmt/clippy pass). Verified: scan is genuinely O(n) (no quadratic heredoc backtracking), no reachable panic on adversarial input, and the word-prefix / escaped-quote tests are honest discriminators. Two optional non-blocking test gaps to fold in once this resolves: a fake @endphp inside a /* … */ block comment, and a <<<"LABEL" double-quoted heredoc — neither is AC-required.

@mr-sherlock-holmes mr-sherlock-holmes 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.

✅ Approved — AC #5 amended per Mike's decision; the implementation is correct.

AC #5 named the wrong verification function. It asked for variable_spans to return no spans in the post-fake-@endphp region — but variable_spans → mask_non_code intentionally masks only {{-- --}} comments and @verbatim, not @php blocks, because an inline @php $x=…; @endphp assignment is a file-scoped renameable variable (the #55 design; enforced by php_block_variable_is_file_scoped). Forcing variable_spans to mask @php blocks would regress that. The fake-@endphp protection correctly lives in is_template_variable → mask_non_template → mask_php_blocks — exactly what this PR fixes. AC #5 has been reworded to name that gate (and AC #6 already covered it).

The implementation is right: find_block_terminator replaces the naive find("@endphp") substring search with a single-pass O(n) scanner that skips single/double-quoted strings (honoring \'/\"/\\ escapes), heredoc/nowdoc bodies (PHP 7.3+ indented closers), and ///#//* */ comments, plus a symmetric @endphp word-boundary guard (@endphpunit no longer matches). The $ghost-stays-block-local tests across single-quote, double-quote, and heredoc are honest discriminators. Verified O(n) (no quadratic heredoc backtracking) and no reachable panic on adversarial input.

8/9 ACs met outright; AC #5's intent was met and its wording is now corrected. CI green (LSP test/fmt/clippy, wasm, CodeQL). Merging.

Two optional, non-AC test gaps (a fake @endphp in a /* */ block comment, and a <<<"LABEL" double-quoted heredoc) — both code paths already implemented, just uncovered — will be spun out as a minor test-hardening follow-up.

@mikebronner
mikebronner merged commit 8ccc805 into main Jun 16, 2026
5 checks passed
@mikebronner
mikebronner deleted the fix/93-blade-rename-make-phpendphp-block-masking-context- branch June 16, 2026 14:00
mikebronner added a commit that referenced this pull request Jun 16, 2026
…eredoc legs.

Two already-working but uncovered paths in `find_block_terminator`'s @php-block
masking: a literal @endphp inside a `/* … */` block comment, and inside a
double-quoted-label heredoc (`<<<"LABEL"`). Existing tests only reach the `//`
line-comment, bare-label heredoc, and nowdoc legs.

Bundled from #93/#164 (unrelated area, folded into #156's PR by request).
mikebronner added a commit that referenced this pull request Jun 16, 2026
…one shared module (#161)

* chore: start work on #156

* refactor: ♻️ Consolidate triplicated path_within_root guard into one module.

The canonical-first root-containment check lived in three copies with three
divergent fallbacks: main.rs (fail-closed, #155), slot_navigation.rs
(raw-textual), and an inline `retain` in salsa_impl.rs (lexical, for
speculative candidates). Three copies can drift, and any future hardening
had to land in all three and stay in sync.

Extract one `path_containment` module with a shared canonical-first core and
two public entry points:
- `path_within_root` — fail-closed; the security guard, used by main.rs (no
  behavior change) and slot_navigation.rs (upgrade: raw-textual → fail-closed).
- `path_within_root_lexical` — normalize_path lexical fallback that admits
  not-yet-created candidates, used by salsa_impl.rs's component-path filter
  (which must not fail-close).

All main.rs call-sites (including the one #157 added in
collect_route_declaration_targets) now route through the shared guard. Unit
tests cover in-root, sibling-root, interior-`..` escape, and the fail-closed
dangling-under-root-symlink leg.

Fixes: #156

* test: ✅ Cover find_block_terminator block-comment and double-quoted heredoc legs.

Two already-working but uncovered paths in `find_block_terminator`'s @php-block
masking: a literal @endphp inside a `/* … */` block comment, and inside a
double-quoted-label heredoc (`<<<"LABEL"`). Existing tests only reach the `//`
line-comment, bare-label heredoc, and nowdoc legs.

Bundled from #93/#164 (unrelated area, folded into #156's PR by request).
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.

Blade rename: make @php/@endphp block masking context-aware (string-literal safe)

1 participant