| use std::ops::Range; | |
| use parse::Position::ArgumentNamed; | |
| use rustc_ast::tokenstream::TokenStream; | |
| use rustc_ast::{ | |
| Expr, ExprKind, FormatAlignment, FormatArgPosition, FormatArgPositionKind, FormatArgs, | |
| FormatArgsPiece, FormatArgument, FormatArgumentKind, FormatArguments, FormatCount, | |
| FormatDebugHex, FormatOptions, FormatPlaceholder, FormatSign, FormatTrait, Recovered, StmtKind, | |
| token, | |
| }; | |
| use rustc_data_structures::fx::FxHashSet; | |
| use rustc_errors::{ | |
| Applicability, BufferedEarlyLint, DecorateDiagCompat, Diag, Diagnostic, MultiSpan, PResult, | |
| SingleLabelManySpans, listify, pluralize, | |
| }; | |
| use rustc_expand::base::*; | |
| use rustc_lint_defs::LintId; | |
| use rustc_lint_defs::builtin::NAMED_ARGUMENTS_USED_POSITIONALLY; | |
| use rustc_parse::exp; | |
| use rustc_parse_format as parse; | |
| use rustc_span::{BytePos, ErrorGuaranteed, Ident, InnerSpan, Span, Symbol}; | |
| use crate::diagnostics; | |
| use crate::util::{ExprToSpannedString, expr_to_spanned_string}; | |
| // The format_args!() macro is expanded in three steps: | |
| // 1. First, `parse_args` will parse the `(literal, arg, arg, name=arg, name=arg)` syntax, | |
| // but doesn't parse the template (the literal) itself. | |
| // 2. Second, `make_format_args` will parse the template, the format options, resolve argument references, | |
| // produce diagnostics, and turn the whole thing into a `FormatArgs` AST node. | |
| // 3. Much later, in AST lowering (rustc_ast_lowering), that `FormatArgs` structure will be turned | |
| // into the expression of type `core::fmt::Arguments`. | |
| // See rustc_ast/src/format.rs for the FormatArgs structure and glossary. | |
| // Only used in parse_args and report_invalid_references, | |
| // to indicate how a referred argument was used. | |
| #[derive(Clone, Copy, Debug, PartialEq, Eq)] | |
| enum PositionUsedAs { | |
| Placeholder(Option<Span>), | |
| Precision, | |
| Width, | |
| } | |
| use PositionUsedAs::*; | |
| #[derive(Debug)] | |
| struct MacroInput { | |
| fmtstr: Box<Expr>, | |
| args: FormatArguments, | |
| /// Whether the first argument was a string literal or a result from eager macro expansion. | |
| /// If it's not a string literal, we disallow implicit argument capturing. | |
| /// | |
| /// This does not correspond to whether we can treat spans to the literal normally, as the whole | |
| /// invocation might be the result of another macro expansion, in which case this flag may still be true. | |
| /// | |
| /// See [RFC 2795] for more information. | |
| /// | |
| /// [RFC 2795]: https://rust-lang.github.io/rfcs/2795-format-args-implicit-identifiers.html#macro-hygiene | |
| is_direct_literal: bool, | |
| } | |
| /// Parses the arguments from the given list of tokens, returning the diagnostic | |
| /// if there's a parse error so we can continue parsing other format! | |
| /// expressions. | |
| /// | |
| /// If parsing succeeds, the return value is: | |
| /// | |
| /// ```text | |
| /// Ok((fmtstr, parsed arguments)) | |
| /// ``` | |
| fn parse_args<'a>(ecx: &ExtCtxt<'a>, sp: Span, tts: TokenStream) -> PResult<'a, MacroInput> { | |
| let mut p = ecx.new_parser_from_tts(tts); | |
| // parse the format string | |
| let fmtstr = match p.token.kind { | |
| token::Eof => { | |
| return Err(ecx.dcx().create_err(diagnostics::FormatRequiresString { span: sp })); | |
| } | |
| // This allows us to properly handle cases when the first comma | |
| // after the format string is mistakenly replaced with any operator, | |
| // which cause the expression parser to eat too much tokens. | |
| token::Literal(token::Lit { kind: token::Str | token::StrRaw(_), .. }) => { | |
| p.parse_literal_maybe_minus()? | |
| } | |
| // Otherwise, we fall back to the expression parser. | |
| _ => p.parse_expr()?, | |
| }; | |
| // parse comma FormatArgument pairs | |
| let mut args = FormatArguments::new(); | |
| let mut first = true; | |
| while p.token != token::Eof { | |
| // parse a comma, or else report an error | |
| if !p.eat(exp!(Comma)) { | |
| if first { | |
| p.clear_expected_token_types(); | |
| } | |
| match p.expect(exp!(Comma)) { | |
| Err(err) => { | |
| if token::TokenKind::Comma.similar_tokens().contains(&p.token.kind) { | |
| // If a similar token is found, then it may be a typo. We | |
| // consider it as a comma, and continue parsing. | |
| err.emit(); | |
| p.bump(); | |
| } else { | |
| // Otherwise stop the parsing and return the error. | |
| return Err(err); | |
| } | |
| } | |
| Ok(Recovered::Yes(_)) => (), | |
| Ok(Recovered::No) => unreachable!(), | |
| } | |
| } | |
| first = false; | |
| // accept a trailing comma | |
| if p.token == token::Eof { | |
| break; | |
| } | |
| // parse a FormatArgument | |
| match p.token.ident() { | |
| Some((ident, _)) if p.look_ahead(1, |t| *t == token::Eq) => { | |
| p.bump(); | |
| p.expect(exp!(Eq))?; | |
| let expr = p.parse_expr()?; | |
| if let Some((_, prev)) = args.by_name(ident.name) { | |
| ecx.dcx().emit_err(diagnostics::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() { | |
| return Err(ecx.dcx().create_err(diagnostics::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 }); | |
| } | |
| } | |
| } | |
| // Only allow implicit captures for direct literals | |
| let is_direct_literal = matches!(fmtstr.kind, ExprKind::Lit(_)); | |
| Ok(MacroInput { fmtstr, args, is_direct_literal }) | |
| } | |
| fn make_format_args( | |
| ecx: &mut ExtCtxt<'_>, | |
| input: MacroInput, | |
| append_newline: bool, | |
| macro_span: Span, | |
| ) -> ExpandResult<Result<FormatArgs, ErrorGuaranteed>, ()> { | |
| 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 ExprToSpannedString { | |
| symbol: fmt_str, | |
| span: fmt_span, | |
| style: fmt_style, | |
| uncooked_symbol: uncooked_fmt_str, | |
| } = { | |
| // Extract snippet so that we can check cases `{}`, `{:?}` and `{:#?}` and emit help for | |
| // them later. | |
| let snippet = if let ExprKind::Block(b, None) = &efmt.kind | |
| && b.stmts.len() <= 1 | |
| { | |
| Some(ecx.sess.source_map().span_to_snippet(unexpanded_fmt_span)) | |
| } else { | |
| None | |
| }; | |
| let ExpandResult::Ready(mac) = expr_to_spanned_string(ecx, efmt.clone(), msg) else { | |
| return ExpandResult::Retry(()); | |
| }; | |
| match mac { | |
| Ok(mut fmt) if append_newline => { | |
| fmt.symbol = Symbol::intern(&format!("{}\n", fmt.symbol)); | |
| fmt | |
| } | |
| Ok(fmt) => fmt, | |
| Err(err) => { | |
| let guar = match err { | |
| Ok((mut err, suggested)) => { | |
| if !suggested { | |
| if let ExprKind::Block(block, None) = &efmt.kind | |
| && let [stmt] = block.stmts.as_slice() | |
| && let StmtKind::Expr(expr) = &stmt.kind | |
| && let ExprKind::Path(None, path) = &expr.kind | |
| && path.segments.len() == 1 | |
| && path.segments[0].args.is_none() | |
| { | |
| 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 { | |
| // `{}` or `()` | |
| let should_suggest = |kind: &ExprKind| -> bool { | |
| match kind { | |
| ExprKind::Block(b, None) if b.stmts.is_empty() => true, | |
| ExprKind::Tup(v) if v.is_empty() => true, | |
| _ => false, | |
| } | |
| }; | |
| let mut sugg_fmt = String::new(); | |
| for kind in std::iter::once(&efmt.kind) | |
| .chain(args.explicit_args().into_iter().map(|a| &a.expr.kind)) | |
| { | |
| sugg_fmt.push_str(if should_suggest(kind) { | |
| "{:?} " | |
| } else { | |
| "{} " | |
| }); | |
| } | |
| sugg_fmt = sugg_fmt.trim_end().to_string(); | |
| err.span_suggestion_verbose( | |
| unexpanded_fmt_span.shrink_to_lo(), | |
| "you might be missing a string literal to format with", | |
| format!("\"{sugg_fmt}\", "), | |
| Applicability::MaybeIncorrect, | |
| ); | |
| if let Some(Ok(snippet)) = snippet.as_ref() { | |
| match snippet.as_str() { | |
| "{}" | "{:?}" | "{:#?}" => { | |
| err.span_suggestion_verbose( | |
| unexpanded_fmt_span, | |
| format!("you might want to enclose `{snippet}` with `\"\"`"), | |
| format!("\"{snippet}\""), | |
| Applicability::MaybeIncorrect, | |
| ); | |
| } | |
| _ => {} | |
| }; | |
| } | |
| } | |
| } | |
| err.emit() | |
| } | |
| Err(guar) => guar, | |
| }; | |
| return ExpandResult::Ready(Err(guar)); | |
| } | |
| } | |
| }; | |
| 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(); // for the suggestions below | |
| 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 { | |
| // The format string could be another macro invocation, e.g.: | |
| // format!(concat!("abc", "{}"), 4); | |
| // However, `err.span` is an inner span relative to the *result* of | |
| // the macro invocation, which is why we would get a nonsensical | |
| // result calling `fmt_span.from_inner(err.span)` as above, and | |
| // might even end up inside a multibyte character (issue #86085). | |
| // Therefore, we conservatively report the error for the entire | |
| // argument span here. | |
| fmt_span | |
| }; | |
| let mut e = diagnostics::InvalidFormatString { | |
| span: sp, | |
| note_: None, | |
| label_: None, | |
| sugg_: None, | |
| desc: err.description, | |
| label1: err.label, | |
| }; | |
| if let Some(note) = err.note { | |
| e.note_ = Some(diagnostics::InvalidFormatStringNote { note }); | |
| } | |
| if let Some((label, span)) = err.secondary_label | |
| && is_source_literal | |
| { | |
| e.label_ = Some(diagnostics::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(diagnostics::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(diagnostics::InvalidFormatStringSuggestion::RemoveRawIdent { span }) | |
| } | |
| } | |
| parse::Suggestion::ReorderFormatParameter(span, replacement) => { | |
| let span = fmt_span.from_inner(InnerSpan::new(span.start, span.end)); | |
| e.sugg_ = | |
| Some(diagnostics::InvalidFormatStringSuggestion::ReorderFormatParameter { | |
| span, | |
| replacement, | |
| }); | |
| } | |
| parse::Suggestion::AddMissingColon(span) => { | |
| let span = fmt_span.from_inner(InnerSpan::new(span.start, span.end)); | |
| e.sugg_ = | |
| Some(diagnostics::InvalidFormatStringSuggestion::AddMissingColon { span }); | |
| } | |
| parse::Suggestion::UseRustDebugPrintingMacro => { | |
| // This targets `println!("{=}", x);` and `println!("{0=}", x);` | |
| if let [arg] = args.all_args() { | |
| let expr_span = arg.expr.span; | |
| if let Ok(expr_snippet) = ecx.source_map().span_to_snippet(expr_span) { | |
| let replacement = format!("{}!({})", "dbg", expr_snippet); | |
| let call_span = macro_span.source_callsite(); | |
| e.sugg_ = Some( | |
| diagnostics::InvalidFormatStringSuggestion::UseRustDebugPrintingMacro { | |
| macro_span: call_span, | |
| replacement, | |
| }, | |
| ); | |
| } | |
| } | |
| } | |
| } | |
| let guar = ecx.dcx().emit_err(e); | |
| return ExpandResult::Ready(Err(guar)); | |
| } | |
| let to_span = |inner_span: Range<usize>| { | |
| 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_references_to_named_arg = Vec::new(); | |
| enum ArgRef<'a> { | |
| Index(usize), | |
| Name(&'a str, Option<Span>), | |
| } | |
| use ArgRef::*; | |
| let mut unnamed_arg_after_named_arg = false; | |
| 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() { | |
| // This was a named argument, but it was used as a positional argument. | |
| numeric_references_to_named_arg.push((index, span, used_as)); | |
| } | |
| Ok(index) | |
| } else { | |
| // Doesn't exist as an explicit argument. | |
| 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) { | |
| // Name found in `args`, so we resolve it to its index. | |
| if index < args.explicit_args().len() { | |
| // Mark it as used, if it was an explicit argument. | |
| used[index] = true; | |
| } | |
| Ok(index) | |
| } else { | |
| // Name not found in `args`, so we add it as an implicitly captured argument. | |
| 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 { | |
| // For the moment capturing variables from format strings expanded from macros is | |
| // disabled (see RFC #2795) | |
| let guar = ecx.dcx().emit_err(diagnostics::FormatNoArgNamed { span, name }); | |
| unnamed_arg_after_named_arg = true; | |
| DummyResult::raw_expr(span, Some(guar)) | |
| }; | |
| 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.clone() { | |
| parse::Piece::Lit(s) => { | |
| unfinished_literal.push_str(s); | |
| } | |
| parse::Piece::NextArgument(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.clone())); | |
| 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<_>>(); | |
| let has_unused = !unused.is_empty(); | |
| if has_unused { | |
| // If there's a lot of unused arguments, | |
| // let's check if this format arguments looks like another syntax (printf / shell). | |
| let detect_foreign_fmt = unused.len() > args.explicit_args().len() / 2; | |
| let foreign_fmt_str = | |
| if append_newline { fmt_str.strip_suffix('\n').unwrap_or(fmt_str) } else { fmt_str }; | |
| report_missing_placeholders( | |
| ecx, | |
| unused, | |
| &used, | |
| &args, | |
| &pieces, | |
| &invalid_refs, | |
| detect_foreign_fmt, | |
| str_style, | |
| foreign_fmt_str, | |
| uncooked_fmt_str.1.as_str(), | |
| fmt_span, | |
| ); | |
| } | |
| // Only check for unused named argument names if there are no other errors to avoid causing | |
| // too much noise in output errors, such as when a named argument is entirely unused. | |
| if invalid_refs.is_empty() && !has_unused && !unnamed_arg_after_named_arg { | |
| for &(index, span, used_as) in &numeric_references_to_named_arg { | |
| let (position_sp_to_replace, position_sp_for_msg) = match used_as { | |
| Placeholder(pspan) => (span, pspan), | |
| Precision => { | |
| // Strip the leading `.` for 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: Some(arg_name.span.into()), | |
| node_id: rustc_ast::CRATE_NODE_ID, | |
| lint_id: LintId::of(NAMED_ARGUMENTS_USED_POSITIONALLY), | |
| diagnostic: DecorateDiagCompat(Box::new(move |dcx, level, sess| { | |
| let (suggestion, name) = | |
| if let Some(positional_arg_to_replace) = position_sp_to_replace { | |
| let mut name = arg_name.name.to_string(); | |
| let is_formatting_arg = matches!(used_as, Width | Precision); | |
| if is_formatting_arg { | |
| name.push('$') | |
| }; | |
| let span_to_replace = if let Ok(positional_arg_content) = sess | |
| .downcast_ref::<rustc_session::Session>() | |
| .expect("expected a `Session`") | |
| .source_map() | |
| .span_to_snippet(positional_arg_to_replace) | |
| && positional_arg_content.starts_with(':') | |
| { | |
| positional_arg_to_replace.shrink_to_lo() | |
| } else { | |
| positional_arg_to_replace | |
| }; | |
| (Some(span_to_replace), name) | |
| } else { | |
| (None, String::new()) | |
| }; | |
| diagnostics::NamedArgumentUsedPositionally { | |
| named_arg_sp: arg_name.span, | |
| position_label_sp: position_sp_for_msg, | |
| suggestion, | |
| name, | |
| named_arg_name: arg_name.name.to_string(), | |
| } | |
| .into_diag(dcx, level) | |
| })), | |
| }); | |
| } | |
| } | |
| ExpandResult::Ready(Ok(FormatArgs { | |
| span: fmt_span, | |
| template, | |
| arguments: args, | |
| uncooked_fmt_str, | |
| is_source_literal, | |
| })) | |
| } | |
| fn invalid_placeholder_type_error( | |
| ecx: &ExtCtxt<'_>, | |
| ty: &str, | |
| ty_span: Option<Range<usize>>, | |
| 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)| diagnostics::FormatUnknownTraitSugg { span: sp, fmt, trait_name }) | |
| .collect() | |
| } else { | |
| vec![] | |
| }; | |
| ecx.dcx().emit_err(diagnostics::FormatUnknownTrait { span: sp.unwrap_or(fmt_span), ty, suggs }); | |
| } | |
| fn report_missing_placeholders( | |
| ecx: &ExtCtxt<'_>, | |
| unused: Vec<(Span, bool)>, | |
| used: &[bool], | |
| args: &FormatArguments, | |
| pieces: &[parse::Piece<'_>], | |
| invalid_refs: &[(usize, Option<Span>, PositionUsedAs, FormatArgPositionKind)], | |
| detect_foreign_fmt: bool, | |
| str_style: Option<usize>, | |
| fmt_str: &str, | |
| uncooked_fmt_str: &str, | |
| fmt_span: Span, | |
| ) { | |
| let mut diag = if let &[(span, named)] = &unused[..] { | |
| ecx.dcx().create_err(diagnostics::FormatUnusedArg { span, named }) | |
| } else { | |
| let unused_labels = unused | |
| .iter() | |
| .map(|&(span, named)| diagnostics::FormatUnusedArg { span, named }) | |
| .collect(); | |
| let unused_spans = unused.iter().map(|&(span, _)| span).collect(); | |
| ecx.dcx().create_err(diagnostics::FormatUnusedArgs { | |
| fmt: fmt_span, | |
| unused: unused_spans, | |
| unused_labels, | |
| }) | |
| }; | |
| let placeholders = pieces | |
| .iter() | |
| .filter_map(|piece| { | |
| if let parse::Piece::NextArgument(argument) = piece | |
| && let ArgumentNamed(binding) = argument.position | |
| { | |
| let span = fmt_span.from_inner(InnerSpan::new( | |
| argument.position_span.start, | |
| argument.position_span.end, | |
| )); | |
| Some((span, binding)) | |
| } else { | |
| None | |
| } | |
| }) | |
| .collect::<Vec<_>>(); | |
| if !placeholders.is_empty() { | |
| if let Some(new_diag) = report_redundant_format_arguments(ecx, args, used, placeholders) { | |
| diag.cancel(); | |
| new_diag.emit(); | |
| return; | |
| } | |
| } | |
| // Used to ensure we only report translations for *one* kind of foreign format. | |
| let mut found_foreign = false; | |
| // Decide if we want to look for foreign formatting directives. | |
| if detect_foreign_fmt { | |
| use super::format_foreign as foreign; | |
| // The set of foreign substitutions we've explained. This prevents spamming the user | |
| // with `%d should be written as {}` over and over again. | |
| let mut explained = FxHashSet::default(); | |
| macro_rules! check_foreign { | |
| ($kind:ident) => {{ | |
| let mut show_doc_note = false; | |
| let mut suggestions = vec![]; | |
| // account for `"` and account for raw strings `r#` | |
| 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), | |
| // If it has no translation, don't call it out specifically. | |
| _ => continue, | |
| }; | |
| let pos = sub.position(); | |
| if !explained.insert(sub.to_string()) { | |
| continue; | |
| } | |
| if !found_foreign { | |
| found_foreign = true; | |
| show_doc_note = true; | |
| } | |
| let sp = fmt_span.from_inner(pos); | |
| if success { | |
| suggestions.push((sp, trn)); | |
| } else { | |
| diag.span_note( | |
| sp, | |
| format!("format specifiers use curly braces, and {}", 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"); | |
| } | |
| if !found_foreign && invalid_refs.is_empty() { | |
| // Show example if user didn't use any format specifiers | |
| let show_example = !used.contains(&true); | |
| if !show_example { | |
| if unused.len() > 1 { | |
| diag.note(format!("consider adding {} format specifiers", unused.len())); | |
| } | |
| } else { | |
| let msg = if unused.len() == 1 { | |
| "a format specifier".to_string() | |
| } else { | |
| format!("{} format specifiers", unused.len()) | |
| }; | |
| let sugg = match str_style { | |
| None => format!("\"{}{}\"", uncooked_fmt_str, "{}".repeat(unused.len())), | |
| Some(n_hashes) => format!( | |
| "r{hashes}\"{uncooked_fmt_str}{fmt_specifiers}\"{hashes}", | |
| hashes = "#".repeat(n_hashes), | |
| fmt_specifiers = "{}".repeat(unused.len()) | |
| ), | |
| }; | |
| let msg = format!("format specifiers use curly braces, consider adding {msg}"); | |
| diag.span_suggestion_verbose(fmt_span, msg, sugg, Applicability::MaybeIncorrect); | |
| } | |
| } | |
| diag.emit(); | |
| } | |
| /// This function detects and reports unused format!() arguments that are | |
| /// redundant due to implicit captures (e.g. `format!("{x}", x)`). | |
| fn report_redundant_format_arguments<'a>( | |
| ecx: &ExtCtxt<'a>, | |
| args: &FormatArguments, | |
| used: &[bool], | |
| placeholders: Vec<(Span, &str)>, | |
| ) -> Option<Diag<'a>> { | |
| let mut fmt_arg_indices = vec![]; | |
| let mut args_spans = vec![]; | |
| let mut fmt_spans = vec![]; | |
| for (i, unnamed_arg) in args.unnamed_args().iter().enumerate().rev() { | |
| let Some(ty) = unnamed_arg.expr.to_ty() else { continue }; | |
| let Some(argument_binding) = ty.kind.is_simple_path() else { continue }; | |
| let argument_binding = argument_binding.as_str(); | |
| if used[i] { | |
| continue; | |
| } | |
| let matching_placeholders = placeholders | |
| .iter() | |
| .filter(|(_, inline_binding)| argument_binding == *inline_binding) | |
| .map(|(span, _)| span) | |
| .collect::<Vec<_>>(); | |
| if !matching_placeholders.is_empty() { | |
| fmt_arg_indices.push(i); | |
| args_spans.push(unnamed_arg.expr.span); | |
| for span in &matching_placeholders { | |
| if fmt_spans.contains(*span) { | |
| continue; | |
| } | |
| fmt_spans.push(**span); | |
| } | |
| } | |
| } | |
| if !args_spans.is_empty() { | |
| let multispan = MultiSpan::from(fmt_spans); | |
| let mut suggestion_spans = vec![]; | |
| for (arg_span, fmt_arg_idx) in args_spans.iter().zip(fmt_arg_indices.iter()) { | |
| let span = if fmt_arg_idx + 1 == args.explicit_args().len() { | |
| *arg_span | |
| } else { | |
| arg_span.until(args.explicit_args()[*fmt_arg_idx + 1].expr.span) | |
| }; | |
| suggestion_spans.push(span); | |
| } | |
| let sugg = if args.named_args().len() == 0 { | |
| Some(diagnostics::FormatRedundantArgsSugg { spans: suggestion_spans }) | |
| } else { | |
| None | |
| }; | |
| return Some(ecx.dcx().create_err(diagnostics::FormatRedundantArgs { | |
| n: args_spans.len(), | |
| span: MultiSpan::from(args_spans), | |
| note: multispan, | |
| sugg, | |
| })); | |
| } | |
| None | |
| } | |
| /// Handle invalid references to positional arguments. Output different | |
| /// errors for the case where all arguments are positional and for when | |
| /// there are named arguments or numbered positional arguments in the | |
| /// format string. | |
| fn report_invalid_references( | |
| ecx: &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, | |
| }) { | |
| // There are no numeric positions. | |
| // Collect all the implicit positions: | |
| let mut spans = Vec::new(); | |
| let mut num_placeholders = 0; | |
| let mut has_white_space_only_missing_arg = false; | |
| for piece in template { | |
| let mut placeholder = None; | |
| // `{arg:.*}` | |
| 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, index, .. }, | |
| span, | |
| .. | |
| }) = piece | |
| { | |
| placeholder = *span; | |
| num_placeholders += 1; | |
| // Check whether there's any non-space whitespace in the placeholder. If so, we should emit a note suggesting an escaping `{`. | |
| if index.is_err() | |
| && let Some(span) = span | |
| && let Ok(snippet) = ecx.source_map().span_to_snippet(*span) | |
| && snippet.chars().any(|c| c.is_whitespace() && c != ' ') | |
| { | |
| has_white_space_only_missing_arg = true; | |
| } | |
| } | |
| // For `{:.*}`, we only push one span. | |
| spans.extend(placeholder); | |
| } | |
| let span = if spans.is_empty() { | |
| MultiSpan::from_span(fmt_span) | |
| } else { | |
| MultiSpan::from_spans(spans) | |
| }; | |
| e = ecx.dcx().create_err(diagnostics::FormatPositionalMismatch { | |
| span, | |
| n: num_placeholders, | |
| desc: num_args_desc, | |
| highlight: SingleLabelManySpans { | |
| spans: args.explicit_args().iter().map(|arg| arg.expr.span).collect(), | |
| label: "", | |
| }, | |
| }); | |
| // Point out `{:.*}` placeholders: those take an extra argument. | |
| 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"); | |
| } | |
| if has_white_space_only_missing_arg { | |
| e.note("if you intended to print `{`, you can escape it with `{{`"); | |
| } | |
| } else { | |
| let mut indexes: Vec<_> = invalid_refs.iter().map(|&(index, _, _, _)| index).collect(); | |
| // Avoid `invalid reference to positional arguments 7 and 7 (there is 1 argument)` | |
| // for `println!("{7:7$}", 1);` | |
| 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 = format!( | |
| "argument{} {}", | |
| pluralize!(indexes.len()), | |
| listify(&indexes, |i: &usize| i.to_string()).unwrap_or_default() | |
| ); | |
| e = ecx.dcx().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, | |
| ) -> MacroExpanderResult<'cx> { | |
| sp = ecx.with_def_site_ctxt(sp); | |
| ExpandResult::Ready(match parse_args(ecx, sp, tts) { | |
| Ok(input) => { | |
| let ExpandResult::Ready(mac) = make_format_args(ecx, input, nl, sp) else { | |
| return ExpandResult::Retry(()); | |
| }; | |
| match mac { | |
| Ok(format_args) => { | |
| MacEager::expr(ecx.expr(sp, ExprKind::FormatArgs(Box::new(format_args)))) | |
| } | |
| Err(guar) => MacEager::expr(DummyResult::raw_expr(sp, Some(guar))), | |
| } | |
| } | |
| Err(err) => { | |
| let guar = err.emit(); | |
| DummyResult::any(sp, guar) | |
| } | |
| }) | |
| } | |
| pub(crate) fn expand_format_args<'cx>( | |
| ecx: &'cx mut ExtCtxt<'_>, | |
| sp: Span, | |
| tts: TokenStream, | |
| ) -> MacroExpanderResult<'cx> { | |
| expand_format_args_impl(ecx, sp, tts, false) | |
| } | |
| pub(crate) fn expand_format_args_nl<'cx>( | |
| ecx: &'cx mut ExtCtxt<'_>, | |
| sp: Span, | |
| tts: TokenStream, | |
| ) -> MacroExpanderResult<'cx> { | |
| expand_format_args_impl(ecx, sp, tts, true) | |
| } |