Skip to content

Commit 1219f72

Browse files
committed
Emit warning when named arguments are used positionally in format
Addresses Issue 98466 by emitting a warning if a named argument is used like a position argument (i.e. the name is not used in the string to be formatted). Fixes #98466
1 parent c80dde4 commit 1219f72

File tree

10 files changed

+324
-16
lines changed

10 files changed

+324
-16
lines changed

compiler/rustc_builtin_macros/src/format.rs

+72-13
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,9 @@ use rustc_span::symbol::{sym, Ident, Symbol};
1414
use rustc_span::{InnerSpan, Span};
1515
use smallvec::SmallVec;
1616

17+
use rustc_lint_defs::builtin::NAMED_ARGUMENTS_USED_POSITIONALLY;
18+
use rustc_lint_defs::{BufferedEarlyLint, BuiltinLintDiagnostics, LintId};
19+
use rustc_parse_format::{Count, FormatSpec};
1720
use std::borrow::Cow;
1821
use std::collections::hash_map::Entry;
1922

@@ -57,7 +60,7 @@ struct Context<'a, 'b> {
5760
/// Unique format specs seen for each argument.
5861
arg_unique_types: Vec<Vec<ArgumentType>>,
5962
/// Map from named arguments to their resolved indices.
60-
names: FxHashMap<Symbol, usize>,
63+
names: FxHashMap<Symbol, (usize, Span)>,
6164

6265
/// The latest consecutive literal strings, or empty if there weren't any.
6366
literal: String,
@@ -130,9 +133,9 @@ fn parse_args<'a>(
130133
ecx: &mut ExtCtxt<'a>,
131134
sp: Span,
132135
tts: TokenStream,
133-
) -> PResult<'a, (P<ast::Expr>, Vec<P<ast::Expr>>, FxHashMap<Symbol, usize>)> {
136+
) -> PResult<'a, (P<ast::Expr>, Vec<P<ast::Expr>>, FxHashMap<Symbol, (usize, Span)>)> {
134137
let mut args = Vec::<P<ast::Expr>>::new();
135-
let mut names = FxHashMap::<Symbol, usize>::default();
138+
let mut names = FxHashMap::<Symbol, (usize, Span)>::default();
136139

137140
let mut p = ecx.new_parser_from_tts(tts);
138141

@@ -197,7 +200,7 @@ fn parse_args<'a>(
197200
p.bump();
198201
p.expect(&token::Eq)?;
199202
let e = p.parse_expr()?;
200-
if let Some(prev) = names.get(&ident.name) {
203+
if let Some((prev, _)) = names.get(&ident.name) {
201204
ecx.struct_span_err(e.span, &format!("duplicate argument named `{}`", ident))
202205
.span_label(args[*prev].span, "previously here")
203206
.span_label(e.span, "duplicate argument")
@@ -210,7 +213,7 @@ fn parse_args<'a>(
210213
// if the input is valid, we can simply append to the positional
211214
// args. And remember the names.
212215
let slot = args.len();
213-
names.insert(ident.name, slot);
216+
names.insert(ident.name, (slot, ident.span));
214217
args.push(e);
215218
}
216219
_ => {
@@ -222,7 +225,7 @@ fn parse_args<'a>(
222225
);
223226
err.span_label(e.span, "positional arguments must be before named arguments");
224227
for pos in names.values() {
225-
err.span_label(args[*pos].span, "named argument");
228+
err.span_label(args[pos.0].span, "named argument");
226229
}
227230
err.emit();
228231
}
@@ -242,7 +245,8 @@ impl<'a, 'b> Context<'a, 'b> {
242245
fn resolve_name_inplace(&self, p: &mut parse::Piece<'_>) {
243246
// NOTE: the `unwrap_or` branch is needed in case of invalid format
244247
// arguments, e.g., `format_args!("{foo}")`.
245-
let lookup = |s: &str| *self.names.get(&Symbol::intern(s)).unwrap_or(&0);
248+
let lookup =
249+
|s: &str| self.names.get(&Symbol::intern(s)).unwrap_or(&(0, Span::default())).0;
246250

247251
match *p {
248252
parse::String(_) => {}
@@ -548,7 +552,7 @@ impl<'a, 'b> Context<'a, 'b> {
548552
match self.names.get(&name) {
549553
Some(&idx) => {
550554
// Treat as positional arg.
551-
self.verify_arg_type(Capture(idx), ty)
555+
self.verify_arg_type(Capture(idx.0), ty)
552556
}
553557
None => {
554558
// For the moment capturing variables from format strings expanded from macros is
@@ -565,7 +569,7 @@ impl<'a, 'b> Context<'a, 'b> {
565569
};
566570
self.num_captured_args += 1;
567571
self.args.push(self.ecx.expr_ident(span, Ident::new(name, span)));
568-
self.names.insert(name, idx);
572+
self.names.insert(name, (idx, span));
569573
self.verify_arg_type(Capture(idx), ty)
570574
} else {
571575
let msg = format!("there is no argument named `{}`", name);
@@ -967,14 +971,57 @@ pub fn expand_format_args_nl<'cx>(
967971
expand_format_args_impl(ecx, sp, tts, true)
968972
}
969973

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+
match a.format {
989+
FormatSpec { width: Count::CountIsName(s, _), .. }
990+
| FormatSpec { precision: Count::CountIsName(s, _), .. } => {
991+
used_argument_names.insert(s);
992+
}
993+
_ => {}
994+
};
995+
}
996+
}
997+
998+
for (symbol, (index, span)) in names {
999+
if !used_argument_names.contains(symbol.as_str()) {
1000+
let msg = format!("named argument `{}` is not used by name", symbol.as_str());
1001+
let arg_span = cx.arg_spans[index];
1002+
cx.ecx.buffered_early_lint.push(BufferedEarlyLint {
1003+
span: MultiSpan::from_span(span),
1004+
msg: msg.clone(),
1005+
node_id: ast::CRATE_NODE_ID,
1006+
lint_id: LintId::of(&NAMED_ARGUMENTS_USED_POSITIONALLY),
1007+
diagnostic: BuiltinLintDiagnostics::NamedArgumentUsedPositionally(
1008+
arg_span,
1009+
span,
1010+
symbol.to_string(),
1011+
),
1012+
});
1013+
}
1014+
}
1015+
}
1016+
9701017
/// Take the various parts of `format_args!(efmt, args..., name=names...)`
9711018
/// and construct the appropriate formatting expression.
9721019
pub fn expand_preparsed_format_args(
9731020
ecx: &mut ExtCtxt<'_>,
9741021
sp: Span,
9751022
efmt: P<ast::Expr>,
9761023
args: Vec<P<ast::Expr>>,
977-
names: FxHashMap<Symbol, usize>,
1024+
names: FxHashMap<Symbol, (usize, Span)>,
9781025
append_newline: bool,
9791026
) -> P<ast::Expr> {
9801027
// NOTE: this verbose way of initializing `Vec<Vec<ArgumentType>>` is because
@@ -1073,7 +1120,12 @@ pub fn expand_preparsed_format_args(
10731120
.map(|span| fmt_span.from_inner(InnerSpan::new(span.start, span.end)))
10741121
.collect();
10751122

1076-
let named_pos: FxHashSet<usize> = names.values().cloned().collect();
1123+
let named_pos: FxHashSet<usize> = names.values().cloned().map(|(i, _)| i).collect();
1124+
1125+
// Clone `names` because `names` in Context get updated by verify_piece, which includes usages
1126+
// of the names of named arguments, resulting in incorrect errors if a name argument is used
1127+
// but not declared, such as: `println!("x = {x}");`
1128+
let named_arguments = names.clone();
10771129

10781130
let mut cx = Context {
10791131
ecx,
@@ -1101,9 +1153,11 @@ pub fn expand_preparsed_format_args(
11011153
is_literal: parser.is_literal,
11021154
};
11031155

1104-
// This needs to happen *after* the Parser has consumed all pieces to create all the spans
1156+
// This needs to happen *after* the Parser has consumed all pieces to create all the spans.
1157+
// unverified_pieces is used later to check named argument names are used, so clone each piece.
11051158
let pieces = unverified_pieces
1106-
.into_iter()
1159+
.iter()
1160+
.cloned()
11071161
.map(|mut piece| {
11081162
cx.verify_piece(&piece);
11091163
cx.resolve_name_inplace(&mut piece);
@@ -1265,6 +1319,11 @@ pub fn expand_preparsed_format_args(
12651319
}
12661320

12671321
diag.emit();
1322+
} else if cx.invalid_refs.is_empty() && !named_arguments.is_empty() {
1323+
// Only check for unused named argument names if there are no other errors to avoid causing
1324+
// too much noise in output errors, such as when a named argument is entirely unused.
1325+
// We also only need to perform this check if there are actually named arguments.
1326+
lint_named_arguments_used_positionally(named_arguments, &mut cx, unverified_pieces);
12681327
}
12691328

12701329
cx.into_expr()

compiler/rustc_expand/src/base.rs

+4-1
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ use rustc_data_structures::fx::{FxHashMap, FxHashSet};
1212
use rustc_data_structures::sync::{self, Lrc};
1313
use rustc_errors::{Applicability, DiagnosticBuilder, ErrorGuaranteed, MultiSpan, PResult};
1414
use rustc_lint_defs::builtin::PROC_MACRO_BACK_COMPAT;
15-
use rustc_lint_defs::BuiltinLintDiagnostics;
15+
use rustc_lint_defs::{BufferedEarlyLint, BuiltinLintDiagnostics};
1616
use rustc_parse::{self, parser, MACRO_ARGUMENTS};
1717
use rustc_session::{parse::ParseSess, Limit, Session, SessionDiagnostic};
1818
use rustc_span::def_id::{CrateNum, DefId, LocalDefId};
@@ -988,6 +988,8 @@ pub struct ExtCtxt<'a> {
988988
pub expansions: FxHashMap<Span, Vec<String>>,
989989
/// Used for running pre-expansion lints on freshly loaded modules.
990990
pub(super) lint_store: LintStoreExpandDyn<'a>,
991+
/// Used for storing lints generated during expansion, like `NAMED_ARGUMENTS_USED_POSITIONALLY`
992+
pub buffered_early_lint: Vec<BufferedEarlyLint>,
991993
/// When we 'expand' an inert attribute, we leave it
992994
/// in the AST, but insert it here so that we know
993995
/// not to expand it again.
@@ -1020,6 +1022,7 @@ impl<'a> ExtCtxt<'a> {
10201022
force_mode: false,
10211023
expansions: FxHashMap::default(),
10221024
expanded_inert_attrs: MarkedAttrs::new(),
1025+
buffered_early_lint: vec![],
10231026
}
10241027
}
10251028

compiler/rustc_interface/src/passes.rs

+5-2
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ use rustc_errors::{Applicability, ErrorGuaranteed, MultiSpan, PResult};
1414
use rustc_expand::base::{ExtCtxt, LintStoreExpand, ResolverExpand};
1515
use rustc_hir::def_id::{StableCrateId, LOCAL_CRATE};
1616
use rustc_hir::definitions::Definitions;
17-
use rustc_lint::{EarlyCheckNode, LintStore};
17+
use rustc_lint::{BufferedEarlyLint, EarlyCheckNode, LintStore};
1818
use rustc_metadata::creader::CStore;
1919
use rustc_metadata::{encode_metadata, EncodedMetadata};
2020
use rustc_middle::arena::Arena;
@@ -336,12 +336,15 @@ pub fn configure_and_expand(
336336

337337
let lint_store = LintStoreExpandImpl(lint_store);
338338
let mut ecx = ExtCtxt::new(sess, cfg, resolver, Some(&lint_store));
339-
340339
// Expand macros now!
341340
let krate = sess.time("expand_crate", || ecx.monotonic_expander().expand_crate(krate));
342341

343342
// The rest is error reporting
344343

344+
sess.parse_sess.buffered_lints.with_lock(|buffered_lints: &mut Vec<BufferedEarlyLint>| {
345+
buffered_lints.append(&mut ecx.buffered_early_lint);
346+
});
347+
345348
sess.time("check_unused_macros", || {
346349
ecx.check_unused_macros();
347350
});

compiler/rustc_lint/src/context.rs

+12
Original file line numberDiff line numberDiff line change
@@ -857,6 +857,18 @@ pub trait LintContext: Sized {
857857
Applicability::MachineApplicable,
858858
);
859859
},
860+
BuiltinLintDiagnostics::NamedArgumentUsedPositionally(positional_arg, named_arg, name) => {
861+
db.span_label(named_arg, "this named argument is only referred to by position in formatting string");
862+
let msg = format!("this formatting argument uses named argument `{}` by position", name);
863+
db.span_label(positional_arg, msg);
864+
db.span_suggestion_verbose(
865+
positional_arg,
866+
"use the named argument by name to avoid ambiguity",
867+
format!("{{{}}}", name),
868+
Applicability::MaybeIncorrect,
869+
);
870+
871+
}
860872
}
861873
// Rewrap `db`, and pass control to the user.
862874
decorate(LintDiagnosticBuilder::new(db));

compiler/rustc_lint_defs/src/builtin.rs

+31
Original file line numberDiff line numberDiff line change
@@ -3292,6 +3292,7 @@ declare_lint_pass! {
32923292
TEST_UNSTABLE_LINT,
32933293
FFI_UNWIND_CALLS,
32943294
REPR_TRANSPARENT_EXTERNAL_PRIVATE_FIELDS,
3295+
NAMED_ARGUMENTS_USED_POSITIONALLY,
32953296
]
32963297
}
32973298

@@ -3996,3 +3997,33 @@ declare_lint! {
39963997
"call to foreign functions or function pointers with FFI-unwind ABI",
39973998
@feature_gate = sym::c_unwind;
39983999
}
4000+
4001+
declare_lint! {
4002+
/// The `named_arguments_used_positionally` lint detects cases where named arguments are only
4003+
/// used positionally in format strings. This usage is valid but potentially very confusing.
4004+
///
4005+
/// ### Example
4006+
///
4007+
/// ```rust,compile_fail
4008+
/// #![deny(named_arguments_used_positionally)]
4009+
/// fn main() {
4010+
/// let _x = 5;
4011+
/// println!("{}", _x = 1); // Prints 1, will trigger lint
4012+
///
4013+
/// println!("{}", _x); // Prints 5, no lint emitted
4014+
/// println!("{_x}", _x = _x); // Prints 5, no lint emitted
4015+
/// }
4016+
/// ```
4017+
///
4018+
/// {{produces}}
4019+
///
4020+
/// ### Explanation
4021+
///
4022+
/// Rust formatting strings can refer to named arguments by their position, but this usage is
4023+
/// potentially confusing. In particular, readers can incorrectly assume that the declaration
4024+
/// of named arguments is an assignment (which would produce the unit type).
4025+
/// For backwards compatibility, this is not a hard error.
4026+
pub NAMED_ARGUMENTS_USED_POSITIONALLY,
4027+
Warn,
4028+
"named arguments in format used positionally"
4029+
}

compiler/rustc_lint_defs/src/lib.rs

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

472473
/// Lints that are buffered up early on in the `Session` before the
+16
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
// check-pass
2+
#![allow(named_arguments_used_positionally)]
3+
4+
fn main() {
5+
let mut _x: usize;
6+
_x = 1;
7+
println!("_x is {}", _x = 5);
8+
println!("_x is {}", y = _x);
9+
println!("first positional arg {}, second positional arg {}, _x is {}", 1, 2, y = _x);
10+
11+
let mut _x: usize;
12+
_x = 1;
13+
let _f = format!("_x is {}", _x = 5);
14+
let _f = format!("_x is {}", y = _x);
15+
let _f = format!("first positional arg {}, second positional arg {}, _x is {}", 1, 2, y = _x);
16+
}

src/test/ui/macros/issue-98466.fixed

+51
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
// check-pass
2+
// run-rustfix
3+
4+
fn main() {
5+
let mut _x: usize;
6+
_x = 1;
7+
println!("_x is {_x}", _x = 5);
8+
//~^ WARNING named argument `_x` is not used by name [named_arguments_used_positionally]
9+
//~| HELP use the named argument by name to avoid ambiguity
10+
println!("_x is {y}", y = _x);
11+
//~^ WARNING named argument `y` is not used by name [named_arguments_used_positionally]
12+
//~| HELP use the named argument by name to avoid ambiguity
13+
println!("first positional arg {}, second positional arg {}, _x is {y}", 1, 2, y = _x);
14+
//~^ WARNING named argument `y` is not used by name [named_arguments_used_positionally]
15+
//~| HELP use the named argument by name to avoid ambiguity
16+
17+
let mut _x: usize;
18+
_x = 1;
19+
let _f = format!("_x is {_x}", _x = 5);
20+
//~^ WARNING named argument `_x` is not used by name [named_arguments_used_positionally]
21+
//~| HELP use the named argument by name to avoid ambiguity
22+
let _f = format!("_x is {y}", y = _x);
23+
//~^ WARNING named argument `y` is not used by name [named_arguments_used_positionally]
24+
//~| HELP use the named argument by name to avoid ambiguity
25+
let _f = format!("first positional arg {}, second positional arg {}, _x is {y}", 1, 2, y = _x);
26+
//~^ WARNING named argument `y` is not used by name [named_arguments_used_positionally]
27+
//~| HELP use the named argument by name to avoid ambiguity
28+
29+
let s = "0.009";
30+
// Confirm that named arguments used in formatting are correctly considered.
31+
println!(".{:0<width$}", s, width = _x);
32+
33+
let region = "abc";
34+
let width = 8;
35+
let ls = "abcde";
36+
let full = "abcde";
37+
// Confirm that named arguments used in formatting are correctly considered.
38+
println!(
39+
"| {r:rw$?} | {ui:4?} | {v}",
40+
r = region,
41+
rw = width,
42+
ui = ls,
43+
v = full,
44+
);
45+
46+
// Confirm that named arguments used in formatting are correctly considered.
47+
println!("{:.a$}", "aaaaaaaaaaaaaaaaaa", a = 4);
48+
49+
// Confirm that named arguments used in formatting are correctly considered.
50+
println!("{:._a$}", "aaaaaaaaaaaaaaaaaa", _a = 4);
51+
}

0 commit comments

Comments
 (0)