GCC Code Coverage Report


src/
Coverage:
low: ≥ 0%
medium: ≥ 75.0%
high: ≥ 90.0%
Lines:
26 / 26
100.0%
Branches:
13 / 18
72.2%
Functions:
4 / 4
100.0%

linter/rules/cyclomatic-complexity-rule.spice
Line Branch Exec Source
1 // Copyright (c) 2021-2026 ChilliBits. All rights reserved.
2
3 // Std imports
4 import "std/type/type-conversion";
5 import "std/data/vector";
6
7 // Own imports
8 import "@/ast/ast-nodes";
9 import "@/linter/lint-finding";
10
11 // Flags functions/procedures whose body has grown too many independent branches to review or test with
12 // confidence in one piece. McCabe cyclomatic complexity: starts at 1 for the single straight-line path through
13 // the body, +1 for every `if`, loop, and `case` branch. A style/maintainability check, not a correctness one,
14 // so it lives in `spice lint` rather than as a compiler warning.
15 const string CYCLOMATIC_COMPLEXITY_RULE_ID = "cyclomatic-complexity";
16
17 // McCabe's original recommendation for the complexity ceiling of a single routine.
18 const long MAX_CYCLOMATIC_COMPLEXITY = 10l;
19
20 /**
21 * Count the decision points in a subtree (each if/loop/case branch counts as 1). Walks into every descendant,
22 * including nested lambda bodies, as a deliberate simplification: a lambda dense enough to matter on its own
23 * will usually also push the enclosing function over the threshold.
24 *
25 * @param node Root of the subtree to walk
26 * @return Number of decision points found
27 */
28 201 f<long> countDecisionPoints(ASTNode* node) {
29 201 long count = 0l;
30 201 const ASTNodeKind kind = node.kind;
31
4/6
✓ Branch 0 taken 190 times.
✓ Branch 1 taken 11 times.
✓ Branch 2 taken 190 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 190 times.
✗ Branch 5 not taken.
201 if kind == ASTNodeKind::IF_STMT || kind == ASTNodeKind::FOR_LOOP || kind == ASTNodeKind::FOREACH_LOOP ||
32
4/6
✓ Branch 0 taken 190 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✓ Branch 3 taken 190 times.
✓ Branch 4 taken 190 times.
✓ Branch 5 taken 11 times.
201 kind == ASTNodeKind::WHILE_LOOP || kind == ASTNodeKind::DO_WHILE_LOOP || kind == ASTNodeKind::CASE_BRANCH {
33 11 count++;
34 11 }
35
2/2
✓ Branch 0 taken 201 times.
✓ Branch 1 taken 191 times.
392 foreach ASTNode* child : node.getChildren() {
36 191 count += countDecisionPoints(child);
37 191 }
38 201 return count;
39 }
40
41 /**
42 * Emit a finding if the given function/procedure body's cyclomatic complexity exceeds MAX_CYCLOMATIC_COMPLEXITY.
43 *
44 * @param fctDefBase Function/procedure base node
45 * @param kindLabel "Function" or "Procedure", for the finding message
46 * @param findings Findings vector to append to
47 */
48 10 p checkComplexity(ASTFctDefBaseNode& fctDefBase, const string kindLabel, Vector<LintFinding>& findings) {
49 // No body (e.g. declarations without a body) means nothing to measure
50
1/2
✓ Branch 0 taken 10 times.
✗ Branch 1 not taken.
10 if fctDefBase.body == nil<IVisitable*> { return; }
51 10 const long complexity = 1l + countDecisionPoints(getASTNode(fctDefBase.body));
52
2/2
✓ Branch 0 taken 9 times.
✓ Branch 1 taken 1 time.
10 if complexity > MAX_CYCLOMATIC_COMPLEXITY {
53 1 const String message = String(kindLabel) + " '" + fctDefBase.name.name + "' has a cyclomatic complexity of " +
54 1 toString(complexity) + " (max " + toString(MAX_CYCLOMATIC_COMPLEXITY) + "); consider splitting it up";
55 1 findings.pushBack(LintFinding(fctDefBase.codeLoc, CYCLOMATIC_COMPLEXITY_RULE_ID, LintSeverity::WARNING, message));
56 1 }
57 10 }
58
59 7 public p checkComplexityFctDef(ASTFctDefNode* node, Vector<LintFinding>& findings) {
60 7 checkComplexity(node.node, "Function", findings);
61 7 }
62
63 3 public p checkComplexityProcDef(ASTProcDefNode* node, Vector<LintFinding>& findings) {
64 3 checkComplexity(node.node, "Procedure", findings);
65 3 }
66