Unnamed repository; edit this file 'description' to name the repository.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
use crate::{Diagnostic, DiagnosticCode, DiagnosticsContext};

// Diagnostic: private-assoc-item
//
// This diagnostic is triggered if the referenced associated item is not visible from the current
// module.
pub(crate) fn private_assoc_item(
    ctx: &DiagnosticsContext<'_>,
    d: &hir::PrivateAssocItem,
) -> Diagnostic {
    // FIXME: add quickfix
    let name = d
        .item
        .name(ctx.sema.db)
        .map(|name| format!("`{}` ", name.display(ctx.sema.db)))
        .unwrap_or_default();
    Diagnostic::new_with_syntax_node_ptr(
        ctx,
        DiagnosticCode::RustcHardError("E0624"),
        format!(
            "{} {}is private",
            match d.item {
                hir::AssocItem::Function(_) => "function",
                hir::AssocItem::Const(_) => "const",
                hir::AssocItem::TypeAlias(_) => "type alias",
            },
            name,
        ),
        d.expr_or_pat.map(Into::into),
    )
}

#[cfg(test)]
mod tests {
    use crate::tests::check_diagnostics;

    #[test]
    fn private_method() {
        check_diagnostics(
            r#"
mod module {
    pub struct Struct;
    impl Struct {
        fn method(&self) {}
    }
}
fn main(s: module::Struct) {
    s.method();
  //^^^^^^^^^^ error: function `method` is private
}
"#,
        );
    }

    #[test]
    fn private_func() {
        check_diagnostics(
            r#"
mod module {
    pub struct Struct;
    impl Struct {
        fn func() {}
    }
}
fn main() {
    module::Struct::func();
  //^^^^^^^^^^^^^^^^^^^^ error: function `func` is private
}
"#,
        );
    }

    #[test]
    fn private_const() {
        check_diagnostics(
            r#"
mod module {
    pub struct Struct;
    impl Struct {
        const CONST: u32 = 0;
    }
}
fn main() {
    module::Struct::CONST;
  //^^^^^^^^^^^^^^^^^^^^^ error: const `CONST` is private
}
"#,
        );
    }

    #[test]
    fn private_but_shadowed_in_deref() {
        check_diagnostics(
            r#"
//- minicore: deref
mod module {
    pub struct Struct { field: Inner }
    pub struct Inner;
    impl core::ops::Deref for Struct {
        type Target = Inner;
        fn deref(&self) -> &Inner { &self.field }
    }
    impl Struct {
        fn method(&self) {}
    }
    impl Inner {
        pub fn method(&self) {}
    }
}
fn main(s: module::Struct) {
    s.method();
}
"#,
        );
    }

    #[test]
    fn can_see_through_top_level_anonymous_const() {
        // regression test for #14046.
        check_diagnostics(
            r#"
struct S;
mod m {
    const _: () = {
        impl crate::S {
            pub(crate) fn method(self) {}
            pub(crate) const A: usize = 42;
        }
    };
    mod inner {
        const _: () = {
            impl crate::S {
                pub(crate) fn method2(self) {}
                pub(crate) const B: usize = 42;
                pub(super) fn private(self) {}
                pub(super) const PRIVATE: usize = 42;
            }
        };
    }
}
fn main() {
    S.method();
    S::A;
    S.method2();
    S::B;
    S.private();
  //^^^^^^^^^^^ error: function `private` is private
    S::PRIVATE;
  //^^^^^^^^^^ error: const `PRIVATE` is private
}
"#,
        );
    }
}
href='#n293'>293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521
use std::iter;

use hir::{db::ExpandDatabase, Adt, FileRange, HasSource, HirDisplay, InFile, Struct, Union};
use ide_db::text_edit::TextEdit;
use ide_db::{
    assists::{Assist, AssistId, AssistKind},
    helpers::is_editable_crate,
    label::Label,
    source_change::{SourceChange, SourceChangeBuilder},
};
use syntax::{
    algo,
    ast::{self, edit::IndentLevel, make, FieldList, Name, Visibility},
    AstNode, AstPtr, Direction, SyntaxKind, TextSize,
};
use syntax::{
    ast::{edit::AstNodeEdit, Type},
    SyntaxNode,
};

use crate::{adjusted_display_range, Diagnostic, DiagnosticCode, DiagnosticsContext};

// Diagnostic: unresolved-field
//
// This diagnostic is triggered if a field does not exist on a given type.
pub(crate) fn unresolved_field(
    ctx: &DiagnosticsContext<'_>,
    d: &hir::UnresolvedField,
) -> Diagnostic {
    let method_suffix = if d.method_with_same_name_exists {
        ", but a method with a similar name exists"
    } else {
        ""
    };
    Diagnostic::new(
        DiagnosticCode::RustcHardError("E0559"),
        format!(
            "no field `{}` on type `{}`{method_suffix}",
            d.name.display(ctx.sema.db, ctx.edition),
            d.receiver.display(ctx.sema.db, ctx.edition)
        ),
        adjusted_display_range(ctx, d.expr, &|expr| {
            Some(
                match expr {
                    ast::Expr::MethodCallExpr(it) => it.name_ref(),
                    ast::Expr::FieldExpr(it) => it.name_ref(),
                    _ => None,
                }?
                .syntax()
                .text_range(),
            )
        }),
    )
    .with_fixes(fixes(ctx, d))
    .experimental()
}

fn fixes(ctx: &DiagnosticsContext<'_>, d: &hir::UnresolvedField) -> Option<Vec<Assist>> {
    let mut fixes = Vec::new();
    if d.method_with_same_name_exists {
        fixes.extend(method_fix(ctx, &d.expr));
    }
    fixes.extend(field_fix(ctx, d));
    if fixes.is_empty() {
        None
    } else {
        Some(fixes)
    }
}

// FIXME: Add Snippet Support
fn field_fix(ctx: &DiagnosticsContext<'_>, d: &hir::UnresolvedField) -> Option<Assist> {
    // Get the FileRange of the invalid field access
    let root = ctx.sema.db.parse_or_expand(d.expr.file_id);
    let expr = d.expr.value.to_node(&root);

    let error_range = ctx.sema.original_range_opt(expr.syntax())?;
    let field_name = d.name.as_str();
    // Convert the receiver to an ADT
    let adt = d.receiver.strip_references().as_adt()?;
    let target_module = adt.module(ctx.sema.db);

    let suggested_type = if let Some(new_field_type) =
        ctx.sema.type_of_expr(&expr).map(|v| v.adjusted()).filter(|it| !it.is_unknown())
    {
        let display =
            new_field_type.display_source_code(ctx.sema.db, target_module.into(), false).ok();
        make::ty(display.as_deref().unwrap_or("()"))
    } else {
        make::ty("()")
    };

    if !is_editable_crate(target_module.krate(), ctx.sema.db)
        || SyntaxKind::from_keyword(field_name, ctx.edition).is_some()
    {
        return None;
    }

    match adt {
        Adt::Struct(adt_struct) => {
            add_field_to_struct_fix(ctx, adt_struct, field_name, suggested_type, error_range)
        }
        Adt::Union(adt_union) => {
            add_variant_to_union(ctx, adt_union, field_name, suggested_type, error_range)
        }
        _ => None,
    }
}

fn add_variant_to_union(
    ctx: &DiagnosticsContext<'_>,
    adt_union: Union,
    field_name: &str,
    suggested_type: Type,
    error_range: FileRange,
) -> Option<Assist> {
    let adt_source = adt_union.source(ctx.sema.db)?;
    let adt_syntax = adt_source.syntax();
    let field_list = adt_source.value.record_field_list()?;
    let range = adt_syntax.original_file_range_rooted(ctx.sema.db);
    let field_name = make::name(field_name);

    let (offset, record_field) =
        record_field_layout(None, field_name, suggested_type, field_list, adt_syntax.value)?;

    let mut src_change_builder = SourceChangeBuilder::new(range.file_id);
    src_change_builder.insert(offset, record_field);
    Some(Assist {
        id: AssistId("add-variant-to-union", AssistKind::QuickFix),
        label: Label::new("Add field to union".to_owned()),
        group: None,
        target: error_range.range,
        source_change: Some(src_change_builder.finish()),
        command: None,
    })
}

fn add_field_to_struct_fix(
    ctx: &DiagnosticsContext<'_>,
    adt_struct: Struct,
    field_name: &str,
    suggested_type: Type,
    error_range: FileRange,
) -> Option<Assist> {
    let struct_source = adt_struct.source(ctx.sema.db)?;
    let struct_syntax = struct_source.syntax();
    let struct_range = struct_syntax.original_file_range_rooted(ctx.sema.db);
    let field_list = struct_source.value.field_list();
    match field_list {
        Some(FieldList::RecordFieldList(field_list)) => {
            // Get range of final field in the struct
            let visibility = if error_range.file_id == struct_range.file_id {
                None
            } else {
                Some(make::visibility_pub_crate())
            };

            let field_name = match field_name.chars().next() {
                Some(ch) if ch.is_numeric() => return None,
                Some(_) => make::name(field_name),
                None => return None,
            };

            let (offset, record_field) = record_field_layout(
                visibility,
                field_name,
                suggested_type,
                field_list,
                struct_syntax.value,
            )?;

            let mut src_change_builder = SourceChangeBuilder::new(struct_range.file_id);

            // FIXME: Allow for choosing a visibility modifier see https://github.com/rust-lang/rust-analyzer/issues/11563
            src_change_builder.insert(offset, record_field);
            Some(Assist {
                id: AssistId("add-field-to-record-struct", AssistKind::QuickFix),
                label: Label::new("Add field to Record Struct".to_owned()),
                group: None,
                target: error_range.range,
                source_change: Some(src_change_builder.finish()),
                command: None,
            })
        }
        None => {
            // Add a field list to the Unit Struct
            let mut src_change_builder = SourceChangeBuilder::new(struct_range.file_id);
            let field_name = match field_name.chars().next() {
                // FIXME : See match arm below regarding tuple structs.
                Some(ch) if ch.is_numeric() => return None,
                Some(_) => make::name(field_name),
                None => return None,
            };
            let visibility = if error_range.file_id == struct_range.file_id {
                None
            } else {
                Some(make::visibility_pub_crate())
            };
            // FIXME: Allow for choosing a visibility modifier see https://github.com/rust-lang/rust-analyzer/issues/11563
            let indent = IndentLevel::from_node(struct_syntax.value) + 1;

            let field = make::record_field(visibility, field_name, suggested_type).indent(indent);
            let record_field_list = make::record_field_list(iter::once(field));
            // A Unit Struct with no `;` is invalid syntax. We should not suggest this fix.
            let semi_colon =
                algo::skip_trivia_token(struct_syntax.value.last_token()?, Direction::Prev)?;
            if semi_colon.kind() != SyntaxKind::SEMICOLON {
                return None;
            }
            src_change_builder.replace(semi_colon.text_range(), record_field_list.to_string());

            Some(Assist {
                id: AssistId("convert-unit-struct-to-record-struct", AssistKind::QuickFix),
                label: Label::new("Convert Unit Struct to Record Struct and add field".to_owned()),
                group: None,
                target: error_range.range,
                source_change: Some(src_change_builder.finish()),
                command: None,
            })
        }
        Some(FieldList::TupleFieldList(_tuple)) => {
            // FIXME: Add support for Tuple Structs. Tuple Structs are not sent to this diagnostic
            None
        }
    }
}

/// Used to determine the layout of the record field in the struct.
fn record_field_layout(
    visibility: Option<Visibility>,
    name: Name,
    suggested_type: Type,
    field_list: ast::RecordFieldList,
    struct_syntax: &SyntaxNode,
) -> Option<(TextSize, String)> {
    let (offset, needs_comma, trailing_new_line, indent) = match field_list.fields().last() {
        Some(record_field) => {
            let syntax = algo::skip_trivia_token(field_list.r_curly_token()?, Direction::Prev)?;

            let last_field_syntax = record_field.syntax();
            let last_field_indent = IndentLevel::from_node(last_field_syntax);
            (
                last_field_syntax.text_range().end(),
                syntax.kind() != SyntaxKind::COMMA,
                false,
                last_field_indent,
            )
        }
        // Empty Struct. Add a field right before the closing brace
        None => {
            let indent = IndentLevel::from_node(struct_syntax) + 1;
            let offset = field_list.r_curly_token()?.text_range().start();
            (offset, false, true, indent)
        }
    };
    let comma = if needs_comma { ",\n" } else { "" };
    let trailing_new_line = if trailing_new_line { "\n" } else { "" };
    let record_field = make::record_field(visibility, name, suggested_type);

    Some((offset, format!("{comma}{indent}{record_field}{trailing_new_line}")))
}

// FIXME: We should fill out the call here, move the cursor and trigger signature help
fn method_fix(
    ctx: &DiagnosticsContext<'_>,
    expr_ptr: &InFile<AstPtr<ast::Expr>>,
) -> Option<Assist> {
    let root = ctx.sema.db.parse_or_expand(expr_ptr.file_id);
    let expr = expr_ptr.value.to_node(&root);
    let FileRange { range, file_id } = ctx.sema.original_range_opt(expr.syntax())?;
    Some(Assist {
        id: AssistId("expected-field-found-method-call-fix", AssistKind::QuickFix),
        label: Label::new("Use parentheses to call the method".to_owned()),
        group: None,
        target: range,
        source_change: Some(SourceChange::from_text_edit(
            file_id,
            TextEdit::insert(range.end(), "()".to_owned()),
        )),
        command: None,
    })
}
#[cfg(test)]
mod tests {

    use crate::{
        tests::{
            check_diagnostics, check_diagnostics_with_config, check_diagnostics_with_disabled,
            check_fix, check_no_fix,
        },
        DiagnosticsConfig,
    };

    #[test]
    fn smoke_test() {
        check_diagnostics(
            r#"
fn main() {
    ().foo;
    // ^^^ error: no field `foo` on type `()`
}
"#,
        );
    }

    #[test]
    fn method_clash() {
        check_diagnostics(
            r#"
struct Foo;
impl Foo {
    fn bar(&self) {}
}
fn foo() {
    Foo.bar;
     // ^^^ 💡 error: no field `bar` on type `Foo`, but a method with a similar name exists
}
"#,
        );
    }

    #[test]
    fn method_trait_() {
        check_diagnostics(
            r#"
struct Foo;
trait Bar {
    fn bar(&self) {}
}
impl Bar for Foo {}
fn foo() {
    Foo.bar;
     // ^^^ 💡 error: no field `bar` on type `Foo`, but a method with a similar name exists
}
"#,
        );
    }

    #[test]
    fn method_trait_2() {
        check_diagnostics(
            r#"
struct Foo;
trait Bar {
    fn bar(&self);
}
impl Bar for Foo {
    fn bar(&self) {}
}
fn foo() {
    Foo.bar;
     // ^^^ 💡 error: no field `bar` on type `Foo`, but a method with a similar name exists
}
"#,
        );
    }

    #[test]
    fn no_diagnostic_on_unknown() {
        check_diagnostics_with_disabled(
            r#"
fn foo() {
    x.foo;
    (&x).foo;
    (&((x,),),).foo;
}
"#,
            &["E0425"],
        );
    }

    #[test]
    fn no_diagnostic_for_missing_name() {
        let mut config = DiagnosticsConfig::test_sample();
        config.disabled.insert("syntax-error".to_owned());
        check_diagnostics_with_config(config, "fn foo() { (). }");
    }

    #[test]
    fn unresolved_field_fix_on_unit() {
        check_fix(
            r#"
                struct Foo;

                fn foo() {
                    Foo.bar$0;
                }
            "#,
            r#"
                struct Foo{ bar: () }

                fn foo() {
                    Foo.bar;
                }
            "#,
        );
    }
    #[test]
    fn unresolved_field_fix_on_empty() {
        check_fix(
            r#"
                struct Foo{
                }

                fn foo() {
                    let foo = Foo{};
                    foo.bar$0;
                }
            "#,
            r#"
                struct Foo{
                    bar: ()
                }

                fn foo() {
                    let foo = Foo{};
                    foo.bar;
                }
            "#,
        );
    }
    #[test]
    fn unresolved_field_fix_on_struct() {
        check_fix(
            r#"
                struct Foo{
                    a: i32
                }

                fn foo() {
                    let foo = Foo{a: 0};
                    foo.bar$0;
                }
            "#,
            r#"
                struct Foo{
                    a: i32,
                    bar: ()
                }

                fn foo() {
                    let foo = Foo{a: 0};
                    foo.bar;
                }
            "#,
        );
    }
    #[test]
    fn unresolved_field_fix_on_union() {
        check_fix(
            r#"
                union Foo{
                    a: i32
                }

                fn foo() {
                    let foo = Foo{a: 0};
                    foo.bar$0;
                }
            "#,
            r#"
                union Foo{
                    a: i32,
                    bar: ()
                }

                fn foo() {
                    let foo = Foo{a: 0};
                    foo.bar;
                }
            "#,
        );
    }

    #[test]
    fn no_fix_when_indexed() {
        check_no_fix(
            r#"
            struct Kek {}
impl Kek {
    pub fn foo(self) {
        self.$00
    }
}

fn main() {}
            "#,
        )
    }

    #[test]
    fn no_fix_when_without_field() {
        check_no_fix(
            r#"
            struct Kek {}
impl Kek {
    pub fn foo(self) {
        self.$0
    }
}

fn main() {}
            "#,
        )
    }

    #[test]
    fn regression_18683() {
        check_diagnostics(
            r#"
struct S;
impl S {
    fn f(self) {
        self.self
          // ^^^^ error: no field `self` on type `S`
    }
}
        "#,
        );
    }
}