use rustc_ast::ptr::P;
use rustc_ast::tokenstream::TokenStream;
use rustc_ast::{token, StmtKind};
use rustc_ast::{
Expr, ExprKind, FormatAlignment, FormatArgPosition, FormatArgPositionKind, FormatArgs,
FormatArgsPiece, FormatArgument, FormatArgumentKind, FormatArguments, FormatCount,
FormatDebugHex, FormatOptions, FormatPlaceholder, FormatSign, FormatTrait,
};
use rustc_data_structures::fx::FxHashSet;
use rustc_errors::{Applicability, MultiSpan, PResult, SingleLabelManySpans};
use rustc_expand::base::{self, *};
use rustc_parse_format as parse;
use rustc_span::symbol::{Ident, Symbol};
use rustc_span::{BytePos, InnerSpan, Span};
use rustc_lint_defs::builtin::NAMED_ARGUMENTS_USED_POSITIONALLY;
use rustc_lint_defs::{BufferedEarlyLint, BuiltinLintDiagnostics, LintId};
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
enum PositionUsedAs {
Placeholder(Option<Span>),
Precision,
Width,
}
use PositionUsedAs::*;
use crate::errors;
struct MacroInput {
fmtstr: P<Expr>,
args: FormatArguments,
is_direct_literal: bool,
}
fn parse_args<'a>(ecx: &mut ExtCtxt<'a>, sp: Span, tts: TokenStream) -> PResult<'a, MacroInput> {
let mut args = FormatArguments::new();
let mut p = ecx.new_parser_from_tts(tts);
if p.token == token::Eof {
return Err(ecx.create_err(errors::FormatRequiresString { span: sp }));
}
let first_token = &p.token;
let fmtstr = if let token::Literal(lit) = first_token.kind && matches!(lit.kind, token::Str | token::StrRaw(_)) {
p.parse_literal_maybe_minus()?
} else {
p.parse_expr()?
};
let is_direct_literal = matches!(fmtstr.kind, ExprKind::Lit(_));
let mut first = true;
while p.token != token::Eof {
if !p.eat(&token::Comma) {
if first {
p.clear_expected_tokens();
}
match p.expect(&token::Comma) {
Err(mut err) => {
match token::TokenKind::Comma.similar_tokens() {
Some(tks) if tks.contains(&p.token.kind) => {
err.emit();
p.bump();
}
_ => return Err(err),
}
}
Ok(recovered) => {
assert!(recovered);
}
}
}
first = false;
if p.token == token::Eof {
break;
} match p.token.ident() {
Some((ident, _)) if p.look_ahead(1, |t| *t == token::Eq) => {
p.bump();
p.expect(&token::Eq)?;
let expr = p.parse_expr()?;
if let Some((_, prev)) = args.by_name(ident.name) {
ecx.emit_err(errors::FormatDuplicateArg {
span: ident.span,
prev: prev.kind.ident().unwrap().span,
duplicate: ident.span,
ident,
});
continue;
}
args.add(FormatArgument { kind: FormatArgumentKind::Named(ident), expr });
}
_ => {
let expr = p.parse_expr()?;
if !args.named_args().is_empty() {
ecx.emit_err(errors::PositionalAfterNamed {
span: expr.span,
args: args
.named_args()
.iter()
.filter_map(|a| a.kind.ident().map(|ident| (a, ident)))
.map(|(arg, n)| n.span.to(arg.expr.span))
.collect(),
});
}
args.add(FormatArgument { kind: FormatArgumentKind::Normal, expr });
}
}
}
Ok(MacroInput { fmtstr, args, is_direct_literal })
}
fn make_format_args(
ecx: &mut ExtCtxt<'_>,
input: MacroInput,
append_newline: bool,
) -> Result<FormatArgs, ()> {
let msg = "format argument must be a string literal";
let unexpanded_fmt_span = input.fmtstr.span;
let MacroInput { fmtstr: efmt, mut args, is_direct_literal } = input;
let (fmt_str, fmt_style, fmt_span) = match expr_to_spanned_string(ecx, efmt.clone(), msg) {
Ok(mut fmt) if append_newline => {
fmt.0 = Symbol::intern(&format!("{}\n", fmt.0));
fmt
}
Ok(fmt) => fmt,
Err(err) => {
if let Some((mut err, suggested)) = err {
if !suggested {
if let ExprKind::Block(block, None) = &efmt.kind
&& block.stmts.len() == 1
&& let StmtKind::Expr(expr) = &block.stmts[0].kind
&& let ExprKind::Path(None, path) = &expr.kind
&& path.is_potential_trivial_const_arg()
{
err.multipart_suggestion(
"quote your inlined format argument to use as string literal",
vec![
(unexpanded_fmt_span.shrink_to_hi(), "\"".to_string()),
(unexpanded_fmt_span.shrink_to_lo(), "\"".to_string()),
],
Applicability::MaybeIncorrect,
);
} else {
let sugg_fmt = match args.explicit_args().len() {
0 => "{}".to_string(),
_ => format!("{}{{}}", "{} ".repeat(args.explicit_args().len())),
};
err.span_suggestion(
unexpanded_fmt_span.shrink_to_lo(),
"you might be missing a string literal to format with",
format!("\"{sugg_fmt}\", "),
Applicability::MaybeIncorrect,
);
}
}
err.emit();
}
return Err(());
}
};
let str_style = match fmt_style {
rustc_ast::StrStyle::Cooked => None,
rustc_ast::StrStyle::Raw(raw) => Some(raw as usize),
};
let fmt_str = fmt_str.as_str(); let fmt_snippet = ecx.source_map().span_to_snippet(unexpanded_fmt_span).ok();
let mut parser = parse::Parser::new(
fmt_str,
str_style,
fmt_snippet,
append_newline,
parse::ParseMode::Format,
);
let mut pieces = Vec::new();
while let Some(piece) = parser.next() {
if !parser.errors.is_empty() {
break;
} else {
pieces.push(piece);
}
}
let is_source_literal = parser.is_source_literal;
if !parser.errors.is_empty() {
let err = parser.errors.remove(0);
let sp = if is_source_literal {
fmt_span.from_inner(InnerSpan::new(err.span.start, err.span.end))
} else {
fmt_span
};
let mut e = errors::InvalidFormatString {
span: sp,
note_: None,
label_: None,
sugg_: None,
desc: err.description,
label1: err.label,
};
if let Some(note) = err.note {
e.note_ = Some(errors::InvalidFormatStringNote { note });
}
if let Some((label, span)) = err.secondary_label && is_source_literal {
e.label_ = Some(errors::InvalidFormatStringLabel { span: fmt_span.from_inner(InnerSpan::new(span.start, span.end)), label } );
}
match err.suggestion {
parse::Suggestion::None => {}
parse::Suggestion::UsePositional => {
let captured_arg_span =
fmt_span.from_inner(InnerSpan::new(err.span.start, err.span.end));
if let Ok(arg) = ecx.source_map().span_to_snippet(captured_arg_span) {
let span = match args.unnamed_args().last() {
Some(arg) => arg.expr.span,
None => fmt_span,
};
e.sugg_ = Some(errors::InvalidFormatStringSuggestion::UsePositional {
captured: captured_arg_span,
len: args.unnamed_args().len().to_string(),
span: span.shrink_to_hi(),
arg,
});
}
}
parse::Suggestion::RemoveRawIdent(span) => {
if is_source_literal {
let span = fmt_span.from_inner(InnerSpan::new(span.start, span.end));
e.sugg_ = Some(errors::InvalidFormatStringSuggestion::RemoveRawIdent { span })
}
}
}
ecx.emit_err(e);
return Err(());
}
let to_span = |inner_span: rustc_parse_format::InnerSpan| {
is_source_literal.then(|| {
fmt_span.from_inner(InnerSpan { start: inner_span.start, end: inner_span.end })
})
};
let mut used = vec![false; args.explicit_args().len()];
let mut invalid_refs = Vec::new();
let mut numeric_refences_to_named_arg = Vec::new();
enum ArgRef<'a> {
Index(usize),
Name(&'a str, Option<Span>),
}
use ArgRef::*;
let mut lookup_arg = |arg: ArgRef<'_>,
span: Option<Span>,
used_as: PositionUsedAs,
kind: FormatArgPositionKind|
-> FormatArgPosition {
let index = match arg {
Index(index) => {
if let Some(arg) = args.by_index(index) {
used[index] = true;
if arg.kind.ident().is_some() {
numeric_refences_to_named_arg.push((index, span, used_as));
}
Ok(index)
} else {
invalid_refs.push((index, span, used_as, kind));
Err(index)
}
}
Name(name, span) => {
let name = Symbol::intern(name);
if let Some((index, _)) = args.by_name(name) {
if index < args.explicit_args().len() {
used[index] = true;
}
Ok(index)
} else {
let span = span.unwrap_or(fmt_span);
let ident = Ident::new(name, span);
let expr = if is_direct_literal {
ecx.expr_ident(span, ident)
} else {
ecx.emit_err(errors::FormatNoArgNamed { span, name });
DummyResult::raw_expr(span, true)
};
Ok(args.add(FormatArgument { kind: FormatArgumentKind::Captured(ident), expr }))
}
}
};
FormatArgPosition { index, kind, span }
};
let mut template = Vec::new();
let mut unfinished_literal = String::new();
let mut placeholder_index = 0;
for piece in pieces {
match piece {
parse::Piece::String(s) => {
unfinished_literal.push_str(s);
}
parse::Piece::NextArgument(box parse::Argument { position, position_span, format }) => {
if !unfinished_literal.is_empty() {
template.push(FormatArgsPiece::Literal(Symbol::intern(&unfinished_literal)));
unfinished_literal.clear();
}
let span = parser.arg_places.get(placeholder_index).and_then(|&s| to_span(s));
placeholder_index += 1;
let position_span = to_span(position_span);
let argument = match position {
parse::ArgumentImplicitlyIs(i) => lookup_arg(
Index(i),
position_span,
Placeholder(span),
FormatArgPositionKind::Implicit,
),
parse::ArgumentIs(i) => lookup_arg(
Index(i),
position_span,
Placeholder(span),
FormatArgPositionKind::Number,
),
parse::ArgumentNamed(name) => lookup_arg(
Name(name, position_span),
position_span,
Placeholder(span),
FormatArgPositionKind::Named,
),
};
let alignment = match format.align {
parse::AlignUnknown => None,
parse::AlignLeft => Some(FormatAlignment::Left),
parse::AlignRight => Some(FormatAlignment::Right),
parse::AlignCenter => Some(FormatAlignment::Center),
};
let format_trait = match format.ty {
"" => FormatTrait::Display,
"?" => FormatTrait::Debug,
"e" => FormatTrait::LowerExp,
"E" => FormatTrait::UpperExp,
"o" => FormatTrait::Octal,
"p" => FormatTrait::Pointer,
"b" => FormatTrait::Binary,
"x" => FormatTrait::LowerHex,
"X" => FormatTrait::UpperHex,
_ => {
invalid_placeholder_type_error(ecx, format.ty, format.ty_span, fmt_span);
FormatTrait::Display
}
};
let precision_span = format.precision_span.and_then(to_span);
let precision = match format.precision {
parse::CountIs(n) => Some(FormatCount::Literal(n)),
parse::CountIsName(name, name_span) => Some(FormatCount::Argument(lookup_arg(
Name(name, to_span(name_span)),
precision_span,
Precision,
FormatArgPositionKind::Named,
))),
parse::CountIsParam(i) => Some(FormatCount::Argument(lookup_arg(
Index(i),
precision_span,
Precision,
FormatArgPositionKind::Number,
))),
parse::CountIsStar(i) => Some(FormatCount::Argument(lookup_arg(
Index(i),
precision_span,
Precision,
FormatArgPositionKind::Implicit,
))),
parse::CountImplied => None,
};
let width_span = format.width_span.and_then(to_span);
let width = match format.width {
parse::CountIs(n) => Some(FormatCount::Literal(n)),
parse::CountIsName(name, name_span) => Some(FormatCount::Argument(lookup_arg(
Name(name, to_span(name_span)),
width_span,
Width,
FormatArgPositionKind::Named,
))),
parse::CountIsParam(i) => Some(FormatCount::Argument(lookup_arg(
Index(i),
width_span,
Width,
FormatArgPositionKind::Number,
))),
parse::CountIsStar(_) => unreachable!(),
parse::CountImplied => None,
};
template.push(FormatArgsPiece::Placeholder(FormatPlaceholder {
argument,
span,
format_trait,
format_options: FormatOptions {
fill: format.fill,
alignment,
sign: format.sign.map(|s| match s {
parse::Sign::Plus => FormatSign::Plus,
parse::Sign::Minus => FormatSign::Minus,
}),
alternate: format.alternate,
zero_pad: format.zero_pad,
debug_hex: format.debug_hex.map(|s| match s {
parse::DebugHex::Lower => FormatDebugHex::Lower,
parse::DebugHex::Upper => FormatDebugHex::Upper,
}),
precision,
width,
},
}));
}
}
}
if !unfinished_literal.is_empty() {
template.push(FormatArgsPiece::Literal(Symbol::intern(&unfinished_literal)));
}
if !invalid_refs.is_empty() {
report_invalid_references(ecx, &invalid_refs, &template, fmt_span, &args, parser);
}
let unused = used
.iter()
.enumerate()
.filter(|&(_, used)| !used)
.map(|(i, _)| {
let named = matches!(args.explicit_args()[i].kind, FormatArgumentKind::Named(_));
(args.explicit_args()[i].expr.span, named)
})
.collect::<Vec<_>>();
if !unused.is_empty() {
let detect_foreign_fmt = unused.len() > args.explicit_args().len() / 2;
report_missing_placeholders(ecx, unused, detect_foreign_fmt, str_style, fmt_str, fmt_span);
}
if invalid_refs.is_empty() && ecx.sess.err_count() == 0 {
for &(index, span, used_as) in &numeric_refences_to_named_arg {
let (position_sp_to_replace, position_sp_for_msg) = match used_as {
Placeholder(pspan) => (span, pspan),
Precision => {
let span = span.map(|span| span.with_lo(span.lo() + BytePos(1)));
(span, span)
}
Width => (span, span),
};
let arg_name = args.explicit_args()[index].kind.ident().unwrap();
ecx.buffered_early_lint.push(BufferedEarlyLint {
span: arg_name.span.into(),
msg: format!("named argument `{}` is not used by name", arg_name.name).into(),
node_id: rustc_ast::CRATE_NODE_ID,
lint_id: LintId::of(&NAMED_ARGUMENTS_USED_POSITIONALLY),
diagnostic: BuiltinLintDiagnostics::NamedArgumentUsedPositionally {
position_sp_to_replace,
position_sp_for_msg,
named_arg_sp: arg_name.span,
named_arg_name: arg_name.name.to_string(),
is_formatting_arg: matches!(used_as, Width | Precision),
},
});
}
}
Ok(FormatArgs { span: fmt_span, template, arguments: args })
}
fn invalid_placeholder_type_error(
ecx: &ExtCtxt<'_>,
ty: &str,
ty_span: Option<rustc_parse_format::InnerSpan>,
fmt_span: Span,
) {
let sp = ty_span.map(|sp| fmt_span.from_inner(InnerSpan::new(sp.start, sp.end)));
let suggs = if let Some(sp) = sp {
[
("", "Display"),
("?", "Debug"),
("e", "LowerExp"),
("E", "UpperExp"),
("o", "Octal"),
("p", "Pointer"),
("b", "Binary"),
("x", "LowerHex"),
("X", "UpperHex"),
]
.into_iter()
.map(|(fmt, trait_name)| errors::FormatUnknownTraitSugg { span: sp, fmt, trait_name })
.collect()
} else {
vec![]
};
ecx.emit_err(errors::FormatUnknownTrait { span: sp.unwrap_or(fmt_span), ty, suggs });
}
fn report_missing_placeholders(
ecx: &mut ExtCtxt<'_>,
unused: Vec<(Span, bool)>,
detect_foreign_fmt: bool,
str_style: Option<usize>,
fmt_str: &str,
fmt_span: Span,
) {
let mut diag = if let &[(span, named)] = &unused[..] {
ecx.create_err(errors::FormatUnusedArg { span, named })
} else {
let unused_labels =
unused.iter().map(|&(span, named)| errors::FormatUnusedArg { span, named }).collect();
let unused_spans = unused.iter().map(|&(span, _)| span).collect();
ecx.create_err(errors::FormatUnusedArgs {
fmt: fmt_span,
unused: unused_spans,
unused_labels,
})
};
let mut found_foreign = false;
if detect_foreign_fmt {
use super::format_foreign as foreign;
let mut explained = FxHashSet::default();
macro_rules! check_foreign {
($kind:ident) => {{
let mut show_doc_note = false;
let mut suggestions = vec![];
let padding = str_style.map(|i| i + 2).unwrap_or(1);
for sub in foreign::$kind::iter_subs(fmt_str, padding) {
let (trn, success) = match sub.translate() {
Ok(trn) => (trn, true),
Err(Some(msg)) => (msg, false),
_ => continue,
};
let pos = sub.position();
let sub = String::from(sub.as_str());
if explained.contains(&sub) {
continue;
}
explained.insert(sub.clone());
if !found_foreign {
found_foreign = true;
show_doc_note = true;
}
if let Some(inner_sp) = pos {
let sp = fmt_span.from_inner(inner_sp);
if success {
suggestions.push((sp, trn));
} else {
diag.span_note(
sp,
format!("format specifiers use curly braces, and {}", trn),
);
}
} else {
if success {
diag.help(format!("`{}` should be written as `{}`", sub, trn));
} else {
diag.note(format!("`{}` should use curly braces, and {}", sub, trn));
}
}
}
if show_doc_note {
diag.note(concat!(
stringify!($kind),
" formatting is not supported; see the documentation for `std::fmt`",
));
}
if suggestions.len() > 0 {
diag.multipart_suggestion(
"format specifiers use curly braces",
suggestions,
Applicability::MachineApplicable,
);
}
}};
}
check_foreign!(printf);
if !found_foreign {
check_foreign!(shell);
}
}
if !found_foreign && unused.len() == 1 {
diag.span_label(fmt_span, "formatting specifier missing");
}
diag.emit();
}
fn report_invalid_references(
ecx: &mut ExtCtxt<'_>,
invalid_refs: &[(usize, Option<Span>, PositionUsedAs, FormatArgPositionKind)],
template: &[FormatArgsPiece],
fmt_span: Span,
args: &FormatArguments,
parser: parse::Parser<'_>,
) {
let num_args_desc = match args.explicit_args().len() {
0 => "no arguments were given".to_string(),
1 => "there is 1 argument".to_string(),
n => format!("there are {n} arguments"),
};
let mut e;
if template.iter().all(|piece| match piece {
FormatArgsPiece::Placeholder(FormatPlaceholder {
argument: FormatArgPosition { kind: FormatArgPositionKind::Number, .. },
..
}) => false,
FormatArgsPiece::Placeholder(FormatPlaceholder {
format_options:
FormatOptions {
precision:
Some(FormatCount::Argument(FormatArgPosition {
kind: FormatArgPositionKind::Number,
..
})),
..
}
| FormatOptions {
width:
Some(FormatCount::Argument(FormatArgPosition {
kind: FormatArgPositionKind::Number,
..
})),
..
},
..
}) => false,
_ => true,
}) {
let mut spans = Vec::new();
let mut num_placeholders = 0;
for piece in template {
let mut placeholder = None;
if let FormatArgsPiece::Placeholder(FormatPlaceholder {
format_options:
FormatOptions {
precision:
Some(FormatCount::Argument(FormatArgPosition {
span,
kind: FormatArgPositionKind::Implicit,
..
})),
..
},
..
}) = piece
{
placeholder = *span;
num_placeholders += 1;
}
if let FormatArgsPiece::Placeholder(FormatPlaceholder {
argument: FormatArgPosition { kind: FormatArgPositionKind::Implicit, .. },
span,
..
}) = piece
{
placeholder = *span;
num_placeholders += 1;
}
spans.extend(placeholder);
}
let span = if spans.is_empty() {
MultiSpan::from_span(fmt_span)
} else {
MultiSpan::from_spans(spans)
};
e = ecx.create_err(errors::FormatPositionalMismatch {
span,
n: num_placeholders,
desc: num_args_desc,
highlight: SingleLabelManySpans {
spans: args.explicit_args().iter().map(|arg| arg.expr.span).collect(),
label: "",
kind: rustc_errors::LabelKind::Label,
},
});
let mut has_precision_star = false;
for piece in template {
if let FormatArgsPiece::Placeholder(FormatPlaceholder {
format_options:
FormatOptions {
precision:
Some(FormatCount::Argument(FormatArgPosition {
index,
span: Some(span),
kind: FormatArgPositionKind::Implicit,
..
})),
..
},
..
}) = piece
{
let (Ok(index) | Err(index)) = index;
has_precision_star = true;
e.span_label(
*span,
format!(
"this precision flag adds an extra required argument at position {}, which is why there {} expected",
index,
if num_placeholders == 1 {
"is 1 argument".to_string()
} else {
format!("are {num_placeholders} arguments")
},
),
);
}
}
if has_precision_star {
e.note("positional arguments are zero-based");
}
} else {
let mut indexes: Vec<_> = invalid_refs.iter().map(|&(index, _, _, _)| index).collect();
indexes.sort();
indexes.dedup();
let span: MultiSpan = if !parser.is_source_literal || parser.arg_places.is_empty() {
MultiSpan::from_span(fmt_span)
} else {
MultiSpan::from_spans(invalid_refs.iter().filter_map(|&(_, span, _, _)| span).collect())
};
let arg_list = if let &[index] = &indexes[..] {
format!("argument {index}")
} else {
let tail = indexes.pop().unwrap();
format!(
"arguments {head} and {tail}",
head = indexes.into_iter().map(|i| i.to_string()).collect::<Vec<_>>().join(", ")
)
};
e = ecx.struct_span_err(
span,
format!("invalid reference to positional {arg_list} ({num_args_desc})"),
);
e.note("positional arguments are zero-based");
}
if template.iter().any(|piece| match piece {
FormatArgsPiece::Placeholder(FormatPlaceholder { format_options: f, .. }) => {
*f != FormatOptions::default()
}
_ => false,
}) {
e.note("for information about formatting flags, visit https://doc.rust-lang.org/std/fmt/index.html");
}
e.emit();
}
fn expand_format_args_impl<'cx>(
ecx: &'cx mut ExtCtxt<'_>,
mut sp: Span,
tts: TokenStream,
nl: bool,
) -> Box<dyn base::MacResult + 'cx> {
sp = ecx.with_def_site_ctxt(sp);
match parse_args(ecx, sp, tts) {
Ok(input) => {
if let Ok(format_args) = make_format_args(ecx, input, nl) {
MacEager::expr(ecx.expr(sp, ExprKind::FormatArgs(P(format_args))))
} else {
MacEager::expr(DummyResult::raw_expr(sp, true))
}
}
Err(mut err) => {
err.emit();
DummyResult::any(sp)
}
}
}
pub fn expand_format_args<'cx>(
ecx: &'cx mut ExtCtxt<'_>,
sp: Span,
tts: TokenStream,
) -> Box<dyn base::MacResult + 'cx> {
expand_format_args_impl(ecx, sp, tts, false)
}
pub fn expand_format_args_nl<'cx>(
ecx: &'cx mut ExtCtxt<'_>,
sp: Span,
tts: TokenStream,
) -> Box<dyn base::MacResult + 'cx> {
expand_format_args_impl(ecx, sp, tts, true)
}