src/tools/rust-analyzer/crates/ide-diagnostics/src/handlers/missing_fields.rs RUST 985 lines View on github.com → Search inside
1use either::Either;2use hir::{3    AssocItem, FindPathConfig, HasVisibility, HirDisplay, InFile, Type,4    db::{ExpandDatabase, HirDatabase},5    sym,6};7use ide_db::{8    FxHashMap,9    assists::{Assist, ExprFillDefaultMode},10    famous_defs::FamousDefs,11    imports::import_assets::item_for_path_search,12    source_change::SourceChange,13    syntax_helpers::tree_diff::diff,14    text_edit::TextEdit,15    use_trivial_constructor::use_trivial_constructor,16};17use stdx::format_to;18use syntax::{19    AstNode, Edition, SyntaxNode, SyntaxNodePtr, ToSmolStr,20    ast::{self, make},21};2223use crate::{Diagnostic, DiagnosticCode, DiagnosticsContext, fix};2425// Diagnostic: missing-fields26//27// This diagnostic is triggered if record lacks some fields that exist in the corresponding structure.28//29// Example:30//31// ```rust32// struct A { a: u8, b: u8 }33//34// let a = A { a: 10 };35// ```36pub(crate) fn missing_fields(ctx: &DiagnosticsContext<'_>, d: &hir::MissingFields) -> Diagnostic {37    let mut message = String::from("missing structure fields:\n");38    for (field, _) in &d.missed_fields {39        format_to!(message, "- {}\n", field.display(ctx.sema.db, ctx.edition));40    }4142    let ptr = InFile::new(43        d.file,44        d.field_list_parent_path45            .map(SyntaxNodePtr::from)46            .unwrap_or_else(|| d.field_list_parent.into()),47    );4849    Diagnostic::new_with_syntax_node_ptr(ctx, DiagnosticCode::RustcHardError("E0063"), message, ptr)50        .stable()51        .with_fixes(fixes(ctx, d))52}5354fn fixes(ctx: &DiagnosticsContext<'_>, d: &hir::MissingFields) -> Option<Vec<Assist>> {55    // Note that although we could add a diagnostics to56    // fill the missing tuple field, e.g :57    // `struct A(usize);`58    // `let a = A { 0: () }`59    // but it is uncommon usage and it should not be encouraged.60    if d.missed_fields.iter().any(|(name, _)| name.as_tuple_index().is_some()) {61        return None;62    }6364    let root = ctx.sema.db.parse_or_expand(d.file);6566    let current_module =67        ctx.sema.scope(d.field_list_parent.to_node(&root).syntax()).map(|it| it.module());68    let range = InFile::new(d.file, d.field_list_parent.text_range())69        .original_node_file_range_rooted_opt(ctx.sema.db)?;7071    if let Some(current_module) = current_module72        && d.missed_fields.iter().any(|(_, field)| !field.is_visible_from(ctx.db(), current_module))73    {74        return None;75    }7677    let build_text_edit = |new_syntax: &SyntaxNode, old_syntax| {78        let edit = {79            let old_range = ctx.sema.original_range_opt(old_syntax)?;80            if old_range.file_id != range.file_id {81                return None;82            }83            let mut builder = TextEdit::builder();84            if d.file.is_macro() {85                // we can't map the diff up into the macro input unfortunately, as the macro loses all86                // whitespace information so the diff wouldn't be applicable no matter what87                // This has the downside that the cursor will be moved in macros by doing it without a diff88                // but that is a trade off we can make.89                // FIXME: this also currently discards a lot of whitespace in the input... we really need a formatter here90                builder.replace(old_range.range, new_syntax.to_string());91            } else {92                diff(old_syntax, new_syntax).into_text_edit(&mut builder);93            }94            builder.finish()95        };96        Some(vec![fix(97            "fill_missing_fields",98            "Fill struct fields",99            SourceChange::from_text_edit(range.file_id.file_id(ctx.sema.db), edit),100            range.range,101        )])102    };103104    match &d.field_list_parent.to_node(&root) {105        Either::Left(field_list_parent) => {106            let missing_fields = ctx.sema.record_literal_missing_fields(field_list_parent);107108            let mut locals = FxHashMap::default();109            ctx.sema.scope(field_list_parent.syntax())?.process_all_names(&mut |name, def| {110                if let hir::ScopeDef::Local(local) = def {111                    locals.insert(name, local);112                }113            });114115            let generate_fill_expr = |ty: &Type<'_>| match ctx.config.expr_fill_default {116                ExprFillDefaultMode::Todo => make::ext::expr_todo(),117                ExprFillDefaultMode::Underscore => make::ext::expr_underscore(),118                ExprFillDefaultMode::Default => {119                    get_default_constructor(ctx, d, ty).unwrap_or_else(make::ext::expr_todo)120                }121            };122123            let old_field_list = field_list_parent.record_expr_field_list()?;124            let new_field_list = old_field_list.clone_for_update();125            for (f, ty) in missing_fields.iter() {126                let field_expr = if let Some(local_candidate) = locals.get(&f.name(ctx.sema.db)) {127                    cov_mark::hit!(field_shorthand);128                    let candidate_ty = local_candidate.ty(ctx.sema.db);129                    if candidate_ty.could_coerce_to(ctx.sema.db, ty) {130                        None131                    } else {132                        Some(generate_fill_expr(ty))133                    }134                } else {135                    let expr = (|| -> Option<ast::Expr> {136                        let item_in_ns = hir::ItemInNs::from(hir::ModuleDef::from(ty.as_adt()?));137138                        let type_path = current_module?.find_path(139                            ctx.sema.db,140                            item_for_path_search(ctx.sema.db, item_in_ns)?,141                            FindPathConfig {142                                prefer_no_std: ctx.config.prefer_no_std,143                                prefer_prelude: ctx.config.prefer_prelude,144                                prefer_absolute: ctx.config.prefer_absolute,145                                allow_unstable: ctx.is_nightly,146                            },147                        )?;148149                        use_trivial_constructor(150                            ctx.sema.db,151                            ide_db::helpers::mod_path_to_ast(&type_path, ctx.edition),152                            ty,153                            ctx.edition,154                        )155                    })();156157                    if expr.is_some() { expr } else { Some(generate_fill_expr(ty)) }158                };159                let field = make::record_expr_field(160                    make::name_ref(&f.name(ctx.sema.db).display_no_db(ctx.edition).to_smolstr()),161                    field_expr,162                );163                new_field_list.add_field(field.clone_for_update());164            }165            build_text_edit(new_field_list.syntax(), old_field_list.syntax())166        }167        Either::Right(field_list_parent) => {168            let missing_fields = ctx.sema.record_pattern_missing_fields(field_list_parent);169170            let old_field_list = field_list_parent.record_pat_field_list()?;171            let new_field_list = old_field_list.clone_for_update();172            for (f, _) in missing_fields.iter() {173                let field = make::record_pat_field_shorthand(174                    make::ident_pat(175                        false,176                        false,177                        make::name(&f.name(ctx.sema.db).display_no_db(ctx.edition).to_smolstr()),178                    )179                    .into(),180                );181                new_field_list.add_field(field.clone_for_update());182            }183            build_text_edit(new_field_list.syntax(), old_field_list.syntax())184        }185    }186}187188fn make_ty(189    ty: &hir::Type<'_>,190    db: &dyn HirDatabase,191    module: hir::Module,192    edition: Edition,193) -> ast::Type {194    let ty_str = match ty.as_adt() {195        Some(adt) => adt.name(db).display(db, edition).to_string(),196        None => {197            ty.display_source_code(db, module.into(), false).ok().unwrap_or_else(|| "_".to_owned())198        }199    };200201    make::ty(&ty_str)202}203204fn get_default_constructor(205    ctx: &DiagnosticsContext<'_>,206    d: &hir::MissingFields,207    ty: &Type<'_>,208) -> Option<ast::Expr> {209    if let Some(builtin_ty) = ty.as_builtin() {210        if builtin_ty.is_int() || builtin_ty.is_uint() {211            return Some(make::ext::zero_number());212        }213        if builtin_ty.is_float() {214            return Some(make::ext::zero_float());215        }216        if builtin_ty.is_char() {217            return Some(make::ext::empty_char());218        }219        if builtin_ty.is_str() {220            return Some(make::ext::empty_str());221        }222        if builtin_ty.is_bool() {223            return Some(make::ext::default_bool());224        }225    }226227    let krate = ctx228        .sema229        .file_to_module_def(d.file.original_file(ctx.sema.db).file_id(ctx.sema.db))?230        .krate(ctx.sema.db);231    let module = krate.root_module(ctx.sema.db);232233    // Look for a ::new() associated function234    let has_new_func = ty235        .iterate_assoc_items(ctx.sema.db, |assoc_item| {236            if let AssocItem::Function(func) = assoc_item237                && func.name(ctx.sema.db) == sym::new238                && func.assoc_fn_params(ctx.sema.db).is_empty()239            {240                return Some(());241            }242243            None244        })245        .is_some();246247    let famous_defs = FamousDefs(&ctx.sema, krate);248    if has_new_func {249        Some(make::ext::expr_ty_new(&make_ty(ty, ctx.sema.db, module, ctx.edition)))250    } else if ty.as_adt() == famous_defs.core_option_Option()?.ty(ctx.sema.db).as_adt() {251        Some(make::ext::option_none())252    } else if !ty.is_array()253        && ty.impls_trait(ctx.sema.db, famous_defs.core_default_Default()?, &[])254    {255        Some(make::ext::expr_ty_default(&make_ty(ty, ctx.sema.db, module, ctx.edition)))256    } else {257        None258    }259}260261#[cfg(test)]262mod tests {263    use crate::tests::{check_diagnostics, check_fix, check_no_fix};264265    #[test]266    fn missing_record_pat_field_diagnostic() {267        check_diagnostics(268            r#"269struct S { foo: i32, bar: () }270fn baz(s: S) {271    let S { foo: _ } = s;272      //^ 💡 error: missing structure fields:273      //| - bar274}275"#,276        );277    }278279    #[test]280    fn missing_record_pat_field_no_diagnostic_if_not_exhaustive() {281        check_diagnostics(282            r"283struct S { foo: i32, bar: () }284fn baz(s: S) -> i32 {285    match s {286        S { foo, .. } => foo,287    }288}289",290        )291    }292293    #[test]294    fn missing_record_pat_field_box() {295        check_diagnostics(296            r"297struct S { s: Box<u32> }298fn x(a: S) {299    let S { box s } = a;300}301",302        )303    }304305    #[test]306    fn missing_record_pat_field_ref() {307        check_diagnostics(308            r"309struct S { s: u32 }310fn x(a: S) {311    let S { ref s } = a;312    _ = s;313}314",315        )316    }317318    #[test]319    fn missing_record_expr_in_assignee_expr() {320        check_diagnostics(321            r"322struct S { s: usize, t: usize }323struct S2 { s: S, t: () }324struct T(S);325fn regular(a: S) {326    let s;327    S { s, .. } = a;328    _ = s;329}330fn nested(a: S2) {331    let s;332    S2 { s: S { s, .. }, .. } = a;333    _ = s;334}335fn in_tuple(a: (S,)) {336    let s;337    (S { s, .. },) = a;338    _ = s;339}340fn in_array(a: [S;1]) {341    let s;342    [S { s, .. },] = a;343    _ = s;344}345fn in_tuple_struct(a: T) {346    let s;347    T(S { s, .. }) = a;348    _ = s;349}350            ",351        );352    }353354    #[test]355    fn range_mapping_out_of_macros() {356        check_fix(357            r#"358fn some() {}359fn items() {}360fn here() {}361362macro_rules! id { ($($tt:tt)*) => { $($tt)*}; }363364fn main() {365    let _x = id![Foo { a: $042 }];366}367368pub struct Foo { pub a: i32, pub b: i32 }369"#,370            r#"371fn some() {}372fn items() {}373fn here() {}374375macro_rules! id { ($($tt:tt)*) => { $($tt)*}; }376377fn main() {378    let _x = id![Foo {a:42, b: 0 }];379}380381pub struct Foo { pub a: i32, pub b: i32 }382"#,383        );384    }385386    #[test]387    fn test_fill_struct_fields_empty() {388        check_fix(389            r#"390//- minicore: option391struct TestStruct { one: i32, two: i64, three: Option<i32>, four: bool }392393fn test_fn() {394    let s = TestStruct {$0};395}396"#,397            r#"398struct TestStruct { one: i32, two: i64, three: Option<i32>, four: bool }399400fn test_fn() {401    let s = TestStruct { one: 0, two: 0, three: None, four: false };402}403"#,404        );405    }406407    #[test]408    fn test_fill_struct_zst_fields() {409        check_fix(410            r#"411struct Empty;412413struct TestStruct { one: i32, two: Empty }414415fn test_fn() {416    let s = TestStruct {$0};417}418"#,419            r#"420struct Empty;421422struct TestStruct { one: i32, two: Empty }423424fn test_fn() {425    let s = TestStruct { one: 0, two: Empty };426}427"#,428        );429        check_fix(430            r#"431enum Empty { Foo };432433struct TestStruct { one: i32, two: Empty }434435fn test_fn() {436    let s = TestStruct {$0};437}438"#,439            r#"440enum Empty { Foo };441442struct TestStruct { one: i32, two: Empty }443444fn test_fn() {445    let s = TestStruct { one: 0, two: Empty::Foo };446}447"#,448        );449450        // make sure the assist doesn't fill non Unit variants451        check_fix(452            r#"453struct Empty {};454455struct TestStruct { one: i32, two: Empty }456457fn test_fn() {458    let s = TestStruct {$0};459}460"#,461            r#"462struct Empty {};463464struct TestStruct { one: i32, two: Empty }465466fn test_fn() {467    let s = TestStruct { one: 0, two: todo!() };468}469"#,470        );471        check_fix(472            r#"473enum Empty { Foo {} };474475struct TestStruct { one: i32, two: Empty }476477fn test_fn() {478    let s = TestStruct {$0};479}480"#,481            r#"482enum Empty { Foo {} };483484struct TestStruct { one: i32, two: Empty }485486fn test_fn() {487    let s = TestStruct { one: 0, two: todo!() };488}489"#,490        );491    }492493    #[test]494    fn test_fill_struct_fields_self() {495        check_fix(496            r#"497struct TestStruct { one: i32 }498499impl TestStruct {500    fn test_fn() { let s = Self {$0}; }501}502"#,503            r#"504struct TestStruct { one: i32 }505506impl TestStruct {507    fn test_fn() { let s = Self { one: 0 }; }508}509"#,510        );511    }512513    #[test]514    fn test_fill_struct_fields_enum() {515        check_fix(516            r#"517enum Expr {518    Bin { lhs: Box<Expr>, rhs: Box<Expr> }519}520521impl Expr {522    fn new_bin(lhs: Box<Expr>, rhs: Box<Expr>) -> Expr {523        Expr::Bin {$0 }524    }525}526"#,527            r#"528enum Expr {529    Bin { lhs: Box<Expr>, rhs: Box<Expr> }530}531532impl Expr {533    fn new_bin(lhs: Box<Expr>, rhs: Box<Expr>) -> Expr {534        Expr::Bin { lhs, rhs }535    }536}537"#,538        );539    }540541    #[test]542    fn test_fill_struct_fields_partial() {543        check_fix(544            r#"545struct TestStruct { one: i32, two: i64 }546547fn test_fn() {548    let s = TestStruct{ two: 2$0 };549}550"#,551            r"552struct TestStruct { one: i32, two: i64 }553554fn test_fn() {555    let s = TestStruct{ two: 2, one: 0 };556}557",558        );559    }560561    #[test]562    fn test_fill_struct_fields_new() {563        check_fix(564            r#"565struct TestWithNew(usize);566impl TestWithNew {567    pub fn new() -> Self {568        Self(0)569    }570}571struct TestStruct { one: i32, two: TestWithNew }572573fn test_fn() {574    let s = TestStruct{ $0 };575}576"#,577            r"578struct TestWithNew(usize);579impl TestWithNew {580    pub fn new() -> Self {581        Self(0)582    }583}584struct TestStruct { one: i32, two: TestWithNew }585586fn test_fn() {587    let s = TestStruct{ one: 0, two: TestWithNew::new()  };588}589",590        );591    }592593    #[test]594    fn test_fill_struct_fields_default() {595        check_fix(596            r#"597//- minicore: default, option, slice598struct TestWithDefault(usize);599impl Default for TestWithDefault {600    pub fn default() -> Self {601        Self(0)602    }603}604struct TestStruct { one: i32, two: TestWithDefault, r: &'static [i32] }605606fn test_fn() {607    let s = TestStruct{ $0 };608}609"#,610            r"611struct TestWithDefault(usize);612impl Default for TestWithDefault {613    pub fn default() -> Self {614        Self(0)615    }616}617struct TestStruct { one: i32, two: TestWithDefault, r: &'static [i32] }618619fn test_fn() {620    let s = TestStruct{ one: 0, two: TestWithDefault::default(), r: <&'static [i32]>::default()  };621}622",623        );624    }625626    #[test]627    fn test_fill_struct_fields_raw_ident() {628        check_fix(629            r#"630struct TestStruct { r#type: u8 }631632fn test_fn() {633    TestStruct { $0 };634}635"#,636            r"637struct TestStruct { r#type: u8 }638639fn test_fn() {640    TestStruct { r#type: 0  };641}642",643        );644    }645646    #[test]647    fn test_fill_struct_fields_no_diagnostic() {648        check_diagnostics(649            r#"650struct TestStruct { one: i32, two: i64 }651652fn test_fn() {653    let one = 1;654    let _s = TestStruct{ one, two: 2 };655}656        "#,657        );658    }659660    #[test]661    fn test_fill_struct_fields_no_diagnostic_on_spread() {662        check_diagnostics(663            r#"664struct TestStruct { one: i32, two: i64 }665666fn test_fn() {667    let one = 1;668    let a = TestStruct{ one, two: 2 };669    let _ = TestStruct{ ..a };670}671"#,672        );673    }674675    #[test]676    fn test_fill_struct_fields_blank_line() {677        check_fix(678            r#"679struct S { a: (), b: () }680681fn f() {682    S {683        $0684    };685}686"#,687            r#"688struct S { a: (), b: () }689690fn f() {691    S {692        a: todo!(),693        b: todo!(),694    };695}696"#,697        );698    }699700    #[test]701    fn test_fill_struct_fields_shorthand() {702        cov_mark::check!(field_shorthand);703        check_fix(704            r#"705struct S { a: &'static str, b: i32 }706707fn f() {708    let a = "hello";709    let b = 1i32;710    S {711        $0712    };713}714"#,715            r#"716struct S { a: &'static str, b: i32 }717718fn f() {719    let a = "hello";720    let b = 1i32;721    S {722        a,723        b,724    };725}726"#,727        );728    }729730    #[test]731    fn test_fill_struct_fields_shorthand_ty_mismatch() {732        check_fix(733            r#"734struct S { a: &'static str, b: i32 }735736fn f() {737    let a = "hello";738    let b = 1usize;739    S {740        $0741    };742}743"#,744            r#"745struct S { a: &'static str, b: i32 }746747fn f() {748    let a = "hello";749    let b = 1usize;750    S {751        a,752        b: 0,753    };754}755"#,756        );757    }758759    #[test]760    fn test_fill_struct_fields_shorthand_unifies() {761        check_fix(762            r#"763struct S<T> { a: &'static str, b: T }764765fn f() {766    let a = "hello";767    let b = 1i32;768    S {769        $0770    };771}772"#,773            r#"774struct S<T> { a: &'static str, b: T }775776fn f() {777    let a = "hello";778    let b = 1i32;779    S {780        a,781        b,782    };783}784"#,785        );786    }787788    #[test]789    fn test_fill_struct_pat_fields() {790        check_fix(791            r#"792struct S { a: &'static str, b: i32 }793794fn f() {795    let S {796        $0797    };798}799"#,800            r#"801struct S { a: &'static str, b: i32 }802803fn f() {804    let S {805        a,806        b,807    };808}809"#,810        );811    }812813    #[test]814    fn test_fill_struct_pat_fields_partial() {815        check_fix(816            r#"817struct S { a: &'static str, b: i32 }818819fn f() {820    let S {821        a,$0822    };823}824"#,825            r#"826struct S { a: &'static str, b: i32 }827828fn f() {829    let S {830        a,831        b,832    };833}834"#,835        );836    }837838    #[test]839    fn import_extern_crate_clash_with_inner_item() {840        // This is more of a resolver test, but doesn't really work with the hir_def testsuite.841842        check_diagnostics(843            r#"844//- /lib.rs crate:lib deps:jwt845mod permissions;846847use permissions::jwt;848849fn f() {850    fn inner() {}851    jwt::Claims {}; // should resolve to the local one with 0 fields, and not get a diagnostic852}853854//- /permissions.rs855pub mod jwt  {856    pub struct Claims {}857}858859//- /jwt/lib.rs crate:jwt860pub struct Claims {861    field: u8,862}863        "#,864        );865    }866867    #[test]868    fn test_default_field_values_basic() {869        // This should work without errors - only field 'b' is required870        check_diagnostics(871            r#"872#![feature(default_field_values)]873struct Struct {874    a: usize = 0,875    b: usize,876}877878fn main() {879    Struct { b: 1, .. };880}881"#,882        );883    }884885    #[test]886    fn test_default_field_values_missing_field_error() {887        // This should report a missing field error because email is required888        check_diagnostics(889            r#"890#![feature(default_field_values)]891struct UserInfo {892    id: i32,893    age: f32 = 1.0,894    email: String,895}896897fn main() {898    UserInfo { id: 20, .. };899//  ^^^^^^^^💡 error: missing structure fields:900//         |- email901}902"#,903        );904    }905906    #[test]907    fn test_default_field_values_requires_spread_syntax() {908        // without `..` should report missing fields909        check_diagnostics(910            r#"911#![feature(default_field_values)]912struct Point {913    x: i32 = 0,914    y: i32 = 0,915}916917fn main() {918    Point { x: 0 };919//  ^^^^^💡 error: missing structure fields:920//      |- y921}922"#,923        );924    }925926    #[test]927    fn test_default_field_values_pattern_matching() {928        check_diagnostics(929            r#"930#![feature(default_field_values)]931struct Point {932    x: i32 = 0,933    y: i32 = 0,934    z: i32,935}936937fn main() {938    let Point { x, .. } = Point { z: 5, .. };939}940"#,941        );942    }943944    #[test]945    fn coerce_existing_local() {946        check_fix(947            r#"948struct A {949    v: f64,950}951952fn f() -> A {953    let v = loop {};954    A {$0}955}956        "#,957            r#"958struct A {959    v: f64,960}961962fn f() -> A {963    let v = loop {};964    A { v }965}966        "#,967        );968    }969970    #[test]971    fn inaccessible_fields() {972        check_no_fix(973            r#"974mod foo {975    pub struct Bar { baz: i32 }976}977978fn qux() {979    foo::Bar {$0};980}981        "#,982        );983    }984}

Code quality findings 7

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>.
warning correctness unchecked-indexing
struct TestStruct { one: i32, two: TestWithDefault, r: &'static [i32] }
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>.
warning correctness unchecked-indexing
struct TestStruct { one: i32, two: TestWithDefault, r: &'static [i32] }
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>.
warning correctness unchecked-indexing
let s = TestStruct{ one: 0, two: TestWithDefault::default(), r: <&'static [i32]>::default() };
Maintainability Info: `todo!()` or `unimplemented!()` macros indicate incomplete code paths that will panic at runtime if reached. Ensure these are replaced with actual logic before production use.
info correctness todo-unimplemented
let s = TestStruct { one: 0, two: todo!() };
Maintainability Info: `todo!()` or `unimplemented!()` macros indicate incomplete code paths that will panic at runtime if reached. Ensure these are replaced with actual logic before production use.
info correctness todo-unimplemented
let s = TestStruct { one: 0, two: todo!() };
Maintainability Info: `todo!()` or `unimplemented!()` macros indicate incomplete code paths that will panic at runtime if reached. Ensure these are replaced with actual logic before production use.
info correctness todo-unimplemented
a: todo!(),
Maintainability Info: `todo!()` or `unimplemented!()` macros indicate incomplete code paths that will panic at runtime if reached. Ensure these are replaced with actual logic before production use.
info correctness todo-unimplemented
b: todo!(),

Get this view in your editor

Same data, no extra tab — call code_get_file + code_get_findings over MCP from Claude/Cursor/Copilot.