rust-lang/rust-analyzer · error
unmatched closing delimiter from syntax fixup
Error message
unmatched closing delimiter from syntax fixup
What it means
While converting a syntax tree into a token tree (used for macro expansion and syntax fixup), the converter tracks delimiter nesting. If it reaches a closing delimiter character (`)`, `}`, `]`) with no open subtree to close, the fixup logic instead opens a new subtree for it — but only for the three known delimiters; any other char reaching that arm is impossible by construction, so it panics. It signals corrupted delimiter state produced by the syntax fixup heuristics on broken code.
Source
Thrown at crates/syntax-bridge/src/lib.rs:243
let found_expected_delimiter =
builder.expected_delimiters().enumerate().find(|(_, delim)| match delim {
tt::DelimiterKind::Parenthesis => char == ')',
tt::DelimiterKind::Brace => char == '}',
tt::DelimiterKind::Bracket => char == ']',
tt::DelimiterKind::Invisible => false,
});
if let Some((idx, _)) = found_expected_delimiter {
for _ in 0..=idx {
builder.close(span);
}
continue;
}
let delim = match char {
'(' => tt::DelimiterKind::Parenthesis,
'{' => tt::DelimiterKind::Brace,
'[' => tt::DelimiterKind::Bracket,
_ => panic!("unmatched closing delimiter from syntax fixup"),
};
// Start a new subtree
builder.open(delim, span);
continue;
}
Some(leaf) => leaf.clone(),
None => match token.kind(conv) {
// Desugar doc comments into doc attributes
kind @ (INNER_DOC_COMMENT | OUTER_DOC_COMMENT) => {
let span = conv.span_for(abs_range);
conv.convert_doc_comment(&token, kind == INNER_DOC_COMMENT, span, &mut builder);
continue;
}
kind if kind.is_punct() && kind != UNDERSCORE => {
let found_expected_delimiter =
builder.expected_delimiters().enumerate().find(|(_, delim)| match delim {
tt::DelimiterKind::Parenthesis => kind == T![')'],View on GitHub (pinned to e8f7e90aa3)
Solutions
- Inspect the input syntax for unbalanced delimiters before conversion (pre-scan and strip stray closers)
- Update the syntax fixup logic so every closing delimiter is matched to an open subtree
- Capture a minimal reproducing fixture and add it to the fixup tests
Defensive patterns
Strategy: validation
Validate before calling
// Balance-check delimiters before converting to a token tree:
fn delimiters_balanced(tokens: &[tt::TokenTree]) -> bool {
let mut depth = 0i32;
for tt in tokens {
match tt {
tt::TokenTree::Subtree(_) => { /* handled recursively */ }
tt::TokenTree::Leaf(tt::Leaf::Punct(p)) => match p.char {
'(' | '{' | '[' => depth += 1,
')' | '}' | ']' => { depth -= 1; if depth < 0 { return false; } }
_ => {}
},
_ => {}
}
}
depth == 0
} Try / catch
let tt = std::panic::catch_unwind(AssertUnwindSafe(||
syntax_node_to_token_tree(&node, span_map, span, mode)
)).ok()?; // fall back to reparse without fixup Prevention
- Prefer parsing complete buffers; for fragments, keep fixup enabled and current
- Update fixup tests whenever changing delimiter handling
- Fuzz syntax_node_to_token_tree with malformed code containing stray closers
When it happens
Trigger: Calling `syntax_node_to_token_tree`, `syntax_node_to_token_tree_modified`, or `parse_to_token_tree(_static_span)` on code where the fixup introduced unbalanced closing delimiters, combined with a `char` that is not one of `(`/`{`/`[` in the close-handling arm — a bug in fixup token classification rather than user input.
Common situations: Users typing incomplete code with stray closing brackets while rust-analyzer computes macro expansions/completions; fuzzer-generated broken syntax; changes to the fixup algorithm that misclassify punctuation.
Related errors
- Token from lexer must be single char: token = {token:#?}
- Next token must be ident
- We explicitly do not provide canonicalization API, as that i
- bad kind {other}
- bad spacing {other}
AI-assisted analysis of rust-lang/rust-analyzer@e8f7e90aa3 (2026-09-03).
Data as JSON: /api/errors/524540e65d7764ef.
Report an issue: GitHub.