1use std::cell::OnceCell;
4use std::collections::HashMap;
5
6mod collection;
7mod layout;
8mod local;
9mod metrics;
10mod pairs;
11mod value;
12
13use crate::check::config::{Config, ConfigError, KindRules, RuleSeverity, config_section};
14use crate::check::expr::{
15 self, Atom, Domain, Lhs, LhsExpr, Node, NumberExpr, Op, QuantKind, Rhs, SegmentScope,
16 VerticalLayout,
17};
18use code_moniker_core::core::code_graph::{CodeGraph, DefRecord};
19use code_moniker_core::core::kinds::{KIND_COMMENT, REF_CALLS, REF_METHOD_CALL};
20use code_moniker_core::core::moniker::query::bare_callable_name;
21use code_moniker_core::core::shape::Shape;
22use code_moniker_core::core::uri::{UriConfig, to_uri};
23use code_moniker_core::lang::Lang;
24use code_moniker_workspace::lines::line_range;
25
26use collection::{collection_has_pair_binding, eval_collection_size, eval_collection_subset};
27use layout::eval_vertical_layout;
28use local::{
29 AggregateEval, DomainItem, domain_items, eval_aggregate, eval_entropy, eval_mode,
30 project_def_lhs_value,
31};
32use metrics::eval_metric;
33use pairs::{eval_pair_count, eval_pair_quantifier};
34use value::{Value, apply_op, apply_op_values, number_expr_label};
35
36fn is_call_ref_kind(kind: &[u8]) -> bool {
37 matches!(kind, REF_CALLS | REF_METHOD_CALL)
38}
39
40#[derive(Debug, Clone, serde::Serialize)]
41pub struct Violation {
42 pub rule_id: String,
43 pub severity: RuleSeverity,
44 pub moniker: String,
45 pub kind: String,
46 #[serde(serialize_with = "serialize_lines")]
47 pub lines: (u32, u32),
48 pub message: String,
49 #[serde(skip_serializing_if = "Option::is_none")]
50 pub explanation: Option<String>,
51}
52
53fn serialize_lines<S: serde::Serializer>(v: &(u32, u32), s: S) -> Result<S::Ok, S::Error> {
54 use serde::ser::SerializeTuple;
55 let mut t = s.serialize_tuple(2)?;
56 t.serialize_element(&v.0)?;
57 t.serialize_element(&v.1)?;
58 t.end()
59}
60
61#[cfg(test)]
62pub(crate) fn evaluate(
63 graph: &CodeGraph,
64 source: &str,
65 lang: Lang,
66 cfg: &Config,
67 scheme: &str,
68) -> Result<Vec<Violation>, ConfigError> {
69 let compiled = compile_rules(cfg, lang, scheme)?;
70 Ok(evaluate_compiled(graph, source, lang, scheme, &compiled))
71}
72
73pub fn compile_rules(cfg: &Config, lang: Lang, scheme: &str) -> Result<CompiledRules, ConfigError> {
78 CompiledRules::for_lang(cfg, lang, scheme)
79}
80
81pub fn evaluate_compiled(
82 graph: &CodeGraph,
83 source: &str,
84 lang: Lang,
85 scheme: &str,
86 compiled: &CompiledRules,
87) -> Vec<Violation> {
88 evaluate_compiled_with_requirements(graph, source, lang, scheme, compiled, None)
89}
90
91pub(in crate::check) trait RequirementResolver: Sync {
92 fn exists(&self, pattern: &str, source: &DefRecord, scheme: &str) -> bool;
93 fn descendant_defs<'a>(&'a self, _owner: &DefRecord, _inner: &Domain) -> Vec<&'a DefRecord> {
94 Vec::new()
95 }
96}
97
98pub(in crate::check) fn evaluate_compiled_with_requirements(
99 graph: &CodeGraph,
100 source: &str,
101 lang: Lang,
102 scheme: &str,
103 compiled: &CompiledRules,
104 requirements: Option<&dyn RequirementResolver>,
105) -> Vec<Violation> {
106 let need_doc_anchors = compiled
107 .by_kind
108 .values()
109 .any(|r| r.require_doc_for_vis.is_some())
110 || compiled
111 .by_shape
112 .values()
113 .any(|r| r.require_doc_for_vis.is_some());
114 let ctx = EvalCtx {
115 graph,
116 requirements,
117 source,
118 lang,
119 uri_cfg: UriConfig { scheme },
120 parent_counts: parent_counts_by_kind(graph),
121 children_by_parent: children_by_parent(graph),
122 out_refs_by_source: out_refs_by_source(graph),
123 in_refs_by_target: in_refs_by_target(graph),
124 comment_ends: if need_doc_anchors {
125 comment_end_bytes(graph)
126 } else {
127 Vec::new()
128 },
129 doc_anchors: if need_doc_anchors {
130 doc_anchors_by_def(graph)
131 } else {
132 HashMap::new()
133 },
134 def_index: OnceCell::new(),
135 };
136 let mut out = Vec::new();
137
138 for (idx, d) in graph.defs().enumerate() {
139 let Ok(kind_str) = std::str::from_utf8(&d.kind) else {
140 continue;
141 };
142 let kind_rules = compiled.for_kind(kind_str);
143 if let Some(rules) = kind_rules {
144 let target = RuleTarget {
145 scope: DefScope { record: d, idx },
146 kind: kind_str,
147 };
148 for rule in &rules.rules {
149 eval_rule(rule, d, idx, kind_str, &ctx, &mut out);
150 }
151 check_require_doc_comment(target, rules, &ctx, &mut out);
152 }
153 if let Some(shape) = d.shape()
154 && let Some(rules) = compiled.for_shape(shape)
155 {
156 for rule in &rules.rules {
157 if rule.explicit_id
158 && kind_rules
159 .is_some_and(|kind_rules| kind_rules.has_explicit_rule_id(&rule.id))
160 {
161 continue;
162 }
163 eval_shape_rule(rule, d, idx, kind_str, &ctx, &mut out);
164 }
165 if kind_rules.is_none_or(|kind_rules| kind_rules.require_doc_for_vis.is_none()) {
166 if let Some(rule_id) = &rules.require_doc_rule_id {
167 let target = RuleTarget {
168 scope: DefScope { record: d, idx },
169 kind: kind_str,
170 };
171 check_require_doc_comment_with_id(
172 target,
173 rules,
174 rule_id.clone(),
175 &ctx,
176 &mut out,
177 );
178 }
179 }
180 }
181 }
182
183 for r in graph.refs() {
184 for rule in &compiled.refs {
185 eval_ref_rule(rule, r, graph, &ctx, &mut out);
186 }
187 }
188
189 out
190}
191
192#[derive(Debug, Clone, serde::Serialize)]
193pub struct RuleReport {
194 pub rule_id: String,
195 pub severity: RuleSeverity,
196 pub domain: String,
197 pub evaluated: usize,
198 pub matches: usize,
199 pub violations: usize,
200 #[serde(skip_serializing_if = "Option::is_none")]
201 pub antecedent_matches: Option<usize>,
202 #[serde(skip_serializing_if = "Option::is_none")]
203 pub warning: Option<String>,
204}
205
206pub fn rule_report_compiled(
207 graph: &CodeGraph,
208 source: &str,
209 lang: Lang,
210 scheme: &str,
211 compiled: &CompiledRules,
212) -> Vec<RuleReport> {
213 rule_report_compiled_with_requirements(graph, source, lang, scheme, compiled, None)
214}
215
216pub(in crate::check) fn rule_report_compiled_with_requirements(
217 graph: &CodeGraph,
218 source: &str,
219 lang: Lang,
220 scheme: &str,
221 compiled: &CompiledRules,
222 requirements: Option<&dyn RequirementResolver>,
223) -> Vec<RuleReport> {
224 let need_doc_anchors = compiled
225 .by_kind
226 .values()
227 .any(|r| r.require_doc_for_vis.is_some())
228 || compiled
229 .by_shape
230 .values()
231 .any(|r| r.require_doc_for_vis.is_some());
232 let ctx = EvalCtx {
233 graph,
234 requirements,
235 source,
236 lang,
237 uri_cfg: UriConfig { scheme },
238 parent_counts: parent_counts_by_kind(graph),
239 children_by_parent: children_by_parent(graph),
240 out_refs_by_source: out_refs_by_source(graph),
241 in_refs_by_target: in_refs_by_target(graph),
242 comment_ends: if need_doc_anchors {
243 comment_end_bytes(graph)
244 } else {
245 Vec::new()
246 },
247 doc_anchors: if need_doc_anchors {
248 doc_anchors_by_def(graph)
249 } else {
250 HashMap::new()
251 },
252 def_index: OnceCell::new(),
253 };
254 let mut out = Vec::new();
255 push_kind_rule_reports(&mut out, graph, lang, &ctx, compiled);
256 push_shape_rule_reports(&mut out, graph, &ctx, compiled);
257 push_ref_rule_reports(&mut out, graph, &ctx, compiled);
258 out.sort_by(|a, b| a.rule_id.cmp(&b.rule_id));
259 out
260}
261
262fn push_kind_rule_reports(
263 out: &mut Vec<RuleReport>,
264 graph: &CodeGraph,
265 lang: Lang,
266 ctx: &EvalCtx<'_, '_>,
267 compiled: &CompiledRules,
268) {
269 for (kind, rules) in &compiled.by_kind {
270 for rule in &rules.rules {
271 let mut report = RuleReport::new(rule_id(lang, kind, &rule.id), kind.clone(), rule);
272 for (idx, d) in graph.defs().enumerate() {
273 if d.kind.as_ref() != kind.as_bytes() {
274 continue;
275 }
276 report.evaluated += 1;
277 let premise =
278 implication_premise(rule).map(|premise| eval_node(premise, d, idx, ctx));
279 report.record(eval_node(&rule.root, d, idx, ctx), premise);
280 }
281 out.push(report);
282 }
283 if rules.require_doc_for_vis.is_some() {
284 let mut report = RuleReport::new_require_doc(
285 rule_id(lang, kind, "require_doc_comment"),
286 kind.clone(),
287 );
288 for (idx, d) in graph.defs().enumerate() {
289 if d.kind.as_ref() != kind.as_bytes() {
290 continue;
291 }
292 report.evaluated += 1;
293 report.record(
294 eval_require_doc_comment(d, idx, rules, ctx).map_or(
295 NodeOutcome::NotApplicable,
296 |has_doc| {
297 if has_doc {
298 NodeOutcome::Pass
299 } else {
300 NodeOutcome::Fail(Failure {
301 atom_raw: "require_doc_comment".to_string(),
302 lhs_label: "doc_comment".to_string(),
303 actual: "missing".to_string(),
304 expected: "present".to_string(),
305 def_idx: None,
306 details: None,
307 })
308 }
309 },
310 ),
311 None,
312 );
313 }
314 out.push(report);
315 }
316 }
317}
318
319fn push_shape_rule_reports(
320 out: &mut Vec<RuleReport>,
321 graph: &CodeGraph,
322 ctx: &EvalCtx<'_, '_>,
323 compiled: &CompiledRules,
324) {
325 for (shape, rules) in &compiled.by_shape {
326 for rule in &rules.rules {
327 let mut report =
328 RuleReport::new(rule.rule_id.clone(), format!("shape:{shape} defs"), rule);
329 for (idx, d) in graph.defs().enumerate() {
330 if !def_has_shape(d, shape) {
331 continue;
332 }
333 let Ok(kind_str) = std::str::from_utf8(&d.kind) else {
334 continue;
335 };
336 if compiled.for_kind(kind_str).is_some_and(|kind_rules| {
337 rule.explicit_id && kind_rules.has_explicit_rule_id(&rule.id)
338 }) {
339 continue;
340 }
341 report.evaluated += 1;
342 let premise =
343 implication_premise(rule).map(|premise| eval_node(premise, d, idx, ctx));
344 report.record(eval_node(&rule.root, d, idx, ctx), premise);
345 }
346 out.push(report);
347 }
348 if let Some(rule_id) = &rules.require_doc_rule_id {
349 let mut report =
350 RuleReport::new_require_doc(rule_id.clone(), format!("shape:{shape} defs"));
351 for (idx, d) in graph.defs().enumerate() {
352 if !def_has_shape(d, shape) {
353 continue;
354 }
355 let Ok(kind_str) = std::str::from_utf8(&d.kind) else {
356 continue;
357 };
358 if compiled
359 .for_kind(kind_str)
360 .is_some_and(|kind_rules| kind_rules.require_doc_for_vis.is_some())
361 {
362 continue;
363 }
364 report.evaluated += 1;
365 report.record(
366 eval_require_doc_comment(d, idx, rules, ctx).map_or(
367 NodeOutcome::NotApplicable,
368 |has_doc| {
369 if has_doc {
370 NodeOutcome::Pass
371 } else {
372 NodeOutcome::Fail(Failure {
373 atom_raw: "require_doc_comment".to_string(),
374 lhs_label: "doc_comment".to_string(),
375 actual: "missing".to_string(),
376 expected: "present".to_string(),
377 def_idx: None,
378 details: None,
379 })
380 }
381 },
382 ),
383 None,
384 );
385 }
386 out.push(report);
387 }
388 }
389}
390
391fn push_ref_rule_reports(
392 out: &mut Vec<RuleReport>,
393 graph: &CodeGraph,
394 ctx: &EvalCtx<'_, '_>,
395 compiled: &CompiledRules,
396) {
397 for rule in &compiled.refs {
398 let mut report = RuleReport::new(rule.rule_id.clone(), "refs".to_string(), rule);
399 for r in graph.refs() {
400 report.evaluated += 1;
401 let premise = implication_premise(rule).map(|premise| eval_ref_node(premise, r, ctx));
402 report.record(eval_ref_node(&rule.root, r, ctx), premise);
403 }
404 out.push(report);
405 }
406}
407
408impl RuleReport {
409 fn new(rule_id: String, domain: String, rule: &CompiledRule) -> Self {
410 Self {
411 rule_id,
412 severity: rule.severity,
413 domain,
414 evaluated: 0,
415 matches: 0,
416 violations: 0,
417 antecedent_matches: implication_premise(rule).map(|_| 0),
418 warning: None,
419 }
420 }
421
422 fn new_require_doc(rule_id: String, domain: String) -> Self {
423 Self {
424 rule_id,
425 severity: RuleSeverity::Error,
426 domain,
427 evaluated: 0,
428 matches: 0,
429 violations: 0,
430 antecedent_matches: None,
431 warning: None,
432 }
433 }
434
435 fn record(&mut self, outcome: NodeOutcome, premise: Option<NodeOutcome>) {
436 if matches!(premise, Some(NodeOutcome::Pass)) {
437 self.antecedent_matches = Some(self.antecedent_matches.unwrap_or(0) + 1);
438 }
439 match outcome {
440 NodeOutcome::Pass => {
441 if premise.is_none() || matches!(premise, Some(NodeOutcome::Pass)) {
442 self.matches += 1;
443 }
444 }
445 NodeOutcome::Fail(_) => self.violations += 1,
446 NodeOutcome::NotApplicable => {}
447 }
448 }
449}
450
451fn implication_premise(rule: &CompiledRule) -> Option<&Node> {
452 match &rule.root {
453 Node::Implies(premise, _) => Some(premise),
454 _ => None,
455 }
456}
457
458struct EvalCtx<'g, 'src> {
459 graph: &'g CodeGraph,
460 requirements: Option<&'g dyn RequirementResolver>,
461 source: &'src str,
462 lang: Lang,
463 uri_cfg: UriConfig<'src>,
464 parent_counts: HashMap<(usize, &'g [u8]), u32>,
465 children_by_parent: HashMap<usize, Vec<usize>>,
466 out_refs_by_source: HashMap<usize, Vec<usize>>,
467 in_refs_by_target: HashMap<Vec<u8>, Vec<usize>>,
468 comment_ends: Vec<u32>,
469 doc_anchors: HashMap<usize, u32>,
470 def_index: OnceCell<HashMap<Vec<u8>, usize>>,
471}
472
473#[derive(Debug)]
474struct CompiledRule {
475 id: String,
476 explicit_id: bool,
477 rule_id: String,
478 raw_expr: String,
479 expanded_expr: String,
480 root: Node,
481 severity: RuleSeverity,
482 message: Option<String>,
483 rationale: Option<String>,
484}
485
486#[derive(Default)]
487struct CompiledKindRules {
488 rules: Vec<CompiledRule>,
489 require_doc_for_vis: Option<String>,
490 require_doc_rule_id: Option<String>,
491}
492
493pub struct CompiledRules {
494 by_kind: HashMap<String, CompiledKindRules>,
495 by_shape: HashMap<String, CompiledKindRules>,
496 refs: Vec<CompiledRule>,
497}
498
499#[derive(Debug, Clone, serde::Serialize)]
500pub struct CompiledRuleSpec {
501 pub rule_id: String,
502 pub severity: RuleSeverity,
503 pub lang: String,
504 pub domain: String,
505 pub kind: Option<String>,
506 pub expr: String,
507 pub expanded_expr: String,
508 pub message: Option<String>,
509 #[serde(skip_serializing_if = "Option::is_none")]
510 pub rationale: Option<String>,
511 pub require_doc_comment: Option<String>,
512}
513
514impl CompiledRules {
515 fn for_lang(cfg: &Config, lang: Lang, scheme: &str) -> Result<Self, ConfigError> {
516 compile_rules_for_lang(cfg, lang, scheme)
517 }
518
519 fn for_kind(&self, kind: &str) -> Option<&CompiledKindRules> {
520 self.by_kind.get(kind)
521 }
522
523 fn for_shape(&self, shape: Shape) -> Option<&CompiledKindRules> {
524 self.by_shape.get(shape.as_str())
525 }
526
527 pub fn specs(&self, lang: Lang) -> Vec<CompiledRuleSpec> {
528 compiled_rule_specs(self, lang)
529 }
530}
531
532fn compile_rules_for_lang(
533 cfg: &Config,
534 lang: Lang,
535 scheme: &str,
536) -> Result<CompiledRules, ConfigError> {
537 let section = config_section(lang);
538 let allowed = crate::check::config::allowed_kinds_for(lang);
539 let aliases = crate::check::config::resolve_aliases(&cfg.aliases)?;
540 let mut by_kind: HashMap<String, CompiledKindRules> = HashMap::new();
541 let mut by_shape: HashMap<String, CompiledKindRules> = HashMap::new();
542 let mut per_lang_refs: Vec<&crate::check::config::RuleEntry> = Vec::new();
543 for (kind, rules) in cfg.for_lang(lang).kinds.iter() {
544 if kind == "refs" {
545 per_lang_refs.extend(rules.rules.iter());
546 continue;
547 }
548 by_kind.insert(
549 kind.clone(),
550 compile(rules, section, kind, scheme, &allowed, &aliases)?,
551 );
552 }
553 for (kind, rules) in cfg.default.kinds.iter() {
554 if kind == "refs" {
555 continue;
556 }
557 if !allowed.contains(&kind.as_str()) {
558 continue;
559 }
560 if !by_kind.contains_key(kind.as_str()) {
561 by_kind.insert(
562 kind.clone(),
563 compile(rules, "default", kind, scheme, &allowed, &aliases)?,
564 );
565 }
566 }
567 compile_shape_rules_into(
568 &mut by_shape,
569 &cfg.shape,
570 "shape",
571 scheme,
572 &allowed,
573 &aliases,
574 )?;
575 compile_shape_rules_into(
576 &mut by_shape,
577 &cfg.for_lang(lang).shape,
578 &format!("{section}.shape"),
579 scheme,
580 &allowed,
581 &aliases,
582 )?;
583 let mut refs = Vec::with_capacity(cfg.refs.rules.len() + per_lang_refs.len());
584 for (idx, entry) in cfg.refs.rules.iter().enumerate() {
585 let id = entry.fallback_id(idx);
586 let at = format!("refs.{id}");
587 refs.push(compile_rule_entry(
588 entry, id, at, scheme, &allowed, &aliases,
589 )?);
590 }
591 for (idx, entry) in per_lang_refs.iter().enumerate() {
592 let id = entry.fallback_id(idx);
593 let at = format!("{section}.refs.{id}");
594 refs.push(compile_rule_entry(
595 entry, id, at, scheme, &allowed, &aliases,
596 )?);
597 }
598 Ok(CompiledRules {
599 by_kind,
600 by_shape,
601 refs,
602 })
603}
604
605fn compiled_rule_specs(rules: &CompiledRules, lang: Lang) -> Vec<CompiledRuleSpec> {
606 let mut out = Vec::new();
607 for (kind, rules) in &rules.by_kind {
608 for rule in &rules.rules {
609 out.push(CompiledRuleSpec {
610 rule_id: rule_id(lang, kind, &rule.id),
611 lang: lang.tag().to_string(),
612 domain: format!("{kind} defs"),
613 kind: Some(kind.clone()),
614 expr: rule.raw_expr.clone(),
615 expanded_expr: rule.expanded_expr.clone(),
616 message: rule.message.clone(),
617 severity: rule.severity,
618 rationale: rule.rationale.clone(),
619 require_doc_comment: None,
620 });
621 }
622 if let Some(value) = &rules.require_doc_for_vis {
623 out.push(CompiledRuleSpec {
624 rule_id: rule_id(lang, kind, "require_doc_comment"),
625 lang: lang.tag().to_string(),
626 domain: format!("{kind} defs"),
627 kind: Some(kind.clone()),
628 expr: format!("require_doc_comment = \"{value}\""),
629 expanded_expr: format!("require_doc_comment = \"{value}\""),
630 message: None,
631 severity: RuleSeverity::Error,
632 rationale: None,
633 require_doc_comment: Some(value.clone()),
634 });
635 }
636 }
637 for (shape, rules) in &rules.by_shape {
638 for rule in &rules.rules {
639 out.push(CompiledRuleSpec {
640 rule_id: rule.rule_id.clone(),
641 lang: lang.tag().to_string(),
642 domain: format!("shape:{shape} defs"),
643 kind: None,
644 expr: rule.raw_expr.clone(),
645 expanded_expr: rule.expanded_expr.clone(),
646 message: rule.message.clone(),
647 severity: rule.severity,
648 rationale: rule.rationale.clone(),
649 require_doc_comment: None,
650 });
651 }
652 if let (Some(value), Some(rule_id)) =
653 (&rules.require_doc_for_vis, &rules.require_doc_rule_id)
654 {
655 out.push(CompiledRuleSpec {
656 rule_id: rule_id.clone(),
657 lang: lang.tag().to_string(),
658 domain: format!("shape:{shape} defs"),
659 kind: None,
660 expr: format!("require_doc_comment = \"{value}\""),
661 expanded_expr: format!("require_doc_comment = \"{value}\""),
662 message: None,
663 severity: RuleSeverity::Error,
664 rationale: None,
665 require_doc_comment: Some(value.clone()),
666 });
667 }
668 }
669 for rule in &rules.refs {
670 out.push(CompiledRuleSpec {
671 rule_id: rule.rule_id.clone(),
672 lang: lang.tag().to_string(),
673 domain: "refs".to_string(),
674 kind: None,
675 expr: rule.raw_expr.clone(),
676 expanded_expr: rule.expanded_expr.clone(),
677 message: rule.message.clone(),
678 severity: rule.severity,
679 rationale: rule.rationale.clone(),
680 require_doc_comment: None,
681 });
682 }
683 out.sort_by(|a, b| a.rule_id.cmp(&b.rule_id));
684 out
685}
686
687fn compile_rule_entry(
688 entry: &crate::check::config::RuleEntry,
689 id: String,
690 at: String,
691 scheme: &str,
692 allowed_kinds: &[&str],
693 aliases: &HashMap<String, String>,
694) -> Result<CompiledRule, ConfigError> {
695 let expanded = crate::check::config::substitute_aliases(&entry.expr, aliases, &at)?;
696 let parsed = expr::parse(&expanded, scheme, allowed_kinds).map_err(|error| {
697 ConfigError::InvalidExpr {
698 at: at.clone(),
699 error,
700 }
701 })?;
702 Ok(CompiledRule {
703 id,
704 explicit_id: entry.id.is_some(),
705 rule_id: at,
706 raw_expr: entry.expr.clone(),
707 expanded_expr: expanded,
708 root: parsed.root,
709 message: entry.message.clone(),
710 severity: entry.severity,
711 rationale: entry.rationale.clone(),
712 })
713}
714
715fn compile(
716 rules: &KindRules,
717 section: &str,
718 kind: &str,
719 scheme: &str,
720 allowed_kinds: &[&str],
721 aliases: &HashMap<String, String>,
722) -> Result<CompiledKindRules, ConfigError> {
723 let mut compiled = Vec::with_capacity(rules.rules.len());
724 for (idx, entry) in rules.rules.iter().enumerate() {
725 let id = entry.fallback_id(idx);
726 let at = format!("{section}.{kind}.{id}");
727 let expanded = crate::check::config::substitute_aliases(&entry.expr, aliases, &at)?;
728 let parsed = expr::parse(&expanded, scheme, allowed_kinds).map_err(|error| {
729 ConfigError::InvalidExpr {
730 at: at.clone(),
731 error,
732 }
733 })?;
734 compiled.push(CompiledRule {
735 id,
736 explicit_id: entry.id.is_some(),
737 rule_id: at,
738 raw_expr: entry.expr.clone(),
739 expanded_expr: expanded,
740 root: parsed.root,
741 message: entry.message.clone(),
742 severity: entry.severity,
743 rationale: entry.rationale.clone(),
744 });
745 }
746 Ok(CompiledKindRules {
747 rules: compiled,
748 require_doc_for_vis: rules.require_doc_comment.clone(),
749 require_doc_rule_id: rules
750 .require_doc_comment
751 .as_ref()
752 .map(|_| format!("{section}.{kind}.require_doc_comment")),
753 })
754}
755
756fn compile_shape_rules_into(
757 dst: &mut HashMap<String, CompiledKindRules>,
758 src: &HashMap<String, KindRules>,
759 section: &str,
760 scheme: &str,
761 allowed_kinds: &[&str],
762 aliases: &HashMap<String, String>,
763) -> Result<(), ConfigError> {
764 for (shape, rules) in src {
765 let compiled = compile(rules, section, shape, scheme, allowed_kinds, aliases)?;
766 match dst.get_mut(shape) {
767 Some(existing) => merge_compiled_kind_rules(existing, compiled),
768 None => {
769 dst.insert(shape.clone(), compiled);
770 }
771 }
772 }
773 Ok(())
774}
775
776fn merge_compiled_kind_rules(base: &mut CompiledKindRules, ov: CompiledKindRules) {
777 for rule in ov.rules {
778 match rule
779 .explicit_id
780 .then(|| {
781 base.rules
782 .iter()
783 .position(|r| r.explicit_id && r.id == rule.id)
784 })
785 .flatten()
786 {
787 Some(idx) => base.rules[idx] = rule,
788 None => base.rules.push(rule),
789 }
790 }
791 if ov.require_doc_for_vis.is_some() {
792 base.require_doc_for_vis = ov.require_doc_for_vis;
793 base.require_doc_rule_id = ov.require_doc_rule_id;
794 }
795}
796
797impl CompiledKindRules {
798 fn has_explicit_rule_id(&self, id: &str) -> bool {
799 self.rules
800 .iter()
801 .any(|rule| rule.explicit_id && rule.id == id)
802 }
803}
804
805fn rule_id(lang: Lang, kind: &str, rule: &str) -> String {
806 format!("{}.{}.{}", config_section(lang), kind, rule)
807}
808
809fn lines_of(d: &DefRecord, source: &str) -> (u32, u32) {
810 match d.position {
811 Some((s, e)) => line_range(source, s, e),
812 None => (0, 0),
813 }
814}
815
816fn def_name(d: &DefRecord) -> Option<String> {
817 let last = d.moniker.as_view().segments().last()?;
818 let bare = bare_callable_name(last.name);
819 std::str::from_utf8(bare).ok().map(|s| s.to_string())
820}
821
822fn render_template(tpl: &str, vars: &[(&str, &str)]) -> String {
823 let mut out = tpl.to_string();
824 for (k, v) in vars {
825 let placeholder = format!("{{{k}}}");
826 if out.contains(&placeholder) {
827 out = out.replace(&placeholder, v);
828 }
829 }
830 out
831}
832
833#[derive(Clone, Copy)]
834struct DefScope<'a> {
835 record: &'a DefRecord,
836 idx: usize,
837}
838
839#[derive(Clone, Copy)]
840struct RuleTarget<'a> {
841 scope: DefScope<'a>,
842 kind: &'a str,
843}
844
845fn eval_rule(
846 rule: &CompiledRule,
847 d: &DefRecord,
848 def_idx: usize,
849 kind: &str,
850 ctx: &EvalCtx<'_, '_>,
851 out: &mut Vec<Violation>,
852) {
853 let target = RuleTarget {
854 scope: DefScope {
855 record: d,
856 idx: def_idx,
857 },
858 kind,
859 };
860 eval_rule_with_id(rule, target, rule_id(ctx.lang, kind, &rule.id), ctx, out);
861}
862
863fn eval_shape_rule(
864 rule: &CompiledRule,
865 d: &DefRecord,
866 def_idx: usize,
867 kind: &str,
868 ctx: &EvalCtx<'_, '_>,
869 out: &mut Vec<Violation>,
870) {
871 let target = RuleTarget {
872 scope: DefScope {
873 record: d,
874 idx: def_idx,
875 },
876 kind,
877 };
878 eval_rule_with_id(rule, target, rule.rule_id.clone(), ctx, out);
879}
880
881fn eval_rule_with_id(
882 rule: &CompiledRule,
883 target: RuleTarget<'_>,
884 rule_id: String,
885 ctx: &EvalCtx<'_, '_>,
886 out: &mut Vec<Violation>,
887) {
888 let Failure {
889 atom_raw,
890 lhs_label,
891 actual,
892 expected,
893 def_idx,
894 details,
895 } = match eval_node(&rule.root, target.scope.record, target.scope.idx, ctx) {
896 NodeOutcome::Pass | NodeOutcome::NotApplicable => return,
897 NodeOutcome::Fail(f) => f,
898 };
899 let diagnostic = def_idx
900 .map(|idx| ctx.graph.def_at(idx))
901 .unwrap_or(target.scope.record);
902 let diagnostic_kind = std::str::from_utf8(&diagnostic.kind).unwrap_or(target.kind);
903 let name = def_name(diagnostic).unwrap_or_default();
904 let name_snake = to_snake_case(&name);
905 let moniker = to_uri(&diagnostic.moniker, &ctx.uri_cfg);
906 let (start_line, end_line) = lines_of(diagnostic, ctx.source);
907 let message = format!(
908 "{diagnostic_kind} `{name}` fails `{atom_raw}` ({lhs_label} = {actual}, expected {expected})",
909 );
910 let explanation = rule
911 .message
912 .as_ref()
913 .map(|tpl| {
914 let mut rendered = render_template(
915 tpl,
916 &[
917 ("name", &name),
918 ("name.snake", &name_snake),
919 ("kind", diagnostic_kind),
920 ("moniker", &moniker),
921 ("expr", &rule.raw_expr),
922 ("actual", &actual),
923 ("value", &actual),
924 ("expected", &expected),
925 ("pattern", &expected),
926 ("lines", &actual),
927 ("limit", &expected),
928 ("count", &actual),
929 ],
930 );
931 if let Some(details) = &details {
932 if !rendered.is_empty() {
933 rendered.push('\n');
934 }
935 rendered.push_str(details);
936 }
937 rendered
938 })
939 .or(details);
940 out.push(Violation {
941 rule_id,
942 severity: rule.severity,
943 moniker,
944 kind: diagnostic_kind.to_string(),
945 lines: (start_line, end_line),
946 message,
947 explanation,
948 });
949}
950
951fn eval_ref_rule(
952 rule: &CompiledRule,
953 r: &code_moniker_core::core::code_graph::RefRecord,
954 graph: &CodeGraph,
955 ctx: &EvalCtx<'_, '_>,
956 out: &mut Vec<Violation>,
957) {
958 let Failure {
959 atom_raw,
960 lhs_label,
961 actual,
962 expected,
963 def_idx: _,
964 details,
965 } = match eval_ref_node(&rule.root, r, ctx) {
966 NodeOutcome::Pass | NodeOutcome::NotApplicable => return,
967 NodeOutcome::Fail(f) => f,
968 };
969 let source_def = graph.def_at(r.source);
970 let source_uri = to_uri(&source_def.moniker, &ctx.uri_cfg);
971 let target_uri = to_uri(&r.target, &ctx.uri_cfg);
972 let ref_kind = std::str::from_utf8(&r.kind).unwrap_or_default();
973 let (start_line, end_line) = match r.position {
974 Some((s, e)) => line_range(ctx.source, s, e),
975 None => (0, 0),
976 };
977 let message = format!(
978 "ref {ref_kind} {source_uri} → {target_uri} fails `{atom_raw}` ({lhs_label} = {actual}, expected {expected})"
979 );
980 let source_name = name_of(&source_def.moniker).unwrap_or_default();
981 let source_kind = last_segment_kind(&source_def.moniker).unwrap_or_default();
982 let source_shape = shape_name_of_last_segment(&source_def.moniker);
983 let target_name = name_of(&r.target).unwrap_or_default();
984 let target_kind = last_segment_kind(&r.target).unwrap_or_default();
985 let target_shape = shape_name_of_last_segment(&r.target);
986 let explanation = rule
987 .message
988 .as_ref()
989 .map(|tpl| {
990 let mut rendered = render_template(
991 tpl,
992 &[
993 ("kind", ref_kind),
994 ("source.name", &source_name),
995 ("source.kind", &source_kind),
996 ("source.shape", &source_shape),
997 ("source.moniker", &source_uri),
998 ("target.name", &target_name),
999 ("target.kind", &target_kind),
1000 ("target.shape", &target_shape),
1001 ("target.moniker", &target_uri),
1002 ("atom", &atom_raw),
1003 ("actual", &actual),
1004 ("expected", &expected),
1005 ],
1006 );
1007 if let Some(details) = &details {
1008 if !rendered.is_empty() {
1009 rendered.push('\n');
1010 }
1011 rendered.push_str(details);
1012 }
1013 rendered
1014 })
1015 .or(details);
1016 out.push(Violation {
1017 rule_id: rule.rule_id.clone(),
1018 severity: rule.severity,
1019 moniker: target_uri,
1020 kind: ref_kind.to_string(),
1021 lines: (start_line, end_line),
1022 message,
1023 explanation,
1024 });
1025}
1026
1027fn eval_ref_node(
1028 node: &Node,
1029 r: &code_moniker_core::core::code_graph::RefRecord,
1030 ctx: &EvalCtx<'_, '_>,
1031) -> NodeOutcome {
1032 eval_ref_node_with_current(node, r, r, ctx)
1033}
1034
1035fn eval_ref_node_with_current(
1036 node: &Node,
1037 r: &code_moniker_core::core::code_graph::RefRecord,
1038 current: &code_moniker_core::core::code_graph::RefRecord,
1039 ctx: &EvalCtx<'_, '_>,
1040) -> NodeOutcome {
1041 walk_node(
1042 node,
1043 &|a| eval_ref_atom(a, r, current, ctx),
1044 &|kind, domain, filter| eval_quantifier_ref(kind, domain, filter, r, ctx),
1045 &|_| NodeOutcome::NotApplicable,
1046 &|_| NodeOutcome::NotApplicable,
1047 )
1048}
1049
1050fn eval_ref_atom(
1051 atom: &Atom,
1052 r: &code_moniker_core::core::code_graph::RefRecord,
1053 current: &code_moniker_core::core::code_graph::RefRecord,
1054 ctx: &EvalCtx<'_, '_>,
1055) -> AtomOutcome {
1056 let Some(value) = eval_ref_lhs_expr_value(&atom.lhs, r, ctx) else {
1057 return AtomOutcome::NotApplicable;
1058 };
1059 if let Rhs::Projection(other) = &atom.rhs {
1060 let Some(rhs_val) = resolve_ref_lhs(*other, r, ctx) else {
1061 return AtomOutcome::NotApplicable;
1062 };
1063 return apply_op_values(&value, atom.op, &rhs_val);
1064 }
1065 if let Rhs::CurrentProjection(other) = &atom.rhs {
1066 let Some(rhs_val) = resolve_ref_lhs(*other, current, ctx) else {
1067 return AtomOutcome::NotApplicable;
1068 };
1069 return apply_op_values(&value, atom.op, &rhs_val);
1070 }
1071 if let Rhs::Number(expr) = &atom.rhs {
1072 let Some(rhs_val) = eval_number_expr_ref(expr, r, ctx).map(Value::Number) else {
1073 return AtomOutcome::NotApplicable;
1074 };
1075 return apply_op_values(&value, atom.op, &rhs_val);
1076 }
1077 apply_op(&value, atom)
1078}
1079
1080fn eval_ref_lhs_expr_value(
1081 lhs: &LhsExpr,
1082 r: &code_moniker_core::core::code_graph::RefRecord,
1083 ctx: &EvalCtx<'_, '_>,
1084) -> Option<Value> {
1085 match lhs {
1086 LhsExpr::Attr(lhs) => resolve_ref_lhs(*lhs, r, ctx),
1087 LhsExpr::SegmentOf { scope, kind } => match scope {
1088 SegmentScope::Def => None,
1089 SegmentScope::Source => {
1090 let source_def = ctx.graph.def_at(r.source);
1091 Some(Value::Str(first_segment_name(
1092 &source_def.moniker,
1093 kind.as_bytes(),
1094 )))
1095 }
1096 SegmentScope::Target => {
1097 Some(Value::Str(first_segment_name(&r.target, kind.as_bytes())))
1098 }
1099 },
1100 LhsExpr::Number(expr) => eval_number_expr_ref(expr, r, ctx).map(Value::Number),
1101 LhsExpr::Collection(_) | LhsExpr::Mode(_) | LhsExpr::PairProjection(_) => None,
1102 }
1103}
1104
1105fn eval_quantifier_ref(
1106 kind: QuantKind,
1107 domain: &Domain,
1108 filter: &Node,
1109 r: &code_moniker_core::core::code_graph::RefRecord,
1110 ctx: &EvalCtx<'_, '_>,
1111) -> NodeOutcome {
1112 let items = ref_domain_items(domain, r, ctx);
1113 if items.is_empty() {
1114 return match kind {
1115 QuantKind::All | QuantKind::None => NodeOutcome::Pass,
1116 QuantKind::Any => NodeOutcome::Fail(Failure {
1117 atom_raw: format!("any({})", domain_debug_label(domain)),
1118 lhs_label: "any".to_string(),
1119 actual: "0 matches".to_string(),
1120 expected: "at least one".to_string(),
1121 def_idx: None,
1122 details: None,
1123 }),
1124 };
1125 }
1126 let mut matched = 0usize;
1127 for item in items {
1128 let outcome = match item {
1129 DomainItem::Ref { record } => eval_ref_node_with_current(filter, record, r, ctx),
1130 DomainItem::Def {
1131 idx: Some(idx),
1132 def,
1133 } => eval_node_with_self(filter, def, idx, r.source, ctx),
1134 DomainItem::Def { idx: None, def } => eval_external_def_node(filter, def, ctx),
1135 DomainItem::Segment { kind, name } => eval_node_segment(filter, kind, name),
1136 };
1137 if matches!(outcome, NodeOutcome::Pass) {
1138 matched += 1;
1139 if kind == QuantKind::Any {
1140 return NodeOutcome::Pass;
1141 }
1142 } else if matches!(outcome, NodeOutcome::Fail(_)) && kind == QuantKind::All {
1143 return outcome;
1144 }
1145 }
1146 match kind {
1147 QuantKind::Any => NodeOutcome::Fail(Failure {
1148 atom_raw: format!("any({})", domain_debug_label(domain)),
1149 lhs_label: "any".to_string(),
1150 actual: format!("{matched} matches"),
1151 expected: "at least one".to_string(),
1152 def_idx: None,
1153 details: None,
1154 }),
1155 QuantKind::All => NodeOutcome::Pass,
1156 QuantKind::None if matched == 0 => NodeOutcome::Pass,
1157 QuantKind::None => NodeOutcome::Fail(Failure {
1158 atom_raw: format!("none({})", domain_debug_label(domain)),
1159 lhs_label: "none".to_string(),
1160 actual: format!("{matched} matches"),
1161 expected: "0 matches".to_string(),
1162 def_idx: None,
1163 details: None,
1164 }),
1165 }
1166}
1167
1168fn ref_domain_items<'a>(
1169 domain: &Domain,
1170 r: &code_moniker_core::core::code_graph::RefRecord,
1171 ctx: &'a EvalCtx<'_, '_>,
1172) -> Vec<DomainItem<'a>> {
1173 match domain {
1174 Domain::SourceOutRefs | Domain::OutRefs => ctx
1175 .out_refs_by_source
1176 .get(&r.source)
1177 .into_iter()
1178 .flatten()
1179 .map(|idx| DomainItem::Ref {
1180 record: ctx.graph.ref_at(*idx),
1181 })
1182 .collect(),
1183 Domain::SourceInRefs | Domain::InRefs => {
1184 let source = ctx.graph.def_at(r.source);
1185 let key = source.moniker.as_encoded();
1186 ctx.in_refs_by_target
1187 .get(key)
1188 .into_iter()
1189 .flatten()
1190 .map(|idx| DomainItem::Ref {
1191 record: ctx.graph.ref_at(*idx),
1192 })
1193 .collect()
1194 }
1195 Domain::SourceAncestorOutRefs => ancestor_ref_items(r.source, ctx, true),
1196 Domain::SourceAncestorInRefs => ancestor_ref_items(r.source, ctx, false),
1197 Domain::Segments => ctx
1198 .graph
1199 .def_at(r.source)
1200 .moniker
1201 .as_view()
1202 .segments()
1203 .map(|seg| DomainItem::Segment {
1204 kind: seg.kind,
1205 name: seg.name,
1206 })
1207 .collect(),
1208 Domain::Children(_) | Domain::ChildrenByShape(_) | Domain::Descendants(_) => {
1209 domain_items(domain, r.source, ctx)
1210 }
1211 Domain::Pairs(_) => Vec::new(),
1212 }
1213}
1214
1215fn domain_debug_label(domain: &Domain) -> &'static str {
1216 match domain {
1217 Domain::Children(_) => "children",
1218 Domain::ChildrenByShape(_) => "shape",
1219 Domain::Descendants(_) => "descendants",
1220 Domain::Pairs(_) => "pairs",
1221 Domain::Segments => "segment",
1222 Domain::OutRefs => "out_refs",
1223 Domain::InRefs => "in_refs",
1224 Domain::SourceOutRefs => "source.out_refs",
1225 Domain::SourceInRefs => "source.in_refs",
1226 Domain::SourceAncestorOutRefs => "source.ancestors.out_refs",
1227 Domain::SourceAncestorInRefs => "source.ancestors.in_refs",
1228 }
1229}
1230
1231fn ancestor_ref_items<'a>(
1232 def_idx: usize,
1233 ctx: &'a EvalCtx<'_, '_>,
1234 outgoing: bool,
1235) -> Vec<DomainItem<'a>> {
1236 ancestor_ref_indexes(def_idx, ctx, outgoing)
1237 .into_iter()
1238 .map(|ref_idx| DomainItem::Ref {
1239 record: ctx.graph.ref_at(ref_idx),
1240 })
1241 .collect()
1242}
1243
1244fn ancestor_ref_indexes(def_idx: usize, ctx: &EvalCtx<'_, '_>, outgoing: bool) -> Vec<usize> {
1245 let mut items = Vec::new();
1246 let mut parent = ctx.graph.def_at(def_idx).parent;
1247 while let Some(idx) = parent {
1248 if outgoing {
1249 if let Some(refs) = ctx.out_refs_by_source.get(&idx) {
1250 items.extend(refs.iter().copied());
1251 }
1252 } else {
1253 let key = ctx.graph.def_at(idx).moniker.as_encoded();
1254 if let Some(refs) = ctx.in_refs_by_target.get(key) {
1255 items.extend(refs.iter().copied());
1256 }
1257 }
1258 parent = ctx.graph.def_at(idx).parent;
1259 }
1260 items
1261}
1262
1263fn resolve_ref_lhs(
1264 lhs: Lhs,
1265 r: &code_moniker_core::core::code_graph::RefRecord,
1266 ctx: &EvalCtx<'_, '_>,
1267) -> Option<Value> {
1268 let graph = ctx.graph;
1269 let source_def = graph.def_at(r.source);
1270 Some(match lhs {
1271 Lhs::Kind => Value::Str(std::str::from_utf8(&r.kind).ok()?.to_string()),
1272 Lhs::Confidence => Value::Str(std::str::from_utf8(&r.confidence).ok()?.to_string()),
1273 Lhs::StartLine => {
1274 let (s, e) = r.position?;
1275 let (sl, _) = line_range(ctx.source, s, e);
1276 Value::Number(sl as f64)
1277 }
1278 Lhs::EndLine => {
1279 let (s, e) = r.position?;
1280 let (_, el) = line_range(ctx.source, s, e);
1281 Value::Number(el as f64)
1282 }
1283 Lhs::StartByte => {
1284 let (s, _) = r.position?;
1285 Value::Number(s as f64)
1286 }
1287 Lhs::EndByte => {
1288 let (_, e) = r.position?;
1289 Value::Number(e as f64)
1290 }
1291 Lhs::Text => Value::Str(ref_text(r, ctx)?),
1292 Lhs::Moniker | Lhs::SourceMoniker => Value::Moniker(source_def.moniker.clone()),
1293 Lhs::ParentMoniker => Value::Moniker(source_def.moniker.parent()?),
1294 Lhs::SourceParentMoniker => Value::Moniker(source_def.moniker.parent()?),
1295 Lhs::ParentName => Value::Str(name_of(&source_def.moniker.parent()?)?),
1296 Lhs::ParentKind => Value::Str(last_segment_kind(&source_def.moniker.parent()?)?),
1297 Lhs::TargetMoniker => Value::Moniker(r.target.clone()),
1298 Lhs::TargetParentMoniker => Value::Moniker(r.target.parent()?),
1299 Lhs::SourceName => Value::Str(name_of(&source_def.moniker)?),
1300 Lhs::TargetName => Value::Str(name_of(&r.target)?),
1301 Lhs::SourceKind => Value::Str(last_segment_kind(&source_def.moniker)?),
1302 Lhs::TargetKind => Value::Str(last_segment_kind(&r.target)?),
1303 Lhs::Shape | Lhs::SourceShape => Value::Str(
1304 shape_of_last_segment(&source_def.moniker)?
1305 .as_str()
1306 .to_string(),
1307 ),
1308 Lhs::TargetShape => Value::Str(shape_of_last_segment(&r.target)?.as_str().to_string()),
1309 Lhs::ParentShape => {
1310 let segs: Vec<_> = source_def.moniker.as_view().segments().collect();
1311 if segs.len() < 2 {
1312 return None;
1313 }
1314 let parent_kind = segs[segs.len() - 2].kind;
1315 Value::Str(
1316 code_moniker_core::core::shape::shape_of(parent_kind)?
1317 .as_str()
1318 .to_string(),
1319 )
1320 }
1321 Lhs::SourceVisibility => Value::Str(
1322 std::str::from_utf8(&source_def.visibility)
1323 .ok()?
1324 .to_string(),
1325 ),
1326 Lhs::TargetVisibility => {
1327 let def = resolve_local_def(graph, &r.target)?;
1328 Value::Str(std::str::from_utf8(&def.visibility).ok()?.to_string())
1329 }
1330 Lhs::Name
1331 | Lhs::Visibility
1332 | Lhs::Lines
1333 | Lhs::Depth
1334 | Lhs::SegmentName
1335 | Lhs::SegmentKind => return None,
1336 })
1337}
1338
1339fn ref_text(
1340 r: &code_moniker_core::core::code_graph::RefRecord,
1341 ctx: &EvalCtx<'_, '_>,
1342) -> Option<String> {
1343 let (start, end) = r.position?;
1344 ctx.source
1345 .get(start as usize..end as usize)
1346 .map(ToString::to_string)
1347}
1348
1349fn name_of(m: &code_moniker_core::core::moniker::Moniker) -> Option<String> {
1350 let last = m.as_view().segments().last()?;
1351 let bare = code_moniker_core::core::moniker::query::bare_callable_name(last.name);
1352 std::str::from_utf8(bare).ok().map(|s| s.to_string())
1353}
1354
1355fn first_segment_name(m: &code_moniker_core::core::moniker::Moniker, kind: &[u8]) -> String {
1356 for seg in m.as_view().segments() {
1357 if seg.kind == kind {
1358 return std::str::from_utf8(seg.name)
1359 .unwrap_or_default()
1360 .to_string();
1361 }
1362 }
1363 String::new()
1364}
1365
1366fn last_segment_kind(m: &code_moniker_core::core::moniker::Moniker) -> Option<String> {
1367 let last = m.as_view().segments().last()?;
1368 std::str::from_utf8(last.kind).ok().map(|s| s.to_string())
1369}
1370
1371fn shape_of_last_segment(
1372 m: &code_moniker_core::core::moniker::Moniker,
1373) -> Option<code_moniker_core::core::shape::Shape> {
1374 let last = m.as_view().segments().last()?;
1375 code_moniker_core::core::shape::shape_of(last.kind)
1376}
1377
1378fn shape_name_of_last_segment(m: &code_moniker_core::core::moniker::Moniker) -> String {
1379 shape_of_last_segment(m)
1380 .map(|shape| shape.as_str().to_string())
1381 .unwrap_or_default()
1382}
1383
1384fn resolve_local_def<'g>(
1385 graph: &'g CodeGraph,
1386 m: &code_moniker_core::core::moniker::Moniker,
1387) -> Option<&'g DefRecord> {
1388 graph.defs().find(|d| d.moniker == *m)
1389}
1390
1391fn describe_lhs(lhs: &LhsExpr) -> &str {
1392 match lhs {
1393 LhsExpr::Attr(a) => a.as_str(),
1394 LhsExpr::Number(n) => number_expr_label(n),
1395 LhsExpr::Collection(_) => "collection",
1396 LhsExpr::Mode(_) => "mode",
1397 LhsExpr::PairProjection(_) => "pair",
1398 LhsExpr::SegmentOf { .. } => "segment",
1399 }
1400}
1401
1402#[derive(Debug)]
1403struct Failure {
1404 atom_raw: String,
1405 lhs_label: String,
1406 actual: String,
1407 expected: String,
1408 def_idx: Option<usize>,
1409 details: Option<String>,
1410}
1411
1412#[derive(Debug)]
1413enum NodeOutcome {
1414 Pass,
1415 Fail(Failure),
1416 NotApplicable,
1417}
1418
1419enum AtomOutcome {
1420 Pass,
1421 Fail { actual: String, expected: String },
1422 NotApplicable,
1423}
1424
1425fn walk_node<A, Q, R, L>(
1429 node: &Node,
1430 atom_eval: &A,
1431 quant_eval: &Q,
1432 require_eval: &R,
1433 layout_eval: &L,
1434) -> NodeOutcome
1435where
1436 A: Fn(&Atom) -> AtomOutcome,
1437 Q: Fn(QuantKind, &Domain, &Node) -> NodeOutcome,
1438 R: Fn(&str) -> NodeOutcome,
1439 L: Fn(&VerticalLayout) -> NodeOutcome,
1440{
1441 match node {
1442 Node::Atom(atom) => match atom_eval(atom) {
1443 AtomOutcome::Pass => NodeOutcome::Pass,
1444 AtomOutcome::Fail { actual, expected } => NodeOutcome::Fail(Failure {
1445 atom_raw: atom.raw.clone(),
1446 lhs_label: describe_lhs(&atom.lhs).to_string(),
1447 actual,
1448 expected,
1449 def_idx: None,
1450 details: None,
1451 }),
1452 AtomOutcome::NotApplicable => NodeOutcome::NotApplicable,
1453 },
1454 Node::And(children) => {
1455 let mut na = false;
1456 for c in children {
1457 match walk_node(c, atom_eval, quant_eval, require_eval, layout_eval) {
1458 NodeOutcome::Pass => {}
1459 NodeOutcome::Fail(f) => return NodeOutcome::Fail(f),
1460 NodeOutcome::NotApplicable => na = true,
1461 }
1462 }
1463 if na {
1464 NodeOutcome::NotApplicable
1465 } else {
1466 NodeOutcome::Pass
1467 }
1468 }
1469 Node::Or(children) => {
1470 let mut last_fail: Option<Failure> = None;
1471 let mut na = false;
1472 for c in children {
1473 match walk_node(c, atom_eval, quant_eval, require_eval, layout_eval) {
1474 NodeOutcome::Pass => return NodeOutcome::Pass,
1475 NodeOutcome::Fail(f) => last_fail = Some(f),
1476 NodeOutcome::NotApplicable => na = true,
1477 }
1478 }
1479 if na {
1480 NodeOutcome::NotApplicable
1481 } else if let Some(f) = last_fail {
1482 NodeOutcome::Fail(f)
1483 } else {
1484 NodeOutcome::NotApplicable
1485 }
1486 }
1487 Node::Not(inner) => {
1488 match walk_node(inner, atom_eval, quant_eval, require_eval, layout_eval) {
1489 NodeOutcome::Pass => NodeOutcome::Fail(Failure {
1490 atom_raw: "NOT (...)".to_string(),
1491 lhs_label: "NOT".to_string(),
1492 actual: "true".to_string(),
1493 expected: "false".to_string(),
1494 def_idx: None,
1495 details: None,
1496 }),
1497 NodeOutcome::Fail(_) => NodeOutcome::Pass,
1498 NodeOutcome::NotApplicable => NodeOutcome::NotApplicable,
1499 }
1500 }
1501 Node::Implies(prem, cons) => {
1502 match walk_node(prem, atom_eval, quant_eval, require_eval, layout_eval) {
1503 NodeOutcome::Pass => {
1504 walk_node(cons, atom_eval, quant_eval, require_eval, layout_eval)
1505 }
1506 NodeOutcome::Fail(_) => NodeOutcome::Pass,
1507 NodeOutcome::NotApplicable => NodeOutcome::NotApplicable,
1508 }
1509 }
1510 Node::Require(pattern) => require_eval(pattern),
1511 Node::VerticalLayout(layout) => layout_eval(layout),
1512 Node::Quantifier {
1513 kind,
1514 domain,
1515 filter,
1516 } => quant_eval(*kind, domain, filter),
1517 }
1518}
1519
1520fn eval_node(node: &Node, d: &DefRecord, def_idx: usize, ctx: &EvalCtx<'_, '_>) -> NodeOutcome {
1521 eval_node_with_self(node, d, def_idx, def_idx, ctx)
1522}
1523
1524fn eval_node_with_self(
1525 node: &Node,
1526 d: &DefRecord,
1527 def_idx: usize,
1528 self_idx: usize,
1529 ctx: &EvalCtx<'_, '_>,
1530) -> NodeOutcome {
1531 walk_node(
1532 node,
1533 &|a| eval_atom(a, d, def_idx, self_idx, ctx),
1534 &|kind, domain, filter| {
1535 eval_quantifier_def(
1536 kind,
1537 domain,
1538 filter,
1539 DefScope {
1540 record: d,
1541 idx: def_idx,
1542 },
1543 self_idx,
1544 ctx,
1545 )
1546 },
1547 &|pattern| eval_require(pattern, d, ctx),
1548 &|layout| eval_vertical_layout(layout, d, def_idx, ctx),
1549 )
1550}
1551
1552fn eval_require(pattern: &str, d: &DefRecord, ctx: &EvalCtx<'_, '_>) -> NodeOutcome {
1553 let Some(rendered) = render_requirement_pattern(pattern, d) else {
1554 return NodeOutcome::NotApplicable;
1555 };
1556 if local_requirement_exists(&rendered, ctx)
1557 || ctx
1558 .requirements
1559 .is_some_and(|resolver| resolver.exists(&rendered, d, ctx.uri_cfg.scheme))
1560 {
1561 return NodeOutcome::Pass;
1562 }
1563 NodeOutcome::Fail(Failure {
1564 atom_raw: format!("require(\"{pattern}\")"),
1565 lhs_label: "require".to_string(),
1566 actual: "missing".to_string(),
1567 expected: rendered,
1568 def_idx: None,
1569 details: None,
1570 })
1571}
1572
1573fn local_requirement_exists(pattern: &str, ctx: &EvalCtx<'_, '_>) -> bool {
1574 let Ok(pattern) = crate::check::path::parse(pattern) else {
1575 return false;
1576 };
1577 ctx.graph
1578 .defs()
1579 .any(|def| crate::check::path::matches(&pattern, &def.moniker))
1580}
1581
1582fn render_requirement_pattern(pattern: &str, d: &DefRecord) -> Option<String> {
1583 let name = def_name(d)?;
1584 Some(
1585 pattern
1586 .replace("{name}", &name)
1587 .replace("{name.snake}", &to_snake_case(&name)),
1588 )
1589}
1590
1591fn to_snake_case(name: &str) -> String {
1592 let mut out = String::new();
1593 for (idx, ch) in name.chars().enumerate() {
1594 if ch.is_ascii_uppercase() {
1595 if idx > 0 {
1596 out.push('_');
1597 }
1598 out.push(ch.to_ascii_lowercase());
1599 } else {
1600 out.push(ch);
1601 }
1602 }
1603 out
1604}
1605
1606fn resolve_def_lhs(lhs: Lhs, d: &DefRecord, ctx: &EvalCtx<'_, '_>) -> Option<Value> {
1607 let source = ctx.source;
1608 let value = match lhs {
1609 Lhs::Name => Value::Str(def_name(d)?),
1610 Lhs::Kind => Value::Str(std::str::from_utf8(&d.kind).ok()?.to_string()),
1611 Lhs::Visibility => Value::Str(std::str::from_utf8(&d.visibility).ok()?.to_string()),
1612 Lhs::Lines => {
1613 let (s, e) = d.position?;
1614 let (sl, el) = line_range(source, s, e);
1615 Value::Number((el - sl + 1) as f64)
1616 }
1617 Lhs::StartLine => {
1618 let (s, e) = d.position?;
1619 let (sl, _) = line_range(source, s, e);
1620 Value::Number(sl as f64)
1621 }
1622 Lhs::EndLine => {
1623 let (s, e) = d.position?;
1624 let (_, el) = line_range(source, s, e);
1625 Value::Number(el as f64)
1626 }
1627 Lhs::StartByte => {
1628 let (s, _) = d.position?;
1629 Value::Number(s as f64)
1630 }
1631 Lhs::EndByte => {
1632 let (_, e) = d.position?;
1633 Value::Number(e as f64)
1634 }
1635 Lhs::Text => {
1636 let (s, e) = d.position?;
1637 Value::Str(source.get(s as usize..e as usize).unwrap_or("").to_string())
1638 }
1639 Lhs::Moniker => Value::Moniker(d.moniker.clone()),
1640 Lhs::ParentMoniker => Value::Moniker(d.moniker.parent()?),
1641 Lhs::Depth => Value::Number(d.moniker.as_view().segments().count() as f64),
1642 Lhs::ParentName => {
1643 let segs: Vec<_> = d.moniker.as_view().segments().collect();
1644 if segs.len() < 2 {
1645 return None;
1646 }
1647 let p = &segs[segs.len() - 2];
1648 let bare = bare_callable_name(p.name);
1649 Value::Str(std::str::from_utf8(bare).ok()?.to_string())
1650 }
1651 Lhs::ParentKind => {
1652 let segs: Vec<_> = d.moniker.as_view().segments().collect();
1653 if segs.len() < 2 {
1654 return None;
1655 }
1656 let p = &segs[segs.len() - 2];
1657 Value::Str(std::str::from_utf8(p.kind).ok()?.to_string())
1658 }
1659 Lhs::Shape => Value::Str(d.shape()?.as_str().to_string()),
1660 Lhs::ParentShape => {
1661 let segs: Vec<_> = d.moniker.as_view().segments().collect();
1662 if segs.len() < 2 {
1663 return None;
1664 }
1665 let parent_kind = segs[segs.len() - 2].kind;
1666 Value::Str(
1667 code_moniker_core::core::shape::shape_of(parent_kind)?
1668 .as_str()
1669 .to_string(),
1670 )
1671 }
1672 Lhs::SourceName => Value::Str(def_name(d)?),
1673 Lhs::SourceKind => Value::Str(std::str::from_utf8(&d.kind).ok()?.to_string()),
1674 Lhs::SourceShape => Value::Str(d.shape()?.as_str().to_string()),
1675 Lhs::SourceVisibility => Value::Str(std::str::from_utf8(&d.visibility).ok()?.to_string()),
1676 Lhs::SourceMoniker => Value::Moniker(d.moniker.clone()),
1677 Lhs::SourceParentMoniker => Value::Moniker(d.moniker.parent()?),
1678 Lhs::Confidence
1679 | Lhs::TargetName
1680 | Lhs::TargetKind
1681 | Lhs::TargetShape
1682 | Lhs::TargetVisibility
1683 | Lhs::TargetMoniker
1684 | Lhs::TargetParentMoniker
1685 | Lhs::SegmentName
1686 | Lhs::SegmentKind => return None,
1687 };
1688 Some(value)
1689}
1690
1691fn eval_count(
1694 domain: &Domain,
1695 filter: Option<&Node>,
1696 d: &DefRecord,
1697 def_idx: usize,
1698 self_idx: usize,
1699 ctx: &EvalCtx<'_, '_>,
1700) -> u32 {
1701 match domain {
1702 Domain::Children(kind) => match filter {
1703 None => ctx
1704 .parent_counts
1705 .get(&(def_idx, kind.as_bytes()))
1706 .copied()
1707 .unwrap_or(0),
1708 Some(node) => count_children_filtered(d, def_idx, self_idx, kind, node, ctx),
1709 },
1710 Domain::ChildrenByShape(shape) => {
1711 count_children_by_shape(def_idx, self_idx, shape, filter, ctx)
1712 }
1713 Domain::Descendants(_) => count_domain_items(domain, filter, def_idx, self_idx, ctx),
1714 Domain::Pairs(inner) => eval_pair_count(inner, filter, def_idx, self_idx, ctx),
1715 Domain::Segments => count_segments(d, filter),
1716 Domain::OutRefs | Domain::SourceOutRefs => count_out_refs(d, def_idx, filter, ctx),
1717 Domain::InRefs | Domain::SourceInRefs => count_in_refs(d, filter, ctx),
1718 Domain::SourceAncestorOutRefs | Domain::SourceAncestorInRefs => {
1719 count_domain_items(domain, filter, def_idx, self_idx, ctx)
1720 }
1721 }
1722}
1723
1724fn count_domain_items(
1725 domain: &Domain,
1726 filter: Option<&Node>,
1727 def_idx: usize,
1728 self_idx: usize,
1729 ctx: &EvalCtx<'_, '_>,
1730) -> u32 {
1731 let items = domain_items(domain, def_idx, ctx);
1732 let Some(node) = filter else {
1733 return items.len() as u32;
1734 };
1735 items
1736 .into_iter()
1737 .filter(|item| match item {
1738 DomainItem::Def {
1739 idx: Some(idx),
1740 def,
1741 } => {
1742 matches!(
1743 eval_node_with_self(node, def, *idx, self_idx, ctx),
1744 NodeOutcome::Pass
1745 )
1746 }
1747 DomainItem::Def { idx: None, def } => {
1748 matches!(eval_external_def_node(node, def, ctx), NodeOutcome::Pass)
1749 }
1750 DomainItem::Ref { record } => {
1751 matches!(eval_ref_node(node, record, ctx), NodeOutcome::Pass)
1752 }
1753 DomainItem::Segment { kind, name } => {
1754 matches!(eval_node_segment(node, kind, name), NodeOutcome::Pass)
1755 }
1756 })
1757 .count() as u32
1758}
1759
1760fn eval_number_expr_def(
1761 expr: &NumberExpr,
1762 d: &DefRecord,
1763 def_idx: usize,
1764 self_idx: usize,
1765 ctx: &EvalCtx<'_, '_>,
1766) -> Option<f64> {
1767 match expr {
1768 NumberExpr::Literal(n) => Some(*n),
1769 NumberExpr::Projection(lhs) => match resolve_def_lhs(*lhs, d, ctx)? {
1770 Value::Number(n) => Some(n),
1771 _ => None,
1772 },
1773 NumberExpr::Count { domain, filter } => {
1774 Some(eval_count(domain, filter.as_deref(), d, def_idx, self_idx, ctx) as f64)
1775 }
1776 NumberExpr::Aggregate {
1777 kind,
1778 domain,
1779 expr,
1780 percentile,
1781 } => eval_aggregate(
1782 AggregateEval {
1783 kind: *kind,
1784 domain,
1785 expr,
1786 percentile: *percentile,
1787 def_idx,
1788 self_idx,
1789 },
1790 ctx,
1791 ),
1792 NumberExpr::Metric { kind, binding } => {
1793 eval_metric(*kind, *binding, def_idx, self_idx, ctx)
1794 }
1795 NumberExpr::Entropy(collection) => eval_entropy(collection, def_idx, self_idx, ctx),
1796 NumberExpr::Size(collection) => {
1797 if collection_has_pair_binding(collection) {
1798 return None;
1799 }
1800 Some(eval_collection_size(collection, def_idx, self_idx, ctx) as f64)
1801 }
1802 }
1803}
1804
1805fn eval_number_expr_ref(
1806 expr: &NumberExpr,
1807 r: &code_moniker_core::core::code_graph::RefRecord,
1808 ctx: &EvalCtx<'_, '_>,
1809) -> Option<f64> {
1810 match expr {
1811 NumberExpr::Literal(n) => Some(*n),
1812 NumberExpr::Projection(lhs) => match resolve_ref_lhs(*lhs, r, ctx)? {
1813 Value::Number(n) => Some(n),
1814 _ => None,
1815 },
1816 NumberExpr::Count { domain, filter } => {
1817 Some(eval_count_ref(domain, filter.as_deref(), r, ctx) as f64)
1818 }
1819 NumberExpr::Aggregate { .. }
1820 | NumberExpr::Metric { .. }
1821 | NumberExpr::Entropy(_)
1822 | NumberExpr::Size(_) => None,
1823 }
1824}
1825
1826fn eval_count_ref(
1827 domain: &Domain,
1828 filter: Option<&Node>,
1829 r: &code_moniker_core::core::code_graph::RefRecord,
1830 ctx: &EvalCtx<'_, '_>,
1831) -> u32 {
1832 ref_domain_items(domain, r, ctx)
1833 .into_iter()
1834 .filter(|item| {
1835 let Some(filter) = filter else {
1836 return true;
1837 };
1838 match item {
1839 DomainItem::Ref { record } => {
1840 matches!(
1841 eval_ref_node_with_current(filter, record, r, ctx),
1842 NodeOutcome::Pass
1843 )
1844 }
1845 DomainItem::Def {
1846 idx: Some(idx),
1847 def,
1848 } => matches!(
1849 eval_node_with_self(filter, def, *idx, r.source, ctx),
1850 NodeOutcome::Pass
1851 ),
1852 DomainItem::Def { idx: None, def } => {
1853 matches!(eval_external_def_node(filter, def, ctx), NodeOutcome::Pass)
1854 }
1855 DomainItem::Segment { kind, name } => {
1856 matches!(eval_node_segment(filter, kind, name), NodeOutcome::Pass)
1857 }
1858 }
1859 })
1860 .count() as u32
1861}
1862
1863fn eval_number_expr_segment(expr: &NumberExpr) -> Option<f64> {
1864 match expr {
1865 NumberExpr::Literal(n) => Some(*n),
1866 NumberExpr::Projection(_)
1867 | NumberExpr::Count { .. }
1868 | NumberExpr::Aggregate { .. }
1869 | NumberExpr::Metric { .. }
1870 | NumberExpr::Entropy(_)
1871 | NumberExpr::Size(_) => None,
1872 }
1873}
1874
1875fn count_children_filtered(
1876 _d: &DefRecord,
1877 def_idx: usize,
1878 self_idx: usize,
1879 kind: &str,
1880 filter: &Node,
1881 ctx: &EvalCtx<'_, '_>,
1882) -> u32 {
1883 let Some(child_idxs) = ctx.children_by_parent.get(&def_idx) else {
1884 return 0;
1885 };
1886 let mut n = 0;
1887 for &ci in child_idxs {
1888 let cd = ctx.graph.def_at(ci);
1889 if cd.kind.as_ref() != kind.as_bytes() {
1890 continue;
1891 }
1892 if let NodeOutcome::Pass = eval_node_with_self(filter, cd, ci, self_idx, ctx) {
1893 n += 1;
1894 }
1895 }
1896 n
1897}
1898
1899fn count_children_by_shape(
1900 def_idx: usize,
1901 self_idx: usize,
1902 shape: &str,
1903 filter: Option<&Node>,
1904 ctx: &EvalCtx<'_, '_>,
1905) -> u32 {
1906 let Some(child_idxs) = ctx.children_by_parent.get(&def_idx) else {
1907 return 0;
1908 };
1909 let mut n = 0;
1910 for &ci in child_idxs {
1911 let cd = ctx.graph.def_at(ci);
1912 if !def_has_shape(cd, shape) {
1913 continue;
1914 }
1915 match filter {
1916 None => n += 1,
1917 Some(node) => {
1918 if let NodeOutcome::Pass = eval_node_with_self(node, cd, ci, self_idx, ctx) {
1919 n += 1;
1920 }
1921 }
1922 }
1923 }
1924 n
1925}
1926
1927fn def_has_shape(d: &DefRecord, shape: &str) -> bool {
1928 d.shape().is_some_and(|actual| actual.as_str() == shape)
1929}
1930
1931fn count_segments(d: &DefRecord, filter: Option<&Node>) -> u32 {
1932 let mut n = 0;
1933 for seg in d.moniker.as_view().segments() {
1934 match filter {
1935 None => n += 1,
1936 Some(node) => {
1937 if let NodeOutcome::Pass = eval_node_segment(node, seg.kind, seg.name) {
1938 n += 1;
1939 }
1940 }
1941 }
1942 }
1943 n
1944}
1945
1946fn count_out_refs(
1947 _d: &DefRecord,
1948 def_idx: usize,
1949 filter: Option<&Node>,
1950 ctx: &EvalCtx<'_, '_>,
1951) -> u32 {
1952 let Some(ref_idxs) = ctx.out_refs_by_source.get(&def_idx) else {
1953 return 0;
1954 };
1955 let mut n = 0;
1956 for &ri in ref_idxs {
1957 let r = ctx.graph.ref_at(ri);
1958 match filter {
1959 None => n += 1,
1960 Some(node) => {
1961 if let NodeOutcome::Pass = eval_ref_node(node, r, ctx) {
1962 n += 1;
1963 }
1964 }
1965 }
1966 }
1967 n
1968}
1969
1970fn count_in_refs(d: &DefRecord, filter: Option<&Node>, ctx: &EvalCtx<'_, '_>) -> u32 {
1971 let key = d.moniker.as_encoded();
1972 let Some(ref_idxs) = ctx.in_refs_by_target.get(key) else {
1973 return 0;
1974 };
1975 let mut n = 0;
1976 for &ri in ref_idxs {
1977 let r = ctx.graph.ref_at(ri);
1978 match filter {
1979 None => n += 1,
1980 Some(node) => {
1981 if let NodeOutcome::Pass = eval_ref_node(node, r, ctx) {
1982 n += 1;
1983 }
1984 }
1985 }
1986 }
1987 n
1988}
1989
1990fn eval_quantifier_def(
1992 kind: QuantKind,
1993 domain: &Domain,
1994 filter: &Node,
1995 scope: DefScope<'_>,
1996 self_idx: usize,
1997 ctx: &EvalCtx<'_, '_>,
1998) -> NodeOutcome {
1999 let mut total = 0u32;
2000 let mut passes = 0u32;
2001 match domain {
2002 Domain::Children(_) | Domain::ChildrenByShape(_) | Domain::Descendants(_) => {
2003 match eval_def_domain_quantifier(kind, domain, filter, scope.idx, self_idx, ctx) {
2004 Ok((domain_total, domain_passes)) => {
2005 total = domain_total;
2006 passes = domain_passes;
2007 }
2008 Err(outcome) => return *outcome,
2009 }
2010 }
2011 Domain::Pairs(inner) => {
2012 return eval_pair_quantifier(kind, inner, filter, scope.idx, self_idx, ctx);
2013 }
2014 Domain::Segments => {
2015 for seg in scope.record.moniker.as_view().segments() {
2016 total += 1;
2017 if matches!(
2018 eval_node_segment(filter, seg.kind, seg.name),
2019 NodeOutcome::Pass
2020 ) {
2021 passes += 1;
2022 }
2023 }
2024 }
2025 Domain::OutRefs | Domain::SourceOutRefs | Domain::SourceAncestorOutRefs => {
2026 let empty = Vec::new();
2027 let ancestor_refs;
2028 let ref_idxs: &[usize] = if matches!(domain, Domain::SourceAncestorOutRefs) {
2029 ancestor_refs = ancestor_ref_indexes(scope.idx, ctx, true);
2030 &ancestor_refs
2031 } else {
2032 ctx.out_refs_by_source.get(&scope.idx).unwrap_or(&empty)
2033 };
2034 for &ri in ref_idxs {
2035 let r = ctx.graph.ref_at(ri);
2036 total += 1;
2037 if matches!(eval_ref_node(filter, r, ctx), NodeOutcome::Pass) {
2038 passes += 1;
2039 }
2040 }
2041 }
2042 Domain::InRefs | Domain::SourceInRefs | Domain::SourceAncestorInRefs => {
2043 let empty = Vec::new();
2044 let ancestor_refs;
2045 let ref_idxs: &[usize] = if matches!(domain, Domain::SourceAncestorInRefs) {
2046 ancestor_refs = ancestor_ref_indexes(scope.idx, ctx, false);
2047 &ancestor_refs
2048 } else {
2049 let key = scope.record.moniker.as_encoded();
2050 ctx.in_refs_by_target.get(key).unwrap_or(&empty)
2051 };
2052 for &ri in ref_idxs {
2053 let r = ctx.graph.ref_at(ri);
2054 total += 1;
2055 if matches!(eval_ref_node(filter, r, ctx), NodeOutcome::Pass) {
2056 passes += 1;
2057 }
2058 }
2059 }
2060 }
2061 let label = match kind {
2062 QuantKind::Any => "any",
2063 QuantKind::All => "all",
2064 QuantKind::None => "none",
2065 };
2066 let ok = match kind {
2067 QuantKind::Any => passes > 0,
2068 QuantKind::All => total == 0 || passes == total,
2069 QuantKind::None => passes == 0,
2070 };
2071 if ok {
2072 NodeOutcome::Pass
2073 } else {
2074 NodeOutcome::Fail(Failure {
2075 atom_raw: format!("{label}(...)"),
2076 lhs_label: label.to_string(),
2077 actual: format!("{passes}/{total}"),
2078 expected: match kind {
2079 QuantKind::Any => "≥ 1 match".to_string(),
2080 QuantKind::All => "all match".to_string(),
2081 QuantKind::None => "zero matches".to_string(),
2082 },
2083 def_idx: None,
2084 details: None,
2085 })
2086 }
2087}
2088
2089fn eval_def_domain_quantifier(
2090 kind: QuantKind,
2091 domain: &Domain,
2092 filter: &Node,
2093 def_idx: usize,
2094 self_idx: usize,
2095 ctx: &EvalCtx<'_, '_>,
2096) -> Result<(u32, u32), Box<NodeOutcome>> {
2097 let mut total = 0u32;
2098 let mut passes = 0u32;
2099 for item in domain_items(domain, def_idx, ctx) {
2100 let DomainItem::Def { idx, def } = item else {
2101 continue;
2102 };
2103 total += 1;
2104 let outcome = match idx {
2105 Some(idx) => eval_node_with_self(filter, def, idx, self_idx, ctx),
2106 None => eval_external_def_node(filter, def, ctx),
2107 };
2108 match outcome {
2109 NodeOutcome::Pass => passes += 1,
2110 NodeOutcome::Fail(mut failure) if kind == QuantKind::All => {
2111 if let Some(idx) = idx {
2112 failure.def_idx.get_or_insert(idx);
2113 }
2114 return Err(Box::new(NodeOutcome::Fail(failure)));
2115 }
2116 NodeOutcome::Fail(_) | NodeOutcome::NotApplicable => {}
2117 }
2118 }
2119 Ok((total, passes))
2120}
2121
2122fn eval_external_def_node(node: &Node, def: &DefRecord, ctx: &EvalCtx<'_, '_>) -> NodeOutcome {
2123 walk_node(
2124 node,
2125 &|atom| eval_external_def_atom(atom, def, ctx),
2126 &|_, _, _| NodeOutcome::NotApplicable,
2127 &|_| NodeOutcome::NotApplicable,
2128 &|_| NodeOutcome::NotApplicable,
2129 )
2130}
2131
2132fn eval_external_def_atom(atom: &Atom, def: &DefRecord, ctx: &EvalCtx<'_, '_>) -> AtomOutcome {
2133 let LhsExpr::Attr(lhs) = &atom.lhs else {
2134 return AtomOutcome::NotApplicable;
2135 };
2136 let Some(value) = project_def_lhs_value(None, def, *lhs, ctx) else {
2137 return AtomOutcome::NotApplicable;
2138 };
2139 if let Rhs::Projection(rhs) = &atom.rhs
2140 && let Some(rhs_value) = project_def_lhs_value(None, def, *rhs, ctx)
2141 {
2142 return apply_op_values(&value, atom.op, &rhs_value);
2143 }
2144 apply_op(&value, atom)
2145}
2146
2147fn eval_node_segment(node: &Node, seg_kind: &[u8], seg_name: &[u8]) -> NodeOutcome {
2148 walk_node(
2149 node,
2150 &|a| eval_atom_segment(a, seg_kind, seg_name),
2151 &|_, _, _| NodeOutcome::NotApplicable,
2152 &|_| NodeOutcome::NotApplicable,
2153 &|_| NodeOutcome::NotApplicable,
2154 )
2155}
2156
2157fn eval_atom_segment(atom: &Atom, seg_kind: &[u8], seg_name: &[u8]) -> AtomOutcome {
2158 let value: Value = match &atom.lhs {
2159 LhsExpr::Attr(Lhs::SegmentKind) => Value::Str(
2160 std::str::from_utf8(seg_kind)
2161 .unwrap_or_default()
2162 .to_string(),
2163 ),
2164 LhsExpr::Attr(Lhs::SegmentName) => Value::Str(
2165 std::str::from_utf8(seg_name)
2166 .unwrap_or_default()
2167 .to_string(),
2168 ),
2169 _ => return AtomOutcome::NotApplicable,
2170 };
2171 if let Rhs::Projection(other) = &atom.rhs {
2172 let rhs_val = match other {
2173 Lhs::SegmentKind => Value::Str(
2174 std::str::from_utf8(seg_kind)
2175 .unwrap_or_default()
2176 .to_string(),
2177 ),
2178 Lhs::SegmentName => Value::Str(
2179 std::str::from_utf8(seg_name)
2180 .unwrap_or_default()
2181 .to_string(),
2182 ),
2183 _ => return AtomOutcome::NotApplicable,
2184 };
2185 return apply_op_values(&value, atom.op, &rhs_val);
2186 }
2187 if let Rhs::Number(expr) = &atom.rhs {
2188 let Some(rhs_val) = eval_number_expr_segment(expr).map(Value::Number) else {
2189 return AtomOutcome::NotApplicable;
2190 };
2191 return apply_op_values(&value, atom.op, &rhs_val);
2192 }
2193 apply_op(&value, atom)
2194}
2195
2196fn eval_atom(
2197 atom: &Atom,
2198 d: &DefRecord,
2199 def_idx: usize,
2200 self_idx: usize,
2201 ctx: &EvalCtx<'_, '_>,
2202) -> AtomOutcome {
2203 if let (LhsExpr::Collection(left), Op::Subset, Rhs::Collection(right)) =
2204 (&atom.lhs, atom.op, &atom.rhs)
2205 {
2206 if collection_has_pair_binding(left) || collection_has_pair_binding(right) {
2207 return AtomOutcome::NotApplicable;
2208 }
2209 return if eval_collection_subset(left, right, def_idx, self_idx, ctx) {
2210 AtomOutcome::Pass
2211 } else {
2212 AtomOutcome::Fail {
2213 actual: "not subset".to_string(),
2214 expected: "subset".to_string(),
2215 }
2216 };
2217 }
2218 let value: Value = match &atom.lhs {
2219 LhsExpr::Attr(lhs) => {
2220 let Some(value) = resolve_def_lhs(*lhs, d, ctx) else {
2221 return AtomOutcome::NotApplicable;
2222 };
2223 value
2224 }
2225 LhsExpr::Number(expr) => {
2226 let Some(n) = eval_number_expr_def(expr, d, def_idx, self_idx, ctx) else {
2227 return AtomOutcome::NotApplicable;
2228 };
2229 Value::Number(n)
2230 }
2231 LhsExpr::Mode(collection) => {
2232 let Some(value) = eval_mode(collection, def_idx, self_idx, ctx) else {
2233 return AtomOutcome::NotApplicable;
2234 };
2235 value
2236 }
2237 LhsExpr::PairProjection(_) => return AtomOutcome::NotApplicable,
2238 LhsExpr::SegmentOf { scope, kind } => match scope {
2239 SegmentScope::Def => Value::Str(first_segment_name(&d.moniker, kind.as_bytes())),
2240 SegmentScope::Source | SegmentScope::Target => {
2241 return AtomOutcome::NotApplicable;
2242 }
2243 },
2244 LhsExpr::Collection(_) => return AtomOutcome::NotApplicable,
2245 };
2246 if let Rhs::Projection(other) = &atom.rhs {
2247 let Some(rhs_val) = resolve_def_lhs(*other, d, ctx) else {
2248 return AtomOutcome::NotApplicable;
2249 };
2250 return apply_op_values(&value, atom.op, &rhs_val);
2251 }
2252 if let Rhs::CurrentProjection(other) = &atom.rhs {
2253 let Some(rhs_val) = resolve_def_lhs(*other, ctx.graph.def_at(self_idx), ctx) else {
2254 return AtomOutcome::NotApplicable;
2255 };
2256 return apply_op_values(&value, atom.op, &rhs_val);
2257 }
2258 if let Rhs::Number(expr) = &atom.rhs {
2259 let Some(rhs_val) =
2260 eval_number_expr_def(expr, d, def_idx, self_idx, ctx).map(Value::Number)
2261 else {
2262 return AtomOutcome::NotApplicable;
2263 };
2264 return apply_op_values(&value, atom.op, &rhs_val);
2265 }
2266 apply_op(&value, atom)
2267}
2268
2269fn children_by_parent(graph: &CodeGraph) -> HashMap<usize, Vec<usize>> {
2270 let mut m: HashMap<usize, Vec<usize>> = HashMap::new();
2271 for (idx, d) in graph.defs().enumerate() {
2272 if let Some(p) = d.parent {
2273 m.entry(p).or_default().push(idx);
2274 }
2275 }
2276 m
2277}
2278
2279fn out_refs_by_source(graph: &CodeGraph) -> HashMap<usize, Vec<usize>> {
2280 let mut m: HashMap<usize, Vec<usize>> = HashMap::new();
2281 for (idx, r) in graph.refs().enumerate() {
2282 m.entry(r.source).or_default().push(idx);
2283 }
2284 m
2285}
2286
2287fn in_refs_by_target(graph: &CodeGraph) -> HashMap<Vec<u8>, Vec<usize>> {
2288 let mut m: HashMap<Vec<u8>, Vec<usize>> = HashMap::new();
2289 for (idx, r) in graph.refs().enumerate() {
2290 m.entry(r.target.as_encoded().to_vec())
2291 .or_default()
2292 .push(idx);
2293 }
2294 m
2295}
2296
2297fn parent_counts_by_kind(graph: &CodeGraph) -> HashMap<(usize, &[u8]), u32> {
2298 let mut m: HashMap<(usize, &[u8]), u32> = HashMap::new();
2299 for d in graph.defs() {
2300 if let Some(p) = d.parent {
2301 *m.entry((p, d.kind.as_ref())).or_insert(0) += 1;
2302 }
2303 }
2304 m
2305}
2306
2307fn comment_end_bytes(graph: &CodeGraph) -> Vec<u32> {
2308 let mut v: Vec<u32> = graph
2309 .defs()
2310 .filter(|d| d.kind.as_ref() == KIND_COMMENT)
2311 .filter_map(|d| d.position.map(|(_, e)| e))
2312 .collect();
2313 v.sort_unstable();
2314 v
2315}
2316
2317fn doc_anchors_by_def(graph: &CodeGraph) -> HashMap<usize, u32> {
2321 let mut m: HashMap<usize, u32> = HashMap::new();
2322 for r in graph.refs() {
2323 if r.kind != b"annotates" {
2324 continue;
2325 }
2326 let Some((start, _)) = r.position else {
2327 continue;
2328 };
2329 m.entry(r.source)
2330 .and_modify(|cur| {
2331 if start < *cur {
2332 *cur = start;
2333 }
2334 })
2335 .or_insert(start);
2336 }
2337 m
2338}
2339
2340fn comment_attaches_to(source: &str, comment_end: u32, header_start: u32) -> bool {
2341 if comment_end > header_start {
2342 return false;
2343 }
2344 let last_comment_byte = comment_end.saturating_sub(1);
2345 let (cl, _) = line_range(source, last_comment_byte, last_comment_byte + 1);
2346 let (hl, _) = line_range(source, header_start, header_start + 1);
2347 hl == cl || hl == cl + 1
2348}
2349
2350fn check_require_doc_comment(
2351 target: RuleTarget<'_>,
2352 rules: &CompiledKindRules,
2353 ctx: &EvalCtx<'_, '_>,
2354 out: &mut Vec<Violation>,
2355) {
2356 check_require_doc_comment_with_id(
2357 target,
2358 rules,
2359 rule_id(ctx.lang, target.kind, "require_doc_comment"),
2360 ctx,
2361 out,
2362 );
2363}
2364
2365fn check_require_doc_comment_with_id(
2366 target: RuleTarget<'_>,
2367 rules: &CompiledKindRules,
2368 rule_id: String,
2369 ctx: &EvalCtx<'_, '_>,
2370 out: &mut Vec<Violation>,
2371) {
2372 if eval_require_doc_comment(target.scope.record, target.scope.idx, rules, ctx) != Some(false) {
2373 return;
2374 }
2375
2376 let moniker = to_uri(&target.scope.record.moniker, &ctx.uri_cfg);
2377 let name = def_name(target.scope.record).unwrap_or_default();
2378 let (start_line, end_line) = lines_of(target.scope.record, ctx.source);
2379 out.push(Violation {
2380 rule_id,
2381 severity: RuleSeverity::Error,
2382 moniker,
2383 kind: target.kind.to_string(),
2384 lines: (start_line, end_line),
2385 message: format!(
2386 "{} `{name}` is missing a doc comment immediately before it",
2387 target.kind
2388 ),
2389 explanation: None,
2390 });
2391}
2392
2393fn eval_require_doc_comment(
2394 d: &DefRecord,
2395 def_idx: usize,
2396 rules: &CompiledKindRules,
2397 ctx: &EvalCtx<'_, '_>,
2398) -> Option<bool> {
2399 let filter = rules.require_doc_for_vis.as_ref()?;
2400 let vis = std::str::from_utf8(&d.visibility).unwrap_or("");
2401 if filter != "any" && filter != vis {
2402 return None;
2403 }
2404 let (def_start, _) = d.position?;
2405 let header_start = ctx
2406 .doc_anchors
2407 .get(&def_idx)
2408 .copied()
2409 .map(|anc| anc.min(def_start))
2410 .unwrap_or(def_start);
2411
2412 let idx = ctx.comment_ends.partition_point(|&end| end <= header_start);
2413 let has_doc =
2414 idx > 0 && comment_attaches_to(ctx.source, ctx.comment_ends[idx - 1], header_start);
2415 Some(has_doc)
2416}
2417
2418#[cfg(test)]
2419mod tests;