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