From 7a19385b0d149412091f8fb41103e2cb72afe8d4 Mon Sep 17 00:00:00 2001 From: Benjamin Brienen Date: Tue, 1 Sep 2026 15:21:20 +0200 Subject: [PATCH] Add missing body diagnostics --- crates/hir-def/src/expr_store.rs | 10 +++ crates/hir-def/src/expr_store/body.rs | 18 +++-- crates/hir-def/src/expr_store/lower.rs | 61 ++++++++++++++-- crates/hir-def/src/signatures.rs | 10 +-- crates/hir/src/diagnostics.rs | 12 +++- .../src/handlers/missing_body.rs | 72 +++++++++++++++++++ crates/ide-diagnostics/src/lib.rs | 2 + 7 files changed, 163 insertions(+), 22 deletions(-) create mode 100644 crates/ide-diagnostics/src/handlers/missing_body.rs diff --git a/crates/hir-def/src/expr_store.rs b/crates/hir-def/src/expr_store.rs index 7d6b191c17ad..3a63ca80ffc6 100644 --- a/crates/hir-def/src/expr_store.rs +++ b/crates/hir-def/src/expr_store.rs @@ -321,6 +321,15 @@ struct FormatTemplate { implicit_capture_to_source: FxHashMap>, } +#[derive(Debug, PartialEq, Eq, Clone, Copy, Hash)] +pub enum MissingBodyItemKind { + AssocConst, + AssocType, + Const, + Static, + TypeAlias, +} + #[derive(Debug, Eq, PartialEq)] pub enum ExpressionStoreDiagnostics { InactiveCode { node: InFile, cfg: CfgExpr, opts: CfgOptions }, @@ -330,6 +339,7 @@ pub enum ExpressionStoreDiagnostics { UndeclaredLabel { node: InFile>, name: Name }, PatternArgInExternFn { node: InFile> }, FruInDestructuringAssignment { node: InFile> }, + MissingBody { node: InFile, kind: MissingBodyItemKind }, } impl ExpressionStoreBuilder { diff --git a/crates/hir-def/src/expr_store/body.rs b/crates/hir-def/src/expr_store/body.rs index 74c86ca4e89d..cd04e2c33573 100644 --- a/crates/hir-def/src/expr_store/body.rs +++ b/crates/hir-def/src/expr_store/body.rs @@ -5,7 +5,7 @@ use std::ops; use base_db::SourceDatabase; use hir_expand::{InFile, Lookup}; use span::Edition; -use syntax::ast; +use syntax::{SyntaxNodePtr, ast}; use triomphe::Arc; use crate::{ @@ -96,7 +96,7 @@ impl Body { let mut is_async_fn = false; let mut is_gen_fn = false; - let InFile { file_id, value: body } = { + let (InFile { file_id, value: body }, syntax_node) = { match def { DefWithBodyId::FunctionId(f) => { let f = f.lookup(db); @@ -104,28 +104,32 @@ impl Body { params = src.value.param_list(); is_async_fn = src.value.async_token().is_some(); is_gen_fn = src.value.gen_token().is_some(); - src.map(|it| it.body().map(ast::Expr::from)) + let syntax_node = SyntaxNodePtr::new(src.syntax().value); + (src.map(|it| it.body().map(ast::Expr::from)), syntax_node) } DefWithBodyId::ConstId(c) => { let c = c.lookup(db); let src = c.source(db); - src.map(|it| it.body()) + let syntax_node = SyntaxNodePtr::new(src.syntax().value); + (src.map(|it| it.body()), syntax_node) } DefWithBodyId::StaticId(s) => { let s = s.lookup(db); let src = s.source(db); - src.map(|it| it.body()) + let syntax_node = SyntaxNodePtr::new(src.syntax().value); + (src.map(|it| it.body()), syntax_node) } DefWithBodyId::VariantId(v) => { let s = v.lookup(db); let src = s.source(db); - src.map(|it| it.const_arg()?.expr()) + let syntax_node = SyntaxNodePtr::new(src.syntax().value); + (src.map(|it| it.const_arg()?.expr()), syntax_node) } } }; let module = def.module(db); let (body, source_map) = - lower_body(db, def, file_id, module, params, body, is_async_fn, is_gen_fn); + lower_body(db, def, syntax_node, file_id, module, params, body, is_async_fn, is_gen_fn); (Arc::new(body), source_map) } diff --git a/crates/hir-def/src/expr_store/lower.rs b/crates/hir-def/src/expr_store/lower.rs index c6f07f303761..6463bb1e1d61 100644 --- a/crates/hir-def/src/expr_store/lower.rs +++ b/crates/hir-def/src/expr_store/lower.rs @@ -35,14 +35,14 @@ use thin_vec::ThinVec; use tt::TextRange; use crate::{ - AdtId, BlockId, BlockIdLt, ConstId, DefWithBodyId, FunctionId, GenericDefId, ImplId, + AdtId, BlockId, BlockIdLt, ConstId, DefWithBodyId, FunctionId, GenericDefId, HasModule, ImplId, ItemContainerId, LoweringMode, MacroId, ModuleDefId, ModuleId, TraitId, TypeAliasId, UnresolvedMacro, attrs::AttrFlags, expr_store::{ Body, BodySourceMap, ExprPtr, ExprRoot, ExpressionStore, ExpressionStoreBuilder, ExpressionStoreDiagnostics, ExpressionStoreSourceMap, HygieneId, LabelPtr, LifetimePtr, - PatPtr, StoreVisitor, TypePtr, + MissingBodyItemKind, PatPtr, StoreVisitor, TypePtr, body::Param, expander::Expander, lower::generics::ImplTraitLowerFn, @@ -70,6 +70,7 @@ pub use self::path::hir_segment_to_ast_segment; pub(super) fn lower_body( db: &dyn SourceDatabase, owner: DefWithBodyId, + syntax_node: SyntaxNodePtr, current_file_id: HirFileId, module: ModuleId, parameters: Option, @@ -133,7 +134,7 @@ pub(super) fn lower_body( BodySourceMap { self_param: source_map_self_param, store: source_map }, ); } - + validate_required_body(db, owner, current_file_id, syntax_node, body.as_ref(), &mut collector); collector.with_expr_root(|collector| { if let DefWithBodyId::FunctionId(func) = owner && let Some(param_list) = parameters @@ -206,6 +207,40 @@ pub(super) fn lower_body( ) } +fn validate_required_body( + db: &(dyn SourceDatabase + 'static), + owner: DefWithBodyId, + current_file_id: HirFileId, + syntax_node: SyntaxNodePtr, + body: Option<&ast::Expr>, + collector: &mut ExprCollector<'_>, +) { + if body.is_some() { + return; + } + let diagnostic_kind = match owner { + // FIXME: add diagnostic for missing body + // rustc says: if body.is_none() && !is_intrinsic && !self.is_sdylib_interface + DefWithBodyId::FunctionId(_function_id) => None, + DefWithBodyId::StaticId(id) => match id.loc(db).container { + ItemContainerId::ModuleId(_) => Some(MissingBodyItemKind::Static), + ItemContainerId::ExternBlockId(_) + | ItemContainerId::ImplId(_) + | ItemContainerId::TraitId(_) => None, + }, + DefWithBodyId::ConstId(id) => match id.loc(db).container { + ItemContainerId::ModuleId(_) => Some(MissingBodyItemKind::Const), + ItemContainerId::ImplId(_) => Some(MissingBodyItemKind::AssocConst), + ItemContainerId::ExternBlockId(_) | ItemContainerId::TraitId(_) => None, + }, + DefWithBodyId::VariantId(_) => None, + }; + if let Some(kind) = diagnostic_kind { + let node = InFile::new(current_file_id, syntax_node); + collector.store.diagnostics.push(ExpressionStoreDiagnostics::MissingBody { node, kind }); + } +} + pub(crate) fn lower_type_ref( db: &dyn SourceDatabase, module: ModuleId, @@ -290,12 +325,13 @@ pub(crate) fn lower_trait( pub(crate) fn lower_type_alias( db: &dyn SourceDatabase, - module: ModuleId, + container: ItemContainerId, alias: InFile, type_alias_id: TypeAliasId, ) -> (ExpressionStore, ExpressionStoreSourceMap, GenericParams, Box<[TypeBound]>, Option) { - let mut expr_collector = ExprCollector::new(db, module, alias.file_id, LoweringMode::Analysis); + let mut expr_collector = + ExprCollector::new(db, container.module(db), alias.file_id, LoweringMode::Analysis); let bounds = alias .value .type_bound_list() @@ -319,6 +355,21 @@ pub(crate) fn lower_type_alias( .value .ty() .map(|ty| expr_collector.lower_type_ref(ty, &mut ExprCollector::impl_trait_allocator)); + if alias.value.ty().is_none() { + let diagnostic_kind = match container { + ItemContainerId::ModuleId(_) => Some(MissingBodyItemKind::TypeAlias), + ItemContainerId::ImplId(_) => Some(MissingBodyItemKind::AssocType), + ItemContainerId::ExternBlockId(_) => None, + ItemContainerId::TraitId(_) => None, + }; + if let Some(kind) = diagnostic_kind { + let node = InFile::new(alias.file_id, SyntaxNodePtr::new(alias.value.syntax())); + expr_collector + .store + .diagnostics + .push(ExpressionStoreDiagnostics::MissingBody { node, kind }); + } + }; let (store, source_map) = expr_collector.store.finish(); (store, source_map, params, bounds, type_ref) } diff --git a/crates/hir-def/src/signatures.rs b/crates/hir-def/src/signatures.rs index b46d258686ad..c61cffd56467 100644 --- a/crates/hir-def/src/signatures.rs +++ b/crates/hir-def/src/signatures.rs @@ -834,7 +834,7 @@ impl TypeAliasSignature { let source = loc.source(db); let name = as_name_opt(source.value.name()); let (store, source_map, generic_params, bounds, ty) = - lower_type_alias(db, loc.container.module(db), source, id); + lower_type_alias(db, loc.container, source, id); ( Arc::new(TypeAliasSignature { store, generic_params, flags, bounds, name, ty }), @@ -849,14 +849,6 @@ pub struct FunctionBody { pub parameters: Box<[PatId]>, } -#[derive(Debug, PartialEq, Eq)] -pub struct SimpleBody { - pub store: ExpressionStore, -} -pub type StaticBody = SimpleBody; -pub type ConstBody = SimpleBody; -pub type EnumVariantBody = SimpleBody; - #[derive(Debug, PartialEq, Eq)] pub struct VariantFieldsBody { pub store: ExpressionStore, diff --git a/crates/hir/src/diagnostics.rs b/crates/hir/src/diagnostics.rs index c20c6e471ac7..5c3b628f386d 100644 --- a/crates/hir/src/diagnostics.rs +++ b/crates/hir/src/diagnostics.rs @@ -62,7 +62,7 @@ use crate::{ struct_tail_raw, }; -pub use hir_def::VariantId; +pub use hir_def::{VariantId, expr_store::MissingBodyItemKind}; pub use hir_ty::{ GenericArgsProhibitedReason, IncorrectGenericsLenKind, ReturnKind, diagnostics::{CaseType, IncorrectCase}, @@ -141,6 +141,7 @@ diagnostics![AnyDiagnostic<'db> -> ExpectedFunction<'db>, ExplicitDropMethodUse, FruInDestructuringAssignment, + MissingBody, FunctionalRecordUpdateOnNonStruct, GenericDefaultRefersToSelf, InactiveCode, @@ -401,6 +402,12 @@ pub struct FruInDestructuringAssignment { pub node: InFile>, } +#[derive(Debug)] +pub struct MissingBody { + pub node: InFile, + pub kind: MissingBodyItemKind, +} + #[derive(Debug)] pub struct FunctionalRecordUpdateOnNonStruct { pub base_expr: InFile, @@ -1392,6 +1399,9 @@ impl<'a, 'db> DiagnosticsCollector<'a, 'db> { ExpressionStoreDiagnostics::FruInDestructuringAssignment { node } => { FruInDestructuringAssignment { node: *node }.into() } + ExpressionStoreDiagnostics::MissingBody { node, kind } => { + MissingBody { node: *node, kind: *kind }.into() + } }); } diff --git a/crates/ide-diagnostics/src/handlers/missing_body.rs b/crates/ide-diagnostics/src/handlers/missing_body.rs new file mode 100644 index 000000000000..790533627264 --- /dev/null +++ b/crates/ide-diagnostics/src/handlers/missing_body.rs @@ -0,0 +1,72 @@ +use crate::{Diagnostic, DiagnosticCode, DiagnosticsContext}; + +// Diagnostic: missing-body +// +// This diagnostic is triggered when a body is missing. +pub(crate) fn missing_body(ctx: &DiagnosticsContext<'_, '_>, d: &hir::MissingBody) -> Diagnostic { + let message = match d.kind { + hir::MissingBodyItemKind::AssocConst => "associated constant in `impl` without body", + hir::MissingBodyItemKind::AssocType => "associated type in `impl` without body", + hir::MissingBodyItemKind::Const => "free constant item without body", + hir::MissingBodyItemKind::Static => "free static item without body", + hir::MissingBodyItemKind::TypeAlias => "free type alias without body", + }; + Diagnostic::new_with_syntax_node_ptr(ctx, DiagnosticCode::SyntaxError, message, d.node).stable() +} + +#[cfg(test)] +mod tests { + use crate::tests::check_diagnostics; + + #[test] + fn associated_const() { + check_diagnostics( + r#" +trait Foo { const BAR: u32; } +impl Foo for () { const BAR: u32; } + //^^^^^^^^^^^^^^^ error: associated constant in `impl` without body + "#, + ); + } + + #[test] + fn associated_type_impl() { + check_diagnostics( + r#" +trait Foo { type Bar; } +impl Foo for () { type Bar; } + //^^^^^^^^^ error: associated type in `impl` without body + "#, + ); + } + + #[test] + fn free_const() { + check_diagnostics( + r#" + const FOO: u32; +//^^^^^^^^^^^^^^^ error: free constant item without body + "#, + ); + } + + #[test] + fn free_static() { + check_diagnostics( + r#" + static FOO: u32; +//^^^^^^^^^^^^^^^^ error: free static item without body + "#, + ); + } + + #[test] + fn type_alias_module() { + check_diagnostics( + r#" + type Foo; +//^^^^^^^^^ error: free type alias without body + "#, + ); + } +} diff --git a/crates/ide-diagnostics/src/lib.rs b/crates/ide-diagnostics/src/lib.rs index 5d816a8d41c3..4e941f8da070 100644 --- a/crates/ide-diagnostics/src/lib.rs +++ b/crates/ide-diagnostics/src/lib.rs @@ -60,6 +60,7 @@ mod handlers { pub(crate) mod method_call_illegal_sized_bound; pub(crate) mod mismatched_arg_count; pub(crate) mod mismatched_array_pat_len; + pub(crate) mod missing_body; pub(crate) mod missing_fields; pub(crate) mod missing_lifetime; pub(crate) mod missing_match_arms; @@ -545,6 +546,7 @@ pub fn semantic_diagnostics( } AnyDiagnostic::UnimplementedTrait(d) => handlers::unimplemented_trait::unimplemented_trait(&ctx, &d), AnyDiagnostic::FruInDestructuringAssignment(d) => handlers::fru_in_destructuring_assignment::fru_in_destructuring_assignment(&ctx, &d), + AnyDiagnostic::MissingBody(d) => handlers::missing_body::missing_body(&ctx, &d), AnyDiagnostic::ExplicitDropMethodUse(d) => handlers::explicit_drop_method_use::explicit_drop_method_use(&ctx, &d), AnyDiagnostic::YieldOutsideCoroutine(d) => handlers::yield_outside_coroutine::yield_outside_coroutine(&ctx, &d), AnyDiagnostic::ReturnOutsideFunction(d) => handlers::return_outside_function::return_outside_function(&ctx, &d),