Skip to content

Commit d7243ee

Browse files
committed
Don't trim non-parenthesis spans in unused_parens for bounds
`poly_trait_ref.parens` only records that the parser saw parentheses around a trait-object/impl-trait bound; it does not guarantee that the bound's span actually points at those parentheses in the source. A proc-macro can synthesize the parentheses while reusing an unrelated span from its input, so the span may not be wrapped in parentheses at all. Previously the lint unconditionally trimmed the first and last byte of the span to build the "remove these parentheses" suggestion. On such reused spans this produced an invalid suggestion (e.g. rewriting a field `val: u8` to `al: u`) and could even ICE when the span started or ended on a multibyte character. Only emit the lint when the source text at the span really is wrapped in parentheses.
1 parent a962594 commit d7243ee

3 files changed

Lines changed: 81 additions & 13 deletions

File tree

compiler/rustc_lint/src/unused.rs

Lines changed: 25 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -937,21 +937,33 @@ impl EarlyLintPass for UnusedParens {
937937
&& !dyn2015_exception
938938
{
939939
let s = poly_trait_ref.span;
940-
let spans = (!s.from_expansion()).then(|| {
941-
(
940+
// `poly_trait_ref.parens` only records that the parser saw
941+
// parentheses around this bound; it does not guarantee that `s`
942+
// points at them in the source. A proc-macro can synthesize the
943+
// parentheses while reusing an unrelated span from its input (see
944+
// issue #144378), in which case trimming the first and last byte
945+
// of `s` would corrupt the suggestion and could even ICE on a
946+
// multibyte character. Only lint when the source really is
947+
// wrapped in parentheses.
948+
if !s.from_expansion()
949+
&& let Ok(snippet) = cx.sess().source_map().span_to_snippet(s)
950+
&& snippet.starts_with('(')
951+
&& snippet.ends_with(')')
952+
{
953+
let spans = Some((
942954
s.with_hi(s.lo() + rustc_span::BytePos(1)),
943955
s.with_lo(s.hi() - rustc_span::BytePos(1)),
944-
)
945-
});
946-
947-
self.emit_unused_delims(
948-
cx,
949-
poly_trait_ref.span,
950-
spans,
951-
"type",
952-
(false, false),
953-
false,
954-
);
956+
));
957+
958+
self.emit_unused_delims(
959+
cx,
960+
poly_trait_ref.span,
961+
spans,
962+
"type",
963+
(false, false),
964+
false,
965+
);
966+
}
955967
}
956968
}
957969
}
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
extern crate proc_macro;
2+
3+
use proc_macro::{Group, Span, TokenStream, TokenTree};
4+
5+
// Recursively overwrite the span of every token (including group delimiters)
6+
// with `span`.
7+
fn respan(span: Span, stream: TokenStream) -> TokenStream {
8+
stream
9+
.into_iter()
10+
.map(|tt| match tt {
11+
TokenTree::Group(group) => {
12+
let mut group = Group::new(group.delimiter(), respan(span, group.stream()));
13+
group.set_span(span);
14+
TokenTree::Group(group)
15+
}
16+
mut tt => {
17+
tt.set_span(span);
18+
tt
19+
}
20+
})
21+
.collect()
22+
}
23+
24+
/// Emits `const _: &dyn (Send) = &();` with every token carrying the span of the
25+
/// macro's first input token. The parenthesized trait-object bound is therefore
26+
/// reported at a span that does not actually contain parentheses in the source.
27+
#[proc_macro]
28+
pub fn emit_parenthesized_bound(input: TokenStream) -> TokenStream {
29+
let span = input.into_iter().next().unwrap().span();
30+
let code: TokenStream = "const _: &dyn (Send) = &();".parse().unwrap();
31+
respan(span, code)
32+
}
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
//@ check-pass
2+
//@ edition: 2021
3+
//@ proc-macro: unused-parens-bound-proc-macro.rs
4+
5+
// Regression test for #144378.
6+
//
7+
// A proc-macro can synthesize parentheses around a trait-object bound while
8+
// reusing a span from its input. That span does not actually point at the
9+
// parentheses (or at anything resembling them), so the `unused_parens` lint
10+
// must not blindly trim its first and last byte: doing so produced an invalid
11+
// suggestion (e.g. turning a field `val: u8` into `al: u`) and could ICE when
12+
// the reused span started or ended on a multibyte character.
13+
14+
#![deny(unused_parens)]
15+
#![allow(uncommon_codepoints)]
16+
17+
extern crate unused_parens_bound_proc_macro;
18+
19+
// The generated `&dyn (Send)` reuses the span of the multibyte identifier
20+
// `naïve`. Before the fix this ICEd; now the lint is silently skipped because
21+
// the source span is not wrapped in parentheses.
22+
unused_parens_bound_proc_macro::emit_parenthesized_bound!(naïve);
23+
24+
fn main() {}

0 commit comments

Comments
 (0)