1pub mod outside;
13pub mod verdict;
14
15pub use outside::Dropped;
16pub use verdict::{
17 ALL_PROFILES, ConstructKind, FacetProperty, Verdict, construct_verdict, facet_verdict,
18};
19
20use crate::facet::VendorKind;
21use crate::model::Ast;
22use crate::node::AnyId;
23use crate::node::type_::TypeKind;
24use alloc::string::{String, ToString};
25use alloc::vec::Vec;
26use core::fmt;
27use zerodds_ast_core::NodeId;
28
29#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
31pub enum Profile {
32 Idl42Core,
34 XTypes13,
36 Corba33,
38 Ros2,
40 DdsXml,
42 FlatData,
44 Vendor(VendorKind),
46}
47
48impl Profile {
49 pub const ALL: [Self; 6] = [
51 Self::Idl42Core,
52 Self::XTypes13,
53 Self::Corba33,
54 Self::Ros2,
55 Self::DdsXml,
56 Self::FlatData,
57 ];
58
59 #[must_use]
61 pub const fn as_str(self) -> &'static str {
62 match self {
63 Self::Idl42Core => "IDL 4.2",
64 Self::XTypes13 => "DDS-XTypes 1.3",
65 Self::Corba33 => "CORBA 3.3",
66 Self::Ros2 => "ROS 2",
67 Self::DdsXml => "DDS-XML 1.0",
68 Self::FlatData => "FlatData",
69 Self::Vendor(_) => "vendor dialect",
70 }
71 }
72}
73
74#[derive(Debug, Clone, PartialEq, Eq)]
76pub struct ProfileViolation {
77 pub node: NodeId,
79 pub profile: Profile,
81 pub rule: &'static str,
83 pub spec_reference: &'static str,
85 pub detail: String,
87}
88
89impl fmt::Display for ProfileViolation {
90 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
91 write!(
92 f,
93 "{}: {} ({}) at {:?}: {}",
94 self.profile.as_str(),
95 self.rule,
96 self.spec_reference,
97 self.node,
98 self.detail
99 )
100 }
101}
102
103impl core::error::Error for ProfileViolation {}
104
105impl Ast {
106 pub fn validate(&self, profile: Profile) -> Result<(), Vec<ProfileViolation>> {
116 let mut out = Vec::new();
117
118 for (id, node) in self.types() {
119 let node_id = node.node_id;
120
121 if profile != Profile::FlatData {
124 let construct = ConstructKind::of(&node.kind);
125 if let Verdict::Rejected {
126 rule,
127 spec_reference,
128 detail,
129 } = construct_verdict(profile, construct)
130 {
131 out.push(ProfileViolation {
132 node: node_id,
133 profile,
134 rule,
135 spec_reference,
136 detail: detail.to_string(),
137 });
138 }
139 }
140
141 check_facets(self, node.facets_ref(), node_id, profile, &mut out);
142
143 for member in node.kind.members() {
144 let Ok(m) = self.member(*member) else {
145 continue;
146 };
147 check_facets(self, &m.facets, m.node_id, profile, &mut out);
148 }
149
150 match profile {
151 Profile::XTypes13 => check_xtypes_bounds(self, id, node_id, &mut out),
152 Profile::Corba33 => check_corba_shapes(self, id, node_id, &mut out),
153 Profile::Ros2 => {
154 let mut seen = alloc::collections::BTreeSet::new();
155 check_ros_shapes(self, id, node_id, &mut seen, &mut out);
156 }
157 Profile::FlatData => check_flatdata(self, id, node_id, &mut out),
158 _ => {}
159 }
160 }
161
162 if matches!(profile, Profile::DdsXml) {
163 check_dds_xml(self, &mut out);
164 }
165
166 if out.is_empty() { Ok(()) } else { Err(out) }
167 }
168
169 pub fn validate_complete(&self) -> Result<(), Vec<ProfileViolation>> {
179 let mut out = Vec::new();
180 for (_, node) in self.types() {
181 for reference in node.kind.type_refs() {
182 if reference.is_pending() {
183 out.push(ProfileViolation {
184 node: node.node_id,
185 profile: Profile::Idl42Core,
186 rule: "no unresolved reference",
187 spec_reference: "zerodds-ast-1.0 §8.2",
188 detail: "a type reference is still pending".to_string(),
189 });
190 } else if reference.is_broken() {
191 out.push(ProfileViolation {
192 node: node.node_id,
193 profile: Profile::Idl42Core,
194 rule: "no broken reference",
195 spec_reference: "zerodds-ast-1.0 §8.2",
196 detail: "a type reference could not be resolved".to_string(),
197 });
198 }
199 }
200 if node.completeness == crate::Completeness::Forward {
201 out.push(ProfileViolation {
202 node: node.node_id,
203 profile: Profile::Idl42Core,
204 rule: "forward declaration defined",
205 spec_reference: "zerodds-ast-1.0 §8.2",
206 detail: "declared but never defined".to_string(),
207 });
208 }
209 }
210 for (id, member) in self.members_all() {
211 let _ = id;
212 if member.type_.is_pending() || member.type_.is_broken() {
213 out.push(ProfileViolation {
214 node: member.node_id,
215 profile: Profile::Idl42Core,
216 rule: "no unresolved reference",
217 spec_reference: "zerodds-ast-1.0 §8.2",
218 detail: "the member's type is not resolved".to_string(),
219 });
220 }
221 }
222 if out.is_empty() { Ok(()) } else { Err(out) }
223 }
224
225 pub(crate) fn members_all(
226 &self,
227 ) -> impl Iterator<Item = (zerodds_ast_core::Id<crate::MemberNode>, &crate::MemberNode)> {
228 self.members.iter()
229 }
230
231 pub(crate) fn topics_all(
232 &self,
233 ) -> impl Iterator<Item = (zerodds_ast_core::Id<crate::TopicNode>, &crate::TopicNode)> {
234 self.topics.iter()
235 }
236
237 pub(crate) fn qos_profiles_all(
238 &self,
239 ) -> impl Iterator<
240 Item = (
241 zerodds_ast_core::Id<crate::QosProfileNode>,
242 &crate::QosProfileNode,
243 ),
244 > {
245 self.qos_profiles.iter()
246 }
247}
248
249fn violation(
250 node: NodeId,
251 profile: Profile,
252 rule: &'static str,
253 spec_reference: &'static str,
254 detail: &str,
255) -> ProfileViolation {
256 ProfileViolation {
257 node,
258 profile,
259 rule,
260 spec_reference,
261 detail: detail.to_string(),
262 }
263}
264
265fn check_facets(
267 ast: &Ast,
268 facets: &crate::Facets,
269 node_id: NodeId,
270 profile: Profile,
271 out: &mut Vec<ProfileViolation>,
272) {
273 let _ = ast;
274 for property in FacetProperty::ALL {
275 if !carries(facets, property) {
276 continue;
277 }
278 if let Verdict::Rejected {
279 rule,
280 spec_reference,
281 detail,
282 } = facet_verdict(profile, property)
283 {
284 out.push(ProfileViolation {
285 node: node_id,
286 profile,
287 rule,
288 spec_reference,
289 detail: detail.to_string(),
290 });
291 }
292 }
293}
294
295fn carries(facets: &crate::Facets, property: FacetProperty) -> bool {
297 let xtypes = facets.xtypes.as_ref();
298 match property {
299 FacetProperty::Key => facets.is_key() || xtypes.is_some_and(|x| x.discriminator_is_key),
304 FacetProperty::Optional => facets.is_optional(),
305 FacetProperty::External => xtypes.is_some_and(|x| x.is_external),
306 FacetProperty::MustUnderstand => facets.is_must_understand(),
307 FacetProperty::NonSerialized => xtypes.is_some_and(|x| x.is_non_serialized),
308 FacetProperty::TryConstruct => xtypes.is_some_and(|x| {
314 x.try_construct.is_some()
315 || x.element_try_construct.is_some()
316 || x.key_try_construct.is_some()
317 || x.discriminator_try_construct.is_some()
318 }),
319 FacetProperty::NonFinalExtensibility => matches!(
320 facets.extensibility(),
321 Some(crate::Extensibility::Appendable | crate::Extensibility::Mutable)
322 ),
323 FacetProperty::ExplicitMemberId => xtypes.is_some_and(|x| {
333 x.member_id.as_ref().is_some_and(|id| {
334 !matches!(
335 id.origin,
336 crate::facet::xtypes::MemberIdOrigin::PreviousPlusOne
337 )
338 })
339 }),
340 FacetProperty::DefaultValue => xtypes.is_some_and(|x| x.default_value.is_some()),
341 FacetProperty::RangeConstraint => xtypes.is_some_and(|x| x.range.is_some()),
342 FacetProperty::Unit => xtypes.is_some_and(|x| x.unit.is_some()),
343 FacetProperty::Verbatim => xtypes.is_some_and(|x| !x.verbatim.is_empty()),
344 FacetProperty::Ros2DefaultValue => facets
345 .ros2
346 .as_ref()
347 .is_some_and(|r| r.default_value.is_some()),
348 FacetProperty::VendorDirective => !facets.vendor.is_empty(),
349 }
350}
351
352fn check_corba_shapes(
374 ast: &Ast,
375 id: zerodds_ast_core::Id<crate::TypeNode>,
376 node_id: NodeId,
377 out: &mut Vec<ProfileViolation>,
378) {
379 let Ok(node) = ast.type_(id) else {
380 return;
381 };
382 match &node.kind {
383 TypeKind::Structure { base: Some(_), .. } => out.push(violation(
384 node_id,
385 Profile::Corba33,
386 "a structure with no base",
387 "CORBA 3.3 part 1 §7.4.1.4.4.2.2",
388 "CORBA IDL has no syntax for a structure with a base",
389 )),
390 TypeKind::Enumeration { .. } => {
391 if ast.effective_bit_bound(id) != 32 {
392 out.push(violation(
393 node_id,
394 Profile::Corba33,
395 "an enumeration in CORBA's holder",
396 "CORBA 3.3 part 2 §9.3.2.5",
397 "an enumeration with a @bit_bound, where CORBA's holder is \
398 always a 32-bit unsigned long",
399 ));
400 }
401 for (position, enumerator) in ast.enumerators_of(id).iter().enumerate() {
402 let ordinal = i128::try_from(position).unwrap_or(i128::MAX);
403 if ast.effective_enumerator_value(*enumerator) != Some(ordinal) {
404 out.push(violation(
405 node_id,
406 Profile::Corba33,
407 "an enumerator at its ordinal",
408 "CORBA 3.3 part 2 §9.3.2.5",
409 "an enumerator with an explicit @value, where CORBA's is \
410 its ordinal position",
411 ));
412 break;
413 }
414 }
415 }
416 _ => {}
417 }
418}
419
420const MAX_ALIAS_DEPTH: usize = 64;
433
434fn check_ros_shapes(
438 ast: &Ast,
439 id: zerodds_ast_core::Id<crate::TypeNode>,
440 node_id: NodeId,
441 seen: &mut alloc::collections::BTreeSet<zerodds_ast_core::Id<crate::TypeNode>>,
442 out: &mut Vec<ProfileViolation>,
443) {
444 if !seen.insert(id) {
449 return;
450 }
451 let Ok(node) = ast.type_(id) else {
452 return;
453 };
454 if let TypeKind::Structure { members, .. } = &node.kind {
460 for member in members {
461 let Ok(member_node) = ast.member(*member) else {
462 continue;
463 };
464 if let Some(inner) = member_node.type_.resolved() {
465 check_ros_shapes(ast, inner, node_id, seen, out);
466 }
467 }
468 }
469 match &node.kind {
470 TypeKind::Structure { base: Some(_), .. } => out.push(violation(
471 node_id,
472 Profile::Ros2,
473 "a structure with no base",
474 "ROS 2 interface definition rules",
475 "rosidl's IDL grammar has no syntax for a struct with a base",
476 )),
477 TypeKind::Sequence { element, .. } | TypeKind::Array { element, .. } => {
478 let mut current = element.resolved();
483 let mut nested = false;
484 for _ in 0..MAX_ALIAS_DEPTH {
485 let Some(inner) = current else { break };
486 let Ok(node) = ast.type_(inner) else { break };
487 match &node.kind {
488 TypeKind::Sequence { .. } | TypeKind::Array { .. } | TypeKind::Map { .. } => {
489 nested = true;
490 break;
491 }
492 TypeKind::Alias { aliased } => current = aliased.resolved(),
493 _ => break,
494 }
495 }
496 if nested {
497 out.push(violation(
498 node_id,
499 Profile::Ros2,
500 "a sequence of a non-collection",
501 "ROS 2 interface definition rules",
502 "a collection whose element is itself a collection, which rosidl \
503 has no shape for",
504 ));
505 }
506 }
507 _ => {}
508 }
509}
510
511fn check_xtypes_bounds(
512 ast: &Ast,
513 id: zerodds_ast_core::Id<crate::TypeNode>,
514 node_id: NodeId,
515 out: &mut Vec<ProfileViolation>,
516) {
517 let Ok(node) = ast.type_(id) else {
518 return;
519 };
520 let bit_bound = node.facets.xtypes.as_ref().and_then(|x| x.bit_bound);
521
522 match &node.kind {
523 TypeKind::Enumeration { .. } => {
524 if let Some(bound) = bit_bound {
525 if bound == 0 || bound > 32 {
526 out.push(violation(
527 node_id,
528 Profile::XTypes13,
529 "enum bit bound in 1..=32",
530 "DDS-XTypes 1.3 §7.3.1.2.1.9",
531 "an enumeration's @bit_bound must be between 1 and 32",
532 ));
533 }
534 }
535 }
536 TypeKind::Bitmask { bits } => {
537 let bound = bit_bound.unwrap_or(32);
538 if bound == 0 || bound > 64 {
539 out.push(violation(
540 node_id,
541 Profile::XTypes13,
542 "bitmask bit bound in 1..=64",
543 "DDS-XTypes 1.3 §7.3.1.2.1.10",
544 "a bitmask's @bit_bound must be between 1 and 64",
545 ));
546 }
547 for bit in bits {
548 let Ok(b) = ast.bit_value(*bit) else {
549 continue;
550 };
551 if let Some(position) = b.position {
552 if position >= bound {
553 out.push(violation(
554 b.node_id,
555 Profile::XTypes13,
556 "bit position within bound",
557 "DDS-XTypes 1.3 §7.3.1.2.1.10",
558 "@position is at or beyond the bitmask's bit bound",
559 ));
560 }
561 }
562 }
563 }
564 _ => {}
565 }
566}
567
568fn check_flatdata(
571 ast: &Ast,
572 id: zerodds_ast_core::Id<crate::TypeNode>,
573 node_id: NodeId,
574 out: &mut Vec<ProfileViolation>,
575) {
576 let Ok(node) = ast.type_(id) else {
577 return;
578 };
579 if !node
582 .facets
583 .flatdata
584 .as_ref()
585 .is_some_and(|f| f.is_plain || f.explicit_fixed_layout)
586 {
587 return;
588 }
589
590 for entry in ast.descendants(AnyId::Type(id, node_id)) {
591 let Some(type_id) = entry.as_type() else {
592 continue;
593 };
594 let Ok(reachable) = ast.type_(type_id) else {
595 continue;
596 };
597 let construct = ConstructKind::of(&reachable.kind);
598 if let Verdict::Rejected {
599 rule,
600 spec_reference,
601 detail,
602 } = construct_verdict(Profile::FlatData, construct)
603 {
604 out.push(ProfileViolation {
605 node: node_id,
606 profile: Profile::FlatData,
607 rule,
608 spec_reference,
609 detail: detail.to_string(),
610 });
611 }
612 check_facets(ast, &reachable.facets, node_id, Profile::FlatData, out);
613 for member in reachable.kind.members() {
614 if let Ok(m) = ast.member(*member) {
615 check_facets(ast, &m.facets, node_id, Profile::FlatData, out);
616 }
617 }
618 }
619}
620
621fn check_dds_xml(ast: &Ast, out: &mut Vec<ProfileViolation>) {
622 for (id, topic) in ast.topics_all() {
623 let _ = id;
624 if !topic.type_.is_resolved() {
625 out.push(violation(
626 topic.node_id,
627 Profile::DdsXml,
628 "topic type resolvable",
629 "DDS-XML 1.0 §7.3",
630 "a topic must name a type the model holds",
631 ));
632 }
633 }
634
635 for (id, profile) in ast.qos_profiles_all() {
637 let mut seen = alloc::collections::BTreeSet::new();
638 let _ = seen.insert(profile.node_id);
639 let mut current = profile
640 .base
641 .as_ref()
642 .and_then(zerodds_ast_core::Ref::resolved);
643 let mut cyclic = false;
644 while let Some(next) = current {
645 let Ok(node) = ast.qos_profile(next) else {
646 break;
647 };
648 if !seen.insert(node.node_id) {
649 cyclic = true;
650 break;
651 }
652 current = node.base.as_ref().and_then(zerodds_ast_core::Ref::resolved);
653 }
654 if cyclic {
655 let _ = id;
656 out.push(violation(
657 profile.node_id,
658 Profile::DdsXml,
659 "acyclic profile inheritance",
660 "DDS-XML 1.0 §7.5",
661 "the profile's base chain closes a cycle",
662 ));
663 }
664 }
665}