cccc_core/ir.rs
1//! The language-agnostic intermediate representation (IR) that the complexity
2//! engine scores.
3//!
4//! A language adapter (e.g. `cccc-typescript`) lowers its native AST into a
5//! `Vec<Node>` describing only the constructs that affect Cognitive / Cyclomatic
6//! Complexity — branches, loops, switches, exception handlers, logical-operator
7//! sequences, function boundaries, and calls. Everything else collapses into
8//! [`Node::Group`], which is a transparent container the engine simply recurses
9//! into. All scoring rules live in [`crate::engine`]; the IR carries no scores.
10//!
11//! The top level handed to the engine is itself a `&[Node]`, representing
12//! module-level code; the engine scores it under an implicit module frame.
13
14/// A logical operator, normalized across languages.
15///
16/// The adapter is responsible for folding a run of like operators into a single
17/// [`Node::Logical`] (e.g. `a && b && c` is one `Logical` with three operands);
18/// the engine counts one cognitive point per `Logical` node.
19#[derive(Debug, Clone, Copy, PartialEq, Eq)]
20pub enum LogicalOp {
21 And,
22 Or,
23 /// Nullish coalescing (`??`).
24 Coalesce,
25}
26
27/// One arm of a [`Node::Switch`].
28#[derive(Debug, Clone)]
29pub struct SwitchCase {
30 /// `true` for the `default` arm, which is not a cyclomatic decision point.
31 pub is_default: bool,
32 pub body: Vec<Node>,
33}
34
35/// A node of the normalized complexity IR.
36///
37/// Fields that hold sub-expressions or sub-statements are `Vec<Node>` so the
38/// adapter can drop irrelevant detail and the engine can recurse uniformly.
39#[derive(Debug, Clone)]
40pub enum Node {
41 /// A function-like unit (function, method, arrow, accessor, …). The engine
42 /// scores each one independently — nesting resets to 0 at this boundary —
43 /// and reports it as a child of the enclosing unit. `kind`/`name` are
44 /// opaque, adapter-chosen labels (the engine never interprets them).
45 Function {
46 name: String,
47 kind: String,
48 /// 1-based line where the unit starts.
49 line: u32,
50 body: Vec<Node>,
51 },
52
53 /// An `if` (with optional `else` / `else if`). `then` is scored with a
54 /// nesting bonus; `alternate` (a nested `Branch` for `else if`, or any other
55 /// node for a plain `else`) is scored flat — one cognitive point, no bonus.
56 Branch {
57 test: Vec<Node>,
58 then: Vec<Node>,
59 alternate: Option<Box<Node>>,
60 },
61
62 /// A ternary `?:`. Scored like a branch: +1 plus nesting bonus.
63 Conditional {
64 test: Vec<Node>,
65 then: Vec<Node>,
66 alternate: Vec<Node>,
67 },
68
69 /// Any loop (`for` / `for-in` / `for-of` / `while` / `do-while`), normalized
70 /// to one shape. +1 plus nesting bonus, and a cyclomatic point.
71 Loop { body: Vec<Node> },
72
73 /// A `switch`. +1 plus nesting bonus (but the switch itself is not a McCabe
74 /// decision point — each non-default `case` is, scored via [`SwitchCase`]).
75 Switch { cases: Vec<SwitchCase> },
76
77 /// A `catch` clause. +1 plus nesting bonus, and a cyclomatic point.
78 Catch { body: Vec<Node> },
79
80 /// A `break` / `continue`. Only a *labelled* jump adds one cognitive point.
81 Jump { labeled: bool },
82
83 /// A run of like logical operators. One cognitive point per node; the
84 /// adapter folds `a && b && c` into a single node with three operands.
85 Logical { op: LogicalOp, operands: Vec<Node> },
86
87 /// A function call. If `callee` names the nearest enclosing function, the
88 /// engine counts it as recursion (one cognitive point).
89 Call { callee: Option<String> },
90
91 /// A transparent container for any construct that holds children but carries
92 /// no score of its own (statements, expressions, blocks). The engine simply
93 /// recurses into it.
94 Group(Vec<Node>),
95}