use std::collections::HashSet;
use crate::diagnostic::Diagnostic;
use crate::ir::{FieldDef, PhenotypeModule, PhenotypeType, RenderNode, SeparatorExpr, ValueType};
use crate::symbol::{Cardinality, FieldId, Requiredness, TypeId};
pub fn validate_module(module: &PhenotypeModule, file: &str) -> Vec<Diagnostic> {
let mut diags = Vec::new();
for pt in &module.types {
validate_type(pt, module, file, &mut diags);
}
validate_cyclic_types(module, file, &mut diags);
diags
}
fn validate_type(
pt: &PhenotypeType,
module: &PhenotypeModule,
file: &str,
diags: &mut Vec<Diagnostic>,
) {
if pt.render.is_empty() {
diags.push(super::error(
file,
format!("type `{}` has no render expressions", pt.singular_name),
));
}
for field in &pt.fields {
if let ValueType::Union(members) = &field.ty {
if members.iter().any(|m| matches!(m, ValueType::Imported(_))) {
diags.push(super::error(
file,
format!(
"field `{}` in type `{}`: imported types are not yet supported \
inside unions",
field.name, pt.singular_name
),
));
}
}
}
for node in &pt.render {
validate_render_node(node, pt, module, file, diags, false);
}
}
#[allow(clippy::only_used_in_recursion)]
fn validate_render_node(
node: &RenderNode,
pt: &PhenotypeType,
module: &PhenotypeModule,
file: &str,
diags: &mut Vec<Diagnostic>,
inside_guard: bool,
) {
match node {
RenderNode::ParentFieldRef { .. } => {}
RenderNode::Emit(field_id) => {
if let Some(field) = find_field(pt, *field_id) {
if is_collection_type(&field.ty, &field.cardinality) {
diags.push(super::error_with_suggestion(
file,
format!(
"cannot directly emit collection field `{}` in type `{}`",
field.name, pt.singular_name
),
"collection fields must be rendered with @join".to_string(),
format!("use @join({}, separator) instead", field.name),
));
}
if field.requiredness == Requiredness::Optional && !inside_guard {
diags.push(super::error_with_suggestion(
file,
format!(
"cannot directly emit optional field `{}` in type `{}`",
field.name, pt.singular_name
),
"optional fields must be guarded by @ifset".to_string(),
format!("use @ifset({}) {{ @({}) }} instead", field.name, field.name),
));
}
}
}
RenderNode::Join { field, separator } => {
if let Some(sep_field) = match separator {
SeparatorExpr::Field(fid) => find_field(pt, *fid),
SeparatorExpr::Literal(_) => None,
} {
validate_separator_field(sep_field, pt, file, diags);
}
if let Some(field_def) = find_field(pt, *field) {
if !is_collection_type(&field_def.ty, &field_def.cardinality) {
diags.push(super::error(
file,
format!(
"@join field `{}` in type `{}` is not a collection",
field_def.name, pt.singular_name
),
));
}
}
}
RenderNode::Eol { field } => {
if let Some(fid) = field {
if let Some(field_def) = find_field(pt, *fid) {
if !is_scalar_string(&field_def.ty) {
diags.push(super::error(
file,
format!(
"@eol field `{}` in type `{}` must be a string type",
field_def.name, pt.singular_name
),
));
}
}
}
}
RenderNode::IfSet { field, body } => {
if let Some(field_def) = find_field(pt, *field) {
if field_def.requiredness != Requiredness::Optional {
diags.push(super::error_with_explanation(
file,
format!(
"@ifset on non-optional field `{}` in type `{}`",
field_def.name, pt.singular_name
),
"@ifset is only meaningful for optional fields".to_string(),
));
}
}
if body.is_empty() {
diags.push(super::error(
file,
format!(
"@ifset block for `{}` in type `{}` has empty body",
field_id_name(pt, *field),
pt.singular_name
),
));
}
for child in body {
validate_render_node(child, pt, module, file, diags, true);
}
}
RenderNode::IfNotEmpty { field, body } => {
if let Some(field_def) = find_field(pt, *field) {
if !is_collection_type(&field_def.ty, &field_def.cardinality) {
diags.push(super::error_with_explanation(
file,
format!(
"@ifnotempty on non-collection field `{}` in type `{}`",
field_def.name, pt.singular_name
),
"@ifnotempty is only meaningful for collection fields (plural types or cardinalized fields)"
.to_string(),
));
}
}
if body.is_empty() {
diags.push(super::error(
file,
format!(
"@ifnotempty block for `{}` in type `{}` has empty body",
field_id_name(pt, *field),
pt.singular_name
),
));
}
for child in body {
validate_render_node(child, pt, module, file, diags, true);
}
}
RenderNode::Text(_) => {
}
}
}
fn validate_separator_field(
field: &FieldDef,
pt: &PhenotypeType,
file: &str,
diags: &mut Vec<Diagnostic>,
) {
if !is_scalar_string(&field.ty) {
diags.push(super::error(
file,
format!(
"@join separator field `{}` in type `{}` must be a string type",
field.name, pt.singular_name
),
));
}
if field.cardinality != Cardinality::One {
diags.push(super::error(
file,
format!(
"@join separator field `{}` in type `{}` must be singular (not a collection)",
field.name, pt.singular_name
),
));
}
}
fn validate_cyclic_types(module: &PhenotypeModule, file: &str, diags: &mut Vec<Diagnostic>) {
for pt in &module.types {
let mut visited = HashSet::new();
if has_cycle(pt.id, module, &mut visited) {
diags.push(super::error_with_explanation(
file,
format!("cyclic type definition involving `{}`", pt.singular_name),
"recursive type definitions are not supported in v1".to_string(),
));
}
}
}
fn has_cycle(type_id: TypeId, module: &PhenotypeModule, visited: &mut HashSet<TypeId>) -> bool {
if !visited.insert(type_id) {
return true;
}
if let Some(pt) = module.types.iter().find(|t| t.id == type_id) {
for field in &pt.fields {
if field.requiredness == Requiredness::Required && field.cardinality == Cardinality::One
{
if let Some(ref_id) = referenced_type_id(&field.ty) {
if has_cycle(ref_id, module, visited) {
return true;
}
}
}
}
}
visited.remove(&type_id);
false
}
fn find_field(pt: &PhenotypeType, id: FieldId) -> Option<&FieldDef> {
pt.fields.iter().find(|f| f.id == id)
}
fn field_id_name(pt: &PhenotypeType, id: FieldId) -> String {
find_field(pt, id)
.map(|f| f.name.clone())
.unwrap_or_else(|| "<unknown>".to_string())
}
fn is_scalar_string(ty: &ValueType) -> bool {
matches!(
ty,
ValueType::Primitive(crate::symbol::PrimitiveType::String)
)
}
fn is_collection_type(ty: &ValueType, cardinality: &Cardinality) -> bool {
if *cardinality != Cardinality::One {
return true;
}
matches!(
ty,
ValueType::UserPlural { .. }
| ValueType::Imported(crate::ir::ImportedRef {
kind: crate::symbol::ImportedKind::Plural,
..
})
)
}
fn referenced_type_id(ty: &ValueType) -> Option<TypeId> {
match ty {
ValueType::UserSingular(id) => Some(*id),
ValueType::UserPlural { collection_of } => Some(*collection_of),
_ => None,
}
}