oxc-project/oxc · error

[HIRBuilder] expected block {:?} to exist

Error message

[HIRBuilder] expected block {:?} to exist

What it means

Diagnostic from oxlint's vue/valid-define-emits rule (crates/oxc_linter/src/rules/vue/valid_define_emits.rs). `defineEmits` is a compiler macro and may be called exactly once per `<script setup>` block; a second call is reported with both call sites labeled. Only the first call's result is wired to the component by the compiler, so events declared in later calls silently don't exist.

Source

Thrown at crates/oxc_react_compiler/src/react_compiler_lowering/hir_builder.rs:935

        visited: &mut FxHashSet<BlockId>,
        used: &mut FxHashSet<BlockId>,
        used_fallthroughs: &mut FxHashSet<BlockId>,
        postorder: &mut Vec<BlockId>,
    ) {
        let was_used = used.contains(&block_id);
        let was_visited = visited.contains(&block_id);
        visited.insert(block_id);
        if is_used {
            used.insert(block_id);
        }
        if was_visited && (was_used || !is_used) {
            return;
        }

        let block = hir
            .blocks
            .get(&block_id)
            .unwrap_or_else(|| panic!("[HIRBuilder] expected block {:?} to exist", block_id));

        // Visit successors in reverse order so that when we reverse the
        // postorder list, sibling edges come out in program order.
        let mut successors = each_terminal_successor(&block.terminal);
        successors.reverse();

        let fallthrough = terminal_fallthrough(&block.terminal);

        // Visit fallthrough first (marking as not-yet-used) to ensure its
        // block ID is emitted in the correct position.
        if let Some(ft) = fallthrough {
            if is_used {
                used_fallthroughs.insert(ft);
            }
            visit(hir, ft, false, visited, used, used_fallthroughs, postorder);
        }
        for successor in successors {
            visit(hir, successor, is_used, visited, used, used_fallthroughs, postorder);

View on GitHub (pinned to e1e7af627c)

Solutions

  1. Merge all events into a single call: `defineEmits({ notify: null, submit: null })` (or one union of call signatures in TS).
  2. Delete the leftover call after merging.
  3. Assign once — `const emit = defineEmits([...])` — and reuse `emit` everywhere.

Example fix

// before
const emitA = defineEmits(['notify']);
const emitB = defineEmits(['submit']);

// after
const emit = defineEmits(['notify', 'submit']);
Defensive patterns

Strategy: validation

Validate before calling

// only one defineEmits call per <script setup>
const count = (setupSource.match(/\bdefineEmits\s*\(?/g) || []).length;
if (count > 1) throw new Error(`defineEmits called ${count} times; merge the declarations`);

Prevention

When it happens

Trigger: Two or more `defineEmits(...)` calls in the same `<script setup>` block — e.g. `defineEmits({ notify: null }); defineEmits({ submit: null });`. The rule records the first call's span and reports every subsequent call alongside it.

Common situations: Merging components or copy-pasting blocks; appending a new `defineEmits` when adding events instead of extending the existing call.

Related errors


AI-assisted analysis of oxc-project/oxc@e1e7af627c (2026-08-20). Data as JSON: /api/errors/c98381a4cfe5b1e2. Report an issue: GitHub.