Implement default field values .. syntax

- Added `RecordSpread` enum to distinguish between no spread, field defaults, and spread expressions
- Updated `FieldData` to include `default_value` field
- Modified record literal lowering to handle default field values
- Updated diagnostics to check for missing fields considering defaults
- Added methods to get matched fields for records for completions
- Enhanced hover support for struct rest patterns
This commit is contained in:
kouhe 2026-01-05 19:10:34 +08:00 committed by kouhe3
parent d7136caaf1
commit b75c58d4b3
19 changed files with 355 additions and 75 deletions

View file

@ -32,7 +32,7 @@ use crate::{
expr_store::path::Path,
hir::{
Array, AsmOperand, Binding, BindingId, Expr, ExprId, ExprOrPatId, Label, LabelId, Pat,
PatId, RecordFieldPat, Statement,
PatId, RecordFieldPat, RecordSpread, Statement,
},
nameres::{DefMap, block_def_map},
type_ref::{LifetimeRef, LifetimeRefId, PathId, TypeRef, TypeRefId},
@ -575,8 +575,8 @@ impl ExpressionStore {
for field in fields.iter() {
f(field.expr);
}
if let &Some(expr) = spread {
f(expr);
if let RecordSpread::Expr(expr) = spread {
f(*expr);
}
}
Expr::Closure { body, .. } => {
@ -706,8 +706,8 @@ impl ExpressionStore {
for field in fields.iter() {
f(field.expr);
}
if let &Some(expr) = spread {
f(expr);
if let RecordSpread::Expr(expr) = spread {
f(*expr);
}
}
Expr::Closure { body, .. } => {

View file

@ -47,7 +47,7 @@ use crate::{
hir::{
Array, Binding, BindingAnnotation, BindingId, BindingProblems, CaptureBy, ClosureKind,
Expr, ExprId, Item, Label, LabelId, Literal, MatchArm, Movability, OffsetOf, Pat, PatId,
RecordFieldPat, RecordLitField, Statement, generics::GenericParams,
RecordFieldPat, RecordLitField, RecordSpread, Statement, generics::GenericParams,
},
item_scope::BuiltinShadowMode,
item_tree::FieldsShape,
@ -1266,10 +1266,16 @@ impl<'db> ExprCollector<'db> {
Some(RecordLitField { name, expr })
})
.collect();
let spread = nfl.spread().map(|s| self.collect_expr(s));
let spread_expr = nfl.spread().map(|s| self.collect_expr(s));
let has_spread_syntax = nfl.dotdot_token().is_some();
let spread = match (spread_expr, has_spread_syntax) {
(None, false) => RecordSpread::None,
(None, true) => RecordSpread::FieldDefaults,
(Some(expr), _) => RecordSpread::Expr(expr),
};
Expr::RecordLit { path, fields, spread }
} else {
Expr::RecordLit { path, fields: Box::default(), spread: None }
Expr::RecordLit { path, fields: Box::default(), spread: RecordSpread::None }
};
self.alloc_expr(record_lit, syntax_ptr)
@ -1995,7 +2001,7 @@ impl<'db> ExprCollector<'db> {
}
}
fn collect_expr_opt(&mut self, expr: Option<ast::Expr>) -> ExprId {
pub fn collect_expr_opt(&mut self, expr: Option<ast::Expr>) -> ExprId {
match expr {
Some(expr) => self.collect_expr(expr),
None => self.missing_expr(),

View file

@ -10,7 +10,8 @@ use crate::{
builtin_type::BuiltinUint,
expr_store::{HygieneId, lower::ExprCollector, path::Path},
hir::{
Array, BindingAnnotation, Expr, ExprId, Literal, Pat, RecordLitField, Statement,
Array, BindingAnnotation, Expr, ExprId, Literal, Pat, RecordLitField, RecordSpread,
Statement,
format_args::{
self, FormatAlignment, FormatArgs, FormatArgsPiece, FormatArgument, FormatArgumentKind,
FormatArgumentsCollector, FormatCount, FormatDebugHex, FormatOptions,
@ -869,7 +870,7 @@ impl<'db> ExprCollector<'db> {
self.alloc_expr_desugared(Expr::RecordLit {
path: self.lang_path(lang_items.FormatPlaceholder).map(Box::new),
fields: Box::new([position, flags, precision, width]),
spread: None,
spread: RecordSpread::None,
})
} else {
let format_placeholder_new =

View file

@ -16,7 +16,8 @@ use crate::{
attrs::AttrFlags,
expr_store::path::{GenericArg, GenericArgs},
hir::{
Array, BindingAnnotation, CaptureBy, ClosureKind, Literal, Movability, Statement,
Array, BindingAnnotation, CaptureBy, ClosureKind, Literal, Movability, RecordSpread,
Statement,
generics::{GenericParams, WherePredicate},
},
lang_item::LangItemTarget,
@ -139,7 +140,7 @@ pub fn print_variant_body_hir(db: &dyn DefDatabase, owner: VariantId, edition: E
}
for (_, data) in fields.fields().iter() {
let FieldData { name, type_ref, visibility, is_unsafe } = data;
let FieldData { name, type_ref, visibility, is_unsafe, default_value: _ } = data;
match visibility {
crate::item_tree::RawVisibility::Module(interned, _visibility_explicitness) => {
w!(p, "pub(in {})", interned.display(db, p.edition))
@ -679,10 +680,17 @@ impl Printer<'_> {
p.print_expr(field.expr);
wln!(p, ",");
}
if let Some(spread) = spread {
w!(p, "..");
p.print_expr(*spread);
wln!(p);
match spread {
RecordSpread::None => {}
RecordSpread::FieldDefaults => {
w!(p, "..");
wln!(p);
}
RecordSpread::Expr(spread_expr) => {
w!(p, "..");
p.print_expr(*spread_expr);
wln!(p);
}
}
});
w!(self, "}}");

View file

@ -187,6 +187,13 @@ impl From<ast::LiteralKind> for Literal {
}
}
#[derive(Debug, Clone, Eq, PartialEq, Copy)]
pub enum RecordSpread {
None,
FieldDefaults,
Expr(ExprId),
}
#[derive(Debug, Clone, Eq, PartialEq)]
pub enum Expr {
/// This is produced if the syntax tree does not have a required expression piece.
@ -259,7 +266,7 @@ pub enum Expr {
RecordLit {
path: Option<Box<Path>>,
fields: Box<[RecordLitField]>,
spread: Option<ExprId>,
spread: RecordSpread,
},
Field {
expr: ExprId,

View file

@ -12,7 +12,7 @@ use intern::{Symbol, sym};
use la_arena::{Arena, Idx};
use rustc_abi::{IntegerType, ReprOptions};
use syntax::{
NodeOrToken, SyntaxNodePtr, T,
AstNode, NodeOrToken, SyntaxNodePtr, T,
ast::{self, HasGenericParams, HasName, HasVisibility, IsString},
};
use thin_vec::ThinVec;
@ -754,6 +754,7 @@ pub struct FieldData {
pub type_ref: TypeRefId,
pub visibility: RawVisibility,
pub is_unsafe: bool,
pub default_value: Option<ExprId>,
}
pub type LocalFieldId = Idx<FieldData>;
@ -903,7 +904,14 @@ fn lower_fields<Field: ast::HasAttrs + ast::HasVisibility>(
.filter_map(NodeOrToken::into_token)
.any(|token| token.kind() == T![unsafe]);
let name = field_name(idx, &field);
arena.alloc(FieldData { name, type_ref, visibility, is_unsafe });
// Check if field has default value (only for record fields)
let default_value = ast::RecordField::cast(field.syntax().clone())
.and_then(|rf| rf.eq_token().is_some().then_some(rf.expr()))
.flatten()
.map(|expr| col.collect_expr_opt(Some(expr)));
arena.alloc(FieldData { name, type_ref, visibility, is_unsafe, default_value });
idx += 1;
}
Err(cfg) => {

View file

@ -41,7 +41,7 @@ use crate::{
pub(crate) use hir_def::{
LocalFieldId, VariantId,
expr_store::Body,
hir::{Expr, ExprId, MatchArm, Pat, PatId, Statement},
hir::{Expr, ExprId, MatchArm, Pat, PatId, RecordSpread, Statement},
};
pub enum BodyValidationDiagnostic {
@ -123,7 +123,7 @@ impl<'db> ExprValidator<'db> {
}
for (id, expr) in body.exprs() {
if let Some((variant, missed_fields, true)) =
if let Some((variant, missed_fields)) =
record_literal_missing_fields(db, self.infer, id, expr)
{
self.diagnostics.push(BodyValidationDiagnostic::RecordMissingFields {
@ -154,7 +154,7 @@ impl<'db> ExprValidator<'db> {
}
for (id, pat) in body.pats() {
if let Some((variant, missed_fields, true)) =
if let Some((variant, missed_fields)) =
record_pattern_missing_fields(db, self.infer, id, pat)
{
self.diagnostics.push(BodyValidationDiagnostic::RecordMissingFields {
@ -557,9 +557,9 @@ pub fn record_literal_missing_fields(
infer: &InferenceResult,
id: ExprId,
expr: &Expr,
) -> Option<(VariantId, Vec<LocalFieldId>, /*exhaustive*/ bool)> {
let (fields, exhaustive) = match expr {
Expr::RecordLit { fields, spread, .. } => (fields, spread.is_none()),
) -> Option<(VariantId, Vec<LocalFieldId>)> {
let (fields, spread) = match expr {
Expr::RecordLit { fields, spread, .. } => (fields, spread),
_ => return None,
};
@ -571,15 +571,28 @@ pub fn record_literal_missing_fields(
let variant_data = variant_def.fields(db);
let specified_fields: FxHashSet<_> = fields.iter().map(|f| &f.name).collect();
// don't show missing fields if:
// - has ..expr
// - or has default value + ..
// - or already in code
let missed_fields: Vec<LocalFieldId> = variant_data
.fields()
.iter()
.filter_map(|(f, d)| if specified_fields.contains(&d.name) { None } else { Some(f) })
.filter_map(|(f, d)| {
if specified_fields.contains(&d.name)
|| matches!(spread, RecordSpread::Expr(_))
|| (d.default_value.is_some() && matches!(spread, RecordSpread::FieldDefaults))
{
None
} else {
Some(f)
}
})
.collect();
if missed_fields.is_empty() {
return None;
}
Some((variant_def, missed_fields, exhaustive))
Some((variant_def, missed_fields))
}
pub fn record_pattern_missing_fields(
@ -587,9 +600,9 @@ pub fn record_pattern_missing_fields(
infer: &InferenceResult,
id: PatId,
pat: &Pat,
) -> Option<(VariantId, Vec<LocalFieldId>, /*exhaustive*/ bool)> {
let (fields, exhaustive) = match pat {
Pat::Record { path: _, args, ellipsis } => (args, !ellipsis),
) -> Option<(VariantId, Vec<LocalFieldId>)> {
let (fields, ellipsis) = match pat {
Pat::Record { path: _, args, ellipsis } => (args, *ellipsis),
_ => return None,
};
@ -601,15 +614,22 @@ pub fn record_pattern_missing_fields(
let variant_data = variant_def.fields(db);
let specified_fields: FxHashSet<_> = fields.iter().map(|f| &f.name).collect();
// don't show missing fields if:
// - in code
// - or has ..
let missed_fields: Vec<LocalFieldId> = variant_data
.fields()
.iter()
.filter_map(|(f, d)| if specified_fields.contains(&d.name) { None } else { Some(f) })
.filter_map(
|(f, d)| {
if specified_fields.contains(&d.name) || ellipsis { None } else { Some(f) }
},
)
.collect();
if missed_fields.is_empty() {
return None;
}
Some((variant_def, missed_fields, exhaustive))
Some((variant_def, missed_fields))
}
fn types_of_subpatterns_do_match(pat: PatId, body: &Body, infer: &InferenceResult) -> bool {

View file

@ -8,7 +8,7 @@ use hir_def::{
expr_store::path::Path,
hir::{
Array, AsmOperand, BinaryOp, BindingId, CaptureBy, Expr, ExprId, ExprOrPatId, Pat, PatId,
Statement, UnaryOp,
RecordSpread, Statement, UnaryOp,
},
item_tree::FieldsShape,
resolver::ValueNs,
@ -627,7 +627,7 @@ impl<'db> InferenceContext<'_, 'db> {
self.consume_expr(expr);
}
Expr::RecordLit { fields, spread, .. } => {
if let &Some(expr) = spread {
if let RecordSpread::Expr(expr) = *spread {
self.consume_expr(expr);
}
self.consume_exprs(fields.iter().map(|it| it.expr));

View file

@ -8,7 +8,7 @@ use hir_def::{
expr_store::path::{GenericArgs as HirGenericArgs, Path},
hir::{
Array, AsmOperand, AsmOptions, BinaryOp, BindingAnnotation, Expr, ExprId, ExprOrPatId,
InlineAsmKind, LabelId, Literal, Pat, PatId, Statement, UnaryOp,
InlineAsmKind, LabelId, Literal, Pat, PatId, RecordSpread, Statement, UnaryOp,
},
resolver::ValueNs,
};
@ -657,8 +657,8 @@ impl<'db> InferenceContext<'_, 'db> {
}
}
}
if let Some(expr) = spread {
self.infer_expr(*expr, &Expectation::has_type(ty), ExprIsRead::Yes);
if let RecordSpread::Expr(expr) = *spread {
self.infer_expr(expr, &Expectation::has_type(ty), ExprIsRead::Yes);
}
ty
}

View file

@ -2,7 +2,8 @@
//! between `Deref` and `DerefMut` or `Index` and `IndexMut` or similar.
use hir_def::hir::{
Array, AsmOperand, BinaryOp, BindingAnnotation, Expr, ExprId, Pat, PatId, Statement, UnaryOp,
Array, AsmOperand, BinaryOp, BindingAnnotation, Expr, ExprId, Pat, PatId, RecordSpread,
Statement, UnaryOp,
};
use rustc_ast_ir::Mutability;
@ -132,8 +133,11 @@ impl<'db> InferenceContext<'_, 'db> {
Expr::Become { expr } => {
self.infer_mut_expr(*expr, Mutability::Not);
}
Expr::RecordLit { path: _, fields, spread } => {
self.infer_mut_not_expr_iter(fields.iter().map(|it| it.expr).chain(*spread))
Expr::RecordLit { path: _, fields, spread, .. } => {
self.infer_mut_not_expr_iter(fields.iter().map(|it| it.expr));
if let RecordSpread::Expr(expr) = *spread {
self.infer_mut_expr(expr, Mutability::Not);
}
}
&Expr::Index { base, index } => {
if mutability == Mutability::Mut {

View file

@ -9,7 +9,7 @@ use hir_def::{
expr_store::{Body, ExpressionStore, HygieneId, path::Path},
hir::{
ArithOp, Array, BinaryOp, BindingAnnotation, BindingId, ExprId, LabelId, Literal, MatchArm,
Pat, PatId, RecordFieldPat, RecordLitField,
Pat, PatId, RecordFieldPat, RecordLitField, RecordSpread,
},
item_tree::FieldsShape,
lang_item::LangItems,
@ -867,16 +867,17 @@ impl<'a, 'db> MirLowerCtx<'a, 'db> {
}
Expr::Become { .. } => not_supported!("tail-calls"),
Expr::Yield { .. } => not_supported!("yield"),
Expr::RecordLit { fields, path, spread } => {
let spread_place = match spread {
&Some(it) => {
Expr::RecordLit { fields, path, spread, .. } => {
let spread_place = match *spread {
RecordSpread::Expr(it) => {
let Some((p, c)) = self.lower_expr_as_place(current, it, true)? else {
return Ok(None);
};
current = c;
Some(p)
}
None => None,
RecordSpread::None => None,
RecordSpread::FieldDefaults => not_supported!("empty record spread"),
};
let variant_id =
self.infer.variant_resolution_for_expr(expr_id).ok_or_else(|| match path {

View file

@ -2003,6 +2003,15 @@ impl<'db> SemanticsImpl<'db> {
.unwrap_or_default()
}
pub fn record_literal_matched_fields(
&self,
literal: &ast::RecordExpr,
) -> Vec<(Field, Type<'db>)> {
self.analyze(literal.syntax())
.and_then(|it| it.record_literal_matched_fields(self.db, literal))
.unwrap_or_default()
}
pub fn record_pattern_missing_fields(
&self,
pattern: &ast::RecordPat,
@ -2012,6 +2021,15 @@ impl<'db> SemanticsImpl<'db> {
.unwrap_or_default()
}
pub fn record_pattern_matched_fields(
&self,
pattern: &ast::RecordPat,
) -> Vec<(Field, Type<'db>)> {
self.analyze(pattern.syntax())
.and_then(|it| it.record_pattern_matched_fields(self.db, pattern))
.unwrap_or_default()
}
fn with_ctx<F: FnOnce(&mut SourceToDefCtx<'_, '_>) -> T, T>(&self, f: F) -> T {
let mut ctx = SourceToDefCtx { db: self.db, cache: &mut self.s2d_cache.borrow_mut() };
f(&mut ctx)

View file

@ -17,7 +17,7 @@ use hir_def::{
path::Path,
scope::{ExprScopes, ScopeId},
},
hir::{BindingId, Expr, ExprId, ExprOrPatId, Pat},
hir::{BindingId, Expr, ExprId, ExprOrPatId, Pat, PatId},
lang_item::LangItems,
nameres::MacroSubNs,
resolver::{HasResolver, Resolver, TypeNs, ValueNs, resolver_for_scope},
@ -44,6 +44,7 @@ use hir_ty::{
};
use intern::sym;
use itertools::Itertools;
use rustc_hash::FxHashSet;
use rustc_type_ir::{
AliasTyKind,
inherent::{AdtDef, IntoKind, Ty as _},
@ -1241,21 +1242,31 @@ impl<'db> SourceAnalyzer<'db> {
let body = self.store()?;
let infer = self.infer()?;
let expr_id = self.expr_id(literal.clone().into())?;
let substs = infer.expr_or_pat_ty(expr_id).as_adt()?.1;
let (variant, missing_fields, _exhaustive) = match expr_id {
ExprOrPatId::ExprId(expr_id) => {
record_literal_missing_fields(db, infer, expr_id, &body[expr_id])?
}
ExprOrPatId::PatId(pat_id) => {
record_pattern_missing_fields(db, infer, pat_id, &body[pat_id])?
}
};
let expr_id = self.expr_id(literal.clone().into())?.as_expr()?;
let substs = infer.expr_ty(expr_id).as_adt()?.1;
let (variant, missing_fields) =
record_literal_missing_fields(db, infer, expr_id, &body[expr_id])?;
let res = self.missing_fields(db, substs, variant, missing_fields);
Some(res)
}
pub(crate) fn record_literal_matched_fields(
&self,
db: &'db dyn HirDatabase,
literal: &ast::RecordExpr,
) -> Option<Vec<(Field, Type<'db>)>> {
let body = self.store()?;
let infer = self.infer()?;
let expr_id = self.expr_id(literal.clone().into())?.as_expr()?;
let substs = infer.expr_ty(expr_id).as_adt()?.1;
let (variant, matched_fields) =
record_literal_matched_fields(db, infer, expr_id, &body[expr_id])?;
let res = self.missing_fields(db, substs, variant, matched_fields);
Some(res)
}
pub(crate) fn record_pattern_missing_fields(
&self,
db: &'db dyn HirDatabase,
@ -1267,12 +1278,29 @@ impl<'db> SourceAnalyzer<'db> {
let pat_id = self.pat_id(&pattern.clone().into())?.as_pat()?;
let substs = infer.pat_ty(pat_id).as_adt()?.1;
let (variant, missing_fields, _exhaustive) =
let (variant, missing_fields) =
record_pattern_missing_fields(db, infer, pat_id, &body[pat_id])?;
let res = self.missing_fields(db, substs, variant, missing_fields);
Some(res)
}
pub(crate) fn record_pattern_matched_fields(
&self,
db: &'db dyn HirDatabase,
pattern: &ast::RecordPat,
) -> Option<Vec<(Field, Type<'db>)>> {
let body = self.store()?;
let infer = self.infer()?;
let pat_id = self.pat_id(&pattern.clone().into())?.as_pat()?;
let substs = infer.pat_ty(pat_id).as_adt()?.1;
let (variant, matched_fields) =
record_pattern_matched_fields(db, infer, pat_id, &body[pat_id])?;
let res = self.missing_fields(db, substs, variant, matched_fields);
Some(res)
}
fn missing_fields(
&self,
db: &'db dyn HirDatabase,
@ -1810,3 +1838,67 @@ pub(crate) fn name_hygiene(db: &dyn HirDatabase, name: InFile<&SyntaxNode>) -> H
let ctx = span_map.span_at(name.value.text_range().start()).ctx;
HygieneId::new(ctx.opaque_and_semiopaque(db))
}
fn record_literal_matched_fields(
db: &dyn HirDatabase,
infer: &InferenceResult,
id: ExprId,
expr: &Expr,
) -> Option<(VariantId, Vec<LocalFieldId>)> {
let (fields, _spread) = match expr {
Expr::RecordLit { fields, spread, .. } => (fields, spread),
_ => return None,
};
let variant_def = infer.variant_resolution_for_expr(id)?;
if let VariantId::UnionId(_) = variant_def {
return None;
}
let variant_data = variant_def.fields(db);
let specified_fields: FxHashSet<_> = fields.iter().map(|f| &f.name).collect();
// suggest fields if:
// - not in code
let matched_fields: Vec<LocalFieldId> = variant_data
.fields()
.iter()
.filter_map(|(f, d)| (!specified_fields.contains(&d.name)).then_some(f))
.collect();
if matched_fields.is_empty() {
return None;
}
Some((variant_def, matched_fields))
}
fn record_pattern_matched_fields(
db: &dyn HirDatabase,
infer: &InferenceResult,
id: PatId,
pat: &Pat,
) -> Option<(VariantId, Vec<LocalFieldId>)> {
let (fields, _ellipsis) = match pat {
Pat::Record { path: _, args, ellipsis } => (args, *ellipsis),
_ => return None,
};
let variant_def = infer.variant_resolution_for_pat(id)?;
if let VariantId::UnionId(_) = variant_def {
return None;
}
let variant_data = variant_def.fields(db);
let specified_fields: FxHashSet<_> = fields.iter().map(|f| &f.name).collect();
// suggest fields if:
// - not in code
let matched_fields: Vec<LocalFieldId> = variant_data
.fields()
.iter()
.filter_map(|(f, d)| if !specified_fields.contains(&d.name) { Some(f) } else { None })
.collect();
if matched_fields.is_empty() {
return None;
}
Some((variant_def, matched_fields))
}

View file

@ -33,8 +33,8 @@ fn expand_record_rest_pattern(
record_pat: ast::RecordPat,
rest_pat: ast::RestPat,
) -> Option<()> {
let missing_fields = ctx.sema.record_pattern_missing_fields(&record_pat);
if missing_fields.is_empty() {
let matched_fields = ctx.sema.record_pattern_matched_fields(&record_pat);
if matched_fields.is_empty() {
cov_mark::hit!(no_missing_fields);
return None;
}
@ -53,7 +53,7 @@ fn expand_record_rest_pattern(
|builder| {
let make = SyntaxFactory::with_mappings();
let mut editor = builder.make_editor(rest_pat.syntax());
let new_fields = old_field_list.fields().chain(missing_fields.iter().map(|(f, _)| {
let new_fields = old_field_list.fields().chain(matched_fields.iter().map(|(f, _)| {
make.record_pat_field_shorthand(
make.ident_pat(
false,

View file

@ -340,7 +340,7 @@ pub(crate) fn complete_expr_path(
let missing_fields =
ctx.sema.record_literal_missing_fields(record_expr);
if !missing_fields.is_empty() {
add_default_update(acc, ctx, ty);
add_default_update(acc, ctx, ty.as_ref());
}
}
};

View file

@ -36,7 +36,7 @@ pub(crate) fn complete_record_pattern_fields(
true => return,
}
}
_ => ctx.sema.record_pattern_missing_fields(record_pat),
_ => ctx.sema.record_pattern_matched_fields(record_pat),
};
complete_fields(acc, ctx, missing_fields);
}
@ -69,14 +69,14 @@ pub(crate) fn complete_record_expr_fields(
}
}
_ => {
let missing_fields = ctx.sema.record_literal_missing_fields(record_expr);
let suggest_fields = ctx.sema.record_literal_matched_fields(record_expr);
let update_exists = record_expr
.record_expr_field_list()
.is_some_and(|list| list.dotdot_token().is_some());
if !missing_fields.is_empty() && !update_exists {
if !suggest_fields.is_empty() && !update_exists {
cov_mark::hit!(functional_update_field);
add_default_update(acc, ctx, ty);
add_default_update(acc, ctx, ty.as_ref());
}
if dot_prefix {
cov_mark::hit!(functional_update_one_dot);
@ -90,7 +90,7 @@ pub(crate) fn complete_record_expr_fields(
item.add_to(acc, ctx.db);
return;
}
missing_fields
suggest_fields
}
};
complete_fields(acc, ctx, missing_fields);
@ -99,11 +99,11 @@ pub(crate) fn complete_record_expr_fields(
pub(crate) fn add_default_update(
acc: &mut Completions,
ctx: &CompletionContext<'_>,
ty: Option<hir::TypeInfo<'_>>,
ty: Option<&hir::TypeInfo<'_>>,
) {
let default_trait = ctx.famous_defs().core_default_Default();
let impls_default_trait = default_trait
.zip(ty.as_ref())
.zip(ty)
.is_some_and(|(default_trait, ty)| ty.original.impls_trait(ctx.db, default_trait, &[]));
if impls_default_trait {
// FIXME: This should make use of scope_def like completions so we get all the other goodies

View file

@ -286,6 +286,24 @@ fn main() {
);
}
#[test]
fn functional_update_fields_completion() {
// Complete fields before functional update `..`
check(
r#"
struct Point { x: i32 = 0, y: i32 = 0 }
fn main() {
let p = Point { $0, .. };
}
"#,
expect![[r#"
fd x i32
fd y i32
"#]],
);
}
#[test]
fn empty_union_literal() {
check(
@ -302,7 +320,27 @@ fn foo() {
fd bar f32
fd foo u32
"#]],
)
);
}
#[test]
fn record_pattern_field_with_rest_pat() {
// When .. is present, complete all unspecified fields (even those with default values)
check(
r#"
struct UserInfo { id: i32, age: f32, email: u64 }
fn foo(u1: UserInfo) {
let UserInfo { id, $0, .. } = u1;
}
"#,
expect![[r#"
fd age f32
fd email u64
kw mut
kw ref
"#]],
);
}
#[test]

View file

@ -857,4 +857,81 @@ pub struct Claims {
"#,
);
}
#[test]
fn test_default_field_values_basic() {
// This should work without errors - only field 'b' is required
check_diagnostics(
r#"
#![feature(default_field_values)]
struct Struct {
a: usize = 0,
b: usize,
}
fn main() {
Struct { b: 1, .. };
}
"#,
);
}
#[test]
fn test_default_field_values_missing_field_error() {
// This should report a missing field error because email is required
check_diagnostics(
r#"
#![feature(default_field_values)]
struct UserInfo {
id: i32,
age: f32 = 1.0,
email: String,
}
fn main() {
UserInfo { id: 20, .. };
// ^^^^^^^^💡 error: missing structure fields:
// |- email
}
"#,
);
}
#[test]
fn test_default_field_values_requires_spread_syntax() {
// without `..` should report missing fields
check_diagnostics(
r#"
#![feature(default_field_values)]
struct Point {
x: i32 = 0,
y: i32 = 0,
}
fn main() {
Point { x: 0 };
// ^^^^^💡 error: missing structure fields:
// |- y
}
"#,
);
}
#[test]
fn test_default_field_values_pattern_matching() {
check_diagnostics(
r#"
#![feature(default_field_values)]
struct Point {
x: i32 = 0,
y: i32 = 0,
z: i32,
}
fn main() {
let Point { x, .. } = Point { z: 5, .. };
}
"#,
);
}
}

View file

@ -272,9 +272,9 @@ pub(super) fn struct_rest_pat(
edition: Edition,
display_target: DisplayTarget,
) -> HoverResult {
let missing_fields = sema.record_pattern_missing_fields(pattern);
let matched_fields = sema.record_pattern_matched_fields(pattern);
// if there are no missing fields, the end result is a hover that shows ".."
// if there are no matched fields, the end result is a hover that shows ".."
// should be left in to indicate that there are no more fields in the pattern
// example, S {a: 1, b: 2, ..} when struct S {a: u32, b: u32}
@ -285,13 +285,13 @@ pub(super) fn struct_rest_pat(
targets.push(item);
}
};
for (_, t) in &missing_fields {
for (_, t) in &matched_fields {
walk_and_push_ty(sema.db, t, &mut push_new_def);
}
res.markup = {
let mut s = String::from(".., ");
for (f, _) in &missing_fields {
for (f, _) in &matched_fields {
s += f.display(sema.db, display_target).to_string().as_ref();
s += ", ";
}