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