1use alloc::collections::BTreeMap;
7use alloc::format;
8use alloc::string::{String, ToString};
9use alloc::vec::Vec;
10
11use crate::ir::{Block, Chart, Exec, ExecKind, Param, Span, State, StateElement, StateKind};
12
13#[derive(Clone, Copy, Debug, PartialEq, Eq)]
15pub enum InitialSource {
16 Attribute,
18 Element,
20 FirstChild,
22 None,
24}
25
26#[derive(Clone, Debug, PartialEq)]
28pub struct ResolvedInitial<'a> {
29 pub targets: Vec<&'a str>,
31 pub source: InitialSource,
33 pub body: Option<&'a Block>,
35}
36
37pub fn resolve_initial(state: &State) -> ResolvedInitial<'_> {
41 if state.kind() != StateKind::Compound {
42 return ResolvedInitial {
43 targets: Vec::new(),
44 source: InitialSource::None,
45 body: None,
46 };
47 }
48 if !state.initial.is_empty() {
49 return ResolvedInitial {
50 targets: state.initial.iter().map(String::as_str).collect(),
51 source: InitialSource::Attribute,
52 body: None,
53 };
54 }
55 if let Some(init) = &state.initial_element {
56 return ResolvedInitial {
57 targets: init.transition.targets.iter().map(String::as_str).collect(),
58 source: InitialSource::Element,
59 body: Some(&init.transition.body),
60 };
61 }
62 first_child(&state.children)
63}
64
65pub fn resolve_root_initial(chart: &Chart) -> ResolvedInitial<'_> {
67 if !chart.initial.is_empty() {
68 return ResolvedInitial {
69 targets: chart.initial.iter().map(String::as_str).collect(),
70 source: InitialSource::Attribute,
71 body: None,
72 };
73 }
74 first_child(&chart.states)
75}
76
77fn first_child(children: &[State]) -> ResolvedInitial<'_> {
78 match children
79 .iter()
80 .find(|c| !matches!(c.element, StateElement::History(_)))
81 {
82 Some(c) => ResolvedInitial {
83 targets: alloc::vec![c.id.as_str()],
84 source: InitialSource::FirstChild,
85 body: None,
86 },
87 None => ResolvedInitial {
88 targets: Vec::new(),
89 source: InitialSource::None,
90 body: None,
91 },
92 }
93}
94
95#[derive(Clone, Debug, PartialEq, Eq)]
97pub enum NodeRef {
98 Root,
100 State {
102 id: String,
104 },
105 Transition {
107 source: String,
109 index: usize,
111 },
112 Initial {
114 state: String,
116 },
117 Data {
119 id: String,
121 },
122 Invoke {
124 state: String,
126 index: usize,
128 },
129 Exec {
131 state: String,
133 },
134}
135
136#[derive(Clone, Debug, PartialEq, Eq)]
138pub enum ViolationKind {
139 EmptyChart,
141 MissingId,
143 DuplicateStateId {
145 id: String,
147 },
148 UnknownTarget {
150 target: String,
152 },
153 IllegalInitialTarget {
156 target: String,
158 },
159 InitialAttributeAndElement,
161 InitialOnNonCompound,
163 IllegalInitialTransition,
165 HistoryTransitionCount,
167 IllegalHistoryTransition,
169 IllegalHistoryTarget {
171 target: String,
173 },
174 HistoryAtRoot,
176 HistoryContent,
178 FinalContent {
180 what: String,
182 },
183 DoneDataOutsideFinal,
185 ParallelHistoryOnly,
187 StateHistoryOnly,
189 IllegalTargetSet {
191 first: String,
193 second: String,
195 },
196 HistoryDefaultCycle {
201 history: String,
203 },
204 DuplicateDataId {
206 id: String,
208 },
209 ConflictingAttributes {
211 element: String,
213 attributes: String,
215 },
216}
217
218#[derive(Clone, Debug, PartialEq, Eq)]
220pub struct Violation {
221 pub kind: ViolationKind,
223 pub node: NodeRef,
225 pub span: Span,
227}
228
229impl core::fmt::Display for NodeRef {
230 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
231 match self {
232 NodeRef::Root => f.write_str("the root"),
233 NodeRef::State { id } => write!(f, "state `{id}`"),
234 NodeRef::Transition { source, index } => {
235 write!(f, "transition {index} of state `{source}`")
236 }
237 NodeRef::Initial { state } => write!(f, "the initial of state `{state}`"),
238 NodeRef::Data { id } => write!(f, "data `{id}`"),
239 NodeRef::Invoke { state, index } => write!(f, "invoke {index} of state `{state}`"),
240 NodeRef::Exec { state } => write!(f, "executable content in state `{state}`"),
241 }
242 }
243}
244
245impl core::fmt::Display for ViolationKind {
246 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
247 match self {
248 ViolationKind::EmptyChart => f.write_str("the chart has no states"),
249 ViolationKind::MissingId => f.write_str("a state has no identifier"),
250 ViolationKind::DuplicateStateId { id } => write!(f, "duplicate state id `{id}`"),
251 ViolationKind::UnknownTarget { target } => write!(f, "unknown target `{target}`"),
252 ViolationKind::IllegalInitialTarget { target } => {
253 write!(f, "initial target `{target}` is not a proper descendant")
254 }
255 ViolationKind::InitialAttributeAndElement => {
256 f.write_str("both an initial attribute and an <initial> element")
257 }
258 ViolationKind::InitialOnNonCompound => f.write_str("initial on a non-compound state"),
259 ViolationKind::IllegalInitialTransition => {
260 f.write_str("an <initial> transition needs a target and no event or condition")
261 }
262 ViolationKind::HistoryTransitionCount => {
263 f.write_str("a history state has more than one transition")
264 }
265 ViolationKind::IllegalHistoryTransition => {
266 f.write_str("a history default transition needs a target and no event or condition")
267 }
268 ViolationKind::IllegalHistoryTarget { target } => {
269 write!(
270 f,
271 "history default target `{target}` is outside the history's parent"
272 )
273 }
274 ViolationKind::HistoryAtRoot => f.write_str("a history state at the root"),
275 ViolationKind::HistoryContent => {
276 f.write_str("a history state with content other than its default transition")
277 }
278 ViolationKind::FinalContent { what } => write!(f, "a final state contains {what}"),
279 ViolationKind::DoneDataOutsideFinal => f.write_str("donedata on a non-final state"),
280 ViolationKind::ParallelHistoryOnly => {
281 f.write_str("a parallel state whose children are all history states")
282 }
283 ViolationKind::StateHistoryOnly => {
284 f.write_str("a state whose children are all history states")
285 }
286 ViolationKind::IllegalTargetSet { first, second } => {
287 write!(
288 f,
289 "targets `{first}` and `{second}` cannot be active together"
290 )
291 }
292 ViolationKind::HistoryDefaultCycle { history } => {
293 write!(
294 f,
295 "the default entry of history `{history}` never reaches a state"
296 )
297 }
298 ViolationKind::DuplicateDataId { id } => write!(f, "duplicate data id `{id}`"),
299 ViolationKind::ConflictingAttributes {
300 element,
301 attributes,
302 } => write!(f, "<{element}>: conflicting or missing {attributes}"),
303 }
304 }
305}
306
307impl core::fmt::Display for Violation {
308 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
309 write!(f, "{}: {}", self.node, self.kind)
310 }
311}
312
313impl core::error::Error for Violation {}
314
315#[derive(Clone, Debug, PartialEq, Eq)]
322pub struct Violations(pub Vec<Violation>);
323
324impl From<Vec<Violation>> for Violations {
325 fn from(v: Vec<Violation>) -> Self {
326 Violations(v)
327 }
328}
329
330impl core::fmt::Display for Violations {
331 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
332 for (i, v) in self.0.iter().enumerate() {
333 if i > 0 {
334 f.write_str("\n")?;
335 }
336 write!(f, "{v}")?;
337 }
338 Ok(())
339 }
340}
341
342impl core::error::Error for Violations {}
343
344pub(crate) struct Flat<'a> {
346 pub states: Vec<&'a State>,
348 pub parent: Vec<Option<usize>>,
350 pub last: Vec<usize>,
352 pub by_id: BTreeMap<&'a str, usize>,
354}
355
356impl<'a> Flat<'a> {
357 pub(crate) fn new(chart: &'a Chart) -> Self {
358 let mut f = Flat {
359 states: Vec::new(),
360 parent: Vec::new(),
361 last: Vec::new(),
362 by_id: BTreeMap::new(),
363 };
364 for s in &chart.states {
365 f.push(s, None);
366 }
367 f
368 }
369
370 fn push(&mut self, s: &'a State, parent: Option<usize>) -> usize {
371 let i = self.states.len();
372 self.states.push(s);
373 self.parent.push(parent);
374 self.last.push(i);
375 self.by_id.entry(s.id.as_str()).or_insert(i);
376 for c in &s.children {
377 let l = self.push(c, Some(i));
378 self.last[i] = l;
379 }
380 self.last[i]
381 }
382
383 pub(crate) fn is_descendant(&self, d: usize, a: usize) -> bool {
385 a < d && d <= self.last[a]
386 }
387}
388
389pub fn validate(chart: &Chart) -> Result<(), Vec<Violation>> {
392 let flat = Flat::new(chart);
393 let mut v = Vec::new();
394 let mut push = |kind: ViolationKind, node: NodeRef, span: Span| {
395 v.push(Violation { kind, node, span });
396 };
397
398 if chart.states.is_empty() {
399 push(ViolationKind::EmptyChart, NodeRef::Root, chart.span);
400 }
401
402 let mut seen: BTreeMap<&str, usize> = BTreeMap::new();
404 for s in &flat.states {
405 if s.id.is_empty() {
406 push(
407 ViolationKind::MissingId,
408 NodeRef::State { id: String::new() },
409 s.span,
410 );
411 continue;
412 }
413 let n = seen.entry(s.id.as_str()).or_insert(0);
414 *n += 1;
415 if *n == 2 {
416 push(
417 ViolationKind::DuplicateStateId { id: s.id.clone() },
418 NodeRef::State { id: s.id.clone() },
419 s.span,
420 );
421 }
422 }
423
424 for t in &chart.initial {
426 if !flat.by_id.contains_key(t.as_str()) {
427 push(
428 ViolationKind::UnknownTarget { target: t.clone() },
429 NodeRef::Root,
430 chart.span,
431 );
432 }
433 }
434 check_target_set(&flat, &chart.initial, NodeRef::Root, chart.span, &mut push);
435 for s in &chart.states {
436 if matches!(s.element, StateElement::History(_)) {
437 push(
438 ViolationKind::HistoryAtRoot,
439 NodeRef::State { id: s.id.clone() },
440 s.span,
441 );
442 }
443 }
444
445 check_history_defaults(&flat, &mut push);
446
447 let mut data_seen: BTreeMap<&str, usize> = BTreeMap::new();
449 let all_data = chart
450 .data
451 .iter()
452 .chain(flat.states.iter().flat_map(|s| s.data.iter()));
453 for d in all_data {
454 let n = data_seen.entry(d.id.as_str()).or_insert(0);
455 *n += 1;
456 if *n == 2 {
457 push(
458 ViolationKind::DuplicateDataId { id: d.id.clone() },
459 NodeRef::Data { id: d.id.clone() },
460 d.span,
461 );
462 }
463 let given = [d.src.is_some() || d.content.is_some(), d.expr.is_some()]
466 .iter()
467 .filter(|b| **b)
468 .count();
469 if given > 1 {
470 push(
471 conflict("data", "src, expr, content"),
472 NodeRef::Data { id: d.id.clone() },
473 d.span,
474 );
475 }
476 }
477
478 for (i, s) in flat.states.iter().enumerate() {
479 let sid = || NodeRef::State { id: s.id.clone() };
480 let kind = s.kind();
481
482 let has_initial = !s.initial.is_empty() || s.initial_element.is_some();
484 if has_initial && kind != StateKind::Compound {
485 push(ViolationKind::InitialOnNonCompound, sid(), s.span);
486 }
487 if !s.initial.is_empty() && s.initial_element.is_some() {
488 push(
489 ViolationKind::InitialAttributeAndElement,
490 NodeRef::Initial {
491 state: s.id.clone(),
492 },
493 s.span,
494 );
495 }
496 let init_node = || NodeRef::Initial {
497 state: s.id.clone(),
498 };
499 for t in &s.initial {
500 match flat.by_id.get(t.as_str()) {
501 None => push(
502 ViolationKind::UnknownTarget { target: t.clone() },
503 init_node(),
504 s.span,
505 ),
506 Some(&ti) if !flat.is_descendant(ti, i) => push(
507 ViolationKind::IllegalInitialTarget { target: t.clone() },
508 init_node(),
509 s.span,
510 ),
511 _ => {}
512 }
513 }
514 check_target_set(&flat, &s.initial, init_node(), s.span, &mut push);
515 if let Some(init) = &s.initial_element {
516 let tr = &init.transition;
517 if !tr.events.is_empty() || tr.cond.is_some() || tr.targets.is_empty() {
518 push(
519 ViolationKind::IllegalInitialTransition,
520 init_node(),
521 init.span,
522 );
523 }
524 for t in &tr.targets {
525 match flat.by_id.get(t.as_str()) {
526 None => push(
527 ViolationKind::UnknownTarget { target: t.clone() },
528 init_node(),
529 init.span,
530 ),
531 Some(&ti) if !flat.is_descendant(ti, i) => push(
532 ViolationKind::IllegalInitialTarget { target: t.clone() },
533 init_node(),
534 init.span,
535 ),
536 _ => {}
537 }
538 }
539 check_target_set(&flat, &tr.targets, init_node(), init.span, &mut push);
540 check_block(&tr.body, &s.id, &mut push);
541 }
542
543 for (ti, t) in s.transitions.iter().enumerate() {
545 let tnode = || NodeRef::Transition {
546 source: s.id.clone(),
547 index: ti,
548 };
549 for target in &t.targets {
550 if !flat.by_id.contains_key(target.as_str()) {
551 push(
552 ViolationKind::UnknownTarget {
553 target: target.clone(),
554 },
555 tnode(),
556 t.span,
557 );
558 }
559 }
560 check_target_set(&flat, &t.targets, tnode(), t.span, &mut push);
561 check_block(&t.body, &s.id, &mut push);
562 }
563
564 match kind {
566 StateKind::History(_) => {
567 if s.transitions.len() > 1 {
568 push(ViolationKind::HistoryTransitionCount, sid(), s.span);
569 }
570 if let Some(t) = s.transitions.first() {
571 if !t.events.is_empty() || t.cond.is_some() || t.targets.is_empty() {
572 push(
573 ViolationKind::IllegalHistoryTransition,
574 NodeRef::Transition {
575 source: s.id.clone(),
576 index: 0,
577 },
578 t.span,
579 );
580 }
581 if let Some(p) = flat.parent[i] {
582 for target in &t.targets {
583 if let Some(&ti) = flat.by_id.get(target.as_str())
584 && !flat.is_descendant(ti, p)
585 {
586 push(
587 ViolationKind::IllegalHistoryTarget {
588 target: target.clone(),
589 },
590 NodeRef::Transition {
591 source: s.id.clone(),
592 index: 0,
593 },
594 t.span,
595 );
596 }
597 }
598 }
599 }
600 if !s.children.is_empty()
601 || !s.onentry.is_empty()
602 || !s.onexit.is_empty()
603 || !s.invokes.is_empty()
604 || !s.data.is_empty()
605 {
606 push(ViolationKind::HistoryContent, sid(), s.span);
607 }
608 }
609 StateKind::Final => {
610 let mut what = Vec::new();
611 if !s.children.is_empty() {
612 what.push("child states");
613 }
614 if !s.transitions.is_empty() {
615 what.push("transitions");
616 }
617 if !s.invokes.is_empty() {
618 what.push("invokes");
619 }
620 if !s.data.is_empty() {
621 what.push("data");
622 }
623 if !what.is_empty() {
624 push(
625 ViolationKind::FinalContent {
626 what: what.join(", "),
627 },
628 sid(),
629 s.span,
630 );
631 }
632 }
633 StateKind::Parallel | StateKind::Compound
634 if !s.children.is_empty()
635 && s.children
636 .iter()
637 .all(|c| matches!(c.element, StateElement::History(_))) =>
638 {
639 let k = if kind == StateKind::Parallel {
643 ViolationKind::ParallelHistoryOnly
644 } else {
645 ViolationKind::StateHistoryOnly
646 };
647 push(k, sid(), s.span);
648 }
649 _ => {}
650 }
651 if s.donedata.is_some() && kind != StateKind::Final {
652 push(ViolationKind::DoneDataOutsideFinal, sid(), s.span);
653 }
654 if let Some(dd) = &s.donedata {
655 if dd.content.is_some() && !dd.params.is_empty() {
656 push(conflict("donedata", "content, param"), sid(), dd.span);
657 }
658 check_params(&dd.params, &s.id, &mut push);
659 }
660
661 for b in s.onentry.iter().chain(s.onexit.iter()) {
663 check_block(b, &s.id, &mut push);
664 }
665 for (ii, inv) in s.invokes.iter().enumerate() {
666 let inode = || NodeRef::Invoke {
667 state: s.id.clone(),
668 index: ii,
669 };
670 let pairs = [
671 (
672 inv.type_.is_some() && inv.typeexpr.is_some(),
673 "type, typeexpr",
674 ),
675 (inv.src.is_some() && inv.srcexpr.is_some(), "src, srcexpr"),
676 (
677 inv.id.is_some() && inv.idlocation.is_some(),
678 "id, idlocation",
679 ),
680 (
684 inv.content.as_ref().is_some_and(|c| {
685 inv.srcexpr.is_some() || (inv.src.is_some() && c.expr.is_some())
686 }),
687 "content, src",
688 ),
689 (
690 !inv.params.is_empty() && !inv.namelist.is_empty(),
691 "param, namelist",
692 ),
693 ];
694 for (bad, attrs) in pairs {
695 if bad {
696 push(conflict("invoke", attrs), inode(), inv.span);
697 }
698 }
699 check_params(&inv.params, &s.id, &mut push);
700 check_block(&inv.finalize, &s.id, &mut push);
701 }
702 }
703
704 if v.is_empty() { Ok(()) } else { Err(v) }
705}
706
707fn conflict(element: &str, attributes: &str) -> ViolationKind {
708 ViolationKind::ConflictingAttributes {
709 element: element.to_string(),
710 attributes: attributes.to_string(),
711 }
712}
713
714fn history_default_edges(flat: &Flat<'_>, h: usize) -> Vec<usize> {
718 let st = flat.states[h];
719 let targets: Vec<&str> = match (st.transitions.first(), flat.parent[h]) {
720 (Some(t), _) => t.targets.iter().map(String::as_str).collect(),
721 (None, Some(p)) => resolve_initial(flat.states[p]).targets,
722 (None, None) => Vec::new(),
723 };
724 targets
725 .iter()
726 .filter_map(|t| flat.by_id.get(t).copied())
727 .filter(|&i| matches!(flat.states[i].element, StateElement::History(_)))
728 .collect()
729}
730
731fn check_history_defaults(flat: &Flat<'_>, push: &mut impl FnMut(ViolationKind, NodeRef, Span)) {
735 let histories: Vec<usize> = (0..flat.states.len())
736 .filter(|&i| matches!(flat.states[i].element, StateElement::History(_)))
737 .collect();
738 for &h in &histories {
739 let mut seen = alloc::vec![false; flat.states.len()];
740 let mut stack = history_default_edges(flat, h);
741 let mut cyclic = false;
742 while let Some(x) = stack.pop() {
743 if x == h {
744 cyclic = true;
745 break;
746 }
747 if !core::mem::replace(&mut seen[x], true) {
748 stack.extend(history_default_edges(flat, x));
749 }
750 }
751 if cyclic {
752 let id = flat.states[h].id.clone();
753 push(
754 ViolationKind::HistoryDefaultCycle {
755 history: id.clone(),
756 },
757 NodeRef::State { id },
758 flat.states[h].span,
759 );
760 }
761 }
762}
763
764fn check_target_set(
769 flat: &Flat<'_>,
770 targets: &[String],
771 node: NodeRef,
772 span: Span,
773 push: &mut impl FnMut(ViolationKind, NodeRef, Span),
774) {
775 let idx: Vec<(usize, &String)> = targets
776 .iter()
777 .filter_map(|t| flat.by_id.get(t.as_str()).map(|i| (*i, t)))
778 .filter_map(|(i, t)| {
779 if matches!(flat.states[i].element, StateElement::History(_)) {
780 flat.parent[i].map(|p| (p, t))
782 } else {
783 Some((i, t))
784 }
785 })
786 .collect();
787 for a in 0..idx.len() {
788 for b in (a + 1)..idx.len() {
789 let (x, xs) = idx[a];
790 let (y, ys) = idx[b];
791 let ok = if x == y || flat.is_descendant(x, y) || flat.is_descendant(y, x) {
792 false
793 } else {
794 let mut anc = flat.parent[x];
796 loop {
797 match anc {
798 Some(p) if flat.is_descendant(y, p) => {
799 break flat.states[p].kind() == StateKind::Parallel;
800 }
801 Some(p) => anc = flat.parent[p],
802 None => break false,
803 }
804 }
805 };
806 if !ok {
807 push(
808 ViolationKind::IllegalTargetSet {
809 first: xs.clone(),
810 second: ys.clone(),
811 },
812 node.clone(),
813 span,
814 );
815 }
816 }
817 }
818}
819
820fn check_params(
821 params: &[Param],
822 state: &str,
823 push: &mut impl FnMut(ViolationKind, NodeRef, Span),
824) {
825 for p in params {
826 if p.expr.is_some() == p.location.is_some() {
827 push(
828 conflict("param", "expr, location"),
829 NodeRef::Exec {
830 state: state.to_string(),
831 },
832 p.span,
833 );
834 }
835 }
836}
837
838fn check_block(block: &Block, state: &str, push: &mut impl FnMut(ViolationKind, NodeRef, Span)) {
839 for e in block {
840 check_exec(e, state, push);
841 }
842}
843
844fn check_exec(e: &Exec, state: &str, push: &mut impl FnMut(ViolationKind, NodeRef, Span)) {
845 let node = || NodeRef::Exec {
846 state: state.to_string(),
847 };
848 match &e.kind {
849 ExecKind::Send(s) => {
850 let pairs = [
851 (
852 s.event.is_some() && s.eventexpr.is_some(),
853 "event, eventexpr",
854 ),
855 (
856 s.target.is_some() && s.targetexpr.is_some(),
857 "target, targetexpr",
858 ),
859 (s.type_.is_some() && s.typeexpr.is_some(), "type, typeexpr"),
860 (s.id.is_some() && s.idlocation.is_some(), "id, idlocation"),
861 (
862 s.delay.is_some() && s.delayexpr.is_some(),
863 "delay, delayexpr",
864 ),
865 (
866 s.content.is_some() && (!s.params.is_empty() || !s.namelist.is_empty()),
867 "content, param, namelist",
868 ),
869 ];
870 for (bad, attrs) in pairs {
871 if bad {
872 push(conflict("send", attrs), node(), e.span);
873 }
874 }
875 check_params(&s.params, state, push);
876 }
877 ExecKind::Cancel { sendid, sendidexpr } if sendid.is_some() == sendidexpr.is_some() => {
878 push(conflict("cancel", "sendid, sendidexpr"), node(), e.span);
879 }
880 ExecKind::Assign { expr, content, .. } if expr.is_some() && content.is_some() => {
881 push(conflict("assign", "expr, content"), node(), e.span);
882 }
883 ExecKind::If { branches } => {
884 for (bi, br) in branches.iter().enumerate() {
885 let is_else = br.cond.is_none();
886 if is_else && bi + 1 != branches.len() {
887 push(
888 conflict("if", &format!("else before branch {}", bi + 1)),
889 node(),
890 br.span,
891 );
892 }
893 if bi == 0 && is_else {
894 push(conflict("if", "cond"), node(), br.span);
895 }
896 check_block(&br.body, state, push);
897 }
898 }
899 ExecKind::Foreach { body, item, .. } => {
900 if item.is_empty() {
901 push(conflict("foreach", "item"), node(), e.span);
902 }
903 check_block(body, state, push);
904 }
905 _ => {}
906 }
907}
908
909#[cfg(test)]
910mod error_tests {
911 use super::*;
912 use alloc::string::ToString;
913 use alloc::vec;
914
915 fn unknown(source: &str, target: &str) -> Violation {
916 Violation {
917 kind: ViolationKind::UnknownTarget {
918 target: target.into(),
919 },
920 node: NodeRef::Transition {
921 source: source.into(),
922 index: 0,
923 },
924 span: Span::default(),
925 }
926 }
927
928 #[test]
929 fn violation_displays_node_and_kind() {
930 let v = unknown("a", "nowhere");
931 assert_eq!(
932 v.to_string(),
933 "transition 0 of state `a`: unknown target `nowhere`"
934 );
935 let _: &dyn core::error::Error = &v;
936 }
937
938 #[test]
939 fn violations_display_one_per_line_and_box_with_question_mark() {
940 let vs = vec![unknown("a", "x"), unknown("b", "y")];
941 let w = Violations::from(vs.clone());
942 assert_eq!(w.0, vs);
943 assert_eq!(
944 w.to_string(),
945 "transition 0 of state `a`: unknown target `x`\ntransition 0 of state `b`: unknown target `y`"
946 );
947 fn run(vs: Vec<Violation>) -> Result<(), alloc::boxed::Box<dyn core::error::Error>> {
948 Err(Violations(vs))?
949 }
950 assert!(run(vs).unwrap_err().to_string().contains("`y`"));
951 }
952}