Skip to content

Commit 3330c7d

Browse files
committed
Generate correct suggestion with named arguments used positionally
Address issue #99265 by checking each positionally used argument to see if the argument is named and adding a lint to use the name instead. This way, when named arguments are used positionally in a different order than their argument order, the suggested lint is correct. For example: ``` println!("{b} {}", a=1, b=2); ``` This will now generate the suggestion: ``` println!("{b} {a}", a=1, b=2); ``` Additionally, this check now also correctly replaces or inserts only where the positional argument is (or would be if implicit). Also, width and precision are replaced with their argument names when they exists. Since the issues were so closely related, this fix for issue #99265 also fixes issue #99266. Fixes #99265 Fixes #99266
1 parent 530c0a8 commit 3330c7d

File tree

12 files changed

+1093
-102
lines changed

12 files changed

+1093
-102
lines changed

compiler/rustc_builtin_macros/src/asm.rs

+1-1
Original file line numberDiff line numberDiff line change
@@ -656,7 +656,7 @@ fn expand_preparsed_asm(ecx: &mut ExtCtxt<'_>, args: AsmArgs) -> Option<ast::Inl
656656
let span = arg_spans.next().unwrap_or(template_sp);
657657

658658
let operand_idx = match arg.position {
659-
parse::ArgumentIs(idx) | parse::ArgumentImplicitlyIs(idx) => {
659+
parse::ArgumentIs(idx, _) | parse::ArgumentImplicitlyIs(idx) => {
660660
if idx >= args.operands.len()
661661
|| named_pos.contains_key(&idx)
662662
|| args.reg_args.contains(&idx)

compiler/rustc_builtin_macros/src/format.rs

+187-60
Original file line numberDiff line numberDiff line change
@@ -11,14 +11,14 @@ use rustc_errors::{pluralize, Applicability, MultiSpan, PResult};
1111
use rustc_expand::base::{self, *};
1212
use rustc_parse_format as parse;
1313
use rustc_span::symbol::{sym, Ident, Symbol};
14-
use rustc_span::{InnerSpan, Span};
14+
use rustc_span::{BytePos, InnerSpan, Span};
1515
use smallvec::SmallVec;
1616

1717
use rustc_lint_defs::builtin::NAMED_ARGUMENTS_USED_POSITIONALLY;
1818
use rustc_lint_defs::{BufferedEarlyLint, BuiltinLintDiagnostics, LintId};
19-
use rustc_parse_format::Count;
2019
use std::borrow::Cow;
2120
use std::collections::hash_map::Entry;
21+
use std::ops::Deref;
2222

2323
#[derive(PartialEq)]
2424
enum ArgumentType {
@@ -32,6 +32,105 @@ enum Position {
3232
Named(Symbol, InnerSpan),
3333
}
3434

35+
/// Indicates how positional named argument (i.e. an named argument which is used by position
36+
/// instead of by name) is used in format string
37+
/// * `Arg` is the actual argument to print
38+
/// * `Width` is width format argument
39+
/// * `Precision` is precion format argument
40+
/// Example: `{Arg:Width$.Precision$}
41+
#[derive(Debug, Eq, PartialEq)]
42+
enum PositionalNamedArgType {
43+
Arg,
44+
Width,
45+
Precision,
46+
}
47+
48+
/// Contains information necessary to create a lint for a positional named argument
49+
#[derive(Debug)]
50+
struct PositionalNamedArg {
51+
ty: PositionalNamedArgType,
52+
/// The piece of the using this argument (multiple pieces can use the same argument)
53+
cur_piece: usize,
54+
/// The InnerSpan for in the string to be replaced with the named argument
55+
/// This will be None when the position is implicit
56+
inner_span_to_replace: Option<rustc_parse_format::InnerSpan>,
57+
/// The name to use instead of the position
58+
replacement: Symbol,
59+
/// The span for the positional named argument (so the lint can point a message to it)
60+
positional_named_arg_span: Span,
61+
}
62+
63+
impl PositionalNamedArg {
64+
/// Determines what span to replace with the name of the named argument
65+
fn get_span_to_replace(&self, cx: &Context<'_, '_>) -> Option<Span> {
66+
if let Some(inner_span) = &self.inner_span_to_replace {
67+
return match self.ty {
68+
PositionalNamedArgType::Arg | PositionalNamedArgType::Width => Some(Span::new(
69+
cx.fmtsp.lo() + BytePos(inner_span.start.try_into().unwrap()),
70+
cx.fmtsp.lo() + BytePos(inner_span.end.try_into().unwrap()),
71+
self.positional_named_arg_span.ctxt(),
72+
self.positional_named_arg_span.parent(),
73+
)),
74+
PositionalNamedArgType::Precision => Some(Span::new(
75+
cx.fmtsp.lo() + BytePos(inner_span.start.try_into().unwrap()) + BytePos(1),
76+
cx.fmtsp.lo() + BytePos(inner_span.end.try_into().unwrap()),
77+
self.positional_named_arg_span.ctxt(),
78+
self.positional_named_arg_span.parent(),
79+
)),
80+
};
81+
} else if self.ty == PositionalNamedArgType::Arg {
82+
// In the case of a named argument whose position is implicit, there will not be a span
83+
// to replace. Instead, we insert the name after the `{`, which is the first character
84+
// of arg_span.
85+
if let Some(arg_span) = cx.arg_spans.get(self.cur_piece).copied() {
86+
return Some(Span::new(
87+
arg_span.lo() + BytePos(1),
88+
arg_span.lo() + BytePos(1),
89+
self.positional_named_arg_span.ctxt(),
90+
self.positional_named_arg_span.parent(),
91+
));
92+
}
93+
}
94+
95+
None
96+
}
97+
}
98+
99+
/// Encapsulates all the named arguments that have been used positionally
100+
#[derive(Debug)]
101+
struct PositionalNamedArgsLint {
102+
positional_named_args: Vec<PositionalNamedArg>,
103+
}
104+
105+
impl PositionalNamedArgsLint {
106+
/// Try constructing a PositionalNamedArg struct and pushing it into the vec of positional
107+
/// named arguments. If a named arg associated with `format_argument_index` cannot be found,
108+
/// a new item will not be added as the lint cannot be emitted in this case.
109+
fn maybe_push(
110+
&mut self,
111+
format_argument_index: usize,
112+
ty: PositionalNamedArgType,
113+
cur_piece: usize,
114+
inner_span: Option<rustc_parse_format::InnerSpan>,
115+
names: &FxHashMap<Symbol, (usize, Span)>,
116+
) {
117+
let named_arg = names
118+
.iter()
119+
.find(|name| name.deref().1.0 == format_argument_index)
120+
.map(|found| found.clone());
121+
122+
if let Some(named_arg) = named_arg {
123+
self.positional_named_args.push(PositionalNamedArg {
124+
ty,
125+
cur_piece,
126+
inner_span_to_replace: inner_span,
127+
replacement: named_arg.0.clone(),
128+
positional_named_arg_span: named_arg.1.1.clone(),
129+
});
130+
}
131+
}
132+
}
133+
35134
struct Context<'a, 'b> {
36135
ecx: &'a mut ExtCtxt<'b>,
37136
/// The macro's call site. References to unstable formatting internals must
@@ -118,6 +217,7 @@ struct Context<'a, 'b> {
118217

119218
/// Whether this format string came from a string literal, as opposed to a macro.
120219
is_literal: bool,
220+
unused_names_lint: PositionalNamedArgsLint,
121221
}
122222

123223
/// Parses the arguments from the given list of tokens, returning the diagnostic
@@ -242,7 +342,7 @@ impl<'a, 'b> Context<'a, 'b> {
242342
self.args.len() - self.num_captured_args
243343
}
244344

245-
fn resolve_name_inplace(&self, p: &mut parse::Piece<'_>) {
345+
fn resolve_name_inplace(&mut self, p: &mut parse::Piece<'_>) {
246346
// NOTE: the `unwrap_or` branch is needed in case of invalid format
247347
// arguments, e.g., `format_args!("{foo}")`.
248348
let lookup =
@@ -252,7 +352,7 @@ impl<'a, 'b> Context<'a, 'b> {
252352
parse::String(_) => {}
253353
parse::NextArgument(ref mut arg) => {
254354
if let parse::ArgumentNamed(s, _) = arg.position {
255-
arg.position = parse::ArgumentIs(lookup(s));
355+
arg.position = parse::ArgumentIs(lookup(s), None);
256356
}
257357
if let parse::CountIsName(s, _) = arg.format.width {
258358
arg.format.width = parse::CountIsParam(lookup(s));
@@ -273,15 +373,50 @@ impl<'a, 'b> Context<'a, 'b> {
273373
parse::NextArgument(ref arg) => {
274374
// width/precision first, if they have implicit positional
275375
// parameters it makes more sense to consume them first.
276-
self.verify_count(arg.format.width);
277-
self.verify_count(arg.format.precision);
376+
self.verify_count(
377+
arg.format.width,
378+
&arg.format.width_span,
379+
PositionalNamedArgType::Width,
380+
);
381+
self.verify_count(
382+
arg.format.precision,
383+
&arg.format.precision_span,
384+
PositionalNamedArgType::Precision,
385+
);
278386

279387
// argument second, if it's an implicit positional parameter
280388
// it's written second, so it should come after width/precision.
281389
let pos = match arg.position {
282-
parse::ArgumentIs(i) | parse::ArgumentImplicitlyIs(i) => Exact(i),
390+
parse::ArgumentIs(i, arg_end) => {
391+
let start_of_named_args = self.args.len() - self.names.len();
392+
if self.curpiece >= start_of_named_args {
393+
self.unused_names_lint.maybe_push(
394+
i,
395+
PositionalNamedArgType::Arg,
396+
self.curpiece,
397+
arg_end,
398+
&self.names,
399+
);
400+
}
401+
402+
Exact(i)
403+
}
404+
parse::ArgumentImplicitlyIs(i) => {
405+
let start_of_named_args = self.args.len() - self.names.len();
406+
if self.curpiece >= start_of_named_args {
407+
self.unused_names_lint.maybe_push(
408+
i,
409+
PositionalNamedArgType::Arg,
410+
self.curpiece,
411+
None,
412+
&self.names,
413+
);
414+
}
415+
Exact(i)
416+
}
283417
parse::ArgumentNamed(s, span) => {
284-
Named(Symbol::intern(s), InnerSpan::new(span.start, span.end))
418+
let symbol = Symbol::intern(s);
419+
Named(symbol, InnerSpan::new(span.start, span.end))
285420
}
286421
};
287422

@@ -349,10 +484,25 @@ impl<'a, 'b> Context<'a, 'b> {
349484
}
350485
}
351486

352-
fn verify_count(&mut self, c: parse::Count<'_>) {
487+
fn verify_count(
488+
&mut self,
489+
c: parse::Count<'_>,
490+
inner_span: &Option<rustc_parse_format::InnerSpan>,
491+
named_arg_type: PositionalNamedArgType,
492+
) {
353493
match c {
354494
parse::CountImplied | parse::CountIs(..) => {}
355495
parse::CountIsParam(i) => {
496+
let start_of_named_args = self.args.len() - self.names.len();
497+
if i >= start_of_named_args {
498+
self.unused_names_lint.maybe_push(
499+
i,
500+
named_arg_type,
501+
self.curpiece,
502+
inner_span.clone(),
503+
&self.names,
504+
);
505+
}
356506
self.verify_arg_type(Exact(i), Count);
357507
}
358508
parse::CountIsName(s, span) => {
@@ -673,7 +823,7 @@ impl<'a, 'b> Context<'a, 'b> {
673823
// Build the position
674824
let pos = {
675825
match arg.position {
676-
parse::ArgumentIs(i) | parse::ArgumentImplicitlyIs(i) => {
826+
parse::ArgumentIs(i, ..) | parse::ArgumentImplicitlyIs(i) => {
677827
// Map to index in final generated argument array
678828
// in case of multiple types specified
679829
let arg_idx = match arg_index_consumed.get_mut(i) {
@@ -701,7 +851,7 @@ impl<'a, 'b> Context<'a, 'b> {
701851
// track the current argument ourselves.
702852
let i = self.curarg;
703853
self.curarg += 1;
704-
parse::ArgumentIs(i)
854+
parse::ArgumentIs(i, None)
705855
},
706856
format: parse::FormatSpec {
707857
fill: arg.format.fill,
@@ -971,43 +1121,27 @@ pub fn expand_format_args_nl<'cx>(
9711121
expand_format_args_impl(ecx, sp, tts, true)
9721122
}
9731123

974-
fn lint_named_arguments_used_positionally(
975-
names: FxHashMap<Symbol, (usize, Span)>,
976-
cx: &mut Context<'_, '_>,
977-
unverified_pieces: Vec<parse::Piece<'_>>,
978-
) {
979-
let mut used_argument_names = FxHashSet::<&str>::default();
980-
for piece in unverified_pieces {
981-
if let rustc_parse_format::Piece::NextArgument(a) = piece {
982-
match a.position {
983-
rustc_parse_format::Position::ArgumentNamed(arg_name, _) => {
984-
used_argument_names.insert(arg_name);
985-
}
986-
_ => {}
987-
};
988-
if let Count::CountIsName(s, _) = a.format.width {
989-
used_argument_names.insert(s);
990-
}
991-
if let Count::CountIsName(s, _) = a.format.precision {
992-
used_argument_names.insert(s);
993-
}
994-
}
995-
}
1124+
fn create_lints_for_named_arguments_used_positionally(cx: &mut Context<'_, '_>) {
1125+
for named_arg in &cx.unused_names_lint.positional_named_args {
1126+
let arg_span = named_arg.get_span_to_replace(cx);
9961127

997-
for (symbol, (index, span)) in names {
998-
if !used_argument_names.contains(symbol.as_str()) {
999-
let msg = format!("named argument `{}` is not used by name", symbol.as_str());
1000-
let arg_span = cx.arg_spans.get(index).copied();
1001-
cx.ecx.buffered_early_lint.push(BufferedEarlyLint {
1002-
span: MultiSpan::from_span(span),
1003-
msg: msg.clone(),
1004-
node_id: ast::CRATE_NODE_ID,
1005-
lint_id: LintId::of(&NAMED_ARGUMENTS_USED_POSITIONALLY),
1006-
diagnostic: BuiltinLintDiagnostics::NamedArgumentUsedPositionally(
1007-
arg_span, span, symbol,
1008-
),
1009-
});
1010-
}
1128+
let msg = format!("named argument `{}` is not used by name", named_arg.replacement);
1129+
let replacement = match named_arg.ty {
1130+
PositionalNamedArgType::Arg => named_arg.replacement.to_string(),
1131+
_ => named_arg.replacement.to_string() + "$",
1132+
};
1133+
1134+
cx.ecx.buffered_early_lint.push(BufferedEarlyLint {
1135+
span: MultiSpan::from_span(named_arg.positional_named_arg_span),
1136+
msg: msg.clone(),
1137+
node_id: ast::CRATE_NODE_ID,
1138+
lint_id: LintId::of(&NAMED_ARGUMENTS_USED_POSITIONALLY),
1139+
diagnostic: BuiltinLintDiagnostics::NamedArgumentUsedPositionally(
1140+
arg_span,
1141+
named_arg.positional_named_arg_span,
1142+
replacement,
1143+
),
1144+
});
10111145
}
10121146
}
10131147

@@ -1119,11 +1253,6 @@ pub fn expand_preparsed_format_args(
11191253

11201254
let named_pos: FxHashSet<usize> = names.values().cloned().map(|(i, _)| i).collect();
11211255

1122-
// Clone `names` because `names` in Context get updated by verify_piece, which includes usages
1123-
// of the names of named arguments, resulting in incorrect errors if a name argument is used
1124-
// but not declared, such as: `println!("x = {x}");`
1125-
let named_arguments = names.clone();
1126-
11271256
let mut cx = Context {
11281257
ecx,
11291258
args,
@@ -1148,13 +1277,12 @@ pub fn expand_preparsed_format_args(
11481277
arg_spans,
11491278
arg_with_formatting: Vec::new(),
11501279
is_literal: parser.is_literal,
1280+
unused_names_lint: PositionalNamedArgsLint { positional_named_args: vec![] },
11511281
};
11521282

1153-
// This needs to happen *after* the Parser has consumed all pieces to create all the spans.
1154-
// unverified_pieces is used later to check named argument names are used, so clone each piece.
1283+
// This needs to happen *after* the Parser has consumed all pieces to create all the spans
11551284
let pieces = unverified_pieces
1156-
.iter()
1157-
.cloned()
1285+
.into_iter()
11581286
.map(|mut piece| {
11591287
cx.verify_piece(&piece);
11601288
cx.resolve_name_inplace(&mut piece);
@@ -1164,7 +1292,7 @@ pub fn expand_preparsed_format_args(
11641292

11651293
let numbered_position_args = pieces.iter().any(|arg: &parse::Piece<'_>| match *arg {
11661294
parse::String(_) => false,
1167-
parse::NextArgument(arg) => matches!(arg.position, parse::Position::ArgumentIs(_)),
1295+
parse::NextArgument(arg) => matches!(arg.position, parse::Position::ArgumentIs(..)),
11681296
});
11691297

11701298
cx.build_index_map();
@@ -1316,11 +1444,10 @@ pub fn expand_preparsed_format_args(
13161444
}
13171445

13181446
diag.emit();
1319-
} else if cx.invalid_refs.is_empty() && !named_arguments.is_empty() {
1447+
} else if cx.invalid_refs.is_empty() && cx.ecx.sess.err_count() == 0 {
13201448
// Only check for unused named argument names if there are no other errors to avoid causing
13211449
// too much noise in output errors, such as when a named argument is entirely unused.
1322-
// We also only need to perform this check if there are actually named arguments.
1323-
lint_named_arguments_used_positionally(named_arguments, &mut cx, unverified_pieces);
1450+
create_lints_for_named_arguments_used_positionally(&mut cx);
13241451
}
13251452

13261453
cx.into_expr()

compiler/rustc_lint/src/context.rs

+2-2
Original file line numberDiff line numberDiff line change
@@ -861,10 +861,10 @@ pub trait LintContext: Sized {
861861
if let Some(positional_arg) = positional_arg {
862862
let msg = format!("this formatting argument uses named argument `{}` by position", name);
863863
db.span_label(positional_arg, msg);
864-
db.span_suggestion_verbose(
864+
db.span_suggestion_verbose(
865865
positional_arg,
866866
"use the named argument by name to avoid ambiguity",
867-
format!("{{{}}}", name),
867+
name,
868868
Applicability::MaybeIncorrect,
869869
);
870870
}

compiler/rustc_lint_defs/src/lib.rs

+1-1
Original file line numberDiff line numberDiff line change
@@ -467,7 +467,7 @@ pub enum BuiltinLintDiagnostics {
467467
/// If true, the lifetime will be fully elided.
468468
use_span: Option<(Span, bool)>,
469469
},
470-
NamedArgumentUsedPositionally(Option<Span>, Span, Symbol),
470+
NamedArgumentUsedPositionally(Option<Span>, Span, String),
471471
}
472472

473473
/// Lints that are buffered up early on in the `Session` before the

0 commit comments

Comments
 (0)