Skip to content

Commit 6b95f31

Browse files
committed
Add format_args_capture feature
1 parent 1557fb0 commit 6b95f31

12 files changed

+267
-5
lines changed

src/librustc_builtin_macros/format.rs

+57-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,59 @@ 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
511+
// literals is 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+
"for hygiene reasons 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.note(&format!(
545+
"did you intend to capture a variable `{}` from \
546+
the surrounding scope?",
547+
name
548+
));
549+
err.help(
550+
"add `#![feature(format_args_capture)]` to the crate \
551+
attributes to enable",
552+
);
553+
}
554+
555+
err.emit();
556+
}
505557
}
506558
}
507559
}
@@ -951,6 +1003,7 @@ pub fn expand_preparsed_format_args(
9511003
invalid_refs: Vec::new(),
9521004
arg_spans,
9531005
arg_with_formatting: Vec::new(),
1006+
is_literal: parser.is_literal,
9541007
};
9551008

9561009
// 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
@@ -577,6 +577,9 @@ declare_features! (
577577
/// Be more precise when looking for live drops in a const context.
578578
(active, const_precise_live_drops, "1.46.0", Some(73255), None),
579579

580+
/// Allows capturing variables in scope using format_args!
581+
(active, format_args_capture, "1.46.0", Some(67984), None),
582+
580583
// -------------------------------------------------------------------------
581584
// feature-group-end: actual feature gates
582585
// -------------------------------------------------------------------------

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
@@ -338,6 +338,7 @@ symbols! {
338338
forbid,
339339
format_args,
340340
format_args_nl,
341+
format_args_capture,
341342
from,
342343
From,
343344
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,20 @@
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+
= note: did you intend to capture a variable `foo` from the surrounding scope?
8+
= help: add `#![feature(format_args_capture)]` to the crate attributes to enable
9+
10+
error: there is no argument named `foo`
11+
--> $DIR/feature-gate-format-args-capture.rs:5:13
12+
|
13+
LL | panic!("{foo} {bar}", bar=1);
14+
| ^^^^^
15+
|
16+
= note: did you intend to capture a variable `foo` from the surrounding scope?
17+
= help: add `#![feature(format_args_capture)]` to the crate attributes to enable
18+
19+
error: aborting due to 2 previous errors
20+
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: for hygiene reasons 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: for hygiene reasons 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`.
+62
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
// run-pass
2+
#![feature(format_args_capture)]
3+
4+
fn main() {
5+
named_argument_takes_precedence_to_captured();
6+
panic_with_single_argument_does_not_get_formatted();
7+
panic_with_multiple_arguments_is_formatted();
8+
formatting_parameters_can_be_captured();
9+
}
10+
11+
fn named_argument_takes_precedence_to_captured() {
12+
let foo = "captured";
13+
let s = format!("{foo}", foo="named");
14+
assert_eq!(&s, "named");
15+
16+
let s = format!("{foo}-{foo}-{foo}", foo="named");
17+
assert_eq!(&s, "named-named-named");
18+
19+
let s = format!("{}-{bar}-{foo}", "positional", bar="named");
20+
assert_eq!(&s, "positional-named-captured");
21+
}
22+
23+
fn panic_with_single_argument_does_not_get_formatted() {
24+
// panic! with a single argument does not perform string formatting.
25+
// RFC #2795 suggests that this may need to change so that captured arguments are formatted.
26+
// For stability reasons this will need to part of an edition change.
27+
28+
let msg = std::panic::catch_unwind(|| {
29+
panic!("{foo}");
30+
}).unwrap_err();
31+
32+
assert_eq!(msg.downcast_ref::<&str>(), Some(&"{foo}"))
33+
}
34+
35+
fn panic_with_multiple_arguments_is_formatted() {
36+
let foo = "captured";
37+
38+
let msg = std::panic::catch_unwind(|| {
39+
panic!("{}-{bar}-{foo}", "positional", bar="named");
40+
}).unwrap_err();
41+
42+
assert_eq!(msg.downcast_ref::<String>(), Some(&"positional-named-captured".to_string()))
43+
}
44+
45+
fn formatting_parameters_can_be_captured() {
46+
let width = 9;
47+
let precision = 3;
48+
49+
let x = 7.0;
50+
51+
let s = format!("{x:width$}");
52+
assert_eq!(&s, " 7");
53+
54+
let s = format!("{x:<width$}");
55+
assert_eq!(&s, "7 ");
56+
57+
let s = format!("{x:-^width$}");
58+
assert_eq!(&s, "----7----");
59+
60+
let s = format!("{x:-^width$.precision$}");
61+
assert_eq!(&s, "--7.000--");
62+
}

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

+15
Original file line numberDiff line numberDiff line change
@@ -63,18 +63,27 @@ error: there is no argument named `foo`
6363
|
6464
LL | format!("{} {foo} {} {bar} {}", 1, 2, 3);
6565
| ^^^^^
66+
|
67+
= note: did you intend to capture a variable `foo` from the surrounding scope?
68+
= help: add `#![feature(format_args_capture)]` to the crate attributes to enable
6669

6770
error: there is no argument named `bar`
6871
--> $DIR/ifmt-bad-arg.rs:27:26
6972
|
7073
LL | format!("{} {foo} {} {bar} {}", 1, 2, 3);
7174
| ^^^^^
75+
|
76+
= note: did you intend to capture a variable `bar` from the surrounding scope?
77+
= help: add `#![feature(format_args_capture)]` to the crate attributes to enable
7278

7379
error: there is no argument named `foo`
7480
--> $DIR/ifmt-bad-arg.rs:31:14
7581
|
7682
LL | format!("{foo}");
7783
| ^^^^^
84+
|
85+
= note: did you intend to capture a variable `foo` from the surrounding scope?
86+
= help: add `#![feature(format_args_capture)]` to the crate attributes to enable
7887

7988
error: multiple unused formatting arguments
8089
--> $DIR/ifmt-bad-arg.rs:32:17
@@ -155,6 +164,9 @@ error: there is no argument named `valueb`
155164
|
156165
LL | format!("{valuea} {valueb}", valuea=5, valuec=7);
157166
| ^^^^^^^^
167+
|
168+
= note: did you intend to capture a variable `valueb` from the surrounding scope?
169+
= help: add `#![feature(format_args_capture)]` to the crate attributes to enable
158170

159171
error: named argument never used
160172
--> $DIR/ifmt-bad-arg.rs:45:51
@@ -205,6 +217,9 @@ error: there is no argument named `foo`
205217
|
206218
LL | {foo}
207219
| ^^^^^
220+
|
221+
= note: did you intend to capture a variable `foo` from the surrounding scope?
222+
= help: add `#![feature(format_args_capture)]` to the crate attributes to enable
208223

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

0 commit comments

Comments
 (0)