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
406const MRID: &str = "IdentifiedObject.mRID";
408
409fn mrid_exempt(t: &Resolved<'_>, prop: &PropShape) -> bool {
419 t.described && prop.path == Path::Forward(MRID)
420}
421
422fn check_prop<'a>(
423 ctx: &Ctx<'a>,
424 el: &'a Fields,
425 class: &str,
426 mrid: &'a str,
427 prop: &PropShape,
428 no_min: bool,
429 out: &mut Vec<Violation>,
430) {
431 let values = resolve(ctx, el, mrid, &prop.path);
432 for c in prop.checks {
433 if no_min && matches!(c.constraint, Constraint::MinCount(_)) {
434 continue;
435 }
436 if fails(ctx, el, &values, &c.constraint) {
437 out.push(violation(mrid, class, &path_label(&prop.path), c));
438 }
439 }
440}
441
442fn branch_conforms<'a>(ctx: &Ctx<'a>, el: &'a Fields, mrid: &'a str, branch: &Branch) -> Option<bool> {
445 let mut holds = true;
446 for prop in branch.props {
447 let values = resolve(ctx, el, mrid, &prop.path);
448 if matches!(values, Values::Unknown) {
449 return None;
450 }
451 if prop.checks.iter().any(|c| fails(ctx, el, &values, &c.constraint)) {
452 holds = false;
453 break;
454 }
455 }
456 Some(holds != branch.negate)
457}
458
459fn check_logic<'a>(
462 ctx: &Ctx<'a>,
463 el: &'a Fields,
464 class: &str,
465 mrid: &'a str,
466 logic: &Logic,
467 out: &mut Vec<Violation>,
468) {
469 let mut conforming = 0usize;
470 for branch in logic.branches {
471 match branch_conforms(ctx, el, mrid, branch) {
472 Some(true) => conforming += 1,
473 Some(false) => {}
474 None => return,
476 }
477 }
478 let holds = match logic.op {
479 LogicOp::And => conforming == logic.branches.len(),
480 LogicOp::Or => conforming >= 1,
481 LogicOp::Xone => conforming == 1,
482 };
483 if !holds {
484 out.push(Violation {
485 object_id: mrid.to_string(),
486 rule_id: logic.rule_id.to_string(),
487 class: class.to_string(),
488 property: logic.name.to_string(),
489 message: logic.message.to_string(),
490 severity: logic.severity.to_string(),
491 name: logic.name.to_string(),
492 description: logic.description.to_string(),
493 });
494 }
495}
496
497fn pair(values: &Values, el: &Fields, other: &str, holds: impl Fn(f64, f64) -> bool) -> bool {
499 let others = field_values(el, other);
500 values.any_text(|a| {
501 number(a).is_some_and(|a| others.any_text(|b| number(b).is_some_and(|b| !holds(a, b))))
502 })
503}
504
505fn check_closed(el: &Fields, class: &str, mrid: &str, closed: &ClosedShape, out: &mut Vec<Violation>) {
507 let mut extra: Vec<&str> = el
508 .keys()
509 .copied()
510 .filter(|k| !closed.allowed.contains(k))
511 .collect();
512 extra.sort_unstable();
515
516 for field in extra {
517 out.push(Violation {
518 object_id: mrid.to_string(),
519 rule_id: closed.rule_id.to_string(),
520 class: class.to_string(),
521 property: field.to_string(),
522 message: closed.message.to_string(),
523 severity: closed.severity.to_string(),
524 name: closed.name.to_string(),
525 description: closed.description.to_string(),
526 });
527 }
528}
529
530#[derive(Clone, Copy)]
534struct Resolved<'a> {
535 mrid: &'a String,
536 class: &'static str,
537 fields: &'a Fields,
538 described: bool,
539}
540
541impl<'a> Resolved<'a> {
542 fn new(source: Source, mrid: &'a String, entry: &'a Element, fields: &'a Fields) -> Self {
543 let described = source == Source::Cgmes && !entry.defined();
544 Resolved { mrid, class: entry.type_name(), fields, described }
545 }
546}
547
548fn resolve_target<'a>(ds: &'a CimDataset, source: Source, mrid: &'a String) -> Option<Resolved<'a>> {
552 let entry = ds.entries.get(mrid)?;
553 let fields = source.fields(entry)?;
554 Some(Resolved::new(source, mrid, entry, fields))
555}
556
557fn build_indexes<'a>(
568 ds: &'a CimDataset,
569 source: Source,
570 count_fields: &[&'static str],
571 list_fields: &[&'static str],
572 subject_fields: &[&'static str],
573) -> (ReverseIndex<'a>, FastMap<&'static str, Vec<Resolved<'a>>>) {
574 if count_fields.is_empty() && list_fields.is_empty() && subject_fields.is_empty() {
575 return (ReverseIndex::default(), FastMap::default());
576 }
577 type Partial<'a> = (ReverseIndex<'a>, FastMap<&'static str, Vec<Resolved<'a>>>);
578 let index = |entries: &mut dyn Iterator<Item = (&'a String, &'a Element)>| -> Partial<'a> {
579 let mut reverse = ReverseIndex::default();
580 let mut subjects: FastMap<&'static str, Vec<Resolved<'a>>> =
581 subject_fields.iter().map(|f| (*f, Vec::new())).collect();
582 for (mrid, entry) in entries {
583 let Some(f) = source.fields(entry) else { continue };
584 for field in count_fields {
585 for target in refs_of(f, field) {
586 *reverse.counts.entry((target.as_str(), *field)).or_insert(0) += 1;
587 }
588 }
589 for field in list_fields {
590 for target in refs_of(f, field) {
591 reverse.sources.entry((target.as_str(), *field)).or_default().push(mrid);
592 }
593 }
594 for field in subject_fields {
595 if f.contains_key(*field) {
596 let r = Resolved::new(source, mrid, entry, f);
597 subjects.get_mut(field).expect("seeded above").push(r);
598 }
599 }
600 }
601 (reverse, subjects)
602 };
603
604 let threads = threads_for(ds.entries.len());
605 if threads == 1 {
606 return index(&mut ds.entries.iter());
607 }
608 let all: Vec<(&'a String, &'a Element)> = ds.entries.iter().collect();
612 let parts: Vec<Partial<'a>> = par_map(&runs(&all, threads, |_| 1), |run| index(&mut run.iter().copied()));
613 let mut parts = parts.into_iter();
614 let (mut reverse, mut subjects) = parts.next().expect("at least one run");
615 for (r, subj) in parts {
616 for (k, n) in r.counts {
617 *reverse.counts.entry(k).or_insert(0) += n;
618 }
619 for (k, list) in r.sources {
620 reverse.sources.entry(k).or_default().extend(list);
621 }
622 for (field, list) in subj {
623 subjects.get_mut(field).expect("seeded above").extend(list);
624 }
625 }
626 (reverse, subjects)
627}
628
629fn targets_of<'a>(
637 ds: &'a CimDataset,
638 source: Source,
639 shape: &ShapeDef,
640 subjects: &FastMap<&'static str, Vec<Resolved<'a>>>,
641 by_class: &mut FastMap<&'static str, Vec<Resolved<'a>>>,
642) -> Vec<Resolved<'a>> {
643 let mut out: Vec<Resolved<'a>> = Vec::new();
644 for target in shape.targets {
645 match target {
646 Target::Class(classes) => {
647 for class in *classes {
648 let list = by_class.entry(class).or_insert_with(|| {
649 ds.by_type
650 .get(*class)
651 .into_iter()
652 .flatten()
653 .filter_map(|mrid| resolve_target(ds, source, mrid))
654 .collect()
655 });
656 out.extend_from_slice(list);
657 }
658 }
659 Target::SubjectsOf(field) => {
660 out.extend_from_slice(subjects.get(field).map_or(&[], Vec::as_slice));
661 }
662 }
663 }
664 let mut seen = FastSet::default();
668 out.retain(|t| seen.insert(t.mrid));
669 out
670}
671
672fn run_shape<'a>(ctx: &Ctx<'a>, shape: &ShapeDef, t: &Resolved<'a>, out: &mut Vec<Violation>) {
674 for prop in shape.props {
675 check_prop(ctx, t.fields, t.class, t.mrid, prop, mrid_exempt(t, prop), out);
676 }
677 for logic in shape.logic {
678 check_logic(ctx, t.fields, t.class, t.mrid, logic, out);
679 }
680 if let Some(closed) = shape.closed {
681 check_closed(t.fields, t.class, t.mrid, closed, out);
682 }
683}
684
685pub fn validate_profile(
691 ds: &CimDataset,
692 profile: &str,
693 shapes: &'static [ShapeDef],
694 _cfg: &Config,
695) -> Vec<Violation> {
696 let active: Vec<&ShapeDef> = shapes
697 .iter()
698 .filter(|s| s.profiles.contains(&profile))
699 .collect();
700 validate_shapes(ds, Source::Nc, &active)
701}
702
703const SLICE: usize = 2048;
705
706pub fn validate_shapes(ds: &CimDataset, source: Source, active: &[&ShapeDef]) -> Vec<Violation> {
715 if active.is_empty() {
716 return Vec::new();
717 }
718
719 let mut count_fields: Vec<&'static str> = Vec::new();
722 let mut list_fields: Vec<&'static str> = Vec::new();
723 let paths = active.iter().flat_map(|s| {
724 s.props
725 .iter()
726 .map(|p| &p.path)
727 .chain(s.logic.iter().flat_map(|l| l.branches.iter().flat_map(|b| b.props.iter().map(|p| &p.path))))
728 });
729 for path in paths {
730 match path {
731 Path::Inverse(f) => count_fields.push(f),
732 Path::Alternative(branches) => count_fields.extend(branches.iter().filter_map(|b| match b {
733 AltBranch::Inverse(f) => Some(*f),
734 AltBranch::Forward(_) => None,
735 })),
736 Path::Chain(steps) => list_fields.extend(steps.iter().filter_map(|s| match s {
737 Step::Inverse(f) => Some(*f),
738 _ => None,
739 })),
740 Path::Forward(_) => {}
741 }
742 }
743 count_fields.sort_unstable();
744 count_fields.dedup();
745 list_fields.sort_unstable();
746 list_fields.dedup();
747
748 let mut subject_fields: Vec<&'static str> = active
749 .iter()
750 .flat_map(|s| s.targets.iter())
751 .filter_map(|t| match t {
752 Target::SubjectsOf(f) => Some(*f),
753 Target::Class(_) => None,
754 })
755 .collect();
756 subject_fields.sort_unstable();
757 subject_fields.dedup();
758
759 let (reverse, subjects) = build_indexes(ds, source, &count_fields, &list_fields, &subject_fields);
760 let ctx = Ctx { ds, source, reverse };
761
762 let mut by_class_shapes: FastMap<&'static str, Vec<&ShapeDef>> = FastMap::default();
773 let mut by_subject_shapes: FastMap<&'static str, Vec<&ShapeDef>> = FastMap::default();
774 let mut mixed: Vec<&ShapeDef> = Vec::new();
775 for shape in active {
776 let all_class = shape.targets.iter().all(|t| matches!(t, Target::Class(_)));
777 match shape.targets {
778 _ if all_class => {
779 let mut classes: Vec<&'static str> = shape
780 .targets
781 .iter()
782 .flat_map(|t| match t {
783 Target::Class(cs) => cs.iter().copied(),
784 Target::SubjectsOf(_) => [].iter().copied(),
785 })
786 .collect();
787 classes.sort_unstable();
788 classes.dedup();
789 for class in classes {
790 by_class_shapes.entry(class).or_default().push(shape);
791 }
792 }
793 [Target::SubjectsOf(field)] => by_subject_shapes.entry(field).or_default().push(shape),
794 _ => mixed.push(shape),
795 }
796 }
797
798 let work: Vec<(&[&ShapeDef], &[String])> = by_class_shapes
805 .iter()
806 .flat_map(|(class, shapes)| {
807 ds.by_type
808 .get(*class)
809 .map_or(&[][..], Vec::as_slice)
810 .chunks(SLICE)
811 .map(move |mrids| (shapes.as_slice(), mrids))
812 })
813 .collect();
814 let total: usize = work.iter().map(|(_, m)| m.len()).sum();
815 let mut out = par_concat(&runs(&work, threads_for(total), |(_, m)| m.len()), |items| {
816 let mut out = Vec::new();
817 for (shapes, mrids) in items {
818 for t in mrids.iter().filter_map(|mrid| resolve_target(ds, source, mrid)) {
819 for shape in *shapes {
820 run_shape(&ctx, shape, &t, &mut out);
821 }
822 }
823 }
824 out
825 });
826
827 let work: Vec<(&[&ShapeDef], &[Resolved])> = by_subject_shapes
829 .iter()
830 .flat_map(|(field, shapes)| {
831 subjects
832 .get(field)
833 .map_or(&[][..], Vec::as_slice)
834 .chunks(SLICE)
835 .map(move |targets| (shapes.as_slice(), targets))
836 })
837 .collect();
838 let total: usize = work.iter().map(|(_, t)| t.len()).sum();
839 out.extend(par_concat(&runs(&work, threads_for(total), |(_, t)| t.len()), |items| {
840 let mut out = Vec::new();
841 for (shapes, targets) in items {
842 for t in *targets {
843 for shape in *shapes {
844 run_shape(&ctx, shape, t, &mut out);
845 }
846 }
847 }
848 out
849 }));
850 let mut by_class: FastMap<&'static str, Vec<Resolved>> = FastMap::default();
851 for shape in mixed {
852 for t in targets_of(ds, source, shape, &subjects, &mut by_class) {
853 run_shape(&ctx, shape, &t, &mut out);
854 }
855 }
856
857 let mut seen = FastSet::default();
862 let keep: Vec<bool> = out
863 .iter()
864 .map(|v| seen.insert((v.object_id.as_str(), v.rule_id.as_str(), v.property.as_str(), v.message.as_str())))
865 .collect();
866 drop(seen);
867 let mut keep = keep.into_iter();
868 out.retain(|_| keep.next().unwrap_or(true));
869 out
870}