Skip to content

Commit 4a8d9ea

Browse files
authored
Rollup merge of rust-lang#73670 - davidhewitt:format-args-capture, r=varkor
Add `format_args_capture` feature This is the initial implementation PR for [RFC 2795](rust-lang/rfcs#2795). Note that, as dicussed in the tracking issue (rust-lang#67984), the feature gate has been called `format_args_capture`. Next up I guess I need to add documentation for this feature. I've not written any docs before for rustc / std so I would appreciate suggestions on where I should add docs.
2 parents df8f551 + 93d662f commit 4a8d9ea

13 files changed

+305
-5
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
# `format_args_capture`
2+
3+
The tracking issue for this feature is: [#67984]
4+
5+
[#67984]: https://github.com/rust-lang/rust/issues/67984
6+
7+
------------------------
8+
9+
Enables `format_args!` (and macros which use `format_args!` in their implementation, such
10+
as `format!`, `print!` and `panic!`) to capture variables from the surrounding scope.
11+
This avoids the need to pass named parameters when the binding in question
12+
already exists in scope.
13+
14+
```rust
15+
#![feature(format_args_capture)]
16+
17+
let (person, species, name) = ("Charlie Brown", "dog", "Snoopy");
18+
19+
// captures named argument `person`
20+
print!("Hello {person}");
21+
22+
// captures named arguments `species` and `name`
23+
format!("The {species}'s name is {name}.");
24+
```
25+
26+
This also works for formatting parameters such as width and precision:
27+
28+
```rust
29+
#![feature(format_args_capture)]
30+
31+
let precision = 2;
32+
let s = format!("{:.precision$}", 1.324223);
33+
34+
assert_eq!(&s, "1.32");
35+
```
36+
37+
A non-exhaustive list of macros which benefit from this functionality include:
38+
- `format!`
39+
- `print!` and `println!`
40+
- `eprint!` and `eprintln!`
41+
- `write!` and `writeln!`
42+
- `panic!`
43+
- `unreachable!`
44+
- `unimplemented!`
45+
- `todo!`
46+
- `assert!` and similar
47+
- macros in many thirdparty crates, such as `log`

src/librustc_builtin_macros/format.rs

+53-4
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,9 @@ struct Context<'a, 'b> {
107107
arg_spans: Vec<Span>,
108108
/// All the formatting arguments that have formatting flags set, in order for diagnostics.
109109
arg_with_formatting: Vec<parse::FormatSpec<'a>>,
110+
111+
/// Whether this format string came from a string literal, as opposed to a macro.
112+
is_literal: bool,
110113
}
111114

112115
/// Parses the arguments from the given list of tokens, returning the diagnostic
@@ -498,10 +501,55 @@ impl<'a, 'b> Context<'a, 'b> {
498501
self.verify_arg_type(Exact(idx), ty)
499502
}
500503
None => {
501-
let msg = format!("there is no argument named `{}`", name);
502-
let sp = *self.arg_spans.get(self.curpiece).unwrap_or(&self.fmtsp);
503-
let mut err = self.ecx.struct_span_err(sp, &msg[..]);
504-
err.emit();
504+
let capture_feature_enabled = self
505+
.ecx
506+
.ecfg
507+
.features
508+
.map_or(false, |features| features.format_args_capture);
509+
510+
// For the moment capturing variables from format strings expanded from macros is
511+
// disabled (see RFC #2795)
512+
let can_capture = capture_feature_enabled && self.is_literal;
513+
514+
if can_capture {
515+
// Treat this name as a variable to capture from the surrounding scope
516+
let idx = self.args.len();
517+
self.arg_types.push(Vec::new());
518+
self.arg_unique_types.push(Vec::new());
519+
self.args.push(
520+
self.ecx.expr_ident(self.fmtsp, Ident::new(name, self.fmtsp)),
521+
);
522+
self.names.insert(name, idx);
523+
self.verify_arg_type(Exact(idx), ty)
524+
} else {
525+
let msg = format!("there is no argument named `{}`", name);
526+
let sp = if self.is_literal {
527+
*self.arg_spans.get(self.curpiece).unwrap_or(&self.fmtsp)
528+
} else {
529+
self.fmtsp
530+
};
531+
let mut err = self.ecx.struct_span_err(sp, &msg[..]);
532+
533+
if capture_feature_enabled && !self.is_literal {
534+
err.note(&format!(
535+
"did you intend to capture a variable `{}` from \
536+
the surrounding scope?",
537+
name
538+
));
539+
err.note(
540+
"to avoid ambiguity, `format_args!` cannot capture variables \
541+
when the format string is expanded from a macro",
542+
);
543+
} else if self.ecx.parse_sess().unstable_features.is_nightly_build() {
544+
err.help(&format!(
545+
"if you intended to capture `{}` from the surrounding scope, add \
546+
`#![feature(format_args_capture)]` to the crate attributes",
547+
name
548+
));
549+
}
550+
551+
err.emit();
552+
}
505553
}
506554
}
507555
}
@@ -951,6 +999,7 @@ pub fn expand_preparsed_format_args(
951999
invalid_refs: Vec::new(),
9521000
arg_spans,
9531001
arg_with_formatting: Vec::new(),
1002+
is_literal: parser.is_literal,
9541003
};
9551004

9561005
// This needs to happen *after* the Parser has consumed all pieces to create all the spans

src/librustc_feature/active.rs

+3
Original file line numberDiff line numberDiff line change
@@ -567,6 +567,9 @@ declare_features! (
567567
/// Be more precise when looking for live drops in a const context.
568568
(active, const_precise_live_drops, "1.46.0", Some(73255), None),
569569

570+
/// Allows capturing variables in scope using format_args!
571+
(active, format_args_capture, "1.46.0", Some(67984), None),
572+
570573
// -------------------------------------------------------------------------
571574
// feature-group-end: actual feature gates
572575
// -------------------------------------------------------------------------

src/librustc_parse_format/lib.rs

+1-1
Original file line numberDiff line numberDiff line change
@@ -190,7 +190,7 @@ pub struct Parser<'a> {
190190
/// Whether the source string is comes from `println!` as opposed to `format!` or `print!`
191191
append_newline: bool,
192192
/// Whether this formatting string is a literal or it comes from a macro.
193-
is_literal: bool,
193+
pub is_literal: bool,
194194
/// Start position of the current line.
195195
cur_line_start: usize,
196196
/// Start and end byte offset of every line of the format string. Excludes

src/librustc_span/symbol.rs

+1
Original file line numberDiff line numberDiff line change
@@ -342,6 +342,7 @@ symbols! {
342342
forbid,
343343
format_args,
344344
format_args_nl,
345+
format_args_capture,
345346
from,
346347
From,
347348
from_desugaring,
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
fn main() {
2+
format!("{foo}"); //~ ERROR: there is no argument named `foo`
3+
4+
// panic! doesn't hit format_args! unless there are two or more arguments.
5+
panic!("{foo} {bar}", bar=1); //~ ERROR: there is no argument named `foo`
6+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
error: there is no argument named `foo`
2+
--> $DIR/feature-gate-format-args-capture.rs:2:14
3+
|
4+
LL | format!("{foo}");
5+
| ^^^^^
6+
|
7+
= help: if you intended to capture `foo` from the surrounding scope, add `#![feature(format_args_capture)]` to the crate attributes
8+
9+
error: there is no argument named `foo`
10+
--> $DIR/feature-gate-format-args-capture.rs:5:13
11+
|
12+
LL | panic!("{foo} {bar}", bar=1);
13+
| ^^^^^
14+
|
15+
= help: if you intended to capture `foo` from the surrounding scope, add `#![feature(format_args_capture)]` to the crate attributes
16+
17+
error: aborting due to 2 previous errors
18+
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
#![feature(format_args_capture)]
2+
3+
fn main() {
4+
format!(concat!("{foo}")); //~ ERROR: there is no argument named `foo`
5+
format!(concat!("{ba", "r} {}"), 1); //~ ERROR: there is no argument named `bar`
6+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
error: there is no argument named `foo`
2+
--> $DIR/format-args-capture-macro-hygiene.rs:4:13
3+
|
4+
LL | format!(concat!("{foo}"));
5+
| ^^^^^^^^^^^^^^^^
6+
|
7+
= note: did you intend to capture a variable `foo` from the surrounding scope?
8+
= note: to avoid ambiguity, `format_args!` cannot capture variables when the format string is expanded from a macro
9+
= note: this error originates in a macro (in Nightly builds, run with -Z macro-backtrace for more info)
10+
11+
error: there is no argument named `bar`
12+
--> $DIR/format-args-capture-macro-hygiene.rs:5:13
13+
|
14+
LL | format!(concat!("{ba", "r} {}"), 1);
15+
| ^^^^^^^^^^^^^^^^^^^^^^^
16+
|
17+
= note: did you intend to capture a variable `bar` from the surrounding scope?
18+
= note: to avoid ambiguity, `format_args!` cannot capture variables when the format string is expanded from a macro
19+
= note: this error originates in a macro (in Nightly builds, run with -Z macro-backtrace for more info)
20+
21+
error: aborting due to 2 previous errors
22+
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
#![feature(format_args_capture)]
2+
3+
fn main() {
4+
format!("{} {foo} {} {bar} {}", 1, 2, 3);
5+
//~^ ERROR: cannot find value `foo` in this scope
6+
//~^^ ERROR: cannot find value `bar` in this scope
7+
8+
format!("{foo}"); //~ ERROR: cannot find value `foo` in this scope
9+
10+
format!("{valuea} {valueb}", valuea=5, valuec=7);
11+
//~^ ERROR cannot find value `valueb` in this scope
12+
//~^^ ERROR named argument never used
13+
14+
format!(r##"
15+
16+
{foo}
17+
18+
"##);
19+
//~^^^^^ ERROR: cannot find value `foo` in this scope
20+
21+
panic!("{foo} {bar}", bar=1); //~ ERROR: cannot find value `foo` in this scope
22+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
error: named argument never used
2+
--> $DIR/format-args-capture-missing-variables.rs:10:51
3+
|
4+
LL | format!("{valuea} {valueb}", valuea=5, valuec=7);
5+
| ------------------- ^ named argument never used
6+
| |
7+
| formatting specifier missing
8+
9+
error[E0425]: cannot find value `foo` in this scope
10+
--> $DIR/format-args-capture-missing-variables.rs:4:13
11+
|
12+
LL | format!("{} {foo} {} {bar} {}", 1, 2, 3);
13+
| ^^^^^^^^^^^^^^^^^^^^^^ not found in this scope
14+
15+
error[E0425]: cannot find value `bar` in this scope
16+
--> $DIR/format-args-capture-missing-variables.rs:4:13
17+
|
18+
LL | format!("{} {foo} {} {bar} {}", 1, 2, 3);
19+
| ^^^^^^^^^^^^^^^^^^^^^^ not found in this scope
20+
21+
error[E0425]: cannot find value `foo` in this scope
22+
--> $DIR/format-args-capture-missing-variables.rs:8:13
23+
|
24+
LL | format!("{foo}");
25+
| ^^^^^^^ not found in this scope
26+
27+
error[E0425]: cannot find value `valueb` in this scope
28+
--> $DIR/format-args-capture-missing-variables.rs:10:13
29+
|
30+
LL | format!("{valuea} {valueb}", valuea=5, valuec=7);
31+
| ^^^^^^^^^^^^^^^^^^^ not found in this scope
32+
33+
error[E0425]: cannot find value `foo` in this scope
34+
--> $DIR/format-args-capture-missing-variables.rs:14:13
35+
|
36+
LL | format!(r##"
37+
| _____________^
38+
LL | |
39+
LL | | {foo}
40+
LL | |
41+
LL | | "##);
42+
| |_______^ not found in this scope
43+
44+
error[E0425]: cannot find value `foo` in this scope
45+
--> $DIR/format-args-capture-missing-variables.rs:21:12
46+
|
47+
LL | panic!("{foo} {bar}", bar=1);
48+
| ^^^^^^^^^^^^^ not found in this scope
49+
50+
error: aborting due to 7 previous errors
51+
52+
For more information about this error, try `rustc --explain E0425`.
+64
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
// run-pass
2+
// ignore-wasm32
3+
// ignore-wasm64
4+
#![feature(format_args_capture)]
5+
6+
fn main() {
7+
named_argument_takes_precedence_to_captured();
8+
panic_with_single_argument_does_not_get_formatted();
9+
panic_with_multiple_arguments_is_formatted();
10+
formatting_parameters_can_be_captured();
11+
}
12+
13+
fn named_argument_takes_precedence_to_captured() {
14+
let foo = "captured";
15+
let s = format!("{foo}", foo="named");
16+
assert_eq!(&s, "named");
17+
18+
let s = format!("{foo}-{foo}-{foo}", foo="named");
19+
assert_eq!(&s, "named-named-named");
20+
21+
let s = format!("{}-{bar}-{foo}", "positional", bar="named");
22+
assert_eq!(&s, "positional-named-captured");
23+
}
24+
25+
fn panic_with_single_argument_does_not_get_formatted() {
26+
// panic! with a single argument does not perform string formatting.
27+
// RFC #2795 suggests that this may need to change so that captured arguments are formatted.
28+
// For stability reasons this will need to part of an edition change.
29+
30+
let msg = std::panic::catch_unwind(|| {
31+
panic!("{foo}");
32+
}).unwrap_err();
33+
34+
assert_eq!(msg.downcast_ref::<&str>(), Some(&"{foo}"))
35+
}
36+
37+
fn panic_with_multiple_arguments_is_formatted() {
38+
let foo = "captured";
39+
40+
let msg = std::panic::catch_unwind(|| {
41+
panic!("{}-{bar}-{foo}", "positional", bar="named");
42+
}).unwrap_err();
43+
44+
assert_eq!(msg.downcast_ref::<String>(), Some(&"positional-named-captured".to_string()))
45+
}
46+
47+
fn formatting_parameters_can_be_captured() {
48+
let width = 9;
49+
let precision = 3;
50+
51+
let x = 7.0;
52+
53+
let s = format!("{x:width$}");
54+
assert_eq!(&s, " 7");
55+
56+
let s = format!("{x:<width$}");
57+
assert_eq!(&s, "7 ");
58+
59+
let s = format!("{x:-^width$}");
60+
assert_eq!(&s, "----7----");
61+
62+
let s = format!("{x:-^width$.precision$}");
63+
assert_eq!(&s, "--7.000--");
64+
}

src/test/ui/if/ifmt-bad-arg.stderr

+10
Original file line numberDiff line numberDiff line change
@@ -63,18 +63,24 @@ error: there is no argument named `foo`
6363
|
6464
LL | format!("{} {foo} {} {bar} {}", 1, 2, 3);
6565
| ^^^^^
66+
|
67+
= help: if you intended to capture `foo` from the surrounding scope, add `#![feature(format_args_capture)]` to the crate attributes
6668

6769
error: there is no argument named `bar`
6870
--> $DIR/ifmt-bad-arg.rs:27:26
6971
|
7072
LL | format!("{} {foo} {} {bar} {}", 1, 2, 3);
7173
| ^^^^^
74+
|
75+
= help: if you intended to capture `bar` from the surrounding scope, add `#![feature(format_args_capture)]` to the crate attributes
7276

7377
error: there is no argument named `foo`
7478
--> $DIR/ifmt-bad-arg.rs:31:14
7579
|
7680
LL | format!("{foo}");
7781
| ^^^^^
82+
|
83+
= help: if you intended to capture `foo` from the surrounding scope, add `#![feature(format_args_capture)]` to the crate attributes
7884

7985
error: multiple unused formatting arguments
8086
--> $DIR/ifmt-bad-arg.rs:32:17
@@ -155,6 +161,8 @@ error: there is no argument named `valueb`
155161
|
156162
LL | format!("{valuea} {valueb}", valuea=5, valuec=7);
157163
| ^^^^^^^^
164+
|
165+
= help: if you intended to capture `valueb` from the surrounding scope, add `#![feature(format_args_capture)]` to the crate attributes
158166

159167
error: named argument never used
160168
--> $DIR/ifmt-bad-arg.rs:45:51
@@ -205,6 +213,8 @@ error: there is no argument named `foo`
205213
|
206214
LL | {foo}
207215
| ^^^^^
216+
|
217+
= help: if you intended to capture `foo` from the surrounding scope, add `#![feature(format_args_capture)]` to the crate attributes
208218

209219
error: invalid format string: expected `'}'`, found `'t'`
210220
--> $DIR/ifmt-bad-arg.rs:75:1

0 commit comments

Comments
 (0)