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