diff --git a/compiler/rustc_lint/src/unused.rs b/compiler/rustc_lint/src/unused.rs index 7c934b2d16751..d439f6b67acb4 100644 --- a/compiler/rustc_lint/src/unused.rs +++ b/compiler/rustc_lint/src/unused.rs @@ -937,21 +937,41 @@ impl EarlyLintPass for UnusedParens { && !dyn2015_exception { let s = poly_trait_ref.span; - let spans = (!s.from_expansion()).then(|| { - ( + // `poly_trait_ref.parens == Yes` only promises that the *parser* + // saw parentheses around this bound. It does not guarantee that + // the source text covered by `s` is literally wrapped in ASCII + // parentheses, which is what the byte arithmetic below assumes: + // + // - A proc-macro can synthesize the parentheses while reusing an + // unrelated span from its input (issue #144378), so `s` may not + // point at parentheses at all. + // - The parser recovers from Unicode parens such as `( )`, so + // the first and last *characters* of `s` may be parens, but + // multibyte ones. + // + // In both cases trimming a single byte off each end would corrupt + // the suggestion or even ICE by slicing through a multibyte char. + // So only lint when the source really is wrapped in ASCII parens; + // those are exactly one byte each, which makes the trimming sound. + if !s.from_expansion() + && let Ok(snippet) = cx.sess().source_map().span_to_snippet(s) + && snippet.starts_with('(') + && snippet.ends_with(')') + { + let spans = Some(( s.with_hi(s.lo() + rustc_span::BytePos(1)), s.with_lo(s.hi() - rustc_span::BytePos(1)), - ) - }); - - self.emit_unused_delims( - cx, - poly_trait_ref.span, - spans, - "type", - (false, false), - false, - ); + )); + + self.emit_unused_delims( + cx, + poly_trait_ref.span, + spans, + "type", + (false, false), + false, + ); + } } } } diff --git a/tests/ui/lint/unused/auxiliary/unused-parens-bound-proc-macro.rs b/tests/ui/lint/unused/auxiliary/unused-parens-bound-proc-macro.rs new file mode 100644 index 0000000000000..7cb7e2112eba4 --- /dev/null +++ b/tests/ui/lint/unused/auxiliary/unused-parens-bound-proc-macro.rs @@ -0,0 +1,32 @@ +extern crate proc_macro; + +use proc_macro::{Group, Span, TokenStream, TokenTree}; + +// Recursively overwrite the span of every token (including group delimiters) +// with `span`. +fn respan(span: Span, stream: TokenStream) -> TokenStream { + stream + .into_iter() + .map(|tt| match tt { + TokenTree::Group(group) => { + let mut group = Group::new(group.delimiter(), respan(span, group.stream())); + group.set_span(span); + TokenTree::Group(group) + } + mut tt => { + tt.set_span(span); + tt + } + }) + .collect() +} + +/// Emits `const _: &dyn (Send) = &();` with every token carrying the span of the +/// macro's first input token. The parenthesized trait-object bound is therefore +/// reported at a span that does not actually contain parentheses in the source. +#[proc_macro] +pub fn emit_parenthesized_bound(input: TokenStream) -> TokenStream { + let span = input.into_iter().next().unwrap().span(); + let code: TokenStream = "const _: &dyn (Send) = &();".parse().unwrap(); + respan(span, code) +} diff --git a/tests/ui/lint/unused/unused-parens-trait-obj-proc-macro-144378.rs b/tests/ui/lint/unused/unused-parens-trait-obj-proc-macro-144378.rs new file mode 100644 index 0000000000000..58f4cd932a21f --- /dev/null +++ b/tests/ui/lint/unused/unused-parens-trait-obj-proc-macro-144378.rs @@ -0,0 +1,25 @@ +//@ check-pass +//@ edition: 2021 +//@ proc-macro: unused-parens-bound-proc-macro.rs + +// Regression test for #144378. +// +// A proc-macro can synthesize parentheses around a trait-object bound while +// reusing a span from its input. That span does not actually point at the +// parentheses, so the `unused_parens` lint must not blindly trim its first and +// last byte: doing so produced an invalid suggestion (e.g. turning a field +// `val: u8` into `al: u`) and, when the reused span started or ended on a +// multibyte character, ICEd by slicing through that character. + +#![deny(unused_parens)] +#![allow(uncommon_codepoints)] + +extern crate unused_parens_bound_proc_macro; + +// The generated `&dyn (Send)` reuses the span of the identifier `é`, whose +// first byte is the start of a two-byte character. Before the fix, trimming one +// byte off the front of that span sliced through `é` and ICEd; now the lint is +// skipped because the source span is not wrapped in parentheses. +unused_parens_bound_proc_macro::emit_parenthesized_bound!(é); + +fn main() {}