Skip to content

fix(compiler-core): avoid codegen crash on template with invalid structural directive#14955

Open
sarathfrancis90 wants to merge 1 commit into
vuejs:mainfrom
sarathfrancis90:fix/codegen-orphan-template-crash
Open

fix(compiler-core): avoid codegen crash on template with invalid structural directive#14955
sarathfrancis90 wants to merge 1 commit into
vuejs:mainfrom
sarathfrancis90:fix/codegen-orphan-template-crash

Conversation

@sarathfrancis90

@sarathfrancis90 sarathfrancis90 commented Jun 12, 2026

Copy link
Copy Markdown
Contributor

While fuzzing baseCompile with a non-throwing onError, I hit a raw internal crash on a few valid-to-parse templates.

A <template> that carries a structural directive which is invalid in its position — a misplaced v-slot/#slot, or a v-else/v-else-if with no adjacent v-if — has its structural transform report a proper compiler error and bail, but the element node is left in the tree without a codegenNode. The default onError throws, so this never surfaces; but onError is a public option and tooling commonly supplies a collecting (non-throwing) handler. In that case the orphaned node reaches codegen, and genNode throws Error: Codegen node is missing for element/if/for node (a TypeError in production builds where the assert is stripped) instead of degrading after the already-reported error.

Minimal repro:

import { baseCompile } from '@vue/compiler-core'
baseCompile('<template v-else>x</template>', { onError: () => {} }) // throws "Codegen node is missing..."

Other triggers: <template v-else-if="a">x</template>, <template #slot>x</template>, <div><template #slot>x</template></div>.

The fix is in genNode: when an element/if/for node has no codegenNode (which only happens after such an error was reported and swallowed), emit a null placeholder rather than throwing. The underlying compiler error is still reported through onError.

Added integration tests in compile.spec.ts covering the trigger cases and asserting the underlying error is still reported. Full compiler-core + compiler-dom suites, typecheck and lint pass locally.

Summary by CodeRabbit

  • Bug Fixes

    • Improved error handling in template compilation. The compiler now gracefully reports errors for invalid structural directives instead of crashing during code generation.
  • Tests

    • Added integration tests verifying proper error reporting for invalid template directives.

…ctural directive

A <template> that carries a structural directive which is invalid in its
position (a misplaced v-slot, or a v-else/v-else-if with no adjacent v-if)
has its directive transform report a compiler error and bail, leaving the
node in the tree without a codegenNode. With a non-throwing onError the
node still reaches codegen, which threw an internal "Codegen node is
missing" error (and a TypeError in production builds).

Emit a null placeholder for an element/if/for node whose codegenNode is
absent so the compiler degrades gracefully after the reported error
instead of throwing a raw JS error.
@coderabbitai

coderabbitai Bot commented Jun 12, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

The PR fixes a code-generation crash that occurs when Vue templates use invalid structural directive positioning (e.g., v-else without an adjacent v-if). The compiler now emits a null placeholder when a node's codegenNode is missing and reports the error via callback instead of crashing. Integration tests verify the graceful failure and error reporting.

Changes

Invalid Structural Directive Handling

Layer / File(s) Summary
Codegen null-check guard
packages/compiler-core/src/codegen.ts
genNode checks for node.codegenNode == null in the ELEMENT/IF/FOR branch and emits null as a placeholder instead of asserting and crashing.
Integration tests for structural directive errors
packages/compiler-core/__tests__/compile.spec.ts
ErrorCodes is imported and a new parameterized test suite verifies that invalid v-else positioning does not crash compilation, returns code with return statements, and reports ErrorCodes.X_V_ELSE_NO_ADJACENT_IF via onError callback.

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~12 minutes

Possibly related PRs

  • vuejs/core#13699: Adds the integration test expecting ErrorCodes.X_V_ELSE_NO_ADJACENT_IF and updates codegen.ts to avoid crashing when invalid structural directive transforms leave node.codegenNode unset.

Suggested labels

ready to merge, scope: compiler, :hammer: p3-minor-bug

Suggested reviewers

  • KazariEX
  • sxzz

Poem

🐰 A template with v-else all alone,
Once crashed the compiler, its code turned to stone.
But now with a null check, we're graceful and kind,
The error flows gently—no crashes we'll find! ✨

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately and specifically describes the main fix: preventing codegen crashes when templates contain invalid structural directives, which matches the core change in codegen.ts and the test coverage in compile.spec.ts.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Warning

There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure.

🔧 ESLint

If the error stems from missing dependencies, add them to the package.json file. For unrecoverable errors (e.g., due to private dependencies), disable the tool in the CodeRabbit configuration.

ESLint install timed out. The project may have too many dependencies for the sandbox.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@github-actions

Copy link
Copy Markdown

Size Report

Bundles

File Size Gzip Brotli
runtime-dom.global.prod.js 106 kB 40.2 kB 36.1 kB
vue.global.prod.js 165 kB (+76 B) 60.2 kB (+16 B) 53.5 kB (+21 B)

Usages

Name Size Gzip Brotli
createApp (CAPI only) 48.8 kB 19 kB 17.4 kB
createApp 56.9 kB 22 kB 20.1 kB
createSSRApp 61.2 kB 23.8 kB 21.7 kB
defineCustomElement 63.1 kB 23.9 kB 21.8 kB
overall 71.7 kB 27.4 kB 25 kB

@pkg-pr-new

pkg-pr-new Bot commented Jun 12, 2026

Copy link
Copy Markdown

Open in StackBlitz

@vue/compiler-core

pnpm add https://pkg.pr.new/@vue/compiler-core@14955
npm i https://pkg.pr.new/@vue/compiler-core@14955
yarn add https://pkg.pr.new/@vue/compiler-core@14955.tgz

@vue/compiler-dom

pnpm add https://pkg.pr.new/@vue/compiler-dom@14955
npm i https://pkg.pr.new/@vue/compiler-dom@14955
yarn add https://pkg.pr.new/@vue/compiler-dom@14955.tgz

@vue/compiler-sfc

pnpm add https://pkg.pr.new/@vue/compiler-sfc@14955
npm i https://pkg.pr.new/@vue/compiler-sfc@14955
yarn add https://pkg.pr.new/@vue/compiler-sfc@14955.tgz

@vue/compiler-ssr

pnpm add https://pkg.pr.new/@vue/compiler-ssr@14955
npm i https://pkg.pr.new/@vue/compiler-ssr@14955
yarn add https://pkg.pr.new/@vue/compiler-ssr@14955.tgz

@vue/reactivity

pnpm add https://pkg.pr.new/@vue/reactivity@14955
npm i https://pkg.pr.new/@vue/reactivity@14955
yarn add https://pkg.pr.new/@vue/reactivity@14955.tgz

@vue/runtime-core

pnpm add https://pkg.pr.new/@vue/runtime-core@14955
npm i https://pkg.pr.new/@vue/runtime-core@14955
yarn add https://pkg.pr.new/@vue/runtime-core@14955.tgz

@vue/runtime-dom

pnpm add https://pkg.pr.new/@vue/runtime-dom@14955
npm i https://pkg.pr.new/@vue/runtime-dom@14955
yarn add https://pkg.pr.new/@vue/runtime-dom@14955.tgz

@vue/server-renderer

pnpm add https://pkg.pr.new/@vue/server-renderer@14955
npm i https://pkg.pr.new/@vue/server-renderer@14955
yarn add https://pkg.pr.new/@vue/server-renderer@14955.tgz

@vue/shared

pnpm add https://pkg.pr.new/@vue/shared@14955
npm i https://pkg.pr.new/@vue/shared@14955
yarn add https://pkg.pr.new/@vue/shared@14955.tgz

vue

pnpm add https://pkg.pr.new/vue@14955
npm i https://pkg.pr.new/vue@14955
yarn add https://pkg.pr.new/vue@14955.tgz

@vue/compat

pnpm add https://pkg.pr.new/@vue/compat@14955
npm i https://pkg.pr.new/@vue/compat@14955
yarn add https://pkg.pr.new/@vue/compat@14955.tgz

commit: 211db37

@coderabbitai coderabbitai 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.

🧹 Nitpick comments (2)
packages/compiler-core/__tests__/compile.spec.ts (2)

286-294: ⚡ Quick win

Verify error codes for all test cases, not just v-else.

The test suite includes four different invalid structural directive patterns (v-else, v-else-if, #slot standalone, #slot nested), but only the v-else case is verified to report the correct error code (X_V_ELSE_NO_ADJACENT_IF). Each pattern may report a different error code (e.g., v-else-if might report X_V_ELSE_NO_ADJACENT_IF, but #slot cases would report a different error code for misplaced slots).

Consider expanding this test to verify each case reports its appropriate error code.

💡 Example expansion
-    test('still reports the underlying error', () => {
+    test.each([
+      { source: `<template v-else>x</template>`, code: ErrorCodes.X_V_ELSE_NO_ADJACENT_IF },
+      { source: `<template v-else-if="a">x</template>`, code: ErrorCodes.X_V_ELSE_NO_ADJACENT_IF },
+      { source: `<template `#slot`>x</template>`, code: ErrorCodes.X_V_SLOT_MISPLACED },
+      { source: `<div><template `#slot`>x</template></div>`, code: ErrorCodes.X_V_SLOT_MISPLACED },
+    ])('reports correct error for $source', ({ source, code: expectedCode }) => {
       const onError = vi.fn()
-      expect(() =>
-        compile(`<template v-else>x</template>`, { onError }),
-      ).not.toThrow()
+      expect(() => compile(source, { onError })).not.toThrow()
       expect(onError).toHaveBeenCalledWith(
-        expect.objectContaining({ code: ErrorCodes.X_V_ELSE_NO_ADJACENT_IF }),
+        expect.objectContaining({ code: expectedCode }),
       )
     })

Note: Verify the actual error code names for slot-related errors in the ErrorCodes enum.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/compiler-core/__tests__/compile.spec.ts` around lines 286 - 294, The
test currently only asserts the error code for the v-else case; update the test
around the compile(...) calls to verify each invalid structural directive
reports its specific error code by invoking compile with the four inputs and
asserting onError was called with expect.objectContaining({ code:
ErrorCodes.X_V_ELSE_NO_ADJACENT_IF }) for the v-else and v-else-if cases (or the
correct enum if v-else-if differs), and the appropriate slot-related ErrorCodes
for the standalone and nested `#slot` cases (use the ErrorCodes enum names for
misplaced slot errors); keep using the existing onError spy and expect(...)
.not.toThrow() pattern so each compile invocation checks the corresponding
ErrorCodes entry.

272-284: ⚡ Quick win

Consider strengthening the generated code assertion.

The assertion expect(code).toContain('return') only verifies that the string 'return' appears somewhere in the generated code. This is a weak check that could pass even if the code structure is broken.

Consider adding assertions that verify the generated code is syntactically valid JavaScript or has the expected function structure.

💡 Example of stronger assertion
       expect(() => {
         code = compile(source, { onError }).code
       }).not.toThrow()
-      expect(code).toContain(`return`)
+      expect(code).toContain(`return`)
+      expect(code).toMatch(/function\s+render\s*\([^)]*\)\s*\{[\s\S]*return[\s\S]*\}/)

This regex verifies the code contains a render function with a return statement inside it, providing stronger validation of the generated structure.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/compiler-core/__tests__/compile.spec.ts` around lines 272 - 284, The
current test uses a weak assertion expect(code).toContain('return'); update the
assertion to validate actual generated function structure and syntactic
validity: after obtaining code from compile(source, { onError }), either (a)
assert with a regex that the render function exists and contains a return (e.g.
match /function\s+render\([^)]*\)\s*{[\s\S]*return[\s\S]*}/ or the emitted
arrow/const render pattern used by compile), or (b) execute a syntactic check by
wrapping the emitted code in new Function(code) and asserting it does not throw;
apply this change in the test.each block (referencing compile, onError and code)
so the test verifies both structure and valid JS instead of just the substring
"return".
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@packages/compiler-core/__tests__/compile.spec.ts`:
- Around line 286-294: The test currently only asserts the error code for the
v-else case; update the test around the compile(...) calls to verify each
invalid structural directive reports its specific error code by invoking compile
with the four inputs and asserting onError was called with
expect.objectContaining({ code: ErrorCodes.X_V_ELSE_NO_ADJACENT_IF }) for the
v-else and v-else-if cases (or the correct enum if v-else-if differs), and the
appropriate slot-related ErrorCodes for the standalone and nested `#slot` cases
(use the ErrorCodes enum names for misplaced slot errors); keep using the
existing onError spy and expect(...) .not.toThrow() pattern so each compile
invocation checks the corresponding ErrorCodes entry.
- Around line 272-284: The current test uses a weak assertion
expect(code).toContain('return'); update the assertion to validate actual
generated function structure and syntactic validity: after obtaining code from
compile(source, { onError }), either (a) assert with a regex that the render
function exists and contains a return (e.g. match
/function\s+render\([^)]*\)\s*{[\s\S]*return[\s\S]*}/ or the emitted arrow/const
render pattern used by compile), or (b) execute a syntactic check by wrapping
the emitted code in new Function(code) and asserting it does not throw; apply
this change in the test.each block (referencing compile, onError and code) so
the test verifies both structure and valid JS instead of just the substring
"return".

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 02d5878f-6b2c-4470-a093-fd08c1141330

📥 Commits

Reviewing files that changed from the base of the PR and between 478e3e8 and 211db37.

📒 Files selected for processing (2)
  • packages/compiler-core/__tests__/compile.spec.ts
  • packages/compiler-core/src/codegen.ts

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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants