diff --git a/clippy_lints/src/cognitive_complexity.rs b/clippy_lints/src/cognitive_complexity.rs index 81ff839408d3..5bce50433a56 100644 --- a/clippy_lints/src/cognitive_complexity.rs +++ b/clippy_lints/src/cognitive_complexity.rs @@ -1,7 +1,7 @@ use clippy_config::Conf; use clippy_utils::diagnostics::span_lint_and_help; use clippy_utils::res::MaybeDef as _; -use clippy_utils::source::{IntoSpan as _, SpanExt as _}; +use clippy_utils::source::{FileRangeExt as _, SpanExt as _, StrExt as _}; use clippy_utils::visitors::for_each_expr_without_closures; use clippy_utils::{LimitStack, get_async_fn_body, sym}; use core::ops::ControlFlow; @@ -107,12 +107,12 @@ impl CognitiveComplexity { let fn_span = match kind { FnKind::ItemFn(ident, _, _) | FnKind::Method(ident, _) => ident.span, FnKind::Closure => { - let header_span = body_span.with_hi(decl.output.span().lo()); - 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) + if let Some(sp) = body_span.map_range(cx, |scx, range| { + range + .shrink_end_to(scx, decl.output.span().lo_ctxt())? + .map_range_text(scx, |src| src.find_bounded_inclusive('|')) }) { - range.with_ctxt(header_span.ctxt()) + sp } else { return; } diff --git a/clippy_lints/src/collapsible_if.rs b/clippy_lints/src/collapsible_if.rs index cee21987ba3c..3e3b70afcbe3 100644 --- a/clippy_lints/src/collapsible_if.rs +++ b/clippy_lints/src/collapsible_if.rs @@ -1,8 +1,8 @@ use clippy_config::Conf; use clippy_utils::diagnostics::span_lint_hir_and_then; use clippy_utils::msrvs::Msrv; -use clippy_utils::source::{IntoSpan as _, SpanExt as _, snippet, snippet_block_with_applicability}; -use clippy_utils::{can_use_if_let_chains, span_contains_cfg, span_contains_non_whitespace, sym, tokenize_with_text}; +use clippy_utils::source::{FileRangeExt as _, SpanExt as _, StrExt as _, snippet, snippet_block_with_applicability}; +use clippy_utils::{can_use_if_let_chains, span_contains_non_whitespace, sym, text_contains_cfg, tokenize_with_text}; use rustc_ast::{BinOpKind, MetaItemInner}; use rustc_errors::Applicability; use rustc_hir::{Block, Expr, ExprKind, StmtKind}; @@ -99,6 +99,7 @@ impl CollapsibleIf { && !else_.span.from_expansion() && let ExprKind::If(else_if_cond, ..) = else_.kind && self.check_significant_tokens_and_expect_attrs(cx, else_block, else_, sym::collapsible_else_if) + && let Some([_, inner_if_span, _]) = peel_parens(cx, else_.span) { span_lint_hir_and_then( cx, @@ -112,15 +113,15 @@ impl CollapsibleIf { if self.lint_commented_code && let Some(else_keyword_span) = span_extract_keyword(cx, up_to_else, "else") && let Some(else_if_keyword_span) = span_extract_keyword(cx, else_before_if, "if") + && let Some(else_keyword_span) = + else_keyword_span.map_range(cx, |scx, range| range.with_leading_whitespace(scx)) + && let Some([else_open_bracket, else_closing_bracket]) = + else_block.span.map_split_range(cx, |scx, range| { + range + .map_split_range_text(scx, |src| src.get_prefix_suffix('{', '}'))? + .try_map(|r| r.with_leading_whitespace(scx)) + }) { - let else_keyword_span = else_keyword_span.with_leading_whitespace(cx).into_span(); - let else_open_bracket = else_block.span.split_at(1).0.with_leading_whitespace(cx).into_span(); - let else_closing_bracket = { - let end = else_block.span.shrink_to_hi(); - end.with_lo(end.lo() - BytePos(1)) - .with_leading_whitespace(cx) - .into_span() - }; let sugg = vec![ // Remove the outer else block `else` (else_keyword_span, String::new()), @@ -135,9 +136,6 @@ impl CollapsibleIf { return; } - // Peel off any parentheses. - let (_, else_block_span, _) = peel_parens(cx, else_.span); - // Prevent "elseif" // Check that the "else" is followed by whitespace // Note: We intentionally use char::is_whitespace instead of rustc_lexer::is_whitespace here to @@ -152,7 +150,7 @@ impl CollapsibleIf { if requires_space { " " } else { "" }, snippet_block_with_applicability( cx, - else_block_span, + inner_if_span, "..", Some(else_block.span), &mut applicability @@ -171,11 +169,16 @@ impl CollapsibleIf { && self.eligible_condition(cx, check_inner) && expr.span.eq_ctxt(inner.span) && self.check_significant_tokens_and_expect_attrs(cx, then, inner, sym::collapsible_if) - && let then_closing_bracket = { - let end = then.span.shrink_to_hi(); - end.with_lo(end.lo() - BytePos(1)) - } - && !span_contains_cfg(cx, inner.span.between(then_closing_bracket)) + && let Some([then_open_bracket, then_closing_bracket]) = then.span.map_split_range(cx, |scx, range| { + range + .map_split_range_text(scx, |src| src.get_prefix_suffix('{', '}')) + .filter(|[_, end]| { + scx.get_text(scx.span_to_file_range(inner.span).end..end.start) + .is_some_and(|x| !text_contains_cfg(x)) + })? + .try_map(|r| r.with_leading_whitespace(scx)) + }) + && let Some([paren_start, inner_if_span, paren_end]) = peel_parens(cx, inner.span) { span_lint_hir_and_then( cx, @@ -184,9 +187,6 @@ impl CollapsibleIf { expr.span, "this `if` statement can be collapsed", |diag| { - let then_open_bracket = then.span.split_at(1).0.with_leading_whitespace(cx).into_span(); - let then_closing_bracket = then_closing_bracket.with_leading_whitespace(cx).into_span(); - let (paren_start, inner_if_span, paren_end) = peel_parens(cx, inner.span); let inner_if = inner_if_span.split_at(2).0; let mut sugg = vec![ // Remove the outer then block `{` @@ -320,51 +320,29 @@ pub(super) fn parens_around(expr: &Expr<'_>) -> Vec<(Span, String)> { } fn span_extract_keyword(cx: &LateContext<'_>, span: Span, keyword: &str) -> Option { - span.with_source_text(cx, |snippet| { - tokenize_with_text(snippet) - .filter(|(t, s, _)| matches!(t, TokenKind::Ident if *s == keyword)) - .map(|(_, _, inner)| { - span.split_at(u32::try_from(inner.start).unwrap()) - .1 - .split_at(u32::try_from(inner.end - inner.start).unwrap()) - .0 - }) - .next() + span.map_range(cx, |scx, range| { + range.map_range_text(scx, |s| { + tokenize_with_text(s) + .find(|&(t, s, _)| matches!(t, TokenKind::Ident if s == keyword)) + .map(|(_, _, inner)| &s[inner.start..inner.end]) + }) }) - .flatten() } /// Peel the parentheses from an `if` expression, e.g. `((if true {} else {}))`. -pub(super) fn peel_parens(cx: &LateContext<'_>, mut span: Span) -> (Span, Span, Span) { - use crate::rustc_span::Pos as _; - - let start = span.shrink_to_lo(); - let end = span.shrink_to_hi(); - - span.with_source_text(cx, |snippet| { - if let Some((trim_start, _, trim_end)) = peel_parens_str(snippet) { - let mut data = span.data(); - data.lo = data.lo + BytePos::from_usize(trim_start); - data.hi = data.hi - BytePos::from_usize(trim_end); - span = data.span(); - } - }); - - (start.with_hi(span.lo()), span, end.with_lo(span.hi())) -} - -fn peel_parens_str(snippet: &str) -> Option<(usize, &str, usize)> { - let trimmed = snippet.trim(); - if !(trimmed.starts_with('(') && trimmed.ends_with(')')) { - return None; - } - - let trim_start = (snippet.len() - snippet.trim_start().len()) + 1; - let trim_end = (snippet.len() - snippet.trim_end().len()) + 1; - - let inner = snippet.get(trim_start..snippet.len() - trim_end)?; - Some(match peel_parens_str(inner) { - None => (trim_start, inner, trim_end), - Some((start, inner, end)) => (trim_start + start, inner, trim_end + end), +pub(super) fn peel_parens(cx: &LateContext<'_>, span: Span) -> Option<[Span; 3]> { + span.map_split_range(cx, |scx, range| { + range.map_split_range_text(scx, |s| { + let mut trimmed = s; + while let Some(s) = trimmed.strip_prefix('(') + && let Some(s) = s.strip_suffix(')') + { + trimmed = s.trim(); + } + let pos = trimmed.as_ptr().addr() - s.as_ptr().addr(); + let (pre, s) = s.split_at(pos); + let (mid, post) = s.split_at(trimmed.len()); + Some([pre, mid, post]) + }) }) } diff --git a/clippy_lints/src/double_parens.rs b/clippy_lints/src/double_parens.rs index 49a58711ea8e..f1ac759ebaf9 100644 --- a/clippy_lints/src/double_parens.rs +++ b/clippy_lints/src/double_parens.rs @@ -1,5 +1,5 @@ use clippy_utils::diagnostics::span_lint_and_sugg; -use clippy_utils::source::{SpanExt as _, snippet_with_applicability, snippet_with_context}; +use clippy_utils::source::{FileRangeExt as _, SpanExt as _, snippet_with_applicability, snippet_with_context}; use rustc_ast::ast::{Expr, ExprKind, MethodCall}; use rustc_errors::Applicability; use rustc_lint::{EarlyContext, EarlyLintPass, LintContext as _}; @@ -102,18 +102,20 @@ impl EarlyLintPass for DoubleParens { /// Check that the span does indeed look like `( (..) )` fn check_source(cx: &EarlyContext<'_>, inner: &Expr) -> bool { - if let Some(sfr) = inner.span.get_source_range(cx) - // this is the same as `SourceFileRange::as_str`, but doesn't apply the range right away, because - // we're interested in the source code outside it - && let Some(src) = sfr.sf.src.as_ref().map(|src| src.as_str()) - && let Some((start, outer_after_inner)) = src.split_at_checked(sfr.range.end) - && let Some((outer_before_inner, inner)) = start.split_at_checked(sfr.range.start) - && outer_before_inner.trim_end().ends_with('(') - && inner.starts_with('(') - && inner.ends_with(')') - && outer_after_inner.trim_start().starts_with(')') - // Don't lint macro repetition patterns like `($($result),*)` where parens are necessary - && !inner.trim_start_matches('(').trim_start().starts_with("$(") + if let Some((scx, range)) = inner.span.mk_edit_cx(cx) + && let Some(text) = scx.get_text(range.clone()) + && let Some(text) = text.strip_prefix('(') + && let Some(text) = text.strip_suffix(')') + // Don't lint pattern expansions. e.g. `(($($var)*))` may expand to `((foo))` or + // `((foo, bar))` depending on what `$var` expands to. + && !text + .trim_start() + .strip_prefix('$') + .is_some_and(|text| text.trim_start().starts_with('(')) + && let Some(range) = range.with_trailing_whitespace(&scx) + && let Some(range) = range.with_leading_whitespace(&scx) + && let Some(range) = range.with_trailing_match(&scx, ')') + && range.with_leading_match(&scx, '(').is_some() { true } else { diff --git a/clippy_lints/src/empty_enums.rs b/clippy_lints/src/empty_enums.rs index 2cac6597949d..ace679db9d54 100644 --- a/clippy_lints/src/empty_enums.rs +++ b/clippy_lints/src/empty_enums.rs @@ -63,6 +63,7 @@ impl LateLintPass<'_> for EmptyEnums { && def.variants.is_empty() // Only suggest the `never_type` if the feature is enabled && cx.tcx.features().never_type() + && !item.span.in_external_macro(cx.tcx.sess.source_map()) && !span_contains_cfg(cx, item.span) { span_lint_and_help( diff --git a/clippy_lints/src/empty_with_brackets.rs b/clippy_lints/src/empty_with_brackets.rs index b01745008510..66cddd567e46 100644 --- a/clippy_lints/src/empty_with_brackets.rs +++ b/clippy_lints/src/empty_with_brackets.rs @@ -1,7 +1,6 @@ use clippy_utils::attrs::span_contains_cfg; use clippy_utils::diagnostics::{span_lint_and_then, span_lint_hir_and_then}; -use clippy_utils::source::SpanExt; -use clippy_utils::span_contains_non_whitespace; +use clippy_utils::source::{FileRangeExt as _, SpanExt as _}; use rustc_data_structures::fx::{FxIndexMap, IndexEntry}; use rustc_errors::Applicability; use rustc_hir::def::DefKind::Ctor; @@ -12,7 +11,7 @@ use rustc_hir::{Expr, ExprKind, Item, ItemKind, Node, Pat, PatKind, Path, QPath, use rustc_lint::{LateContext, LateLintPass}; use rustc_middle::ty::{self, TyCtxt}; use rustc_session::impl_lint_pass; -use rustc_span::{BytePos, Span}; +use rustc_span::Span; declare_clippy_lint! { /// ### What it does @@ -190,13 +189,19 @@ impl LateLintPass<'_> for EmptyWithBrackets { }; // Span of the parentheses in variant definition - let span = variant.span.with_lo(variant.ident.span.hi()); - let span_inner = span - .with_lo(SpanExt::trim_start(span, cx).start + BytePos(1)) - .with_hi(span.hi() - BytePos(1)); - if span_contains_non_whitespace(cx, span_inner, false) { + let Some(span) = variant.span.map_range(cx, |scx, range| { + let range = range.shrink_start_to(scx, variant.ident.span.hi_ctxt())?; + matches!( + scx.get_text(range.clone())? + .trim_start() + .strip_prefix(['(', '{'])? + .trim_start(), + ")" | "}", + ) + .then_some(range) + }) else { continue; - } + }; span_lint_hir_and_then( cx, EMPTY_ENUM_VARIANTS_WITH_BRACKETS, diff --git a/clippy_lints/src/ifs/branches_sharing_code.rs b/clippy_lints/src/ifs/branches_sharing_code.rs index ff4683349ad8..54bc388d439d 100644 --- a/clippy_lints/src/ifs/branches_sharing_code.rs +++ b/clippy_lints/src/ifs/branches_sharing_code.rs @@ -1,6 +1,8 @@ use clippy_utils::diagnostics::span_lint_and_then; use clippy_utils::res::MaybeResPath as _; -use clippy_utils::source::{IntoSpan as _, SpanExt as _, first_line_of_span, indent_of, reindent_multiline, snippet}; +use clippy_utils::source::{ + FileRangeExt as _, SpanExt as _, first_line_of_span, indent_of, reindent_multiline, snippet, +}; use clippy_utils::ty::needs_ordered_drop; use clippy_utils::visitors::for_each_expr_without_closures; use clippy_utils::{ @@ -47,21 +49,18 @@ pub(super) fn check<'tcx>( let suggestion = reindent_multiline(&suggestion, true, cond_indent); (replace_span, suggestion) }); - let end_suggestion = res.end_span(last_block, sm).map(|span| { + let end_suggestion = res.end_span(last_block, sm).and_then(|span| { let moved_snipped = reindent_multiline(&snippet(cx, span, "_"), true, None); let indent = indent_of(cx, expr.span.shrink_to_hi()); let suggestion = "}\n".to_string() + &moved_snipped; let suggestion = reindent_multiline(&suggestion, true, indent); - let span = span.with_hi(last_block.span.hi()); - // Improve formatting if the inner block has indentation (i.e. normal Rust formatting) - 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.clone()) + span.map_range(cx, |scx, range| { + let range = range.extend_end_to(scx, last_block.span.hi_ctxt())?; + // Improve formatting if the inner block has indentation (i.e. normal Rust formatting) + Some(range.clone().with_leading_match(scx, " ").unwrap_or(range)) + }) + .map(|sp| (sp, suggestion)) }); let (span, msg, end_span) = match (&start_suggestion, &end_suggestion) { diff --git a/clippy_lints/src/implicit_hasher.rs b/clippy_lints/src/implicit_hasher.rs index 758e3114089e..302f04d2338b 100644 --- a/clippy_lints/src/implicit_hasher.rs +++ b/clippy_lints/src/implicit_hasher.rs @@ -13,7 +13,7 @@ use rustc_session::declare_lint_pass; use rustc_span::Span; use clippy_utils::diagnostics::span_lint_and_then; -use clippy_utils::source::{IntoSpan as _, SpanExt as _, snippet, snippet_with_context}; +use clippy_utils::source::{FileRangeExt as _, SpanExt as _, snippet, snippet_with_context}; use clippy_utils::sym; declare_clippy_lint! { @@ -118,16 +118,19 @@ impl<'tcx> LateLintPass<'tcx> for ImplicitHasher { return; } - let generics_suggestion_span = impl_.generics.span.substitute_dummy({ - 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()) + let generics_suggestion_span = if impl_.generics.span.is_dummy() { + if let Some(sp) = item.span.map_range(cx, |scx, range| { + range + .shrink_end_to(scx, target.span().lo_ctxt())? + .map_range_text(scx, |src| src.split_once("impl").map(|(_, x)| x)) + }) { + sp } else { return; } - }); + } else { + impl_.generics.span + }; let mut ctr_vis = ImplicitHasherConstructorVisitor::new(cx, target); for item in impl_.items.iter().map(|&item| cx.tcx.hir_impl_item(item)) { @@ -164,19 +167,24 @@ impl<'tcx> LateLintPass<'tcx> for ImplicitHasher { if generics.span.from_expansion() { continue; } - let generics_suggestion_span = generics.span.substitute_dummy({ - 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()) + + let generics_suggestion_span = if generics.span.is_dummy() { + if let Some(sp) = item.span.map_range(cx, |scx, range| { + range + .shrink_end_to(scx, body.params[0].pat.span.lo_ctxt())? + .map_range_text(scx, |src| { + src.split_once("fn") + .and_then(|(_, x)| x.split_once('(')) + .map(|(_, x)| x) + }) + }) { + sp } else { return; } - }); + } else { + generics.span + }; let mut ctr_vis = ImplicitHasherConstructorVisitor::new(cx, target); ctr_vis.visit_body(body); diff --git a/clippy_lints/src/ineffective_open_options.rs b/clippy_lints/src/ineffective_open_options.rs index 7c52b4b9e364..67d0cf8a42f3 100644 --- a/clippy_lints/src/ineffective_open_options.rs +++ b/clippy_lints/src/ineffective_open_options.rs @@ -1,6 +1,6 @@ use clippy_utils::diagnostics::span_lint_and_sugg; use clippy_utils::res::MaybeDef as _; -use clippy_utils::source::SpanExt as _; +use clippy_utils::source::{FileRangeExt as _, SpanExt as _}; use clippy_utils::{peel_blocks, peel_hir_expr_while, sym}; use rustc_ast::LitKind; use rustc_errors::Applicability; @@ -68,15 +68,10 @@ impl<'tcx> LateLintPass<'tcx> for IneffectiveOpenOptions { match name.ident.name { sym::append => append = true, sym::write - if let Some(range) = call_span.map_range(cx, |_, text, range| { - if text.get(..range.start)?.ends_with('.') { - Some(range.start - 1..range.end) - } else { - None - } - }) => + if let Some(sp) = + call_span.map_range(cx, |scx, range| range.with_leading_match(scx, '.')) => { - write = Some(call_span.with_lo(range.start)); + write = Some(sp); }, _ => {}, } diff --git a/clippy_lints/src/legacy_numeric_constants.rs b/clippy_lints/src/legacy_numeric_constants.rs index 0b7aaa707eec..0af143ee6873 100644 --- a/clippy_lints/src/legacy_numeric_constants.rs +++ b/clippy_lints/src/legacy_numeric_constants.rs @@ -121,8 +121,9 @@ impl<'tcx> LateLintPass<'tcx> for LegacyNumericConstants { && let QPath::TypeRelative(ty, last_segment) = qpath && let Some(def_id) = cx.qpath_res(qpath, func.hir_id).opt_def_id() && is_integer_method(cx, def_id) - && let Some(mod_name) = ty.span.get_text(cx) && ty.span.eq_ctxt(last_segment.ident.span) + && !ty.span.in_external_macro(cx.tcx.sess.source_map()) + && let Some(mod_name) = ty.span.get_text(cx) { let name = last_segment.ident.name.as_str()[..=2].to_ascii_uppercase(); (format!("{mod_name}::{name}"), "usage of a legacy numeric method") diff --git a/clippy_lints/src/let_with_type_underscore.rs b/clippy_lints/src/let_with_type_underscore.rs index 99d9b5d6011a..6ad2a0efc4ea 100644 --- a/clippy_lints/src/let_with_type_underscore.rs +++ b/clippy_lints/src/let_with_type_underscore.rs @@ -1,6 +1,6 @@ use clippy_utils::diagnostics::span_lint_and_then; use clippy_utils::is_from_proc_macro; -use clippy_utils::source::{IntoSpan as _, SpanExt as _}; +use clippy_utils::source::{FileRangeExt as _, SpanExt as _}; use rustc_ast::{Local, TyKind}; use rustc_errors::Applicability; use rustc_lint::{EarlyContext, EarlyLintPass, LintContext as _}; @@ -35,12 +35,12 @@ impl EarlyLintPass for UnderscoreTyped { && let sm = cx.sess().source_map() && !local.span.in_external_macro(sm) && !is_from_proc_macro(cx, &**ty) + && let Some(span_to_remove) = ty.span.map_range(cx, |scx, range| { + range.with_leading_whitespace(scx)? + .with_leading_match(scx, ':')? + .with_leading_whitespace(scx) + }) { - let span_to_remove = sm - .span_extend_to_prev_char_before(ty.span, ':', true) - .with_leading_whitespace(cx) - .into_span(); - span_lint_and_then( cx, LET_WITH_TYPE_UNDERSCORE, diff --git a/clippy_lints/src/lib.rs b/clippy_lints/src/lib.rs index 8ab462c05374..3b7e6d7bf028 100644 --- a/clippy_lints/src/lib.rs +++ b/clippy_lints/src/lib.rs @@ -1,3 +1,4 @@ +#![feature(array_try_map)] #![feature(control_flow_into_value)] #![feature(deref_patterns)] #![feature(exact_div)] diff --git a/clippy_lints/src/loops/unused_enumerate_index.rs b/clippy_lints/src/loops/unused_enumerate_index.rs index 3d46991d1c22..5938d951b9a0 100644 --- a/clippy_lints/src/loops/unused_enumerate_index.rs +++ b/clippy_lints/src/loops/unused_enumerate_index.rs @@ -1,12 +1,12 @@ use super::UNUSED_ENUMERATE_INDEX; use clippy_utils::diagnostics::span_lint_hir_and_then; use clippy_utils::res::MaybeDef as _; -use clippy_utils::source::{SpanExt as _, walk_span_to_context}; +use clippy_utils::source::{FileRangeExt as _, SpanExt as _}; use clippy_utils::{expr_or_init, pat_is_wild, sym}; use rustc_errors::Applicability; use rustc_hir::{Closure, Expr, ExprKind, Pat, PatKind, TyKind}; use rustc_lint::LateContext; -use rustc_span::{Span, SyntaxContext}; +use rustc_span::Span; pub(super) fn check<'tcx>( cx: &LateContext<'tcx>, @@ -26,13 +26,8 @@ pub(super) fn check<'tcx>( && !pat.span.from_expansion() && !idx_pat.span.from_expansion() && !inner_pat.span.from_expansion() - && let Some(enumerate_range) = enumerate_span.map_range(cx, |_, text, range| { - text.get(..range.start)? - .ends_with('.') - .then_some(range.start - 1..range.end) - }) + && let Some(enumerate_span) = enumerate_span.map_range(cx, |scx, range| range.with_leading_match(scx, '.')) { - let enumerate_span = Span::new(enumerate_range.start, enumerate_range.end, SyntaxContext::root(), None); span_lint_hir_and_then( cx, UNUSED_ENUMERATE_INDEX, @@ -73,10 +68,7 @@ pub(super) fn check_method<'tcx>( && !param.span.from_expansion() { let ty_spans = if let TyKind::Tup([_, inner]) = input.kind { - let Some(inner) = walk_span_to_context(inner.span, SyntaxContext::root()) else { - return; - }; - Some((input.span, inner)) + Some((input.span, inner.span.walk_to_root())) } else { None }; diff --git a/clippy_lints/src/matches/collapsible_match.rs b/clippy_lints/src/matches/collapsible_match.rs index 6510af5db854..f74d5277074a 100644 --- a/clippy_lints/src/matches/collapsible_match.rs +++ b/clippy_lints/src/matches/collapsible_match.rs @@ -2,7 +2,7 @@ use clippy_utils::diagnostics::span_lint_hir_and_then; use clippy_utils::higher::{If, IfLetOrMatch}; use clippy_utils::msrvs::Msrv; use clippy_utils::res::MaybeResPath as _; -use clippy_utils::source::{IntoSpan as _, SpanExt as _, snippet}; +use clippy_utils::source::{FileRangeExt as _, SpanExt as _, StrExt as _, snippet}; use clippy_utils::usage::mutated_variables; use clippy_utils::visitors::is_local_used; use clippy_utils::{ @@ -15,7 +15,7 @@ use rustc_hir_typeck::expr_use_visitor::{Delegate, ExprUseVisitor, PlaceBase, Pl use rustc_lint::LateContext; use rustc_middle::mir::FakeReadCause; use rustc_middle::ty; -use rustc_span::{BytePos, Ident, Span, SyntaxContext}; +use rustc_span::{Ident, Span, SyntaxContext}; use super::{COLLAPSIBLE_MATCH, pat_contains_disallowed_or}; use crate::collapsible_if::{parens_around, peel_parens}; @@ -150,6 +150,27 @@ fn check_arm<'tcx>( (Some(a), Some(b)) => SpanlessEq::new(cx).eq_expr(ctxt, a, b), } && !pat_bindings_moved_or_mutated(cx, outer_pat, inner.cond) + && let Some([paren_start, inner_if_span, paren_end]) = peel_parens(cx, inner_expr.span) + && let Some((scx, outer_then_range)) = outer_then_body.span.mk_edit_cx(cx) + && let Some(inner_if_range) = scx + .span_to_file_range(inner_if_span) + .map_range_text(&scx, |s| s.split_prefix("if").map(|[x, _]| x)) + && let outer_arrow_hi = outer_guard.map_or(outer_pat.span, |g| g.span).hi_ctxt() + && let Some((outer_then_start, outer_then_end)) = if matches!(outer_then_body.kind, ExprKind::Block(..)) { + outer_then_range + .clone() + .map_split_range_text(&scx, |s| s.get_prefix_suffix('{', '}')) + .and_then(|[start, end]| { + Some(( + start.extend_start_to(&scx, outer_arrow_hi)?, + Some(end.with_leading_whitespace(&scx)?), + )) + }) + } else { + (inner_if_range.start..inner_if_range.start) + .extend_start_to(&scx, outer_arrow_hi) + .map(|x| (x, None)) + } { span_lint_hir_and_then( cx, @@ -158,36 +179,18 @@ fn check_arm<'tcx>( inner_expr.span, "this `if` can be collapsed into the outer `match`", |diag| { - let outer_arrow_end = if let Some(outer_guard) = outer_guard { - outer_guard.span.shrink_to_hi() - } else { - outer_pat.span.shrink_to_hi() - }; - let (paren_start, inner_if_span, paren_end) = peel_parens(cx, inner_expr.span); - let inner_if = inner_if_span.split_at(2).0; let mut sugg = vec![(inner.then.span.shrink_to_lo(), "=> ".to_string())]; - if matches!(outer_then_body.kind, ExprKind::Block(..)) { - let outer_then_open_bracket = outer_then_body - .span - .split_at(1) - .0 - .with_leading_whitespace(cx) - .into_span(); - let outer_then_closing_bracket = { - let end = outer_then_body.span.shrink_to_hi(); - end.with_lo(end.lo() - BytePos(1)) - .with_leading_whitespace(cx) - .into_span() - }; - sugg.push((outer_arrow_end.to(outer_then_open_bracket), String::new())); - sugg.push((outer_then_closing_bracket, String::new())); + + if let Some(outer_then_end) = outer_then_end { + sugg.push((scx.mk_span(outer_then_start, None), String::new())); + sugg.push((scx.mk_span(outer_then_end, None), String::new())); } else { - sugg.push((outer_arrow_end.until(inner_if), " ".to_string())); + sugg.push((scx.mk_span(outer_then_start, None), String::from(" "))); } if let Some(outer_guard) = outer_guard { sugg.extend(parens_around(outer_guard)); - sugg.push((inner_if, "&&".to_string())); + sugg.push((scx.mk_span(inner_if_range, None), "&&".to_string())); } if !paren_start.is_empty() { diff --git a/clippy_lints/src/matches/manual_unwrap_or.rs b/clippy_lints/src/matches/manual_unwrap_or.rs index a5c61fafb514..b04f23db2d70 100644 --- a/clippy_lints/src/matches/manual_unwrap_or.rs +++ b/clippy_lints/src/matches/manual_unwrap_or.rs @@ -155,9 +155,13 @@ fn handle( ); } else if let Some(ty_name) = find_type_name(cx, cx.typeck_results().expr_ty(condition)) && cx.typeck_results().expr_adjustments(body_some).is_empty() - && let Some(or_body_snippet) = peel_blocks(body_none).span.get_text(cx) + && let ctxt = expr.span.ctxt() + && let body_none = peel_blocks(body_none) + && body_none.span.ctxt() == ctxt + && !ctxt.in_external_macro(cx.tcx.sess.source_map()) + && let Some(or_body_snippet) = body_none.span.get_text(cx) && let Some(indent) = indent_of(cx, expr.span) - && ConstEvalCtxt::new(cx).eval_local(body_none, expr.span.ctxt()).is_some() + && ConstEvalCtxt::new(cx).eval_local(body_none, ctxt).is_some() { let reindented_or_body = reindent_multiline(&or_body_snippet, true, Some(indent)); let mut app = Applicability::MachineApplicable; diff --git a/clippy_lints/src/matches/mod.rs b/clippy_lints/src/matches/mod.rs index 0d2eb1653fc8..a6865607f0f0 100644 --- a/clippy_lints/src/matches/mod.rs +++ b/clippy_lints/src/matches/mod.rs @@ -26,7 +26,7 @@ mod wild_in_or_pats; use clippy_config::Conf; use clippy_utils::msrvs::{self, Msrv}; -use clippy_utils::source::SpanExt as _; +use clippy_utils::source::{FileRangeExt as _, SpanExt as _}; use clippy_utils::{ higher, is_direct_expn_of, is_in_const_context, is_lint_allowed, is_span_match, sym, tokenize_with_text, }; @@ -1248,23 +1248,19 @@ fn walk_intra_arm_text( arms: &[Arm<'_>], mut f: impl FnMut(&str), ) -> bool { - if let Some(src) = match_sp.get_source_range(cx) - && let scrutinee_sp = scrutinee_sp.source_callsite().data() - && let block_start = (scrutinee_sp.hi.0 - src.sf.start_pos.0) as usize - && let Some(src_text) = src.sf.src.as_ref().map(|x| &***x) - && let Some(block_text) = src_text.get(block_start..src.range.end) - && let Some(stripped_text) = block_text.trim_start_matches(is_whitespace).strip_prefix('{') - && let arms_start = block_start + (block_text.len() - stripped_text.len()) - && let Some(arms_end) = stripped_text - .trim_end_matches(|c| is_whitespace(c) || c == ')') - .strip_suffix('}') - .map(|s| src.range.end - (stripped_text.len() - s.len())) - && let Some(range) = arms.iter().try_fold(arms_start..arms_end, |range, arm| { - let arm_sp: rustc_span::SpanData = arm.span.source_callsite().data(); - let arm_range = (arm_sp.lo.0 - src.sf.start_pos.0) as usize..(arm_sp.hi.0 - src.sf.start_pos.0) as usize; + if let Some((scx, range)) = match_sp.mk_edit_cx(cx) + && let Some(range) = range.shrink_start_to(&scx, scrutinee_sp.walk_to_root().hi_ctxt()) + && let Some(range) = range.map_range_text(&scx, |s| { + s.trim_start() + .strip_prefix('{')? + .trim_end_matches(|c| is_whitespace(c) || c == ')') + .strip_suffix('}') + }) + && let Some(range) = arms.iter().try_fold(range, |range, arm| { + let arm_range = scx.span_to_file_range(arm.span.walk_to_root()); if range.start <= arm_range.start && arm_range.end <= range.end - && let Some(src) = src_text.get(range.start..arm_range.start) + && let Some(src) = scx.get_text(range.start..arm_range.start) { f(src); Some(arm_range.end..range.end) @@ -1272,7 +1268,7 @@ fn walk_intra_arm_text( None } }) - && let Some(src) = src_text.get(range) + && let Some(src) = scx.get_text(range) { f(src); true diff --git a/clippy_lints/src/methods/manual_inspect.rs b/clippy_lints/src/methods/manual_inspect.rs index aff6477c3254..da4a126c1102 100644 --- a/clippy_lints/src/methods/manual_inspect.rs +++ b/clippy_lints/src/methods/manual_inspect.rs @@ -1,7 +1,7 @@ use clippy_utils::diagnostics::span_lint_and_then; use clippy_utils::msrvs::{self, Msrv}; use clippy_utils::res::{MaybeDef as _, MaybeResPath as _}; -use clippy_utils::source::{IntoSpan as _, SpanExt as _}; +use clippy_utils::source::{FileRangeExt as _, SpanExt as _, StrExt as _}; use clippy_utils::ty::get_field_by_name; use clippy_utils::visitors::{for_each_expr, for_each_expr_without_closures}; use clippy_utils::{ExprUseNode, get_expr_use_site, sym}; @@ -100,18 +100,22 @@ 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((s.with_leading_whitespace(cx).with_ctxt(s.ctxt()), String::new())), + UseKind::Return(s) => { + if let Some(sp) = s.map_range(cx, |scx, range| range.with_leading_whitespace(scx)) { + edits.push((sp, String::new())); + } else { + return; + } + }, UseKind::Borrowed(s) => { - 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(sp) = s.map_range(cx, |scx, range| { + range.map_range_text(scx, |src| { + src.trim_start_matches([' ', '\t', '\n', '\r', '(']) + .split_prefix('&') + .map(|[x, _]| x) }) - }); - if let Some(range) = range { - addr_of_edits.push((range.with_ctxt(s.ctxt()), String::new())); + }) { + addr_of_edits.push((sp, String::new())); } else { requires_copy = true; requires_deref = true; @@ -162,6 +166,9 @@ pub(crate) fn check(cx: &LateContext<'_>, expr: &Expr<'_>, arg: &Expr<'_>, name: && (!requires_copy || cx.type_is_copy_modulo_regions(arg_ty)) // This case could be handled, but a fair bit of care would need to be taken. && (!requires_deref || arg_ty.is_freeze(cx.tcx, cx.typing_env())) + && let Some(final_expr_span) = final_expr + .span + .map_range(cx, |scx, range| range.with_leading_whitespace(scx)) { if requires_deref { edits.push((param.span.shrink_to_lo(), "&".into())); @@ -174,13 +181,7 @@ pub(crate) fn check(cx: &LateContext<'_>, expr: &Expr<'_>, arg: &Expr<'_>, name: _ => return, }; edits.push((name_span, edit.to_string())); - edits.push(( - final_expr - .span - .with_leading_whitespace(cx) - .with_ctxt(final_expr.span.ctxt()), - String::new(), - )); + edits.push((final_expr_span, String::new())); let app = if edits.iter().any(|(s, _)| s.from_expansion()) { Applicability::MaybeIncorrect } else { diff --git a/clippy_lints/src/methods/manual_ok_or.rs b/clippy_lints/src/methods/manual_ok_or.rs index b010f7a8996f..606a02a1b5fd 100644 --- a/clippy_lints/src/methods/manual_ok_or.rs +++ b/clippy_lints/src/methods/manual_ok_or.rs @@ -28,7 +28,7 @@ pub(super) fn check<'tcx>( && err_path.res(cx).ctor_parent(cx).is_lang_item(cx, ResultErr) && is_ok_wrapping(cx, map_expr) && let Some(recv_snippet) = recv.span.get_text(cx) - && let Some(err_arg_snippet) = err_arg.span.get_text(cx) + && let Some(err_arg_snippet) = err_arg.span.get_text_within_other(cx, &expr.span.data()) && let Some(indent) = indent_of(cx, expr.span) { let reindented_err_arg_snippet = reindent_multiline(err_arg_snippet.as_str(), true, Some(indent + 4)); diff --git a/clippy_lints/src/multiple_bound_locations.rs b/clippy_lints/src/multiple_bound_locations.rs index 2f0e404ad1a3..ebddebe2659b 100644 --- a/clippy_lints/src/multiple_bound_locations.rs +++ b/clippy_lints/src/multiple_bound_locations.rs @@ -1,7 +1,7 @@ use rustc_ast::visit::FnKind; use rustc_ast::{Fn, NodeId, WherePredicateKind}; use rustc_data_structures::fx::FxHashMap; -use rustc_lint::{EarlyContext, EarlyLintPass}; +use rustc_lint::{EarlyContext, EarlyLintPass, LintContext as _}; use rustc_session::declare_lint_pass; use rustc_span::Span; @@ -38,15 +38,17 @@ declare_clippy_lint! { declare_lint_pass!(MultipleBoundLocations => [MULTIPLE_BOUND_LOCATIONS]); impl EarlyLintPass for MultipleBoundLocations { - fn check_fn(&mut self, cx: &EarlyContext<'_>, kind: FnKind<'_>, _: Span, _: NodeId) { + fn check_fn(&mut self, cx: &EarlyContext<'_>, kind: FnKind<'_>, sp: Span, _: NodeId) { if let FnKind::Fn(_, _, Fn { generics, .. }) = kind && !generics.params.is_empty() && !generics.where_clause.predicates.is_empty() + && let ctxt = sp.ctxt() + && !ctxt.in_external_macro(cx.sess().source_map()) { let mut generic_params_with_bounds = FxHashMap::default(); for param in &generics.params { - if !param.bounds.is_empty() { + if !param.bounds.is_empty() && param.ident.span.ctxt() == ctxt { generic_params_with_bounds.insert(param.ident.as_str(), param.ident.span); } } @@ -54,10 +56,8 @@ impl EarlyLintPass for MultipleBoundLocations { match &clause.kind { WherePredicateKind::BoundPredicate(pred) => { if (!pred.bound_generic_params.is_empty() || !pred.bounds.is_empty()) - && let Some(Some(bound_span)) = pred - .bounded_ty - .span - .with_source_text(cx, |src| generic_params_with_bounds.get(src)) + && let Some(src) = pred.bounded_ty.span.get_text(cx) + && let Some(bound_span) = generic_params_with_bounds.get(&*src) { emit_lint(cx, *bound_span, pred.bounded_ty.span); } diff --git a/clippy_lints/src/mutex_atomic.rs b/clippy_lints/src/mutex_atomic.rs index fb9b60c25f78..e226baab4af3 100644 --- a/clippy_lints/src/mutex_atomic.rs +++ b/clippy_lints/src/mutex_atomic.rs @@ -1,11 +1,11 @@ use clippy_utils::diagnostics::span_lint_and_then; use clippy_utils::res::MaybeDef as _; -use clippy_utils::source::{IntoSpan as _, SpanExt as _}; +use clippy_utils::source::{FileRangeExt as _, SpanExt as _}; use clippy_utils::sugg::Sugg; use clippy_utils::ty::ty_from_hir_ty; use rustc_errors::{Applicability, Diag}; use rustc_hir::{self as hir, Expr, ExprKind, Item, ItemKind, LetStmt, QPath}; -use rustc_lint::{LateContext, LateLintPass, LintContext as _}; +use rustc_lint::{LateContext, LateLintPass}; use rustc_middle::mir::Mutability; use rustc_middle::ty::{self, IntTy, Ty, UintTy}; use rustc_session::declare_lint_pass; @@ -150,13 +150,16 @@ fn check_expr<'tcx>(cx: &LateContext<'tcx>, expr: &Expr<'tcx>, ty_ascription: &T suggs.push((ty_ascription.span, format!("std::sync::atomic::{atomic_name}"))); }, TypeAscriptionKind::Optional(Some(ty_ascription)) => { - // See https://github.com/rust-lang/rust-clippy/pull/15386 for why this is - // required - let colon_ascription = (cx.sess().source_map()) - .span_extend_to_prev_char_before(ty_ascription.span, ':', true) - .with_leading_whitespace(cx) - .into_span(); - suggs.push((colon_ascription, String::new())); + if let Some(sp) = ty_ascription.span.map_range(cx, |scx, range| { + range + .with_leading_whitespace(scx)? + .with_leading_match(scx, ':')? + .with_leading_whitespace(scx) + }) { + suggs.push((sp, String::new())); + } else { + return; + } }, TypeAscriptionKind::Optional(None) => {}, // nothing to remove/replace } diff --git a/clippy_lints/src/needless_else.rs b/clippy_lints/src/needless_else.rs index 69975618d3d8..adc6d82d72e9 100644 --- a/clippy_lints/src/needless_else.rs +++ b/clippy_lints/src/needless_else.rs @@ -1,5 +1,5 @@ use clippy_utils::diagnostics::span_lint_and_sugg; -use clippy_utils::source::{IntoSpan as _, SpanExt as _}; +use clippy_utils::source::{FileRangeExt as _, SpanExt as _}; use rustc_ast::ast::{Expr, ExprKind}; use rustc_errors::Applicability; use rustc_lint::{EarlyContext, EarlyLintPass}; @@ -39,19 +39,26 @@ impl EarlyLintPass for NeedlessElse { fn check_expr(&mut self, cx: &EarlyContext<'_>, expr: &Expr) { if let ExprKind::If(_, then_block, Some(else_clause)) = &expr.kind && let ExprKind::Block(block, _) = &else_clause.kind + && !then_block.span.from_expansion() && !expr.span.from_expansion() && !else_clause.span.from_expansion() && block.stmts.is_empty() - && let range = (then_block.span.hi()..expr.span.hi()).trim_start(cx) - && range.clone().check_text(cx, |src| { - // Ignore else blocks that contain comments or #[cfg]s - !src.contains(['/', '#']) + // Only take the span of `else { .. }` if no comments/cfgs/macros exist. + && let Some(lint_sp) = else_clause.span.map_range(cx, |scx, range| { + range.extend_start_to(scx, then_block.span.hi_ctxt())? + .map_range_text(scx, |src| { + let src = src.trim_start(); + (src.strip_prefix("else")? + .trim_start() + .strip_prefix('{')? + .trim_start() == "}").then_some(src) + }) }) { span_lint_and_sugg( cx, NEEDLESS_ELSE, - range.with_ctxt(expr.span.ctxt()), + lint_sp, "this `else` branch is empty", "you can remove it", String::new(), diff --git a/clippy_lints/src/ranges.rs b/clippy_lints/src/ranges.rs index baa15ce720f6..c52a814aa4a3 100644 --- a/clippy_lints/src/ranges.rs +++ b/clippy_lints/src/ranges.rs @@ -3,7 +3,7 @@ use clippy_utils::consts::{ConstEvalCtxt, Constant}; use clippy_utils::diagnostics::{span_lint, span_lint_and_sugg, span_lint_and_then}; use clippy_utils::msrvs::{self, Msrv}; use clippy_utils::res::MaybeResPath as _; -use clippy_utils::source::{SpanExt as _, snippet, snippet_with_applicability}; +use clippy_utils::source::{FileRangeExt as _, SpanExt as _, snippet, snippet_with_applicability}; use clippy_utils::sugg::Sugg; use clippy_utils::ty::implements_trait; use clippy_utils::{ @@ -197,8 +197,12 @@ impl<'tcx> LateLintPass<'tcx> for Ranges { ) && self.msrv.meets(cx, msrvs::RANGE_CONTAINS) && !is_in_const_context(cx) + && let ctxt = expr.span.ctxt() + && l.span.ctxt() == ctxt + && r.span.ctxt() == ctxt + && !ctxt.in_external_macro(cx.tcx.sess.source_map()) { - check_possible_range_contains(cx, op.node, l, r, expr, expr.span); + check_possible_range_contains(cx, op.node, l, r, expr.span, expr.span.ctxt()); } if let Some(range) = higher::Range::hir(cx, expr) { @@ -214,8 +218,8 @@ fn check_possible_range_contains( op: BinOpKind, left: &Expr<'_>, right: &Expr<'_>, - expr: &Expr<'_>, span: Span, + ctxt: SyntaxContext, ) { let combine_and = match op { BinOpKind::And | BinOpKind::BitAnd => true, @@ -302,13 +306,16 @@ fn check_possible_range_contains( // the same operator precedence 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()) - && new_span.check_text(cx, |src| { - // Do not continue if we have mismatched number of parens, otherwise the suggestion is wrong - src.matches('(').count() == src.matches(')').count() + && let new_lhs_data = new_lhs.span.data() + && new_lhs_data.ctxt == ctxt + && let Some(new_sp) = new_lhs_data.map_range(cx, |scx, range| { + range.extend_end_to(scx, right.span.hi_ctxt()).filter(|range| { + scx.get_text(range.clone()) + .is_some_and(|src| src.matches('(').count() == src.matches(')').count()) + }) }) { - check_possible_range_contains(cx, op, new_lhs, right, expr, new_span); + check_possible_range_contains(cx, op, new_lhs, right, new_sp, ctxt); } } @@ -544,7 +551,7 @@ fn check_range_switch<'tcx>( .to_string() }); let end = Sugg::hir_with_context(cx, y, span.ctxt(), "", &mut app).maybe_paren(); - match span.with_source_text(cx, |src| src.starts_with('(') && src.ends_with(')')) { + match span.get_text(cx).map(|src| src.starts_with('(') && src.ends_with(')')) { Some(true) => { diag.span_suggestion(span, "use", format!("({start}{operator}{end})"), app); }, diff --git a/clippy_lints/src/returns/needless_return.rs b/clippy_lints/src/returns/needless_return.rs index ad81b5efcfe1..7f4d7decdea4 100644 --- a/clippy_lints/src/returns/needless_return.rs +++ b/clippy_lints/src/returns/needless_return.rs @@ -10,7 +10,7 @@ use rustc_hir::intravisit::FnKind; use rustc_hir::{Body, Expr, ExprKind, HirId, LangItem, MatchSource, StmtKind}; use rustc_lint::{LateContext, Level, LintContext as _}; use rustc_middle::ty::{self, Ty}; -use rustc_span::{BytePos, Pos as _, Span}; +use rustc_span::{BytePos, DesugaringKind, ExpnKind, Pos as _, Span}; use std::borrow::Cow; use std::fmt::Display; @@ -59,6 +59,13 @@ pub(super) fn check_fn<'tcx>(cx: &LateContext<'tcx>, kind: FnKind<'tcx>, body: & if sp.from_expansion() { return; } + // Hackish workaround for async fn. + if body.value.span.from_expansion() + && let expn = body.value.span.ctxt().outer_expn_data() + && (!matches!(expn.kind, ExpnKind::Desugaring(DesugaringKind::Async)) || expn.call_site.from_expansion()) + { + return; + } match kind { FnKind::Closure => { @@ -82,7 +89,9 @@ fn check_block_return<'tcx>(cx: &LateContext<'tcx>, expr_kind: &ExprKind<'tcx>, if let ExprKind::Block(block, _) = expr_kind { if let Some(block_expr) = block.expr { check_final_expr(cx, block_expr, semi_spans, RetReplacement::Empty, None); - } else if let Some(stmt) = block.stmts.last() { + } else if let Some(stmt) = block.stmts.last() + && !stmt.span.from_expansion() + { if span_contains_cfg( cx, Span::between( diff --git a/clippy_lints/src/transmute/missing_transmute_annotations.rs b/clippy_lints/src/transmute/missing_transmute_annotations.rs index ddcc577cbd19..d6c8fa89e578 100644 --- a/clippy_lints/src/transmute/missing_transmute_annotations.rs +++ b/clippy_lints/src/transmute/missing_transmute_annotations.rs @@ -1,12 +1,11 @@ -use std::borrow::Cow; - use clippy_utils::diagnostics::span_lint_and_then; use clippy_utils::source::SpanExt as _; +use core::fmt; +use rustc_data_structures::either::Either; use rustc_errors::Applicability; use rustc_hir::{Expr, GenericArg, HirId, LetStmt, Node, Path, TyKind}; use rustc_lint::LateContext; use rustc_middle::ty::{self, Ty}; -use rustc_span::Span; use crate::transmute::MISSING_TRANSMUTE_ANNOTATIONS; @@ -84,9 +83,18 @@ pub(super) fn check<'tcx>( let to_ty_no_name = ty_cannot_be_named(to_ty); if from_ty_no_name || to_ty_no_name { let to_name = match (from_ty_no_name, to_ty_no_name) { - (true, false) => maybe_name_by_expr(cx, arg.span, "the origin type"), - (false, true) => "the destination type".into(), - _ => "the source and destination types".into(), + (true, false) => { + let data = arg.span.data(); + if data.hi.0 - data.lo.0 < 6 + && let Some(src) = data.get_text(cx) + { + Either::Left(fmt::from_fn(move |f| write!(f, "`{src}`'s type"))) + } else { + Either::Right("the origin type") + } + }, + (false, true) => Either::Right("the destination type"), + _ => Either::Right("the source and destination types"), }; diag.help(format!( "consider giving {to_name} a name, and adding missing type annotations" @@ -116,11 +124,3 @@ fn ty_cannot_be_named(ty: Ty<'_>) -> bool { ) ) } - -fn maybe_name_by_expr<'a>(cx: &LateContext<'_>, span: Span, default: &'a str) -> Cow<'a, str> { - span.with_source_text(cx, |name| { - (name.len() + 9 < default.len()).then_some(format!("`{name}`'s type").into()) - }) - .flatten() - .unwrap_or(default.into()) -} diff --git a/clippy_lints/src/unnecessary_mut_passed.rs b/clippy_lints/src/unnecessary_mut_passed.rs index c75801583cd3..39ded5e3f9ce 100644 --- a/clippy_lints/src/unnecessary_mut_passed.rs +++ b/clippy_lints/src/unnecessary_mut_passed.rs @@ -1,5 +1,5 @@ use clippy_utils::diagnostics::span_lint_and_then; -use clippy_utils::source::SpanExt as _; +use clippy_utils::source::{FileRangeExt as _, SpanExt as _}; use rustc_errors::Applicability; use rustc_hir::{BorrowKind, Expr, ExprKind, Mutability, intravisit}; use rustc_hir_pretty::PpAnn; @@ -100,34 +100,32 @@ fn check_arguments<'tcx>( let parameters = type_definition.fn_sig(cx.tcx).skip_binder().inputs(); for (argument, parameter) in iter::zip(arguments, parameters) { if let ty::Ref(_, _, Mutability::Not) | ty::RawPtr(_, Mutability::Not) = parameter.kind() - && let ExprKind::AddrOf(BorrowKind::Ref, Mutability::Mut, arg) = argument.kind + && let ExprKind::AddrOf(BorrowKind::Ref, Mutability::Mut, _) = argument.kind + && let Some(mut_span) = argument.span.map_range(cx, |scx, range| { + range.map_range_text(scx, |src| { + src.trim_start_matches(|c: char| c.is_whitespace() || c == '(') + .strip_prefix('&') + .and_then(|s| { + // Get just the `mut` and surrounding whitespace. + s.trim_start() + .strip_prefix("mut") + .map(|x| &s[..s.len() - x.trim_start().len()]) + }) + }) + }) { - let applicability = Applicability::MachineApplicable; - - let span_to_remove = { - let span_until_arg = argument.span.until(arg.span); - if let Some(Some(ref_pos)) = span_until_arg.with_source_text(cx, |src| { - src - // we don't use `strip_prefix` here, because `argument` might be enclosed in parens, in - // which case `&` is no longer the prefix - .find('&') - // just a sanity check, in case some proc-macro messes up the spans - .filter(|ref_pos| src[*ref_pos..].contains("mut")) - }) && let Ok(lo) = u32::try_from(ref_pos + '&'.len_utf8()) - { - span_until_arg.split_at(lo).1 - } else { - return; - } - }; - span_lint_and_then( cx, UNNECESSARY_MUT_PASSED, argument.span, format!("the {fn_kind} `{}` doesn't need a mutable reference", name()), |diag| { - diag.span_suggestion_verbose(span_to_remove, "remove this `mut`", String::new(), applicability); + diag.span_suggestion_verbose( + mut_span, + "remove this `mut`", + String::new(), + Applicability::MachineApplicable, + ); }, ); } diff --git a/clippy_lints/src/unused_unit.rs b/clippy_lints/src/unused_unit.rs index c84a24a28e93..1f284dba2192 100644 --- a/clippy_lints/src/unused_unit.rs +++ b/clippy_lints/src/unused_unit.rs @@ -1,5 +1,5 @@ use clippy_utils::diagnostics::span_lint_and_sugg; -use clippy_utils::source::{SpanExt as _, position_before_rarrow}; +use clippy_utils::source::{FileRangeExt as _, SpanExt as _}; use clippy_utils::{is_never_expr, is_unit_expr}; use rustc_ast::{Block, StmtKind}; use rustc_errors::Applicability; @@ -12,7 +12,7 @@ use rustc_hir::{ use rustc_lint::{EarlyContext, EarlyLintPass, LateContext, LateLintPass}; use rustc_session::declare_lint_pass; use rustc_span::edition::Edition; -use rustc_span::{BytePos, Pos as _, Span, sym}; +use rustc_span::{Span, sym}; declare_clippy_lint! { /// ### What it does @@ -78,7 +78,7 @@ impl<'tcx> LateLintPass<'tcx> for UnusedUnit { return; } - lint_unneeded_unit_return(cx, hir_ty.span, span); + lint_unneeded_unit_return(cx, hir_ty.span); } } @@ -112,7 +112,7 @@ impl<'tcx> LateLintPass<'tcx> for UnusedUnit { && args.span_ext.hi() != hir_ty.span.hi() && is_unit_ty(hir_ty) { - lint_unneeded_unit_return(cx, hir_ty.span, poly.span); + lint_unneeded_unit_return(cx, hir_ty.span); } } } @@ -158,24 +158,21 @@ fn get_def(span: Span) -> Option { } } -fn lint_unneeded_unit_return(cx: &LateContext<'_>, ty_span: Span, span: Span) { - let (ret_span, appl) = - if let Some(Some(rpos)) = span.with_hi(ty_span.hi()).with_source_text(cx, position_before_rarrow) { - ( - ty_span.with_lo(span.lo() + BytePos::from_usize(rpos)), - Applicability::MachineApplicable, - ) - } else { - (ty_span, Applicability::MaybeIncorrect) - }; - - span_lint_and_sugg( - cx, - UNUSED_UNIT, - ret_span, - "unneeded unit return type", - "remove the `-> ()`", - String::new(), - appl, - ); +fn lint_unneeded_unit_return(cx: &LateContext<'_>, ty_span: Span) { + if let Some(sp) = ty_span.map_range(cx, |scx, range| { + range + .with_leading_whitespace(scx)? + .with_leading_match(scx, "->")? + .with_leading_whitespace(scx) + }) { + span_lint_and_sugg( + cx, + UNUSED_UNIT, + sp, + "unneeded unit return type", + "remove the `-> ()`", + String::new(), + Applicability::MachineApplicable, + ); + } } diff --git a/clippy_lints/src/utils/format_args_collector.rs b/clippy_lints/src/utils/format_args_collector.rs index 3cc06f40c236..0679d16817d0 100644 --- a/clippy_lints/src/utils/format_args_collector.rs +++ b/clippy_lints/src/utils/format_args_collector.rs @@ -1,5 +1,5 @@ use clippy_utils::macros::FormatArgsStorage; -use clippy_utils::source::{SpanExt as _, walk_span_to_context}; +use clippy_utils::source::SpanExt as _; use rustc_ast::{Crate, Expr, ExprKind, FormatArgs}; use rustc_data_structures::fx::FxHashMap; use rustc_lexer::{FrontmatterAllowed, TokenKind, tokenize}; @@ -43,7 +43,6 @@ impl EarlyLintPass for FormatArgsCollector { self.storage.set(mem::take(&mut self.format_args)); } } - impl FormatArgsCollector { /// Detects if the format string or an argument has its span set by a proc macro to something /// inside a macro callsite, e.g. @@ -68,8 +67,8 @@ impl FormatArgsCollector { let mut fmt_sp = fmt_sp.data(); // Find the first macro call that contains the format string. - let arg_sp = if let Some(arg_sp) = walk_span_to_context(args.span, fmt_sp.ctxt) { - arg_sp.data() + let arg_sp = if let Some(arg_sp) = args.span.walk_into_other(&fmt_sp) { + arg_sp } else { // Try to find a common parent for the format call and the format string. self.parent_spans.clear(); @@ -98,10 +97,7 @@ impl FormatArgsCollector { if fmt_sp.ctxt.in_external_macro(sm) { return true; } - let Some(src) = arg_sp.get_source_range(sm) else { - return true; - }; - let Some(src_text) = src.sf.src.as_ref().map(|x| &***x) else { + let Some((scx, arg_range)) = arg_sp.mk_edit_cx(sm) else { return true; }; @@ -109,11 +105,9 @@ impl FormatArgsCollector { args.arguments .explicit_args() .iter() - .try_fold(src.range.end, |start, arg| { - let expr_sp = walk_span_to_context(arg.expr.span, fmt_sp.ctxt)?.data(); - let expr_start = (expr_sp.lo.0 - src.sf.start_pos.0) as usize; - let expr_end = (expr_sp.hi.0 - src.sf.start_pos.0) as usize; - let mut tks = tokenize(src_text.get(start..expr_start)?, FrontmatterAllowed::No) + .try_fold(arg_range.end, |start, arg| { + let range = scx.span_to_file_range(arg.expr.span.walk_into_other(&fmt_sp)?); + let mut tks = tokenize(scx.get_text(start..range.start)?, FrontmatterAllowed::No) .map(|x| x.kind) .filter(|x| { !matches!( @@ -135,7 +129,7 @@ impl FormatArgsCollector { None => true, } && tks.next().is_none(); - matches.then_some(expr_end) + matches.then_some(range.end) }) .is_none() } diff --git a/clippy_utils/src/attrs.rs b/clippy_utils/src/attrs.rs index 9feb723e7a44..6c1d440e5e62 100644 --- a/clippy_utils/src/attrs.rs +++ b/clippy_utils/src/attrs.rs @@ -103,32 +103,35 @@ pub fn has_non_exhaustive_attr(tcx: TyCtxt<'_>, adt: AdtDef<'_>) -> bool { .any(|field_def| find_attr!(tcx, field_def.did, NonExhaustive(..))) } -/// Checks whether the given span contains a `#[cfg(..)]` attribute +/// Checks whether the given span contains a `#[cfg(..)]` attribute. pub fn span_contains_cfg(cx: &LateContext<'_>, s: Span) -> bool { - s.check_text(cx, |src| { - // PERF: A `#[cfg]` needs a literal `#`, so skip the lexer when the source has none. - if !src.contains('#') { - return false; - } + s.check_text(cx, text_contains_cfg) +} - let mut iter = tokenize_with_text(src); - - // Search for the token sequence [`#`, `[`, `cfg`] - while iter.any(|(t, ..)| matches!(t, TokenKind::Pound)) { - let mut iter = iter.by_ref().skip_while(|(t, ..)| { - matches!( - t, - TokenKind::Whitespace | TokenKind::LineComment { .. } | TokenKind::BlockComment { .. } - ) - }); - if matches!(iter.next(), Some((TokenKind::OpenBracket, ..))) - && matches!(iter.next(), Some((TokenKind::Ident, "cfg", _))) - { - return true; - } +/// Checks whether the text contains a `#[cfg(..)]` attribute. +pub fn text_contains_cfg(src: &str) -> bool { + // PERF: A `#[cfg]` needs a literal `#`, so skip the lexer when the source has none. + if !src.contains('#') { + return false; + } + + let mut iter = tokenize_with_text(src); + + // Search for the token sequence [`#`, `[`, `cfg`] + while iter.any(|(t, ..)| matches!(t, TokenKind::Pound)) { + let mut iter = iter.by_ref().skip_while(|(t, ..)| { + matches!( + t, + TokenKind::Whitespace | TokenKind::LineComment { .. } | TokenKind::BlockComment { .. } + ) + }); + if matches!(iter.next(), Some((TokenKind::OpenBracket, ..))) + && matches!(iter.next(), Some((TokenKind::Ident, "cfg", _))) + { + return true; } - false - }) + } + false } /// Currently used to keep track of the current value of `#[clippy::cognitive_complexity(N)]` diff --git a/clippy_utils/src/consts.rs b/clippy_utils/src/consts.rs index bcdc7754da6f..745f3bc4a385 100644 --- a/clippy_utils/src/consts.rs +++ b/clippy_utils/src/consts.rs @@ -5,7 +5,7 @@ #![expect(clippy::float_cmp)] use crate::res::MaybeDef as _; -use crate::source::{SpanExt as _, walk_span_to_context}; +use crate::source::{FileRangeExt as _, SpanExt as _}; use crate::{clip, is_direct_expn_of, sext, sym, unsext}; use rustc_abi::Size; @@ -941,11 +941,9 @@ impl<'tcx> ConstEvalCtxt<'tcx> { // Try to detect any `cfg`ed statements or empty macro expansions. let span = block.span.data(); if span.ctxt == SyntaxContext::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) = (span.lo..expr_lo).get_source_range(self.tcx) - && let Some(src) = src.as_str() + if let Some((scx, range)) = span.mk_edit_cx(self.tcx) + && let Some(search_range) = range.shrink_end_to(&scx, expr.span.walk_to_root().lo_ctxt()) + && let Some(src) = scx.get_text(search_range) { use rustc_lexer::TokenKind::{BlockComment, LineComment, OpenBrace, Semi, Whitespace}; if !tokenize(src, FrontmatterAllowed::No) diff --git a/clippy_utils/src/hir_utils.rs b/clippy_utils/src/hir_utils.rs index 75987ea96ce9..8741ee0ac384 100644 --- a/clippy_utils/src/hir_utils.rs +++ b/clippy_utils/src/hir_utils.rs @@ -1,6 +1,6 @@ use crate::consts::ConstEvalCtxt; use crate::macros::macro_backtrace; -use crate::source::{SpanExt as _, SpanRange, walk_span_to_context}; +use crate::source::{SpanExt as _, walk_span_to_context}; use crate::{sym, tokenize_with_text}; use core::mem; use rustc_ast::ast; @@ -20,9 +20,8 @@ use rustc_hir::{ use rustc_lexer::{FrontmatterAllowed, TokenKind, tokenize}; use rustc_lint::LateContext; use rustc_middle::ty::TypeckResults; -use rustc_span::{BytePos, ExpnKind, MacroKind, Symbol, SyntaxContext}; +use rustc_span::{ExpnKind, MacroKind, Symbol, SyntaxContext}; use std::hash::{Hash as _, Hasher as _}; -use std::ops::Range; use std::slice; /// Callback that is called when two expressions are not equal in the sense of `SpanlessEq`, but @@ -403,6 +402,15 @@ impl HirEqInterExpr<'_, '_, '_> { }, } + let Some((lscx, _)) = lspan.mk_edit_cx(self.inner.cx) else { + // Can't access the crate-local file, should be impossible. + return false; + }; + let Some((rscx, _)) = rspan.mk_edit_cx(self.inner.cx) else { + // Can't access the crate-local file, should be impossible. + return false; + }; + let mut lstart = lspan.lo; let mut rstart = rspan.lo; @@ -412,10 +420,10 @@ impl HirEqInterExpr<'_, '_, '_> { } // Try to detect any `cfg`ed statements or empty macro expansions. - let Some(lstmt_span) = walk_span_to_context(left.span, lspan.ctxt) else { + let Some(lstmt_span) = left.span.walk_to_parent(lspan.ctxt) else { return false; }; - let Some(rstmt_span) = walk_span_to_context(right.span, rspan.ctxt) else { + let Some(rstmt_span) = right.span.walk_to_parent(rspan.ctxt) else { return false; }; let lstmt_span = lstmt_span.data(); @@ -430,9 +438,11 @@ impl HirEqInterExpr<'_, '_, '_> { // Only one of the blocks had a weird macro. return false; } - if !eq_span_tokens(self.inner.cx, lstart..lstmt_span.lo, rstart..rstmt_span.lo, |t| { - !matches!(t, Whitespace | Semi) - }) { + if !eq_tokens( + lscx.get_text_by_src_range(lstart..lstmt_span.lo), + rscx.get_text_by_src_range(rstart..rstmt_span.lo), + |t| !matches!(t, Whitespace | Semi), + ) { return false; } @@ -465,9 +475,11 @@ impl HirEqInterExpr<'_, '_, '_> { // Only one of the blocks had a weird macro return false; } - eq_span_tokens(self.inner.cx, lstart..lend, rstart..rend, |t| { - !matches!(t, Whitespace | Semi) - }) + eq_tokens( + lscx.get_text_by_src_range(lstart..lend), + rscx.get_text_by_src_range(rstart..rend), + |t| !matches!(t, Whitespace | Semi), + ) } fn should_ignore(&self, expr: &Expr<'_>) -> bool { @@ -958,10 +970,9 @@ impl HirEqInterExpr<'_, '_, '_> { // Finally if the outermost expansion is a macro call, check if the // tokens are the same. if let ExpnKind::Macro(MacroKind::Bang, _) = left_data.kind { - return Some(eq_span_tokens( - self.inner.cx, - left_data.call_site, - right_data.call_site, + return Some(eq_tokens( + left_data.call_site.get_text(self.inner.cx).as_deref(), + right_data.call_site.get_text(self.inner.cx).as_deref(), |t| !matches!(t, Whitespace | LineComment { .. } | BlockComment { .. }), )); } @@ -1735,30 +1746,20 @@ pub fn hash_expr(cx: &LateContext<'_>, e: &Expr<'_>) -> u64 { h.finish() } -fn eq_span_tokens( - cx: &LateContext<'_>, - left: impl SpanRange, - right: impl SpanRange, - pred: impl Fn(TokenKind) -> bool, -) -> bool { - fn f(cx: &LateContext<'_>, left: Range, right: Range, pred: impl Fn(TokenKind) -> bool) -> bool { - if let Some(lsrc) = left.get_source_range(cx) - && let Some(lsrc) = lsrc.as_str() - && let Some(rsrc) = right.get_source_range(cx) - && let Some(rsrc) = rsrc.as_str() - { - let pred = |&(token, ..): &(TokenKind, _, _)| pred(token); - let map = |(_, source, _)| source; +fn eq_tokens(lsrc: Option<&str>, rsrc: Option<&str>, pred: impl Fn(TokenKind) -> bool) -> bool { + if let Some(lsrc) = lsrc + && let Some(rsrc) = rsrc + { + let pred = |&(token, ..): &(TokenKind, _, _)| pred(token); + let map = |(_, source, _)| source; - let ltok = tokenize_with_text(lsrc).filter(pred).map(map); - let rtok = tokenize_with_text(rsrc).filter(pred).map(map); - ltok.eq(rtok) - } else { - // Unable to access the source. Conservatively assume the blocks aren't equal. - false - } + let ltok = tokenize_with_text(lsrc).filter(pred).map(map); + let rtok = tokenize_with_text(rsrc).filter(pred).map(map); + ltok.eq(rtok) + } else { + // Unable to access the source. Conservatively assume the blocks aren't equal. + false } - f(cx, left.into_range(), right.into_range(), pred) } /// Returns true if the expression contains ambiguous literals (unsuffixed float or int literals) diff --git a/clippy_utils/src/lib.rs b/clippy_utils/src/lib.rs index f9c71975ddce..df63af7ee51f 100644 --- a/clippy_utils/src/lib.rs +++ b/clippy_utils/src/lib.rs @@ -1,5 +1,8 @@ #![feature(deref_patterns)] +#![feature(closure_track_caller)] #![feature(macro_metavar_expr)] +#![feature(maybe_uninit_array_assume_init)] +#![feature(pattern)] #![feature(rustc_private)] #![feature(unwrap_infallible)] #![recursion_limit = "512"] @@ -2843,6 +2846,7 @@ pub fn tokenize_with_text(s: &str) -> impl Iterator(sm: impl HasSourceMap<'sm>, span: Span) -> bool { span.check_text(sm, |snippet| { tokenize(snippet, FrontmatterAllowed::No).any(|token| { @@ -2858,6 +2862,7 @@ pub fn span_contains_comment<'sm>(sm: impl HasSourceMap<'sm>, span: Span) -> boo /// token, including comments unless `skip_comments` is set. /// This is useful to determine if there are any actual code tokens in the span that are omitted in /// the late pass, such as platform-specific code. +#[cfg_attr(debug_assertions, track_caller)] pub fn span_contains_non_whitespace<'sm>(sm: impl HasSourceMap<'sm>, span: Span, skip_comments: bool) -> bool { span.check_text(sm, |snippet| { tokenize_with_text(snippet).any(|(token, _, _)| match token { @@ -2871,6 +2876,7 @@ pub fn span_contains_non_whitespace<'sm>(sm: impl HasSourceMap<'sm>, span: Span, /// Returns all the comments a given span contains /// /// Comments are returned wrapped with their relevant delimiters +#[cfg_attr(debug_assertions, track_caller)] pub fn span_extract_comment<'sm>(sm: impl HasSourceMap<'sm>, span: Span) -> String { span_extract_comments(sm, span).join("\n") } @@ -2878,14 +2884,14 @@ pub fn span_extract_comment<'sm>(sm: impl HasSourceMap<'sm>, span: Span) -> Stri /// Returns all the comments a given span contains. /// /// Comments are returned wrapped with their relevant delimiters. +#[cfg_attr(debug_assertions, track_caller)] pub fn span_extract_comments<'sm>(sm: impl HasSourceMap<'sm>, span: Span) -> Vec { - span.with_source_text(sm, |snippet| { - tokenize_with_text(snippet) + span.get_text(sm).map_or(Vec::new(), |s| { + tokenize_with_text(&s) .filter(|(t, ..)| matches!(t, TokenKind::BlockComment { .. } | TokenKind::LineComment { .. })) .map(|(_, s, _)| s.to_string()) .collect::>() }) - .unwrap_or_default() } pub fn span_find_starting_semi(sm: &SourceMap, span: Span) -> Span { diff --git a/clippy_utils/src/source.rs b/clippy_utils/src/source.rs index 9110f6cbf6b4..50f4831eed8a 100644 --- a/clippy_utils/src/source.rs +++ b/clippy_utils/src/source.rs @@ -1,25 +1,92 @@ -//! Utils for extracting, inspecting or transforming source code - -#![expect(clippy::module_name_repetitions)] - -use std::sync::Arc; +//! Utilities for interacting with the source text and manipulating spans. +//! +//! The main entry points for working with the source text are on the [`SpanExt`] trait. This trait +//! is implemented on a few types and exists as a bridge between a [`Span`] and the source text +//! backing it. The following are the main functions: +//! +//! * [`SpanExt::mk_edit_cx`]: This is the most general method interacting with the source text and +//! the other methods should be preferred when possible. This is useful for handling multiple +//! sub-spans (e.g. multiple items in a list), or when checking a predicate on the text +//! surrounding a span. +//! * [`SpanExt::get_text`]: Gets a `SourceText` representing the text the span refers to. It works +//! very similarly to an `Arc`. This is normally used when building a suggestion. +//! * [`SpanExt::get_text_within_other`]: Like `get_text`, but first adjusts the context to match +//! another span. This is needed when the node this span is from might be created by a macro. +//! * [`SpanExt::check_text`]: A slightly simpler way to check a predicate on the text than using +//! `get_text`. Prefer this if possible. +//! * [`SpanExt::map_range`]: The main way to adjust the range portion of a span. See [`SpanEditCx`] +//! for range adjustment utilities. +//! * [`SpanExt::map_split_range`]: Similar to the previous, but for when you need to create +//! multiple spans from a single span. +//! +//! # Working With Macros +//! +//! All parts of the linting infrastructure work after macro expansion meaning that there are no +//! nodes in the AST or HIR representing a macro call (AST nodes have a variant for these, but it's +//! only used before expansion). Because of this there's no direct way of knowing where a macro call +//! occurred. Instead the `Span`, or more specifically the [`SyntaxContext`], notes which expansion +//! created the node. +//! +//! Each `SyntaxContext` is associated with a particular macro call or AST desugaring (e.g. a `for` +//! loop). These expansions form a tree starting from a singular root context with each macro call +//! and desugaring creating a new child node. As an example: +//! +//! ```rust +//! macro_rules! m1 { +//! ($e1:expr) => { $e1 + 1 }; +//! } +//! macro_rules! m2 { +//! ($e2:expr) => {{ m1!($e2) + m1!($e2) }}; +//! } +//! let x = m2!(1u32); +//! let y = m1!(1u32); +//! ``` +//! +//! This would create the following expansions: +//! +//! ```none +//! root +//! / \ +//! m2 m1 +//! / \ +//! m1 m1 +//! ``` +//! +//! When accessing the text for the initializer of `x`, just naively using the span for the +//! expression will get the contents of the `m2` macro (`m1!($e2) + m1!($e2)`). Similarly, the span +//! for the left-hand side expression of the addition in `m1` (`$e1 + 1`) will be the span of the +//! argument at the call site. In both cases [`SpanExt::get_text_within_other`] can be used to get +//! the span of the macro call (`m2!(1u32)`) or the metavar expansion (`$e1`) respectively. +//! +//! # Warnings +//! +//! You _cannot_ assume anything about the `Span` or source text of any item. The parser will apply +//! token substitution in some cases (e.g. replacing `(`, with `(`), macros can rearrange tokens, +//! and proc-macros in particular can freely set the `Span` of any token to a different one. These +//! can only be detected by checking the source text. With this the source text of all AST/HIR +//! items can be almost anything. In short, validate all range adjustments against the source text. +use core::fmt; +use core::mem::{ManuallyDrop, MaybeUninit}; +use core::ops::{Deref, Index, Range, RangeFrom, RangeFull, RangeTo}; +use core::slice::SliceIndex; +use core::str::pattern::{Pattern, ReverseSearcher}; use rustc_ast::{LitKind, StrStyle}; use rustc_errors::Applicability; +use rustc_hir::def_id::LocalDefId; use rustc_hir::{BlockCheckMode, Expr, ExprKind, UnsafeSource}; -use rustc_lexer::{FrontmatterAllowed, LiteralKind, TokenKind, tokenize}; -use rustc_lint::{EarlyContext, LateContext}; -use rustc_middle::ty::TyCtxt; +use rustc_lexer::{FrontmatterAllowed, LiteralKind, TokenKind, is_whitespace, tokenize}; +use rustc_lint::{EarlyContext, LateContext, LintContext as _}; +use rustc_middle::ty::{TyCtxt, tls}; use rustc_session::Session; use rustc_span::source_map::{SourceMap, original_sp}; use rustc_span::{ - BytePos, DUMMY_SP, DesugaringKind, Pos as _, RelativeBytePos, SourceFile, SourceFileAndLine, Span, SpanData, - SyntaxContext, hygiene, + BytePos, DUMMY_SP, DesugaringKind, ExpnKind, Pos as _, RelativeBytePos, SourceFile, SourceFileAndLine, Span, + SpanData, SyntaxContext, hygiene, with_metavar_spans, }; -use std::borrow::Cow; -use std::fmt; -use std::ops::{Deref, Index, Range}; +use std::borrow::{Borrow, Cow}; +/// A type which contains a `SourceMap`. pub trait HasSourceMap<'sm>: Copy { #[must_use] fn source_map(self) -> &'sm SourceMap; @@ -45,7 +112,7 @@ impl<'sm> HasSourceMap<'sm> for TyCtxt<'sm> { impl<'sm> HasSourceMap<'sm> for &'sm EarlyContext<'_> { #[inline] fn source_map(self) -> &'sm SourceMap { - ::rustc_lint::LintContext::sess(self).source_map() + self.sess().source_map() } } impl<'sm> HasSourceMap<'sm> for &LateContext<'sm> { @@ -55,158 +122,512 @@ impl<'sm> HasSourceMap<'sm> for &LateContext<'sm> { } } -/// Conversion of a value into the range portion of a `Span`. -pub trait SpanRange: Sized { - fn into_range(self) -> Range; +/// A position in the `SourceMap` and the `SyntaxContext` it came from. +#[derive(Clone, Copy)] +pub struct PosWithCtxt { + pub pos: BytePos, + pub ctxt: SyntaxContext, } -impl SpanRange for Span { - fn into_range(self) -> Range { - let data = self.data(); - data.lo..data.hi + +// Workaround for `array::map` not working with `track_caller`. This will fail to drop values if a +// panic occurs, but that doesn't matter for our use. +const fn mk_uninit(_: &[T; N]) -> [MaybeUninit; N] { + [const { MaybeUninit::uninit() }; N] +} +macro_rules! map_array_inline { + ($a:expr, $p:pat => $e:expr $(,)?) => {{ + let src = ManuallyDrop::new($a); + let mut dst = mk_uninit(&src); + for i in 0..dst.len() { + // SAFETY: `src` was fully initialized at the start of the loop. + let $p = unsafe { (&raw const src[i]).read() }; + dst[i].write($e); + } + // SAFETY: The loop has written every element in the array. + unsafe { MaybeUninit::array_assume_init(dst) } + }}; +} + +pub trait ToSpanData: Copy { + #[must_use] + fn data(self) -> SpanData; +} +impl ToSpanData for SpanData { + #[inline] + fn data(self) -> SpanData { + self } } -impl SpanRange for SpanData { - fn into_range(self) -> Range { - self.lo..self.hi +impl ToSpanData for &SpanData { + #[inline] + fn data(self) -> SpanData { + *self } } -impl SpanRange for Range { - fn into_range(self) -> Range { - self +impl ToSpanData for Span { + #[inline] + fn data(self) -> SpanData { + self.data() } } -/// Conversion of a value into a `Span` -pub trait IntoSpan: Sized { - fn into_span(self) -> Span; - fn with_ctxt(self, ctxt: SyntaxContext) -> Span; +/// Marker trait for `Span`. +/// +/// Only exists so all functions can exist on the `SpanExt` trait. +pub trait CompressedSpan: Copy { + #[must_use] + fn span(self) -> Span; } -impl IntoSpan for Span { - fn into_span(self) -> Span { +impl CompressedSpan for Span { + #[inline] + fn 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() + +pub trait SpanExt: Sized { + #[must_use] + fn span_range(self) -> Range; + + /// Gets the `lo` position and the `SyntaxContext` + #[inline] + #[must_use] + fn lo_ctxt(self) -> PosWithCtxt + where + Self: ToSpanData, + { + let data = self.data(); + PosWithCtxt { + pos: data.lo, + ctxt: data.ctxt, + } } - fn with_ctxt(self, ctxt: SyntaxContext) -> Span { - Span::new(self.lo, self.hi, ctxt, self.parent) + + /// Gets the `hi` position and the `SyntaxContext` + #[inline] + #[must_use] + fn hi_ctxt(self) -> PosWithCtxt + where + Self: ToSpanData, + { + let data = self.data(); + PosWithCtxt { + pos: data.hi, + ctxt: data.ctxt, + } } -} -impl IntoSpan for Range { - fn into_span(self) -> Span { - Span::with_root_ctxt(self.start, self.end) + + /// Attempts to get a handle to the source text of a crate-local file. Returns `None` if the + /// range is within a non-local file or cannot index the file's text. + /// + /// With debug assertions this will assert that the range: + /// * Is within a crate-local file. + /// * Does not start after it's end. + /// * Does not exceed the bounds of a single source file. + /// * Lies on a UTF-8 boundary. + #[inline] + #[must_use] + #[cfg_attr(debug_assertions, track_caller)] + fn get_text<'sm>(self, sm: impl HasSourceMap<'sm>) -> Option { + SourceText::for_local_range(sm.source_map(), self.span_range()) } - fn with_ctxt(self, ctxt: SyntaxContext) -> Span { - Span::new(self.start, self.end, ctxt, None) + + /// Checks if the source text of a crate-local file satisfies the given predicate. Returns + /// `false` if the range is within a non-local file or cannot index the file's text. + /// + /// With debug assertions this will assert that the range: + /// * Does not start after it's end. + /// * Does not exceed the bounds of a single source file. + /// * Lies on a UTF-8 boundary. + #[inline] + #[must_use] + #[cfg_attr(debug_assertions, track_caller)] + fn check_text<'sm>(self, sm: impl HasSourceMap<'sm>, pred: impl FnOnce(&str) -> bool) -> bool { + self.get_text(sm).as_deref().is_some_and(pred) } -} -pub trait SpanExt: SpanRange { - /// Attempts to get a handle to the source text. Returns `None` if either the span is malformed, - /// or the source text is not accessible. - fn get_text<'sm>(self, sm: impl HasSourceMap<'sm>) -> Option { - get_source_range(sm.source_map(), self.into_range()).and_then(SourceText::new) + /// Walks this span into the context of another; either up to the call site or down to the + /// metavar expansion site. Returns `None` if either the target context could be reached or the + /// adjusted span does not lie within the target span. + /// + /// # Examples + /// + /// Given the following code: + /// + /// ```rust,ignore + /// macro_rules! m1 { ($e1:expr) => { f1($e1) }; } + /// macro_rules! m2 { ($e2:expr) => { f2(m1!($e2)) }; } + /// f3(m2!(0)) + /// ``` + /// + /// This expands to `f3(f2(f1(0)))` with the following `SyntaxContext`s: + /// + /// |Context |Contents | + /// |------------|------------| + /// |Root context|`f3(_)`, `0`| + /// |`m2!` |`f2(_)` | + /// |`m1!` |`f1(_)` | + /// + /// The following table lists the results of various possible argument combinations: + /// + /// |Span |Context |Result | + /// |-------|------------|--------| + /// |`f3(_)`|Root |`f3(_)` | + /// |`f3(_)`|`m1!`, `m2!`|None | + /// |`f2(_)`|Root |`m2!(0)`| + /// |`f2(_)`|`m2!` |`f2(_)` | + /// |`f2(_)`|`m1!` |None | + /// |`f1(_)`|Root |`m2!(0)`| + /// |`f1(_)`|`m2!` |`m1!(0)`| + /// |`f1(_)`|`m1!` |`f1(_)` | + /// |`0` |Root |`0` | + /// |`0` |`m2!` |`$e2` | + /// |`0` |`m1!` |`$e1` | + #[inline] + #[must_use] + fn walk_into_other(self, target: &SpanData) -> Option + where + Self: CompressedSpan, + { + fn f(sp: Span, target: &SpanData) -> Option { + let mut sp_data = sp.data(); + if sp_data.ctxt != target.ctxt { + let expn = sp_data.ctxt.outer_expn_data(); + let call_site = expn.call_site.data(); + if call_site.ctxt != target.ctxt { + sp_data = hygiene::walk_chain(expn.call_site, target.ctxt).data(); + if sp_data.ctxt != target.ctxt { + sp_data = with_metavar_spans(|mspans| mspans.get(sp))?.data(); + if sp_data.ctxt != target.ctxt { + return None; + } + } + } else if matches!(expn.kind, ExpnKind::Desugaring(DesugaringKind::RangeExpr)) { + // The call site of a range desugaring does not include any surrounding parenthesis. + // e.g. `(1..2)` will have `1..2` as the call site, but `(1..2)` as the desugaring + // span. + // + // Currently some uses are assuming that we will keep the surrounding parenthesis so + // we can't use the call site. + sp_data.ctxt = target.ctxt; + } else { + sp_data = expn.call_site.data(); + } + } + (target.lo <= sp_data.lo && sp_data.hi <= target.hi).then_some(sp_data) + } + f(self.span(), target) } - /// 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_range<'sm>(self, sm: impl HasSourceMap<'sm>) -> Option { - get_source_range(sm.source_map(), self.into_range()) + /// Walks this span up the expansion call chain into the target context. Returns `None` if the + /// target context cannot be found this way. + /// + /// This is similar to [`SpanExt::walk_into_other`] except it will not walk to a metavar + /// expansion site, nor will it check the adjusted span's range. + #[inline] + #[must_use] + fn walk_to_parent(self, target: SyntaxContext) -> Option + where + Self: CompressedSpan, + { + #[cold] + #[inline(never)] + fn slow(sp: Span, sp_ctxt: SyntaxContext, target: SyntaxContext) -> Option { + let expn = sp_ctxt.outer_expn_data(); + if expn.call_site.ctxt() != target { + let sp = hygiene::walk_chain(expn.call_site, SyntaxContext::root()); + (sp.ctxt() == target).then_some(sp) + } else if matches!(expn.kind, ExpnKind::Desugaring(DesugaringKind::RangeExpr)) { + // The call site of a range desugaring does not include any surrounding parenthesis. + // e.g. `(1..2)` will have `1..2` as the call site, but `(1..2)` as the desugaring + // span. + // + // Currently some uses are assuming that we will keep the surrounding parenthesis so + // we can't use the call site. + Some(sp.with_ctxt(target)) + } else { + Some(expn.call_site) + } + } + + let sp = self.span(); + let sp_ctxt = sp.ctxt(); + if sp_ctxt == target { + Some(sp) + } else { + slow(sp, sp_ctxt, target) + } } - /// 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<'sm, T>(self, sm: impl HasSourceMap<'sm>, f: impl for<'a> FnOnce(&'a str) -> T) -> Option { - with_source_text(sm.source_map(), self.into_range(), f) + /// Walks this span up the expansion call chain to the root context. + #[inline] + #[must_use] + fn walk_to_root(self) -> Span + where + Self: CompressedSpan, + { + #[cold] + #[inline(never)] + fn slow(sp: Span) -> Span { + let expn = sp.ctxt().outer_expn_data(); + if expn.call_site.from_expansion() { + hygiene::walk_chain(expn.call_site, SyntaxContext::root()) + } else if matches!(expn.kind, ExpnKind::Desugaring(DesugaringKind::RangeExpr)) { + // The call site of a range desugaring does not include any surrounding parenthesis. + // e.g. `(1..2)` will have `1..2` as the call site, but `(1..2)` as the desugaring + // span. + // + // Currently some uses are assuming that we will keep the surrounding parenthesis so + // we can't use the call site. + sp.with_ctxt(SyntaxContext::root()) + } else { + expn.call_site + } + } + + let sp = self.span(); + if sp.from_expansion() { slow(sp) } else { sp } } - /// Checks if the referenced source text satisfies the given predicate. Returns `false` if the - /// source text cannot be retrieved. - fn check_text<'sm>(self, sm: impl HasSourceMap<'sm>, pred: impl for<'a> FnOnce(&'a str) -> bool) -> bool { - self.with_source_text(sm, pred).unwrap_or(false) + /// Attempts to get a handle to the source text of a crate-local file after adjusting this span + /// to be in the same context as another. This will return `None` if this span could not be + /// adjusted to the target's context; or the adjusted span does not lie within the target or + /// cannot index the text of a single file. + /// + /// See `[SpanExt::walk_to_other]` for details about how context adjustment works. + /// + /// With debug assertions this will assert that the adjusted range: + /// * Lies within a single crate-local file. + /// * Does not start after it's end. + /// * Does not exceed the bounds of a single source file. + /// * Lies on a UTF-8 boundary. + #[inline] + #[must_use] + #[cfg_attr(debug_assertions, track_caller)] + fn get_text_within_other<'sm>(self, sm: impl HasSourceMap<'sm>, other: &SpanData) -> Option + where + Self: CompressedSpan, + { + self.walk_into_other(other).and_then(|sp| sp.get_text(sm)) } - /// 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<'sm, T>( - self, - sm: impl HasSourceMap<'sm>, - f: impl for<'a> FnOnce(&'a str, Range) -> T, - ) -> Option { - with_source_text_and_range(sm.source_map(), self.into_range(), f) + /// Attempts to create a new edit context for a source range within a crate-local file. Returns + /// both the context and the adjusted range, or `None` if the range is within a non-local file. + /// + /// With debug assertions this will assert that the range: + /// * Is within a crate-local file. + /// * Does not start after it's end. + /// * Does not exceed the bounds of a single source file. + /// * Lies on a UTF-8 boundary. + #[inline] + #[must_use] + #[cfg_attr(debug_assertions, track_caller)] + fn mk_edit_cx<'sm>(self, sm: impl HasSourceMap<'sm>) -> Option<(SpanEditCx, FileRange)> + where + Self: ToSpanData, + { + SpanEditCx::for_local(sm.source_map(), self.data()) } - /// 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. + /// Maps the range of the current span within a crate-local file. Returns `None` if the given + /// function returns `None`, or the span is within a non-local file. /// - /// The new range must reside within the same source file. + /// With debug assertions this will assert that both the initial and mapped ranges: + /// * Do not start after their respective ends. + /// * Do not exceed the bounds of a single source file. + /// * Lie on a UTF-8 boundary. + #[inline] + #[must_use] + #[cfg_attr(debug_assertions, track_caller)] fn map_range<'sm>( self, sm: impl HasSourceMap<'sm>, - f: impl for<'a> FnOnce(&'a SourceFile, &'a str, Range) -> Option>, - ) -> Option> { - map_range(sm.source_map(), self.into_range(), f) + f: impl FnOnce(&SpanEditCx, FileRange) -> Option, + ) -> Option + where + Self: ToSpanData, + { + if let Some((scx, range)) = self.mk_edit_cx(sm) + && let Some(mapped) = f(&scx, range.clone()) + { + Some(scx.mk_span(mapped, Some(range))) + } else { + None + } } - /// Extends the range to include all preceding whitespace characters. + /// Maps and splits the range of the current span within a crate-local file. Returns `None` if + /// the given function returns `None`, or the span is within a non-local file. /// - /// The range will not be expanded if it would cross a line boundary, the line the range would - /// be extended to ends with a line comment and the text after the range contains a - /// non-whitespace character on the same line. e.g. - /// - /// ```ignore - /// ( // Some comment - /// foo) - /// ``` - /// - /// When the range points to `foo`, suggesting to remove the range after it's been extended will - /// cause the `)` to be placed inside the line comment as `( // Some comment)`. - fn with_leading_whitespace<'sm>(self, sm: impl HasSourceMap<'sm>) -> Range { - with_leading_whitespace(sm.source_map(), self.into_range()) + /// With debug assertions this will assert that both the initial and mapped ranges: + /// * Do not start after their respective ends. + /// * Do not exceed the bounds of a single source file. + /// * Lie on a UTF-8 boundary. + #[inline] + #[must_use] + #[cfg_attr(debug_assertions, track_caller)] + fn map_split_range<'sm, const N: usize>( + self, + sm: impl HasSourceMap<'sm>, + f: impl FnOnce(&SpanEditCx, FileRange) -> Option<[FileRange; N]>, + ) -> Option<[Span; N]> + where + Self: ToSpanData, + { + if let Some((scx, range)) = self.mk_edit_cx(sm) + && let Some(mapped) = f(&scx, range.clone()) + { + Some(map_array_inline!(mapped, r => scx.mk_span(r, Some(range.clone())))) + } else { + None + } } - - /// Trims the leading whitespace from the range. - fn trim_start<'sm>(self, sm: impl HasSourceMap<'sm>) -> Range { - trim_start(sm.source_map(), self.into_range()) +} +impl SpanExt for Span { + #[inline] + fn span_range(self) -> Range { + self.data().span_range() + } +} +impl SpanExt for SpanData { + #[inline] + fn span_range(self) -> Range { + self.lo..self.hi + } +} +impl SpanExt for Range { + #[inline] + fn span_range(self) -> Range { + self } } -impl SpanExt for T {} -/// Handle to a range of text in a source file. -pub struct SourceText(SourceFileRange); -impl SourceText { - /// Takes ownership of the source file handle if the source text is accessible. - pub fn new(text: SourceFileRange) -> Option { - if text.as_str().is_some() { - Some(Self(text)) - } else { - None - } +mod source_text { + use rustc_span::SourceFile; + use std::sync::Arc; + + /// Handle to a substring of text in a source file. + #[derive(Clone)] + pub struct SourceText { + file: Arc, + // This is a pointer into the text owned by the source file. If the source is external + // then the `FreezeLock` on the text must be frozen. + text: *const str, } + impl SourceText { + /// Gets the text of the given crate-local file. Returns `None` if the file is non-local. + /// + /// With debug assertions this will assert that the file is local. + #[inline] + #[must_use] + #[cfg_attr(debug_assertions, track_caller)] + pub fn for_local_file(file: Arc) -> Option { + let text: *const str = if let Some(text) = &file.src { + &raw const ***text + } else { + debug_assert!( + false, + "attempted to access the non-local file `{}` as local.", + file.name.prefer_local_unconditionally() + ); + return None; + }; + Some(Self { file, text }) + } + + /// Gets the source text. + #[inline] + #[must_use] + pub fn as_str(&self) -> &str { + // SAFETY: `text` is owned by `file` and comes from either an `Option>`, or a + // frozen `FeezeLock` (which ultimately contains an `Arc`). Neither + // of these can change so long as we own `file`. + unsafe { &*self.text } + } + + /// Gets the source file containing the text. + #[inline] + #[must_use] + pub fn file(&self) -> &Arc { + &self.file + } + + /// Takes ownership of the source file handle. + #[inline] + #[must_use] + pub fn into_file(self) -> Arc { + self.file + } + + /// Applies the mapping function to the contained string. + #[inline] + #[must_use] + pub fn map_text(mut self, f: impl FnOnce(&SourceText) -> &str) -> Self { + // The only strings that `f` can return are those with a lifetime derived from it's + // input, and `'static` strings. Both are safe to use here. + self.text = f(&self); + self + } - /// Gets the source text. - pub fn as_str(&self) -> &str { - self.0.as_str().unwrap() + /// Applies the mapping function to the contained string. Returns `None` if the function + /// does. + #[inline] + #[must_use] + pub fn try_map_text(mut self, f: impl FnOnce(&SourceText) -> Option<&str>) -> Option { + // The only strings that `f` can return are those with a lifetime derived from it's + // input, and `'static` strings. Both are safe to use here. + match f(&self) { + Some(s) => { + self.text = s; + Some(self) + }, + None => None, + } + } + } +} +pub use self::source_text::SourceText; +impl SourceText { + #[must_use] + #[cfg_attr(debug_assertions, track_caller)] + pub fn for_local_range(sm: &SourceMap, range: Range) -> Option { + let sfp = sm.lookup_byte_offset(range.start); + let text = Self::for_local_file(sfp.sf)?; + let range = RelativeBytePos(sfp.pos.0)..RelativeBytePos(range.end.0.wrapping_sub(text.file().start_pos.0)); + dbg_check_range(&text, None, range.clone()); + text.apply_index(range.into_slice_idx()) } /// Converts this into an owned string. + #[inline] + #[must_use] pub fn to_owned(&self) -> String { self.as_str().to_owned() } + + /// Applies an indexing operation to the contained string. Returns `None` if the index is + /// not valid. + #[inline] + #[must_use] + pub fn apply_index(self, idx: impl SliceIndex) -> Option { + self.try_map_text(|s| s.get(idx)) + } } impl Deref for SourceText { type Target = str; + #[inline] fn deref(&self) -> &Self::Target { self.as_str() } } +impl Borrow for SourceText { + #[inline] + fn borrow(&self) -> &str { + self.as_str() + } +} impl AsRef for SourceText { + #[inline] fn as_ref(&self) -> &str { self.as_str() } @@ -216,81 +637,767 @@ where str: Index, { type Output = >::Output; + #[inline] fn index(&self, idx: T) -> &Self::Output { &self.as_str()[idx] } } impl fmt::Display for SourceText { + #[inline] fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { self.as_str().fmt(f) } } impl fmt::Debug for SourceText { + #[inline] fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { self.as_str().fmt(f) } } -fn get_source_range(sm: &SourceMap, sp: Range) -> Option { - let start = sm.lookup_byte_offset(sp.start); - let end = sm.lookup_byte_offset(sp.end); - if !Arc::ptr_eq(&start.sf, &end.sf) || start.pos > end.pos { - return None; +/// Like `SliceIndex`, but for indexing a file's text rather than any string. This uses +/// `RelativeBytePos` instead of `usize`. +pub trait FileIndex { + type SliceRange: SliceIndex; + + #[must_use] + fn into_slice_idx(self) -> Self::SliceRange; + + /// Converts this into a bounded range by limiting unbounded ends to the file's bounds. + #[must_use] + fn into_file_range(self, scx: &SpanEditCx) -> FileRange; +} +impl FileIndex for RangeFull { + type SliceRange = Self; + #[inline] + fn into_slice_idx(self) -> Self::SliceRange { + self + } + #[inline] + fn into_file_range(self, scx: &SpanEditCx) -> FileRange { + RelativeBytePos(0)..scx.file().normalized_source_len + } +} +impl FileIndex for Range { + type SliceRange = Range; + #[inline] + fn into_slice_idx(self) -> Self::SliceRange { + self.start.to_usize()..self.end.to_usize() + } + #[inline] + fn into_file_range(self, _: &SpanEditCx) -> FileRange { + self + } +} +impl FileIndex for RangeTo { + type SliceRange = RangeTo; + #[inline] + fn into_slice_idx(self) -> Self::SliceRange { + ..self.end.to_usize() + } + #[inline] + fn into_file_range(self, _: &SpanEditCx) -> FileRange { + RelativeBytePos(0)..self.end + } +} +impl FileIndex for RangeFrom { + type SliceRange = RangeFrom; + #[inline] + fn into_slice_idx(self) -> Self::SliceRange { + self.start.to_usize().. + } + #[inline] + fn into_file_range(self, scx: &SpanEditCx) -> FileRange { + self.start..scx.file().normalized_source_len + } +} + +/// The range type used for specifying a range within a file. +pub type FileRange = Range; + +#[cfg_attr(not(debug_assertions), inline)] +#[cfg_attr(debug_assertions, track_caller)] +fn dbg_check_range(text: &SourceText, old: Option, new: FileRange) { + if cfg!(debug_assertions) && text.get(new.clone().into_slice_idx()).is_none() { + tls::with( + #[track_caller] + |tcx| { + let file = &**text.file(); + let dcx = tcx.dcx(); + let mut diag = dcx.struct_bug(format!( + "invalid range `{}..{}` within file `{}`", + // Signed numbers will better show most errors. + new.start.0.cast_signed(), + new.end.0.cast_signed(), + file.name.prefer_local_unconditionally(), + )); + if new.start <= file.normalized_source_len { + let (sp, msg) = if text.is_char_boundary(new.start.to_usize()) { + let pos = BytePos(new.start.0 + file.start_pos.0); + ( + Span::new(pos, pos, SyntaxContext::root(), None), + "the range starts here", + ) + } else { + #[expect(clippy::cast_possible_truncation)] + let lo = BytePos(text.floor_char_boundary(new.start.to_usize()) as u32 + file.start_pos.0); + #[expect(clippy::cast_possible_truncation)] + let hi = BytePos(text.ceil_char_boundary(new.start.to_usize()) as u32 + file.start_pos.0); + ( + Span::new(lo, hi, SyntaxContext::root(), None), + "the range starts within this character", + ) + }; + diag.span_note(sp, msg); + } else { + diag.note("the range starts outside this file"); + } + if new.end <= file.normalized_source_len { + let (sp, msg) = if text.is_char_boundary(new.end.to_usize()) { + let pos = BytePos(new.end.0 + file.start_pos.0); + (Span::new(pos, pos, SyntaxContext::root(), None), "the range ends here") + } else { + #[expect(clippy::cast_possible_truncation)] + let lo = BytePos(text.floor_char_boundary(new.end.to_usize()) as u32 + file.start_pos.0); + #[expect(clippy::cast_possible_truncation)] + let hi = BytePos(text.ceil_char_boundary(new.end.to_usize()) as u32 + file.start_pos.0); + ( + Span::new(lo, hi, SyntaxContext::root(), None), + "the range ends within this character", + ) + }; + diag.span_note(sp, msg); + } else { + diag.note("the range ends outside this file"); + } + if let Some(old) = old + && text.get(old.clone().into_slice_idx()).is_some() + { + let lo = BytePos(old.start.0 + file.start_pos.0); + let hi = BytePos(old.end.0 + file.start_pos.0); + diag.span_note( + Span::new(lo, hi, SyntaxContext::root(), None), + "the range was transformed from this span", + ); + } + diag.emit(); + }, + ); } - sm.ensure_source_file_source_present(&start.sf); - 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_range(sm, sp) - && let Some(src) = src.as_str() +/// The context used to manipulate source ranges within a single file. +pub struct SpanEditCx { + text: SourceText, + ctxt: SyntaxContext, + parent: Option, +} +impl SpanEditCx { + /// Creates a new edit context for a span within a crate-local file. Returns `None` if span is + /// within a non-local file. + /// + /// With debug assertions this will validate that the span: + /// * Is within a single crate-local file. + /// * The start and end do not overlap. + /// * Lies on UTF-8 boundaries. + #[must_use] + #[cfg_attr(debug_assertions, track_caller)] + pub fn for_local(sm: &SourceMap, data: SpanData) -> Option<(Self, FileRange)> { + let sfp = sm.lookup_byte_offset(data.lo); + let range = RelativeBytePos(sfp.pos.0)..RelativeBytePos(data.hi.0.wrapping_sub(sfp.sf.start_pos.0)); + + let scx = Self { + text: SourceText::for_local_file(sfp.sf)?, + ctxt: data.ctxt, + parent: data.parent, + }; + scx.dbg_check_range(None, range.clone()); + Some((scx, range)) + } + + /// Converts this into the inner `SourceText`. + #[inline] + #[must_use] + pub fn into_file_text(self) -> SourceText { + self.text + } + + /// Converts this into the inner `SourceText` after slicing it. Returns `None` if the text can't + /// be indexed by the range. + #[inline] + #[must_use] + pub fn into_sliced_text(self, idx: impl FileIndex) -> Option { + self.text.apply_index(idx.into_slice_idx()) + } + + /// Gets a reference to the contained source file. + #[inline] + #[must_use] + pub fn file(&self) -> &SourceFile { + self.text.file() + } + + /// Gets the text of the whole file. + #[inline] + #[must_use] + pub fn file_text(&self) -> &str { + self.text.as_str() + } + + /// Gets a subslice of the file's text. Returns `None` if the range is invalid. + #[inline] + #[must_use] + pub fn get_text(&self, index: impl FileIndex) -> Option<&str> { + self.text.as_str().get(index.into_slice_idx()) + } + + /// Gets a subslice of the file's text. Returns `None` if the span is invalid. + /// + /// With debug assertions this will validate that the span: + /// * Is from the same syntax context. + /// * Is contained within the current file. + /// * The start and end do not overlap. + /// * Lies on UTF-8 boundaries. + #[inline] + #[must_use] + pub fn get_text_by_span(&self, sp: impl ToSpanData) -> Option<&str> { + self.get_text(self.span_to_file_range(sp)) + } + + /// Gets a subslice of the file's text. Returns `None` if the range is invalid. + /// + /// With debug assertions this will validate that the range: + /// * Is contained within the current file. + /// * The start and end do not overlap. + /// * Lies on UTF-8 boundaries. + #[inline] + #[must_use] + pub fn get_text_by_src_range(&self, range: Range) -> Option<&str> { + self.get_text(self.src_to_file_range(range)) + } + + /// Gets the `SyntaxContext` this was created with. + #[inline] + #[must_use] + pub fn ctxt(&self) -> SyntaxContext { + self.ctxt + } + + /// Checks if this file contains the specified `SourceMap` position. + #[inline] + #[must_use] + pub fn contains_pos(&self, pos: BytePos) -> bool { + self.file().contains(pos) + } + + /// Converts the file range into a `SourceMap` range. The previous range can be provided + /// additional context for the debug assertions. + /// + /// With debug assertions this will validate that the range: + /// * Is contained within the current file. + /// * The start and end do not overlap. + /// * Lies on UTF-8 boundaries. + #[inline] + #[must_use] + #[cfg_attr(debug_assertions, track_caller)] + pub fn mk_source_range(&self, range: FileRange, old: Option) -> Range { + self.dbg_check_range(old, range.clone()); + let offset = self.file().start_pos.0; + BytePos(range.start.0.wrapping_add(offset))..BytePos(range.end.0.wrapping_add(offset)) + } + + /// Converts the file range into a `Span`. The previous range can be provided additional context + /// for the debug assertions. + /// + /// With debug assertions this will validate that the range: + /// * Is contained within the current file. + /// * The start and end do not overlap. + /// * Lies on UTF-8 boundaries. + #[inline] + #[must_use] + #[cfg_attr(debug_assertions, track_caller)] + pub fn mk_span(&self, range: FileRange, old: Option) -> Span { + let range = self.mk_source_range(range, old); + Span::new(range.start, range.end, self.ctxt, self.parent) + } + + /// Converts the span into a file range. + /// + /// With debug assertions this will validate that the span: + /// * Is from the same syntax context. + /// * Is contained within the current file. + /// * The start and end do not overlap. + /// * Lies on UTF-8 boundaries. + #[inline] + #[must_use] + #[cfg_attr(debug_assertions, track_caller)] + pub fn span_to_file_range(&self, sp: impl ToSpanData) -> FileRange { + let data = sp.data(); + debug_assert_eq!(self.ctxt, data.ctxt); + self.src_to_file_range(data.lo..data.hi) + } + + /// Converts the `SourceMap` range into a file range. + /// + /// With debug assertions this will validate that the range: + /// * Is contained within the current file. + /// * The start and end do not overlap. + /// * Lies on UTF-8 boundaries. + #[inline] + #[must_use] + #[cfg_attr(debug_assertions, track_caller)] + pub fn src_to_file_range(&self, range: Range) -> FileRange { + let offset = self.file().start_pos.0; + let range = + RelativeBytePos(range.start.0.wrapping_sub(offset))..RelativeBytePos(range.end.0.wrapping_sub(offset)); + self.dbg_check_range(None, range.clone()); + range + } + + /// Gets the indent text of the line containing the specified position. Returns `None` if the + /// position is outside the file's text. + /// + /// If the position is inside the line indent only the indent up to the position will be + /// retrieved. + #[must_use] + pub fn get_line_indent_before(&self, pos: RelativeBytePos) -> Option<&str> { + let file = self.file(); + let lines = file.lines(); + + // `lines` either starts with zero or is empty. If it's empty we can use zero as the line + // start. + let line = lines.partition_point(|&start| start <= pos); + let start = lines.get(line.wrapping_sub(1)).map_or(RelativeBytePos(0), |&x| x); + self.get_text(start..pos) + .map(|src| &src[..src.len() - src.trim_start_matches(is_whitespace).len()]) + } + + /// Runs debug checks on a range, panicking on failure. Does nothing if debug assertions are + /// disabled. + /// + /// A second range can be given as a previous range before a transformation occurred. This will + /// be displayed as additional context in the panic message, but will not cause additional + /// validation. + #[inline] + #[cfg_attr(debug_assertions, track_caller)] + fn dbg_check_range(&self, old: Option, new: FileRange) { + dbg_check_range(&self.text, old, new); + } +} + +/// A collection of helper functions for adjusting a range within a file. +pub trait FileRangeExt: Sized + FileIndex { + /// If the range doesn't overlap with the specified span returns the range between the two. + /// Returns `None` otherwise. + /// + /// With debug assertions enabled this will assert that the span: + /// * Is within the same `SyntaxContext` + /// * Is within the same file as the current range. + /// * Lies on a UTF-8 boundary. + #[inline] + #[must_use] + #[cfg_attr(debug_assertions, track_caller)] + fn get_range_between(self, scx: &SpanEditCx, other: impl ToSpanData) -> Option { + ::get_range_between(self.into_file_range(scx), scx, other) + } + + /// If the range starts at or after the specified position returns the range from that position + /// to the end of the range. Returns `None` otherwise. + /// + /// With debug assertions enabled this will assert that the position: + /// * Is within the same `SyntaxContext` + /// * Is within the same file. + /// * Lies on a UTF-8 boundary. + #[inline] + #[must_use] + #[cfg_attr(debug_assertions, track_caller)] + fn extend_start_to(self, scx: &SpanEditCx, pos: PosWithCtxt) -> Option { + ::extend_start_to(self.into_file_range(scx), scx, pos) + } + + /// If the range ends before or at the specified position returns the range from the start of + /// the range to that position. Returns `None` otherwise. + /// + /// With debug assertions enabled this will assert that the position: + /// * Is within the same `SyntaxContext` + /// * Is within the same file. + /// * Lies on a UTF-8 boundary. + #[inline] + #[must_use] + #[cfg_attr(debug_assertions, track_caller)] + fn extend_end_to(self, scx: &SpanEditCx, pos: PosWithCtxt) -> Option { + ::extend_end_to(self.into_file_range(scx), scx, pos) + } + + /// If the specified position lies within or at the end of range returns the range from that + /// position to the end of the range. Returns `None` otherwise. + /// + /// With debug assertions enabled this will assert that the position: + /// * Is within the same `SyntaxContext` + /// * Is within the same file. + /// * Lies on a UTF-8 boundary. + #[inline] + #[must_use] + #[cfg_attr(debug_assertions, track_caller)] + fn shrink_start_to(self, scx: &SpanEditCx, pos: PosWithCtxt) -> Option { + ::shrink_start_to(self.into_file_range(scx), scx, pos) + } + + /// If the specified position lies within or at the end of range returns the range from the + /// start of the range to that position. Returns `None` otherwise. + /// + /// With debug assertions enabled this will assert that the position: + /// * Is within the same `SyntaxContext` + /// * Is within the same file. + /// * Lies on a UTF-8 boundary. + #[inline] + #[must_use] + #[cfg_attr(debug_assertions, track_caller)] + fn shrink_end_to(self, scx: &SpanEditCx, pos: PosWithCtxt) -> Option { + ::shrink_end_to(self.into_file_range(scx), scx, pos) + } + + /// Creates a new file range that represents the result of mapping the text of the specified + /// range into a substring. Returns `None` if either the mapping function returns `None`, or the + /// range cannot index the file's text. + /// + /// The string returned by the mapping function must be derived from the input string. A + /// `'static` lifetime string will not work. This case will panic if debug assertions are + /// enabled. + #[inline] + #[must_use] + #[cfg_attr(debug_assertions, track_caller)] + fn map_range_text(self, scx: &SpanEditCx, f: impl FnOnce(&str) -> Option<&str>) -> Option { + ::map_range_text(self.into_file_range(scx), scx, f) + } + + /// Creates a new array of file ranges that represents the result of mapping the text of the + /// specified range into an array of substrings. Returns `None` if either the mapping function + /// returns `None`, or the range cannot index the file's text. + /// + /// The strings returned by the mapping function must be derived from the input string. + /// `'static` lifetime strings will not work. This case will panic if debug assertions are + /// enabled. + #[inline] + #[must_use] + #[cfg_attr(debug_assertions, track_caller)] + fn map_split_range_text( + self, + scx: &SpanEditCx, + f: impl FnOnce(&str) -> Option<[&str; N]>, + ) -> Option<[FileRange; N]> { + ::map_split_range_text(self.into_file_range(scx), scx, f) + } + + /// Extends the range to include all immediately preceding whitespace. Returns `None` if the + /// range cannot index the file's text. + /// + /// The range will not be expanded if it would cross a line boundary, the line the range would + /// be extended to ends with a line comment and the text after the range contains a + /// non-whitespace character on the same line. e.g. + /// + /// ```ignore + /// ( // Some comment + /// foo) + /// ``` + /// + /// When the range points to `foo`, suggesting to remove the range after it's been extended will + /// cause the `)` to be placed inside the line comment as `( // Some comment)`. + #[inline] + #[must_use] + fn with_leading_whitespace(self, scx: &SpanEditCx) -> Option { + ::with_leading_whitespace(self.into_file_range(scx), scx) + } + + /// Extends the range to include all immediately proceeding whitespace. Returns `None` if the + /// range cannot index the file's text. + #[inline] + #[must_use] + fn with_trailing_whitespace(self, scx: &SpanEditCx) -> Option { + ::with_trailing_whitespace(self.into_file_range(scx), scx) + } + + // Extends the range to include the immediately preceding pattern. Returns `None` if the pattern + // does not immediately precede the range, or if the range cannot index the file's text. + #[inline] + #[must_use] + fn with_leading_match

(self, scx: &SpanEditCx, pat: P) -> Option + where + P: Pattern, + for<'a> P::Searcher<'a>: ReverseSearcher<'a>, { - Some(f(src)) - } else { - None + ::with_leading_match(self.into_file_range(scx), scx, pat) + } + + // Extends the range to include the immediately proceeding pattern. Returns `None` if the pattern + // does not immediately proceed the range, or if the range cannot index the file's text. + #[inline] + #[must_use] + fn with_trailing_match(self, scx: &SpanEditCx, pat: impl Pattern) -> Option { + ::with_trailing_match(self.into_file_range(scx), scx, pat) } } +impl FileRangeExt for FileRange { + #[inline] + #[cfg_attr(debug_assertions, track_caller)] + fn get_range_between(self, scx: &SpanEditCx, sp: impl ToSpanData) -> Option { + #[inline] + #[cfg_attr(debug_assertions, track_caller)] + fn f(self_: FileRange, scx: &SpanEditCx, sp: SpanData) -> Option { + debug_assert_eq!(scx.ctxt, sp.ctxt); + let file = scx.file(); + let other = RelativeBytePos(sp.lo.0.wrapping_sub(file.start_pos.0)) + ..RelativeBytePos(sp.hi.0.wrapping_sub(file.start_pos.0)); + scx.dbg_check_range(None, other.clone()); + if self_.end.0 <= other.start.0 { + Some(self_.end..other.start) + } else if self_.start.0 >= other.end.0 { + Some(other.end..self_.start) + } else { + None + } + } + f(self, scx, sp.data()) + } -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_range(sm, sp) - && let Some(text) = &src.sf.src + #[inline] + #[cfg_attr(debug_assertions, track_caller)] + fn extend_start_to(self, scx: &SpanEditCx, pos: PosWithCtxt) -> Option { + debug_assert_eq!(scx.ctxt, pos.ctxt); + let file = scx.file(); + let pos = RelativeBytePos(pos.pos.0.wrapping_sub(file.start_pos.0)); + scx.dbg_check_range(None, pos..pos); + (pos <= self.start).then_some(pos..self.end) + } + + #[inline] + #[cfg_attr(debug_assertions, track_caller)] + fn extend_end_to(self, scx: &SpanEditCx, pos: PosWithCtxt) -> Option { + debug_assert_eq!(scx.ctxt, pos.ctxt); + let file = scx.file(); + let pos = RelativeBytePos(pos.pos.0.wrapping_sub(file.start_pos.0)); + scx.dbg_check_range(None, pos..pos); + (pos >= self.end).then_some(self.start..pos) + } + + #[inline] + #[cfg_attr(debug_assertions, track_caller)] + fn shrink_start_to(self, scx: &SpanEditCx, pos: PosWithCtxt) -> Option { + debug_assert_eq!(scx.ctxt, pos.ctxt); + let file = scx.file(); + let pos = RelativeBytePos(pos.pos.0.wrapping_sub(file.start_pos.0)); + scx.dbg_check_range(None, pos..pos); + (self.start <= pos && pos <= self.end).then_some(pos..self.end) + } + + #[inline] + #[cfg_attr(debug_assertions, track_caller)] + fn shrink_end_to(self, scx: &SpanEditCx, pos: PosWithCtxt) -> Option { + debug_assert_eq!(scx.ctxt, pos.ctxt); + let file = scx.file(); + let pos = RelativeBytePos(pos.pos.0.wrapping_sub(file.start_pos.0)); + scx.dbg_check_range(None, pos..pos); + (self.start <= pos && pos <= self.end).then_some(self.start..pos) + } + + #[inline] + #[cfg_attr(debug_assertions, track_caller)] + #[allow(clippy::manual_map, reason = "track_caller doesn't work through `map`")] + fn map_range_text(self, scx: &SpanEditCx, f: impl FnOnce(&str) -> Option<&str>) -> Option { + let src = scx.text.as_str(); + match src.get(self.start.to_usize()..self.end.to_usize()).and_then(f) { + Some(s) => { + let base = src.as_ptr().addr(); + debug_assert!( + base <= s.as_ptr().addr() && s.as_ptr().addr() + s.len() <= base + src.len(), + "the string is not a valid substring", + ); + let start = s.as_ptr().addr() - base; + Some(RelativeBytePos::from_usize(start)..RelativeBytePos::from_usize(start + s.len())) + }, + None => None, + } + } + + #[inline] + #[cfg_attr(debug_assertions, track_caller)] + #[allow(clippy::manual_map, reason = "track_caller doesn't work through `map`")] + fn map_split_range_text( + self, + scx: &SpanEditCx, + f: impl FnOnce(&str) -> Option<[&str; N]>, + ) -> Option<[FileRange; N]> { + let src = scx.text.as_str(); + match src.get(self.start.to_usize()..self.end.to_usize()).and_then(f) { + Some(s) => { + let base = src.as_ptr().addr(); + Some(map_array_inline!(s, s => { + debug_assert!( + base <= s.as_ptr().addr() && s.as_ptr().addr() + s.len() <= base + src.len(), + "the string is not a valid substring", + ); + let start = s.as_ptr().addr() - base; + RelativeBytePos::from_usize(start)..RelativeBytePos::from_usize(start + s.len()) + })) + }, + None => None, + } + } + + fn with_leading_whitespace(self, scx: &SpanEditCx) -> Option { + let src = scx.file_text(); + let sf = scx.file(); + + let mut trimmed_lf = false; + let text_before = src.get(..self.start.to_usize())?.trim_end_matches(|c: char| { + trimmed_lf |= c == '\n'; + is_whitespace(c) + }); + if trimmed_lf + && let line_starts = sf.lines() + && let post_search_line = line_starts.partition_point(|&pos| pos.to_usize() <= text_before.len()) + // `get` can fail if `line_starts` is missing the starting zero. + // Just start the search at the beginning in that case. + && let search_start = line_starts.get(post_search_line - 1).map_or(0, |&x| x.to_usize()) + && ends_with_line_comment_or_broken(&text_before[search_start..]) + // Is there anything after the range on the same line? + && !src.get(self.end.to_usize()..)?.chars().take_while(|&c| c != '\n').all(is_whitespace) + { + Some(self) + } else { + Some(RelativeBytePos::from_usize(text_before.len())..self.end) + } + } + + fn with_trailing_whitespace(self, scx: &SpanEditCx) -> Option { + scx.get_text(self.end..).map(|s| { + self.start..RelativeBytePos::from_usize(scx.text.len() - s.trim_start_matches(is_whitespace).len()) + }) + } + + fn with_leading_match

(self, scx: &SpanEditCx, pat: P) -> Option + where + P: Pattern, + for<'a> P::Searcher<'a>: ReverseSearcher<'a>, { - Some(f(text, src.range)) - } else { - None + scx.get_text(..self.start) + .and_then(|s| s.strip_suffix(pat)) + .map(|s| RelativeBytePos::from_usize(s.len())..self.end) + } + + fn with_trailing_match(self, scx: &SpanEditCx, pat: impl Pattern) -> Option { + scx.get_text(self.end..) + .and_then(|s| s.strip_prefix(pat)) + .map(|s| self.start..RelativeBytePos::from_usize(scx.text.len() - s.len())) } } +impl FileRangeExt for RangeFull {} +impl FileRangeExt for RangeTo {} +impl FileRangeExt for RangeFrom {} -#[expect(clippy::cast_possible_truncation)] -fn map_range( - sm: &SourceMap, - sp: Range, - f: impl for<'a> FnOnce(&'a SourceFile, &'a str, Range) -> Option>, -) -> Option> { - if let Some(src) = get_source_range(sm, sp.clone()) - && let Some(text) = &src.sf.src - && let Some(range) = f(&src.sf, text, src.range.clone()) +pub trait StrExt { + /// Gets the substring which ranges from the start of the first match of the pattern to the end + /// of the second match. Returns `None` if the pattern doesn't occur twice. + /// + /// # Examples + /// ```rust + /// # #![feature(rustc_private)] + /// # use clippy_utils::source::StrExt; + /// let s = "move |arg| arg.foo()"; + /// assert_eq!(s.find_bounded_inclusive('|'), Some("|arg|")); + /// + /// let s = "foo | bar"; + /// assert_eq!(s.find_bounded_inclusive('|'), None) + /// ``` + fn find_bounded_inclusive(&self, pat: impl Pattern) -> Option<&Self>; + + /// Gets the non-overlapping prefix and suffix. Returns `None` if the string doesn't start with + /// the prefix or end with the suffix. + /// + /// The prefix will be taken first, with the suffix taken from the remainder of the string. + /// + /// # Examples + /// ```rust + /// # #![feature(rustc_private)] + /// # use clippy_utils::source::StrExt; + /// let s = "[a, b, c]"; + /// assert_eq!(s.get_prefix_suffix('[', ']'), Some(["[", "]"])); + /// ``` + fn get_prefix_suffix

(&self, prefix: impl Pattern, suffix: P) -> Option<[&Self; 2]> + where + P: Pattern, + for<'a> P::Searcher<'a>: ReverseSearcher<'a>; + + /// Splits a string into a prefix and everything proceeding it. Returns `None` if the string + /// doesn't start with the prefix. + /// + /// # Examples + /// ```rust + /// # #![feature(rustc_private)] + /// # use clippy_utils::source::StrExt; + /// let s = "fn foo()"; + /// assert_eq!(s.split_prefix("fn"), Some(["fn", " foo()"])); + /// ``` + fn split_prefix(&self, pat: impl Pattern) -> Option<[&Self; 2]>; + + /// Splits a string into a suffix and everything preceding it. Returns `None` if the string + /// doesn't end with the suffix. + /// + /// # Examples + /// ```rust + /// # #![feature(rustc_private)] + /// # use clippy_utils::source::StrExt; + /// let s = "foo.bar()?"; + /// assert_eq!(s.split_suffix('?'), Some(["foo.bar()", "?"])); + /// ``` + fn split_suffix

(&self, pat: P) -> Option<[&Self; 2]> + where + P: Pattern, + for<'a> P::Searcher<'a>: ReverseSearcher<'a>; +} +impl StrExt for str { + fn find_bounded_inclusive(&self, pat: impl Pattern) -> Option<&Self> { + let mut iter = self.match_indices(pat); + if let Some((first_pos, _)) = iter.next() + && let Some((second_pos, second)) = iter.next() + { + Some(&self[first_pos..second_pos + second.len()]) + } else { + None + } + } + + fn get_prefix_suffix

(&self, prefix: impl Pattern, suffix: P) -> Option<[&Self; 2]> + where + P: Pattern, + for<'a> P::Searcher<'a>: ReverseSearcher<'a>, { - debug_assert!( - range.start <= text.len() && range.end <= text.len(), - "Range `{range:?}` is outside the source file (file `{}`, length `{}`)", - src.sf.name.prefer_local_unconditionally(), - 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 + if let Some([pre, s]) = self.split_prefix(prefix) + && let Some([_, suf]) = s.split_suffix(suffix) + { + Some([pre, suf]) + } else { + None + } + } + + #[inline] + fn split_prefix(&self, pat: impl Pattern) -> Option<[&Self; 2]> { + self.strip_prefix(pat) + .map(|rest| [&self[..self.len() - rest.len()], rest]) + } + + #[inline] + fn split_suffix

(&self, pat: P) -> Option<[&Self; 2]> + where + P: Pattern, + for<'a> P::Searcher<'a>: ReverseSearcher<'a>, + { + self.strip_suffix(pat).map(|rest| [rest, &self[rest.len()..]]) } } +/// Checks if the last token of the string is either a line comment or an incomplete token. fn ends_with_line_comment_or_broken(text: &str) -> bool { let Some(last) = tokenize(text, FrontmatterAllowed::No).last() else { return false; @@ -314,55 +1421,6 @@ fn ends_with_line_comment_or_broken(text: &str) -> bool { } } -fn with_leading_whitespace_inner(lines: &[RelativeBytePos], src: &str, range: Range) -> Option { - debug_assert!(lines.is_empty() || lines[0].to_u32() == 0); - - let start = src.get(..range.start)?.trim_end(); - let next_line = lines.partition_point(|&pos| pos.to_usize() <= start.len()); - if let Some(line_end) = lines.get(next_line) - && line_end.to_usize() <= range.start - && let prev_start = lines.get(next_line - 1).map_or(0, |&x| x.to_usize()) - && ends_with_line_comment_or_broken(&start[prev_start..]) - && let next_line = lines.partition_point(|&pos| pos.to_usize() < range.end) - && let next_start = lines.get(next_line).map_or(src.len(), |&x| x.to_usize()) - && tokenize(src.get(range.end..next_start)?, FrontmatterAllowed::No) - .any(|t| !matches!(t.kind, TokenKind::Whitespace)) - { - Some(range.start) - } else { - Some(start.len()) - } -} - -fn with_leading_whitespace(sm: &SourceMap, sp: Range) -> Range { - map_range(sm, sp.clone(), |sf, src, range| { - Some(with_leading_whitespace_inner(sf.lines(), src, range.clone())?..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) -} - -pub struct SourceFileRange { - pub sf: Arc, - pub range: Range, -} -impl SourceFileRange { - /// Attempts to get the text from the source file. This can fail if the source text isn't - /// loaded. - pub fn as_str(&self) -> Option<&str> { - (self.sf.src.as_ref().map(|src| src.as_str())) - .or_else(|| self.sf.external_src.get()?.get_source()) - .and_then(|x| x.get(self.range.clone())) - } -} - /// Like [`snippet_block`], but add braces if the expr is not an `ExprKind::Block` with no label. pub fn expr_block<'sm>( sm: impl HasSourceMap<'sm>,