From 04b93fd2c7061ed41150e3fada3f7c4c85ca8208 Mon Sep 17 00:00:00 2001 From: Sanidhya Singh Date: Wed, 10 Jun 2026 10:26:48 +0530 Subject: [PATCH] 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. --- compiler/rustc_lint/src/unused.rs | 46 +++++++++++++------ .../unused-parens-bound-proc-macro.rs | 32 +++++++++++++ ...used-parens-trait-obj-proc-macro-144378.rs | 25 ++++++++++ 3 files changed, 90 insertions(+), 13 deletions(-) create mode 100644 tests/ui/lint/unused/auxiliary/unused-parens-bound-proc-macro.rs create mode 100644 tests/ui/lint/unused/unused-parens-trait-obj-proc-macro-144378.rs 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() {}