From 4baae5d8b3f5b681097c5afe9ad3e1494f223b1b Mon Sep 17 00:00:00 2001 From: Jason Newcomb Date: Wed, 19 Jun 2024 20:43:33 -0400 Subject: [PATCH] Add new `Span` utils to avoid both allocating and compressing/decompressing spans. --- clippy_lints/src/cognitive_complexity.rs | 22 +- clippy_lints/src/copies.rs | 16 +- clippy_lints/src/implicit_hasher.rs | 40 ++-- clippy_lints/src/matches/single_match.rs | 4 +- clippy_lints/src/methods/manual_inspect.rs | 31 ++- clippy_lints/src/missing_doc.rs | 7 +- clippy_lints/src/multiple_bound_locations.rs | 8 +- clippy_lints/src/needless_else.rs | 16 +- clippy_lints/src/needless_if.rs | 24 +- .../src/non_octal_unix_permissions.rs | 12 +- clippy_lints/src/octal_escapes.rs | 15 +- clippy_lints/src/ranges.rs | 19 +- clippy_utils/src/consts.rs | 8 +- clippy_utils/src/hir_utils.rs | 6 +- clippy_utils/src/source.rs | 224 ++++++++++++++---- 15 files changed, 296 insertions(+), 156 deletions(-) diff --git a/clippy_lints/src/cognitive_complexity.rs b/clippy_lints/src/cognitive_complexity.rs index e41abf42234..60815f4f2af 100644 --- a/clippy_lints/src/cognitive_complexity.rs +++ b/clippy_lints/src/cognitive_complexity.rs @@ -1,7 +1,7 @@ //! calculate cognitive complexity and warn about overly complex functions use clippy_utils::diagnostics::span_lint_and_help; -use clippy_utils::source::snippet_opt; +use clippy_utils::source::{IntoSpan, SpanRangeExt}; use clippy_utils::ty::is_type_diagnostic_item; use clippy_utils::visitors::for_each_expr_without_closures; use clippy_utils::{get_async_fn_body, is_async_fn, LimitStack}; @@ -12,7 +12,7 @@ use rustc_lint::{LateContext, LateLintPass, LintContext}; use rustc_session::impl_lint_pass; use rustc_span::def_id::LocalDefId; -use rustc_span::{sym, BytePos, Span}; +use rustc_span::{sym, Span}; declare_clippy_lint! { /// ### What it does @@ -50,7 +50,6 @@ pub fn new(limit: u64) -> Self { impl_lint_pass!(CognitiveComplexity => [COGNITIVE_COMPLEXITY]); impl CognitiveComplexity { - #[expect(clippy::cast_possible_truncation)] fn check<'tcx>( &mut self, cx: &LateContext<'tcx>, @@ -100,17 +99,12 @@ fn check<'tcx>( FnKind::ItemFn(ident, _, _) | FnKind::Method(ident, _) => ident.span, FnKind::Closure => { let header_span = body_span.with_hi(decl.output.span().lo()); - let pos = snippet_opt(cx, header_span).and_then(|snip| { - let low_offset = snip.find('|')?; - let high_offset = 1 + snip.get(low_offset + 1..)?.find('|')?; - let low = header_span.lo() + BytePos(low_offset as u32); - let high = low + BytePos(high_offset as u32 + 1); - - Some((low, high)) - }); - - if let Some((low, high)) = pos { - Span::new(low, high, header_span.ctxt(), header_span.parent()) + #[expect(clippy::range_plus_one)] + if let Some(range) = header_span.map_range(cx, |src, range| { + let mut idxs = src.get(range.clone())?.match_indices('|'); + Some(range.start + idxs.next()?.0..range.start + idxs.next()?.0 + 1) + }) { + range.with_ctxt(header_span.ctxt()) } else { return; } diff --git a/clippy_lints/src/copies.rs b/clippy_lints/src/copies.rs index 480df675d75..d896452be92 100644 --- a/clippy_lints/src/copies.rs +++ b/clippy_lints/src/copies.rs @@ -1,5 +1,5 @@ use clippy_utils::diagnostics::{span_lint_and_note, span_lint_and_then}; -use clippy_utils::source::{first_line_of_span, indent_of, reindent_multiline, snippet, snippet_opt}; +use clippy_utils::source::{first_line_of_span, indent_of, reindent_multiline, snippet, IntoSpan, SpanRangeExt}; use clippy_utils::ty::{needs_ordered_drop, InteriorMut}; use clippy_utils::visitors::for_each_expr_without_closures; use clippy_utils::{ @@ -14,7 +14,7 @@ use rustc_session::impl_lint_pass; use rustc_span::hygiene::walk_chain; use rustc_span::source_map::SourceMap; -use rustc_span::{BytePos, Span, Symbol}; +use rustc_span::{Span, Symbol}; use std::borrow::Cow; declare_clippy_lint! { @@ -266,12 +266,12 @@ fn lint_branches_sharing_code<'tcx>( let span = span.with_hi(last_block.span.hi()); // Improve formatting if the inner block has indention (i.e. normal Rust formatting) - let test_span = Span::new(span.lo() - BytePos(4), span.lo(), span.ctxt(), span.parent()); - let span = if snippet_opt(cx, test_span).map_or(false, |snip| snip == " ") { - span.with_lo(test_span.lo()) - } else { - span - }; + let span = span + .map_range(cx, |src, range| { + (range.start > 4 && src.get(range.start - 4..range.start)? == " ") + .then_some(range.start - 4..range.end) + }) + .map_or(span, |range| range.with_ctxt(span.ctxt())); (span, suggestion.to_string()) }); diff --git a/clippy_lints/src/implicit_hasher.rs b/clippy_lints/src/implicit_hasher.rs index ca830af3b2f..344a04e6e7e 100644 --- a/clippy_lints/src/implicit_hasher.rs +++ b/clippy_lints/src/implicit_hasher.rs @@ -14,7 +14,7 @@ use rustc_span::Span; use clippy_utils::diagnostics::{multispan_sugg, span_lint_and_then}; -use clippy_utils::source::{snippet, snippet_opt}; +use clippy_utils::source::{snippet, IntoSpan, SpanRangeExt}; use clippy_utils::ty::is_type_diagnostic_item; declare_clippy_lint! { @@ -59,10 +59,8 @@ declare_lint_pass!(ImplicitHasher => [IMPLICIT_HASHER]); impl<'tcx> LateLintPass<'tcx> for ImplicitHasher { - #[expect(clippy::cast_possible_truncation, clippy::too_many_lines)] + #[expect(clippy::too_many_lines)] fn check_item(&mut self, cx: &LateContext<'tcx>, item: &'tcx Item<'_>) { - use rustc_span::BytePos; - fn suggestion( cx: &LateContext<'_>, diag: &mut Diag<'_, ()>, @@ -123,10 +121,11 @@ fn suggestion( } let generics_suggestion_span = impl_.generics.span.substitute_dummy({ - let pos = snippet_opt(cx, item.span.until(target.span())) - .and_then(|snip| Some(item.span.lo() + BytePos(snip.find("impl")? as u32 + 4))); - if let Some(pos) = pos { - Span::new(pos, pos, item.span.ctxt(), item.span.parent()) + let range = (item.span.lo()..target.span().lo()).map_range(cx, |src, range| { + Some(src.get(range.clone())?.find("impl")? + 4..range.end) + }); + if let Some(range) = range { + range.with_ctxt(item.span.ctxt()) } else { return; } @@ -163,21 +162,16 @@ fn suggestion( continue; } let generics_suggestion_span = generics.span.substitute_dummy({ - let pos = snippet_opt( - cx, - Span::new( - item.span.lo(), - body.params[0].pat.span.lo(), - item.span.ctxt(), - item.span.parent(), - ), - ) - .and_then(|snip| { - let i = snip.find("fn")?; - Some(item.span.lo() + BytePos((i + snip[i..].find('(')?) as u32)) - }) - .expect("failed to create span for type parameters"); - Span::new(pos, pos, item.span.ctxt(), item.span.parent()) + let range = (item.span.lo()..body.params[0].pat.span.lo()).map_range(cx, |src, range| { + let (pre, post) = src.get(range.clone())?.split_once("fn")?; + let pos = post.find('(')? + pre.len() + 2; + Some(pos..pos) + }); + if let Some(range) = range { + range.with_ctxt(item.span.ctxt()) + } else { + return; + } }); let mut ctr_vis = ImplicitHasherConstructorVisitor::new(cx, target); diff --git a/clippy_lints/src/matches/single_match.rs b/clippy_lints/src/matches/single_match.rs index 69791414f72..99fdbcff890 100644 --- a/clippy_lints/src/matches/single_match.rs +++ b/clippy_lints/src/matches/single_match.rs @@ -1,5 +1,5 @@ use clippy_utils::diagnostics::span_lint_and_sugg; -use clippy_utils::source::{expr_block, get_source_text, snippet}; +use clippy_utils::source::{expr_block, snippet, SpanRangeExt}; use clippy_utils::ty::{implements_trait, is_type_diagnostic_item, peel_mid_ty_refs}; use clippy_utils::{is_lint_allowed, is_unit_expr, is_wild, peel_blocks, peel_hir_pat_refs, peel_n_hir_expr_refs}; use core::cmp::max; @@ -17,7 +17,7 @@ /// span, e.g. a string literal `"//"`, but we know that this isn't the case for empty /// match arms. fn empty_arm_has_comment(cx: &LateContext<'_>, span: Span) -> bool { - if let Some(ff) = get_source_text(cx, span) + if let Some(ff) = span.get_source_text(cx) && let Some(text) = ff.as_str() { text.as_bytes().windows(2).any(|w| w == b"//" || w == b"/*") diff --git a/clippy_lints/src/methods/manual_inspect.rs b/clippy_lints/src/methods/manual_inspect.rs index 2f9b951c6a7..e3ce64c246a 100644 --- a/clippy_lints/src/methods/manual_inspect.rs +++ b/clippy_lints/src/methods/manual_inspect.rs @@ -1,6 +1,6 @@ use clippy_config::msrvs::{self, Msrv}; use clippy_utils::diagnostics::span_lint_and_then; -use clippy_utils::source::{get_source_text, with_leading_whitespace, SpanRange}; +use clippy_utils::source::{IntoSpan, SpanRangeExt}; use clippy_utils::ty::get_field_by_name; use clippy_utils::visitors::{for_each_expr, for_each_expr_without_closures}; use clippy_utils::{expr_use_ctxt, is_diag_item_method, is_diag_trait_item, path_to_local_id, ExprUseNode}; @@ -9,7 +9,7 @@ use rustc_hir::{BindingMode, BorrowKind, ByRef, ClosureKind, Expr, ExprKind, Mutability, Node, PatKind}; use rustc_lint::LateContext; use rustc_middle::ty::adjustment::{Adjust, Adjustment, AutoBorrow, AutoBorrowMutability}; -use rustc_span::{sym, BytePos, Span, Symbol, DUMMY_SP}; +use rustc_span::{sym, Span, Symbol, DUMMY_SP}; use super::MANUAL_INSPECT; @@ -98,17 +98,19 @@ pub(crate) fn check(cx: &LateContext<'_>, expr: &Expr<'_>, arg: &Expr<'_>, name: let mut addr_of_edits = Vec::with_capacity(delayed.len()); for x in delayed { match x { - UseKind::Return(s) => edits.push((with_leading_whitespace(cx, s).set_span_pos(s), String::new())), + UseKind::Return(s) => edits.push((s.with_leading_whitespace(cx).with_ctxt(s.ctxt()), String::new())), UseKind::Borrowed(s) => { - if let Some(src) = get_source_text(cx, s) - && let Some(src) = src.as_str() - && let trim_src = src.trim_start_matches([' ', '\t', '\n', '\r', '(']) - && trim_src.starts_with('&') - { - let range = s.into_range(); - #[expect(clippy::cast_possible_truncation)] - let start = BytePos(range.start.0 + (src.len() - trim_src.len()) as u32); - addr_of_edits.push(((start..BytePos(start.0 + 1)).set_span_pos(s), String::new())); + #[expect(clippy::range_plus_one)] + let range = s.map_range(cx, |src, range| { + let src = src.get(range.clone())?; + let trimmed = src.trim_start_matches([' ', '\t', '\n', '\r', '(']); + trimmed.starts_with('&').then(|| { + let pos = range.start + src.len() - trimmed.len(); + pos..pos + 1 + }) + }); + if let Some(range) = range { + addr_of_edits.push((range.with_ctxt(s.ctxt()), String::new())); } else { requires_copy = true; requires_deref = true; @@ -174,7 +176,10 @@ pub(crate) fn check(cx: &LateContext<'_>, expr: &Expr<'_>, arg: &Expr<'_>, name: }), )); edits.push(( - with_leading_whitespace(cx, final_expr.span).set_span_pos(final_expr.span), + final_expr + .span + .with_leading_whitespace(cx) + .with_ctxt(final_expr.span.ctxt()), String::new(), )); let app = if edits.iter().any(|(s, _)| s.from_expansion()) { diff --git a/clippy_lints/src/missing_doc.rs b/clippy_lints/src/missing_doc.rs index ca344dc5c81..250fd5cbd48 100644 --- a/clippy_lints/src/missing_doc.rs +++ b/clippy_lints/src/missing_doc.rs @@ -8,7 +8,7 @@ use clippy_utils::attrs::is_doc_hidden; use clippy_utils::diagnostics::span_lint; use clippy_utils::is_from_proc_macro; -use clippy_utils::source::snippet_opt; +use clippy_utils::source::SpanRangeExt; use rustc_ast::ast::{self, MetaItem, MetaItemKind}; use rustc_hir as hir; use rustc_hir::def_id::LocalDefId; @@ -266,8 +266,5 @@ fn check_variant(&mut self, cx: &LateContext<'tcx>, v: &'tcx hir::Variant<'_>) { } fn span_to_snippet_contains_docs(cx: &LateContext<'_>, search_span: Span) -> bool { - let Some(snippet) = snippet_opt(cx, search_span) else { - return false; - }; - snippet.lines().rev().any(|line| line.trim().starts_with("///")) + search_span.check_source_text(cx, |src| src.lines().rev().any(|line| line.trim().starts_with("///"))) } diff --git a/clippy_lints/src/multiple_bound_locations.rs b/clippy_lints/src/multiple_bound_locations.rs index d608f3bf7b4..d276e29bace 100644 --- a/clippy_lints/src/multiple_bound_locations.rs +++ b/clippy_lints/src/multiple_bound_locations.rs @@ -6,7 +6,7 @@ use rustc_span::Span; use clippy_utils::diagnostics::span_lint; -use clippy_utils::source::snippet_opt; +use clippy_utils::source::SpanRangeExt; declare_clippy_lint! { /// ### What it does @@ -54,8 +54,10 @@ fn check_fn(&mut self, cx: &EarlyContext<'_>, kind: FnKind<'_>, _: Span, _: Node match clause { WherePredicate::BoundPredicate(pred) => { if (!pred.bound_generic_params.is_empty() || !pred.bounds.is_empty()) - && let Some(name) = snippet_opt(cx, pred.bounded_ty.span) - && let Some(bound_span) = generic_params_with_bounds.get(name.as_str()) + && let Some(Some(bound_span)) = pred + .bounded_ty + .span + .with_source_text(cx, |src| generic_params_with_bounds.get(src)) { emit_lint(cx, *bound_span, pred.bounded_ty.span); } diff --git a/clippy_lints/src/needless_else.rs b/clippy_lints/src/needless_else.rs index b6aad69d166..f8bb72a16db 100644 --- a/clippy_lints/src/needless_else.rs +++ b/clippy_lints/src/needless_else.rs @@ -1,8 +1,8 @@ use clippy_utils::diagnostics::span_lint_and_sugg; -use clippy_utils::source::{snippet_opt, trim_span}; +use clippy_utils::source::{IntoSpan, SpanRangeExt}; use rustc_ast::ast::{Expr, ExprKind}; use rustc_errors::Applicability; -use rustc_lint::{EarlyContext, EarlyLintPass, LintContext}; +use rustc_lint::{EarlyContext, EarlyLintPass}; use rustc_session::declare_lint_pass; declare_clippy_lint! { @@ -41,16 +41,16 @@ fn check_expr(&mut self, cx: &EarlyContext<'_>, expr: &Expr) { && !expr.span.from_expansion() && !else_clause.span.from_expansion() && block.stmts.is_empty() - && let Some(trimmed) = expr.span.trim_start(then_block.span) - && let span = trim_span(cx.sess().source_map(), trimmed) - && let Some(else_snippet) = snippet_opt(cx, span) - // Ignore else blocks that contain comments or #[cfg]s - && !else_snippet.contains(['/', '#']) + && let range = (then_block.span.hi()..expr.span.hi()).trim_start(cx) + && range.clone().check_source_text(cx, |src| { + // Ignore else blocks that contain comments or #[cfg]s + !src.contains(['/', '#']) + }) { span_lint_and_sugg( cx, NEEDLESS_ELSE, - span, + range.with_ctxt(expr.span.ctxt()), "this `else` branch is empty", "you can remove it", String::new(), diff --git a/clippy_lints/src/needless_if.rs b/clippy_lints/src/needless_if.rs index 51bee4b51f6..1d6233d432a 100644 --- a/clippy_lints/src/needless_if.rs +++ b/clippy_lints/src/needless_if.rs @@ -1,7 +1,7 @@ use clippy_utils::diagnostics::span_lint_and_sugg; use clippy_utils::higher::If; use clippy_utils::is_from_proc_macro; -use clippy_utils::source::snippet_opt; +use clippy_utils::source::{snippet_opt, SpanRangeExt}; use rustc_errors::Applicability; use rustc_hir::{ExprKind, Stmt, StmtKind}; use rustc_lint::{LateContext, LateLintPass, LintContext}; @@ -39,18 +39,24 @@ impl LateLintPass<'_> for NeedlessIf { fn check_stmt<'tcx>(&mut self, cx: &LateContext<'tcx>, stmt: &Stmt<'tcx>) { if let StmtKind::Expr(expr) = stmt.kind - && let Some(If {cond, then, r#else: None }) = If::hir(expr) + && let Some(If { + cond, + then, + r#else: None, + }) = If::hir(expr) && let ExprKind::Block(block, ..) = then.kind && block.stmts.is_empty() && block.expr.is_none() && !in_external_macro(cx.sess(), expr.span) - && let Some(then_snippet) = snippet_opt(cx, then.span) - // Ignore - // - empty macro expansions - // - empty reptitions in macro expansions - // - comments - // - #[cfg]'d out code - && then_snippet.chars().all(|ch| matches!(ch, '{' | '}') || ch.is_ascii_whitespace()) + && then.span.check_source_text(cx, |src| { + // Ignore + // - empty macro expansions + // - empty reptitions in macro expansions + // - comments + // - #[cfg]'d out code + src.bytes() + .all(|ch| matches!(ch, b'{' | b'}') || ch.is_ascii_whitespace()) + }) && let Some(cond_snippet) = snippet_opt(cx, cond.span) && !is_from_proc_macro(cx, expr) { diff --git a/clippy_lints/src/non_octal_unix_permissions.rs b/clippy_lints/src/non_octal_unix_permissions.rs index 2701d6bdca3..b915df52762 100644 --- a/clippy_lints/src/non_octal_unix_permissions.rs +++ b/clippy_lints/src/non_octal_unix_permissions.rs @@ -1,5 +1,5 @@ use clippy_utils::diagnostics::span_lint_and_sugg; -use clippy_utils::source::{snippet_opt, snippet_with_applicability}; +use clippy_utils::source::{snippet_with_applicability, SpanRangeExt}; use clippy_utils::{match_def_path, paths}; use rustc_errors::Applicability; use rustc_hir::{Expr, ExprKind}; @@ -53,8 +53,9 @@ fn check_expr(&mut self, cx: &LateContext<'tcx>, expr: &'tcx Expr<'tcx>) { && cx.tcx.is_diagnostic_item(sym::FsPermissions, adt.did()))) && let ExprKind::Lit(_) = param.kind && param.span.eq_ctxt(expr.span) - && let Some(snip) = snippet_opt(cx, param.span) - && !(snip.starts_with("0o") || snip.starts_with("0b")) + && param + .span + .check_source_text(cx, |src| !matches!(src.as_bytes(), [b'0', b'o' | b'b', ..])) { show_error(cx, param); } @@ -65,8 +66,9 @@ fn check_expr(&mut self, cx: &LateContext<'tcx>, expr: &'tcx Expr<'tcx>) { && match_def_path(cx, def_id, &paths::PERMISSIONS_FROM_MODE) && let ExprKind::Lit(_) = param.kind && param.span.eq_ctxt(expr.span) - && let Some(snip) = snippet_opt(cx, param.span) - && !(snip.starts_with("0o") || snip.starts_with("0b")) + && param + .span + .check_source_text(cx, |src| !matches!(src.as_bytes(), [b'0', b'o' | b'b', ..])) { show_error(cx, param); } diff --git a/clippy_lints/src/octal_escapes.rs b/clippy_lints/src/octal_escapes.rs index 0a7a2cd616c..2eae9b23746 100644 --- a/clippy_lints/src/octal_escapes.rs +++ b/clippy_lints/src/octal_escapes.rs @@ -1,5 +1,5 @@ use clippy_utils::diagnostics::span_lint_and_then; -use clippy_utils::source::get_source_text; +use clippy_utils::source::SpanRangeExt; use rustc_ast::token::LitKind; use rustc_ast::{Expr, ExprKind}; use rustc_errors::Applicability; @@ -87,14 +87,11 @@ fn check_expr(&mut self, cx: &EarlyContext<'_>, expr: &Expr) { // Last check to make sure the source text matches what we read from the string. // Macros are involved somehow if this doesn't match. - if let Some(src) = get_source_text(cx, span) - && let Some(src) = src.as_str() - && match *src.as_bytes() { - [b'\\', b'0', lo] => lo == c_lo, - [b'\\', b'0', hi, lo] => hi == c_hi && lo == c_lo, - _ => false, - } - { + if span.check_source_text(cx, |src| match *src.as_bytes() { + [b'\\', b'0', lo] => lo == c_lo, + [b'\\', b'0', hi, lo] => hi == c_hi && lo == c_lo, + _ => false, + }) { span_lint_and_then(cx, OCTAL_ESCAPES, span, "octal-looking escape in a literal", |diag| { diag.help_once("octal escapes are not supported, `\\0` is always null") .span_suggestion( diff --git a/clippy_lints/src/ranges.rs b/clippy_lints/src/ranges.rs index 186e548d373..4fdaa9f00a1 100644 --- a/clippy_lints/src/ranges.rs +++ b/clippy_lints/src/ranges.rs @@ -1,7 +1,7 @@ use clippy_config::msrvs::{self, Msrv}; use clippy_utils::consts::{constant, Constant}; use clippy_utils::diagnostics::{span_lint, span_lint_and_sugg, span_lint_and_then}; -use clippy_utils::source::{snippet, snippet_opt, snippet_with_applicability}; +use clippy_utils::source::{snippet, snippet_with_applicability, SpanRangeExt}; use clippy_utils::sugg::Sugg; use clippy_utils::{get_parent_expr, higher, in_constant, is_integer_const, path_to_local}; use rustc_ast::ast::RangeLimits; @@ -285,9 +285,10 @@ fn check_possible_range_contains( if let ExprKind::Binary(ref lhs_op, _left, new_lhs) = left.kind && op == lhs_op.node && let new_span = Span::new(new_lhs.span.lo(), right.span.hi(), expr.span.ctxt(), expr.span.parent()) - && let Some(snip) = &snippet_opt(cx, new_span) - // Do not continue if we have mismatched number of parens, otherwise the suggestion is wrong - && snip.matches('(').count() == snip.matches(')').count() + && new_span.check_source_text(cx, |src| { + // Do not continue if we have mismatched number of parens, otherwise the suggestion is wrong + src.matches('(').count() == src.matches(')').count() + }) { check_possible_range_contains(cx, op, new_lhs, right, expr, new_span); } @@ -363,17 +364,19 @@ fn check_exclusive_range_plus_one(cx: &LateContext<'_>, expr: &Expr<'_>) { |diag| { let start = start.map_or(String::new(), |x| Sugg::hir(cx, x, "x").maybe_par().to_string()); let end = Sugg::hir(cx, y, "y").maybe_par(); - if let Some(is_wrapped) = &snippet_opt(cx, span) { - if is_wrapped.starts_with('(') && is_wrapped.ends_with(')') { + match span.with_source_text(cx, |src| src.starts_with('(') && src.ends_with(')')) { + Some(true) => { diag.span_suggestion(span, "use", format!("({start}..={end})"), Applicability::MaybeIncorrect); - } else { + }, + Some(false) => { diag.span_suggestion( span, "use", format!("{start}..={end}"), Applicability::MachineApplicable, // snippet ); - } + }, + None => {}, } }, ); diff --git a/clippy_utils/src/consts.rs b/clippy_utils/src/consts.rs index 8f6389ee077..867c93c278e 100644 --- a/clippy_utils/src/consts.rs +++ b/clippy_utils/src/consts.rs @@ -1,7 +1,7 @@ #![allow(clippy::float_cmp)] use crate::macros::HirNode; -use crate::source::{get_source_text, walk_span_to_context}; +use crate::source::{walk_span_to_context, SpanRangeExt}; use crate::{clip, is_direct_expn_of, sext, unsext}; use rustc_ast::ast::{self, LitFloatType, LitKind}; @@ -15,8 +15,8 @@ use rustc_middle::ty::{self, EarlyBinder, FloatTy, GenericArgsRef, IntTy, List, ScalarInt, Ty, TyCtxt, UintTy}; use rustc_middle::{bug, mir, span_bug}; use rustc_span::def_id::DefId; +use rustc_span::sym; use rustc_span::symbol::{Ident, Symbol}; -use rustc_span::{sym, SyntaxContext}; use rustc_target::abi::Size; use std::cmp::Ordering; use std::hash::{Hash, Hasher}; @@ -664,11 +664,11 @@ fn block(&mut self, block: &Block<'_>) -> Option> { { // Try to detect any `cfg`ed statements or empty macro expansions. let span = block.span.data(); - if span.ctxt == SyntaxContext::root() { + if span.ctxt.is_root() { if let Some(expr_span) = walk_span_to_context(expr.span, span.ctxt) && let expr_lo = expr_span.lo() && expr_lo >= span.lo - && let Some(src) = get_source_text(self.lcx, span.lo..expr_lo) + && let Some(src) = (span.lo..expr_lo).get_source_text(self.lcx) && let Some(src) = src.as_str() { use rustc_lexer::TokenKind::{BlockComment, LineComment, OpenBrace, Semi, Whitespace}; diff --git a/clippy_utils/src/hir_utils.rs b/clippy_utils/src/hir_utils.rs index 50dd8430ac0..8706cec5d38 100644 --- a/clippy_utils/src/hir_utils.rs +++ b/clippy_utils/src/hir_utils.rs @@ -1,6 +1,6 @@ use crate::consts::constant_simple; use crate::macros::macro_backtrace; -use crate::source::{get_source_text, snippet_opt, walk_span_to_context, SpanRange}; +use crate::source::{snippet_opt, walk_span_to_context, SpanRange, SpanRangeExt}; use crate::tokenize_with_text; use rustc_ast::ast::InlineAsmTemplatePiece; use rustc_data_structures::fx::FxHasher; @@ -1173,9 +1173,9 @@ fn eq_span_tokens( pred: impl Fn(TokenKind) -> bool, ) -> bool { fn f(cx: &LateContext<'_>, left: Range, right: Range, pred: impl Fn(TokenKind) -> bool) -> bool { - if let Some(lsrc) = get_source_text(cx, left) + if let Some(lsrc) = left.get_source_text(cx) && let Some(lsrc) = lsrc.as_str() - && let Some(rsrc) = get_source_text(cx, right) + && let Some(rsrc) = right.get_source_text(cx) && let Some(rsrc) = rsrc.as_str() { let pred = |t: &(_, _)| pred(t.0); diff --git a/clippy_utils/src/source.rs b/clippy_utils/src/source.rs index 69b122cbfad..496c8f5b553 100644 --- a/clippy_utils/src/source.rs +++ b/clippy_utils/src/source.rs @@ -9,22 +9,17 @@ use rustc_lint::{LateContext, LintContext}; use rustc_session::Session; use rustc_span::source_map::{original_sp, SourceMap}; -use rustc_span::{hygiene, BytePos, Pos, SourceFile, SourceFileAndLine, Span, SpanData, SyntaxContext, DUMMY_SP}; +use rustc_span::{ + hygiene, BytePos, FileNameDisplayPreference, Pos, SourceFile, SourceFileAndLine, Span, SpanData, SyntaxContext, + DUMMY_SP, +}; use std::borrow::Cow; +use std::fmt; use std::ops::Range; -/// A type which can be converted to the range portion of a `Span`. +/// Conversion of a value into the range portion of a `Span`. pub trait SpanRange: Sized { fn into_range(self) -> Range; - fn set_span_pos(self, sp: Span) -> Span { - let range = self.into_range(); - SpanData { - lo: range.start, - hi: range.end, - ..sp.data() - } - .span() - } } impl SpanRange for Span { fn into_range(self) -> Range { @@ -43,6 +38,182 @@ fn into_range(self) -> Range { } } +/// Conversion of a value into a `Span` +pub trait IntoSpan: Sized { + fn into_span(self) -> Span; + fn with_ctxt(self, ctxt: SyntaxContext) -> Span; +} +impl IntoSpan for Span { + fn into_span(self) -> Span { + self + } + fn with_ctxt(self, ctxt: SyntaxContext) -> Span { + self.with_ctxt(ctxt) + } +} +impl IntoSpan for SpanData { + fn into_span(self) -> Span { + self.span() + } + fn with_ctxt(self, ctxt: SyntaxContext) -> Span { + Span::new(self.lo, self.hi, ctxt, self.parent) + } +} +impl IntoSpan for Range { + fn into_span(self) -> Span { + Span::with_root_ctxt(self.start, self.end) + } + fn with_ctxt(self, ctxt: SyntaxContext) -> Span { + Span::new(self.start, self.end, ctxt, None) + } +} + +pub trait SpanRangeExt: SpanRange { + /// Gets the source file, and range in the file, of the given span. Returns `None` if the span + /// extends through multiple files, or is malformed. + fn get_source_text(self, cx: &impl LintContext) -> Option { + get_source_text(cx.sess().source_map(), self.into_range()) + } + + /// Calls the given function with the source text referenced and returns the value. Returns + /// `None` if the source text cannot be retrieved. + fn with_source_text(self, cx: &impl LintContext, f: impl for<'a> FnOnce(&'a str) -> T) -> Option { + with_source_text(cx.sess().source_map(), self.into_range(), f) + } + + /// Checks if the referenced source text satisfies the given predicate. Returns `false` if the + /// source text cannot be retrieved. + fn check_source_text(self, cx: &impl LintContext, pred: impl for<'a> FnOnce(&'a str) -> bool) -> bool { + self.with_source_text(cx, pred).unwrap_or(false) + } + + /// Calls the given function with the both the text of the source file and the referenced range, + /// and returns the value. Returns `None` if the source text cannot be retrieved. + fn with_source_text_and_range( + self, + cx: &impl LintContext, + f: impl for<'a> FnOnce(&'a str, Range) -> T, + ) -> Option { + with_source_text_and_range(cx.sess().source_map(), self.into_range(), f) + } + + /// Calls the given function with the both the text of the source file and the referenced range, + /// and creates a new span with the returned range. Returns `None` if the source text cannot be + /// retrieved, or no result is returned. + /// + /// The new range must reside within the same source file. + fn map_range( + self, + cx: &impl LintContext, + f: impl for<'a> FnOnce(&'a str, Range) -> Option>, + ) -> Option> { + map_range(cx.sess().source_map(), self.into_range(), f) + } + + /// Extends the range to include all preceding whitespace characters. + fn with_leading_whitespace(self, cx: &impl LintContext) -> Range { + with_leading_whitespace(cx.sess().source_map(), self.into_range()) + } + + /// Trims the leading whitespace from the range. + fn trim_start(self, cx: &impl LintContext) -> Range { + trim_start(cx.sess().source_map(), self.into_range()) + } + + /// Writes the referenced source text to the given writer. Will return `Err` if the source text + /// could not be retrieved. + fn write_source_text_to(self, cx: &impl LintContext, dst: &mut impl fmt::Write) -> fmt::Result { + write_source_text_to(cx.sess().source_map(), self.into_range(), dst) + } + + /// Extracts the referenced source text as an owned string. + fn source_text_to_string(self, cx: &impl LintContext) -> Option { + self.with_source_text(cx, ToOwned::to_owned) + } +} +impl SpanRangeExt for T {} + +fn get_source_text(sm: &SourceMap, sp: Range) -> Option { + let start = sm.lookup_byte_offset(sp.start); + let end = sm.lookup_byte_offset(sp.end); + if !Lrc::ptr_eq(&start.sf, &end.sf) || start.pos > end.pos { + return None; + } + let range = start.pos.to_usize()..end.pos.to_usize(); + Some(SourceFileRange { sf: start.sf, range }) +} + +fn with_source_text(sm: &SourceMap, sp: Range, f: impl for<'a> FnOnce(&'a str) -> T) -> Option { + if let Some(src) = get_source_text(sm, sp) + && let Some(src) = src.as_str() + { + Some(f(src)) + } else { + None + } +} + +fn with_source_text_and_range( + sm: &SourceMap, + sp: Range, + f: impl for<'a> FnOnce(&'a str, Range) -> T, +) -> Option { + if let Some(src) = get_source_text(sm, sp) + && let Some(text) = &src.sf.src + { + Some(f(text, src.range)) + } else { + None + } +} + +#[expect(clippy::cast_possible_truncation)] +fn map_range( + sm: &SourceMap, + sp: Range, + f: impl for<'a> FnOnce(&'a str, Range) -> Option>, +) -> Option> { + if let Some(src) = get_source_text(sm, sp.clone()) + && let Some(text) = &src.sf.src + && let Some(range) = f(text, src.range.clone()) + { + debug_assert!( + range.start <= text.len() && range.end <= text.len(), + "Range `{range:?}` is outside the source file (file `{}`, length `{}`)", + src.sf.name.display(FileNameDisplayPreference::Local), + text.len(), + ); + debug_assert!(range.start <= range.end, "Range `{range:?}` has overlapping bounds"); + let dstart = (range.start as u32).wrapping_sub(src.range.start as u32); + let dend = (range.end as u32).wrapping_sub(src.range.start as u32); + Some(BytePos(sp.start.0.wrapping_add(dstart))..BytePos(sp.start.0.wrapping_add(dend))) + } else { + None + } +} + +fn with_leading_whitespace(sm: &SourceMap, sp: Range) -> Range { + map_range(sm, sp.clone(), |src, range| { + Some(src.get(..range.start)?.trim_end().len()..range.end) + }) + .unwrap_or(sp) +} + +fn trim_start(sm: &SourceMap, sp: Range) -> Range { + map_range(sm, sp.clone(), |src, range| { + let src = src.get(range.clone())?; + Some(range.start + (src.len() - src.trim_start().len())..range.end) + }) + .unwrap_or(sp) +} + +fn write_source_text_to(sm: &SourceMap, sp: Range, dst: &mut impl fmt::Write) -> fmt::Result { + match with_source_text(sm, sp, |src| dst.write_str(src)) { + Some(x) => x, + None => Err(fmt::Error), + } +} + pub struct SourceFileRange { pub sf: Lrc, pub range: Range, @@ -55,37 +226,6 @@ pub fn as_str(&self) -> Option<&str> { } } -/// Gets the source file, and range in the file, of the given span. Returns `None` if the span -/// extends through multiple files, or is malformed. -pub fn get_source_text(cx: &impl LintContext, sp: impl SpanRange) -> Option { - fn f(sm: &SourceMap, sp: Range) -> Option { - let start = sm.lookup_byte_offset(sp.start); - let end = sm.lookup_byte_offset(sp.end); - if !Lrc::ptr_eq(&start.sf, &end.sf) || start.pos > end.pos { - return None; - } - let range = start.pos.to_usize()..end.pos.to_usize(); - Some(SourceFileRange { sf: start.sf, range }) - } - f(cx.sess().source_map(), sp.into_range()) -} - -pub fn with_leading_whitespace(cx: &impl LintContext, sp: impl SpanRange) -> Range { - #[expect(clippy::needless_pass_by_value, clippy::cast_possible_truncation)] - fn f(src: SourceFileRange, sp: Range) -> Range { - let Some(text) = &src.sf.src else { - return sp; - }; - let len = src.range.start - text[..src.range.start].trim_end().len(); - BytePos(sp.start.0 - len as u32)..sp.end - } - let sp = sp.into_range(); - match get_source_text(cx, sp.clone()) { - Some(src) => f(src, sp), - None => sp, - } -} - /// Like `snippet_block`, but add braces if the expr is not an `ExprKind::Block`. pub fn expr_block( cx: &T,