Critical: Use of 'unsafe' keyword bypasses Rust's safety guarantees. Requires careful auditing, clear justification (FFI, specific optimizations), and minimal scope.
// highlight unsafe keyword itself
1use std::iter;23use hir::{EditionedFileId, FilePosition, FileRange, HirFileId, InFile, Semantics, db};4use ide_db::{5 FxHashMap, FxHashSet, RootDatabase,6 defs::{Definition, IdentClass},7 helpers::pick_best_token,8 search::{FileReference, ReferenceCategory, SearchScope},9 syntax_helpers::node_ext::{10 eq_label_lt, find_loops, for_each_tail_expr, full_path_of_name_ref,11 is_closure_or_blk_with_modif, preorder_expr_with_ctx_checker,12 },13};14use syntax::{15 AstNode,16 SyntaxKind::{self, IDENT, INT_NUMBER},17 SyntaxToken, T, TextRange, WalkEvent,18 ast::{self, HasLoopBody},19 match_ast,20};2122use crate::{NavigationTarget, TryToNav, goto_definition, navigation_target::ToNav};2324#[derive(PartialEq, Eq, Hash)]25pub struct HighlightedRange {26 pub range: TextRange,27 // FIXME: This needs to be more precise. Reference category makes sense only28 // for references, but we also have defs. And things like exit points are29 // neither.30 pub category: ReferenceCategory,31}3233#[derive(Default, Clone)]34pub struct HighlightRelatedConfig {35 pub references: bool,36 pub exit_points: bool,37 pub break_points: bool,38 pub closure_captures: bool,39 pub yield_points: bool,40 pub branch_exit_points: bool,41}4243type HighlightMap = FxHashMap<EditionedFileId, FxHashSet<HighlightedRange>>;4445// Feature: Highlight Related46//47// Highlights constructs related to the thing under the cursor:48//49// 1. if on an identifier, highlights all references to that identifier in the current file50// * additionally, if the identifier is a trait in a where clause, type parameter trait bound or use item, highlights all references to that trait's assoc items in the corresponding scope51// 1. if on an `async` or `await` token, highlights all yield points for that async context52// 1. if on a `return` or `fn` keyword, `?` character or `->` return type arrow, highlights all exit points for that context53// 1. if on a `break`, `loop`, `while` or `for` token, highlights all break points for that loop or block context54// 1. if on a `move` or `|` token that belongs to a closure, highlights all captures of the closure.55//56// Note: `?`, `|` and `->` do not currently trigger this behavior in the VSCode editor.57pub(crate) fn highlight_related(58 sema: &Semantics<'_, RootDatabase>,59 config: HighlightRelatedConfig,60 ide_db::FilePosition { offset, file_id }: ide_db::FilePosition,61) -> Option<Vec<HighlightedRange>> {62 let _p = tracing::info_span!("highlight_related").entered();63 let file_id = sema.attach_first_edition(file_id);64 let syntax = sema.parse(file_id).syntax().clone();6566 let token = pick_best_token(syntax.token_at_offset(offset), |kind| match kind {67 T![?] => 4, // prefer `?` when the cursor is sandwiched like in `await$0?`68 T![->] | T![=>] => 4,69 kind if kind.is_keyword(file_id.edition(sema.db)) => 3,70 IDENT | INT_NUMBER => 2,71 T![|] => 1,72 _ => 0,73 })?;74 // most if not all of these should be re-implemented with information seeded from hir75 match token.kind() {76 T![?] if config.exit_points && token.parent().and_then(ast::TryExpr::cast).is_some() => {77 highlight_exit_points(sema, token).remove(&file_id)78 }79 T![fn] | T![return] | T![->] if config.exit_points => {80 highlight_exit_points(sema, token).remove(&file_id)81 }82 T![match] | T![=>] | T![if] if config.branch_exit_points => {83 highlight_branch_exit_points(sema, token).remove(&file_id)84 }85 T![await] | T![async] if config.yield_points => {86 highlight_yield_points(sema, token).remove(&file_id)87 }88 T![for] if config.break_points && token.parent().and_then(ast::ForExpr::cast).is_some() => {89 highlight_break_points(sema, token).remove(&file_id)90 }91 T![break] | T![loop] | T![while] | T![continue] if config.break_points => {92 highlight_break_points(sema, token).remove(&file_id)93 }94 T![unsafe] if token.parent().and_then(ast::BlockExpr::cast).is_some() => {95 highlight_unsafe_points(sema, token).remove(&file_id)96 }97 T![|] if config.closure_captures => highlight_closure_captures(sema, token, file_id),98 T![move] if config.closure_captures => highlight_closure_captures(sema, token, file_id),99 _ if config.references => {100 highlight_references(sema, token, FilePosition { file_id, offset })101 }102 _ => None,103 }104}105106fn highlight_closure_captures(107 sema: &Semantics<'_, RootDatabase>,108 token: SyntaxToken,109 file_id: EditionedFileId,110) -> Option<Vec<HighlightedRange>> {111 let closure = token.parent_ancestors().take(2).find_map(ast::ClosureExpr::cast)?;112 let search_range = closure.body()?.syntax().text_range();113 let ty = &sema.type_of_expr(&closure.into())?.original;114 let c = ty.as_closure()?;115 Some(116 c.captured_items(sema.db)117 .into_iter()118 .map(|capture| capture.local())119 .flat_map(|local| {120 let usages = Definition::Local(local)121 .usages(sema)122 .in_scope(&SearchScope::file_range(FileRange { file_id, range: search_range }))123 .include_self_refs()124 .all()125 .references126 .remove(&file_id)127 .into_iter()128 .flatten()129 .map(|FileReference { category, range, .. }| HighlightedRange {130 range,131 category,132 });133 let category = if local.is_mut(sema.db) {134 ReferenceCategory::WRITE135 } else {136 ReferenceCategory::empty()137 };138 local139 .sources(sema.db)140 .into_iter()141 .flat_map(|x| x.to_nav(sema.db))142 .filter(|decl| decl.file_id == file_id.file_id(sema.db))143 .filter_map(|decl| decl.focus_range)144 .map(move |range| HighlightedRange { range, category })145 .chain(usages)146 })147 .collect(),148 )149}150151fn highlight_references(152 sema: &Semantics<'_, RootDatabase>,153 token: SyntaxToken,154 FilePosition { file_id, offset }: FilePosition,155) -> Option<Vec<HighlightedRange>> {156 let defs = if let Some((range, _, _, resolution)) =157 sema.check_for_format_args_template(token.clone(), offset)158 {159 match resolution.map(Definition::from) {160 Some(def) => iter::once(def).collect(),161 None => {162 return Some(vec![HighlightedRange {163 range,164 category: ReferenceCategory::empty(),165 }]);166 }167 }168 } else {169 find_defs(sema, token.clone())170 };171 let usages = defs172 .iter()173 .filter_map(|&d| {174 d.usages(sema)175 .in_scope(&SearchScope::single_file(file_id))176 .include_self_refs()177 .all()178 .references179 .remove(&file_id)180 })181 .flatten()182 .map(|FileReference { category, range, .. }| HighlightedRange { range, category });183 let mut res = FxHashSet::default();184 for &def in &defs {185 // highlight trait usages186 if let Definition::Trait(t) = def {187 let trait_item_use_scope = (|| {188 let name_ref = token.parent().and_then(ast::NameRef::cast)?;189 let path = full_path_of_name_ref(&name_ref)?;190 let parent = path.syntax().parent()?;191 match_ast! {192 match parent {193 ast::UseTree(it) => it.syntax().ancestors().find(|it| {194 ast::SourceFile::can_cast(it.kind()) || ast::Module::can_cast(it.kind())195 }).zip(Some(true)),196 ast::PathType(it) => it197 .syntax()198 .ancestors()199 .nth(2)200 .and_then(ast::TypeBoundList::cast)?201 .syntax()202 .parent()203 .filter(|it| ast::WhereClause::can_cast(it.kind()) || ast::TypeParam::can_cast(it.kind()))?204 .ancestors()205 .find(|it| {206 ast::Item::can_cast(it.kind())207 }).zip(Some(false)),208 _ => None,209 }210 }211 })();212 if let Some((trait_item_use_scope, use_tree)) = trait_item_use_scope {213 res.extend(214 if use_tree { t.items(sema.db) } else { t.items_with_supertraits(sema.db) }215 .into_iter()216 .filter_map(|item| {217 Definition::from(item)218 .usages(sema)219 .set_scope(Some(&SearchScope::file_range(FileRange {220 file_id,221 range: trait_item_use_scope.text_range(),222 })))223 .include_self_refs()224 .all()225 .references226 .remove(&file_id)227 })228 .flatten()229 .map(|FileReference { category, range, .. }| HighlightedRange {230 range,231 category,232 }),233 );234 }235 }236237 // highlight the tail expr of the labelled block238 if matches!(def, Definition::Label(_)) {239 let label = token.parent_ancestors().nth(1).and_then(ast::Label::cast);240 if let Some(block) =241 label.and_then(|label| label.syntax().parent()).and_then(ast::BlockExpr::cast)242 {243 for_each_tail_expr(&block.into(), &mut |tail| {244 if !matches!(tail, ast::Expr::BreakExpr(_)) {245 res.insert(HighlightedRange {246 range: tail.syntax().text_range(),247 category: ReferenceCategory::empty(),248 });249 }250 });251 }252 }253254 // highlight the defs themselves255 match def {256 Definition::Local(local) => {257 let category = if local.is_mut(sema.db) {258 ReferenceCategory::WRITE259 } else {260 ReferenceCategory::empty()261 };262 local263 .sources(sema.db)264 .into_iter()265 .flat_map(|x| x.to_nav(sema.db))266 .filter(|decl| decl.file_id == file_id.file_id(sema.db))267 .filter_map(|decl| decl.focus_range)268 .map(|range| HighlightedRange { range, category })269 .for_each(|x| {270 res.insert(x);271 });272 }273 def => {274 let navs = match def {275 Definition::Module(module) => {276 NavigationTarget::from_module_to_decl(sema.db, module)277 }278 def => match def.try_to_nav(sema) {279 Some(it) => it,280 None => continue,281 },282 };283 for nav in navs {284 if nav.file_id != file_id.file_id(sema.db) {285 continue;286 }287 let hl_range = nav.focus_range.map(|range| {288 let category = if matches!(def, Definition::Local(l) if l.is_mut(sema.db)) {289 ReferenceCategory::WRITE290 } else {291 ReferenceCategory::empty()292 };293 HighlightedRange { range, category }294 });295 if let Some(hl_range) = hl_range {296 res.insert(hl_range);297 }298 }299 }300 }301 }302303 res.extend(usages);304 if res.is_empty() { None } else { Some(res.into_iter().collect()) }305}306307pub(crate) fn highlight_branch_exit_points(308 sema: &Semantics<'_, RootDatabase>,309 token: SyntaxToken,310) -> FxHashMap<EditionedFileId, Vec<HighlightedRange>> {311 let mut highlights: HighlightMap = FxHashMap::default();312313 let push_to_highlights = |file_id, range, highlights: &mut HighlightMap| {314 if let Some(FileRange { file_id, range }) = original_frange(sema.db, file_id, range) {315 let hrange = HighlightedRange { category: ReferenceCategory::empty(), range };316 highlights.entry(file_id).or_default().insert(hrange);317 }318 };319320 let push_tail_expr = |tail: Option<ast::Expr>, highlights: &mut HighlightMap| {321 let Some(tail) = tail else {322 return;323 };324325 for_each_tail_expr(&tail, &mut |tail| {326 let file_id = sema.hir_file_for(tail.syntax());327 let range = tail.syntax().text_range();328 push_to_highlights(file_id, Some(range), highlights);329 });330 };331332 let nodes = goto_definition::find_branch_root(sema, &token).into_iter();333 match token.kind() {334 T![match] => {335 for match_expr in nodes.filter_map(ast::MatchExpr::cast) {336 let file_id = sema.hir_file_for(match_expr.syntax());337 let range = match_expr.match_token().map(|token| token.text_range());338 push_to_highlights(file_id, range, &mut highlights);339340 let Some(arm_list) = match_expr.match_arm_list() else {341 continue;342 };343 for arm in arm_list.arms() {344 push_tail_expr(arm.expr(), &mut highlights);345 }346 }347 }348 T![=>] => {349 for arm in nodes.filter_map(ast::MatchArm::cast) {350 let file_id = sema.hir_file_for(arm.syntax());351 let range = arm.fat_arrow_token().map(|token| token.text_range());352 push_to_highlights(file_id, range, &mut highlights);353354 push_tail_expr(arm.expr(), &mut highlights);355 }356 }357 T![if] => {358 for mut if_to_process in nodes.map(ast::IfExpr::cast) {359 while let Some(cur_if) = if_to_process.take() {360 let file_id = sema.hir_file_for(cur_if.syntax());361362 let if_kw_range = cur_if.if_token().map(|token| token.text_range());363 push_to_highlights(file_id, if_kw_range, &mut highlights);364365 if let Some(then_block) = cur_if.then_branch() {366 push_tail_expr(Some(then_block.into()), &mut highlights);367 }368369 match cur_if.else_branch() {370 Some(ast::ElseBranch::Block(else_block)) => {371 push_tail_expr(Some(else_block.into()), &mut highlights);372 if_to_process = None;373 }374 Some(ast::ElseBranch::IfExpr(nested_if)) => if_to_process = Some(nested_if),375 None => if_to_process = None,376 }377 }378 }379 }380 _ => {}381 }382383 highlights384 .into_iter()385 .map(|(file_id, ranges)| (file_id, ranges.into_iter().collect()))386 .collect()387}388389fn hl_exit_points(390 sema: &Semantics<'_, RootDatabase>,391 def_token: Option<SyntaxToken>,392 body: ast::Expr,393) -> Option<HighlightMap> {394 let mut highlights: FxHashMap<EditionedFileId, FxHashSet<_>> = FxHashMap::default();395396 let mut push_to_highlights = |file_id, range| {397 if let Some(FileRange { file_id, range }) = original_frange(sema.db, file_id, range) {398 let hrange = HighlightedRange { category: ReferenceCategory::empty(), range };399 highlights.entry(file_id).or_default().insert(hrange);400 }401 };402403 if let Some(tok) = def_token {404 let file_id = sema.hir_file_for(&tok.parent()?);405 let range = Some(tok.text_range());406 push_to_highlights(file_id, range);407 }408409 WalkExpandedExprCtx::new(sema).walk(&body, &mut |_, expr| {410 let file_id = sema.hir_file_for(expr.syntax());411412 let range = match &expr {413 ast::Expr::TryExpr(try_) => try_.question_mark_token().map(|token| token.text_range()),414 ast::Expr::MethodCallExpr(_) | ast::Expr::CallExpr(_) | ast::Expr::MacroExpr(_)415 if sema.type_of_expr(&expr).is_some_and(|ty| ty.original.is_never()) =>416 {417 Some(expr.syntax().text_range())418 }419 _ => None,420 };421422 push_to_highlights(file_id, range);423 });424425 // We should handle `return` separately, because when it is used in a `try` block,426 // it will exit the outside function instead of the block itself.427 WalkExpandedExprCtx::new(sema)428 .with_check_ctx(&WalkExpandedExprCtx::is_async_const_block_or_closure)429 .walk(&body, &mut |_, expr| {430 let file_id = sema.hir_file_for(expr.syntax());431432 let range = match &expr {433 ast::Expr::ReturnExpr(expr) => expr.return_token().map(|token| token.text_range()),434 _ => None,435 };436437 push_to_highlights(file_id, range);438 });439440 let tail = match body {441 ast::Expr::BlockExpr(b) => b.tail_expr(),442 e => Some(e),443 };444445 if let Some(tail) = tail {446 for_each_tail_expr(&tail, &mut |tail| {447 let file_id = sema.hir_file_for(tail.syntax());448 let range = match tail {449 ast::Expr::BreakExpr(b) => b450 .break_token()451 .map_or_else(|| tail.syntax().text_range(), |tok| tok.text_range()),452 _ => tail.syntax().text_range(),453 };454 push_to_highlights(file_id, Some(range));455 });456 }457 Some(highlights)458}459460// If `file_id` is None,461pub(crate) fn highlight_exit_points(462 sema: &Semantics<'_, RootDatabase>,463 token: SyntaxToken,464) -> FxHashMap<EditionedFileId, Vec<HighlightedRange>> {465 let mut res = FxHashMap::default();466 for def in goto_definition::find_fn_or_blocks(sema, &token) {467 let new_map = match_ast! {468 match def {469 ast::Fn(fn_) => fn_.body().and_then(|body| hl_exit_points(sema, fn_.fn_token(), body.into())),470 ast::ClosureExpr(closure) => {471 let pipe_tok = closure.param_list().and_then(|p| p.pipe_token());472 closure.body().and_then(|body| hl_exit_points(sema, pipe_tok, body))473 },474 ast::BlockExpr(blk) => match blk.modifier() {475 Some(ast::BlockModifier::Async(t)) => hl_exit_points(sema, Some(t), blk.into()),476 Some(ast::BlockModifier::Try { try_token: t, .. }) if token.kind() != T![return] => {477 hl_exit_points(sema, Some(t), blk.into())478 },479 _ => continue,480 },481 _ => continue,482 }483 };484 merge_map(&mut res, new_map);485 }486487 res.into_iter().map(|(file_id, ranges)| (file_id, ranges.into_iter().collect())).collect()488}489490pub(crate) fn highlight_break_points(491 sema: &Semantics<'_, RootDatabase>,492 token: SyntaxToken,493) -> FxHashMap<EditionedFileId, Vec<HighlightedRange>> {494 pub(crate) fn hl(495 sema: &Semantics<'_, RootDatabase>,496 cursor_token_kind: SyntaxKind,497 loop_token: Option<SyntaxToken>,498 label: Option<ast::Label>,499 expr: ast::Expr,500 ) -> Option<HighlightMap> {501 let mut highlights: FxHashMap<EditionedFileId, FxHashSet<_>> = FxHashMap::default();502503 let mut push_to_highlights = |file_id, range| {504 if let Some(FileRange { file_id, range }) = original_frange(sema.db, file_id, range) {505 let hrange = HighlightedRange { category: ReferenceCategory::empty(), range };506 highlights.entry(file_id).or_default().insert(hrange);507 }508 };509510 let label_lt = label.as_ref().and_then(|it| it.lifetime());511512 if let Some(range) = cover_range(513 loop_token.as_ref().map(|tok| tok.text_range()),514 label.as_ref().map(|it| it.syntax().text_range()),515 ) {516 let file_id = loop_token517 .and_then(|tok| Some(sema.hir_file_for(&tok.parent()?)))518 .unwrap_or_else(|| sema.hir_file_for(label.unwrap().syntax()));519 push_to_highlights(file_id, Some(range));520 }521522 WalkExpandedExprCtx::new(sema)523 .with_check_ctx(&WalkExpandedExprCtx::is_async_const_block_or_closure)524 .walk(&expr, &mut |depth, expr| {525 let file_id = sema.hir_file_for(expr.syntax());526527 // Only highlight the `break`s for `break` and `continue`s for `continue`528 let (token, token_lt) = match expr {529 ast::Expr::BreakExpr(b) if cursor_token_kind != T![continue] => {530 (b.break_token(), b.lifetime())531 }532 ast::Expr::ContinueExpr(c) if cursor_token_kind != T![break] => {533 (c.continue_token(), c.lifetime())534 }535 _ => return,536 };537538 if !(depth == 1 && token_lt.is_none() || eq_label_lt(&label_lt, &token_lt)) {539 return;540 }541542 let text_range = cover_range(543 token.map(|it| it.text_range()),544 token_lt.map(|it| it.syntax().text_range()),545 );546547 push_to_highlights(file_id, text_range);548 });549550 if matches!(expr, ast::Expr::BlockExpr(_)) {551 for_each_tail_expr(&expr, &mut |tail| {552 if matches!(tail, ast::Expr::BreakExpr(_)) {553 return;554 }555556 let file_id = sema.hir_file_for(tail.syntax());557 let range = tail.syntax().text_range();558 push_to_highlights(file_id, Some(range));559 });560 }561562 Some(highlights)563 }564565 let Some(loops) = find_loops(sema, &token) else {566 return FxHashMap::default();567 };568569 let mut res = FxHashMap::default();570 let token_kind = token.kind();571 for expr in loops {572 let new_map = match &expr {573 ast::Expr::LoopExpr(l) => hl(sema, token_kind, l.loop_token(), l.label(), expr),574 ast::Expr::ForExpr(f) => hl(sema, token_kind, f.for_token(), f.label(), expr),575 ast::Expr::WhileExpr(w) => hl(sema, token_kind, w.while_token(), w.label(), expr),576 ast::Expr::BlockExpr(e) => hl(sema, token_kind, None, e.label(), expr),577 _ => continue,578 };579 merge_map(&mut res, new_map);580 }581582 res.into_iter().map(|(file_id, ranges)| (file_id, ranges.into_iter().collect())).collect()583}584585pub(crate) fn highlight_yield_points(586 sema: &Semantics<'_, RootDatabase>,587 token: SyntaxToken,588) -> FxHashMap<EditionedFileId, Vec<HighlightedRange>> {589 fn hl(590 sema: &Semantics<'_, RootDatabase>,591 async_token: Option<SyntaxToken>,592 body: Option<ast::Expr>,593 ) -> Option<HighlightMap> {594 let mut highlights: FxHashMap<EditionedFileId, FxHashSet<_>> = FxHashMap::default();595596 let mut push_to_highlights = |file_id, range| {597 if let Some(FileRange { file_id, range }) = original_frange(sema.db, file_id, range) {598 let hrange = HighlightedRange { category: ReferenceCategory::empty(), range };599 highlights.entry(file_id).or_default().insert(hrange);600 }601 };602603 let async_token = async_token?;604 let async_tok_file_id = sema.hir_file_for(&async_token.parent()?);605 push_to_highlights(async_tok_file_id, Some(async_token.text_range()));606607 let Some(body) = body else {608 return Some(highlights);609 };610611 WalkExpandedExprCtx::new(sema).walk(&body, &mut |_, expr| {612 let file_id = sema.hir_file_for(expr.syntax());613614 let text_range = match expr {615 ast::Expr::AwaitExpr(expr) => expr.await_token(),616 ast::Expr::ReturnExpr(expr) => expr.return_token(),617 _ => None,618 }619 .map(|it| it.text_range());620621 push_to_highlights(file_id, text_range);622 });623624 Some(highlights)625 }626627 let mut res = FxHashMap::default();628 for anc in goto_definition::find_fn_or_blocks(sema, &token) {629 let new_map = match_ast! {630 match anc {631 ast::Fn(fn_) => hl(sema, fn_.async_token(), fn_.body().map(ast::Expr::BlockExpr)),632 ast::BlockExpr(block_expr) => {633 let Some(async_token) = block_expr.async_token() else {634 continue;635 };636637 // Async blocks act similar to closures. So we want to638 // highlight their exit points too, but only if we are on639 // the async token.640 if async_token == token {641 let exit_points = hl_exit_points(642 sema,643 Some(async_token.clone()),644 block_expr.clone().into(),645 );646 merge_map(&mut res, exit_points);647 }648649 hl(sema, Some(async_token), Some(block_expr.into()))650 },651 ast::ClosureExpr(closure) => hl(sema, closure.async_token(), closure.body()),652 _ => continue,653 }654 };655 merge_map(&mut res, new_map);656 }657658 res.into_iter().map(|(file_id, ranges)| (file_id, ranges.into_iter().collect())).collect()659}660661fn cover_range(r0: Option<TextRange>, r1: Option<TextRange>) -> Option<TextRange> {662 match (r0, r1) {663 (Some(r0), Some(r1)) => Some(r0.cover(r1)),664 (Some(range), None) => Some(range),665 (None, Some(range)) => Some(range),666 (None, None) => None,667 }668}669670fn find_defs(sema: &Semantics<'_, RootDatabase>, token: SyntaxToken) -> FxHashSet<Definition> {671 sema.descend_into_macros_exact(token)672 .into_iter()673 .filter_map(|token| IdentClass::classify_token(sema, &token))674 .flat_map(IdentClass::definitions_no_ops)675 .collect()676}677678fn original_frange(679 db: &dyn db::ExpandDatabase,680 file_id: HirFileId,681 text_range: Option<TextRange>,682) -> Option<FileRange> {683 InFile::new(file_id, text_range?).original_node_file_range_opt(db).map(|(frange, _)| frange)684}685686fn merge_map(res: &mut HighlightMap, new: Option<HighlightMap>) {687 let Some(new) = new else {688 return;689 };690 new.into_iter().for_each(|(file_id, ranges)| {691 res.entry(file_id).or_default().extend(ranges);692 });693}694695/// Preorder walk all the expression's child expressions.696/// For macro calls, the callback will be called on the expanded expressions after697/// visiting the macro call itself.698struct WalkExpandedExprCtx<'a> {699 sema: &'a Semantics<'a, RootDatabase>,700 depth: usize,701 check_ctx: &'static dyn Fn(&ast::Expr) -> bool,702}703704impl<'a> WalkExpandedExprCtx<'a> {705 fn new(sema: &'a Semantics<'a, RootDatabase>) -> Self {706 Self { sema, depth: 0, check_ctx: &is_closure_or_blk_with_modif }707 }708709 fn with_check_ctx(&self, check_ctx: &'static dyn Fn(&ast::Expr) -> bool) -> Self {710 Self { check_ctx, ..*self }711 }712713 fn walk(&mut self, expr: &ast::Expr, cb: &mut dyn FnMut(usize, ast::Expr)) {714 preorder_expr_with_ctx_checker(expr, self.check_ctx, &mut |ev: WalkEvent<ast::Expr>| {715 match ev {716 syntax::WalkEvent::Enter(expr) => {717 cb(self.depth, expr.clone());718719 if Self::should_change_depth(&expr) {720 self.depth += 1;721 }722723 if let ast::Expr::MacroExpr(expr) = expr724 && let Some(expanded) =725 expr.macro_call().and_then(|call| self.sema.expand_macro_call(&call))726 {727 match_ast! {728 match (expanded.value) {729 ast::MacroStmts(it) => {730 self.handle_expanded(it, cb);731 },732 ast::Expr(it) => {733 self.walk(&it, cb);734 },735 _ => {}736 }737 }738 }739 }740 syntax::WalkEvent::Leave(expr) if Self::should_change_depth(&expr) => {741 self.depth -= 1;742 }743 _ => {}744 }745 false746 })747 }748749 fn handle_expanded(&mut self, expanded: ast::MacroStmts, cb: &mut dyn FnMut(usize, ast::Expr)) {750 if let Some(expr) = expanded.expr() {751 self.walk(&expr, cb);752 }753754 for stmt in expanded.statements() {755 if let ast::Stmt::ExprStmt(stmt) = stmt756 && let Some(expr) = stmt.expr()757 {758 self.walk(&expr, cb);759 }760 }761 }762763 fn should_change_depth(expr: &ast::Expr) -> bool {764 match expr {765 ast::Expr::LoopExpr(_) | ast::Expr::WhileExpr(_) | ast::Expr::ForExpr(_) => true,766 ast::Expr::BlockExpr(blk) if blk.label().is_some() => true,767 _ => false,768 }769 }770771 fn is_async_const_block_or_closure(expr: &ast::Expr) -> bool {772 match expr {773 ast::Expr::BlockExpr(b) => matches!(774 b.modifier(),775 Some(ast::BlockModifier::Async(_) | ast::BlockModifier::Const(_))776 ),777 ast::Expr::ClosureExpr(_) => true,778 _ => false,779 }780 }781}782783pub(crate) fn highlight_unsafe_points(784 sema: &Semantics<'_, RootDatabase>,785 token: SyntaxToken,786) -> FxHashMap<EditionedFileId, Vec<HighlightedRange>> {787 fn hl(788 sema: &Semantics<'_, RootDatabase>,789 unsafe_token: &SyntaxToken,790 block_expr: Option<ast::BlockExpr>,791 ) -> Option<FxHashMap<EditionedFileId, Vec<HighlightedRange>>> {792 let mut highlights: FxHashMap<EditionedFileId, Vec<_>> = FxHashMap::default();793794 let mut push_to_highlights = |file_id, range| {795 if let Some(FileRange { file_id, range }) = original_frange(sema.db, file_id, range) {796 let hrange = HighlightedRange { category: ReferenceCategory::empty(), range };797 highlights.entry(file_id).or_default().push(hrange);798 }799 };800801 // highlight unsafe keyword itself802 let unsafe_token_file_id = sema.hir_file_for(&unsafe_token.parent()?);803 push_to_highlights(unsafe_token_file_id, Some(unsafe_token.text_range()));804805 // highlight unsafe operations806 if let Some(block) = block_expr {807 let unsafe_ops = sema.get_unsafe_ops_for_unsafe_block(block);808 for unsafe_op in unsafe_ops {809 push_to_highlights(unsafe_op.file_id, Some(unsafe_op.value.text_range()));810 }811 }812813 Some(highlights)814 }815816 hl(sema, &token, token.parent().and_then(ast::BlockExpr::cast)).unwrap_or_default()817}818819#[cfg(test)]820mod tests {821 use itertools::Itertools;822823 use crate::fixture;824825 use super::*;826827 const ENABLED_CONFIG: HighlightRelatedConfig = HighlightRelatedConfig {828 break_points: true,829 exit_points: true,830 references: true,831 closure_captures: true,832 yield_points: true,833 branch_exit_points: true,834 };835836 #[track_caller]837 fn check(#[rust_analyzer::rust_fixture] ra_fixture: &str) {838 check_with_config(ra_fixture, ENABLED_CONFIG);839 }840841 #[track_caller]842 fn check_with_config(843 #[rust_analyzer::rust_fixture] ra_fixture: &str,844 config: HighlightRelatedConfig,845 ) {846 let (analysis, pos, annotations) = fixture::annotations(ra_fixture);847848 let hls = analysis.highlight_related(config, pos).unwrap().unwrap_or_default();849850 let mut expected =851 annotations.into_iter().map(|(r, access)| (r.range, access)).collect::<Vec<_>>();852853 let mut actual: Vec<(TextRange, String)> = hls854 .into_iter()855 .map(|hl| {856 (857 hl.range,858 hl.category.iter_names().map(|(name, _flag)| name.to_lowercase()).join(","),859 )860 })861 .collect();862 actual.sort_by_key(|(range, _)| range.start());863 expected.sort_by_key(|(range, _)| range.start());864865 assert_eq!(expected, actual);866 }867868 #[test]869 fn test_hl_unsafe_block() {870 check(871 r#"872fn foo() {873 unsafe fn this_is_unsafe_function() {}874875 unsa$0fe {876 //^^^^^^877 let raw_ptr = &42 as *const i32;878 let val = *raw_ptr;879 //^^^^^^^^880881 let mut_ptr = &mut 5 as *mut i32;882 *mut_ptr = 10;883 //^^^^^^^^884885 this_is_unsafe_function();886 //^^^^^^^^^^^^^^^^^^^^^^^^^887 }888889}890"#,891 );892 }893894 #[test]895 fn test_hl_tuple_fields() {896 check(897 r#"898struct Tuple(u32, u32);899900fn foo(t: Tuple) {901 t.0$0;902 // ^ read903 t.0;904 // ^ read905}906"#,907 );908 }909910 #[test]911 fn test_hl_module() {912 check(913 r#"914//- /lib.rs915mod foo$0;916 // ^^^917//- /foo.rs918struct Foo;919"#,920 );921 }922923 #[test]924 fn test_hl_self_in_crate_root() {925 check(926 r#"927use crate$0;928 //^^^^^ import929use self;930 //^^^^ import931mod __ {932 use super;933 //^^^^^ import934}935"#,936 );937 check(938 r#"939//- /main.rs crate:main deps:lib940use lib$0;941 //^^^ import942//- /lib.rs crate:lib943"#,944 );945 }946947 #[test]948 fn test_hl_self_in_module() {949 check(950 r#"951//- /lib.rs952mod foo;953//- /foo.rs954use self$0;955 // ^^^^ import956"#,957 );958 }959960 #[test]961 fn test_hl_local() {962 check(963 r#"964fn foo() {965 let mut bar = 3;966 // ^^^ write967 bar$0;968 // ^^^ read969}970"#,971 );972 }973974 #[test]975 fn test_hl_local_in_attr() {976 check(977 r#"978//- proc_macros: identity979#[proc_macros::identity]980fn foo() {981 let mut bar = 3;982 // ^^^ write983 bar$0;984 // ^^^ read985}986"#,987 );988 }989990 #[test]991 fn test_multi_macro_usage() {992 check(993 r#"994macro_rules! foo {995 ($ident:ident) => {996 fn $ident() -> $ident { loop {} }997 struct $ident;998 }999}10001001foo!(bar$0);1002 // ^^^1003fn foo() {1004 let bar: bar = bar();1005 // ^^^1006 // ^^^1007}1008"#,1009 );1010 check(1011 r#"1012macro_rules! foo {1013 ($ident:ident) => {1014 fn $ident() -> $ident { loop {} }1015 struct $ident;1016 }1017}10181019foo!(bar);1020 // ^^^1021fn foo() {1022 let bar: bar$0 = bar();1023 // ^^^1024}1025"#,1026 );1027 }10281029 #[test]1030 fn test_hl_yield_points() {1031 check(1032 r#"1033pub async fn foo() {1034 // ^^^^^1035 let x = foo()1036 .await$01037 // ^^^^^1038 .await;1039 // ^^^^^1040 || { 0.await };1041 (async { 0.await }).await1042 // ^^^^^1043}1044"#,1045 );1046 }10471048 #[test]1049 fn test_hl_yield_points2() {1050 check(1051 r#"1052pub async$0 fn foo() {1053 // ^^^^^1054 let x = foo()1055 .await1056 // ^^^^^1057 .await;1058 // ^^^^^1059 || { 0.await };1060 (async { 0.await }).await1061 // ^^^^^1062}1063"#,1064 );1065 }10661067 #[test]1068 fn test_hl_exit_points_of_async_blocks() {1069 check(1070 r#"1071pub fn foo() {1072 let x = async$0 {1073 // ^^^^^1074 0.await;1075 // ^^^^^1076 0?;1077 // ^1078 return 0;1079 // ^^^^^^1080 01081 // ^1082 };1083}1084"#,1085 );1086 }10871088 #[test]1089 fn test_hl_let_else_yield_points() {1090 check(1091 r#"1092pub async fn foo() {1093 // ^^^^^1094 let x = foo()1095 .await$01096 // ^^^^^1097 .await;1098 // ^^^^^1099 || { 0.await };1100 let Some(_) = None else {1101 foo().await1102 // ^^^^^1103 };1104 (async { 0.await }).await1105 // ^^^^^1106}1107"#,1108 );1109 }11101111 #[test]1112 fn test_hl_yield_nested_fn() {1113 check(1114 r#"1115async fn foo() {1116 async fn foo2() {1117 // ^^^^^1118 async fn foo3() {1119 0.await1120 }1121 0.await$01122 // ^^^^^1123 }1124 0.await1125}1126"#,1127 );1128 }11291130 #[test]1131 fn test_hl_yield_nested_async_blocks() {1132 check(1133 r#"1134async fn foo() {1135 (async {1136 // ^^^^^1137 (async { 0.await }).await$01138 // ^^^^^1139 }).await;1140}1141"#,1142 );1143 }11441145 #[test]1146 fn test_hl_exit_points() {1147 check(1148 r#"1149 fn foo() -> u32 {1150//^^1151 if true {1152 return$0 0;1153 // ^^^^^^1154 }11551156 0?;1157 // ^1158 0xDEAD_BEEF1159 // ^^^^^^^^^^^1160}1161"#,1162 );1163 }11641165 #[test]1166 fn test_hl_exit_points2() {1167 check(1168 r#"1169 fn foo() ->$0 u32 {1170//^^1171 if true {1172 return 0;1173 // ^^^^^^1174 }11751176 0?;1177 // ^1178 0xDEAD_BEEF1179 // ^^^^^^^^^^^1180}1181"#,1182 );1183 }11841185 #[test]1186 fn test_hl_exit_points3() {1187 check(1188 r#"1189 fn$0 foo() -> u32 {1190//^^1191 if true {1192 return 0;1193 // ^^^^^^1194 }11951196 0?;1197 // ^1198 0xDEAD_BEEF1199 // ^^^^^^^^^^^1200}1201"#,1202 );1203 }12041205 #[test]1206 fn test_hl_let_else_exit_points() {1207 check(1208 r#"1209 fn$0 foo() -> u32 {1210//^^1211 let Some(bar) = None else {1212 return 0;1213 // ^^^^^^1214 };12151216 0?;1217 // ^1218 0xDEAD_BEEF1219 // ^^^^^^^^^^^1220}1221"#,1222 );1223 }12241225 #[test]1226 fn test_hl_prefer_ref_over_tail_exit() {1227 check(1228 r#"1229fn foo() -> u32 {1230// ^^^1231 if true {1232 return 0;1233 }12341235 0?;12361237 foo$0()1238 // ^^^1239}1240"#,1241 );1242 }12431244 #[test]1245 fn test_hl_never_call_is_exit_point() {1246 check(1247 r#"1248struct Never;1249impl Never {1250 fn never(self) -> ! { loop {} }1251}1252macro_rules! never {1253 () => { never() }1254 // ^^^^^^^1255}1256fn never() -> ! { loop {} }1257 fn foo() ->$0 u32 {1258//^^1259 never();1260 // ^^^^^^^1261 never!();1262 // ^^^^^^^^12631264 Never.never();1265 // ^^^^^^^^^^^^^12661267 01268 // ^1269}1270"#,1271 );1272 }12731274 #[test]1275 fn test_hl_inner_tail_exit_points() {1276 check(1277 r#"1278 fn foo() ->$0 u32 {1279//^^1280 if true {1281 unsafe {1282 return 5;1283 // ^^^^^^1284 51285 // ^1286 }1287 } else if false {1288 01289 // ^1290 } else {1291 match 5 {1292 6 => 100,1293 // ^^^1294 7 => loop {1295 break 5;1296 // ^^^^^1297 }1298 8 => 'a: loop {1299 'b: loop {1300 break 'a 5;1301 // ^^^^^1302 break 'b 5;1303 break 5;1304 };1305 }1306 //1307 _ => 500,1308 // ^^^1309 }1310 }1311}1312"#,1313 );1314 }13151316 #[test]1317 fn test_hl_inner_tail_exit_points_labeled_block() {1318 check(1319 r#"1320 fn foo() ->$0 u32 {1321//^^1322 'foo: {1323 break 'foo 0;1324 // ^^^^^1325 loop {1326 break;1327 break 'foo 0;1328 // ^^^^^1329 }1330 01331 // ^1332 }1333}1334"#,1335 );1336 }13371338 #[test]1339 fn test_hl_inner_tail_exit_points_loops() {1340 check(1341 r#"1342 fn foo() ->$0 u32 {1343//^^1344 'foo: while { return 0; true } {1345 // ^^^^^^1346 break 'foo 0;1347 // ^^^^^1348 return 0;1349 // ^^^^^^1350 }1351}1352"#,1353 );1354 }13551356 #[test]1357 fn test_hl_break_loop() {1358 check(1359 r#"1360fn foo() {1361 'outer: loop {1362 // ^^^^^^^^^^^^1363 break;1364 // ^^^^^1365 'inner: loop {1366 break;1367 'innermost: loop {1368 break 'outer;1369 // ^^^^^^^^^^^^1370 break 'inner;1371 }1372 break$0 'outer;1373 // ^^^^^^^^^^^^1374 break;1375 }1376 break;1377 // ^^^^^1378 }1379}1380"#,1381 );1382 }13831384 #[test]1385 fn test_hl_break_loop2() {1386 check(1387 r#"1388fn foo() {1389 'outer: loop {1390 break;1391 'inner: loop {1392 // ^^^^^^^^^^^^1393 break;1394 // ^^^^^1395 'innermost: loop {1396 break 'outer;1397 break 'inner;1398 // ^^^^^^^^^^^^1399 }1400 break 'outer;1401 break$0;1402 // ^^^^^1403 }1404 break;1405 }1406}1407"#,1408 );1409 }14101411 #[test]1412 fn test_hl_break_for() {1413 check(1414 r#"1415fn foo() {1416 'outer: for _ in () {1417 // ^^^^^^^^^^^1418 break;1419 // ^^^^^1420 'inner: for _ in () {1421 break;1422 'innermost: for _ in () {1423 break 'outer;1424 // ^^^^^^^^^^^^1425 break 'inner;1426 }1427 break$0 'outer;1428 // ^^^^^^^^^^^^1429 break;1430 }1431 break;1432 // ^^^^^1433 }1434}1435"#,1436 );1437 }14381439 #[test]1440 fn test_hl_break_for_but_not_continue() {1441 check(1442 r#"1443fn foo() {1444 'outer: for _ in () {1445 // ^^^^^^^^^^^1446 break;1447 // ^^^^^1448 continue;1449 'inner: for _ in () {1450 break;1451 continue;1452 'innermost: for _ in () {1453 continue 'outer;1454 break 'outer;1455 // ^^^^^^^^^^^^1456 continue 'inner;1457 break 'inner;1458 }1459 break$0 'outer;1460 // ^^^^^^^^^^^^1461 continue 'outer;1462 break;1463 continue;1464 }1465 break;1466 // ^^^^^1467 continue;1468 }1469}1470"#,1471 );1472 }14731474 #[test]1475 fn test_hl_continue_for_but_not_break() {1476 check(1477 r#"1478fn foo() {1479 'outer: for _ in () {1480 // ^^^^^^^^^^^1481 break;1482 continue;1483 // ^^^^^^^^1484 'inner: for _ in () {1485 break;1486 continue;1487 'innermost: for _ in () {1488 continue 'outer;1489 // ^^^^^^^^^^^^^^^1490 break 'outer;1491 continue 'inner;1492 break 'inner;1493 }1494 break 'outer;1495 continue$0 'outer;1496 // ^^^^^^^^^^^^^^^1497 break;1498 continue;1499 }1500 break;1501 continue;1502 // ^^^^^^^^1503 }1504}1505"#,1506 );1507 }15081509 #[test]1510 fn test_hl_break_and_continue() {1511 check(1512 r#"1513fn foo() {1514 'outer: fo$0r _ in () {1515 // ^^^^^^^^^^^1516 break;1517 // ^^^^^1518 continue;1519 // ^^^^^^^^1520 'inner: for _ in () {1521 break;1522 continue;1523 'innermost: for _ in () {1524 continue 'outer;1525 // ^^^^^^^^^^^^^^^1526 break 'outer;1527 // ^^^^^^^^^^^^1528 continue 'inner;1529 break 'inner;1530 }1531 break 'outer;1532 // ^^^^^^^^^^^^1533 continue 'outer;1534 // ^^^^^^^^^^^^^^^1535 break;1536 continue;1537 }1538 break;1539 // ^^^^^1540 continue;1541 // ^^^^^^^^1542 }1543}1544"#,1545 );1546 }15471548 #[test]1549 fn test_hl_break_while() {1550 check(1551 r#"1552fn foo() {1553 'outer: while true {1554 // ^^^^^^^^^^^^^1555 break;1556 // ^^^^^1557 'inner: while true {1558 break;1559 'innermost: while true {1560 break 'outer;1561 // ^^^^^^^^^^^^1562 break 'inner;1563 }1564 break$0 'outer;1565 // ^^^^^^^^^^^^1566 break;1567 }1568 break;1569 // ^^^^^1570 }1571}1572"#,1573 );1574 }15751576 #[test]1577 fn test_hl_break_labeled_block() {1578 check(1579 r#"1580fn foo() {1581 'outer: {1582 // ^^^^^^^1583 break;1584 // ^^^^^1585 'inner: {1586 break;1587 'innermost: {1588 break 'outer;1589 // ^^^^^^^^^^^^1590 break 'inner;1591 }1592 break$0 'outer;1593 // ^^^^^^^^^^^^1594 break;1595 }1596 break;1597 // ^^^^^1598 }1599}1600"#,1601 );1602 }16031604 #[test]1605 fn test_hl_break_unlabeled_loop() {1606 check(1607 r#"1608fn foo() {1609 loop {1610 // ^^^^1611 break$0;1612 // ^^^^^1613 }1614}1615"#,1616 );1617 }16181619 #[test]1620 fn test_hl_break_unlabeled_block_in_loop() {1621 check(1622 r#"1623fn foo() {1624 loop {1625 // ^^^^1626 {1627 break$0;1628 // ^^^^^1629 }1630 }1631}1632"#,1633 );1634 }16351636 #[test]1637 fn test_hl_field_shorthand() {1638 check(1639 r#"1640struct Struct { field: u32 }1641 //^^^^^1642fn function(field: u32) {1643 //^^^^^1644 Struct { field$0 }1645 //^^^^^ read1646}1647"#,1648 );1649 }16501651 #[test]1652 fn test_hl_disabled_ref_local() {1653 let config = HighlightRelatedConfig { references: false, ..ENABLED_CONFIG };16541655 check_with_config(1656 r#"1657fn foo() {1658 let x$0 = 5;1659 let y = x * 2;1660}1661"#,1662 config,1663 );1664 }16651666 #[test]1667 fn test_hl_disabled_ref_local_preserved_break() {1668 let config = HighlightRelatedConfig { references: false, ..ENABLED_CONFIG };16691670 check_with_config(1671 r#"1672fn foo() {1673 let x$0 = 5;1674 let y = x * 2;16751676 loop {1677 break;1678 }1679}1680"#,1681 config.clone(),1682 );16831684 check_with_config(1685 r#"1686fn foo() {1687 let x = 5;1688 let y = x * 2;16891690 loop$0 {1691// ^^^^1692 break;1693// ^^^^^1694 }1695}1696"#,1697 config,1698 );1699 }17001701 #[test]1702 fn test_hl_disabled_ref_local_preserved_yield() {1703 let config = HighlightRelatedConfig { references: false, ..ENABLED_CONFIG };17041705 check_with_config(1706 r#"1707async fn foo() {1708 let x$0 = 5;1709 let y = x * 2;17101711 0.await;1712}1713"#,1714 config.clone(),1715 );17161717 check_with_config(1718 r#"1719 async fn foo() {1720// ^^^^^1721 let x = 5;1722 let y = x * 2;17231724 0.await$0;1725// ^^^^^1726}1727"#,1728 config,1729 );1730 }17311732 #[test]1733 fn test_hl_disabled_ref_local_preserved_exit() {1734 let config = HighlightRelatedConfig { references: false, ..ENABLED_CONFIG };17351736 check_with_config(1737 r#"1738fn foo() -> i32 {1739 let x$0 = 5;1740 let y = x * 2;17411742 if true {1743 return y;1744 }17451746 0?1747}1748"#,1749 config.clone(),1750 );17511752 check_with_config(1753 r#"1754 fn foo() ->$0 i32 {1755//^^1756 let x = 5;1757 let y = x * 2;17581759 if true {1760 return y;1761// ^^^^^^1762 }17631764 0?1765// ^1766"#,1767 config,1768 );1769 }17701771 #[test]1772 fn test_hl_disabled_break() {1773 let config = HighlightRelatedConfig { break_points: false, ..ENABLED_CONFIG };17741775 check_with_config(1776 r#"1777fn foo() {1778 loop {1779 break$0;1780 }1781}1782"#,1783 config,1784 );1785 }17861787 #[test]1788 fn test_hl_disabled_yield() {1789 let config = HighlightRelatedConfig { yield_points: false, ..ENABLED_CONFIG };17901791 check_with_config(1792 r#"1793async$0 fn foo() {1794 0.await;1795}1796"#,1797 config,1798 );1799 }18001801 #[test]1802 fn test_hl_disabled_exit() {1803 let config = HighlightRelatedConfig { exit_points: false, ..ENABLED_CONFIG };18041805 check_with_config(1806 r#"1807fn foo() ->$0 i32 {1808 if true {1809 return -1;1810 }18111812 421813}"#,1814 config,1815 );1816 }18171818 #[test]1819 fn test_hl_multi_local() {1820 check(1821 r#"1822fn foo((1823 foo$01824 //^^^1825 | foo1826 //^^^1827 | foo1828 //^^^1829): ()) {1830 foo;1831 //^^^read1832 let foo;1833}1834"#,1835 );1836 check(1837 r#"1838fn foo((1839 foo1840 //^^^1841 | foo$01842 //^^^1843 | foo1844 //^^^1845): ()) {1846 foo;1847 //^^^read1848 let foo;1849}1850"#,1851 );1852 check(1853 r#"1854fn foo((1855 foo1856 //^^^1857 | foo1858 //^^^1859 | foo1860 //^^^1861): ()) {1862 foo$0;1863 //^^^read1864 let foo;1865}1866"#,1867 );1868 }18691870 #[test]1871 fn test_hl_trait_impl_methods() {1872 check(1873 r#"1874trait Trait {1875 fn func$0(self) {}1876 //^^^^1877}18781879impl Trait for () {1880 fn func(self) {}1881 //^^^^1882}18831884fn main() {1885 <()>::func(());1886 //^^^^1887 ().func();1888 //^^^^1889}1890"#,1891 );1892 check(1893 r#"1894trait Trait {1895 fn func(self) {}1896}18971898impl Trait for () {1899 fn func$0(self) {}1900 //^^^^1901}19021903fn main() {1904 <()>::func(());1905 //^^^^1906 ().func();1907 //^^^^1908}1909"#,1910 );1911 check(1912 r#"1913trait Trait {1914 fn func(self) {}1915}19161917impl Trait for () {1918 fn func(self) {}1919 //^^^^1920}19211922fn main() {1923 <()>::func(());1924 //^^^^1925 ().func$0();1926 //^^^^1927}1928"#,1929 );1930 }19311932 #[test]1933 fn test_assoc_type_highlighting() {1934 check(1935 r#"1936trait Trait {1937 type Output;1938 // ^^^^^^1939}1940impl Trait for () {1941 type Output$0 = ();1942 // ^^^^^^1943}1944"#,1945 );1946 }19471948 #[test]1949 fn test_closure_capture_pipe() {1950 check(1951 r#"1952fn f() {1953 let x = 1;1954 // ^1955 let c = $0|y| x + y;1956 // ^ read1957}1958"#,1959 );1960 }19611962 #[test]1963 fn test_closure_capture_move() {1964 check(1965 r#"1966fn f() {1967 let x = 1;1968 // ^1969 let c = move$0 |y| x + y;1970 // ^ read1971}1972"#,1973 );1974 }19751976 #[test]1977 fn test_trait_highlights_assoc_item_uses() {1978 check(1979 r#"1980trait Super {1981 type SuperT;1982}1983trait Foo: Super {1984 //^^^1985 type T;1986 const C: usize;1987 fn f() {}1988 fn m(&self) {}1989}1990impl Foo for i32 {1991 //^^^1992 type T = i32;1993 const C: usize = 0;1994 fn f() {}1995 fn m(&self) {}1996}1997fn f<T: Foo$0>(t: T) {1998 //^^^1999 let _: T::SuperT;2000 //^^^^^^
Same data, no extra tab — call code_get_file + code_get_findings over MCP from Claude/Cursor/Copilot.