yeast: type-check for missing required fields

Add FieldCardinality to Schema to track required/multiple per field,
populated from the ast_types.yml suffixes (bare = required single,
? = optional single, + = required multiple, * = optional multiple).

dump_ast_with_type_errors now emits:
  <-- ERROR: missing required field 'name'
for any node in the output AST whose declared schema requires a field
that is absent from the actual node.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
Asger F
2026-05-29 17:23:02 +02:00
parent 21f216af8c
commit 3f3bed62d3
3 changed files with 65 additions and 0 deletions

View File

@@ -273,6 +273,16 @@ fn dump_node(
}
}
// Check for required fields that are absent
if let Some((schema, _, _)) = type_check {
for (field_id, field_name) in schema.required_fields_for_kind(node.kind_name()) {
if !node.fields.contains_key(&field_id) {
let name = field_name.unwrap_or("child");
writeln!(out, "{prefix} <-- ERROR: missing required field '{name}'").unwrap();
}
}
}
// Unnamed children — skip unnamed tokens (keywords, punctuation)
if let Some(children) = node.fields.get(&CHILD_FIELD) {
let child_type_check = type_check.map(|(schema, _, _)| {

View File

@@ -314,6 +314,14 @@ fn apply_yaml_to_schema(
node_types.sort_by(|a, b| a.kind.cmp(&b.kind).then(a.named.cmp(&b.named)));
node_types.dedup_by(|a, b| a.kind == b.kind && a.named == b.named);
schema.set_field_types(parent_kind, field_id, node_types);
schema.set_field_cardinality(
parent_kind,
field_id,
crate::schema::FieldCardinality {
multiple: spec.multiple,
required: spec.required,
},
);
}
}
}

View File

@@ -8,6 +8,15 @@ pub struct NodeType {
pub named: bool,
}
/// Multiplicity/optionality of a field declaration.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct FieldCardinality {
/// Whether the field may hold more than one child.
pub multiple: bool,
/// Whether at least one child must be present.
pub required: bool,
}
/// A schema defining node kinds and field names for the output AST.
/// Built from a node-types.yml file, independent of any tree-sitter grammar.
///
@@ -32,6 +41,7 @@ pub struct Schema {
kind_names: BTreeMap<KindId, &'static str>,
next_kind_id: KindId,
field_types: BTreeMap<(String, FieldId), Vec<NodeType>>,
field_cardinalities: BTreeMap<(String, FieldId), FieldCardinality>,
supertypes: BTreeMap<String, Vec<NodeType>>,
}
@@ -52,6 +62,7 @@ impl Schema {
kind_names: BTreeMap::new(),
next_kind_id: 1, // 0 is reserved
field_types: BTreeMap::new(),
field_cardinalities: BTreeMap::new(),
supertypes: BTreeMap::new(),
}
}
@@ -196,6 +207,42 @@ impl Schema {
.get(&(parent_kind.to_string(), field_id))
}
pub fn set_field_cardinality(
&mut self,
parent_kind: &str,
field_id: FieldId,
cardinality: FieldCardinality,
) {
self.field_cardinalities
.insert((parent_kind.to_string(), field_id), cardinality);
}
/// Returns the declared cardinality for a field, if known.
pub fn field_cardinality(
&self,
parent_kind: &str,
field_id: FieldId,
) -> Option<FieldCardinality> {
self.field_cardinalities
.get(&(parent_kind.to_string(), field_id))
.copied()
}
/// Returns an iterator over all `(field_id, field_name)` pairs that are
/// declared as required (`required: true`) for the given `parent_kind`.
pub fn required_fields_for_kind<'a>(
&'a self,
parent_kind: &'a str,
) -> impl Iterator<Item = (FieldId, Option<&'static str>)> + 'a {
self.field_cardinalities
.iter()
.filter(move |((kind, _), card)| kind == parent_kind && card.required)
.map(move |((_, field_id), _)| {
let name = self.field_name_for_id(*field_id);
(*field_id, name)
})
}
pub fn set_supertype_members(&mut self, supertype: &str, node_types: Vec<NodeType>) {
self.supertypes.insert(supertype.to_string(), node_types);
}