Warning: Direct indexing (e.g., `vec[i]`, `slice[i]`) panics on out-of-bounds access. Prefer using `.get(index)` or `.get_mut(index)` which return Option<&T>/Option<&mut T>.
// We must pass [Self, Rhs]
1use std::{iter, mem::discriminant};23use crate::Analysis;4use crate::{5 FilePosition, NavigationTarget, RangeInfo, TryToNav, UpmappingResult,6 doc_links::token_as_doc_comment,7 navigation_target::{self, ToNav},8};9use hir::{10 AsAssocItem, AssocItem, CallableKind, FileRange, HasCrate, InFile, ModuleDef, Semantics, sym,11};12use ide_db::ra_fixture::{RaFixtureConfig, UpmapFromRaFixture};13use ide_db::{14 RootDatabase, SymbolKind,15 base_db::{AnchoredPath, SourceDatabase},16 defs::{Definition, IdentClass},17 famous_defs::FamousDefs,18 helpers::pick_best_token,19 syntax_helpers::node_ext::find_loops,20};21use itertools::Itertools;22use span::FileId;23use syntax::{24 AstNode, AstToken, SyntaxKind::*, SyntaxNode, SyntaxToken, T, TextRange, ast, match_ast,25};2627#[derive(Debug)]28pub struct GotoDefinitionConfig<'a> {29 pub ra_fixture: RaFixtureConfig<'a>,30}3132// Feature: Go to Definition33//34// Navigates to the definition of an identifier.35//36// For outline modules, this will navigate to the source file of the module.37//38// | Editor | Shortcut |39// |---------|----------|40// | VS Code | <kbd>F12</kbd> |41//42// 43//44// #### Special Go to Definitions45//46// You can go to definition on operators and keywords as well. The behavior goes as follows:47//48// - On overloadable operators, this will take you to the `impl` of the operator's trait for this type, or to the trait if49// the impl cannot be determined.50// - For `?` on `Result` that goes through a non-trivial `From` (i.e. not the blanket `impl<T> From<T> for T`), it'll take51// you to the `From` impl.52// - On control flow keywords (loops, conditions, etc.) and `fn`, it'll take you to all exit points for this construct53// or its entrance, the opposite of the keyword you're at (e.g. on `fn` it'll take you to all exit points, and on `return`54// it'll take you to the `fn`).55// - It'll skip known blanket impls from the standard library where possible. For example, on a `try_into()` that comes56// from the blanket `impl<T: TryFrom<U>, U> TryInto<T> for U`, it'll take you to the `TryFrom` impl, and if it also57// comes from the blanket `impl<T: From<U>, U> TryFrom<U> for T`, it'll take you to the `From` impl.58pub(crate) fn goto_definition(59 db: &RootDatabase,60 FilePosition { file_id, offset }: FilePosition,61 config: &GotoDefinitionConfig<'_>,62) -> Option<RangeInfo<Vec<NavigationTarget>>> {63 let sema = &Semantics::new(db);64 let file = sema.parse_guess_edition(file_id).syntax().clone();65 let edition = sema.attach_first_edition(file_id).edition(db);66 let original_token = pick_best_token(file.token_at_offset(offset), |kind| match kind {67 IDENT68 | INT_NUMBER69 | LIFETIME_IDENT70 | T![self]71 | T![super]72 | T![crate]73 | T![Self]74 | COMMENT => 4,75 // index and prefix ops76 T!['['] | T![']'] | T![?] | T![*] | T![-] | T![!] => 3,77 kind if kind.is_keyword(edition) => 2,78 T!['('] | T![')'] => 2,79 kind if kind.is_trivia() => 0,80 _ => 1,81 })?;82 if let Some(doc_comment) = token_as_doc_comment(&original_token) {83 return doc_comment.get_definition_with_descend_at(sema, offset, |def, _, link_range| {84 let nav = def.try_to_nav(sema)?;85 Some(RangeInfo::new(link_range, nav.collect()))86 });87 }8889 if let Some((range, _, _, resolution)) =90 sema.check_for_format_args_template(original_token.clone(), offset)91 {92 return Some(RangeInfo::new(93 range,94 match resolution {95 Some(res) => def_to_nav(sema, Definition::from(res)),96 None => vec![],97 },98 ));99 }100101 if let Some(navs) = handle_control_flow_keywords(sema, &original_token) {102 return Some(RangeInfo::new(original_token.text_range(), navs));103 }104105 let tokens = sema.descend_into_macros_no_opaque(original_token.clone(), false);106 let mut navs = Vec::new();107 for token in tokens {108 if let Some(n) = find_definition_for_known_blanket_dual_impls(sema, &token.value) {109 navs.extend(n);110 continue;111 }112113 if let Some(n) = find_definition_for_comparison_operators(sema, &token.value) {114 navs.extend(n);115 continue;116 }117118 let parent = token.value.parent()?;119120 if let Some(question_mark_conversion) = goto_question_mark_conversions(sema, &parent) {121 navs.extend(def_to_nav(sema, question_mark_conversion.into()));122 continue;123 }124125 if let Some(token) = ast::String::cast(token.value.clone())126 && let Some(original_token) = ast::String::cast(original_token.clone())127 && let Some((analysis, fixture_analysis)) =128 Analysis::from_ra_fixture(sema, original_token, &token, &config.ra_fixture)129 && let Some((virtual_file_id, file_offset)) = fixture_analysis.map_offset_down(offset)130 {131 return hir::attach_db_allow_change(&analysis.db, || {132 goto_definition(133 &analysis.db,134 FilePosition { file_id: virtual_file_id, offset: file_offset },135 config,136 )137 })138 .and_then(|navs| {139 navs.upmap_from_ra_fixture(&fixture_analysis, virtual_file_id, file_id).ok()140 });141 }142143 let token_file_id = token.file_id;144 if let Some(token) = ast::String::cast(token.value.clone())145 && let Some(x) =146 try_lookup_include_path(sema, InFile::new(token_file_id, token), file_id)147 {148 navs.push(x);149 continue;150 }151152 if ast::TokenTree::can_cast(parent.kind())153 && let Some(x) = try_lookup_macro_def_in_macro_use(sema, token.value)154 {155 navs.push(x);156 continue;157 }158159 let Some(ident_class) = IdentClass::classify_node(sema, &parent) else { continue };160 navs.extend(ident_class.definitions().into_iter().flat_map(|(def, _)| {161 if let Definition::ExternCrateDecl(crate_def) = def {162 return crate_def163 .resolved_crate(db)164 .map(|it| it.root_module(db).to_nav(db))165 .into_iter()166 .flatten()167 .collect();168 }169 try_filter_trait_item_definition(sema, &def).unwrap_or_else(|| def_to_nav(sema, def))170 }));171 }172 let navs = navs.into_iter().unique().collect();173174 Some(RangeInfo::new(original_token.text_range(), navs))175}176177/// When the `?` operator is used on `Result`, go to the `From` impl if it exists as this provides more value.178fn goto_question_mark_conversions(179 sema: &Semantics<'_, RootDatabase>,180 node: &SyntaxNode,181) -> Option<hir::Function> {182 let node = ast::TryExpr::cast(node.clone())?;183 let try_expr_ty = sema.type_of_expr(&node.expr()?)?.adjusted();184185 let fd = FamousDefs(sema, try_expr_ty.krate(sema.db));186 let result_enum = fd.core_result_Result()?.into();187188 let (try_expr_ty_adt, try_expr_ty_args) = try_expr_ty.as_adt_with_args()?;189 if try_expr_ty_adt != result_enum {190 // FIXME: Support `Poll<Result>`.191 return None;192 }193 let original_err_ty = try_expr_ty_args.get(1)?.clone()?;194195 let returned_ty = sema.try_expr_returned_type(&node)?;196 let (returned_adt, returned_ty_args) = returned_ty.as_adt_with_args()?;197 if returned_adt != result_enum {198 return None;199 }200 let returned_err_ty = returned_ty_args.get(1)?.clone()?;201202 if returned_err_ty.could_unify_with_deeply(sema.db, &original_err_ty) {203 return None;204 }205206 let from_trait = fd.core_convert_From()?;207 let from_fn = from_trait.function(sema.db, sym::from)?;208 sema.resolve_trait_impl_method(209 returned_err_ty.clone(),210 from_trait,211 from_fn,212 [returned_err_ty, original_err_ty],213 )214}215216// If the token is into(), try_into(), search the definition of From, TryFrom.217fn find_definition_for_known_blanket_dual_impls(218 sema: &Semantics<'_, RootDatabase>,219 original_token: &SyntaxToken,220) -> Option<Vec<NavigationTarget>> {221 let method_call = ast::MethodCallExpr::cast(original_token.parent()?.parent()?)?;222 let callable = sema.resolve_method_call_as_callable(&method_call)?;223 let CallableKind::Function(f) = callable.kind() else { return None };224 let assoc = f.as_assoc_item(sema.db)?;225226 let return_type = callable.return_type();227 let fd = FamousDefs(sema, return_type.krate(sema.db));228229 let t = match assoc.container(sema.db) {230 hir::AssocItemContainer::Trait(t) => t,231 hir::AssocItemContainer::Impl(impl_)232 if impl_.self_ty(sema.db).is_str() && f.name(sema.db) == sym::parse =>233 {234 let t = fd.core_convert_FromStr()?;235 let t_f = t.function(sema.db, &sym::from_str)?;236 return sema237 .resolve_trait_impl_method(238 return_type.clone(),239 t,240 t_f,241 [return_type.type_arguments().next()?],242 )243 .map(|f| def_to_nav(sema, f.into()));244 }245 hir::AssocItemContainer::Impl(_) => return None,246 };247248 let fn_name = f.name(sema.db);249 let f = if fn_name == sym::into && fd.core_convert_Into() == Some(t) {250 let dual = fd.core_convert_From()?;251 let dual_f = dual.function(sema.db, &sym::from)?;252 sema.resolve_trait_impl_method(253 return_type.clone(),254 dual,255 dual_f,256 [return_type, callable.receiver_param(sema.db)?.1],257 )?258 } else if fn_name == sym::try_into && fd.core_convert_TryInto() == Some(t) {259 let dual = fd.core_convert_TryFrom()?;260 let dual_f = dual.function(sema.db, &sym::try_from)?;261 sema.resolve_trait_impl_method(262 return_type.clone(),263 dual,264 dual_f,265 // Extract the `T` from `Result<T, ..>`266 [return_type.type_arguments().next()?, callable.receiver_param(sema.db)?.1],267 )?268 } else if fn_name == sym::to_string && fd.alloc_string_ToString() == Some(t) {269 let dual = fd.core_fmt_Display()?;270 let dual_f = dual.function(sema.db, &sym::fmt)?;271 sema.resolve_trait_impl_method(272 return_type.clone(),273 dual,274 dual_f,275 [callable.receiver_param(sema.db)?.1.strip_reference()],276 )?277 } else {278 return None;279 };280 // Assert that we got a trait impl function, if we are back in a trait definition we didn't281 // succeed282 let _t = f.as_assoc_item(sema.db)?.implemented_trait(sema.db)?;283 let def = Definition::from(f);284 Some(def_to_nav(sema, def))285}286287// If the token is a comparison operator (!=, <, <=, >, >=) that resolves to a default trait method, navigate to the corresponding primary method (eq for ne, partial_cmp for the others).288fn find_definition_for_comparison_operators(289 sema: &Semantics<'_, RootDatabase>,290 original_token: &SyntaxToken,291) -> Option<Vec<NavigationTarget>> {292 let bin_expr = ast::BinExpr::cast(original_token.parent()?)?;293294 let f = sema.resolve_bin_expr(&bin_expr)?;295 let assoc = f.as_assoc_item(sema.db)?;296297 let lhs_type = sema.type_of_expr(&bin_expr.lhs()?)?.original;298 let rhs_type = sema.type_of_expr(&bin_expr.rhs()?)?.original;299300 let t = match assoc.container(sema.db) {301 hir::AssocItemContainer::Trait(t) => t,302 hir::AssocItemContainer::Impl(_) => return None, // Already implemented by the type303 };304305 let fn_name = f.name(sema.db);306 let fn_name_str = fn_name.as_str();307308 let trait_name = t.name(sema.db);309 let trait_name_str = trait_name.as_str();310311 let (target_fn_name, expected_trait) = match fn_name_str {312 "ne" => ("eq", "PartialEq"),313 "lt" | "le" | "gt" | "ge" => ("partial_cmp", "PartialOrd"),314 _ => return None,315 };316317 if trait_name_str != expected_trait {318 return None;319 }320321 let primary_f = t.items(sema.db).into_iter().find_map(|item| {322 if let hir::AssocItem::Function(func) = item323 && func.name(sema.db).as_str() == target_fn_name324 {325 return Some(func);326 }327 None328 })?;329330 // Chalk requires ALL trait substitutions, including `Self`!331 // We must pass [Self, Rhs]332 let resolved_f = sema.resolve_trait_impl_method(333 lhs_type.clone(),334 t,335 primary_f,336 [lhs_type.clone(), rhs_type.clone()],337 )?;338339 let def = Definition::from(resolved_f);340341 Some(def_to_nav(sema, def))342}343fn try_lookup_include_path(344 sema: &Semantics<'_, RootDatabase>,345 token: InFile<ast::String>,346 file_id: FileId,347) -> Option<NavigationTarget> {348 let file = token.file_id.macro_file()?;349350 // Check that we are in the eager argument expansion of an include macro351 // that is we are the string input of it352 if !iter::successors(Some(file), |file| file.parent(sema.db).macro_file())353 .any(|file| file.is_include_like_macro(sema.db) && file.eager_arg(sema.db).is_none())354 {355 return None;356 }357 let path = token.value.value().ok()?;358359 let file_id = sema.db.resolve_path(AnchoredPath { anchor: file_id, path: &path })?;360 let size = sema.db.file_text(file_id).text(sema.db).len().try_into().ok()?;361 Some(NavigationTarget {362 file_id,363 full_range: TextRange::new(0.into(), size),364 name: hir::Symbol::intern(&path),365 alias: None,366 focus_range: None,367 kind: None,368 container_name: None,369 description: None,370 })371}372373fn try_lookup_macro_def_in_macro_use(374 sema: &Semantics<'_, RootDatabase>,375 token: SyntaxToken,376) -> Option<NavigationTarget> {377 let extern_crate = token.parent()?.ancestors().find_map(ast::ExternCrate::cast)?;378 let extern_crate = sema.to_def(&extern_crate)?;379 let krate = extern_crate.resolved_crate(sema.db)?;380381 for mod_def in krate.root_module(sema.db).declarations(sema.db) {382 if let ModuleDef::Macro(mac) = mod_def383 && mac.name(sema.db).as_str() == token.text()384 && let Some(nav) = mac.try_to_nav(sema)385 {386 return Some(nav.call_site);387 }388 }389390 None391}392393/// finds the trait definition of an impl'd item, except function394/// e.g.395/// ```rust396/// trait A { type a; }397/// struct S;398/// impl A for S { type a = i32; } // <-- on this associate type, will get the location of a in the trait399/// ```400fn try_filter_trait_item_definition(401 sema: &Semantics<'_, RootDatabase>,402 def: &Definition<'_>,403) -> Option<Vec<NavigationTarget>> {404 let db = sema.db;405 let assoc = def.as_assoc_item(db)?;406 match assoc {407 AssocItem::Function(..) => None,408 AssocItem::Const(..) | AssocItem::TypeAlias(..) => {409 let trait_ = assoc.implemented_trait(db)?;410 let name = def.name(db)?;411 let discriminant_value = discriminant(&assoc);412 trait_413 .items(db)414 .iter()415 .filter(|itm| discriminant(*itm) == discriminant_value)416 .find_map(|itm| (itm.name(db)? == name).then(|| itm.try_to_nav(sema)).flatten())417 .map(|it| it.collect())418 }419 }420}421422fn handle_control_flow_keywords(423 sema: &Semantics<'_, RootDatabase>,424 token: &SyntaxToken,425) -> Option<Vec<NavigationTarget>> {426 match token.kind() {427 // For `fn` / `loop` / `while` / `for` / `async` / `match`, return the keyword it self,428 // so that VSCode will find the references when using `ctrl + click`429 T![fn] | T![async] | T![try] | T![return] => nav_for_exit_points(sema, token),430 T![loop] | T![while] | T![break] | T![continue] => nav_for_break_points(sema, token),431 T![for] if token.parent().and_then(ast::ForExpr::cast).is_some() => {432 nav_for_break_points(sema, token)433 }434 T![match] | T![=>] | T![if] => nav_for_branch_exit_points(sema, token),435 _ => None,436 }437}438439pub(crate) fn find_fn_or_blocks(440 sema: &Semantics<'_, RootDatabase>,441 token: &SyntaxToken,442) -> Vec<SyntaxNode> {443 let find_ancestors = |token: SyntaxToken| {444 let token_kind = token.kind();445446 for anc in sema.token_ancestors_with_macros(token) {447 let node = match_ast! {448 match anc {449 ast::Fn(fn_) => fn_.syntax().clone(),450 ast::ClosureExpr(c) => c.syntax().clone(),451 ast::BlockExpr(blk) => {452 match blk.modifier() {453 Some(ast::BlockModifier::Async(_)) => blk.syntax().clone(),454 Some(ast::BlockModifier::Try { .. }) if token_kind != T![return] => blk.syntax().clone(),455 _ => continue,456 }457 },458 _ => continue,459 }460 };461462 return Some(node);463 }464 None465 };466467 sema.descend_into_macros(token.clone()).into_iter().filter_map(find_ancestors).collect_vec()468}469470fn nav_for_exit_points(471 sema: &Semantics<'_, RootDatabase>,472 token: &SyntaxToken,473) -> Option<Vec<NavigationTarget>> {474 let db = sema.db;475 let token_kind = token.kind();476477 let navs = find_fn_or_blocks(sema, token)478 .into_iter()479 .filter_map(|node| {480 let file_id = sema.hir_file_for(&node);481482 match_ast! {483 match node {484 ast::Fn(fn_) => {485 let mut nav = sema.to_def(&fn_)?.try_to_nav(sema)?;486 // For async token, we navigate to itself, which triggers487 // VSCode to find the references488 let focus_token = if matches!(token_kind, T![async]) {489 fn_.async_token()?490 } else {491 fn_.fn_token()?492 };493494 let focus_frange = InFile::new(file_id, focus_token.text_range())495 .original_node_file_range_opt(db)496 .map(|(frange, _)| frange);497498 if let Some(FileRange { file_id, range }) = focus_frange {499 let contains_frange = |nav: &NavigationTarget| {500 nav.file_id == file_id.file_id(db) && nav.full_range.contains_range(range)501 };502503 if let Some(def_site) = nav.def_site.as_mut() {504 if contains_frange(def_site) {505 def_site.focus_range = Some(range);506 }507 } else if contains_frange(&nav.call_site) {508 nav.call_site.focus_range = Some(range);509 }510 }511512 Some(nav)513 },514 ast::ClosureExpr(c) => {515 let pipe_tok = c.param_list().and_then(|it| it.pipe_token())?.text_range();516 let closure_in_file = InFile::new(file_id, c.into());517 Some(expr_to_nav(db, closure_in_file, Some(pipe_tok)))518 },519 ast::BlockExpr(blk) => {520 match blk.modifier() {521 Some(ast::BlockModifier::Async(_)) => {522 let async_tok = blk.async_token()?.text_range();523 let blk_in_file = InFile::new(file_id, blk.into());524 Some(expr_to_nav(db, blk_in_file, Some(async_tok)))525 },526 Some(ast::BlockModifier::Try { .. }) if token_kind != T![return] => {527 let try_tok = blk.try_block_modifier()?.try_token()?.text_range();528 let blk_in_file = InFile::new(file_id, blk.into());529 Some(expr_to_nav(db, blk_in_file, Some(try_tok)))530 },531 _ => None,532 }533 },534 _ => None,535 }536 }537 })538 .flatten()539 .collect_vec();540541 Some(navs)542}543544pub(crate) fn find_branch_root(545 sema: &Semantics<'_, RootDatabase>,546 token: &SyntaxToken,547) -> Vec<SyntaxNode> {548 let find_nodes = |node_filter: fn(SyntaxNode) -> Option<SyntaxNode>| {549 sema.descend_into_macros(token.clone())550 .into_iter()551 .filter_map(|token| node_filter(token.parent()?))552 .collect_vec()553 };554555 match token.kind() {556 T![match] => find_nodes(|node| Some(ast::MatchExpr::cast(node)?.syntax().clone())),557 T![=>] => find_nodes(|node| Some(ast::MatchArm::cast(node)?.syntax().clone())),558 T![if] => find_nodes(|node| {559 let if_expr = ast::IfExpr::cast(node)?;560561 let root_if = iter::successors(Some(if_expr.clone()), |if_expr| {562 let parent_if = if_expr.syntax().parent().and_then(ast::IfExpr::cast)?;563 let ast::ElseBranch::IfExpr(else_branch) = parent_if.else_branch()? else {564 return None;565 };566567 (else_branch.syntax() == if_expr.syntax()).then_some(parent_if)568 })569 .last()?;570571 Some(root_if.syntax().clone())572 }),573 _ => vec![],574 }575}576577fn nav_for_branch_exit_points(578 sema: &Semantics<'_, RootDatabase>,579 token: &SyntaxToken,580) -> Option<Vec<NavigationTarget>> {581 let db = sema.db;582583 let navs = match token.kind() {584 T![match] => find_branch_root(sema, token)585 .into_iter()586 .filter_map(|node| {587 let file_id = sema.hir_file_for(&node);588 let match_expr = ast::MatchExpr::cast(node)?;589 let focus_range = match_expr.match_token()?.text_range();590 let match_expr_in_file = InFile::new(file_id, match_expr.into());591 Some(expr_to_nav(db, match_expr_in_file, Some(focus_range)))592 })593 .flatten()594 .collect_vec(),595596 T![=>] => find_branch_root(sema, token)597 .into_iter()598 .filter_map(|node| {599 let match_arm = ast::MatchArm::cast(node)?;600 let match_expr = sema601 .ancestors_with_macros(match_arm.syntax().clone())602 .find_map(ast::MatchExpr::cast)?;603 let file_id = sema.hir_file_for(match_expr.syntax());604 let focus_range = match_arm.fat_arrow_token()?.text_range();605 let match_expr_in_file = InFile::new(file_id, match_expr.into());606 Some(expr_to_nav(db, match_expr_in_file, Some(focus_range)))607 })608 .flatten()609 .collect_vec(),610611 T![if] => find_branch_root(sema, token)612 .into_iter()613 .filter_map(|node| {614 let file_id = sema.hir_file_for(&node);615 let if_expr = ast::IfExpr::cast(node)?;616 let focus_range = if_expr.if_token()?.text_range();617 let if_expr_in_file = InFile::new(file_id, if_expr.into());618 Some(expr_to_nav(db, if_expr_in_file, Some(focus_range)))619 })620 .flatten()621 .collect_vec(),622623 _ => return Some(Vec::new()),624 };625626 Some(navs)627}628629fn nav_for_break_points(630 sema: &Semantics<'_, RootDatabase>,631 token: &SyntaxToken,632) -> Option<Vec<NavigationTarget>> {633 let db = sema.db;634635 let navs = find_loops(sema, token)?636 .filter_map(|expr| {637 let file_id = sema.hir_file_for(expr.syntax());638 let expr_in_file = InFile::new(file_id, expr.clone());639 let focus_range = match expr {640 ast::Expr::LoopExpr(loop_) => loop_.loop_token()?.text_range(),641 ast::Expr::WhileExpr(while_) => while_.while_token()?.text_range(),642 ast::Expr::ForExpr(for_) => for_.for_token()?.text_range(),643 // We guarantee that the label exists644 ast::Expr::BlockExpr(blk) => blk.label().unwrap().syntax().text_range(),645 _ => return None,646 };647 let nav = expr_to_nav(db, expr_in_file, Some(focus_range));648 Some(nav)649 })650 .flatten()651 .collect_vec();652653 Some(navs)654}655656fn def_to_nav(sema: &Semantics<'_, RootDatabase>, def: Definition<'_>) -> Vec<NavigationTarget> {657 def.try_to_nav(sema).map(|it| it.collect()).unwrap_or_default()658}659660fn expr_to_nav(661 db: &RootDatabase,662 InFile { file_id, value }: InFile<ast::Expr>,663 focus_range: Option<TextRange>,664) -> UpmappingResult<NavigationTarget> {665 let kind = SymbolKind::Label;666667 let value_range = value.syntax().text_range();668 let navs = navigation_target::orig_range_with_focus_r(db, file_id, value_range, focus_range);669 navs.map(|(hir::FileRangeWrapper { file_id, range }, focus_range)| {670 NavigationTarget::from_syntax(671 file_id,672 hir::Symbol::intern("<expr>"),673 focus_range,674 range,675 kind,676 )677 })678}679680#[cfg(test)]681mod tests {682 use crate::{GotoDefinitionConfig, fixture};683 use ide_db::{FileRange, ra_fixture::RaFixtureConfig};684 use itertools::Itertools;685686 const TEST_CONFIG: GotoDefinitionConfig<'_> =687 GotoDefinitionConfig { ra_fixture: RaFixtureConfig::default() };688689 #[track_caller]690 fn check(#[rust_analyzer::rust_fixture] ra_fixture: &str) {691 let (analysis, position, expected) = fixture::annotations(ra_fixture);692 let navs = analysis693 .goto_definition(position, &TEST_CONFIG)694 .unwrap()695 .expect("no definition found")696 .info;697698 let cmp = |&FileRange { file_id, range }: &_| (file_id, range.start());699 let navs = navs700 .into_iter()701 .map(|nav| FileRange { file_id: nav.file_id, range: nav.focus_or_full_range() })702 .sorted_by_key(cmp)703 .collect::<Vec<_>>();704 let expected = expected705 .into_iter()706 .map(|(FileRange { file_id, range }, _)| FileRange { file_id, range })707 .sorted_by_key(cmp)708 .collect::<Vec<_>>();709710 assert_eq!(expected, navs);711 }712713 fn check_unresolved(#[rust_analyzer::rust_fixture] ra_fixture: &str) {714 let (analysis, position) = fixture::position(ra_fixture);715 let navs = analysis716 .goto_definition(position, &TEST_CONFIG)717 .unwrap()718 .expect("no definition found")719 .info;720721 assert!(navs.is_empty(), "didn't expect this to resolve anywhere: {navs:?}")722 }723724 fn check_name(expected_name: &str, #[rust_analyzer::rust_fixture] ra_fixture: &str) {725 let (analysis, position, _) = fixture::annotations(ra_fixture);726 let navs = analysis727 .goto_definition(position, &TEST_CONFIG)728 .unwrap()729 .expect("no definition found")730 .info;731 assert!(navs.len() < 2, "expected single navigation target but encountered {}", navs.len());732 let Some(target) = navs.into_iter().next() else {733 panic!("expected single navigation target but encountered none");734 };735 assert_eq!(target.name, hir::Symbol::intern(expected_name));736 }737738 #[test]739 fn goto_def_pat_range_to_inclusive() {740 check_name(741 "RangeToInclusive",742 r#"743//- minicore: range744fn f(ch: char) -> bool {745 match ch {746 ..$0='z' => true,747 _ => false748 }749}750"#,751 );752 }753754 #[test]755 fn goto_def_pat_range_to() {756 check_name(757 "RangeTo",758 r#"759//- minicore: range760fn f(ch: char) -> bool {761 match ch {762 .$0.'z' => true,763 _ => false764 }765}766"#,767 );768 }769770 #[test]771 fn goto_def_pat_range() {772 check_name(773 "Range",774 r#"775//- minicore: range776fn f(ch: char) -> bool {777 match ch {778 'a'.$0.'z' => true,779 _ => false780 }781}782"#,783 );784 }785786 #[test]787 fn goto_def_pat_range_inclusive() {788 check_name(789 "RangeInclusive",790 r#"791//- minicore: range792fn f(ch: char) -> bool {793 match ch {794 'a'..$0='z' => true,795 _ => false796 }797}798"#,799 );800 }801802 #[test]803 fn goto_def_pat_range_from() {804 check_name(805 "RangeFrom",806 r#"807//- minicore: range808fn f(ch: char) -> bool {809 match ch {810 'a'..$0 => true,811 _ => false812 }813}814"#,815 );816 }817818 #[test]819 fn goto_def_expr_range() {820 check_name(821 "Range",822 r#"823//- minicore: range824let x = 0.$0.1;825"#,826 );827 }828829 #[test]830 fn goto_def_expr_range_from() {831 check_name(832 "RangeFrom",833 r#"834//- minicore: range835fn f(arr: &[i32]) -> &[i32] {836 &arr[0.$0.]837}838"#,839 );840 }841842 #[test]843 fn goto_def_expr_range_inclusive() {844 check_name(845 "RangeInclusive",846 r#"847//- minicore: range848let x = 0.$0.=1;849"#,850 );851 }852853 #[test]854 fn goto_def_expr_range_full() {855 check_name(856 "RangeFull",857 r#"858//- minicore: range859fn f(arr: &[i32]) -> &[i32] {860 &arr[.$0.]861}862"#,863 );864 }865866 #[test]867 fn goto_def_expr_range_to() {868 check_name(869 "RangeTo",870 r#"871//- minicore: range872fn f(arr: &[i32]) -> &[i32] {873 &arr[.$0.10]874}875"#,876 );877 }878879 #[test]880 fn goto_def_expr_range_to_inclusive() {881 check_name(882 "RangeToInclusive",883 r#"884//- minicore: range885fn f(arr: &[i32]) -> &[i32] {886 &arr[.$0.=10]887}888"#,889 );890 }891892 #[test]893 fn goto_def_in_included_file() {894 check(895 r#"896//- minicore:include897//- /main.rs898899include!("a.rs");900901fn main() {902 foo();903}904905//- /a.rs906fn func_in_include() {907 //^^^^^^^^^^^^^^^908}909910fn foo() {911 func_in_include$0();912}913"#,914 );915 }916917 #[test]918 fn goto_def_in_included_file_nested() {919 check(920 r#"921//- minicore:include922//- /main.rs923924macro_rules! passthrough {925 ($($tt:tt)*) => { $($tt)* }926}927928passthrough!(include!("a.rs"));929930fn main() {931 foo();932}933934//- /a.rs935fn func_in_include() {936 //^^^^^^^^^^^^^^^937}938939fn foo() {940 func_in_include$0();941}942"#,943 );944 }945946 #[test]947 fn goto_def_in_included_file_inside_mod() {948 check(949 r#"950//- minicore:include951//- /main.rs952mod a {953 include!("b.rs");954}955//- /b.rs956fn func_in_include() {957 //^^^^^^^^^^^^^^^958}959fn foo() {960 func_in_include$0();961}962"#,963 );964965 check(966 r#"967//- minicore:include968//- /main.rs969mod a {970 include!("a.rs");971}972//- /a.rs973fn func_in_include() {974 //^^^^^^^^^^^^^^^975}976977fn foo() {978 func_in_include$0();979}980"#,981 );982 }983984 #[test]985 fn goto_def_if_items_same_name() {986 check(987 r#"988trait Trait {989 type A;990 const A: i32;991 //^992}993994struct T;995impl Trait for T {996 type A = i32;997 const A$0: i32 = -9;998}"#,999 );1000 }10011002 #[test]1003 fn goto_def_array_length_prefers_value_namespace() {1004 check(1005 r#"1006struct N;10071008trait Trait {}10091010impl<const N: usize> Trait for [N; N$0] {}1011 //^1012"#,1013 );1014 }10151016 #[test]1017 fn goto_def_in_mac_call_in_attr_invoc() {1018 check(1019 r#"1020//- proc_macros: identity1021pub struct Struct {1022 // ^^^^^^1023 field: i32,1024}10251026macro_rules! identity {1027 ($($tt:tt)*) => {$($tt)*};1028}10291030#[proc_macros::identity]1031fn function() {1032 identity!(Struct$0 { field: 0 });1033}10341035"#,1036 )1037 }10381039 #[test]1040 fn goto_def_for_extern_crate() {1041 check(1042 r#"1043//- /main.rs crate:main deps:std1044extern crate std$0;1045//- /std/lib.rs crate:std1046// empty1047//^file1048"#,1049 )1050 }10511052 #[test]1053 fn goto_def_for_renamed_extern_crate() {1054 check(1055 r#"1056//- /main.rs crate:main deps:std1057extern crate std as abc$0;1058//- /std/lib.rs crate:std1059// empty1060//^file1061"#,1062 )1063 }10641065 #[test]1066 fn goto_def_in_items() {1067 check(1068 r#"1069struct Foo;1070 //^^^1071enum E { X(Foo$0) }1072"#,1073 );1074 }10751076 #[test]1077 fn goto_def_at_start_of_item() {1078 check(1079 r#"1080struct Foo;1081 //^^^1082enum E { X($0Foo) }1083"#,1084 );1085 }10861087 #[test]1088 fn goto_definition_resolves_correct_name() {1089 check(1090 r#"1091//- /lib.rs1092use a::Foo;1093mod a;1094mod b;1095enum E { X(Foo$0) }10961097//- /a.rs1098pub struct Foo;1099 //^^^1100//- /b.rs1101pub struct Foo;1102"#,1103 );1104 }11051106 #[test]1107 fn goto_def_for_module_declaration() {1108 check(1109 r#"1110//- /lib.rs1111mod $0foo;11121113//- /foo.rs1114// empty1115//^file1116"#,1117 );11181119 check(1120 r#"1121//- /lib.rs1122mod $0foo;11231124//- /foo/mod.rs1125// empty1126//^file1127"#,1128 );1129 }11301131 #[test]1132 fn goto_def_for_macros() {1133 check(1134 r#"1135macro_rules! foo { () => { () } }1136 //^^^1137fn bar() {1138 $0foo!();1139}1140"#,1141 );1142 }11431144 #[test]1145 fn goto_def_for_macros_from_other_crates() {1146 check(1147 r#"1148//- /lib.rs crate:main deps:foo1149use foo::foo;1150fn bar() {1151 $0foo!();1152}11531154//- /foo/lib.rs crate:foo1155#[macro_export]1156macro_rules! foo { () => { () } }1157 //^^^1158"#,1159 );1160 }11611162 #[test]1163 fn goto_def_for_macros_in_use_tree() {1164 check(1165 r#"1166//- /lib.rs crate:main deps:foo1167use foo::foo$0;11681169//- /foo/lib.rs crate:foo1170#[macro_export]1171macro_rules! foo { () => { () } }1172 //^^^1173"#,1174 );1175 }11761177 #[test]1178 fn goto_def_for_macro_defined_fn_with_arg() {1179 check(1180 r#"1181//- /lib.rs1182macro_rules! define_fn {1183 ($name:ident) => (fn $name() {})1184}11851186define_fn!(foo);1187 //^^^11881189fn bar() {1190 $0foo();1191}1192"#,1193 );1194 }11951196 #[test]1197 fn goto_def_for_macro_defined_fn_no_arg() {1198 check(1199 r#"1200//- /lib.rs1201macro_rules! define_fn {1202 () => (fn foo() {})1203 //^^^1204}12051206 define_fn!();1207//^^^^^^^^^^1208fn bar() {1209 $0foo();1210}1211"#,1212 );1213 }12141215 #[test]1216 fn goto_definition_works_for_macro_inside_pattern() {1217 check(1218 r#"1219//- /lib.rs1220macro_rules! foo {() => {0}}1221 //^^^12221223fn bar() {1224 match (0,1) {1225 ($0foo!(), _) => {}1226 }1227}1228"#,1229 );1230 }12311232 #[test]1233 fn goto_definition_works_for_macro_inside_match_arm_lhs() {1234 check(1235 r#"1236//- /lib.rs1237macro_rules! foo {() => {0}}1238 //^^^1239fn bar() {1240 match 0 {1241 $0foo!() => {}1242 }1243}1244"#,1245 );1246 }12471248 #[test]1249 fn goto_definition_works_for_consts_inside_range_pattern() {1250 check(1251 r#"1252//- /lib.rs1253const A: u32 = 0;1254 //^12551256fn bar(v: u32) {1257 match v {1258 0..=$0A => {}1259 _ => {}1260 }1261}1262"#,1263 );1264 }12651266 #[test]1267 fn goto_def_for_use_alias() {1268 check(1269 r#"1270//- /lib.rs crate:main deps:foo1271use foo as bar$0;12721273//- /foo/lib.rs crate:foo1274// empty1275//^file1276"#,1277 );1278 }12791280 #[test]1281 fn goto_def_for_use_alias_foo_macro() {1282 check(1283 r#"1284//- /lib.rs crate:main deps:foo1285use foo::foo as bar$0;12861287//- /foo/lib.rs crate:foo1288#[macro_export]1289macro_rules! foo { () => { () } }1290 //^^^1291"#,1292 );1293 }12941295 #[test]1296 fn goto_def_for_methods() {1297 check(1298 r#"1299struct Foo;1300impl Foo {1301 fn frobnicate(&self) { }1302 //^^^^^^^^^^1303}13041305fn bar(foo: &Foo) {1306 foo.frobnicate$0();1307}1308"#,1309 );1310 }13111312 #[test]1313 fn goto_def_for_fields() {1314 check(1315 r#"1316struct Foo {1317 spam: u32,1318} //^^^^13191320fn bar(foo: &Foo) {1321 foo.spam$0;1322}1323"#,1324 );1325 }13261327 #[test]1328 fn goto_def_for_record_fields() {1329 check(1330 r#"1331//- /lib.rs1332struct Foo {1333 spam: u32,1334} //^^^^13351336fn bar() -> Foo {1337 Foo {1338 spam$0: 0,1339 }1340}1341"#,1342 );1343 }13441345 #[test]1346 fn goto_def_for_record_pat_fields() {1347 check(1348 r#"1349//- /lib.rs1350struct Foo {1351 spam: u32,1352} //^^^^13531354fn bar(foo: Foo) -> Foo {1355 let Foo { spam$0: _, } = foo1356}1357"#,1358 );1359 }13601361 #[test]1362 fn goto_def_for_record_fields_macros() {1363 check(1364 r"1365macro_rules! m { () => { 92 };}1366struct Foo { spam: u32 }1367 //^^^^13681369fn bar() -> Foo {1370 Foo { spam$0: m!() }1371}1372",1373 );1374 }13751376 #[test]1377 fn goto_for_tuple_fields() {1378 check(1379 r#"1380struct Foo(u32);1381 //^^^13821383fn bar() {1384 let foo = Foo(0);1385 foo.$00;1386}1387"#,1388 );1389 }13901391 #[test]1392 fn goto_def_for_ufcs_inherent_methods() {1393 check(1394 r#"1395struct Foo;1396impl Foo {1397 fn frobnicate() { }1398} //^^^^^^^^^^13991400fn bar(foo: &Foo) {1401 Foo::frobnicate$0();1402}1403"#,1404 );1405 }14061407 #[test]1408 fn goto_def_for_ufcs_trait_methods_through_traits() {1409 check(1410 r#"1411trait Foo {1412 fn frobnicate();1413} //^^^^^^^^^^14141415fn bar() {1416 Foo::frobnicate$0();1417}1418"#,1419 );1420 }14211422 #[test]1423 fn goto_def_for_ufcs_trait_methods_through_self() {1424 check(1425 r#"1426struct Foo;1427trait Trait {1428 fn frobnicate();1429} //^^^^^^^^^^1430impl Trait for Foo {}14311432fn bar() {1433 Foo::frobnicate$0();1434}1435"#,1436 );1437 }14381439 #[test]1440 fn goto_definition_on_self() {1441 check(1442 r#"1443struct Foo;1444impl Foo {1445 //^^^1446 pub fn new() -> Self {1447 Self$0 {}1448 }1449}1450"#,1451 );1452 check(1453 r#"1454struct Foo;1455impl Foo {1456 //^^^1457 pub fn new() -> Self$0 {1458 Self {}1459 }1460}1461"#,1462 );14631464 check(1465 r#"1466enum Foo { A }1467impl Foo {1468 //^^^1469 pub fn new() -> Self$0 {1470 Foo::A1471 }1472}1473"#,1474 );14751476 check(1477 r#"1478enum Foo { A }1479impl Foo {1480 //^^^1481 pub fn thing(a: &Self$0) {1482 }1483}1484"#,1485 );1486 }14871488 #[test]1489 fn goto_definition_on_self_in_trait_impl() {1490 check(1491 r#"1492struct Foo;1493trait Make {1494 fn new() -> Self;1495}1496impl Make for Foo {1497 //^^^1498 fn new() -> Self {1499 Self$0 {}1500 }1501}1502"#,1503 );15041505 check(1506 r#"1507struct Foo;1508trait Make {1509 fn new() -> Self;1510}1511impl Make for Foo {1512 //^^^1513 fn new() -> Self$0 {1514 Self {}1515 }1516}1517"#,1518 );1519 }15201521 #[test]1522 fn goto_def_when_used_on_definition_name_itself() {1523 check(1524 r#"1525struct Foo$0 { value: u32 }1526 //^^^1527 "#,1528 );15291530 check(1531 r#"1532struct Foo {1533 field$0: string,1534} //^^^^^1535"#,1536 );15371538 check(1539 r#"1540fn foo_test$0() { }1541 //^^^^^^^^1542"#,1543 );15441545 check(1546 r#"1547enum Foo$0 { Variant }1548 //^^^1549"#,1550 );15511552 check(1553 r#"1554enum Foo {1555 Variant1,1556 Variant2$0,1557 //^^^^^^^^1558 Variant3,1559}1560"#,1561 );15621563 check(1564 r#"1565static INNER$0: &str = "";1566 //^^^^^1567"#,1568 );15691570 check(1571 r#"1572const INNER$0: &str = "";1573 //^^^^^1574"#,1575 );15761577 check(1578 r#"1579type Thing$0 = Option<()>;1580 //^^^^^1581"#,1582 );15831584 check(1585 r#"1586trait Foo$0 { }1587 //^^^1588"#,1589 );15901591 check(1592 r#"1593trait Foo$0 = ;1594 //^^^1595"#,1596 );15971598 check(1599 r#"1600mod bar$0 { }1601 //^^^1602"#,1603 );1604 }16051606 #[test]1607 fn goto_from_macro() {1608 check(1609 r#"1610macro_rules! id {1611 ($($tt:tt)*) => { $($tt)* }1612}1613fn foo() {}1614 //^^^1615id! {1616 fn bar() {1617 fo$0o();1618 }1619}1620mod confuse_index { fn foo(); }1621"#,1622 );1623 }16241625 #[test]1626 fn goto_through_format() {1627 check(1628 r#"1629//- minicore: fmt1630#[macro_export]1631macro_rules! format {1632 ($($arg:tt)*) => ($crate::fmt::format($crate::__export::format_args!($($arg)*)))1633}1634pub mod __export {1635 pub use core::format_args;1636 fn foo() {} // for index confusion1637}1638fn foo() -> i8 {}1639 //^^^1640fn test() {1641 format!("{}", fo$0o())1642}1643"#,1644 );1645 }16461647 #[test]1648 fn goto_through_included_file() {1649 check(1650 r#"1651//- /main.rs1652#[rustc_builtin_macro]1653macro_rules! include {}16541655include!("foo.rs");16561657fn f() {1658 foo$0();1659}16601661mod confuse_index {1662 pub fn foo() {}1663}16641665//- /foo.rs1666fn foo() {}1667 //^^^1668 "#,1669 );1670 }16711672 #[test]1673 fn goto_through_included_file_struct_with_doc_comment() {1674 check(1675 r#"1676//- /main.rs1677#[rustc_builtin_macro]1678macro_rules! include {}16791680include!("foo.rs");16811682fn f() {1683 let x = Foo$0;1684}16851686mod confuse_index {1687 pub struct Foo;1688}16891690//- /foo.rs1691/// This is a doc comment1692pub struct Foo;1693 //^^^1694 "#,1695 );1696 }16971698 #[test]1699 fn goto_for_type_param() {1700 check(1701 r#"1702struct Foo<T: Clone> { t: $0T }1703 //^1704"#,1705 );1706 }17071708 #[test]1709 fn goto_within_macro() {1710 check(1711 r#"1712macro_rules! id {1713 ($($tt:tt)*) => ($($tt)*)1714}17151716fn foo() {1717 let x = 1;1718 //^1719 id!({1720 let y = $0x;1721 let z = y;1722 });1723}1724"#,1725 );17261727 check(1728 r#"1729macro_rules! id {1730 ($($tt:tt)*) => ($($tt)*)1731}17321733fn foo() {1734 let x = 1;1735 id!({1736 let y = x;1737 //^1738 let z = $0y;1739 });1740}1741"#,1742 );1743 }17441745 #[test]1746 fn goto_def_in_local_fn() {1747 check(1748 r#"1749fn main() {1750 fn foo() {1751 let x = 92;1752 //^1753 $0x;1754 }1755}1756"#,1757 );1758 }17591760 #[test]1761 fn goto_def_in_local_macro() {1762 check(1763 r#"1764fn bar() {1765 macro_rules! foo { () => { () } }1766 //^^^1767 $0foo!();1768}1769"#,1770 );1771 }17721773 #[test]1774 fn goto_def_for_field_init_shorthand() {1775 check(1776 r#"1777struct Foo { x: i32 }1778 //^1779fn main() {1780 let x = 92;1781 //^1782 Foo { x$0 };1783}1784"#,1785 )1786 }17871788 #[test]1789 fn goto_def_for_enum_variant_field() {1790 check(1791 r#"1792enum Foo {1793 Bar { x: i32 }1794 //^1795}1796fn baz(foo: Foo) {1797 match foo {1798 Foo::Bar { x$0 } => x1799 //^1800 };1801}1802"#,1803 );1804 }18051806 #[test]1807 fn goto_def_for_enum_variant_self_pattern_const() {1808 check(1809 r#"1810enum Foo { Bar }1811 //^^^1812impl Foo {1813 fn baz(self) {1814 match self { Self::Bar$0 => {} }1815 }1816}1817"#,1818 );1819 }18201821 #[test]1822 fn goto_def_for_enum_variant_self_pattern_record() {1823 check(1824 r#"1825enum Foo { Bar { val: i32 } }1826 //^^^1827impl Foo {1828 fn baz(self) -> i32 {1829 match self { Self::Bar$0 { val } => {} }1830 }1831}1832"#,1833 );1834 }18351836 #[test]1837 fn goto_def_for_enum_variant_self_expr_const() {1838 check(1839 r#"1840enum Foo { Bar }1841 //^^^1842impl Foo {1843 fn baz(self) { Self::Bar$0; }1844}1845"#,1846 );1847 }18481849 #[test]1850 fn goto_def_for_enum_variant_self_expr_record() {1851 check(1852 r#"1853enum Foo { Bar { val: i32 } }1854 //^^^1855impl Foo {1856 fn baz(self) { Self::Bar$0 {val: 4}; }1857}1858"#,1859 );1860 }18611862 #[test]1863 fn goto_def_for_type_alias_generic_parameter() {1864 check(1865 r#"1866type Alias<T> = T$0;1867 //^1868"#,1869 )1870 }18711872 #[test]1873 fn goto_def_for_macro_container() {1874 check(1875 r#"1876//- /lib.rs crate:main deps:foo1877foo::module$0::mac!();18781879//- /foo/lib.rs crate:foo1880pub mod module {1881 //^^^^^^1882 #[macro_export]1883 macro_rules! _mac { () => { () } }1884 pub use crate::_mac as mac;1885}1886"#,1887 );1888 }18891890 #[test]1891 fn goto_def_for_assoc_ty_in_path() {1892 check(1893 r#"1894trait Iterator {1895 type Item;1896 //^^^^1897}18981899fn f() -> impl Iterator<Item$0 = u8> {}1900"#,1901 );1902 }19031904 #[test]1905 fn goto_def_for_super_assoc_ty_in_path() {1906 check(1907 r#"1908trait Super {1909 type Item;1910 //^^^^1911}19121913trait Sub: Super {}19141915fn f() -> impl Sub<Item$0 = u8> {}1916"#,1917 );1918 }19191920 #[test]1921 fn goto_def_for_module_declaration_in_path_if_types_and_values_same_name() {1922 check(1923 r#"1924mod bar {1925 pub struct Foo {}1926 //^^^1927 pub fn Foo() {}1928}19291930fn baz() {1931 let _foo_enum: bar::Foo$0 = bar::Foo {};1932}1933 "#,1934 )1935 }19361937 #[test]1938 fn unknown_assoc_ty() {1939 check_unresolved(1940 r#"1941trait Iterator { type Item; }1942fn f() -> impl Iterator<Invalid$0 = u8> {}1943"#,1944 )1945 }19461947 #[test]1948 fn goto_def_for_assoc_ty_in_path_multiple() {1949 check(1950 r#"1951trait Iterator {1952 type A;1953 //^1954 type B;1955}19561957fn f() -> impl Iterator<A$0 = u8, B = ()> {}1958"#,1959 );1960 check(1961 r#"1962trait Iterator {1963 type A;1964 type B;1965 //^1966}19671968fn f() -> impl Iterator<A = u8, B$0 = ()> {}1969"#,1970 );1971 }19721973 #[test]1974 fn goto_def_for_assoc_ty_ufcs() {1975 check(1976 r#"1977trait Iterator {1978 type Item;1979 //^^^^1980}19811982fn g() -> <() as Iterator<Item$0 = ()>>::Item {}1983"#,1984 );1985 }19861987 #[test]1988 fn goto_def_for_assoc_ty_ufcs_multiple() {1989 check(1990 r#"1991trait Iterator {1992 type A;1993 //^1994 type B;1995}19961997fn g() -> <() as Iterator<A$0 = (), B = u8>>::B {}1998"#,1999 );2000 check(
Same data, no extra tab — call code_get_file + code_get_findings over MCP from Claude/Cursor/Copilot.