pub mod outside;
pub mod verdict;
pub use outside::Dropped;
pub use verdict::{
ALL_PROFILES, ConstructKind, FacetProperty, Verdict, construct_verdict, facet_verdict,
};
use crate::facet::VendorKind;
use crate::model::Ast;
use crate::node::AnyId;
use crate::node::type_::TypeKind;
use alloc::string::{String, ToString};
use alloc::vec::Vec;
use core::fmt;
use zerodds_ast_core::NodeId;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum Profile {
Idl42Core,
XTypes13,
Corba33,
Ros2,
DdsXml,
FlatData,
Vendor(VendorKind),
}
impl Profile {
pub const ALL: [Self; 6] = [
Self::Idl42Core,
Self::XTypes13,
Self::Corba33,
Self::Ros2,
Self::DdsXml,
Self::FlatData,
];
#[must_use]
pub const fn as_str(self) -> &'static str {
match self {
Self::Idl42Core => "IDL 4.2",
Self::XTypes13 => "DDS-XTypes 1.3",
Self::Corba33 => "CORBA 3.3",
Self::Ros2 => "ROS 2",
Self::DdsXml => "DDS-XML 1.0",
Self::FlatData => "FlatData",
Self::Vendor(_) => "vendor dialect",
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ProfileViolation {
pub node: NodeId,
pub profile: Profile,
pub rule: &'static str,
pub spec_reference: &'static str,
pub detail: String,
}
impl fmt::Display for ProfileViolation {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
f,
"{}: {} ({}) at {:?}: {}",
self.profile.as_str(),
self.rule,
self.spec_reference,
self.node,
self.detail
)
}
}
impl core::error::Error for ProfileViolation {}
impl Ast {
pub fn validate(&self, profile: Profile) -> Result<(), Vec<ProfileViolation>> {
let mut out = Vec::new();
for (id, node) in self.types() {
let node_id = node.node_id;
if profile != Profile::FlatData {
let construct = ConstructKind::of(&node.kind);
if let Verdict::Rejected {
rule,
spec_reference,
detail,
} = construct_verdict(profile, construct)
{
out.push(ProfileViolation {
node: node_id,
profile,
rule,
spec_reference,
detail: detail.to_string(),
});
}
}
check_facets(self, node.facets_ref(), node_id, profile, &mut out);
for member in node.kind.members() {
let Ok(m) = self.member(*member) else {
continue;
};
check_facets(self, &m.facets, m.node_id, profile, &mut out);
}
match profile {
Profile::XTypes13 => check_xtypes_bounds(self, id, node_id, &mut out),
Profile::Corba33 => check_corba_shapes(self, id, node_id, &mut out),
Profile::Ros2 => {
let mut seen = alloc::collections::BTreeSet::new();
check_ros_shapes(self, id, node_id, &mut seen, &mut out);
}
Profile::FlatData => check_flatdata(self, id, node_id, &mut out),
_ => {}
}
}
if matches!(profile, Profile::DdsXml) {
check_dds_xml(self, &mut out);
}
if out.is_empty() { Ok(()) } else { Err(out) }
}
pub fn validate_complete(&self) -> Result<(), Vec<ProfileViolation>> {
let mut out = Vec::new();
for (_, node) in self.types() {
for reference in node.kind.type_refs() {
if reference.is_pending() {
out.push(ProfileViolation {
node: node.node_id,
profile: Profile::Idl42Core,
rule: "no unresolved reference",
spec_reference: "zerodds-ast-1.0 §8.2",
detail: "a type reference is still pending".to_string(),
});
} else if reference.is_broken() {
out.push(ProfileViolation {
node: node.node_id,
profile: Profile::Idl42Core,
rule: "no broken reference",
spec_reference: "zerodds-ast-1.0 §8.2",
detail: "a type reference could not be resolved".to_string(),
});
}
}
if node.completeness == crate::Completeness::Forward {
out.push(ProfileViolation {
node: node.node_id,
profile: Profile::Idl42Core,
rule: "forward declaration defined",
spec_reference: "zerodds-ast-1.0 §8.2",
detail: "declared but never defined".to_string(),
});
}
}
for (id, member) in self.members_all() {
let _ = id;
if member.type_.is_pending() || member.type_.is_broken() {
out.push(ProfileViolation {
node: member.node_id,
profile: Profile::Idl42Core,
rule: "no unresolved reference",
spec_reference: "zerodds-ast-1.0 §8.2",
detail: "the member's type is not resolved".to_string(),
});
}
}
if out.is_empty() { Ok(()) } else { Err(out) }
}
pub(crate) fn members_all(
&self,
) -> impl Iterator<Item = (zerodds_ast_core::Id<crate::MemberNode>, &crate::MemberNode)> {
self.members.iter()
}
pub(crate) fn topics_all(
&self,
) -> impl Iterator<Item = (zerodds_ast_core::Id<crate::TopicNode>, &crate::TopicNode)> {
self.topics.iter()
}
pub(crate) fn qos_profiles_all(
&self,
) -> impl Iterator<
Item = (
zerodds_ast_core::Id<crate::QosProfileNode>,
&crate::QosProfileNode,
),
> {
self.qos_profiles.iter()
}
}
fn violation(
node: NodeId,
profile: Profile,
rule: &'static str,
spec_reference: &'static str,
detail: &str,
) -> ProfileViolation {
ProfileViolation {
node,
profile,
rule,
spec_reference,
detail: detail.to_string(),
}
}
fn check_facets(
ast: &Ast,
facets: &crate::Facets,
node_id: NodeId,
profile: Profile,
out: &mut Vec<ProfileViolation>,
) {
let _ = ast;
for property in FacetProperty::ALL {
if !carries(facets, property) {
continue;
}
if let Verdict::Rejected {
rule,
spec_reference,
detail,
} = facet_verdict(profile, property)
{
out.push(ProfileViolation {
node: node_id,
profile,
rule,
spec_reference,
detail: detail.to_string(),
});
}
}
}
fn carries(facets: &crate::Facets, property: FacetProperty) -> bool {
let xtypes = facets.xtypes.as_ref();
match property {
FacetProperty::Key => facets.is_key() || xtypes.is_some_and(|x| x.discriminator_is_key),
FacetProperty::Optional => facets.is_optional(),
FacetProperty::External => xtypes.is_some_and(|x| x.is_external),
FacetProperty::MustUnderstand => facets.is_must_understand(),
FacetProperty::NonSerialized => xtypes.is_some_and(|x| x.is_non_serialized),
FacetProperty::TryConstruct => xtypes.is_some_and(|x| {
x.try_construct.is_some()
|| x.element_try_construct.is_some()
|| x.key_try_construct.is_some()
|| x.discriminator_try_construct.is_some()
}),
FacetProperty::NonFinalExtensibility => matches!(
facets.extensibility(),
Some(crate::Extensibility::Appendable | crate::Extensibility::Mutable)
),
FacetProperty::ExplicitMemberId => xtypes.is_some_and(|x| {
x.member_id.as_ref().is_some_and(|id| {
!matches!(
id.origin,
crate::facet::xtypes::MemberIdOrigin::PreviousPlusOne
)
})
}),
FacetProperty::DefaultValue => xtypes.is_some_and(|x| x.default_value.is_some()),
FacetProperty::RangeConstraint => xtypes.is_some_and(|x| x.range.is_some()),
FacetProperty::Unit => xtypes.is_some_and(|x| x.unit.is_some()),
FacetProperty::Verbatim => xtypes.is_some_and(|x| !x.verbatim.is_empty()),
FacetProperty::Ros2DefaultValue => facets
.ros2
.as_ref()
.is_some_and(|r| r.default_value.is_some()),
FacetProperty::VendorDirective => !facets.vendor.is_empty(),
}
}
fn check_corba_shapes(
ast: &Ast,
id: zerodds_ast_core::Id<crate::TypeNode>,
node_id: NodeId,
out: &mut Vec<ProfileViolation>,
) {
let Ok(node) = ast.type_(id) else {
return;
};
match &node.kind {
TypeKind::Structure { base: Some(_), .. } => out.push(violation(
node_id,
Profile::Corba33,
"a structure with no base",
"CORBA 3.3 part 1 §7.4.1.4.4.2.2",
"CORBA IDL has no syntax for a structure with a base",
)),
TypeKind::Enumeration { .. } => {
if ast.effective_bit_bound(id) != 32 {
out.push(violation(
node_id,
Profile::Corba33,
"an enumeration in CORBA's holder",
"CORBA 3.3 part 2 §9.3.2.5",
"an enumeration with a @bit_bound, where CORBA's holder is \
always a 32-bit unsigned long",
));
}
for (position, enumerator) in ast.enumerators_of(id).iter().enumerate() {
let ordinal = i128::try_from(position).unwrap_or(i128::MAX);
if ast.effective_enumerator_value(*enumerator) != Some(ordinal) {
out.push(violation(
node_id,
Profile::Corba33,
"an enumerator at its ordinal",
"CORBA 3.3 part 2 §9.3.2.5",
"an enumerator with an explicit @value, where CORBA's is \
its ordinal position",
));
break;
}
}
}
_ => {}
}
}
const MAX_ALIAS_DEPTH: usize = 64;
fn check_ros_shapes(
ast: &Ast,
id: zerodds_ast_core::Id<crate::TypeNode>,
node_id: NodeId,
seen: &mut alloc::collections::BTreeSet<zerodds_ast_core::Id<crate::TypeNode>>,
out: &mut Vec<ProfileViolation>,
) {
if !seen.insert(id) {
return;
}
let Ok(node) = ast.type_(id) else {
return;
};
if let TypeKind::Structure { members, .. } = &node.kind {
for member in members {
let Ok(member_node) = ast.member(*member) else {
continue;
};
if let Some(inner) = member_node.type_.resolved() {
check_ros_shapes(ast, inner, node_id, seen, out);
}
}
}
match &node.kind {
TypeKind::Structure { base: Some(_), .. } => out.push(violation(
node_id,
Profile::Ros2,
"a structure with no base",
"ROS 2 interface definition rules",
"rosidl's IDL grammar has no syntax for a struct with a base",
)),
TypeKind::Sequence { element, .. } | TypeKind::Array { element, .. } => {
let mut current = element.resolved();
let mut nested = false;
for _ in 0..MAX_ALIAS_DEPTH {
let Some(inner) = current else { break };
let Ok(node) = ast.type_(inner) else { break };
match &node.kind {
TypeKind::Sequence { .. } | TypeKind::Array { .. } | TypeKind::Map { .. } => {
nested = true;
break;
}
TypeKind::Alias { aliased } => current = aliased.resolved(),
_ => break,
}
}
if nested {
out.push(violation(
node_id,
Profile::Ros2,
"a sequence of a non-collection",
"ROS 2 interface definition rules",
"a collection whose element is itself a collection, which rosidl \
has no shape for",
));
}
}
_ => {}
}
}
fn check_xtypes_bounds(
ast: &Ast,
id: zerodds_ast_core::Id<crate::TypeNode>,
node_id: NodeId,
out: &mut Vec<ProfileViolation>,
) {
let Ok(node) = ast.type_(id) else {
return;
};
let bit_bound = node.facets.xtypes.as_ref().and_then(|x| x.bit_bound);
match &node.kind {
TypeKind::Enumeration { .. } => {
if let Some(bound) = bit_bound {
if bound == 0 || bound > 32 {
out.push(violation(
node_id,
Profile::XTypes13,
"enum bit bound in 1..=32",
"DDS-XTypes 1.3 §7.3.1.2.1.9",
"an enumeration's @bit_bound must be between 1 and 32",
));
}
}
}
TypeKind::Bitmask { bits } => {
let bound = bit_bound.unwrap_or(32);
if bound == 0 || bound > 64 {
out.push(violation(
node_id,
Profile::XTypes13,
"bitmask bit bound in 1..=64",
"DDS-XTypes 1.3 §7.3.1.2.1.10",
"a bitmask's @bit_bound must be between 1 and 64",
));
}
for bit in bits {
let Ok(b) = ast.bit_value(*bit) else {
continue;
};
if let Some(position) = b.position {
if position >= bound {
out.push(violation(
b.node_id,
Profile::XTypes13,
"bit position within bound",
"DDS-XTypes 1.3 §7.3.1.2.1.10",
"@position is at or beyond the bitmask's bit bound",
));
}
}
}
}
_ => {}
}
}
fn check_flatdata(
ast: &Ast,
id: zerodds_ast_core::Id<crate::TypeNode>,
node_id: NodeId,
out: &mut Vec<ProfileViolation>,
) {
let Ok(node) = ast.type_(id) else {
return;
};
if !node
.facets
.flatdata
.as_ref()
.is_some_and(|f| f.is_plain || f.explicit_fixed_layout)
{
return;
}
for entry in ast.descendants(AnyId::Type(id, node_id)) {
let Some(type_id) = entry.as_type() else {
continue;
};
let Ok(reachable) = ast.type_(type_id) else {
continue;
};
let construct = ConstructKind::of(&reachable.kind);
if let Verdict::Rejected {
rule,
spec_reference,
detail,
} = construct_verdict(Profile::FlatData, construct)
{
out.push(ProfileViolation {
node: node_id,
profile: Profile::FlatData,
rule,
spec_reference,
detail: detail.to_string(),
});
}
check_facets(ast, &reachable.facets, node_id, Profile::FlatData, out);
for member in reachable.kind.members() {
if let Ok(m) = ast.member(*member) {
check_facets(ast, &m.facets, node_id, Profile::FlatData, out);
}
}
}
}
fn check_dds_xml(ast: &Ast, out: &mut Vec<ProfileViolation>) {
for (id, topic) in ast.topics_all() {
let _ = id;
if !topic.type_.is_resolved() {
out.push(violation(
topic.node_id,
Profile::DdsXml,
"topic type resolvable",
"DDS-XML 1.0 §7.3",
"a topic must name a type the model holds",
));
}
}
for (id, profile) in ast.qos_profiles_all() {
let mut seen = alloc::collections::BTreeSet::new();
let _ = seen.insert(profile.node_id);
let mut current = profile
.base
.as_ref()
.and_then(zerodds_ast_core::Ref::resolved);
let mut cyclic = false;
while let Some(next) = current {
let Ok(node) = ast.qos_profile(next) else {
break;
};
if !seen.insert(node.node_id) {
cyclic = true;
break;
}
current = node.base.as_ref().and_then(zerodds_ast_core::Ref::resolved);
}
if cyclic {
let _ = id;
out.push(violation(
profile.node_id,
Profile::DdsXml,
"acyclic profile inheritance",
"DDS-XML 1.0 §7.5",
"the profile's base chain closes a cycle",
));
}
}
}