#![deny(unstable_features)]
use alloc::boxed::Box;
use alloc::vec::Vec;
use alloc::string::String;
use alloc::string::ToString;
use alloc::borrow::ToOwned;
#[macro_use]
use core::ops::Range;
pub use Alignment::*;
pub use Count::*;
pub use Position::*;
#[derive(Copy, Clone, Debug, Eq, PartialEq)]
pub enum ParseMode {
Format,
InlineAsm,
Diagnostic,
}
#[derive(Clone, Debug, PartialEq)]
pub enum Piece<'input> {
Lit(&'input str),
NextArgument(Box<Argument<'input>>),
}
#[derive(Clone, Debug, PartialEq)]
pub struct Argument<'input> {
pub position: Position<'input>,
pub position_span: Range<usize>,
pub format: FormatSpec<'input>,
}
impl<'input> Argument<'input> {
pub fn is_identifier(&self) -> bool {
matches!(self.position, Position::ArgumentNamed(_)) && self.format == FormatSpec::default()
}
}
#[derive(Clone, Debug, PartialEq, Default)]
pub struct FormatSpec<'input> {
pub fill: Option<char>,
pub fill_span: Option<Range<usize>>,
pub align: Alignment,
pub sign: Option<Sign>,
pub alternate: bool,
pub zero_pad: bool,
pub debug_hex: Option<DebugHex>,
pub precision: Count<'input>,
pub precision_span: Option<Range<usize>>,
pub width: Count<'input>,
pub width_span: Option<Range<usize>>,
pub ty: &'input str,
pub ty_span: Option<Range<usize>>,
}
#[derive(Clone, Debug, PartialEq)]
pub enum Position<'input> {
ArgumentImplicitlyIs(usize),
ArgumentIs(usize),
ArgumentNamed(&'input str),
}
impl Position<'_> {
pub fn index(&self) -> Option<usize> {
match self {
ArgumentIs(i, ..) | ArgumentImplicitlyIs(i) => Some(*i),
_ => None,
}
}
}
#[derive(Copy, Clone, Debug, PartialEq, Default)]
pub enum Alignment {
AlignLeft,
AlignRight,
AlignCenter,
#[default]
AlignUnknown,
}
#[derive(Copy, Clone, Debug, PartialEq)]
pub enum Sign {
Plus,
Minus,
}
#[derive(Copy, Clone, Debug, PartialEq)]
pub enum DebugHex {
Lower,
Upper,
}
#[derive(Clone, Debug, PartialEq, Default)]
pub enum Count<'input> {
CountIs(u16),
CountIsName(&'input str, Range<usize>),
CountIsParam(usize),
CountIsStar(usize),
#[default]
CountImplied,
}
pub struct ParseError {
pub description: String,
pub note: Option<String>,
pub label: String,
pub span: Range<usize>,
pub secondary_label: Option<(String, Range<usize>)>,
pub suggestion: Suggestion,
}
pub enum Suggestion {
None,
UsePositional,
RemoveRawIdent(Range<usize>),
ReorderFormatParameter(Range<usize>, String),
AddMissingColon(Range<usize>),
UseRustDebugPrintingMacro,
}
pub struct Parser<'input> {
mode: ParseMode,
input: &'input str,
input_vec: Vec<(Range<usize>, usize, char)>,
input_vec_index: usize,
pub errors: Vec<ParseError>,
pub curarg: usize,
pub arg_places: Vec<Range<usize>>,
last_open_brace: Option<Range<usize>>,
pub is_source_literal: bool,
end_of_snippet: usize,
cur_line_start: usize,
pub line_spans: Vec<Range<usize>>,
}
impl<'input> Iterator for Parser<'input> {
type Item = Piece<'input>;
fn next(&mut self) -> Option<Piece<'input>> {
if let Some((Range { start, end }, idx, ch)) = self.peek() {
match ch {
'{' => {
self.input_vec_index += 1;
if let Some((_, i, '{')) = self.peek() {
self.input_vec_index += 1;
Some(Piece::Lit(self.string(i)))
} else {
self.last_open_brace = Some(start..end);
let arg = self.argument();
self.ws();
if let Some((close_brace_range, _)) = self.consume_pos('}') {
if self.is_source_literal {
self.arg_places.push(start..close_brace_range.end);
}
} else {
self.missing_closing_brace(&arg);
}
Some(Piece::NextArgument(Box::new(arg)))
}
}
'}' => {
self.input_vec_index += 1;
if let Some((_, i, '}')) = self.peek() {
self.input_vec_index += 1;
Some(Piece::Lit(self.string(i)))
} else {
self.errors.push(ParseError {
description: "unmatched `}` found".into(),
note: Some(
"if you intended to print `}`, you can escape it using `}}`".into(),
),
label: "unmatched `}`".into(),
span: start..end,
secondary_label: None,
suggestion: Suggestion::None,
});
None
}
}
_ => Some(Piece::Lit(self.string(idx))),
}
} else {
if self.is_source_literal {
let span = self.cur_line_start..self.end_of_snippet;
if self.line_spans.last() != Some(&span) {
self.line_spans.push(span);
}
}
None
}
}
}
impl<'input> Parser<'input> {
pub fn new(
input: &'input str,
style: Option<usize>,
snippet: Option<String>,
appended_newline: bool,
mode: ParseMode,
) -> Self {
let quote_offset = style.map_or(1, |nr_hashes| nr_hashes + 2);
let (is_source_literal, end_of_snippet, pre_input_vec) = if let Some(snippet) = snippet {
if let Some(nr_hashes) = style {
let prefix_len = nr_hashes + 2; let suffix_len = nr_hashes + 1; let snippet_bytes = snippet.as_bytes();
let content_end = snippet.len() - suffix_len;
if snippet.len() >= prefix_len + suffix_len && snippet_bytes[0] == b'r'
&& snippet_bytes[1..1 + nr_hashes].iter().all(|&c| c == b'#')
&& snippet_bytes[1 + nr_hashes] == b'"'
&& snippet_bytes[content_end] == b'"'
&& snippet_bytes[content_end + 1..].iter().all(|&c| c == b'#')
{
let snippet_without_quotes = &snippet[prefix_len..content_end];
let input_without_newline =
if appended_newline { &input[..input.len() - 1] } else { input };
if snippet_without_quotes == input_without_newline {
(true, snippet.len() - suffix_len, vec![])
} else {
(false, snippet.len(), vec![])
}
} else {
(false, snippet.len(), vec![])
}
} else {
if snippet.starts_with('"') {
let snippet_without_quotes = &snippet[1..snippet.len() - 1];
let (mut ok, mut vec) = (true, vec![]);
let mut chars = input.chars();
rustc_literal_escaper::unescape_str(snippet_without_quotes, |range, res| {
match res {
Ok(ch) if ok && chars.next().is_some_and(|c| ch == c) => {
vec.push((range, ch));
}
_ => {
ok = false;
vec = vec![];
}
}
});
let end = vec.last().map(|(r, _)| r.end).unwrap_or(0);
if ok {
if appended_newline {
if chars.as_str() == "\n" {
vec.push((end..end + 1, '\n'));
(true, 1 + end, vec)
} else {
(false, snippet.len(), vec![])
}
} else if chars.as_str() == "" {
(true, 1 + end, vec)
} else {
(false, snippet.len(), vec![])
}
} else {
(false, snippet.len(), vec![])
}
} else {
(false, snippet.len(), vec![])
}
}
} else {
(false, input.len() - if appended_newline { 1 } else { 0 }, vec![])
};
let input_vec: Vec<(Range<usize>, usize, char)> = if pre_input_vec.is_empty() {
input
.char_indices()
.map(|(idx, c)| {
let i = idx + quote_offset;
(i..i + c.len_utf8(), idx, c)
})
.collect()
} else {
input
.char_indices()
.zip(pre_input_vec)
.map(|((i, c), (r, _))| (r.start + quote_offset..r.end + quote_offset, i, c))
.collect()
};
Parser {
mode,
input,
input_vec,
input_vec_index: 0,
errors: vec![],
curarg: 0,
arg_places: vec![],
last_open_brace: None,
is_source_literal,
end_of_snippet,
cur_line_start: quote_offset,
line_spans: vec![],
}
}
pub fn peek(&self) -> Option<(Range<usize>, usize, char)> {
self.input_vec.get(self.input_vec_index).cloned()
}
pub fn peek_ahead(&self) -> Option<(Range<usize>, usize, char)> {
self.input_vec.get(self.input_vec_index + 1).cloned()
}
fn consume(&mut self, c: char) -> bool {
self.consume_pos(c).is_some()
}
fn consume_pos(&mut self, ch: char) -> Option<(Range<usize>, usize)> {
if let Some((r, i, c)) = self.peek()
&& ch == c
{
self.input_vec_index += 1;
return Some((r, i));
}
None
}
fn missing_closing_brace(&mut self, arg: &Argument<'_>) {
let (range, description) = if let Some((r, _, c)) = self.peek() {
(r.start..r.start, format!("expected `}}`, found `{}`", c.escape_debug()))
} else {
(
self.end_of_snippet..self.end_of_snippet,
"expected `}` but string was terminated".to_owned(),
)
};
let (note, secondary_label) = if arg.format.fill == Some('}') {
(
Some("the character `}` is interpreted as a fill character because of the `:` that precedes it".to_owned()),
arg.format.fill_span.clone().map(|sp| ("this is not interpreted as a formatting closing brace".to_owned(), sp)),
)
} else {
(
Some("if you intended to print `{`, you can escape it using `{{`".to_owned()),
self.last_open_brace
.clone()
.map(|sp| ("because of this opening brace".to_owned(), sp)),
)
};
self.errors.push(ParseError {
description,
note,
label: "expected `}`".to_owned(),
span: range.start..range.start,
secondary_label,
suggestion: Suggestion::None,
});
if let (Some((_, _, c)), Some((_, _, nc))) = (self.peek(), self.peek_ahead()) {
match (c, nc) {
('?', '}') => self.missing_colon_before_debug_formatter(),
('?', _) => self.suggest_format_debug(),
('<' | '^' | '>', _) => self.suggest_format_align(c),
(',', _) => self.suggest_unsupported_python_numeric_grouping(),
('=', '}') => self.suggest_rust_debug_printing_macro(),
('+', _) => self.suggest_format_missing_colon_for_sign(),
_ => self.suggest_positional_arg_instead_of_captured_arg(arg),
}
}
}
fn ws(&mut self) {
let rest = &self.input_vec[self.input_vec_index..];
let step = rest.iter().position(|&(_, _, c)| !c.is_whitespace()).unwrap_or(rest.len());
self.input_vec_index += step;
}
fn string(&mut self, start: usize) -> &'input str {
while let Some((r, i, c)) = self.peek() {
match c {
'{' | '}' => {
return &self.input[start..i];
}
'\n' if self.is_source_literal => {
self.input_vec_index += 1;
self.line_spans.push(self.cur_line_start..r.start);
self.cur_line_start = r.end;
}
_ => {
self.input_vec_index += 1;
if self.is_source_literal && r.start == self.cur_line_start && c.is_whitespace()
{
self.cur_line_start = r.end;
}
}
}
}
&self.input[start..]
}
fn argument(&mut self) -> Argument<'input> {
let start_idx = self.input_vec_index;
let position = self.position();
self.ws();
let end_idx = self.input_vec_index;
let format = match self.mode {
ParseMode::Format => self.format(),
ParseMode::InlineAsm => self.inline_asm(),
ParseMode::Diagnostic => self.diagnostic(),
};
let position = position.unwrap_or_else(|| {
let i = self.curarg;
self.curarg += 1;
ArgumentImplicitlyIs(i)
});
let position_span =
self.input_vec_index2range(start_idx).start..self.input_vec_index2range(end_idx).start;
Argument { position, position_span, format }
}
fn position(&mut self) -> Option<Position<'input>> {
if let Some(i) = self.integer() {
Some(ArgumentIs(i.into()))
} else {
match self.peek() {
Some((range, _, c)) if crate::rustc_lexer::is_id_start(c) => {
let start = range.start;
let word = self.word();
if word == "r"
&& let Some((r, _, '#')) = self.peek()
&& self.peek_ahead().is_some_and(|(_, _, c)| crate::rustc_lexer::is_id_start(c))
{
self.input_vec_index += 1;
let prefix_end = r.end;
let word = self.word();
let prefix_span = start..prefix_end;
let full_span =
start..self.input_vec_index2range(self.input_vec_index).start;
self.errors.insert(0, ParseError {
description: "raw identifiers are not supported".to_owned(),
note: Some("identifiers in format strings can be keywords and don't need to be prefixed with `r#`".to_string()),
label: "raw identifier used here".to_owned(),
span: full_span,
secondary_label: None,
suggestion: Suggestion::RemoveRawIdent(prefix_span),
});
return Some(ArgumentNamed(word));
}
Some(ArgumentNamed(word))
}
_ => None,
}
}
}
fn input_vec_index2pos(&self, index: usize) -> usize {
if let Some((_, pos, _)) = self.input_vec.get(index) { *pos } else { self.input.len() }
}
fn input_vec_index2range(&self, index: usize) -> Range<usize> {
if let Some((r, _, _)) = self.input_vec.get(index) {
r.clone()
} else {
self.end_of_snippet..self.end_of_snippet
}
}
fn format(&mut self) -> FormatSpec<'input> {
let mut spec = FormatSpec::default();
if !self.consume(':') {
return spec;
}
if let (Some((r, _, c)), Some((_, _, '>' | '<' | '^'))) = (self.peek(), self.peek_ahead()) {
self.input_vec_index += 1;
spec.fill = Some(c);
spec.fill_span = Some(r);
}
if self.consume('<') {
spec.align = AlignLeft;
} else if self.consume('>') {
spec.align = AlignRight;
} else if self.consume('^') {
spec.align = AlignCenter;
}
if self.consume('+') {
spec.sign = Some(Sign::Plus);
} else if self.consume('-') {
spec.sign = Some(Sign::Minus);
}
if self.consume('#') {
spec.alternate = true;
}
let mut havewidth = false;
if let Some((range, _)) = self.consume_pos('0') {
if let Some((r, _)) = self.consume_pos('$') {
spec.width = CountIsParam(0);
spec.width_span = Some(range.start..r.end);
havewidth = true;
} else {
spec.zero_pad = true;
}
}
if !havewidth {
let start_idx = self.input_vec_index;
spec.width = self.count();
if spec.width != CountImplied {
let end = self.input_vec_index2range(self.input_vec_index).start;
spec.width_span = Some(self.input_vec_index2range(start_idx).start..end);
}
}
if let Some((range, _)) = self.consume_pos('.') {
if self.consume('*') {
let i = self.curarg;
self.curarg += 1;
spec.precision = CountIsStar(i);
} else {
spec.precision = self.count();
}
spec.precision_span =
Some(range.start..self.input_vec_index2range(self.input_vec_index).start);
}
let start_idx = self.input_vec_index;
if self.consume('x') {
if self.consume('?') {
spec.debug_hex = Some(DebugHex::Lower);
spec.ty = "?";
} else {
spec.ty = "x";
}
} else if self.consume('X') {
if self.consume('?') {
spec.debug_hex = Some(DebugHex::Upper);
spec.ty = "?";
} else {
spec.ty = "X";
}
} else if let Some((range, _)) = self.consume_pos('?') {
spec.ty = "?";
if let Some((r, _, c @ ('#' | 'x' | 'X'))) = self.peek() {
self.errors.insert(
0,
ParseError {
description: format!("expected `}}`, found `{c}`"),
note: None,
label: "expected `'}'`".into(),
span: r.clone(),
secondary_label: None,
suggestion: Suggestion::ReorderFormatParameter(
range.start..r.end,
format!("{c}?"),
),
},
);
}
} else {
spec.ty = self.word();
if !spec.ty.is_empty() {
let start = self.input_vec_index2range(start_idx).start;
let end = self.input_vec_index2range(self.input_vec_index).start;
spec.ty_span = Some(start..end);
}
}
spec
}
fn inline_asm(&mut self) -> FormatSpec<'input> {
let mut spec = FormatSpec::default();
if !self.consume(':') {
return spec;
}
let start_idx = self.input_vec_index;
spec.ty = self.word();
if !spec.ty.is_empty() {
let start = self.input_vec_index2range(start_idx).start;
let end = self.input_vec_index2range(self.input_vec_index).start;
spec.ty_span = Some(start..end);
}
spec
}
fn diagnostic(&mut self) -> FormatSpec<'input> {
let mut spec = FormatSpec::default();
let Some((Range { start, .. }, _)) = self.consume_pos(':') else {
return spec;
};
spec.ty = self.string(self.input_vec_index2pos(self.input_vec_index));
spec.ty_span = {
let end = self.input_vec_index2range(self.input_vec_index).start;
Some(start..end)
};
spec
}
fn count(&mut self) -> Count<'input> {
if let Some(i) = self.integer() {
if self.consume('$') { CountIsParam(i.into()) } else { CountIs(i) }
} else {
let start_idx = self.input_vec_index;
let word = self.word();
if word.is_empty() {
CountImplied
} else if let Some((r, _)) = self.consume_pos('$') {
CountIsName(word, self.input_vec_index2range(start_idx).start..r.start)
} else {
self.input_vec_index = start_idx;
CountImplied
}
}
}
fn word(&mut self) -> &'input str {
let index = self.input_vec_index;
match self.peek() {
Some((ref r, i, c)) if crate::rustc_lexer::is_id_start(c) => {
self.input_vec_index += 1;
(r.start, i)
}
_ => {
return "";
}
};
let (err_end, end): (usize, usize) = loop {
if let Some((ref r, i, c)) = self.peek() {
if crate::rustc_lexer::is_id_continue(c) {
self.input_vec_index += 1;
} else {
break (r.start, i);
}
} else {
break (self.end_of_snippet, self.input.len());
}
};
let word = &self.input[self.input_vec_index2pos(index)..end];
if word == "_" {
self.errors.push(ParseError {
description: "invalid argument name `_`".into(),
note: Some("argument name cannot be a single underscore".into()),
label: "invalid argument name".into(),
span: self.input_vec_index2range(index).start..err_end,
secondary_label: None,
suggestion: Suggestion::None,
});
}
word
}
fn integer(&mut self) -> Option<u16> {
let mut cur: u16 = 0;
let mut found = false;
let mut overflow = false;
let start_index = self.input_vec_index;
while let Some((_, _, c)) = self.peek() {
if let Some(i) = c.to_digit(10) {
self.input_vec_index += 1;
let (tmp, mul_overflow) = cur.overflowing_mul(10);
let (tmp, add_overflow) = tmp.overflowing_add(i as u16);
if mul_overflow || add_overflow {
overflow = true;
}
cur = tmp;
found = true;
} else {
break;
}
}
if overflow {
let overflowed_int = &self.input[self.input_vec_index2pos(start_index)
..self.input_vec_index2pos(self.input_vec_index)];
self.errors.push(ParseError {
description: format!(
"integer `{}` does not fit into the type `u16` whose range is `0..={}`",
overflowed_int,
u16::MAX
),
note: None,
label: "integer out of range for `u16`".into(),
span: self.input_vec_index2range(start_index).start
..self.input_vec_index2range(self.input_vec_index).end,
secondary_label: None,
suggestion: Suggestion::None,
});
}
found.then_some(cur)
}
fn suggest_format_debug(&mut self) {
if let (Some((range, _)), Some(_)) = (self.consume_pos('?'), self.consume_pos(':')) {
let word = self.word();
self.errors.insert(
0,
ParseError {
description: "expected format parameter to occur after `:`".to_owned(),
note: Some(format!("`?` comes after `:`, try `{}:{}` instead", word, "?")),
label: "expected `?` to occur after `:`".to_owned(),
span: range,
secondary_label: None,
suggestion: Suggestion::None,
},
);
}
}
fn missing_colon_before_debug_formatter(&mut self) {
if let Some((range, _)) = self.consume_pos('?') {
let span = range.clone();
self.errors.insert(
0,
ParseError {
description: "expected `}`, found `?`".to_owned(),
note: Some(format!("to print `{{`, you can escape it using `{{{{`",)),
label: "expected `:` before `?` to format with `Debug`".to_owned(),
span: range,
secondary_label: None,
suggestion: Suggestion::AddMissingColon(span),
},
);
}
}
fn suggest_rust_debug_printing_macro(&mut self) {
if let Some((range, _)) = self.consume_pos('=') {
self.errors.insert(
0,
ParseError {
description:
"python's f-string debug `=` is not supported in rust, use `dbg(x)` instead"
.to_owned(),
note: Some(format!("to print `{{`, you can escape it using `{{{{`",)),
label: "expected `}`".to_owned(),
span: range,
secondary_label: self
.last_open_brace
.clone()
.map(|sp| ("because of this opening brace".to_owned(), sp)),
suggestion: Suggestion::UseRustDebugPrintingMacro,
},
);
}
}
fn suggest_format_align(&mut self, alignment: char) {
if let Some((range, _)) = self.consume_pos(alignment) {
self.errors.insert(
0,
ParseError {
description:
"expected alignment specifier after `:` in format string; example: `{:>?}`"
.to_owned(),
note: None,
label: format!("expected `{}` to occur after `:`", alignment),
span: range,
secondary_label: None,
suggestion: Suggestion::None,
},
);
}
}
fn suggest_format_missing_colon_for_sign(&mut self) {
if let Some((range, _)) = self.consume_pos('+') {
self.errors.insert(
0,
ParseError {
description: "the `+` sign flag must appear after `:` in a format string"
.to_owned(),
note: Some("`+` comes after `:`, try `{:+}` instead of `{+}`".to_owned()),
label: "expected `:` before `+` sign flag".to_owned(),
span: range,
secondary_label: None,
suggestion: Suggestion::None,
},
);
}
}
fn suggest_positional_arg_instead_of_captured_arg(&mut self, arg: &Argument<'_>) {
if !arg.is_identifier() {
return;
}
if let Some((_range, _pos)) = self.consume_pos('.') {
let field = self.argument();
if !self.consume('}') {
return;
}
if let ArgumentNamed(_) = arg.position {
match field.position {
ArgumentNamed(_) => {
self.errors.insert(
0,
ParseError {
description: "field access isn't supported".to_string(),
note: Some(
"consider moving this expression to a local variable and then \
using the local here instead"
.to_owned(),
),
label: "not supported".to_string(),
span: arg.position_span.start..field.position_span.end,
secondary_label: None,
suggestion: Suggestion::UsePositional,
},
);
}
ArgumentIs(_) => {
self.errors.insert(
0,
ParseError {
description: "tuple index access isn't supported".to_string(),
note: Some(
"consider moving this expression to a local variable and then \
using the local here instead"
.to_owned(),
),
label: "not supported".to_string(),
span: arg.position_span.start..field.position_span.end,
secondary_label: None,
suggestion: Suggestion::UsePositional,
},
);
}
_ => {}
};
}
}
}
fn suggest_unsupported_python_numeric_grouping(&mut self) {
if let Some((range, _)) = self.consume_pos(',') {
self.errors.insert(
0,
ParseError {
description:
"python's numeric grouping `,` is not supported in rust format strings"
.to_owned(),
note: Some(format!("to print `{{`, you can escape it using `{{{{`",)),
label: "expected `}`".to_owned(),
span: range,
secondary_label: self
.last_open_brace
.clone()
.map(|sp| ("because of this opening brace".to_owned(), sp)),
suggestion: Suggestion::None,
},
);
}
}
}
#[cfg(all(test, target_pointer_width = "64"))]
crate::static_assert_size!(Piece<'_>, 16);
#[cfg(test)]
mod tests;