From 8bc826dd53a7a88798531d7c79d74dbf4d809e56 Mon Sep 17 00:00:00 2001 From: Lukas Wirth Date: Sun, 28 May 2023 13:30:34 +0200 Subject: [PATCH] Add diagnostic for `_` expressions (typed holes) --- crates/base-db/src/fixture.rs | 7 + crates/hir-def/src/resolver.rs | 6 + crates/hir-ty/src/builder.rs | 13 + crates/hir-ty/src/infer.rs | 45 ++-- crates/hir-ty/src/infer/expr.rs | 25 +- crates/hir/src/diagnostics.rs | 7 + crates/hir/src/lib.rs | 92 ++++--- .../src/handlers/typed_hole.rs | 232 ++++++++++++++++++ crates/ide-diagnostics/src/lib.rs | 2 + 9 files changed, 368 insertions(+), 61 deletions(-) create mode 100644 crates/ide-diagnostics/src/handlers/typed_hole.rs diff --git a/crates/base-db/src/fixture.rs b/crates/base-db/src/fixture.rs index e89d3ae4bb339..5b11343173b30 100644 --- a/crates/base-db/src/fixture.rs +++ b/crates/base-db/src/fixture.rs @@ -21,6 +21,7 @@ use crate::{ pub const WORKSPACE: SourceRootId = SourceRootId(0); pub trait WithFixture: Default + SourceDatabaseExt + 'static { + #[track_caller] fn with_single_file(ra_fixture: &str) -> (Self, FileId) { let fixture = ChangeFixture::parse(ra_fixture); let mut db = Self::default(); @@ -29,6 +30,7 @@ pub trait WithFixture: Default + SourceDatabaseExt + 'static { (db, fixture.files[0]) } + #[track_caller] fn with_many_files(ra_fixture: &str) -> (Self, Vec) { let fixture = ChangeFixture::parse(ra_fixture); let mut db = Self::default(); @@ -37,6 +39,7 @@ pub trait WithFixture: Default + SourceDatabaseExt + 'static { (db, fixture.files) } + #[track_caller] fn with_files(ra_fixture: &str) -> Self { let fixture = ChangeFixture::parse(ra_fixture); let mut db = Self::default(); @@ -45,6 +48,7 @@ pub trait WithFixture: Default + SourceDatabaseExt + 'static { db } + #[track_caller] fn with_files_extra_proc_macros( ra_fixture: &str, proc_macros: Vec<(String, ProcMacro)>, @@ -56,18 +60,21 @@ pub trait WithFixture: Default + SourceDatabaseExt + 'static { db } + #[track_caller] fn with_position(ra_fixture: &str) -> (Self, FilePosition) { let (db, file_id, range_or_offset) = Self::with_range_or_offset(ra_fixture); let offset = range_or_offset.expect_offset(); (db, FilePosition { file_id, offset }) } + #[track_caller] fn with_range(ra_fixture: &str) -> (Self, FileRange) { let (db, file_id, range_or_offset) = Self::with_range_or_offset(ra_fixture); let range = range_or_offset.expect_range(); (db, FileRange { file_id, range }) } + #[track_caller] fn with_range_or_offset(ra_fixture: &str) -> (Self, FileId, RangeOrOffset) { let fixture = ChangeFixture::parse(ra_fixture); let mut db = Self::default(); diff --git a/crates/hir-def/src/resolver.rs b/crates/hir-def/src/resolver.rs index afa3b33cc9fea..3eaff61b154a0 100644 --- a/crates/hir-def/src/resolver.rs +++ b/crates/hir-def/src/resolver.rs @@ -1046,6 +1046,12 @@ impl HasResolver for GenericDefId { } } +impl HasResolver for EnumVariantId { + fn resolver(self, db: &dyn DefDatabase) -> Resolver { + self.parent.resolver(db) + } +} + impl HasResolver for VariantId { fn resolver(self, db: &dyn DefDatabase) -> Resolver { match self { diff --git a/crates/hir-ty/src/builder.rs b/crates/hir-ty/src/builder.rs index 2362b08f79a2a..eec57ba3f80f1 100644 --- a/crates/hir-ty/src/builder.rs +++ b/crates/hir-ty/src/builder.rs @@ -194,6 +194,19 @@ impl TyBuilder<()> { params.placeholder_subst(db) } + pub fn unknown_subst(db: &dyn HirDatabase, def: impl Into) -> Substitution { + let params = generics(db.upcast(), def.into()); + Substitution::from_iter( + Interner, + params.iter_id().map(|id| match id { + either::Either::Left(_) => TyKind::Error.intern(Interner).cast(Interner), + either::Either::Right(id) => { + unknown_const_as_generic(db.const_param_ty(id)).cast(Interner) + } + }), + ) + } + pub fn subst_for_def( db: &dyn HirDatabase, def: impl Into, diff --git a/crates/hir-ty/src/infer.rs b/crates/hir-ty/src/infer.rs index f01ee1b4e600b..11c0ccf547d51 100644 --- a/crates/hir-ty/src/infer.rs +++ b/crates/hir-ty/src/infer.rs @@ -215,6 +215,10 @@ pub enum InferenceDiagnostic { call_expr: ExprId, found: Ty, }, + TypedHole { + expr: ExprId, + expected: Ty, + }, } /// A mismatch between an expected and an inferred type. @@ -600,29 +604,30 @@ impl<'a> InferenceContext<'a> { mismatch.actual = table.resolve_completely(mismatch.actual.clone()); } result.diagnostics.retain_mut(|diagnostic| { - if let InferenceDiagnostic::ExpectedFunction { found: ty, .. } - | InferenceDiagnostic::UnresolvedField { receiver: ty, .. } - | InferenceDiagnostic::UnresolvedMethodCall { receiver: ty, .. } = diagnostic - { - *ty = table.resolve_completely(ty.clone()); - // FIXME: Remove this when we are on par with rustc in terms of inference - if ty.contains_unknown() { - return false; - } + use InferenceDiagnostic::*; + match diagnostic { + ExpectedFunction { found: ty, .. } + | UnresolvedField { receiver: ty, .. } + | UnresolvedMethodCall { receiver: ty, .. } => { + *ty = table.resolve_completely(ty.clone()); + // FIXME: Remove this when we are on par with rustc in terms of inference + if ty.contains_unknown() { + return false; + } - if let InferenceDiagnostic::UnresolvedMethodCall { field_with_same_name, .. } = - diagnostic - { - let clear = if let Some(ty) = field_with_same_name { - *ty = table.resolve_completely(ty.clone()); - ty.contains_unknown() - } else { - false - }; - if clear { - *field_with_same_name = None; + if let UnresolvedMethodCall { field_with_same_name, .. } = diagnostic { + if let Some(ty) = field_with_same_name { + *ty = table.resolve_completely(ty.clone()); + if ty.contains_unknown() { + *field_with_same_name = None; + } + } } } + TypedHole { expected: ty, .. } => { + *ty = table.resolve_completely(ty.clone()); + } + _ => (), } true }); diff --git a/crates/hir-ty/src/infer/expr.rs b/crates/hir-ty/src/infer/expr.rs index f30758484300f..d7c6691ea0c7a 100644 --- a/crates/hir-ty/src/infer/expr.rs +++ b/crates/hir-ty/src/infer/expr.rs @@ -874,9 +874,15 @@ impl<'a> InferenceContext<'a> { }, Expr::Underscore => { // Underscore expressions may only appear in assignee expressions, - // which are handled by `infer_assignee_expr()`, so any underscore - // expression reaching this branch is an error. - self.err_ty() + // which are handled by `infer_assignee_expr()`. + // Any other underscore expression is an error, we render a specialized diagnostic + // to let the user know what type is expected though. + let expected = expected.to_option(&mut self.table).unwrap_or_else(|| self.err_ty()); + self.push_diagnostic(InferenceDiagnostic::TypedHole { + expr: tgt_expr, + expected: expected.clone(), + }); + expected } }; // use a new type variable if we got unknown here @@ -1001,12 +1007,13 @@ impl<'a> InferenceContext<'a> { } &Array::Repeat { initializer, repeat } => { self.infer_expr_coerce(initializer, &Expectation::has_type(elem_ty.clone())); - self.infer_expr( - repeat, - &Expectation::HasType( - TyKind::Scalar(Scalar::Uint(UintTy::Usize)).intern(Interner), - ), - ); + let usize = TyKind::Scalar(Scalar::Uint(UintTy::Usize)).intern(Interner); + match self.body[repeat] { + Expr::Underscore => { + self.write_expr_ty(repeat, usize); + } + _ => _ = self.infer_expr(repeat, &Expectation::HasType(usize)), + } ( elem_ty, diff --git a/crates/hir/src/diagnostics.rs b/crates/hir/src/diagnostics.rs index 10893b62bfb57..b64d81490bb11 100644 --- a/crates/hir/src/diagnostics.rs +++ b/crates/hir/src/diagnostics.rs @@ -52,6 +52,7 @@ diagnostics![ PrivateAssocItem, PrivateField, ReplaceFilterMapNextWithFindMap, + TypedHole, TypeMismatch, UndeclaredLabel, UnimplementedBuiltinMacro, @@ -73,6 +74,12 @@ pub struct BreakOutsideOfLoop { pub bad_value_break: bool, } +#[derive(Debug)] +pub struct TypedHole { + pub expr: InFile>, + pub expected: Type, +} + #[derive(Debug)] pub struct UnresolvedModule { pub decl: InFile>, diff --git a/crates/hir/src/lib.rs b/crates/hir/src/lib.rs index b1583c9d00b5e..5392cb6a32a17 100644 --- a/crates/hir/src/lib.rs +++ b/crates/hir/src/lib.rs @@ -69,7 +69,8 @@ use hir_ty::{ traits::FnTrait, AliasTy, CallableDefId, CallableSig, Canonical, CanonicalVarKinds, Cast, ClosureId, GenericArgData, Interner, ParamKind, QuantifiedWhereClause, Scalar, Substitution, - TraitEnvironment, TraitRefExt, Ty, TyBuilder, TyDefId, TyExt, TyKind, WhereClause, + TraitEnvironment, TraitRefExt, Ty, TyBuilder, TyDefId, TyExt, TyKind, ValueTyDefId, + WhereClause, }; use itertools::Itertools; use nameres::diagnostics::DefDiagnosticKind; @@ -91,10 +92,10 @@ pub use crate::{ IncorrectCase, InvalidDeriveTarget, MacroDefError, MacroError, MacroExpansionParseError, MalformedDerive, MismatchedArgCount, MissingFields, MissingMatchArms, MissingUnsafe, MovedOutOfRef, NeedMut, NoSuchField, PrivateAssocItem, PrivateField, - ReplaceFilterMapNextWithFindMap, TypeMismatch, UndeclaredLabel, UnimplementedBuiltinMacro, - UnreachableLabel, UnresolvedExternCrate, UnresolvedField, UnresolvedImport, - UnresolvedMacroCall, UnresolvedMethodCall, UnresolvedModule, UnresolvedProcMacro, - UnusedMut, + ReplaceFilterMapNextWithFindMap, TypeMismatch, TypedHole, UndeclaredLabel, + UnimplementedBuiltinMacro, UnreachableLabel, UnresolvedExternCrate, UnresolvedField, + UnresolvedImport, UnresolvedMacroCall, UnresolvedMethodCall, UnresolvedModule, + UnresolvedProcMacro, UnusedMut, }, has_source::HasSource, semantics::{PathResolution, Semantics, SemanticsScope, TypeInfo, VisibleTraits}, @@ -1005,6 +1006,10 @@ impl Struct { Type::from_def(db, self.id) } + pub fn constructor_ty(self, db: &dyn HirDatabase) -> Type { + Type::from_value_def(db, self.id) + } + pub fn repr(self, db: &dyn HirDatabase) -> Option { db.struct_data(self.id).repr } @@ -1042,6 +1047,10 @@ impl Union { Type::from_def(db, self.id) } + pub fn constructor_ty(self, db: &dyn HirDatabase) -> Type { + Type::from_value_def(db, self.id) + } + pub fn fields(self, db: &dyn HirDatabase) -> Vec { db.union_data(self.id) .variant_data @@ -1173,6 +1182,10 @@ impl Variant { self.parent } + pub fn constructor_ty(self, db: &dyn HirDatabase) -> Type { + Type::from_value_def(db, EnumVariantId { parent: self.parent.id, local_id: self.id }) + } + pub fn name(self, db: &dyn HirDatabase) -> Name { db.enum_data(self.parent.id).variants[self.id].name.clone() } @@ -1574,6 +1587,16 @@ impl DefWithBody { let expr = expr_syntax(expr); acc.push(BreakOutsideOfLoop { expr, is_break, bad_value_break }.into()) } + hir_ty::InferenceDiagnostic::TypedHole { expr, expected } => { + let expr = expr_syntax(*expr); + acc.push( + TypedHole { + expr, + expected: Type::new(db, DefWithBodyId::from(self), expected.clone()), + } + .into(), + ) + } } } for (pat_or_expr, mismatch) in infer.type_mismatches() { @@ -1806,6 +1829,10 @@ impl Function { db.function_data(self.id).name.clone() } + pub fn ty(self, db: &dyn HirDatabase) -> Type { + Type::from_value_def(db, self.id) + } + /// Get this function's return type pub fn ret_type(self, db: &dyn HirDatabase) -> Type { let resolver = self.id.resolver(db.upcast()); @@ -2085,11 +2112,7 @@ impl Const { } pub fn ty(self, db: &dyn HirDatabase) -> Type { - let data = db.const_data(self.id); - let resolver = self.id.resolver(db.upcast()); - let ctx = hir_ty::TyLoweringContext::new(db, &resolver); - let ty = ctx.lower_ty(&data.type_ref); - Type::new_with_resolver_inner(db, &resolver, ty) + Type::from_value_def(db, self.id) } pub fn render_eval(self, db: &dyn HirDatabase) -> Result { @@ -2136,11 +2159,7 @@ impl Static { } pub fn ty(self, db: &dyn HirDatabase) -> Type { - let data = db.static_data(self.id); - let resolver = self.id.resolver(db.upcast()); - let ctx = hir_ty::TyLoweringContext::new(db, &resolver); - let ty = ctx.lower_ty(&data.type_ref); - Type::new_with_resolver_inner(db, &resolver, ty) + Type::from_value_def(db, self.id) } } @@ -3409,24 +3428,33 @@ impl Type { Type { env: environment, ty } } - fn from_def(db: &dyn HirDatabase, def: impl HasResolver + Into) -> Type { - let ty_def = def.into(); - let parent_subst = match ty_def { - TyDefId::TypeAliasId(id) => match id.lookup(db.upcast()).container { - ItemContainerId::TraitId(id) => { - let subst = TyBuilder::subst_for_def(db, id, None).fill_with_unknown().build(); - Some(subst) - } - ItemContainerId::ImplId(id) => { - let subst = TyBuilder::subst_for_def(db, id, None).fill_with_unknown().build(); - Some(subst) - } - _ => None, + fn from_def(db: &dyn HirDatabase, def: impl Into + HasResolver) -> Type { + let ty = db.ty(def.into()); + let substs = TyBuilder::unknown_subst( + db, + match def.into() { + TyDefId::AdtId(it) => GenericDefId::AdtId(it), + TyDefId::TypeAliasId(it) => GenericDefId::TypeAliasId(it), + TyDefId::BuiltinType(_) => return Type::new(db, def, ty.skip_binders().clone()), }, - _ => None, - }; - let ty = TyBuilder::def_ty(db, ty_def, parent_subst).fill_with_unknown().build(); - Type::new(db, def, ty) + ); + Type::new(db, def, ty.substitute(Interner, &substs)) + } + + fn from_value_def(db: &dyn HirDatabase, def: impl Into + HasResolver) -> Type { + let ty = db.value_ty(def.into()); + let substs = TyBuilder::unknown_subst( + db, + match def.into() { + ValueTyDefId::ConstId(it) => GenericDefId::ConstId(it), + ValueTyDefId::FunctionId(it) => GenericDefId::FunctionId(it), + ValueTyDefId::StructId(it) => GenericDefId::AdtId(AdtId::StructId(it)), + ValueTyDefId::UnionId(it) => GenericDefId::AdtId(AdtId::UnionId(it)), + ValueTyDefId::EnumVariantId(it) => GenericDefId::EnumVariantId(it), + ValueTyDefId::StaticId(_) => return Type::new(db, def, ty.skip_binders().clone()), + }, + ); + Type::new(db, def, ty.substitute(Interner, &substs)) } pub fn new_slice(ty: Type) -> Type { diff --git a/crates/ide-diagnostics/src/handlers/typed_hole.rs b/crates/ide-diagnostics/src/handlers/typed_hole.rs new file mode 100644 index 0000000000000..e12bbcf682068 --- /dev/null +++ b/crates/ide-diagnostics/src/handlers/typed_hole.rs @@ -0,0 +1,232 @@ +use hir::{db::ExpandDatabase, ClosureStyle, HirDisplay, StructKind}; +use ide_db::{ + assists::{Assist, AssistId, AssistKind, GroupLabel}, + label::Label, + source_change::SourceChange, +}; +use syntax::AstNode; +use text_edit::TextEdit; + +use crate::{Diagnostic, DiagnosticsContext}; + +// Diagnostic: typed-hole +// +// This diagnostic is triggered when an underscore expression is used in an invalid position. +pub(crate) fn typed_hole(ctx: &DiagnosticsContext<'_>, d: &hir::TypedHole) -> Diagnostic { + let display_range = ctx.sema.diagnostics_display_range(d.expr.clone().map(|it| it.into())); + let (message, fixes) = if d.expected.is_unknown() { + ("`_` expressions may only appear on the left-hand side of an assignment".to_owned(), None) + } else { + ( + format!( + "invalid `_` expression, expected type `{}`", + d.expected.display(ctx.sema.db).with_closure_style(ClosureStyle::ClosureWithId), + ), + fixes(ctx, d), + ) + }; + + Diagnostic::new("typed-hole", message, display_range.range).with_fixes(fixes) +} + +fn fixes(ctx: &DiagnosticsContext<'_>, d: &hir::TypedHole) -> Option> { + let db = ctx.sema.db; + let root = db.parse_or_expand(d.expr.file_id); + let original_range = + d.expr.as_ref().map(|it| it.to_node(&root)).syntax().original_file_range_opt(db)?; + let scope = ctx.sema.scope(d.expr.value.to_node(&root).syntax())?; + let mut assists = vec![]; + scope.process_all_names(&mut |name, def| { + let ty = match def { + hir::ScopeDef::ModuleDef(it) => match it { + hir::ModuleDef::Function(it) => it.ty(db), + hir::ModuleDef::Adt(hir::Adt::Struct(it)) if it.kind(db) != StructKind::Record => { + it.constructor_ty(db) + } + hir::ModuleDef::Variant(it) if it.kind(db) != StructKind::Record => { + it.constructor_ty(db) + } + hir::ModuleDef::Const(it) => it.ty(db), + hir::ModuleDef::Static(it) => it.ty(db), + _ => return, + }, + hir::ScopeDef::GenericParam(hir::GenericParam::ConstParam(it)) => it.ty(db), + hir::ScopeDef::Local(it) => it.ty(db), + _ => return, + }; + // FIXME: should also check coercions if it is at a coercion site + if !ty.contains_unknown() && ty.could_unify_with(db, &d.expected) { + assists.push(Assist { + id: AssistId("typed-hole", AssistKind::QuickFix), + label: Label::new(format!("Replace `_` with `{}`", name.display(db))), + group: Some(GroupLabel("Replace `_` with a matching entity in scope".to_owned())), + target: original_range.range, + source_change: Some(SourceChange::from_text_edit( + original_range.file_id, + TextEdit::replace(original_range.range, name.display(db).to_string()), + )), + trigger_signature_help: false, + }); + } + }); + if assists.is_empty() { + None + } else { + Some(assists) + } +} + +#[cfg(test)] +mod tests { + use crate::tests::{check_diagnostics, check_fixes}; + + #[test] + fn unknown() { + check_diagnostics( + r#" +fn main() { + _; + //^ error: `_` expressions may only appear on the left-hand side of an assignment +} +"#, + ); + } + + #[test] + fn concrete_expectation() { + check_diagnostics( + r#" +fn main() { + if _ {} + //^ error: invalid `_` expression, expected type `bool` + let _: fn() -> i32 = _; + //^ error: invalid `_` expression, expected type `fn() -> i32` + let _: fn() -> () = _; // FIXME: This should trigger an assist because `main` matches via *coercion* + //^ error: invalid `_` expression, expected type `fn()` +} +"#, + ); + } + + #[test] + fn integer_ty_var() { + check_diagnostics( + r#" +fn main() { + let mut x = 3; + x = _; + //^ 💡 error: invalid `_` expression, expected type `i32` +} +"#, + ); + } + + #[test] + fn ty_var_resolved() { + check_diagnostics( + r#" +fn main() { + let mut x = t(); + x = _; + //^ 💡 error: invalid `_` expression, expected type `&str` + x = ""; +} +fn t() -> T { loop {} } +"#, + ); + } + + #[test] + fn valid_positions() { + check_diagnostics( + r#" +fn main() { + let x = [(); _]; + let y: [(); 10] = [(); _]; + _ = 0; + (_,) = (1,); +} +"#, + ); + } + + #[test] + fn check_quick_fix() { + check_fixes( + r#" +enum Foo { + Bar +} +use Foo::Bar; +const C: Foo = Foo::Bar; +fn main(param: Foo) { + let local = Foo::Bar; + let _: Foo = _$0; + //^ error: invalid `_` expression, expected type `fn()` +} +"#, + vec![ + r#" +enum Foo { + Bar +} +use Foo::Bar; +const C: Foo = Foo::Bar; +fn main(param: Foo) { + let local = Foo::Bar; + let _: Foo = local; + //^ error: invalid `_` expression, expected type `fn()` +} +"#, + r#" +enum Foo { + Bar +} +use Foo::Bar; +const C: Foo = Foo::Bar; +fn main(param: Foo) { + let local = Foo::Bar; + let _: Foo = param; + //^ error: invalid `_` expression, expected type `fn()` +} +"#, + r#" +enum Foo { + Bar +} +use Foo::Bar; +const C: Foo = Foo::Bar; +fn main(param: Foo) { + let local = Foo::Bar; + let _: Foo = CP; + //^ error: invalid `_` expression, expected type `fn()` +} +"#, + r#" +enum Foo { + Bar +} +use Foo::Bar; +const C: Foo = Foo::Bar; +fn main(param: Foo) { + let local = Foo::Bar; + let _: Foo = Bar; + //^ error: invalid `_` expression, expected type `fn()` +} +"#, + r#" +enum Foo { + Bar +} +use Foo::Bar; +const C: Foo = Foo::Bar; +fn main(param: Foo) { + let local = Foo::Bar; + let _: Foo = C; + //^ error: invalid `_` expression, expected type `fn()` +} +"#, + ], + ); + } +} diff --git a/crates/ide-diagnostics/src/lib.rs b/crates/ide-diagnostics/src/lib.rs index 048dedf6bd15e..55a4a482d3b28 100644 --- a/crates/ide-diagnostics/src/lib.rs +++ b/crates/ide-diagnostics/src/lib.rs @@ -44,6 +44,7 @@ mod handlers { pub(crate) mod private_assoc_item; pub(crate) mod private_field; pub(crate) mod replace_filter_map_next_with_find_map; + pub(crate) mod typed_hole; pub(crate) mod type_mismatch; pub(crate) mod unimplemented_builtin_macro; pub(crate) mod unresolved_extern_crate; @@ -290,6 +291,7 @@ pub fn diagnostics( AnyDiagnostic::PrivateAssocItem(d) => handlers::private_assoc_item::private_assoc_item(&ctx, &d), AnyDiagnostic::PrivateField(d) => handlers::private_field::private_field(&ctx, &d), AnyDiagnostic::ReplaceFilterMapNextWithFindMap(d) => handlers::replace_filter_map_next_with_find_map::replace_filter_map_next_with_find_map(&ctx, &d), + AnyDiagnostic::TypedHole(d) => handlers::typed_hole::typed_hole(&ctx, &d), AnyDiagnostic::TypeMismatch(d) => handlers::type_mismatch::type_mismatch(&ctx, &d), AnyDiagnostic::UndeclaredLabel(d) => handlers::undeclared_label::undeclared_label(&ctx, &d), AnyDiagnostic::UnimplementedBuiltinMacro(d) => handlers::unimplemented_builtin_macro::unimplemented_builtin_macro(&ctx, &d),