1203 lines
42 KiB
Rust
1203 lines
42 KiB
Rust
//! Bounded data dictionary traversal and validation, shared by every editor.
|
|
use crate::validation::{Issue, error, valid_text, warning};
|
|
use crate::*;
|
|
use std::collections::{BTreeMap, BTreeSet};
|
|
|
|
/// A contextual field identity is `(path, field)`; item identifies its origin.
|
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
|
pub struct CarriedFieldPath {
|
|
pub item: Id,
|
|
pub path: Vec<Id>,
|
|
pub field: Id,
|
|
pub direction: ParameterDirection,
|
|
}
|
|
|
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
|
pub enum LegacyFieldPathResolution {
|
|
Missing,
|
|
Unique(Vec<Id>),
|
|
Ambiguous,
|
|
}
|
|
|
|
pub fn supertype(e: &Element) -> Option<Id> {
|
|
match e.shared()? {
|
|
SharedBody::ExchangeItem { supertype, .. }
|
|
| SharedBody::Interface { supertype }
|
|
| SharedBody::DataType {
|
|
definition: DataTypeDefinition::Record { supertype },
|
|
..
|
|
} => *supertype,
|
|
_ => None,
|
|
}
|
|
}
|
|
pub fn is_record(p: &Project, id: Id) -> bool {
|
|
matches!(
|
|
p.elements.get(&id).and_then(Element::shared),
|
|
Some(SharedBody::DataType {
|
|
definition: DataTypeDefinition::Record { .. },
|
|
..
|
|
})
|
|
)
|
|
}
|
|
fn ancestry(p: &Project, id: Id) -> Vec<Id> {
|
|
let mut current = Some(id);
|
|
let mut seen = BTreeSet::new();
|
|
let mut result = Vec::new();
|
|
while let Some(id) = current {
|
|
if !seen.insert(id) {
|
|
break;
|
|
}
|
|
result.push(id);
|
|
current = p.elements.get(&id).and_then(supertype);
|
|
}
|
|
result
|
|
}
|
|
/// Base fields precede local fields. Original field IDs and owners are retained.
|
|
/// Scan the model once, so a deep empty inheritance chain does not rescan every
|
|
/// element for every ancestor during validation.
|
|
pub fn effective_fields(p: &Project, owner: Id) -> Vec<&Element> {
|
|
let ranks: BTreeMap<_, _> = ancestry(p, owner)
|
|
.into_iter()
|
|
.rev()
|
|
.enumerate()
|
|
.map(|(rank, id)| (id, rank))
|
|
.collect();
|
|
let mut fields: Vec<_> = p
|
|
.elements
|
|
.values()
|
|
.filter(|e| {
|
|
e.kind() == Kind::DataField && e.parent.is_some_and(|id| ranks.contains_key(&id))
|
|
})
|
|
.collect();
|
|
fields.sort_by_key(|e| (ranks[&e.parent.expect("field owner was filtered")], e.id));
|
|
fields
|
|
}
|
|
pub fn effective_interface_allocations(p: &Project, interface: Id) -> Vec<&Relation> {
|
|
let ranks: BTreeMap<_, _> = ancestry(p, interface)
|
|
.into_iter()
|
|
.rev()
|
|
.enumerate()
|
|
.map(|(rank, id)| (id, rank))
|
|
.collect();
|
|
let mut allocations: Vec<_> = p
|
|
.relations
|
|
.values()
|
|
.filter(|r| {
|
|
r.kind() == RelationKind::InterfaceItemAllocation && ranks.contains_key(&r.source())
|
|
})
|
|
.collect();
|
|
allocations.sort_by_key(|r| {
|
|
let RelationBody::InterfaceItemAllocation { order, .. } = &r.body else {
|
|
unreachable!()
|
|
};
|
|
(ranks[&r.source()], *order, &r.alias, r.id)
|
|
});
|
|
allocations
|
|
}
|
|
pub fn interface_items(p: &Project, interface: Id) -> Vec<Id> {
|
|
effective_interface_allocations(p, interface)
|
|
.into_iter()
|
|
.map(Relation::target)
|
|
.collect()
|
|
}
|
|
/// An association role and multiplicity belong to the named endpoint.
|
|
pub fn association_end_label(relation: &Relation, source_end: bool) -> Option<String> {
|
|
let RelationBody::DataAssociation {
|
|
source_role,
|
|
target_role,
|
|
source_minimum,
|
|
source_maximum,
|
|
target_minimum,
|
|
target_maximum,
|
|
..
|
|
} = &relation.body
|
|
else {
|
|
return None;
|
|
};
|
|
let (role, minimum, maximum) = if source_end {
|
|
(source_role, source_minimum, source_maximum)
|
|
} else {
|
|
(target_role, target_minimum, target_maximum)
|
|
};
|
|
Some(format!(
|
|
"{} [{}..{}]",
|
|
role,
|
|
minimum,
|
|
maximum.map_or_else(|| "*".into(), |n| n.to_string())
|
|
))
|
|
}
|
|
pub fn item_satisfies(p: &Project, carried: Id, declared: Id) -> bool {
|
|
p.elements
|
|
.get(&carried)
|
|
.is_some_and(|e| e.kind() == Kind::ExchangeItem)
|
|
&& p.elements
|
|
.get(&declared)
|
|
.is_some_and(|e| e.kind() == Kind::ExchangeItem)
|
|
&& ancestry(p, carried).contains(&declared)
|
|
}
|
|
pub fn interface_satisfies(p: &Project, provided: Id, required: Id) -> bool {
|
|
p.elements
|
|
.get(&provided)
|
|
.is_some_and(|e| e.kind() == Kind::Interface)
|
|
&& p.elements
|
|
.get(&required)
|
|
.is_some_and(|e| e.kind() == Kind::Interface)
|
|
&& ancestry(p, provided).contains(&required)
|
|
}
|
|
pub fn item_mechanism(p: &Project, item: Id) -> Option<ExchangeMechanism> {
|
|
match p.elements.get(&item)?.shared()? {
|
|
SharedBody::ExchangeItem { mechanism, .. } => Some(*mechanism),
|
|
_ => None,
|
|
}
|
|
}
|
|
|
|
const MAX_FIELD_PATHS: usize = 16_384;
|
|
pub const MAX_VALUE_PATH_DEPTH: usize = 128;
|
|
|
|
/// Enumerate finite representative paths, stopping recursive record descent.
|
|
/// Work exhaustion is separate from a valid recursive type and is diagnosed.
|
|
pub fn try_carried_field_paths(
|
|
p: &Project,
|
|
relation: &Relation,
|
|
) -> Result<Vec<CarriedFieldPath>, String> {
|
|
let mut result = Vec::new();
|
|
let mut identities = BTreeSet::new();
|
|
let mut pending: Vec<_> = relation
|
|
.body
|
|
.items()
|
|
.iter()
|
|
.rev()
|
|
.map(|id| {
|
|
(
|
|
*id,
|
|
*id,
|
|
Vec::<Id>::new(),
|
|
ParameterDirection::Unspecified,
|
|
BTreeSet::<Id>::new(),
|
|
)
|
|
})
|
|
.collect();
|
|
let mut work = 0usize;
|
|
while let Some((item, owner, path, inherited_direction, mut ancestors)) = pending.pop() {
|
|
if !ancestors.insert(owner) {
|
|
continue;
|
|
}
|
|
if path.len() > MAX_VALUE_PATH_DEPTH {
|
|
return Err(format!(
|
|
"Carried field paths exceed the {MAX_VALUE_PATH_DEPTH}-hop authoring limit."
|
|
));
|
|
}
|
|
for field in effective_fields(p, owner) {
|
|
work += 1;
|
|
if work > MAX_FIELD_PATHS {
|
|
return Err(format!(
|
|
"Carried field expansion exceeds the {MAX_FIELD_PATHS}-field authoring limit."
|
|
));
|
|
}
|
|
let Some(SharedBody::DataField {
|
|
direction,
|
|
storage,
|
|
type_ref,
|
|
..
|
|
}) = field.shared()
|
|
else {
|
|
continue;
|
|
};
|
|
let direction = if path.is_empty() {
|
|
*direction
|
|
} else {
|
|
inherited_direction
|
|
};
|
|
if identities.insert((path.clone(), field.id)) {
|
|
result.push(CarriedFieldPath {
|
|
item,
|
|
path: path.clone(),
|
|
field: field.id,
|
|
direction,
|
|
});
|
|
}
|
|
if *storage == FieldStorage::Value
|
|
&& let TypeRef::Named(id) = type_ref
|
|
&& is_record(p, *id)
|
|
{
|
|
let mut nested = path.clone();
|
|
nested.push(field.id);
|
|
pending.push((item, *id, nested, direction, ancestors.clone()));
|
|
}
|
|
}
|
|
}
|
|
Ok(result)
|
|
}
|
|
/// UI convenience; validation always uses the checked traversal above.
|
|
pub fn carried_field_paths(p: &Project, relation: &Relation) -> Vec<CarriedFieldPath> {
|
|
try_carried_field_paths(p, relation).unwrap_or_default()
|
|
}
|
|
/// Unlike the finite picker, legacy inference accounts for paths that revisit
|
|
/// a recursive Value record. A reachable cycle before this field is ambiguous.
|
|
pub fn resolve_legacy_field_path(
|
|
p: &Project,
|
|
relation: &Relation,
|
|
field: Id,
|
|
) -> Result<LegacyFieldPathResolution, String> {
|
|
let paths: Vec<_> = try_carried_field_paths(p, relation)?
|
|
.into_iter()
|
|
.filter(|value| value.field == field)
|
|
.collect();
|
|
if paths.is_empty() {
|
|
return Ok(LegacyFieldPathResolution::Missing);
|
|
}
|
|
if paths.len() > 1 {
|
|
return Ok(LegacyFieldPathResolution::Ambiguous);
|
|
}
|
|
let mut graph: BTreeMap<Id, Vec<Id>> = BTreeMap::new();
|
|
let mut reverse: BTreeMap<Id, Vec<Id>> = BTreeMap::new();
|
|
let mut targets = Vec::new();
|
|
let mut pending = relation.body.items().to_vec();
|
|
while let Some(owner) = pending.pop() {
|
|
if graph.contains_key(&owner) {
|
|
continue;
|
|
}
|
|
let mut edges = Vec::new();
|
|
for e in effective_fields(p, owner) {
|
|
if e.id == field {
|
|
targets.push(owner);
|
|
}
|
|
if let Some(SharedBody::DataField {
|
|
storage: FieldStorage::Value,
|
|
type_ref: TypeRef::Named(target),
|
|
..
|
|
}) = e.shared()
|
|
&& is_record(p, *target)
|
|
{
|
|
edges.push(*target);
|
|
reverse.entry(*target).or_default().push(owner);
|
|
pending.push(*target);
|
|
}
|
|
}
|
|
graph.insert(owner, edges);
|
|
}
|
|
let mut relevant = BTreeSet::new();
|
|
while let Some(owner) = targets.pop() {
|
|
if relevant.insert(owner) {
|
|
targets.extend(reverse.get(&owner).into_iter().flatten());
|
|
}
|
|
}
|
|
let mut remaining: BTreeMap<_, _> = graph
|
|
.iter()
|
|
.filter(|(owner, _)| relevant.contains(owner))
|
|
.map(|(owner, edges)| {
|
|
(
|
|
*owner,
|
|
edges
|
|
.iter()
|
|
.filter(|target| relevant.contains(target))
|
|
.count(),
|
|
)
|
|
})
|
|
.collect();
|
|
let mut pending: Vec<_> = remaining
|
|
.iter()
|
|
.filter_map(|(owner, count)| (*count == 0).then_some(*owner))
|
|
.collect();
|
|
while let Some(owner) = pending.pop() {
|
|
remaining.remove(&owner);
|
|
for source in reverse.get(&owner).into_iter().flatten() {
|
|
if let Some(count) = remaining.get_mut(source) {
|
|
*count -= 1;
|
|
if *count == 0 {
|
|
pending.push(*source);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
Ok(if remaining.is_empty() {
|
|
LegacyFieldPathResolution::Unique(paths[0].path.clone())
|
|
} else {
|
|
LegacyFieldPathResolution::Ambiguous
|
|
})
|
|
}
|
|
/// Resolve an explicit finite path, including a finite path into a recursive type.
|
|
pub fn carried_value_path(
|
|
p: &Project,
|
|
relation: &Relation,
|
|
path: &[Id],
|
|
field: Id,
|
|
) -> Option<CarriedFieldPath> {
|
|
if path.len() > MAX_VALUE_PATH_DEPTH {
|
|
return None;
|
|
}
|
|
for item in relation.body.items() {
|
|
let mut owner = *item;
|
|
let mut direction = ParameterDirection::Unspecified;
|
|
let mut valid = true;
|
|
for (index, hop) in path.iter().chain(std::iter::once(&field)).enumerate() {
|
|
let Some(e) = effective_fields(p, owner)
|
|
.into_iter()
|
|
.find(|e| e.id == *hop)
|
|
else {
|
|
valid = false;
|
|
break;
|
|
};
|
|
let Some(SharedBody::DataField {
|
|
direction: parameter_direction,
|
|
storage,
|
|
type_ref,
|
|
..
|
|
}) = e.shared()
|
|
else {
|
|
valid = false;
|
|
break;
|
|
};
|
|
if index == 0 {
|
|
direction = *parameter_direction;
|
|
}
|
|
if index < path.len() {
|
|
match (storage, type_ref) {
|
|
(FieldStorage::Value, TypeRef::Named(id)) if is_record(p, *id) => owner = *id,
|
|
_ => {
|
|
valid = false;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
if valid {
|
|
return Some(CarriedFieldPath {
|
|
item: *item,
|
|
path: path.to_vec(),
|
|
field,
|
|
direction,
|
|
});
|
|
}
|
|
}
|
|
None
|
|
}
|
|
|
|
/// Existing annotations whose inherited field disappears follow the ordinary
|
|
/// deletion pruning policy. Explicitly edited/new annotations still validate.
|
|
pub(crate) fn prune_removed_inheritance(
|
|
before: &Project,
|
|
after: &mut Project,
|
|
origins: &BTreeMap<Id, Id>,
|
|
) {
|
|
let current: BTreeMap<_, _> = origins.iter().map(|(now, old)| (*old, *now)).collect();
|
|
let removed: BTreeSet<_> = before
|
|
.elements
|
|
.values()
|
|
.filter_map(|e| {
|
|
let base = supertype(e)?;
|
|
let now = after.elements.get(current.get(&e.id)?)?;
|
|
(supertype(now) != current.get(&base).copied()).then_some(e.id)
|
|
})
|
|
.collect();
|
|
if removed.is_empty() {
|
|
return;
|
|
}
|
|
let mut pruned = BTreeMap::new();
|
|
for e in after.elements.values() {
|
|
let Some(SharedBody::Occurrence {
|
|
body:
|
|
OccurrenceBody::Exchange {
|
|
exchange: Some(exchange),
|
|
values,
|
|
..
|
|
},
|
|
..
|
|
}) = e.shared()
|
|
else {
|
|
continue;
|
|
};
|
|
let Some(old) = origins.get(&e.id).and_then(|id| before.elements.get(id)) else {
|
|
continue;
|
|
};
|
|
let Some(SharedBody::Occurrence {
|
|
body:
|
|
OccurrenceBody::Exchange {
|
|
exchange: Some(old_exchange),
|
|
values: old_values,
|
|
..
|
|
},
|
|
..
|
|
}) = old.shared()
|
|
else {
|
|
continue;
|
|
};
|
|
if origins.get(exchange) != Some(old_exchange) {
|
|
continue;
|
|
}
|
|
let (Some(relation), Some(old_relation)) = (
|
|
after.relations.get(exchange),
|
|
before.relations.get(old_exchange),
|
|
) else {
|
|
continue;
|
|
};
|
|
let mut keep = Vec::new();
|
|
for value in values {
|
|
if carried_value_path(after, relation, &value.path, value.field).is_some()
|
|
|| (value.path.is_empty()
|
|
&& matches!(
|
|
resolve_legacy_field_path(after, relation, value.field),
|
|
Ok(LegacyFieldPathResolution::Unique(_)
|
|
| LegacyFieldPathResolution::Ambiguous)
|
|
))
|
|
{
|
|
keep.push(value.clone());
|
|
continue;
|
|
}
|
|
let old_value = MessageValue {
|
|
path: value
|
|
.path
|
|
.iter()
|
|
.map(|id| origins.get(id).copied().unwrap_or(*id))
|
|
.collect(),
|
|
field: origins.get(&value.field).copied().unwrap_or(value.field),
|
|
expression: value.expression.clone(),
|
|
};
|
|
let prior_paths = if old_value.path.is_empty()
|
|
&& carried_value_path(before, old_relation, &old_value.path, old_value.field)
|
|
.is_none()
|
|
{
|
|
carried_field_paths(before, old_relation)
|
|
.into_iter()
|
|
.filter(|path| path.field == old_value.field)
|
|
.map(|path| MessageValue {
|
|
path: path.path,
|
|
..old_value.clone()
|
|
})
|
|
.collect::<Vec<_>>()
|
|
} else {
|
|
vec![old_value.clone()]
|
|
};
|
|
let lost = old_values.contains(&old_value)
|
|
&& prior_paths.iter().any(|old_value| {
|
|
old_relation.body.items().iter().any(|item| {
|
|
if carried_value_path(
|
|
before,
|
|
old_relation,
|
|
&old_value.path,
|
|
old_value.field,
|
|
)
|
|
.is_none()
|
|
{
|
|
return false;
|
|
}
|
|
let mut owner = *item;
|
|
for hop in old_value
|
|
.path
|
|
.iter()
|
|
.chain(std::iter::once(&old_value.field))
|
|
{
|
|
let Some(field) = effective_fields(before, owner)
|
|
.into_iter()
|
|
.find(|e| e.id == *hop)
|
|
else {
|
|
return false;
|
|
};
|
|
let mut inherited_owner = Some(owner);
|
|
let mut seen = BTreeSet::new();
|
|
while let Some(id) = inherited_owner {
|
|
if Some(id) == field.parent || !seen.insert(id) {
|
|
break;
|
|
}
|
|
if removed.contains(&id) {
|
|
return true;
|
|
}
|
|
inherited_owner = before.elements.get(&id).and_then(supertype);
|
|
}
|
|
if let Some(SharedBody::DataField {
|
|
type_ref: TypeRef::Named(id),
|
|
..
|
|
}) = field.shared()
|
|
{
|
|
owner = *id;
|
|
}
|
|
}
|
|
false
|
|
})
|
|
});
|
|
if !lost {
|
|
keep.push(value.clone());
|
|
}
|
|
}
|
|
if keep != *values {
|
|
pruned.insert(e.id, keep);
|
|
}
|
|
}
|
|
for (id, keep) in pruned {
|
|
if let Some(SharedBody::Occurrence {
|
|
body: OccurrenceBody::Exchange { values, .. },
|
|
..
|
|
}) = after.elements.get_mut(&id).and_then(Element::shared_mut)
|
|
{
|
|
*values = keep;
|
|
}
|
|
}
|
|
}
|
|
|
|
pub fn validate_data_value(
|
|
p: &Project,
|
|
type_ref: &TypeRef,
|
|
value: &DataValue,
|
|
) -> Result<(), String> {
|
|
let primitive = match type_ref {
|
|
TypeRef::Boolean => DataTypeDefinition::Boolean,
|
|
TypeRef::Integer => DataTypeDefinition::Integer {
|
|
minimum: None,
|
|
maximum: None,
|
|
},
|
|
TypeRef::Real => DataTypeDefinition::Real {
|
|
minimum: None,
|
|
maximum: None,
|
|
},
|
|
TypeRef::String => DataTypeDefinition::String {
|
|
minimum_length: 0,
|
|
maximum_length: None,
|
|
},
|
|
TypeRef::Named(id) => {
|
|
return match p.elements.get(id).and_then(Element::shared) {
|
|
Some(SharedBody::DataType { definition, .. }) => validate_scalar(definition, value),
|
|
_ => Err("The named scalar type does not exist.".into()),
|
|
};
|
|
}
|
|
};
|
|
validate_scalar(&primitive, value)
|
|
}
|
|
fn validate_scalar(definition: &DataTypeDefinition, value: &DataValue) -> Result<(), String> {
|
|
let valid =
|
|
match (definition, value) {
|
|
(DataTypeDefinition::Boolean, DataValue::Boolean(_)) => true,
|
|
(DataTypeDefinition::Integer { minimum, maximum }, DataValue::Integer(n)) => {
|
|
minimum.is_none_or(|min| *n >= min) && maximum.is_none_or(|max| *n <= max)
|
|
}
|
|
(DataTypeDefinition::Real { minimum, maximum }, DataValue::Real(n)) => {
|
|
n.is_finite()
|
|
&& minimum.is_none_or(|min| *n >= min)
|
|
&& maximum.is_none_or(|max| *n <= max)
|
|
}
|
|
(
|
|
DataTypeDefinition::String {
|
|
minimum_length,
|
|
maximum_length,
|
|
},
|
|
DataValue::String(s),
|
|
) => {
|
|
valid_text(s)
|
|
&& s.chars().count() >= *minimum_length as usize
|
|
&& maximum_length.is_none_or(|max| s.chars().count() <= max as usize)
|
|
}
|
|
(DataTypeDefinition::Enumeration { literals }, DataValue::Enumeration(s)) => {
|
|
literals.contains(s)
|
|
}
|
|
(DataTypeDefinition::Record { .. }, _) => {
|
|
return Err("Record/object literals are not supported as scalar defaults.".into());
|
|
}
|
|
_ => return Err(
|
|
"The default must have exactly the declared scalar type; values are not coerced."
|
|
.into(),
|
|
),
|
|
};
|
|
if valid {
|
|
Ok(())
|
|
} else {
|
|
Err(
|
|
"Default is nonfinite, outside the declared bounds, or not a declared enum literal."
|
|
.into(),
|
|
)
|
|
}
|
|
}
|
|
pub fn effective_field_unit<'a>(p: &'a Project, field: &'a Element) -> Option<&'a str> {
|
|
let SharedBody::DataField { type_ref, unit, .. } = field.shared()? else {
|
|
return None;
|
|
};
|
|
if !unit.is_empty() {
|
|
return Some(unit);
|
|
}
|
|
match type_ref {
|
|
TypeRef::Named(id) => match p.elements.get(id)?.shared()? {
|
|
SharedBody::DataType { unit, .. } => Some(unit),
|
|
_ => None,
|
|
},
|
|
_ => Some(unit),
|
|
}
|
|
}
|
|
pub fn effective_element_default<'a>(p: &'a Project, field: &'a Element) -> Option<&'a DataValue> {
|
|
let SharedBody::DataField {
|
|
type_ref: TypeRef::Named(id),
|
|
default: None,
|
|
..
|
|
} = field.shared()?
|
|
else {
|
|
return None;
|
|
};
|
|
match p.elements.get(id)?.shared()? {
|
|
SharedBody::DataType { default, .. } => default.as_ref(),
|
|
_ => None,
|
|
}
|
|
}
|
|
|
|
fn check_text(out: &mut Vec<Issue>, id: Id, field: &str, value: &str) {
|
|
if !valid_text(value) {
|
|
error(
|
|
out,
|
|
id,
|
|
"invalid_text",
|
|
field,
|
|
"Text contains a forbidden XML character.",
|
|
);
|
|
}
|
|
}
|
|
fn numeric_type(p: &Project, reference: &TypeRef) -> bool {
|
|
match reference {
|
|
TypeRef::Integer | TypeRef::Real => true,
|
|
TypeRef::Named(id) => matches!(
|
|
p.elements.get(id).and_then(Element::shared),
|
|
Some(SharedBody::DataType {
|
|
definition: DataTypeDefinition::Integer { .. } | DataTypeDefinition::Real { .. },
|
|
..
|
|
})
|
|
),
|
|
_ => false,
|
|
}
|
|
}
|
|
pub(crate) fn validate_element(p: &Project, e: &Element, out: &mut Vec<Issue>) {
|
|
if let Some(base) = supertype(e) {
|
|
let compatible = p.elements.get(&base).is_some_and(|b| {
|
|
b.kind() == e.kind()
|
|
&& b.layer() == e.layer()
|
|
&& (e.kind() != Kind::DataType || is_record(p, base))
|
|
});
|
|
if !compatible {
|
|
error(
|
|
out,
|
|
e.id,
|
|
"data_supertype",
|
|
"supertype",
|
|
"Choose a same-family supertype in this layer.",
|
|
);
|
|
}
|
|
if ancestry(p, base).contains(&e.id) {
|
|
error(
|
|
out,
|
|
e.id,
|
|
"data_inheritance_cycle",
|
|
"supertype",
|
|
"Single specialization must be acyclic.",
|
|
);
|
|
}
|
|
if e.kind() != Kind::Interface {
|
|
let names: BTreeSet<_> = effective_fields(p, base)
|
|
.into_iter()
|
|
.map(|f| &f.name)
|
|
.collect();
|
|
for local in p.children(e.id).filter(|f| f.kind() == Kind::DataField) {
|
|
if names.contains(&local.name) {
|
|
error(
|
|
out,
|
|
local.id,
|
|
"inherited_field_shadow",
|
|
"name",
|
|
"A local field cannot shadow an inherited field name.",
|
|
);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
match e.shared() {
|
|
Some(SharedBody::ExchangeItem {
|
|
kind,
|
|
mechanism,
|
|
supertype: base,
|
|
}) => {
|
|
if *mechanism == ExchangeMechanism::Unspecified {
|
|
warning(
|
|
out,
|
|
e.id,
|
|
"unspecified_exchange_mechanism",
|
|
"mechanism",
|
|
"Choose the exchange mechanism when its contract is known.",
|
|
);
|
|
}
|
|
if *kind != ExchangeItemKind::Information
|
|
&& matches!(
|
|
mechanism,
|
|
ExchangeMechanism::Event
|
|
| ExchangeMechanism::Operation
|
|
| ExchangeMechanism::SharedData
|
|
)
|
|
{
|
|
warning(
|
|
out,
|
|
e.id,
|
|
"physical_exchange_mechanism",
|
|
"mechanism",
|
|
"This mechanism usually describes information about a physical exchange; review the nature.",
|
|
);
|
|
}
|
|
if let Some(SharedBody::ExchangeItem {
|
|
kind: base_kind,
|
|
mechanism: base_mechanism,
|
|
..
|
|
}) = base
|
|
.and_then(|id| p.elements.get(&id))
|
|
.and_then(Element::shared)
|
|
&& (kind != base_kind
|
|
|| (*base_mechanism != ExchangeMechanism::Unspecified
|
|
&& mechanism != base_mechanism))
|
|
{
|
|
error(
|
|
out,
|
|
e.id,
|
|
"exchange_specialization_conflict",
|
|
"supertype",
|
|
"An item specialization preserves its base nature and any specified base mechanism.",
|
|
);
|
|
}
|
|
let fields = effective_fields(p, e.id);
|
|
if *mechanism == ExchangeMechanism::Operation {
|
|
for field in fields.iter().filter(|f| {
|
|
f.parent != Some(e.id)
|
|
&& matches!(
|
|
f.shared(),
|
|
Some(SharedBody::DataField {
|
|
direction: ParameterDirection::Unspecified,
|
|
..
|
|
})
|
|
)
|
|
}) {
|
|
warning(
|
|
out,
|
|
e.id,
|
|
"unspecified_inherited_parameter_direction",
|
|
"supertype",
|
|
format!(
|
|
"Inherited parameter '{}' has no direction in this operation.",
|
|
field.name
|
|
),
|
|
);
|
|
}
|
|
}
|
|
if *mechanism == ExchangeMechanism::Operation
|
|
&& fields
|
|
.iter()
|
|
.filter(|f| {
|
|
matches!(
|
|
f.shared(),
|
|
Some(SharedBody::DataField {
|
|
direction: ParameterDirection::Return,
|
|
..
|
|
})
|
|
)
|
|
})
|
|
.count()
|
|
> 1
|
|
{
|
|
error(
|
|
out,
|
|
e.id,
|
|
"multiple_return_parameters",
|
|
"supertype",
|
|
"An operation has at most one Return parameter, including inherited parameters.",
|
|
);
|
|
}
|
|
if fields.is_empty()
|
|
&& !matches!(
|
|
mechanism,
|
|
ExchangeMechanism::Event | ExchangeMechanism::Operation
|
|
)
|
|
&& p.relations.values().any(|r| {
|
|
r.body.items().contains(&e.id)
|
|
|| (r.kind() == RelationKind::InterfaceItemAllocation && r.target() == e.id)
|
|
})
|
|
{
|
|
warning(
|
|
out,
|
|
e.id,
|
|
"empty_exchange_item",
|
|
"body",
|
|
"This carried or allocated item has no defined fields; an empty notification or void operation may be intentional.",
|
|
);
|
|
}
|
|
}
|
|
Some(SharedBody::DataType {
|
|
definition,
|
|
default,
|
|
unit,
|
|
}) => {
|
|
if matches!(definition, DataTypeDefinition::Record { .. })
|
|
&& effective_fields(p, e.id).is_empty()
|
|
&& p.elements.values().any(|field| matches!(field.shared(), Some(SharedBody::DataField { type_ref: TypeRef::Named(id), .. }) if *id == e.id))
|
|
{
|
|
warning(out,e.id,"empty_record","definition","This referenced Record has no defined fields yet.");
|
|
}
|
|
let invalid = match definition {
|
|
DataTypeDefinition::Integer { minimum, maximum } => {
|
|
minimum.zip(*maximum).is_some_and(|(a, b)| a > b)
|
|
}
|
|
DataTypeDefinition::Real { minimum, maximum } => {
|
|
minimum.is_some_and(|n| !n.is_finite())
|
|
|| maximum.is_some_and(|n| !n.is_finite())
|
|
|| minimum.zip(*maximum).is_some_and(|(a, b)| a > b)
|
|
}
|
|
DataTypeDefinition::String {
|
|
minimum_length,
|
|
maximum_length,
|
|
} => maximum_length.is_some_and(|n| n < *minimum_length),
|
|
_ => false,
|
|
};
|
|
if invalid {
|
|
error(
|
|
out,
|
|
e.id,
|
|
"invalid_data_bounds",
|
|
"definition",
|
|
"Bounds must be ordered and real bounds must be finite.",
|
|
);
|
|
}
|
|
if let DataTypeDefinition::Enumeration { literals } = definition {
|
|
if literals.is_empty() {
|
|
warning(
|
|
out,
|
|
e.id,
|
|
"empty_enumeration",
|
|
"literals",
|
|
"Add enumeration literals when the available values are known.",
|
|
);
|
|
}
|
|
let mut seen = BTreeSet::new();
|
|
for literal in literals {
|
|
check_text(out, e.id, "literals", literal);
|
|
if literal.is_empty() {
|
|
error(
|
|
out,
|
|
e.id,
|
|
"empty_literal",
|
|
"literals",
|
|
"Enumeration literals must be nonempty.",
|
|
);
|
|
}
|
|
if !seen.insert(literal) {
|
|
error(
|
|
out,
|
|
e.id,
|
|
"duplicate_literal",
|
|
"literals",
|
|
"Enumeration literals must be unique and case-sensitive.",
|
|
);
|
|
}
|
|
}
|
|
}
|
|
if let Some(value) = default
|
|
&& let Err(message) = validate_scalar(definition, value)
|
|
{
|
|
error(out, e.id, "invalid_data_default", "default", message);
|
|
}
|
|
check_text(out, e.id, "unit", unit);
|
|
if !unit.is_empty()
|
|
&& !matches!(
|
|
definition,
|
|
DataTypeDefinition::Integer { .. } | DataTypeDefinition::Real { .. }
|
|
)
|
|
{
|
|
warning(
|
|
out,
|
|
e.id,
|
|
"nonnumeric_unit",
|
|
"unit",
|
|
"Units on nonnumeric types are preserved as annotations without conversion semantics.",
|
|
);
|
|
}
|
|
}
|
|
Some(SharedBody::DataField {
|
|
type_ref,
|
|
minimum,
|
|
maximum,
|
|
unit,
|
|
direction,
|
|
storage,
|
|
default,
|
|
}) => {
|
|
if maximum.is_some_and(|max| max < *minimum) {
|
|
error(
|
|
out,
|
|
e.id,
|
|
"invalid_multiplicity",
|
|
"maximum",
|
|
"Maximum multiplicity must be at least minimum, or unbounded.",
|
|
);
|
|
}
|
|
if let TypeRef::Named(id) = type_ref
|
|
&& !p
|
|
.elements
|
|
.get(id)
|
|
.is_some_and(|t| t.kind() == Kind::DataType && t.layer() == e.layer())
|
|
{
|
|
error(
|
|
out,
|
|
e.id,
|
|
"field_type",
|
|
"type_ref",
|
|
"Choose a data type in the field's layer.",
|
|
);
|
|
}
|
|
if let Some(owner) = e.parent.and_then(|id| p.elements.get(&id))
|
|
&& owner.kind() == Kind::DataType
|
|
&& !is_record(p, owner.id)
|
|
{
|
|
error(
|
|
out,
|
|
e.id,
|
|
"field_owner",
|
|
"parent",
|
|
"Fields under a data type require a Record type.",
|
|
);
|
|
}
|
|
let operation =
|
|
e.parent.and_then(|id| item_mechanism(p, id)) == Some(ExchangeMechanism::Operation);
|
|
if !operation && *direction != ParameterDirection::Unspecified {
|
|
error(
|
|
out,
|
|
e.id,
|
|
"parameter_direction",
|
|
"direction",
|
|
"Only directly owned operation parameters may declare a direction.",
|
|
);
|
|
} else if operation && *direction == ParameterDirection::Unspecified {
|
|
warning(
|
|
out,
|
|
e.id,
|
|
"unspecified_parameter_direction",
|
|
"direction",
|
|
"Choose the parameter direction from the provider's perspective.",
|
|
);
|
|
}
|
|
if *storage == FieldStorage::Reference
|
|
&& (!matches!(type_ref, TypeRef::Named(id) if is_record(p,*id))
|
|
|| default.is_some())
|
|
{
|
|
error(
|
|
out,
|
|
e.id,
|
|
"reference_field_contract",
|
|
"storage",
|
|
"Reference storage requires a named Record type and no scalar default.",
|
|
);
|
|
}
|
|
if let Some(values) = default {
|
|
if values.len() < *minimum as usize
|
|
|| maximum.is_some_and(|max| values.len() > max as usize)
|
|
{
|
|
error(
|
|
out,
|
|
e.id,
|
|
"field_default_multiplicity",
|
|
"default",
|
|
"The explicit default collection must satisfy the field's multiplicity.",
|
|
);
|
|
}
|
|
if !values.is_empty() && matches!(type_ref, TypeRef::Named(id) if is_record(p,*id))
|
|
{
|
|
error(
|
|
out,
|
|
e.id,
|
|
"record_field_default",
|
|
"default",
|
|
"Record and object-reference defaults are not supported.",
|
|
);
|
|
}
|
|
for value in values {
|
|
if let Err(message) = validate_data_value(p, type_ref, value) {
|
|
error(out, e.id, "invalid_field_default", "default", message);
|
|
}
|
|
}
|
|
}
|
|
check_text(out, e.id, "unit", unit);
|
|
if !unit.is_empty() && !numeric_type(p, type_ref) {
|
|
warning(
|
|
out,
|
|
e.id,
|
|
"nonnumeric_unit",
|
|
"unit",
|
|
"Units on nonnumeric fields are preserved as annotations.",
|
|
);
|
|
}
|
|
if let TypeRef::Named(id) = type_ref
|
|
&& let Some(SharedBody::DataType {
|
|
unit: inherited, ..
|
|
}) = p.elements.get(id).and_then(Element::shared)
|
|
&& !unit.is_empty()
|
|
&& !inherited.is_empty()
|
|
&& unit != inherited
|
|
{
|
|
warning(
|
|
out,
|
|
e.id,
|
|
"field_unit_override",
|
|
"unit",
|
|
"The field unit differs from its named type; conversion compatibility is not evaluated.",
|
|
);
|
|
}
|
|
}
|
|
Some(SharedBody::Interface { .. })
|
|
if effective_interface_allocations(p, e.id).is_empty() =>
|
|
{
|
|
warning(
|
|
out,
|
|
e.id,
|
|
"empty_interface",
|
|
"body",
|
|
"Allocate existing exchange items to define this interface's contract.",
|
|
);
|
|
}
|
|
_ => {}
|
|
}
|
|
}
|
|
|
|
pub(crate) fn validate_relation(p: &Project, r: &Relation, out: &mut Vec<Issue>) {
|
|
match &r.body {
|
|
RelationBody::InterfaceItemAllocation { source, target, .. } => {
|
|
if effective_interface_allocations(p, *source)
|
|
.into_iter()
|
|
.any(|other| other.id != r.id && other.target() == *target)
|
|
{
|
|
error(
|
|
out,
|
|
r.id,
|
|
"duplicate_interface_item",
|
|
"target",
|
|
"An interface cannot allocate an item already allocated locally or inherited from a base.",
|
|
);
|
|
}
|
|
}
|
|
RelationBody::DataAssociation {
|
|
source,
|
|
target,
|
|
source_role,
|
|
target_role,
|
|
source_minimum,
|
|
source_maximum,
|
|
target_minimum,
|
|
target_maximum,
|
|
source_navigable,
|
|
target_navigable,
|
|
} => {
|
|
if !is_record(p, *source) || !is_record(p, *target) {
|
|
error(
|
|
out,
|
|
r.id,
|
|
"association_record_endpoints",
|
|
"endpoints",
|
|
"A data association connects two Record data types.",
|
|
);
|
|
}
|
|
for (field, min, max, role) in [
|
|
("source", source_minimum, source_maximum, source_role),
|
|
("target", target_minimum, target_maximum, target_role),
|
|
] {
|
|
if max.is_some_and(|max| max < *min) {
|
|
error(
|
|
out,
|
|
r.id,
|
|
"association_multiplicity",
|
|
field,
|
|
"An association end maximum must be at least its minimum.",
|
|
);
|
|
}
|
|
check_text(out, r.id, field, role);
|
|
if role.trim().is_empty() {
|
|
warning(
|
|
out,
|
|
r.id,
|
|
"empty_association_role",
|
|
field,
|
|
"Name this association end to explain its role.",
|
|
);
|
|
}
|
|
}
|
|
if source == target
|
|
&& *source_navigable
|
|
&& *target_navigable
|
|
&& (source_role.trim().is_empty()
|
|
|| target_role.trim().is_empty()
|
|
|| source_role == target_role)
|
|
{
|
|
error(
|
|
out,
|
|
r.id,
|
|
"self_association_roles",
|
|
"source_role",
|
|
"A self-association with two navigable ends requires distinct nonempty role names.",
|
|
);
|
|
}
|
|
if p.relations.values().any(|other| other.id != r.id && matches!(&other.body,
|
|
RelationBody::DataAssociation { source:s,target:t,source_role:sr,target_role:tr,.. } if
|
|
(s == source && t == target && sr == source_role && tr == target_role) ||
|
|
(s == target && t == source && sr == target_role && tr == source_role))) {
|
|
warning(out,r.id,"duplicate_association_roles","endpoints","Another identified association describes these same endpoint roles; review whether both are intended.");
|
|
}
|
|
}
|
|
_ => {}
|
|
}
|
|
if !r.body.items().is_empty()
|
|
&& let Err(message) = try_carried_field_paths(p, r)
|
|
{
|
|
error(out, r.id, "data_path_expansion_limit", "items", message);
|
|
}
|
|
}
|
|
|
|
pub(crate) fn validate_record_cycles(p: &Project, out: &mut Vec<Issue>) {
|
|
let graph: BTreeMap<_, Vec<_>> = p
|
|
.elements
|
|
.values()
|
|
.filter(|e| is_record(p, e.id))
|
|
.map(|e| {
|
|
let edges = effective_fields(p, e.id)
|
|
.into_iter()
|
|
.filter_map(|f| match f.shared() {
|
|
Some(SharedBody::DataField {
|
|
storage: FieldStorage::Value,
|
|
minimum,
|
|
type_ref: TypeRef::Named(id),
|
|
..
|
|
}) if *minimum > 0 && is_record(p, *id) => Some(*id),
|
|
_ => None,
|
|
})
|
|
.collect();
|
|
(e.id, edges)
|
|
})
|
|
.collect();
|
|
// Remove finite sinks, then their predecessors. Remaining records either
|
|
// contain a mandatory cycle or necessarily contain a record that does.
|
|
let mut remaining: BTreeMap<_, _> =
|
|
graph.iter().map(|(id, edges)| (*id, edges.len())).collect();
|
|
let mut incoming: BTreeMap<Id, Vec<Id>> = BTreeMap::new();
|
|
for (id, edges) in &graph {
|
|
for target in edges {
|
|
incoming.entry(*target).or_default().push(*id);
|
|
}
|
|
}
|
|
let mut pending: Vec<_> = remaining
|
|
.iter()
|
|
.filter_map(|(id, count)| (*count == 0).then_some(*id))
|
|
.collect();
|
|
while let Some(id) = pending.pop() {
|
|
remaining.remove(&id);
|
|
for source in incoming.get(&id).into_iter().flatten() {
|
|
if let Some(count) = remaining.get_mut(source) {
|
|
*count -= 1;
|
|
if *count == 0 {
|
|
pending.push(*source);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
for id in remaining.keys() {
|
|
warning(
|
|
out,
|
|
*id,
|
|
"record_no_finite_value",
|
|
"definition",
|
|
"Mandatory Value record fields require a cycle; this type cannot have a finite fully populated value.",
|
|
);
|
|
}
|
|
}
|