internal: unified missing fields diagnostic

This commit is contained in:
Aleksey Kladov 2021-06-13 15:48:54 +03:00
parent c6509a4592
commit 6383252cc2
5 changed files with 95 additions and 143 deletions

View File

@ -6,6 +6,7 @@
use std::any::Any; use std::any::Any;
use cfg::{CfgExpr, CfgOptions, DnfExpr}; use cfg::{CfgExpr, CfgOptions, DnfExpr};
use either::Either;
use hir_def::path::ModPath; use hir_def::path::ModPath;
use hir_expand::{name::Name, HirFileId, InFile}; use hir_expand::{name::Name, HirFileId, InFile};
use stdx::format_to; use stdx::format_to;
@ -324,60 +325,11 @@ fn as_any(&self) -> &(dyn Any + Send + 'static) {
#[derive(Debug)] #[derive(Debug)]
pub struct MissingFields { pub struct MissingFields {
pub file: HirFileId, pub file: HirFileId,
pub field_list_parent: AstPtr<ast::RecordExpr>, pub field_list_parent: Either<AstPtr<ast::RecordExpr>, AstPtr<ast::RecordPat>>,
pub field_list_parent_path: Option<AstPtr<ast::Path>>, pub field_list_parent_path: Option<AstPtr<ast::Path>>,
pub missed_fields: Vec<Name>, pub missed_fields: Vec<Name>,
} }
// Diagnostic: missing-pat-fields
//
// This diagnostic is triggered if pattern lacks some fields that exist in the corresponding structure.
//
// Example:
//
// ```rust
// struct A { a: u8, b: u8 }
//
// let a = A { a: 10, b: 20 };
//
// if let A { a } = a {
// // ...
// }
// ```
#[derive(Debug)]
pub struct MissingPatFields {
pub file: HirFileId,
pub field_list_parent: AstPtr<ast::RecordPat>,
pub field_list_parent_path: Option<AstPtr<ast::Path>>,
pub missed_fields: Vec<Name>,
}
impl Diagnostic for MissingPatFields {
fn code(&self) -> DiagnosticCode {
DiagnosticCode("missing-pat-fields")
}
fn message(&self) -> String {
let mut buf = String::from("Missing structure fields:\n");
for field in &self.missed_fields {
format_to!(buf, "- {}\n", field);
}
buf
}
fn display_source(&self) -> InFile<SyntaxNodePtr> {
InFile {
file_id: self.file,
value: self
.field_list_parent_path
.clone()
.map(SyntaxNodePtr::from)
.unwrap_or_else(|| self.field_list_parent.clone().into()),
}
}
fn as_any(&self) -> &(dyn Any + Send + 'static) {
self
}
}
// Diagnostic: replace-filter-map-next-with-find-map // Diagnostic: replace-filter-map-next-with-find-map
// //
// This diagnostic is triggered when `.filter_map(..).next()` is used, rather than the more concise `.find_map(..)`. // This diagnostic is triggered when `.filter_map(..).next()` is used, rather than the more concise `.find_map(..)`.

View File

@ -88,9 +88,9 @@
diagnostics::{ diagnostics::{
AnyDiagnostic, BreakOutsideOfLoop, InactiveCode, InternalBailedOut, MacroError, AnyDiagnostic, BreakOutsideOfLoop, InactiveCode, InternalBailedOut, MacroError,
MismatchedArgCount, MissingFields, MissingMatchArms, MissingOkOrSomeInTailExpr, MismatchedArgCount, MissingFields, MissingMatchArms, MissingOkOrSomeInTailExpr,
MissingPatFields, MissingUnsafe, NoSuchField, RemoveThisSemicolon, MissingUnsafe, NoSuchField, RemoveThisSemicolon, ReplaceFilterMapNextWithFindMap,
ReplaceFilterMapNextWithFindMap, UnimplementedBuiltinMacro, UnresolvedExternCrate, UnimplementedBuiltinMacro, UnresolvedExternCrate, UnresolvedImport, UnresolvedMacroCall,
UnresolvedImport, UnresolvedMacroCall, UnresolvedModule, UnresolvedProcMacro, UnresolvedModule, UnresolvedProcMacro,
}, },
has_source::HasSource, has_source::HasSource,
semantics::{PathResolution, Semantics, SemanticsScope}, semantics::{PathResolution, Semantics, SemanticsScope},
@ -1098,67 +1098,70 @@ pub fn diagnostics(
BodyValidationDiagnostic::collect(db, self.id.into(), internal_diagnostics) BodyValidationDiagnostic::collect(db, self.id.into(), internal_diagnostics)
{ {
match diagnostic { match diagnostic {
BodyValidationDiagnostic::RecordLiteralMissingFields { BodyValidationDiagnostic::RecordMissingFields {
record_expr, record,
variant, variant,
missed_fields, missed_fields,
} => match source_map.expr_syntax(record_expr) { } => {
Ok(source_ptr) => { let variant_data = variant.variant_data(db.upcast());
let root = source_ptr.file_syntax(db.upcast()); let missed_fields = missed_fields
if let ast::Expr::RecordExpr(record_expr) = &source_ptr.value.to_node(&root) .into_iter()
{ .map(|idx| variant_data.fields()[idx].name.clone())
if let Some(_) = record_expr.record_expr_field_list() { .collect();
let variant_data = variant.variant_data(db.upcast());
let missed_fields = missed_fields match record {
.into_iter() Either::Left(record_expr) => match source_map.expr_syntax(record_expr) {
.map(|idx| variant_data.fields()[idx].name.clone()) Ok(source_ptr) => {
.collect(); let root = source_ptr.file_syntax(db.upcast());
acc.push( if let ast::Expr::RecordExpr(record_expr) =
MissingFields { &source_ptr.value.to_node(&root)
file: source_ptr.file_id, {
field_list_parent: AstPtr::new(record_expr), if let Some(_) = record_expr.record_expr_field_list() {
field_list_parent_path: record_expr acc.push(
.path() MissingFields {
.map(|path| AstPtr::new(&path)), file: source_ptr.file_id,
missed_fields, field_list_parent: Either::Left(AstPtr::new(
record_expr,
)),
field_list_parent_path: record_expr
.path()
.map(|path| AstPtr::new(&path)),
missed_fields,
}
.into(),
)
} }
.into(),
)
}
}
}
Err(SyntheticSyntax) => (),
},
BodyValidationDiagnostic::RecordPatMissingFields {
record_pat,
variant,
missed_fields,
} => match source_map.pat_syntax(record_pat) {
Ok(source_ptr) => {
if let Some(expr) = source_ptr.value.as_ref().left() {
let root = source_ptr.file_syntax(db.upcast());
if let ast::Pat::RecordPat(record_pat) = expr.to_node(&root) {
if let Some(_) = record_pat.record_pat_field_list() {
let variant_data = variant.variant_data(db.upcast());
let missed_fields = missed_fields
.into_iter()
.map(|idx| variant_data.fields()[idx].name.clone())
.collect();
sink.push(MissingPatFields {
file: source_ptr.file_id,
field_list_parent: AstPtr::new(&record_pat),
field_list_parent_path: record_pat
.path()
.map(|path| AstPtr::new(&path)),
missed_fields,
})
} }
} }
} Err(SyntheticSyntax) => (),
},
Either::Right(record_pat) => match source_map.pat_syntax(record_pat) {
Ok(source_ptr) => {
if let Some(expr) = source_ptr.value.as_ref().left() {
let root = source_ptr.file_syntax(db.upcast());
if let ast::Pat::RecordPat(record_pat) = expr.to_node(&root) {
if let Some(_) = record_pat.record_pat_field_list() {
acc.push(
MissingFields {
file: source_ptr.file_id,
field_list_parent: Either::Right(AstPtr::new(
&record_pat,
)),
field_list_parent_path: record_pat
.path()
.map(|path| AstPtr::new(&path)),
missed_fields,
}
.into(),
)
}
}
}
}
Err(SyntheticSyntax) => (),
},
} }
Err(SyntheticSyntax) => (), }
},
BodyValidationDiagnostic::ReplaceFilterMapNextWithFindMap { method_call_expr } => { BodyValidationDiagnostic::ReplaceFilterMapNextWithFindMap { method_call_expr } => {
if let Ok(next_source_ptr) = source_map.expr_syntax(method_call_expr) { if let Ok(next_source_ptr) = source_map.expr_syntax(method_call_expr) {
sink.push(ReplaceFilterMapNextWithFindMap { sink.push(ReplaceFilterMapNextWithFindMap {

View File

@ -8,6 +8,7 @@
expr::Statement, path::path, resolver::HasResolver, AssocItemId, DefWithBodyId, HasModule, expr::Statement, path::path, resolver::HasResolver, AssocItemId, DefWithBodyId, HasModule,
}; };
use hir_expand::name; use hir_expand::name;
use itertools::Either;
use rustc_hash::FxHashSet; use rustc_hash::FxHashSet;
use crate::{ use crate::{
@ -26,13 +27,8 @@
}; };
pub enum BodyValidationDiagnostic { pub enum BodyValidationDiagnostic {
RecordLiteralMissingFields { RecordMissingFields {
record_expr: ExprId, record: Either<ExprId, PatId>,
variant: VariantId,
missed_fields: Vec<LocalFieldId>,
},
RecordPatMissingFields {
record_pat: PatId,
variant: VariantId, variant: VariantId,
missed_fields: Vec<LocalFieldId>, missed_fields: Vec<LocalFieldId>,
}, },
@ -95,8 +91,8 @@ fn validate_body(&mut self, db: &dyn HirDatabase) {
if let Some((variant, missed_fields, true)) = if let Some((variant, missed_fields, true)) =
record_literal_missing_fields(db, &self.infer, id, expr) record_literal_missing_fields(db, &self.infer, id, expr)
{ {
self.diagnostics.push(BodyValidationDiagnostic::RecordLiteralMissingFields { self.diagnostics.push(BodyValidationDiagnostic::RecordMissingFields {
record_expr: id, record: Either::Left(id),
variant, variant,
missed_fields, missed_fields,
}); });
@ -116,8 +112,8 @@ fn validate_body(&mut self, db: &dyn HirDatabase) {
if let Some((variant, missed_fields, true)) = if let Some((variant, missed_fields, true)) =
record_pattern_missing_fields(db, &self.infer, id, pat) record_pattern_missing_fields(db, &self.infer, id, pat)
{ {
self.diagnostics.push(BodyValidationDiagnostic::RecordPatMissingFields { self.diagnostics.push(BodyValidationDiagnostic::RecordMissingFields {
record_pat: id, record: Either::Right(id),
variant, variant,
missed_fields, missed_fields,
}); });

View File

@ -1055,20 +1055,6 @@ fn foo() {
)); ));
} }
#[test]
fn missing_record_pat_field_diagnostic() {
check_diagnostics(
r#"
struct S { foo: i32, bar: () }
fn baz(s: S) {
let S { foo: _ } = s;
//^ Missing structure fields:
//| - bar
}
"#,
);
}
#[test] #[test]
fn missing_record_pat_field_no_diagnostic_if_not_exhaustive() { fn missing_record_pat_field_no_diagnostic_if_not_exhaustive() {
check_diagnostics( check_diagnostics(

View File

@ -1,3 +1,4 @@
use either::Either;
use hir::{db::AstDatabase, InFile}; use hir::{db::AstDatabase, InFile};
use ide_assists::Assist; use ide_assists::Assist;
use ide_db::source_change::SourceChange; use ide_db::source_change::SourceChange;
@ -7,7 +8,7 @@
use crate::diagnostics::{fix, Diagnostic, DiagnosticsContext}; use crate::diagnostics::{fix, Diagnostic, DiagnosticsContext};
// Diagnostic: missing-structure-fields // Diagnostic: missing-fields
// //
// This diagnostic is triggered if record lacks some fields that exist in the corresponding structure. // This diagnostic is triggered if record lacks some fields that exist in the corresponding structure.
// //
@ -29,15 +30,11 @@ pub(super) fn missing_fields(ctx: &DiagnosticsContext<'_>, d: &hir::MissingField
d.field_list_parent_path d.field_list_parent_path
.clone() .clone()
.map(SyntaxNodePtr::from) .map(SyntaxNodePtr::from)
.unwrap_or_else(|| d.field_list_parent.clone().into()), .unwrap_or_else(|| d.field_list_parent.clone().either(|it| it.into(), |it| it.into())),
); );
Diagnostic::new( Diagnostic::new("missing-fields", message, ctx.sema.diagnostics_display_range(ptr).range)
"missing-structure-fields", .with_fixes(fixes(ctx, d))
message,
ctx.sema.diagnostics_display_range(ptr).range,
)
.with_fixes(fixes(ctx, d))
} }
fn fixes(ctx: &DiagnosticsContext<'_>, d: &hir::MissingFields) -> Option<Vec<Assist>> { fn fixes(ctx: &DiagnosticsContext<'_>, d: &hir::MissingFields) -> Option<Vec<Assist>> {
@ -51,7 +48,11 @@ fn fixes(ctx: &DiagnosticsContext<'_>, d: &hir::MissingFields) -> Option<Vec<Ass
} }
let root = ctx.sema.db.parse_or_expand(d.file)?; let root = ctx.sema.db.parse_or_expand(d.file)?;
let field_list_parent = d.field_list_parent.to_node(&root); let field_list_parent = match &d.field_list_parent {
Either::Left(record_expr) => record_expr.to_node(&root),
// FIXE: patterns should be fixable as well.
Either::Right(_) => return None,
};
let old_field_list = field_list_parent.record_expr_field_list()?; let old_field_list = field_list_parent.record_expr_field_list()?;
let new_field_list = old_field_list.clone_for_update(); let new_field_list = old_field_list.clone_for_update();
for f in d.missed_fields.iter() { for f in d.missed_fields.iter() {
@ -76,7 +77,21 @@ fn fixes(ctx: &DiagnosticsContext<'_>, d: &hir::MissingFields) -> Option<Vec<Ass
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use crate::diagnostics::tests::{check_fix, check_no_diagnostics}; use crate::diagnostics::tests::{check_diagnostics, check_fix, check_no_diagnostics};
#[test]
fn missing_record_pat_field_diagnostic() {
check_diagnostics(
r#"
struct S { foo: i32, bar: () }
fn baz(s: S) {
let S { foo: _ } = s;
//^ Missing structure fields:
//| - bar
}
"#,
);
}
#[test] #[test]
fn test_fill_struct_fields_empty() { fn test_fill_struct_fields_empty() {