oxc-project/oxc · error
function and catch parameter scopes always have a parent
Error message
function and catch parameter scopes always have a parent
What it means
This is an internal invariant panic in oxc_semantic's scope builder. `resolve_references_for_current_scope` early-resolves references collected while visiting a function or catch parameter scope by looking up that scope's parent via `scope_parent_id(...).expect(...)`. The library asserts the scope current at that point must be a function or catch parameter scope, which by construction is always nested inside another scope, so the parent must exist. If the parent is `None`, the builder entered the function with a malformed/missing scope tree, so it panics instead of silently mis-resolving references.
Source
Thrown at crates/oxc_semantic/src/builder.rs:751
/// so a nested function parameter can still resolve to a later declaration in an enclosing
/// function body while skipping declarations in its own body.
///
/// This is a workaround until function bodies have separate scopes:
/// <https://github.com/oxc-project/backlog/issues/176>.
///
/// Resolved references are removed. Unresolved references stay in the flat
/// list for later resolution by `resolve_all_references` (which handles
/// forward references to declarations not yet visited).
fn resolve_references_for_current_scope(&mut self, unresolved_start: usize) {
if self.unresolved_references.len() == unresolved_start {
return;
}
let current_scope_id = self.current_scope_id;
let parent_scope_id = self
.scoping
.scope_parent_id(current_scope_id)
.expect("function and catch parameter scopes always have a parent");
// Take the list out of `self` while resolving, so the closure can call `&mut self`
// methods. Resolution never pushes new unresolved references, so nothing is lost.
let mut unresolved_references = mem::take(&mut self.unresolved_references);
unresolved_references.retain_from(unresolved_start, |unresolved| {
// Parameter decorators are visited in an outer class scope. Leave those references
// for final resolution because the current function is not on their scope chain.
let lookup_scope_id = unresolved.lookup_scope_id;
if lookup_scope_id != current_scope_id
&& !self.scoping.scope_is_descendant_of(lookup_scope_id, current_scope_id)
{
return true;
}
if self.walk_up_resolve_reference(*unresolved, current_scope_id) {
return false;
}
// Skip this function body during final resolution. An enclosing parameter resolution
// may still resolve the reference before advancing the boundary again.View on GitHub (pinned to a3d33dda7c)
Solutions
- Ensure every function/arrow/catch visit is wrapped in matching enter_scope/leave_scope calls so the parameter scope is a child of an enclosing scope
- Verify you are not invoking the builder's internal visit methods on a manually built scope tree; use the standard `SemanticBuilder::build` entry point
- Update oxc to the latest version — this invariant was added while function-body scoping is being reworked (oxc-project/backlog#176), and internal scope construction has changed across versions
- If you hit this on a reproducible input, file an oxc issue with the snippet; it indicates a scope-tree construction bug, not a recoverable error
Example fix
// before (external code driving the builder) let mut builder = SemanticBuilder::new(); builder.visit_program_without_scoping(&program); // current_scope_id stays root // after let semantic = SemanticBuilder::new().build(&program); // standard entry, scopes pushed correctly
Defensive patterns
Strategy: validation
Validate before calling
// Before resolving, confirm the scope has a parent and unresolved refs exist
if builder.unresolved_references.len() == unresolved_start {
return; // nothing to resolve, no panic path
}
debug_assert!(
scoping.scope_parent_id(current_scope_id).is_some(),
"function/catch parameter scope must have a parent before resolution"
); Type guard
fn has_parent_scope(scoping: &Scoping, scope_id: ScopeId) -> bool {
scoping.scope_parent_id(scope_id).is_some()
} Prevention
- Always use `SemanticBuilder::build` rather than calling internal visit_* methods directly
- Keep enter_scope/leave_scope calls paired when customizing AST traversal
- Run `cargo test -p oxc_semantic` on any change touching scope construction in the builder
- Pin and regularly update oxc versions, since the parameter-scope resolution workaround (backlog#176) is actively changing
When it happens
Trigger: Calling `resolve_references_for_current_scope` (from visit_function, visit_arrow_function_expression, or visit_catch_parameter) while `self.current_scope_id` refers to a scope with no parent in `Scoping` — i.e. the root/global scope, or a scope whose parent edge was never pushed. Only reachable when `unresolved_references.len() != unresolved_start`, meaning parameter-scope unresolved references exist when the function/catch parameter visit ends.
Common situations: Custom AST visits or embedding code that calls `SemanticBuilder` visit methods out of order and leaves `current_scope_id` at the program root; builder refactors that skip the `enter_scope`/`leave_scope` pairs for function or catch clauses; external crates constructing a `Scoping` scope tree manually and calling resolution on a parentless scope.
AI-assisted analysis of oxc-project/oxc@a3d33dda7c (2026-08-31).
Data as JSON: /api/errors/c563c360807e2999.
Report an issue: GitHub.