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 set: &'a [CimDataset],
58 source: Source,
59 reverse: ReverseIndex<'a>,
60}
61
62#[derive(Clone, Copy, PartialEq, Eq)]
64enum Kind {
65 Text,
66 Refs,
67}
68
69enum Values<'a> {
71 Text(&'a [String]),
74 Refs(&'a [String]),
76 Count(u32),
78 Absent,
79 Chain(Kind, Vec<&'a str>),
82 Unknown,
85}
86
87impl<'a> Values<'a> {
88 fn count(&self) -> Option<u32> {
90 Some(match self {
91 Values::Text(v) | Values::Refs(v) => v.len() as u32,
92 Values::Count(n) => *n,
93 Values::Absent => 0,
94 Values::Chain(_, v) => v.len() as u32,
95 Values::Unknown => return None,
96 })
97 }
98
99 fn any_text(&self, mut f: impl FnMut(&str) -> bool) -> bool {
101 match self {
102 Values::Text(vs) => vs.iter().any(|v| f(v)),
103 Values::Chain(Kind::Text, vs) => vs.iter().any(|v| f(v)),
104 _ => false,
105 }
106 }
107
108 fn any_ref(&self, mut f: impl FnMut(&str) -> bool) -> bool {
110 match self {
111 Values::Refs(vs) => vs.iter().any(|v| f(v)),
112 Values::Chain(Kind::Refs, vs) => vs.iter().any(|v| f(v)),
113 _ => false,
114 }
115 }
116
117 fn is_text(&self) -> bool {
118 matches!(self, Values::Text(_) | Values::Chain(Kind::Text, _))
119 }
120
121 fn is_refs(&self) -> bool {
122 matches!(self, Values::Refs(_) | Values::Chain(Kind::Refs, _))
123 }
124}
125
126type Fields = FieldMap;
127
128#[derive(Debug, Clone, Copy, PartialEq, Eq)]
136pub enum Source {
137 Nc,
139 Cgmes,
141}
142
143impl Source {
144 fn fields(self, entry: &Element) -> Option<&Fields> {
146 self.owns(entry).then(|| entry.fields())
147 }
148
149 fn owns(self, entry: &Element) -> bool {
152 entry.type_name().starts_with("nc:") == (self == Source::Nc)
153 }
154}
155
156fn refs_of<'a>(f: &'a Fields, field: &str) -> &'a [String] {
157 match f.get(field) {
158 Some(FieldValue::Resource(s)) => std::slice::from_ref(s),
159 Some(FieldValue::ResourceList(v)) => v.as_slice(),
160 _ => &[],
161 }
162}
163
164fn field_values<'a>(f: &'a Fields, field: &str) -> Values<'a> {
165 match f.get(field) {
166 Some(FieldValue::Text(s)) => Values::Text(std::slice::from_ref(s)),
167 Some(FieldValue::TextList(v)) => Values::Text(v.as_slice()),
168 Some(FieldValue::Resource(_)) | Some(FieldValue::ResourceList(_)) => {
169 Values::Refs(refs_of(f, field))
170 }
171 None => Values::Absent,
172 }
173}
174
175fn number(v: &str) -> Option<f64> {
178 v.trim().parse::<f64>().ok()
179}
180
181fn resolve<'a>(ctx: &Ctx<'a>, el: &'a Fields, mrid: &'a str, path: &Path) -> Values<'a> {
182 match path {
183 Path::Forward(field) => field_values(el, field),
184 Path::Chain([Step::Forward(field), Step::Type]) => field_values(el, field),
187 Path::Inverse(field) => Values::Count(ctx.reverse.count(mrid, field)),
188 Path::Alternative(branches) => {
189 let mut total = 0;
193 for b in *branches {
194 total += match b {
195 AltBranch::Forward(field) => field_values(el, field).count().unwrap_or(0),
196 AltBranch::Inverse(field) => ctx.reverse.count(mrid, field),
197 };
198 }
199 Values::Count(total)
200 }
201 Path::Chain(steps) => chain(ctx, el, mrid, steps),
202 }
203}
204
205fn chain<'a>(ctx: &Ctx<'a>, focus: &'a Fields, mrid: &'a str, steps: &[Step]) -> Values<'a> {
214 let mut nodes: Vec<&'a str> = vec![mrid];
215 for (i, step) in steps.iter().enumerate() {
216 let last = i + 1 == steps.len();
217 match step {
218 Step::Forward(field) => {
219 let mut next: Vec<&'a str> = Vec::new();
220 let mut texts: Vec<&'a str> = Vec::new();
221 for &node in &nodes {
222 let fields = if node == mrid {
223 focus
224 } else {
225 match ctx.ds.entries.get(node).and_then(|e| ctx.source.fields(e)) {
226 Some(f) => f,
227 None => return Values::Unknown,
228 }
229 };
230 match fields.get(*field) {
231 Some(FieldValue::Text(s)) => texts.push(s),
232 Some(FieldValue::TextList(v)) => texts.extend(v.iter().map(String::as_str)),
233 Some(FieldValue::Resource(r)) => next.push(r),
234 Some(FieldValue::ResourceList(v)) => next.extend(v.iter().map(String::as_str)),
235 None => {}
236 }
237 }
238 let before_type = matches!(steps.get(i + 1), Some(Step::Type));
241 if (last || before_type) && !texts.is_empty() {
242 return Values::Chain(Kind::Text, texts);
243 }
244 if last {
245 return Values::Chain(Kind::Refs, next);
246 }
247 nodes = next;
248 }
249 Step::Inverse(field) => {
250 let next: Vec<&'a str> =
251 nodes.iter().flat_map(|n| ctx.reverse.sources(n, field).iter().copied()).collect();
252 if last {
253 return Values::Chain(Kind::Refs, next);
254 }
255 nodes = next;
256 }
257 Step::Type => return Values::Chain(Kind::Refs, nodes),
261 }
262 }
263 Values::Chain(Kind::Refs, nodes)
264}
265
266fn datatype_ok(s: &str, xsd: &str) -> bool {
271 let t = s.trim();
272 match xsd {
273 "string" | "normalizedString" | "token" | "Name" | "NCName" | "language" => true,
274 "boolean" => matches!(t, "true" | "false" | "1" | "0"),
275 "integer" | "int" | "long" | "short" | "byte" | "nonPositiveInteger"
276 | "negativeInteger" => t.parse::<i64>().is_ok(),
277 "nonNegativeInteger" | "positiveInteger" | "unsignedInt" | "unsignedLong"
278 | "unsignedShort" | "unsignedByte" => t.parse::<u64>().is_ok(),
279 "decimal" | "float" | "double" => t.parse::<f64>().is_ok(),
280 "dateTime" | "dateTimeStamp" => helpers::is_xsd_datetime(t),
281 "date" => helpers::is_xsd_date(t),
282 "gMonthDay" => helpers::is_xsd_gmonthday(t),
283 "anyURI" => helpers::is_xsd_anyuri(t),
284 _ => true,
286 }
287}
288
289fn violation(mrid: &str, class: &str, property: &str, c: &Check) -> Violation {
290 Violation {
291 object_id: mrid.to_string(),
292 rule_id: c.rule_id.to_string(),
293 class: class.to_string(),
294 property: property.to_string(),
295 message: c.message.to_string(),
296 severity: c.severity.to_string(),
297 name: c.name.to_string(),
298 description: c.description.to_string(),
299 }
300}
301
302fn path_label(path: &Path) -> std::borrow::Cow<'static, str> {
305 use std::borrow::Cow;
306 let inverse = |f: &str| Cow::Owned(format!("^{f}"));
309 match path {
310 Path::Forward(f) => Cow::Borrowed(f),
311 Path::Inverse(f) => inverse(f),
312 Path::Alternative(branches) => match branches.first() {
313 Some(AltBranch::Forward(f)) => Cow::Borrowed(f),
314 Some(AltBranch::Inverse(f)) => inverse(f),
315 None => Cow::Borrowed(""),
316 },
317 Path::Chain(steps) => match steps.first() {
318 Some(Step::Forward(f)) => Cow::Borrowed(f),
319 Some(Step::Inverse(f)) => inverse(f),
320 _ => Cow::Borrowed(""),
321 },
322 }
323}
324
325fn any_type(ctx: &Ctx<'_>, mrid: &str, test: impl Fn(&cimmodel::base::ClassDef) -> bool) -> Option<bool> {
346 if let Some(e) = ctx.ds.entries.get(mrid) {
347 return Some(e.types().any(test));
348 }
349 if ctx.source != Source::Nc {
350 return None;
351 }
352 let mut held = false;
353 for e in ctx.set.iter().filter_map(|d| d.entries.get(mrid)) {
354 held = true;
355 if e.types().any(&test) {
356 return Some(true);
357 }
358 }
359 held.then_some(false)
360}
361
362fn fails(ctx: &Ctx<'_>, el: &Fields, values: &Values<'_>, constraint: &Constraint) -> bool {
364 if matches!(values, Values::Unknown) {
365 return false;
366 }
367 match constraint {
368 Constraint::MinCount(n) => values.count().is_some_and(|c| c < *n),
369 Constraint::MaxCount(n) => values.count().is_some_and(|c| c > *n),
370
371 Constraint::Datatype(xsd) => values.any_text(|v| !datatype_ok(v, xsd)),
375
376 Constraint::NodeKind(kind) => match kind {
379 NodeKind::Literal => values.is_refs() && values.count() != Some(0),
380 NodeKind::Iri => values.is_text() && values.count() != Some(0),
381 NodeKind::BlankNode => values.is_text() && values.count() != Some(0),
384 },
385
386 Constraint::In(allowed) => {
387 values.any_text(|v| !allowed.contains(&v.trim())) || values.any_ref(|r| !allowed.contains(&r))
388 }
389 Constraint::HasValue(want) => match values {
390 v if v.is_text() => !v.any_text(|t| t.trim() == *want),
391 v if v.is_refs() => !v.any_ref(|r| r == *want),
392 _ => false,
393 },
394
395 Constraint::MaxLength(n) => values.any_text(|v| v.chars().count() as u32 > *n),
396 Constraint::MinLength(n) => values.any_text(|v| (v.chars().count() as u32) < *n),
397 Constraint::Length(n) => values.any_text(|v| v.chars().count() as u32 != *n),
398
399 Constraint::Class(allowed) => {
404 values.any_ref(|r| any_type(ctx, r, |t| allowed.contains(&t.qualified)) == Some(false))
405 }
406 Constraint::RefClass(allowed) => {
407 values.any_ref(|r| any_type(ctx, r, |t| !allowed.contains(&t.qualified)) == Some(true))
408 }
409 Constraint::NotClass(forbidden) => {
410 values.any_ref(|r| any_type(ctx, r, |t| forbidden.contains(&t.qualified)) == Some(true))
411 }
412 Constraint::QualifiedIn { allowed, min } => {
413 let mut n = 0u32;
414 values.any_text(|v| {
415 n += u32::from(allowed.contains(&v.trim()));
416 false
417 });
418 values.any_ref(|r| {
419 n += u32::from(allowed.contains(&r));
420 false
421 });
422 n < *min
423 }
424
425 Constraint::MinInclusive(b) => values.any_text(|v| number(v).is_some_and(|x| x < *b)),
426 Constraint::MaxInclusive(b) => values.any_text(|v| number(v).is_some_and(|x| x > *b)),
427 Constraint::MinExclusive(b) => values.any_text(|v| number(v).is_some_and(|x| x <= *b)),
428 Constraint::MaxExclusive(b) => values.any_text(|v| number(v).is_some_and(|x| x >= *b)),
429
430 Constraint::LessThan(other) => pair(values, el, other, |a, b| a < b),
433 Constraint::LessThanOrEquals(other) => pair(values, el, other, |a, b| a <= b),
434 }
435}
436
437const MRID: &str = "IdentifiedObject.mRID";
439
440fn mrid_exempt(t: &Resolved<'_>, prop: &PropShape) -> bool {
450 t.described && prop.path == Path::Forward(MRID)
451}
452
453fn check_prop<'a>(
454 ctx: &Ctx<'a>,
455 el: &'a Fields,
456 class: &str,
457 mrid: &'a str,
458 prop: &PropShape,
459 no_min: bool,
460 out: &mut Vec<Violation>,
461) {
462 let values = resolve(ctx, el, mrid, &prop.path);
463 for c in prop.checks {
464 if no_min && matches!(c.constraint, Constraint::MinCount(_)) {
465 continue;
466 }
467 if fails(ctx, el, &values, &c.constraint) {
468 out.push(violation(mrid, class, &path_label(&prop.path), c));
469 }
470 }
471}
472
473fn branch_conforms<'a>(ctx: &Ctx<'a>, el: &'a Fields, mrid: &'a str, branch: &Branch) -> Option<bool> {
476 let mut holds = true;
477 for prop in branch.props {
478 let values = resolve(ctx, el, mrid, &prop.path);
479 if matches!(values, Values::Unknown) {
480 return None;
481 }
482 if prop.checks.iter().any(|c| fails(ctx, el, &values, &c.constraint)) {
483 holds = false;
484 break;
485 }
486 }
487 Some(holds != branch.negate)
488}
489
490fn check_logic<'a>(
493 ctx: &Ctx<'a>,
494 el: &'a Fields,
495 class: &str,
496 mrid: &'a str,
497 logic: &Logic,
498 out: &mut Vec<Violation>,
499) {
500 let mut conforming = 0usize;
501 for branch in logic.branches {
502 match branch_conforms(ctx, el, mrid, branch) {
503 Some(true) => conforming += 1,
504 Some(false) => {}
505 None => return,
507 }
508 }
509 let holds = match logic.op {
510 LogicOp::And => conforming == logic.branches.len(),
511 LogicOp::Or => conforming >= 1,
512 LogicOp::Xone => conforming == 1,
513 };
514 if !holds {
515 out.push(Violation {
516 object_id: mrid.to_string(),
517 rule_id: logic.rule_id.to_string(),
518 class: class.to_string(),
519 property: logic.name.to_string(),
520 message: logic.message.to_string(),
521 severity: logic.severity.to_string(),
522 name: logic.name.to_string(),
523 description: logic.description.to_string(),
524 });
525 }
526}
527
528fn pair(values: &Values, el: &Fields, other: &str, holds: impl Fn(f64, f64) -> bool) -> bool {
530 let others = field_values(el, other);
531 values.any_text(|a| {
532 number(a).is_some_and(|a| others.any_text(|b| number(b).is_some_and(|b| !holds(a, b))))
533 })
534}
535
536fn check_closed(el: &Fields, class: &str, mrid: &str, closed: &ClosedShape, out: &mut Vec<Violation>) {
538 let mut extra: Vec<&str> = el
539 .keys()
540 .copied()
541 .filter(|k| !closed.allowed.contains(k))
542 .collect();
543 extra.sort_unstable();
546
547 for field in extra {
548 out.push(Violation {
549 object_id: mrid.to_string(),
550 rule_id: closed.rule_id.to_string(),
551 class: class.to_string(),
552 property: field.to_string(),
553 message: closed.message.to_string(),
554 severity: closed.severity.to_string(),
555 name: closed.name.to_string(),
556 description: closed.description.to_string(),
557 });
558 }
559}
560
561#[derive(Clone, Copy)]
565struct Resolved<'a> {
566 mrid: &'a String,
567 class: &'static str,
568 fields: &'a Fields,
569 described: bool,
570}
571
572impl<'a> Resolved<'a> {
573 fn new(source: Source, mrid: &'a String, entry: &'a Element, fields: &'a Fields) -> Self {
574 let described = source == Source::Cgmes && !entry.defined();
575 Resolved { mrid, class: entry.type_name(), fields, described }
576 }
577}
578
579fn resolve_target<'a>(ds: &'a CimDataset, source: Source, mrid: &'a String) -> Option<Resolved<'a>> {
583 let entry = ds.entries.get(mrid)?;
584 let fields = source.fields(entry)?;
585 Some(Resolved::new(source, mrid, entry, fields))
586}
587
588fn build_indexes<'a>(
599 ds: &'a CimDataset,
600 source: Source,
601 count_fields: &[&'static str],
602 list_fields: &[&'static str],
603 subject_fields: &[&'static str],
604) -> (ReverseIndex<'a>, FastMap<&'static str, Vec<Resolved<'a>>>) {
605 if count_fields.is_empty() && list_fields.is_empty() && subject_fields.is_empty() {
606 return (ReverseIndex::default(), FastMap::default());
607 }
608 type Partial<'a> = (ReverseIndex<'a>, FastMap<&'static str, Vec<Resolved<'a>>>);
609 let index = |entries: &mut dyn Iterator<Item = (&'a String, &'a Element)>| -> Partial<'a> {
610 let mut reverse = ReverseIndex::default();
611 let mut subjects: FastMap<&'static str, Vec<Resolved<'a>>> =
612 subject_fields.iter().map(|f| (*f, Vec::new())).collect();
613 for (mrid, entry) in entries {
614 let Some(f) = source.fields(entry) else { continue };
615 for field in count_fields {
616 for target in refs_of(f, field) {
617 *reverse.counts.entry((target.as_str(), *field)).or_insert(0) += 1;
618 }
619 }
620 for field in list_fields {
621 for target in refs_of(f, field) {
622 reverse.sources.entry((target.as_str(), *field)).or_default().push(mrid);
623 }
624 }
625 for field in subject_fields {
626 if f.contains_key(*field) {
627 let r = Resolved::new(source, mrid, entry, f);
628 subjects.get_mut(field).expect("seeded above").push(r);
629 }
630 }
631 }
632 (reverse, subjects)
633 };
634
635 let threads = threads_for(ds.entries.len());
636 if threads == 1 {
637 return index(&mut ds.entries.iter());
638 }
639 let all: Vec<(&'a String, &'a Element)> = ds.entries.iter().collect();
643 let parts: Vec<Partial<'a>> = par_map(&runs(&all, threads, |_| 1), |run| index(&mut run.iter().copied()));
644 let mut parts = parts.into_iter();
645 let (mut reverse, mut subjects) = parts.next().expect("at least one run");
646 for (r, subj) in parts {
647 for (k, n) in r.counts {
648 *reverse.counts.entry(k).or_insert(0) += n;
649 }
650 for (k, list) in r.sources {
651 reverse.sources.entry(k).or_default().extend(list);
652 }
653 for (field, list) in subj {
654 subjects.get_mut(field).expect("seeded above").extend(list);
655 }
656 }
657 (reverse, subjects)
658}
659
660fn targets_of<'a>(
668 ds: &'a CimDataset,
669 source: Source,
670 shape: &ShapeDef,
671 subjects: &FastMap<&'static str, Vec<Resolved<'a>>>,
672 by_class: &mut FastMap<&'static str, Vec<Resolved<'a>>>,
673) -> Vec<Resolved<'a>> {
674 let mut out: Vec<Resolved<'a>> = Vec::new();
675 for target in shape.targets {
676 match target {
677 Target::Class(classes) => {
678 for class in *classes {
679 let list = by_class.entry(class).or_insert_with(|| {
680 ds.by_type
681 .get(*class)
682 .into_iter()
683 .flatten()
684 .filter_map(|mrid| resolve_target(ds, source, mrid))
685 .collect()
686 });
687 out.extend_from_slice(list);
688 }
689 }
690 Target::SubjectsOf(field) => {
691 out.extend_from_slice(subjects.get(field).map_or(&[], Vec::as_slice));
692 }
693 }
694 }
695 let mut seen = FastSet::default();
699 out.retain(|t| seen.insert(t.mrid));
700 out
701}
702
703fn run_shape<'a>(ctx: &Ctx<'a>, shape: &ShapeDef, t: &Resolved<'a>, out: &mut Vec<Violation>) {
705 for prop in shape.props {
706 check_prop(ctx, t.fields, t.class, t.mrid, prop, mrid_exempt(t, prop), out);
707 }
708 for logic in shape.logic {
709 check_logic(ctx, t.fields, t.class, t.mrid, logic, out);
710 }
711 if let Some(closed) = shape.closed {
712 check_closed(t.fields, t.class, t.mrid, closed, out);
713 }
714}
715
716pub fn validate_profile(
722 ds: &CimDataset,
723 profile: &str,
724 shapes: &'static [ShapeDef],
725 cfg: &Config,
726) -> Vec<Violation> {
727 validate_profile_in(ds, &[], profile, shapes, cfg)
728}
729
730pub fn validate_profile_in(
733 ds: &CimDataset,
734 set: &[CimDataset],
735 profile: &str,
736 shapes: &'static [ShapeDef],
737 _cfg: &Config,
738) -> Vec<Violation> {
739 let active: Vec<&ShapeDef> = shapes
740 .iter()
741 .filter(|s| s.profiles.contains(&profile))
742 .collect();
743 validate_shapes_in(ds, set, Source::Nc, &active)
744}
745
746const SLICE: usize = 2048;
748
749pub fn validate_shapes(ds: &CimDataset, source: Source, active: &[&ShapeDef]) -> Vec<Violation> {
758 validate_shapes_in(ds, &[], source, active)
759}
760
761pub fn validate_shapes_in(ds: &CimDataset, set: &[CimDataset], source: Source, active: &[&ShapeDef]) -> Vec<Violation> {
766 if active.is_empty() {
767 return Vec::new();
768 }
769
770 let mut count_fields: Vec<&'static str> = Vec::new();
773 let mut list_fields: Vec<&'static str> = Vec::new();
774 let paths = active.iter().flat_map(|s| {
775 s.props
776 .iter()
777 .map(|p| &p.path)
778 .chain(s.logic.iter().flat_map(|l| l.branches.iter().flat_map(|b| b.props.iter().map(|p| &p.path))))
779 });
780 for path in paths {
781 match path {
782 Path::Inverse(f) => count_fields.push(f),
783 Path::Alternative(branches) => count_fields.extend(branches.iter().filter_map(|b| match b {
784 AltBranch::Inverse(f) => Some(*f),
785 AltBranch::Forward(_) => None,
786 })),
787 Path::Chain(steps) => list_fields.extend(steps.iter().filter_map(|s| match s {
788 Step::Inverse(f) => Some(*f),
789 _ => None,
790 })),
791 Path::Forward(_) => {}
792 }
793 }
794 count_fields.sort_unstable();
795 count_fields.dedup();
796 list_fields.sort_unstable();
797 list_fields.dedup();
798
799 let mut subject_fields: Vec<&'static str> = active
800 .iter()
801 .flat_map(|s| s.targets.iter())
802 .filter_map(|t| match t {
803 Target::SubjectsOf(f) => Some(*f),
804 Target::Class(_) => None,
805 })
806 .collect();
807 subject_fields.sort_unstable();
808 subject_fields.dedup();
809
810 let (reverse, subjects) = build_indexes(ds, source, &count_fields, &list_fields, &subject_fields);
811 let ctx = Ctx { ds, set, source, reverse };
812
813 let mut by_class_shapes: FastMap<&'static str, Vec<&ShapeDef>> = FastMap::default();
824 let mut by_subject_shapes: FastMap<&'static str, Vec<&ShapeDef>> = FastMap::default();
825 let mut mixed: Vec<&ShapeDef> = Vec::new();
826 for shape in active {
827 let all_class = shape.targets.iter().all(|t| matches!(t, Target::Class(_)));
828 match shape.targets {
829 _ if all_class => {
830 let mut classes: Vec<&'static str> = shape
831 .targets
832 .iter()
833 .flat_map(|t| match t {
834 Target::Class(cs) => cs.iter().copied(),
835 Target::SubjectsOf(_) => [].iter().copied(),
836 })
837 .collect();
838 classes.sort_unstable();
839 classes.dedup();
840 for class in classes {
841 by_class_shapes.entry(class).or_default().push(shape);
842 }
843 }
844 [Target::SubjectsOf(field)] => by_subject_shapes.entry(field).or_default().push(shape),
845 _ => mixed.push(shape),
846 }
847 }
848
849 let work: Vec<(&[&ShapeDef], &[String])> = by_class_shapes
856 .iter()
857 .flat_map(|(class, shapes)| {
858 ds.by_type
859 .get(*class)
860 .map_or(&[][..], Vec::as_slice)
861 .chunks(SLICE)
862 .map(move |mrids| (shapes.as_slice(), mrids))
863 })
864 .collect();
865 let total: usize = work.iter().map(|(_, m)| m.len()).sum();
866 let mut out = par_concat(&runs(&work, threads_for(total), |(_, m)| m.len()), |items| {
867 let mut out = Vec::new();
868 for (shapes, mrids) in items {
869 for t in mrids.iter().filter_map(|mrid| resolve_target(ds, source, mrid)) {
870 for shape in *shapes {
871 run_shape(&ctx, shape, &t, &mut out);
872 }
873 }
874 }
875 out
876 });
877
878 let work: Vec<(&[&ShapeDef], &[Resolved])> = by_subject_shapes
880 .iter()
881 .flat_map(|(field, shapes)| {
882 subjects
883 .get(field)
884 .map_or(&[][..], Vec::as_slice)
885 .chunks(SLICE)
886 .map(move |targets| (shapes.as_slice(), targets))
887 })
888 .collect();
889 let total: usize = work.iter().map(|(_, t)| t.len()).sum();
890 out.extend(par_concat(&runs(&work, threads_for(total), |(_, t)| t.len()), |items| {
891 let mut out = Vec::new();
892 for (shapes, targets) in items {
893 for t in *targets {
894 for shape in *shapes {
895 run_shape(&ctx, shape, t, &mut out);
896 }
897 }
898 }
899 out
900 }));
901 let mut by_class: FastMap<&'static str, Vec<Resolved>> = FastMap::default();
902 for shape in mixed {
903 for t in targets_of(ds, source, shape, &subjects, &mut by_class) {
904 run_shape(&ctx, shape, &t, &mut out);
905 }
906 }
907
908 let mut seen = FastSet::default();
913 let keep: Vec<bool> = out
914 .iter()
915 .map(|v| seen.insert((v.object_id.as_str(), v.rule_id.as_str(), v.property.as_str(), v.message.as_str())))
916 .collect();
917 drop(seen);
918 let mut keep = keep.into_iter();
919 out.retain(|_| keep.next().unwrap_or(true));
920 out
921}