Skip to content

Distinguish delim kind to decide whether to emit unexpected closing delimiter #138554

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 4 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion compiler/rustc_parse/messages.ftl
Original file line number Diff line number Diff line change
Expand Up @@ -553,7 +553,7 @@ parse_meta_bad_delim = wrong meta list delimiters
parse_meta_bad_delim_suggestion = the delimiters should be `(` and `)`

parse_mismatched_closing_delimiter = mismatched closing delimiter: `{$delimiter}`
.label_unmatched = mismatched closing delimiter
.label_unmatched = mismatched closing delimiter{$missing_open_note}
.label_opening_candidate = closing delimiter possibly meant for this
.label_unclosed = unclosed delimiter

Expand Down
1 change: 1 addition & 0 deletions compiler/rustc_parse/src/errors.rs
Original file line number Diff line number Diff line change
Expand Up @@ -945,6 +945,7 @@ pub(crate) struct MismatchedClosingDelimiter {
pub delimiter: String,
#[label(parse_label_unmatched)]
pub unmatched: Span,
pub missing_open_note: String,
#[label(parse_label_opening_candidate)]
pub opening_candidate: Option<Span>,
#[label(parse_label_unclosed)]
Expand Down
99 changes: 74 additions & 25 deletions compiler/rustc_parse/src/lexer/diagnostics.rs
Original file line number Diff line number Diff line change
@@ -1,14 +1,24 @@
use rustc_ast::token::Delimiter;
use rustc_ast::token::{self, Delimiter};
use rustc_ast_pretty::pprust;
use rustc_errors::Diag;
use rustc_session::parse::ParseSess;
use rustc_span::Span;
use rustc_span::source_map::SourceMap;

use super::UnmatchedDelim;
use crate::errors::MismatchedClosingDelimiter;

#[derive(Default)]
pub(super) struct TokenTreeDiagInfo {
/// record span of `(` for diagnostic
pub open_parens: Vec<Span>,
/// record span of `{` for diagnostic
pub open_braces: Vec<Span>,
/// record span of `[` for diagnostic
pub open_brackets: Vec<Span>,

/// Stack of open delimiters and their spans. Used for error message.
pub open_braces: Vec<(Delimiter, Span)>,
pub open_delimiters: Vec<(Delimiter, Span)>,
pub unmatched_delims: Vec<UnmatchedDelim>,

/// Used only for error recovery when arriving to EOF with mismatched braces.
Expand All @@ -22,34 +32,77 @@ pub(super) struct TokenTreeDiagInfo {
pub matching_block_spans: Vec<(Span, Span)>,
}

impl TokenTreeDiagInfo {
pub(super) fn push_open_delimiter(&mut self, delim: Delimiter, span: Span) {
self.open_delimiters.push((delim, span));
match delim {
Delimiter::Parenthesis => self.open_parens.push(span),
Delimiter::Brace => self.open_braces.push(span),
Delimiter::Bracket => self.open_brackets.push(span),
_ => {}
}
}

pub(super) fn pop_open_delimiter(&mut self) -> Option<(Delimiter, Span)> {
let (delim, span) = self.open_delimiters.pop()?;
match delim {
Delimiter::Parenthesis => self.open_parens.pop(),
Delimiter::Brace => self.open_braces.pop(),
Delimiter::Bracket => self.open_brackets.pop(),
_ => unreachable!(),
};
Some((delim, span))
}
}

pub(super) fn same_indentation_level(sm: &SourceMap, open_sp: Span, close_sp: Span) -> bool {
match (sm.span_to_margin(open_sp), sm.span_to_margin(close_sp)) {
(Some(open_padding), Some(close_padding)) => open_padding == close_padding,
_ => false,
}
}

pub(crate) fn make_unclosed_delims_error(
unmatched: UnmatchedDelim,
psess: &ParseSess,
) -> Option<Diag<'_>> {
// `None` here means an `Eof` was found. We already emit those errors elsewhere, we add them to
// `unmatched_delims` only for error recovery in the `Parser`.
let found_delim = unmatched.found_delim?;
let mut spans = vec![unmatched.found_span];
if let Some(sp) = unmatched.unclosed_span {
spans.push(sp);
};

let missing_open_note = report_missing_open_delim(&unmatched)
.map(|s| format!(", may missing open `{s}`"))
.unwrap_or_default();

let err = psess.dcx().create_err(MismatchedClosingDelimiter {
spans,
delimiter: pprust::token_kind_to_string(&token::CloseDelim(found_delim)).to_string(),
unmatched: unmatched.found_span,
missing_open_note,
opening_candidate: unmatched.candidate_span,
unclosed: unmatched.unclosed_span,
});
Some(err)
}

// When we get a `)` or `]` for `{`, we should emit help message here
// it's more friendly compared to report `unmatched error` in later phase
fn report_missing_open_delim(err: &mut Diag<'_>, unmatched_delims: &[UnmatchedDelim]) -> bool {
let mut reported_missing_open = false;
for unmatch_brace in unmatched_delims.iter() {
if let Some(delim) = unmatch_brace.found_delim
&& matches!(delim, Delimiter::Parenthesis | Delimiter::Bracket)
{
let missed_open = match delim {
Delimiter::Parenthesis => "(",
Delimiter::Bracket => "[",
_ => unreachable!(),
};
err.span_label(
unmatch_brace.found_span.shrink_to_lo(),
format!("missing open `{missed_open}` for this delimiter"),
);
reported_missing_open = true;
}
fn report_missing_open_delim(unmatched_delim: &UnmatchedDelim) -> Option<String> {
if let Some(delim) = unmatched_delim.found_delim
&& matches!(delim, Delimiter::Parenthesis | Delimiter::Bracket)
{
let missed_open = match delim {
Delimiter::Parenthesis => "(",
Delimiter::Bracket => "[",
_ => unreachable!(),
};
return Some(missed_open.to_owned());
}
reported_missing_open
None
}

pub(super) fn report_suspicious_mismatch_block(
Expand All @@ -58,10 +111,6 @@ pub(super) fn report_suspicious_mismatch_block(
sm: &SourceMap,
delim: Delimiter,
) {
if report_missing_open_delim(err, &diag_info.unmatched_delims) {
return;
}

let mut matched_spans: Vec<(Span, bool)> = diag_info
.matching_block_spans
.iter()
Expand Down Expand Up @@ -108,7 +157,7 @@ pub(super) fn report_suspicious_mismatch_block(
} else {
// If there is no suspicious span, give the last properly closed block may help
if let Some(parent) = diag_info.matching_block_spans.last()
&& diag_info.open_braces.last().is_none()
&& diag_info.open_delimiters.last().is_none()
&& diag_info.empty_block_spans.iter().all(|&sp| sp != parent.0.to(parent.1))
{
err.span_label(parent.0, "this opening brace...");
Expand Down
3 changes: 2 additions & 1 deletion compiler/rustc_parse/src/lexer/mod.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
use std::ops::Range;

use diagnostics::make_unclosed_delims_error;
use rustc_ast::ast::{self, AttrStyle};
use rustc_ast::token::{self, CommentKind, Delimiter, IdentIsRaw, Token, TokenKind};
use rustc_ast::tokenstream::TokenStream;
Expand All @@ -17,9 +18,9 @@ use rustc_session::parse::ParseSess;
use rustc_span::{BytePos, Pos, Span, Symbol};
use tracing::debug;

use crate::errors;
use crate::lexer::diagnostics::TokenTreeDiagInfo;
use crate::lexer::unicode_chars::UNICODE_ARRAY;
use crate::{errors, make_unclosed_delims_error};

mod diagnostics;
mod tokentrees;
Expand Down
56 changes: 35 additions & 21 deletions compiler/rustc_parse/src/lexer/tokentrees.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ use rustc_ast::token::{self, Delimiter, Token};
use rustc_ast::tokenstream::{DelimSpacing, DelimSpan, Spacing, TokenStream, TokenTree};
use rustc_ast_pretty::pprust::token_to_string;
use rustc_errors::Diag;
use rustc_span::Span;

use super::diagnostics::{report_suspicious_mismatch_block, same_indentation_level};
use super::{Lexer, UnmatchedDelim};
Expand All @@ -23,7 +24,7 @@ impl<'psess, 'src> Lexer<'psess, 'src> {
// Invisible delimiters cannot occur here because `TokenTreesReader` parses
// code directly from strings, with no macro expansion involved.
debug_assert!(!matches!(delim, Delimiter::Invisible(_)));
buf.push(match self.lex_token_tree_open_delim(delim) {
buf.push(match self.lex_token_tree_open_delim(delim, self.token.span) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I guess this may have a performance effect since it's on the normal code path.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Here, after the modification is confirmed, perhaps a time robot can be used to test it.

Ok(val) => val,
Err(errs) => return Err(errs),
})
Expand All @@ -35,6 +36,15 @@ impl<'psess, 'src> Lexer<'psess, 'src> {
return if is_delimited {
Ok((open_spacing, TokenStream::new(buf)))
} else {
let matches_previous_open_delim = match delim {
Delimiter::Parenthesis => self.diag_info.open_parens.last().is_some(),
Delimiter::Brace => self.diag_info.open_braces.last().is_some(),
Delimiter::Bracket => self.diag_info.open_brackets.last().is_some(),
_ => unreachable!(),
};
if matches_previous_open_delim {
return Err(vec![]);
}
Err(vec![self.close_delim_err(delim)])
};
}
Expand All @@ -59,8 +69,8 @@ impl<'psess, 'src> Lexer<'psess, 'src> {
let mut err = self.dcx().struct_span_err(self.token.span, msg);

let unclosed_delimiter_show_limit = 5;
let len = usize::min(unclosed_delimiter_show_limit, self.diag_info.open_braces.len());
for &(_, span) in &self.diag_info.open_braces[..len] {
let len = usize::min(unclosed_delimiter_show_limit, self.diag_info.open_delimiters.len());
for &(_, span) in &self.diag_info.open_delimiters[..len] {
err.span_label(span, "unclosed delimiter");
self.diag_info.unmatched_delims.push(UnmatchedDelim {
found_delim: None,
Expand All @@ -70,19 +80,19 @@ impl<'psess, 'src> Lexer<'psess, 'src> {
});
}

if let Some((_, span)) = self.diag_info.open_braces.get(unclosed_delimiter_show_limit)
&& self.diag_info.open_braces.len() >= unclosed_delimiter_show_limit + 2
if let Some((_, span)) = self.diag_info.open_delimiters.get(unclosed_delimiter_show_limit)
&& self.diag_info.open_delimiters.len() >= unclosed_delimiter_show_limit + 2
{
err.span_label(
*span,
format!(
"another {} unclosed delimiters begin from here",
self.diag_info.open_braces.len() - unclosed_delimiter_show_limit
self.diag_info.open_delimiters.len() - unclosed_delimiter_show_limit
),
);
}

if let Some((delim, _)) = self.diag_info.open_braces.last() {
if let Some((delim, _)) = self.diag_info.open_delimiters.last() {
report_suspicious_mismatch_block(
&mut err,
&self.diag_info,
Expand All @@ -96,11 +106,12 @@ impl<'psess, 'src> Lexer<'psess, 'src> {
fn lex_token_tree_open_delim(
&mut self,
open_delim: Delimiter,
open_delim_span: Span,
) -> Result<TokenTree, Vec<Diag<'psess>>> {
// The span for beginning of the delimited section.
let pre_span = self.token.span;

self.diag_info.open_braces.push((open_delim, self.token.span));
self.diag_info.push_open_delimiter(open_delim, open_delim_span);

// Lex the token trees within the delimiters.
// We stop at any delimiter so we can try to recover if the user
Expand All @@ -114,11 +125,12 @@ impl<'psess, 'src> Lexer<'psess, 'src> {
let close_spacing = match self.token.kind {
// Correct delimiter.
token::CloseDelim(close_delim) if close_delim == open_delim => {
let (open_brace, open_brace_span) = self.diag_info.open_braces.pop().unwrap();
let close_brace_span = self.token.span;
let (open_delimiter, open_delimiter_span) =
self.diag_info.pop_open_delimiter().unwrap();
let close_delim_span = self.token.span;

if tts.is_empty() && close_delim == Delimiter::Brace {
let empty_block_span = open_brace_span.to(close_brace_span);
let empty_block_span = open_delimiter_span.to(close_delim_span);
if !sm.is_multiline(empty_block_span) {
// Only track if the block is in the form of `{}`, otherwise it is
// likely that it was written on purpose.
Expand All @@ -127,9 +139,11 @@ impl<'psess, 'src> Lexer<'psess, 'src> {
}

// only add braces
if let (Delimiter::Brace, Delimiter::Brace) = (open_brace, open_delim) {
if let (Delimiter::Brace, Delimiter::Brace) = (open_delimiter, open_delim)
&& open_delimiter_span == open_delim_span
{
// Add all the matching spans, we will sort by span later
self.diag_info.matching_block_spans.push((open_brace_span, close_brace_span));
self.diag_info.matching_block_spans.push((open_delim_span, close_delim_span));
}

// Move past the closing delimiter.
Expand All @@ -146,26 +160,26 @@ impl<'psess, 'src> Lexer<'psess, 'src> {
// This is a conservative error: only report the last unclosed
// delimiter. The previous unclosed delimiters could actually be
// closed! The lexer just hasn't gotten to them yet.
if let Some(&(_, sp)) = self.diag_info.open_braces.last() {
if let Some(&(_, sp)) = self.diag_info.open_delimiters.last() {
unclosed_delimiter = Some(sp);
};
for (brace, brace_span) in &self.diag_info.open_braces {
if same_indentation_level(sm, self.token.span, *brace_span)
&& brace == &close_delim
for (delim, delim_span) in &self.diag_info.open_delimiters {
if same_indentation_level(sm, self.token.span, *delim_span)
&& delim == &close_delim
{
// high likelihood of these two corresponding
candidate = Some(*brace_span);
candidate = Some(*delim_span);
}
}
let (_, _) = self.diag_info.open_braces.pop().unwrap();
let (_, _) = self.diag_info.open_delimiters.pop().unwrap();
self.diag_info.unmatched_delims.push(UnmatchedDelim {
found_delim: Some(close_delim),
found_span: self.token.span,
unclosed_span: unclosed_delimiter,
candidate_span: candidate,
});
} else {
self.diag_info.open_braces.pop();
self.diag_info.open_delimiters.pop();
}

// If the incorrect delimiter matches an earlier opening
Expand All @@ -175,7 +189,7 @@ impl<'psess, 'src> Lexer<'psess, 'src> {
// fn foo() {
// bar(baz(
// } // Incorrect delimiter but matches the earlier `{`
if !self.diag_info.open_braces.iter().any(|&(b, _)| b == close_delim) {
if !self.diag_info.open_delimiters.iter().any(|&(d, _)| d == close_delim) {
self.bump_minimal()
} else {
// The choice of value here doesn't matter.
Expand Down
2 changes: 1 addition & 1 deletion compiler/rustc_parse/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ pub const MACRO_ARGUMENTS: Option<&str> = Some("macro arguments");

#[macro_use]
pub mod parser;
use parser::{Parser, make_unclosed_delims_error};
use parser::Parser;
pub mod lexer;
pub mod validate_attr;

Expand Down
26 changes: 1 addition & 25 deletions compiler/rustc_parse/src/parser/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -44,11 +44,8 @@ use token_type::TokenTypeSet;
pub use token_type::{ExpKeywordPair, ExpTokenPair, TokenType};
use tracing::debug;

use crate::errors::{
self, IncorrectVisibilityRestriction, MismatchedClosingDelimiter, NonStringAbiLiteral,
};
use crate::errors::{self, IncorrectVisibilityRestriction, NonStringAbiLiteral};
use crate::exp;
use crate::lexer::UnmatchedDelim;

#[cfg(test)]
mod tests;
Expand Down Expand Up @@ -1763,27 +1760,6 @@ impl<'a> Parser<'a> {
}
}

pub(crate) fn make_unclosed_delims_error(
unmatched: UnmatchedDelim,
psess: &ParseSess,
) -> Option<Diag<'_>> {
// `None` here means an `Eof` was found. We already emit those errors elsewhere, we add them to
// `unmatched_delims` only for error recovery in the `Parser`.
let found_delim = unmatched.found_delim?;
let mut spans = vec![unmatched.found_span];
if let Some(sp) = unmatched.unclosed_span {
spans.push(sp);
};
let err = psess.dcx().create_err(MismatchedClosingDelimiter {
spans,
delimiter: pprust::token_kind_to_string(&token::CloseDelim(found_delim)).to_string(),
unmatched: unmatched.found_span,
opening_candidate: unmatched.candidate_span,
unclosed: unmatched.unclosed_span,
});
Some(err)
}

/// A helper struct used when building an `AttrTokenStream` from
/// a `LazyAttrTokenStream`. Both delimiter and non-delimited tokens
/// are stored as `FlatToken::Token`. A vector of `FlatToken`s
Expand Down
2 changes: 1 addition & 1 deletion tests/ui/attributes/z-crate-attr/unbalanced-paren.stderr
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ error: mismatched closing delimiter: `]`
--> <crate attribute>:1:4
|
LL | #![(]
| -^^ mismatched closing delimiter
| -^^ mismatched closing delimiter, may missing open `[`
| ||
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

seems we are suggestting writing code like this?

#![([]

seems not helpful.

| |unclosed delimiter
| closing delimiter possibly meant for this
Expand Down
Loading