1use cimmodel::{CimDataset, Element};
14use cimmodel::base::{FastMap, FastSet, FieldMap, FieldValue};
15
16use crate::helpers;
17use crate::par::{par_concat, par_map, runs, threads_for};
18use crate::shapes::{
19 AltBranch, Branch, Check, ClosedShape, Constraint, Logic, LogicOp, NodeKind, Path, PropShape, ShapeDef, Step,
20 Target,
21};
22use crate::{Config, Violation};
23
24#[derive(Default)]
35struct ReverseIndex<'a> {
36 counts: FastMap<(&'a str, &'static str), u32>,
38 sources: FastMap<(&'a str, &'static str), Vec<&'a str>>,
40}
41
42impl<'a> ReverseIndex<'a> {
43 fn count(&self, mrid: &str, field: &'static str) -> u32 {
44 self.counts.get(&(mrid, field)).copied().unwrap_or(0)
45 }
46
47 fn sources<'s>(&'s self, mrid: &'a str, field: &'static str) -> &'s [&'a str] {
48 self.sources.get(&(mrid, field)).map_or(&[], Vec::as_slice)
49 }
50}
51
52struct Ctx<'a> {
54 ds: &'a CimDataset,
55 source: Source,
56 reverse: ReverseIndex<'a>,
57}
58
59#[derive(Clone, Copy, PartialEq, Eq)]
61enum Kind {
62 Text,
63 Refs,
64}
65
66enum Values<'a> {
68 Text(&'a [String]),
71 Refs(&'a [String]),
73 Count(u32),
75 Absent,
76 Chain(Kind, Vec<&'a str>),
79 Unknown,
82}
83
84impl<'a> Values<'a> {
85 fn count(&self) -> Option<u32> {
87 Some(match self {
88 Values::Text(v) | Values::Refs(v) => v.len() as u32,
89 Values::Count(n) => *n,
90 Values::Absent => 0,
91 Values::Chain(_, v) => v.len() as u32,
92 Values::Unknown => return None,
93 })
94 }
95
96 fn any_text(&self, mut f: impl FnMut(&str) -> bool) -> bool {
98 match self {
99 Values::Text(vs) => vs.iter().any(|v| f(v)),
100 Values::Chain(Kind::Text, vs) => vs.iter().any(|v| f(v)),
101 _ => false,
102 }
103 }
104
105 fn any_ref(&self, mut f: impl FnMut(&str) -> bool) -> bool {
107 match self {
108 Values::Refs(vs) => vs.iter().any(|v| f(v)),
109 Values::Chain(Kind::Refs, vs) => vs.iter().any(|v| f(v)),
110 _ => false,
111 }
112 }
113
114 fn is_text(&self) -> bool {
115 matches!(self, Values::Text(_) | Values::Chain(Kind::Text, _))
116 }
117
118 fn is_refs(&self) -> bool {
119 matches!(self, Values::Refs(_) | Values::Chain(Kind::Refs, _))
120 }
121}
122
123type Fields = FieldMap;
124
125#[derive(Debug, Clone, Copy, PartialEq, Eq)]
133pub enum Source {
134 Nc,
136 Cgmes,
138}
139
140impl Source {
141 fn fields(self, entry: &Element) -> Option<&Fields> {
143 self.owns(entry).then(|| entry.fields())
144 }
145
146 fn owns(self, entry: &Element) -> bool {
149 entry.type_name().starts_with("nc:") == (self == Source::Nc)
150 }
151}
152
153fn refs_of<'a>(f: &'a Fields, field: &str) -> &'a [String] {
154 match f.get(field) {
155 Some(FieldValue::Resource(s)) => std::slice::from_ref(s),
156 Some(FieldValue::ResourceList(v)) => v.as_slice(),
157 _ => &[],
158 }
159}
160
161fn field_values<'a>(f: &'a Fields, field: &str) -> Values<'a> {
162 match f.get(field) {
163 Some(FieldValue::Text(s)) => Values::Text(std::slice::from_ref(s)),
164 Some(FieldValue::TextList(v)) => Values::Text(v.as_slice()),
165 Some(FieldValue::Resource(_)) | Some(FieldValue::ResourceList(_)) => {
166 Values::Refs(refs_of(f, field))
167 }
168 None => Values::Absent,
169 }
170}
171
172fn number(v: &str) -> Option<f64> {
175 v.trim().parse::<f64>().ok()
176}
177
178fn resolve<'a>(ctx: &Ctx<'a>, el: &'a Fields, mrid: &'a str, path: &Path) -> Values<'a> {
179 match path {
180 Path::Forward(field) => field_values(el, field),
181 Path::Chain([Step::Forward(field), Step::Type]) => field_values(el, field),
184 Path::Inverse(field) => Values::Count(ctx.reverse.count(mrid, field)),
185 Path::Alternative(branches) => {
186 let mut total = 0;
190 for b in *branches {
191 total += match b {
192 AltBranch::Forward(field) => field_values(el, field).count().unwrap_or(0),
193 AltBranch::Inverse(field) => ctx.reverse.count(mrid, field),
194 };
195 }
196 Values::Count(total)
197 }
198 Path::Chain(steps) => chain(ctx, el, mrid, steps),
199 }
200}
201
202fn chain<'a>(ctx: &Ctx<'a>, focus: &'a Fields, mrid: &'a str, steps: &[Step]) -> Values<'a> {
211 let mut nodes: Vec<&'a str> = vec![mrid];
212 for (i, step) in steps.iter().enumerate() {
213 let last = i + 1 == steps.len();
214 match step {
215 Step::Forward(field) => {
216 let mut next: Vec<&'a str> = Vec::new();
217 let mut texts: Vec<&'a str> = Vec::new();
218 for &node in &nodes {
219 let fields = if node == mrid {
220 focus
221 } else {
222 match ctx.ds.entries.get(node).and_then(|e| ctx.source.fields(e)) {
223 Some(f) => f,
224 None => return Values::Unknown,
225 }
226 };
227 match fields.get(*field) {
228 Some(FieldValue::Text(s)) => texts.push(s),
229 Some(FieldValue::TextList(v)) => texts.extend(v.iter().map(String::as_str)),
230 Some(FieldValue::Resource(r)) => next.push(r),
231 Some(FieldValue::ResourceList(v)) => next.extend(v.iter().map(String::as_str)),
232 None => {}
233 }
234 }
235 let before_type = matches!(steps.get(i + 1), Some(Step::Type));
238 if (last || before_type) && !texts.is_empty() {
239 return Values::Chain(Kind::Text, texts);
240 }
241 if last {
242 return Values::Chain(Kind::Refs, next);
243 }
244 nodes = next;
245 }
246 Step::Inverse(field) => {
247 let next: Vec<&'a str> =
248 nodes.iter().flat_map(|n| ctx.reverse.sources(n, field).iter().copied()).collect();
249 if last {
250 return Values::Chain(Kind::Refs, next);
251 }
252 nodes = next;
253 }
254 Step::Type => return Values::Chain(Kind::Refs, nodes),
258 }
259 }
260 Values::Chain(Kind::Refs, nodes)
261}
262
263fn datatype_ok(s: &str, xsd: &str) -> bool {
268 let t = s.trim();
269 match xsd {
270 "string" | "normalizedString" | "token" | "Name" | "NCName" | "language" => true,
271 "boolean" => matches!(t, "true" | "false" | "1" | "0"),
272 "integer" | "int" | "long" | "short" | "byte" | "nonPositiveInteger"
273 | "negativeInteger" => t.parse::<i64>().is_ok(),
274 "nonNegativeInteger" | "positiveInteger" | "unsignedInt" | "unsignedLong"
275 | "unsignedShort" | "unsignedByte" => t.parse::<u64>().is_ok(),
276 "decimal" | "float" | "double" => t.parse::<f64>().is_ok(),
277 "dateTime" | "dateTimeStamp" => helpers::is_xsd_datetime(t),
278 "date" => helpers::is_xsd_date(t),
279 "gMonthDay" => helpers::is_xsd_gmonthday(t),
280 "anyURI" => helpers::is_xsd_anyuri(t),
281 _ => true,
283 }
284}
285
286fn violation(mrid: &str, class: &str, property: &str, c: &Check) -> Violation {
287 Violation {
288 object_id: mrid.to_string(),
289 rule_id: c.rule_id.to_string(),
290 class: class.to_string(),
291 property: property.to_string(),
292 message: c.message.to_string(),
293 severity: c.severity.to_string(),
294 name: c.name.to_string(),
295 description: c.description.to_string(),
296 }
297}
298
299fn path_label(path: &Path) -> std::borrow::Cow<'static, str> {
302 use std::borrow::Cow;
303 let inverse = |f: &str| Cow::Owned(format!("^{f}"));
306 match path {
307 Path::Forward(f) => Cow::Borrowed(f),
308 Path::Inverse(f) => inverse(f),
309 Path::Alternative(branches) => match branches.first() {
310 Some(AltBranch::Forward(f)) => Cow::Borrowed(f),
311 Some(AltBranch::Inverse(f)) => inverse(f),
312 None => Cow::Borrowed(""),
313 },
314 Path::Chain(steps) => match steps.first() {
315 Some(Step::Forward(f)) => Cow::Borrowed(f),
316 Some(Step::Inverse(f)) => inverse(f),
317 _ => Cow::Borrowed(""),
318 },
319 }
320}
321
322fn class_of<'a>(ctx: &Ctx<'a>, mrid: &str) -> Option<&'a cimmodel::Element> {
328 ctx.ds.entries.get(mrid)
329}
330
331fn fails(ctx: &Ctx<'_>, el: &Fields, values: &Values<'_>, constraint: &Constraint) -> bool {
333 if matches!(values, Values::Unknown) {
334 return false;
335 }
336 match constraint {
337 Constraint::MinCount(n) => values.count().is_some_and(|c| c < *n),
338 Constraint::MaxCount(n) => values.count().is_some_and(|c| c > *n),
339
340 Constraint::Datatype(xsd) => values.any_text(|v| !datatype_ok(v, xsd)),
344
345 Constraint::NodeKind(kind) => match kind {
348 NodeKind::Literal => values.is_refs() && values.count() != Some(0),
349 NodeKind::Iri => values.is_text() && values.count() != Some(0),
350 NodeKind::BlankNode => values.is_text() && values.count() != Some(0),
353 },
354
355 Constraint::In(allowed) => {
356 values.any_text(|v| !allowed.contains(&v.trim())) || values.any_ref(|r| !allowed.contains(&r))
357 }
358 Constraint::HasValue(want) => match values {
359 v if v.is_text() => !v.any_text(|t| t.trim() == *want),
360 v if v.is_refs() => !v.any_ref(|r| r == *want),
361 _ => false,
362 },
363
364 Constraint::MaxLength(n) => values.any_text(|v| v.chars().count() as u32 > *n),
365 Constraint::MinLength(n) => values.any_text(|v| (v.chars().count() as u32) < *n),
366 Constraint::Length(n) => values.any_text(|v| v.chars().count() as u32 != *n),
367
368 Constraint::Class(allowed) => values.any_ref(|r| {
373 class_of(ctx, r).is_some_and(|e| !e.types().any(|t| allowed.contains(&t.qualified)))
374 }),
375 Constraint::RefClass(allowed) => values.any_ref(|r| {
376 class_of(ctx, r).is_some_and(|e| e.types().any(|t| !allowed.contains(&t.qualified)))
377 }),
378 Constraint::NotClass(forbidden) => values.any_ref(|r| {
379 class_of(ctx, r).is_some_and(|e| e.types().any(|t| forbidden.contains(&t.qualified)))
380 }),
381 Constraint::QualifiedIn { allowed, min } => {
382 let mut n = 0u32;
383 values.any_text(|v| {
384 n += u32::from(allowed.contains(&v.trim()));
385 false
386 });
387 values.any_ref(|r| {
388 n += u32::from(allowed.contains(&r));
389 false
390 });
391 n < *min
392 }
393
394 Constraint::MinInclusive(b) => values.any_text(|v| number(v).is_some_and(|x| x < *b)),
395 Constraint::MaxInclusive(b) => values.any_text(|v| number(v).is_some_and(|x| x > *b)),
396 Constraint::MinExclusive(b) => values.any_text(|v| number(v).is_some_and(|x| x <= *b)),
397 Constraint::MaxExclusive(b) => values.any_text(|v| number(v).is_some_and(|x| x >= *b)),
398
399 Constraint::LessThan(other) => pair(values, el, other, |a, b| a < b),
402 Constraint::LessThanOrEquals(other) => pair(values, el, other, |a, b| a <= b),
403 }
404}
405
406fn check_prop<'a>(
407 ctx: &Ctx<'a>,
408 el: &'a Fields,
409 class: &str,
410 mrid: &'a str,
411 prop: &PropShape,
412 out: &mut Vec<Violation>,
413) {
414 let values = resolve(ctx, el, mrid, &prop.path);
415 for c in prop.checks {
416 if fails(ctx, el, &values, &c.constraint) {
417 out.push(violation(mrid, class, &path_label(&prop.path), c));
418 }
419 }
420}
421
422fn branch_conforms<'a>(ctx: &Ctx<'a>, el: &'a Fields, mrid: &'a str, branch: &Branch) -> Option<bool> {
425 let mut holds = true;
426 for prop in branch.props {
427 let values = resolve(ctx, el, mrid, &prop.path);
428 if matches!(values, Values::Unknown) {
429 return None;
430 }
431 if prop.checks.iter().any(|c| fails(ctx, el, &values, &c.constraint)) {
432 holds = false;
433 break;
434 }
435 }
436 Some(holds != branch.negate)
437}
438
439fn check_logic<'a>(
442 ctx: &Ctx<'a>,
443 el: &'a Fields,
444 class: &str,
445 mrid: &'a str,
446 logic: &Logic,
447 out: &mut Vec<Violation>,
448) {
449 let mut conforming = 0usize;
450 for branch in logic.branches {
451 match branch_conforms(ctx, el, mrid, branch) {
452 Some(true) => conforming += 1,
453 Some(false) => {}
454 None => return,
456 }
457 }
458 let holds = match logic.op {
459 LogicOp::And => conforming == logic.branches.len(),
460 LogicOp::Or => conforming >= 1,
461 LogicOp::Xone => conforming == 1,
462 };
463 if !holds {
464 out.push(Violation {
465 object_id: mrid.to_string(),
466 rule_id: logic.rule_id.to_string(),
467 class: class.to_string(),
468 property: logic.name.to_string(),
469 message: logic.message.to_string(),
470 severity: logic.severity.to_string(),
471 name: logic.name.to_string(),
472 description: logic.description.to_string(),
473 });
474 }
475}
476
477fn pair(values: &Values, el: &Fields, other: &str, holds: impl Fn(f64, f64) -> bool) -> bool {
479 let others = field_values(el, other);
480 values.any_text(|a| {
481 number(a).is_some_and(|a| others.any_text(|b| number(b).is_some_and(|b| !holds(a, b))))
482 })
483}
484
485fn check_closed(el: &Fields, class: &str, mrid: &str, closed: &ClosedShape, out: &mut Vec<Violation>) {
487 let mut extra: Vec<&str> = el
488 .keys()
489 .copied()
490 .filter(|k| !closed.allowed.contains(k))
491 .collect();
492 extra.sort_unstable();
495
496 for field in extra {
497 out.push(Violation {
498 object_id: mrid.to_string(),
499 rule_id: closed.rule_id.to_string(),
500 class: class.to_string(),
501 property: field.to_string(),
502 message: closed.message.to_string(),
503 severity: closed.severity.to_string(),
504 name: closed.name.to_string(),
505 description: closed.description.to_string(),
506 });
507 }
508}
509
510#[derive(Clone, Copy)]
513struct Resolved<'a> {
514 mrid: &'a String,
515 class: &'static str,
516 fields: &'a Fields,
517}
518
519fn resolve_target<'a>(ds: &'a CimDataset, source: Source, mrid: &'a String) -> Option<Resolved<'a>> {
523 let entry = ds.entries.get(mrid)?;
524 let fields = source.fields(entry)?;
525 Some(Resolved { mrid, class: entry.type_name(), fields })
526}
527
528fn build_indexes<'a>(
539 ds: &'a CimDataset,
540 source: Source,
541 count_fields: &[&'static str],
542 list_fields: &[&'static str],
543 subject_fields: &[&'static str],
544) -> (ReverseIndex<'a>, FastMap<&'static str, Vec<Resolved<'a>>>) {
545 if count_fields.is_empty() && list_fields.is_empty() && subject_fields.is_empty() {
546 return (ReverseIndex::default(), FastMap::default());
547 }
548 type Partial<'a> = (ReverseIndex<'a>, FastMap<&'static str, Vec<Resolved<'a>>>);
549 let index = |entries: &mut dyn Iterator<Item = (&'a String, &'a Element)>| -> Partial<'a> {
550 let mut reverse = ReverseIndex::default();
551 let mut subjects: FastMap<&'static str, Vec<Resolved<'a>>> =
552 subject_fields.iter().map(|f| (*f, Vec::new())).collect();
553 for (mrid, entry) in entries {
554 let Some(f) = source.fields(entry) else { continue };
555 for field in count_fields {
556 for target in refs_of(f, field) {
557 *reverse.counts.entry((target.as_str(), *field)).or_insert(0) += 1;
558 }
559 }
560 for field in list_fields {
561 for target in refs_of(f, field) {
562 reverse.sources.entry((target.as_str(), *field)).or_default().push(mrid);
563 }
564 }
565 for field in subject_fields {
566 if f.contains_key(*field) {
567 let r = Resolved { mrid, class: entry.type_name(), fields: f };
568 subjects.get_mut(field).expect("seeded above").push(r);
569 }
570 }
571 }
572 (reverse, subjects)
573 };
574
575 let threads = threads_for(ds.entries.len());
576 if threads == 1 {
577 return index(&mut ds.entries.iter());
578 }
579 let all: Vec<(&'a String, &'a Element)> = ds.entries.iter().collect();
583 let parts: Vec<Partial<'a>> = par_map(&runs(&all, threads, |_| 1), |run| index(&mut run.iter().copied()));
584 let mut parts = parts.into_iter();
585 let (mut reverse, mut subjects) = parts.next().expect("at least one run");
586 for (r, subj) in parts {
587 for (k, n) in r.counts {
588 *reverse.counts.entry(k).or_insert(0) += n;
589 }
590 for (k, list) in r.sources {
591 reverse.sources.entry(k).or_default().extend(list);
592 }
593 for (field, list) in subj {
594 subjects.get_mut(field).expect("seeded above").extend(list);
595 }
596 }
597 (reverse, subjects)
598}
599
600fn targets_of<'a>(
608 ds: &'a CimDataset,
609 source: Source,
610 shape: &ShapeDef,
611 subjects: &FastMap<&'static str, Vec<Resolved<'a>>>,
612 by_class: &mut FastMap<&'static str, Vec<Resolved<'a>>>,
613) -> Vec<Resolved<'a>> {
614 let mut out: Vec<Resolved<'a>> = Vec::new();
615 for target in shape.targets {
616 match target {
617 Target::Class(classes) => {
618 for class in *classes {
619 let list = by_class.entry(class).or_insert_with(|| {
620 ds.by_type
621 .get(*class)
622 .into_iter()
623 .flatten()
624 .filter_map(|mrid| resolve_target(ds, source, mrid))
625 .collect()
626 });
627 out.extend_from_slice(list);
628 }
629 }
630 Target::SubjectsOf(field) => {
631 out.extend_from_slice(subjects.get(field).map_or(&[], Vec::as_slice));
632 }
633 }
634 }
635 let mut seen = FastSet::default();
639 out.retain(|t| seen.insert(t.mrid));
640 out
641}
642
643fn run_shape<'a>(ctx: &Ctx<'a>, shape: &ShapeDef, t: &Resolved<'a>, out: &mut Vec<Violation>) {
645 for prop in shape.props {
646 check_prop(ctx, t.fields, t.class, t.mrid, prop, out);
647 }
648 for logic in shape.logic {
649 check_logic(ctx, t.fields, t.class, t.mrid, logic, out);
650 }
651 if let Some(closed) = shape.closed {
652 check_closed(t.fields, t.class, t.mrid, closed, out);
653 }
654}
655
656pub fn validate_profile(
662 ds: &CimDataset,
663 profile: &str,
664 shapes: &'static [ShapeDef],
665 _cfg: &Config,
666) -> Vec<Violation> {
667 let active: Vec<&ShapeDef> = shapes
668 .iter()
669 .filter(|s| s.profiles.contains(&profile))
670 .collect();
671 validate_shapes(ds, Source::Nc, &active)
672}
673
674const SLICE: usize = 2048;
676
677pub fn validate_shapes(ds: &CimDataset, source: Source, active: &[&ShapeDef]) -> Vec<Violation> {
686 if active.is_empty() {
687 return Vec::new();
688 }
689
690 let mut count_fields: Vec<&'static str> = Vec::new();
693 let mut list_fields: Vec<&'static str> = Vec::new();
694 let paths = active.iter().flat_map(|s| {
695 s.props
696 .iter()
697 .map(|p| &p.path)
698 .chain(s.logic.iter().flat_map(|l| l.branches.iter().flat_map(|b| b.props.iter().map(|p| &p.path))))
699 });
700 for path in paths {
701 match path {
702 Path::Inverse(f) => count_fields.push(f),
703 Path::Alternative(branches) => count_fields.extend(branches.iter().filter_map(|b| match b {
704 AltBranch::Inverse(f) => Some(*f),
705 AltBranch::Forward(_) => None,
706 })),
707 Path::Chain(steps) => list_fields.extend(steps.iter().filter_map(|s| match s {
708 Step::Inverse(f) => Some(*f),
709 _ => None,
710 })),
711 Path::Forward(_) => {}
712 }
713 }
714 count_fields.sort_unstable();
715 count_fields.dedup();
716 list_fields.sort_unstable();
717 list_fields.dedup();
718
719 let mut subject_fields: Vec<&'static str> = active
720 .iter()
721 .flat_map(|s| s.targets.iter())
722 .filter_map(|t| match t {
723 Target::SubjectsOf(f) => Some(*f),
724 Target::Class(_) => None,
725 })
726 .collect();
727 subject_fields.sort_unstable();
728 subject_fields.dedup();
729
730 let (reverse, subjects) = build_indexes(ds, source, &count_fields, &list_fields, &subject_fields);
731 let ctx = Ctx { ds, source, reverse };
732
733 let mut by_class_shapes: FastMap<&'static str, Vec<&ShapeDef>> = FastMap::default();
744 let mut by_subject_shapes: FastMap<&'static str, Vec<&ShapeDef>> = FastMap::default();
745 let mut mixed: Vec<&ShapeDef> = Vec::new();
746 for shape in active {
747 let all_class = shape.targets.iter().all(|t| matches!(t, Target::Class(_)));
748 match shape.targets {
749 _ if all_class => {
750 let mut classes: Vec<&'static str> = shape
751 .targets
752 .iter()
753 .flat_map(|t| match t {
754 Target::Class(cs) => cs.iter().copied(),
755 Target::SubjectsOf(_) => [].iter().copied(),
756 })
757 .collect();
758 classes.sort_unstable();
759 classes.dedup();
760 for class in classes {
761 by_class_shapes.entry(class).or_default().push(shape);
762 }
763 }
764 [Target::SubjectsOf(field)] => by_subject_shapes.entry(field).or_default().push(shape),
765 _ => mixed.push(shape),
766 }
767 }
768
769 let work: Vec<(&[&ShapeDef], &[String])> = by_class_shapes
776 .iter()
777 .flat_map(|(class, shapes)| {
778 ds.by_type
779 .get(*class)
780 .map_or(&[][..], Vec::as_slice)
781 .chunks(SLICE)
782 .map(move |mrids| (shapes.as_slice(), mrids))
783 })
784 .collect();
785 let total: usize = work.iter().map(|(_, m)| m.len()).sum();
786 let mut out = par_concat(&runs(&work, threads_for(total), |(_, m)| m.len()), |items| {
787 let mut out = Vec::new();
788 for (shapes, mrids) in items {
789 for t in mrids.iter().filter_map(|mrid| resolve_target(ds, source, mrid)) {
790 for shape in *shapes {
791 run_shape(&ctx, shape, &t, &mut out);
792 }
793 }
794 }
795 out
796 });
797
798 let work: Vec<(&[&ShapeDef], &[Resolved])> = by_subject_shapes
800 .iter()
801 .flat_map(|(field, shapes)| {
802 subjects
803 .get(field)
804 .map_or(&[][..], Vec::as_slice)
805 .chunks(SLICE)
806 .map(move |targets| (shapes.as_slice(), targets))
807 })
808 .collect();
809 let total: usize = work.iter().map(|(_, t)| t.len()).sum();
810 out.extend(par_concat(&runs(&work, threads_for(total), |(_, t)| t.len()), |items| {
811 let mut out = Vec::new();
812 for (shapes, targets) in items {
813 for t in *targets {
814 for shape in *shapes {
815 run_shape(&ctx, shape, t, &mut out);
816 }
817 }
818 }
819 out
820 }));
821 let mut by_class: FastMap<&'static str, Vec<Resolved>> = FastMap::default();
822 for shape in mixed {
823 for t in targets_of(ds, source, shape, &subjects, &mut by_class) {
824 run_shape(&ctx, shape, &t, &mut out);
825 }
826 }
827
828 let mut seen = FastSet::default();
833 let keep: Vec<bool> = out
834 .iter()
835 .map(|v| seen.insert((v.object_id.as_str(), v.rule_id.as_str(), v.property.as_str(), v.message.as_str())))
836 .collect();
837 drop(seen);
838 let mut keep = keep.into_iter();
839 out.retain(|_| keep.next().unwrap_or(true));
840 out
841}