Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add format_args_capture feature #73670

Merged
merged 6 commits into from
Jul 4, 2020
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
47 changes: 47 additions & 0 deletions src/doc/unstable-book/src/library-features/format-args-capture.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
# `format_args_capture`

The tracking issue for this feature is: [#67984]

[#67984]: https://github.com/rust-lang/rust/issues/67984

------------------------

Enables `format_args!` (and macros which use `format_args!` in their implementation, such
as `format!`, `print!` and `panic!`) to capture variables from the surrounding scope.
This avoids the need to pass named parameters when the binding in question
already exists in scope.

```rust
#![feature(format_args_capture)]

let (person, species, name) = ("Charlie Brown", "dog", "Snoopy");

// captures named argument `person`
print!("Hello {person}");

// captures named arguments `species` and `name`
format!("The {species}'s name is {name}.");
```

This also works for formatting parameters such as width and precision:

```rust
#![feature(format_args_capture)]

let precision = 2;
let s = format!("{:.precision$}", 1.324223);

assert_eq!(&s, "1.32");
```

A non-exhaustive list of macros which benefit from this functionality include:
- `format!`
- `print!` and `println!`
- `eprint!` and `eprintln!`
- `write!` and `writeln!`
- `panic!`
- `unreachable!`
- `unimplemented!`
- `todo!`
- `assert!` and similar
- macros in many thirdparty crates, such as `log`
57 changes: 53 additions & 4 deletions src/librustc_builtin_macros/format.rs
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,9 @@ struct Context<'a, 'b> {
arg_spans: Vec<Span>,
/// All the formatting arguments that have formatting flags set, in order for diagnostics.
arg_with_formatting: Vec<parse::FormatSpec<'a>>,

/// Whether this format string came from a string literal, as opposed to a macro.
is_literal: bool,
}

/// Parses the arguments from the given list of tokens, returning the diagnostic
Expand Down Expand Up @@ -498,10 +501,55 @@ impl<'a, 'b> Context<'a, 'b> {
self.verify_arg_type(Exact(idx), ty)
}
None => {
let msg = format!("there is no argument named `{}`", name);
let sp = *self.arg_spans.get(self.curpiece).unwrap_or(&self.fmtsp);
let mut err = self.ecx.struct_span_err(sp, &msg[..]);
err.emit();
let capture_feature_enabled = self
.ecx
.ecfg
.features
.map_or(false, |features| features.format_args_capture);

// For the moment capturing variables from format strings expanded from macros is
// disabled (see RFC #2795)
let can_capture = capture_feature_enabled && self.is_literal;

if can_capture {
// Treat this name as a variable to capture from the surrounding scope
let idx = self.args.len();
self.arg_types.push(Vec::new());
self.arg_unique_types.push(Vec::new());
self.args.push(
self.ecx.expr_ident(self.fmtsp, Ident::new(name, self.fmtsp)),
);
self.names.insert(name, idx);
self.verify_arg_type(Exact(idx), ty)
} else {
let msg = format!("there is no argument named `{}`", name);
let sp = if self.is_literal {
*self.arg_spans.get(self.curpiece).unwrap_or(&self.fmtsp)
} else {
self.fmtsp
};
let mut err = self.ecx.struct_span_err(sp, &msg[..]);

if capture_feature_enabled && !self.is_literal {
err.note(&format!(
"did you intend to capture a variable `{}` from \
the surrounding scope?",
name
));
davidhewitt marked this conversation as resolved.
Show resolved Hide resolved
err.note(
"for hygiene reasons format_args! cannot capture variables \
davidhewitt marked this conversation as resolved.
Show resolved Hide resolved
when the format string is expanded from a macro",
);
} else if self.ecx.parse_sess().unstable_features.is_nightly_build() {
err.help(&format!(
"if you intended to capture `{}` from the surrounding scope, add \
`#![feature(format_args_capture)]` to the crate attributes",
name
));
}

err.emit();
}
}
}
}
Expand Down Expand Up @@ -951,6 +999,7 @@ pub fn expand_preparsed_format_args(
invalid_refs: Vec::new(),
arg_spans,
arg_with_formatting: Vec::new(),
is_literal: parser.is_literal,
};

// This needs to happen *after* the Parser has consumed all pieces to create all the spans
Expand Down
3 changes: 3 additions & 0 deletions src/librustc_feature/active.rs
Original file line number Diff line number Diff line change
Expand Up @@ -577,6 +577,9 @@ declare_features! (
/// Be more precise when looking for live drops in a const context.
(active, const_precise_live_drops, "1.46.0", Some(73255), None),

/// Allows capturing variables in scope using format_args!
(active, format_args_capture, "1.46.0", Some(67984), None),

// -------------------------------------------------------------------------
// feature-group-end: actual feature gates
// -------------------------------------------------------------------------
Expand Down
2 changes: 1 addition & 1 deletion src/librustc_parse_format/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -190,7 +190,7 @@ pub struct Parser<'a> {
/// Whether the source string is comes from `println!` as opposed to `format!` or `print!`
append_newline: bool,
/// Whether this formatting string is a literal or it comes from a macro.
is_literal: bool,
pub is_literal: bool,
/// Start position of the current line.
cur_line_start: usize,
/// Start and end byte offset of every line of the format string. Excludes
Expand Down
1 change: 1 addition & 0 deletions src/librustc_span/symbol.rs
Original file line number Diff line number Diff line change
Expand Up @@ -338,6 +338,7 @@ symbols! {
forbid,
format_args,
format_args_nl,
format_args_capture,
from,
From,
from_desugaring,
Expand Down
6 changes: 6 additions & 0 deletions src/test/ui/fmt/feature-gate-format-args-capture.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
fn main() {
format!("{foo}"); //~ ERROR: there is no argument named `foo`

// panic! doesn't hit format_args! unless there are two or more arguments.
panic!("{foo} {bar}", bar=1); //~ ERROR: there is no argument named `foo`
}
18 changes: 18 additions & 0 deletions src/test/ui/fmt/feature-gate-format-args-capture.stderr
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
error: there is no argument named `foo`
--> $DIR/feature-gate-format-args-capture.rs:2:14
|
LL | format!("{foo}");
| ^^^^^
|
= help: if you intended to capture `foo` from the surrounding scope, add `#![feature(format_args_capture)]` to the crate attributes

error: there is no argument named `foo`
--> $DIR/feature-gate-format-args-capture.rs:5:13
|
LL | panic!("{foo} {bar}", bar=1);
| ^^^^^
|
= help: if you intended to capture `foo` from the surrounding scope, add `#![feature(format_args_capture)]` to the crate attributes

error: aborting due to 2 previous errors

6 changes: 6 additions & 0 deletions src/test/ui/fmt/format-args-capture-macro-hygiene.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
#![feature(format_args_capture)]

fn main() {
format!(concat!("{foo}")); //~ ERROR: there is no argument named `foo`
format!(concat!("{ba", "r} {}"), 1); //~ ERROR: there is no argument named `bar`
}
22 changes: 22 additions & 0 deletions src/test/ui/fmt/format-args-capture-macro-hygiene.stderr
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
error: there is no argument named `foo`
--> $DIR/format-args-capture-macro-hygiene.rs:4:13
|
LL | format!(concat!("{foo}"));
| ^^^^^^^^^^^^^^^^
|
= note: did you intend to capture a variable `foo` from the surrounding scope?
= note: for hygiene reasons format_args! cannot capture variables when the format string is expanded from a macro
= note: this error originates in a macro (in Nightly builds, run with -Z macro-backtrace for more info)

error: there is no argument named `bar`
--> $DIR/format-args-capture-macro-hygiene.rs:5:13
|
LL | format!(concat!("{ba", "r} {}"), 1);
| ^^^^^^^^^^^^^^^^^^^^^^^
|
= note: did you intend to capture a variable `bar` from the surrounding scope?
= note: for hygiene reasons format_args! cannot capture variables when the format string is expanded from a macro
= note: this error originates in a macro (in Nightly builds, run with -Z macro-backtrace for more info)

error: aborting due to 2 previous errors

22 changes: 22 additions & 0 deletions src/test/ui/fmt/format-args-capture-missing-variables.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
#![feature(format_args_capture)]

fn main() {
format!("{} {foo} {} {bar} {}", 1, 2, 3);
//~^ ERROR: cannot find value `foo` in this scope
//~^^ ERROR: cannot find value `bar` in this scope

format!("{foo}"); //~ ERROR: cannot find value `foo` in this scope

format!("{valuea} {valueb}", valuea=5, valuec=7);
//~^ ERROR cannot find value `valueb` in this scope
//~^^ ERROR named argument never used

format!(r##"

{foo}

"##);
//~^^^^^ ERROR: cannot find value `foo` in this scope

panic!("{foo} {bar}", bar=1); //~ ERROR: cannot find value `foo` in this scope
}
52 changes: 52 additions & 0 deletions src/test/ui/fmt/format-args-capture-missing-variables.stderr
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
error: named argument never used
--> $DIR/format-args-capture-missing-variables.rs:10:51
|
LL | format!("{valuea} {valueb}", valuea=5, valuec=7);
| ------------------- ^ named argument never used
| |
| formatting specifier missing

error[E0425]: cannot find value `foo` in this scope
--> $DIR/format-args-capture-missing-variables.rs:4:13
|
LL | format!("{} {foo} {} {bar} {}", 1, 2, 3);
| ^^^^^^^^^^^^^^^^^^^^^^ not found in this scope

error[E0425]: cannot find value `bar` in this scope
--> $DIR/format-args-capture-missing-variables.rs:4:13
|
LL | format!("{} {foo} {} {bar} {}", 1, 2, 3);
| ^^^^^^^^^^^^^^^^^^^^^^ not found in this scope

error[E0425]: cannot find value `foo` in this scope
--> $DIR/format-args-capture-missing-variables.rs:8:13
|
LL | format!("{foo}");
| ^^^^^^^ not found in this scope

error[E0425]: cannot find value `valueb` in this scope
--> $DIR/format-args-capture-missing-variables.rs:10:13
|
LL | format!("{valuea} {valueb}", valuea=5, valuec=7);
| ^^^^^^^^^^^^^^^^^^^ not found in this scope

error[E0425]: cannot find value `foo` in this scope
--> $DIR/format-args-capture-missing-variables.rs:14:13
|
LL | format!(r##"
| _____________^
LL | |
LL | | {foo}
LL | |
LL | | "##);
| |_______^ not found in this scope

error[E0425]: cannot find value `foo` in this scope
--> $DIR/format-args-capture-missing-variables.rs:21:12
|
LL | panic!("{foo} {bar}", bar=1);
| ^^^^^^^^^^^^^ not found in this scope

error: aborting due to 7 previous errors

For more information about this error, try `rustc --explain E0425`.
62 changes: 62 additions & 0 deletions src/test/ui/fmt/format-args-capture.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
// run-pass
#![feature(format_args_capture)]

fn main() {
named_argument_takes_precedence_to_captured();
panic_with_single_argument_does_not_get_formatted();
panic_with_multiple_arguments_is_formatted();
formatting_parameters_can_be_captured();
}

fn named_argument_takes_precedence_to_captured() {
let foo = "captured";
let s = format!("{foo}", foo="named");
assert_eq!(&s, "named");

let s = format!("{foo}-{foo}-{foo}", foo="named");
assert_eq!(&s, "named-named-named");

let s = format!("{}-{bar}-{foo}", "positional", bar="named");
assert_eq!(&s, "positional-named-captured");
}

fn panic_with_single_argument_does_not_get_formatted() {
// panic! with a single argument does not perform string formatting.
// RFC #2795 suggests that this may need to change so that captured arguments are formatted.
// For stability reasons this will need to part of an edition change.

let msg = std::panic::catch_unwind(|| {
panic!("{foo}");
}).unwrap_err();

assert_eq!(msg.downcast_ref::<&str>(), Some(&"{foo}"))
}

fn panic_with_multiple_arguments_is_formatted() {
let foo = "captured";

let msg = std::panic::catch_unwind(|| {
panic!("{}-{bar}-{foo}", "positional", bar="named");
}).unwrap_err();

assert_eq!(msg.downcast_ref::<String>(), Some(&"positional-named-captured".to_string()))
}

fn formatting_parameters_can_be_captured() {
let width = 9;
let precision = 3;

let x = 7.0;

let s = format!("{x:width$}");
assert_eq!(&s, " 7");

let s = format!("{x:<width$}");
assert_eq!(&s, "7 ");

let s = format!("{x:-^width$}");
assert_eq!(&s, "----7----");

let s = format!("{x:-^width$.precision$}");
assert_eq!(&s, "--7.000--");
}
10 changes: 10 additions & 0 deletions src/test/ui/if/ifmt-bad-arg.stderr
Original file line number Diff line number Diff line change
Expand Up @@ -63,18 +63,24 @@ error: there is no argument named `foo`
|
LL | format!("{} {foo} {} {bar} {}", 1, 2, 3);
| ^^^^^
|
= help: if you intended to capture `foo` from the surrounding scope, add `#![feature(format_args_capture)]` to the crate attributes

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

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

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

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

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