Unnamed repository; edit this file 'description' to name the repository.
| -rw-r--r-- | crates/hir-def/src/attrs.rs | 22 | ||||
| -rw-r--r-- | crates/hir-ty/src/diagnostics/expr.rs | 47 | ||||
| -rw-r--r-- | crates/hir/src/diagnostics.rs | 11 | ||||
| -rw-r--r-- | crates/ide-diagnostics/src/handlers/unused_must_use.rs | 30 |
4 files changed, 76 insertions, 34 deletions
diff --git a/crates/hir-def/src/attrs.rs b/crates/hir-def/src/attrs.rs index 352c98a76a..43bbe99b13 100644 --- a/crates/hir-def/src/attrs.rs +++ b/crates/hir-def/src/attrs.rs @@ -1159,6 +1159,28 @@ impl AttrFlags { }) } } + + /// Returns `None` if there is no `#[must_use]`, `Some(None)` if there is a `#[must_use]` without a message, + /// and `Some(Some(message))` if there is a `#[must_use]` with a message. + pub fn must_use_message(db: &dyn DefDatabase, owner: AttrDefId) -> Option<Option<&str>> { + if !AttrFlags::query(db, owner).contains(AttrFlags::IS_MUST_USE) { + return None; + } + return Some(must_use_message(db, owner)); + + #[salsa::tracked(returns(as_deref))] + fn must_use_message(db: &dyn DefDatabase, owner: AttrDefId) -> Option<Box<str>> { + collect_attrs(db, owner, |attr| { + if let ast::Meta::KeyValueMeta(attr) = attr + && attr.path().is1("must_use") + && let Some(message) = attr.value_string() + { + return ControlFlow::Break(Box::from(&*message)); + } + ControlFlow::Continue(()) + }) + } + } } fn merge_repr(this: &mut ReprOptions, other: ReprOptions) { diff --git a/crates/hir-ty/src/diagnostics/expr.rs b/crates/hir-ty/src/diagnostics/expr.rs index 768b185ae7..760ebd27e0 100644 --- a/crates/hir-ty/src/diagnostics/expr.rs +++ b/crates/hir-ty/src/diagnostics/expr.rs @@ -7,8 +7,7 @@ use std::fmt; use base_db::Crate; use either::Either; use hir_def::{ - AdtId, AssocItemId, AttrDefId, CallableDefId, DefWithBodyId, HasModule, ItemContainerId, - Lookup, + AdtId, AssocItemId, CallableDefId, DefWithBodyId, HasModule, ItemContainerId, Lookup, attrs::AttrFlags, lang_item::LangItems, resolver::{HasResolver, ValueNs}, @@ -46,7 +45,7 @@ pub(crate) use hir_def::{ hir::{Expr, ExprId, MatchArm, Pat, PatId, RecordSpread, Statement}, }; -pub enum BodyValidationDiagnostic { +pub enum BodyValidationDiagnostic<'db> { RecordMissingFields { record: Either<ExprId, PatId>, variant: VariantId, @@ -71,15 +70,16 @@ pub enum BodyValidationDiagnostic { }, UnusedMustUse { expr: ExprId, + message: Option<&'db str>, }, } -impl BodyValidationDiagnostic { +impl<'db> BodyValidationDiagnostic<'db> { pub fn collect( - db: &dyn HirDatabase, + db: &'db dyn HirDatabase, owner: DefWithBodyId, validate_lints: bool, - ) -> Vec<BodyValidationDiagnostic> { + ) -> Vec<BodyValidationDiagnostic<'db>> { let _p = tracing::info_span!("BodyValidationDiagnostic::collect").entered(); let infer = InferenceResult::of(db, owner); let body = Body::of(db, owner); @@ -106,7 +106,7 @@ struct ExprValidator<'db> { body: &'db Body, infer: &'db InferenceResult, env: ParamEnv<'db>, - diagnostics: Vec<BodyValidationDiagnostic>, + diagnostics: Vec<BodyValidationDiagnostic<'db>>, validate_lints: bool, infcx: InferCtxt<'db>, } @@ -354,7 +354,7 @@ impl<'db> ExprValidator<'db> { pattern_arena: &'a Arena<DeconstructedPat<'a, 'db>>, pat: PatId, initializer: Option<ExprId>, - ) -> Option<BodyValidationDiagnostic> { + ) -> Option<BodyValidationDiagnostic<'db>> { if self.infer.pat_has_type_mismatch(pat) { return None; } @@ -415,35 +415,32 @@ impl<'db> ExprValidator<'db> { pattern } - fn check_unused_must_use(&self, expr: ExprId) -> Option<BodyValidationDiagnostic> { - let db = self.db(); - let must_use_fn = match &self.body[expr] { + fn check_unused_must_use(&self, expr: ExprId) -> Option<BodyValidationDiagnostic<'db>> { + let fn_def = match &self.body[expr] { Expr::Call { callee, .. } => { let callee_ty = self.infer.expr_ty(*callee); if let TyKind::FnDef(CallableIdWrapper(CallableDefId::FunctionId(func)), _) = callee_ty.kind() { - AttrFlags::query(db, AttrDefId::FunctionId(func)) - .contains(AttrFlags::IS_MUST_USE) + Some(func.into()) } else { - false + None } } Expr::MethodCall { .. } => { - self.infer.method_resolution(expr).is_some_and(|(func, _)| { - AttrFlags::query(db, AttrDefId::FunctionId(func)) - .contains(AttrFlags::IS_MUST_USE) - }) + self.infer.method_resolution(expr).map(|(func, _)| func.into()) } _ => return None, }; - let must_use_ty = - self.infer.type_of_expr_with_adjust(expr).is_some_and(|ty| match ty.kind() { - TyKind::Adt(adt, _) => AttrFlags::query(db, AttrDefId::AdtId(adt.def_id())) - .contains(AttrFlags::IS_MUST_USE), - _ => false, - }); - (must_use_fn || must_use_ty).then_some(BodyValidationDiagnostic::UnusedMustUse { expr }) + let ty_def = self.infer.type_of_expr_with_adjust(expr).and_then(|ty| match ty.kind() { + TyKind::Adt(adt, _) => Some(adt.def_id().into()), + _ => None, + }); + let must_use_diag = |owner| { + AttrFlags::must_use_message(self.db(), owner?) + .map(|message| BodyValidationDiagnostic::UnusedMustUse { expr, message }) + }; + must_use_diag(fn_def).or_else(|| must_use_diag(ty_def)) } fn check_for_trailing_return(&mut self, body_expr: ExprId, body: &Body) { diff --git a/crates/hir/src/diagnostics.rs b/crates/hir/src/diagnostics.rs index fd9ca8c402..401bbd4a59 100644 --- a/crates/hir/src/diagnostics.rs +++ b/crates/hir/src/diagnostics.rs @@ -133,7 +133,7 @@ diagnostics![AnyDiagnostic<'db> -> PrivateField, RemoveTrailingReturn, RemoveUnnecessaryElse, - UnusedMustUse, + UnusedMustUse<'db>, ReplaceFilterMapNextWithFindMap, TraitImplIncorrectSafety, TraitImplMissingAssocItems, @@ -455,8 +455,9 @@ pub struct RemoveUnnecessaryElse { } #[derive(Debug)] -pub struct UnusedMustUse { +pub struct UnusedMustUse<'db> { pub expr: InFile<ExprOrPatPtr>, + pub message: Option<&'db str>, } #[derive(Debug)] @@ -576,7 +577,7 @@ pub struct UnimplementedTrait<'db> { impl<'db> AnyDiagnostic<'db> { pub(crate) fn body_validation_diagnostic( db: &'db dyn HirDatabase, - diagnostic: BodyValidationDiagnostic, + diagnostic: BodyValidationDiagnostic<'db>, source_map: &hir_def::expr_store::BodySourceMap, ) -> Option<AnyDiagnostic<'db>> { match diagnostic { @@ -697,9 +698,9 @@ impl<'db> AnyDiagnostic<'db> { ); } } - BodyValidationDiagnostic::UnusedMustUse { expr } => { + BodyValidationDiagnostic::UnusedMustUse { expr, message } => { if let Ok(source_ptr) = source_map.expr_syntax(expr) { - return Some(UnusedMustUse { expr: source_ptr }.into()); + return Some(UnusedMustUse { expr: source_ptr, message }.into()); } } } diff --git a/crates/ide-diagnostics/src/handlers/unused_must_use.rs b/crates/ide-diagnostics/src/handlers/unused_must_use.rs index 4b9ecf2169..e8d0717c91 100644 --- a/crates/ide-diagnostics/src/handlers/unused_must_use.rs +++ b/crates/ide-diagnostics/src/handlers/unused_must_use.rs @@ -4,14 +4,18 @@ use crate::{Diagnostic, DiagnosticCode, DiagnosticsContext}; // // This diagnostic is triggered when a value with the `#[must_use]` attribute // is dropped without being used. -pub(crate) fn unused_must_use( - ctx: &DiagnosticsContext<'_, '_>, - d: &hir::UnusedMustUse, +pub(crate) fn unused_must_use<'db>( + ctx: &DiagnosticsContext<'_, 'db>, + d: &hir::UnusedMustUse<'db>, ) -> Diagnostic { + let message = match d.message { + Some(message) => format!("unused return value that must be used: {message}"), + None => "unused return value that must be used".to_owned(), + }; Diagnostic::new_with_syntax_node_ptr( ctx, DiagnosticCode::RustcLint("unused_must_use"), - "unused return value that must be used", + message, d.expr.map(Into::into), ) .stable() @@ -54,6 +58,24 @@ fn main() { } #[test] + fn with_message() { + check_diagnostics( + r#" +struct S; +impl S { + #[must_use = "custom message"] + fn produces(&self) -> i32 { 0 } +} +fn main() { + let s = S; + s.produces(); + //^^^^^^^^^^^^ warn: unused return value that must be used: custom message +} +"#, + ); + } + + #[test] fn unused_must_use_type() { check_diagnostics( r#" |