codecaliper 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (46) hide show
  1. codecaliper/__init__.py +64 -0
  2. codecaliper/_version.py +1 -0
  3. codecaliper/api.py +446 -0
  4. codecaliper/canonical.py +130 -0
  5. codecaliper/cli.py +231 -0
  6. codecaliper/errors.py +27 -0
  7. codecaliper/languages/__init__.py +44 -0
  8. codecaliper/languages/base.py +162 -0
  9. codecaliper/languages/java.py +192 -0
  10. codecaliper/languages/python.py +179 -0
  11. codecaliper/metrics/__init__.py +0 -0
  12. codecaliper/metrics/base.py +55 -0
  13. codecaliper/metrics/cognitive.py +111 -0
  14. codecaliper/metrics/cyclomatic.py +62 -0
  15. codecaliper/metrics/halstead.py +55 -0
  16. codecaliper/metrics/loc.py +62 -0
  17. codecaliper/metrics/mi.py +31 -0
  18. codecaliper/model.py +132 -0
  19. codecaliper/py.typed +0 -0
  20. codecaliper/readability/__init__.py +13 -0
  21. codecaliper/readability/base.py +11 -0
  22. codecaliper/readability/bw2010.py +156 -0
  23. codecaliper/readability/granularity.py +210 -0
  24. codecaliper/readability/retrain.py +67 -0
  25. codecaliper/spec/__init__.py +3 -0
  26. codecaliper/spec/registry.py +112 -0
  27. codecaliper/spec/rulings/bw.toml +156 -0
  28. codecaliper/spec/rulings/cognitive.toml +124 -0
  29. codecaliper/spec/rulings/core.toml +87 -0
  30. codecaliper/spec/rulings/cyclomatic.toml +172 -0
  31. codecaliper/spec/rulings/halstead.toml +20 -0
  32. codecaliper/spec/rulings/index.toml +54 -0
  33. codecaliper/spec/rulings/loc.toml +50 -0
  34. codecaliper/spec/rulings/mi.toml +15 -0
  35. codecaliper/spec/rulings/tokenization.toml +191 -0
  36. codecaliper/spec/validated_grammars.toml +6 -0
  37. codecaliper/syntax/__init__.py +0 -0
  38. codecaliper/syntax/_treesitter.py +179 -0
  39. codecaliper/syntax/grammars.py +45 -0
  40. codecaliper/syntax/tokens.py +165 -0
  41. codecaliper-0.1.0.dist-info/METADATA +180 -0
  42. codecaliper-0.1.0.dist-info/RECORD +46 -0
  43. codecaliper-0.1.0.dist-info/WHEEL +4 -0
  44. codecaliper-0.1.0.dist-info/entry_points.txt +2 -0
  45. codecaliper-0.1.0.dist-info/licenses/LICENSE +21 -0
  46. codecaliper-0.1.0.dist-info/licenses/NOTICE +29 -0
@@ -0,0 +1,112 @@
1
+ """The ruling registry: loads the versioned metric-to-syntax mapping spec.
2
+
3
+ Rulings are machine-readable TOML package data with immutable IDs
4
+ (``{METRIC}-{LANG}-{NNNN}``). ``require()`` raises :class:`SpecError` the moment
5
+ code cites a phantom ID, so the code and the spec cannot drift apart silently
6
+ (ARCHITECTURE.md §3.3).
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import sys
12
+ from dataclasses import dataclass
13
+ from functools import lru_cache
14
+ from importlib import resources
15
+ from typing import Any
16
+
17
+ from codecaliper.errors import SpecError
18
+
19
+ if sys.version_info >= (3, 11):
20
+ import tomllib
21
+ else: # pragma: no cover - py3.10 fallback
22
+ import tomli as tomllib
23
+
24
+ _RULING_FILES = (
25
+ "core.toml",
26
+ "tokenization.toml",
27
+ "cyclomatic.toml",
28
+ "cognitive.toml",
29
+ "halstead.toml",
30
+ "mi.toml",
31
+ "loc.toml",
32
+ "bw.toml",
33
+ )
34
+
35
+
36
+ @dataclass(frozen=True, slots=True)
37
+ class Ruling:
38
+ id: str
39
+ metric: str
40
+ language: str # "all" | "python" | "java" | ...
41
+ title: str
42
+ statement: str
43
+ status: str = "active" # active | draft | superseded
44
+ since_spec: str = "0.1.0"
45
+ mode: str = "" # cognitive only: "whitepaper" | "sonar-compat" | "" (both)
46
+ node_types: tuple[str, ...] = () # tree-sitter node types this ruling binds to
47
+ examples: tuple[str, ...] = () # normative consistency-corpus case IDs
48
+ superseded_by: str = ""
49
+
50
+
51
+ def _load_toml(name: str) -> dict[str, Any]:
52
+ ref = resources.files("codecaliper.spec") / "rulings" / name
53
+ with ref.open("rb") as f:
54
+ data: dict[str, Any] = tomllib.load(f)
55
+ return data
56
+
57
+
58
+ @lru_cache(maxsize=1)
59
+ def spec_version() -> str:
60
+ return str(_load_toml("index.toml")["spec"]["version"])
61
+
62
+
63
+ @lru_cache(maxsize=1)
64
+ def _registry() -> dict[str, Ruling]:
65
+ rulings: dict[str, Ruling] = {}
66
+ for fname in _RULING_FILES:
67
+ data = _load_toml(fname)
68
+ for raw in data.get("ruling", []):
69
+ r = Ruling(
70
+ id=raw["id"],
71
+ metric=raw["metric"],
72
+ language=raw["language"],
73
+ title=raw["title"],
74
+ statement=raw["statement"].strip(),
75
+ status=raw.get("status", "active"),
76
+ since_spec=raw.get("since_spec", "0.1.0"),
77
+ mode=raw.get("mode", ""),
78
+ node_types=tuple(raw.get("node_types", [])),
79
+ examples=tuple(raw.get("examples", [])),
80
+ superseded_by=raw.get("superseded_by", ""),
81
+ )
82
+ if r.id in rulings:
83
+ raise SpecError(f"duplicate ruling id {r.id!r} in {fname}")
84
+ rulings[r.id] = r
85
+ return rulings
86
+
87
+
88
+ def require(ruling_id: str) -> str:
89
+ """Assert the ruling exists and return its ID (call at module import time)."""
90
+ if ruling_id not in _registry():
91
+ raise SpecError(
92
+ f"code cites phantom ruling {ruling_id!r}; add it to the spec or fix the citation"
93
+ )
94
+ return ruling_id
95
+
96
+
97
+ def ruling(ruling_id: str) -> Ruling:
98
+ try:
99
+ return _registry()[ruling_id]
100
+ except KeyError:
101
+ raise SpecError(f"unknown ruling {ruling_id!r}") from None
102
+
103
+
104
+ def iter_rulings(metric: str | None = None, language: str | None = None) -> tuple[Ruling, ...]:
105
+ out = []
106
+ for r in _registry().values():
107
+ if metric is not None and r.metric != metric:
108
+ continue
109
+ if language is not None and r.language not in (language, "all"):
110
+ continue
111
+ out.append(r)
112
+ return tuple(out)
@@ -0,0 +1,156 @@
1
+ # BW-*: Buse-Weimer (2010) Fig. 6 feature delimitation rulings.
2
+ # The paper is ambiguous about token delimitation, comment counting and
3
+ # normalization (ARCHITECTURE.md §7); every ambiguity is pinned here. Rulings marked
4
+ # PROVISIONAL will be arbitrated empirically by the ARCHITECTURE.md §8.3 faithfulness pipeline
5
+ # and superseded if the arbitration disagrees — with the experiment recorded.
6
+
7
+ [[ruling]]
8
+ id = "BW-ALL-0001"
9
+ metric = "bw2010"
10
+ language = "all"
11
+ title = "The 25 features, canonical order, raw vector only"
12
+ statement = """
13
+ The feature set is the 25 features of Buse & Weimer 2010 Fig. 6, emitted in the
14
+ canonical order inherited from the reference implementation
15
+ (avg_line_length ... max_identifier_occurrences). Output is always the raw
16
+ feature vector; no scalar readability score exists anywhere in this tool
17
+ (ARCHITECTURE.md §13). Average features divide by the number of lines; max features
18
+ take the maximum.
19
+ """
20
+ examples = ["py-cc-boolop-001"]
21
+
22
+ [[ruling]]
23
+ id = "BW-ALL-0002"
24
+ metric = "bw2010"
25
+ language = "all"
26
+ title = "Granularity labelling and the calibrated regime"
27
+ statement = """
28
+ granularity is one of snippet | function | file; anything other than snippet is
29
+ extrapolation beyond the model's native unit and is labelled with
30
+ extrapolated=true plus a diagnostic. Even at snippet granularity, a diagnostic
31
+ warns when the unit falls outside the paper's 4-11 line calibrated regime.
32
+ """
33
+ examples = ["java-snippet-constructor-001"]
34
+
35
+ [[ruling]]
36
+ id = "BW-ALL-0003"
37
+ metric = "bw2010"
38
+ language = "all"
39
+ title = "A comment token counts once (PROVISIONAL)"
40
+ statement = """
41
+ avg_comments counts comment TOKENS divided by lines; a multi-line block comment
42
+ counts once (on its start line), mirroring the stdlib-tokenize reference
43
+ behaviour for line comments. PROVISIONAL: per-spanned-line counting is the
44
+ alternative; the faithfulness pipeline will arbitrate.
45
+ """
46
+ examples = ["java-block-comment-001"]
47
+
48
+ [[ruling]]
49
+ id = "BW-ALL-0004"
50
+ metric = "bw2010"
51
+ language = "all"
52
+ title = "Token-level features exclude string contents; character features use raw text"
53
+ statement = """
54
+ Punctuation/operator features (periods, commas, parentheses, arithmetic,
55
+ comparisons, assignments) count TOKENS, so punctuation inside string literals
56
+ and comments is invisible to them. Character-level features (line length,
57
+ indentation, spaces, max_char_occurrences) always use the raw normalized text
58
+ of every line, strings included.
59
+ """
60
+ examples = ["py-multiline-string-001"]
61
+
62
+ [[ruling]]
63
+ id = "BW-ALL-0005"
64
+ metric = "bw2010"
65
+ language = "all"
66
+ title = "Identifier-length feature is the mean of per-line maxima"
67
+ statement = """
68
+ avg_identifier_length = mean over all lines of (the longest identifier on that
69
+ line, 0 for lines without identifiers); max_identifier_length is the global
70
+ maximum. Faithful to the reference implementation's reading of Fig. 6
71
+ ('average/maximum value per line').
72
+ """
73
+ examples = ["py-try-except-001"]
74
+
75
+ [[ruling]]
76
+ id = "BW-ALL-0006"
77
+ metric = "bw2010"
78
+ language = "all"
79
+ title = "Operator classes are adapter tables"
80
+ statement = """
81
+ Which operator spellings count as arithmetic / comparison / assignment is a
82
+ per-language adapter table, part of the spec surface. Short-circuit operators
83
+ (and/or, &&/||) are NOT in these classes: Python's are keywords (counted by
84
+ avg_keywords), Java's are uncategorized operators.
85
+ """
86
+ examples = ["py-comprehension-001"]
87
+
88
+ [[ruling]]
89
+ id = "BW-PY-0001"
90
+ metric = "bw2010"
91
+ language = "python"
92
+ title = "Python keyword set"
93
+ statement = """
94
+ Keywords are keyword.kwlist of the reference CPython (True/False/None
95
+ included). Soft keywords (match/case/type/_) count as identifiers, matching the
96
+ stdlib tokenize reference.
97
+ """
98
+ examples = ["py-try-except-001"]
99
+
100
+ [[ruling]]
101
+ id = "BW-PY-0002"
102
+ metric = "bw2010"
103
+ language = "python"
104
+ title = "Python branch/loop keyword features"
105
+ statement = """
106
+ avg_branches counts if/elif keyword tokens; avg_loops counts for/while keyword
107
+ tokens (including comprehension for/if keywords, which are lexically identical
108
+ - faithful to the reference's lexical reading).
109
+ """
110
+ examples = ["py-elif-chain-001"]
111
+
112
+ [[ruling]]
113
+ id = "BW-JAVA-0001"
114
+ metric = "bw2010"
115
+ language = "java"
116
+ title = "Java keyword set"
117
+ statement = """
118
+ Keywords are the JLS reserved words plus the literals true/false/null —
119
+ procedurally consistent with Python, whose kwlist includes True/False/None.
120
+ (Editorial clarification in spec 1.1.0: the table deliberately omits the
121
+ reserved word `_` — JLS §3.9 lists it since SE 9 — because `_` is ruled an
122
+ identifier token in every position (TOK-JAVA-0002), mirroring BW-PY-0001's
123
+ treatment of Python's `_`. The normative table is unchanged.)
124
+ """
125
+
126
+ [[ruling]]
127
+ id = "BW-JAVA-0002"
128
+ metric = "bw2010"
129
+ language = "java"
130
+ title = "Java branch/loop keyword features"
131
+ statement = """
132
+ avg_branches counts `if` keyword tokens (an else-if contributes via its `if`);
133
+ avg_loops counts for/while/do keyword tokens.
134
+ """
135
+ examples = ["java-elseif-001"]
136
+
137
+ [[ruling]]
138
+ id = "BW-ALL-0007"
139
+ metric = "bw2010"
140
+ language = "all"
141
+ title = "Token-family features use the full lexical stream, ERROR regions included"
142
+ since_spec = "1.0.0"
143
+ statement = """
144
+ The BW construct is lexical: the original instrument was grammar-less and saw
145
+ every token of every (frequently non-compilable) snippet. When the parse tree
146
+ contains errors, token-family BW features are therefore computed over the FULL
147
+ leaf stream, descending into ERROR subtrees (MISSING nodes are zero-width
148
+ fabrications and still yield nothing), and the vector carries a
149
+ bw-lexical-fallback diagnostic. This deliberately scopes CORE-ALL-0002, which
150
+ continues to govern every metric (cyclomatic, cognitive, Halstead, LOC):
151
+ zeroing lexical features on fragments silently changes the construct — a
152
+ parser-based reimplementation degraded on 71/100 original BW snippets before
153
+ this ruling. Arbitrated by the BW faithfulness experiment
154
+ (validation/bw_faithfulness/derived/arbitration_report.md).
155
+ """
156
+ examples = ["py-bw-fallback-001"]
@@ -0,0 +1,124 @@
1
+ # COG-*: cognitive complexity (SonarSource whitepaper, dual-mode).
2
+ # Default mode is whitepaper-faithful; --sonar-compat matches the behaviour of
3
+ # the widely-deployed implementations where they diverge from the paper text.
4
+ # Every inter-mode difference is a mode-tagged ruling: the spec table IS the
5
+ # whitepaper-vs-Sonar diff (ARCHITECTURE.md §0).
6
+
7
+ [[ruling]]
8
+ id = "COG-ALL-0001"
9
+ metric = "cognitive"
10
+ language = "all"
11
+ title = "Structural increments: +1 plus the current nesting level"
12
+ statement = """
13
+ if / loops / catch / switch / ternary each contribute (1 + nesting level at
14
+ their position). Both modes.
15
+ """
16
+ examples = ["py-cc-boolop-001", "java-boolop-catch-001", "py-nested-function-001"]
17
+
18
+ [[ruling]]
19
+ id = "COG-ALL-0002"
20
+ metric = "cognitive"
21
+ language = "all"
22
+ title = "elif/else-if and else are hybrid increments: +1, no nesting penalty"
23
+ statement = """
24
+ An elif/else-if continuation and an if-attached else clause each contribute a
25
+ flat +1 regardless of nesting depth, and an elif does not deepen nesting
26
+ relative to its chain head (chain flattening — Sonar's own rule). Both modes.
27
+ In Java an `else` directly followed by an if_statement is the chain
28
+ continuation and is not additionally counted as an else. ONLY an else attached
29
+ to an if/elif chain is a hybrid increment: Python's for/while/try `else`
30
+ clauses contribute nothing (they are completion handlers, not branch arms).
31
+ """
32
+ examples = ["py-elif-chain-001", "java-elseif-001"]
33
+
34
+ [[ruling]]
35
+ id = "COG-ALL-0003"
36
+ metric = "cognitive"
37
+ language = "all"
38
+ title = "Boolean operator sequences: +1 per sequence of like operators"
39
+ statement = """
40
+ A run of the same short-circuit operator contributes +1 total; each operator
41
+ change starts a new sequence (`a and b or c` = 2; `a && b && c` = 1).
42
+ Parentheses delimit sequences. Both modes.
43
+ """
44
+ examples = ["py-cc-boolop-001", "java-boolop-catch-001"]
45
+
46
+ [[ruling]]
47
+ id = "COG-ALL-0004"
48
+ metric = "cognitive"
49
+ language = "all"
50
+ title = "What deepens nesting"
51
+ statement = """
52
+ Nesting increases inside: if (and its else/elif bodies), loops, catch, switch,
53
+ ternary, and function-like units that are themselves nested inside another
54
+ function (nested functions, lambdas). Nesting does NOT increase inside: try
55
+ bodies, with/synchronized blocks, class bodies, or top-level function bodies.
56
+ Both modes.
57
+ """
58
+ examples = ["py-nested-function-001"]
59
+
60
+ [[ruling]]
61
+ id = "COG-ALL-0005"
62
+ metric = "cognitive"
63
+ language = "all"
64
+ title = "Direct recursion counts +1 per recursive call (whitepaper mode)"
65
+ mode = "whitepaper"
66
+ statement = """
67
+ Each call whose callee name equals the enclosing function's name contributes +1
68
+ (the whitepaper lists recursion as a fundamental increment). Receiver rule,
69
+ applied symmetrically across languages: bare calls count (`f(...)`), and
70
+ self-receiver calls count (Python `self.f(...)`/`cls.f(...)`, Java
71
+ `this.f(...)`); a same-named call on any other receiver does NOT count. Name
72
+ matching is a declared syntactic heuristic: it does not resolve overloads or
73
+ mutual recursion.
74
+ """
75
+ examples = ["py-recursion-001"]
76
+
77
+ [[ruling]]
78
+ id = "COG-ALL-0006"
79
+ metric = "cognitive"
80
+ language = "all"
81
+ title = "No recursion increment (sonar-compat mode)"
82
+ mode = "sonar-compat"
83
+ statement = """
84
+ SonarQube's own analyzers (sonar-python, sonar-java) do not implement the
85
+ whitepaper's recursion increment; sonar-compat mode therefore omits it.
86
+ (Editorial correction in spec 1.1.0: the original justification also named
87
+ the cognitive_complexity Python package as omitting recursion — false; that
88
+ package DOES implement the +1-per-recursive-call increment, as this repo's
89
+ own differential lane classifies. The normative content of this ruling —
90
+ sonar-compat omits the recursion increment — is unchanged.)
91
+ """
92
+ examples = ["py-recursion-001"]
93
+
94
+ [[ruling]]
95
+ id = "COG-JAVA-0001"
96
+ metric = "cognitive"
97
+ language = "java"
98
+ title = "Labeled jumps are a fundamental increment: flat +1, both modes"
99
+ since_spec = "1.1.0"
100
+ statement = """
101
+ A `break LABEL` or `continue LABEL` contributes a flat +1 regardless of
102
+ nesting depth and does not deepen nesting (the whitepaper's fundamental
103
+ increment list names goto LABEL / break LABEL / continue LABEL; sonar-java
104
+ implements it identically, so both modes agree). A bare break/continue
105
+ contributes nothing, and the label declaration itself (`LABEL: for ...`) is
106
+ not separately counted. Cyclomatic is unaffected: a jump is not a decision
107
+ point.
108
+ """
109
+ node_types = ["break_statement", "continue_statement"]
110
+ examples = ["java-labeled-jump-001"]
111
+
112
+ [[ruling]]
113
+ id = "COG-PY-0001"
114
+ metric = "cognitive"
115
+ language = "python"
116
+ title = "Comprehension clauses contribute no cognitive increment"
117
+ statement = """
118
+ Comprehension `for_in_clause` and `if_clause` (guard) contribute no cognitive
119
+ increment and do not deepen nesting: a comprehension reads as a single
120
+ declarative expression (consistent with SonarPython's treatment). Contrast
121
+ with cyclomatic, where the guard IS a decision point (CC-PY-0004).
122
+ """
123
+ node_types = ["if_clause", "for_in_clause"]
124
+ examples = ["py-comprehension-001"]
@@ -0,0 +1,87 @@
1
+ # CORE-*: cross-cutting measurement policies.
2
+
3
+ [[ruling]]
4
+ id = "CORE-ALL-0001"
5
+ metric = "core"
6
+ language = "all"
7
+ title = "Metrics operate on pre-preprocessing source text"
8
+ statement = """
9
+ All measurements are functions of the source text as written, before any
10
+ preprocessing (relevant to C-family languages later; declared now, ARCHITECTURE.md §0).
11
+ A purely syntactic tool is an approximation for preprocessed languages, and this
12
+ instrument says so instead of hiding it.
13
+ """
14
+
15
+ [[ruling]]
16
+ id = "CORE-ALL-0002"
17
+ metric = "core"
18
+ language = "all"
19
+ title = "Parse-error recovery: measure, label, never fabricate"
20
+ statement = """
21
+ tree-sitter always returns a tree. ERROR and MISSING subtrees are classified as
22
+ opaque (not descended into for metric counting), the report carries
23
+ parse_ok=false and a parse-error-recovered diagnostic. --strict upgrades this
24
+ to an error exit. No number is silently fabricated from broken syntax.
25
+ """
26
+ examples = ["py-error-opacity-001"]
27
+
28
+ [[ruling]]
29
+ id = "CORE-ALL-0003"
30
+ metric = "core"
31
+ language = "all"
32
+ title = "Nested-unit attribution"
33
+ statement = """
34
+ Per-function values are computed on the function's own subtree EXCLUDING nested
35
+ named function/method/constructor definitions and nested class declarations,
36
+ which receive their own FunctionReports (lambdas remain part of the enclosing
37
+ unit). File-level values are ONE whole-file walk (top-level code plus all
38
+ units), not the sum of per-function values. This is a classic radon-vs-lizard
39
+ divergence axis, ruled explicitly.
40
+ """
41
+ examples = ["py-nested-function-001"]
42
+
43
+ [[ruling]]
44
+ id = "CORE-ALL-0004"
45
+ metric = "core"
46
+ language = "all"
47
+ title = "Float emission and determinism scope"
48
+ statement = """
49
+ Canonical JSON/CSV output rounds floating-point values to 12 significant digits
50
+ (round-half-even). Outputs contain no timestamps and no hash-order dependence;
51
+ byte-identical output is guaranteed PER PLATFORM (libm differences across
52
+ OS/arch may flip the last ULP of ln(); this is documented, not hidden).
53
+ Consistency-corpus float assertions always carry explicit tolerances.
54
+ """
55
+
56
+ [[ruling]]
57
+ id = "CORE-ALL-0005"
58
+ metric = "core"
59
+ language = "all"
60
+ title = "Language detection is an extension map — never a guess"
61
+ statement = """
62
+ --lang auto maps file extensions to adapters; an unmapped or ambiguous
63
+ extension is an explicit error. Content sniffing is out of scope for 1.0.
64
+ """
65
+
66
+ [[ruling]]
67
+ id = "CORE-JAVA-0001"
68
+ metric = "core"
69
+ language = "java"
70
+ title = "Bare-snippet scaffolding at snippet granularity"
71
+ statement = """
72
+ The Buse-Weimer dataset snippets are mostly statement sequences or bare
73
+ methods, not compilation units. VERIFIED (tree-sitter-java 0.23.5): the
74
+ grammar's `program` rule accepts both bare statement sequences and bare method
75
+ declarations, so BW-shaped snippets parse cleanly WITHOUT scaffolding. As a
76
+ fallback, at granularity="snippet", if the bare parse yields an error-bearing
77
+ tree, the text is re-parsed inside TWO candidate scaffolds — class-body-only
78
+ `class __CC__ { ... }` (rescues constructors and modifier-bearing members) and
79
+ `class __CC__ { void __cc__() { ... } }` — and the strict error-count
80
+ minimizer is adopted only when it beats the bare parse. All features are
81
+ computed ONLY over the original snippet's line range (scaffold lines excluded,
82
+ original indentation unshifted), scaffold-line function units are dropped, all
83
+ emitted spans and traces are rebased to original snippet coordinates, and a
84
+ snippet-scaffolded diagnostic is attached.
85
+ """
86
+ examples = ["java-snippet-constructor-001"]
87
+
@@ -0,0 +1,172 @@
1
+ # CC-*: cyclomatic complexity — decision-point counting rulings.
2
+ # McCabe is defined on the control-flow graph; in practice every tool counts
3
+ # decision points. These rulings pin OUR counting rules per language.
4
+
5
+ [[ruling]]
6
+ id = "CC-ALL-0001"
7
+ metric = "cyclomatic"
8
+ language = "all"
9
+ title = "Base value and decision-point summation"
10
+ statement = """
11
+ cyclomatic = 1 + the number of decision points in the measured unit. What
12
+ counts as a decision point is pinned by the per-language rulings below.
13
+ """
14
+ examples = ["py-cc-boolop-001", "java-elseif-001"]
15
+
16
+ [[ruling]]
17
+ id = "CC-PY-0001"
18
+ metric = "cyclomatic"
19
+ language = "python"
20
+ title = "if statements count +1"
21
+ statement = "Each `if_statement` contributes one decision point."
22
+ node_types = ["if_statement"]
23
+ examples = ["py-cc-boolop-001", "py-elif-chain-001"]
24
+
25
+ [[ruling]]
26
+ id = "CC-PY-0002"
27
+ metric = "cyclomatic"
28
+ language = "python"
29
+ title = "elif clauses count +1 each"
30
+ statement = "Each `elif_clause` contributes one decision point (each arm is a branch)."
31
+ node_types = ["elif_clause"]
32
+ examples = ["py-elif-chain-001"]
33
+
34
+ [[ruling]]
35
+ id = "CC-PY-0003"
36
+ metric = "cyclomatic"
37
+ language = "python"
38
+ title = "Boolean short-circuit operators count per operator"
39
+ statement = """
40
+ Each `boolean_operator` node contributes one decision point. An N-operand chain
41
+ is (N-1) nested nodes, so `a and b or c` contributes 2 — each short-circuit is a
42
+ distinct execution path. Agrees with radon; diverges from tools that count a
43
+ whole chain as one.
44
+ """
45
+ node_types = ["boolean_operator"]
46
+ examples = ["py-cc-boolop-001"]
47
+
48
+ [[ruling]]
49
+ id = "CC-PY-0004"
50
+ metric = "cyclomatic"
51
+ language = "python"
52
+ title = "Comprehension guards count +1; comprehension for-clauses do not"
53
+ statement = """
54
+ Each `if_clause` inside a comprehension/generator contributes one decision
55
+ point (a guard is a branch). The `for_in_clause` itself does not (it is
56
+ iteration syntax, not a decision). A case-clause guard `if_clause` is NOT
57
+ separately counted — the guarded case_clause already counts under CC-PY-0008,
58
+ and counting both would double-count one branch. Diverges from lizard, which
59
+ also counts the comprehension `for` clause as a loop (see divergences.toml,
60
+ lizard/snippet:diff-comp-guard) — lizard counts the guard AND the for-clause;
61
+ ours counts only the guard.
62
+ """
63
+ node_types = ["if_clause"]
64
+ examples = ["py-comprehension-001"]
65
+
66
+ [[ruling]]
67
+ id = "CC-PY-0005"
68
+ metric = "cyclomatic"
69
+ language = "python"
70
+ title = "Loops count +1"
71
+ statement = "Each `for_statement` / `while_statement` contributes one decision point (async included: the async modifier does not change counting)."
72
+ node_types = ["for_statement", "while_statement"]
73
+ examples = ["py-nested-function-001"]
74
+
75
+ [[ruling]]
76
+ id = "CC-PY-0006"
77
+ metric = "cyclomatic"
78
+ language = "python"
79
+ title = "except clauses count +1 each"
80
+ statement = "Each `except_clause` contributes one decision point."
81
+ node_types = ["except_clause"]
82
+ examples = ["py-try-except-001"]
83
+
84
+ [[ruling]]
85
+ id = "CC-PY-0007"
86
+ metric = "cyclomatic"
87
+ language = "python"
88
+ title = "Conditional expressions (ternaries) count +1"
89
+ statement = """
90
+ Each `conditional_expression` contributes one decision point. Diverges from the
91
+ stdlib-ast reference lane (which does not count IfExp); the divergence is
92
+ classified.
93
+ """
94
+ node_types = ["conditional_expression"]
95
+ examples = ["py-ternary-001"]
96
+
97
+ [[ruling]]
98
+ id = "CC-PY-0008"
99
+ metric = "cyclomatic"
100
+ language = "python"
101
+ title = "match case clauses count +1 each; the match statement and bare wildcard do not"
102
+ statement = """
103
+ Each `case_clause` contributes one decision point, EXCEPT a bare-wildcard
104
+ `case _:` with no guard, which is the fall-through path and does not count —
105
+ the exact mirror of Java `default:` (CC-JAVA-0006).
106
+ """
107
+ node_types = ["case_clause"]
108
+ examples = ["py-match-001"]
109
+
110
+ [[ruling]]
111
+ id = "CC-JAVA-0001"
112
+ metric = "cyclomatic"
113
+ language = "java"
114
+ title = "if statements count +1 (else-if chains count each if)"
115
+ statement = """
116
+ Each `if_statement` contributes one decision point, including an if_statement
117
+ that is the `alternative` of another (an else-if arm is a branch).
118
+ """
119
+ node_types = ["if_statement"]
120
+ examples = ["java-elseif-001"]
121
+
122
+ [[ruling]]
123
+ id = "CC-JAVA-0002"
124
+ metric = "cyclomatic"
125
+ language = "java"
126
+ title = "Ternary expressions count +1"
127
+ statement = "Each `ternary_expression` contributes one decision point."
128
+ node_types = ["ternary_expression"]
129
+ examples = ["java-ternary-001"]
130
+
131
+ [[ruling]]
132
+ id = "CC-JAVA-0003"
133
+ metric = "cyclomatic"
134
+ language = "java"
135
+ title = "Short-circuit operators count per operator"
136
+ statement = """
137
+ Each `binary_expression` whose operator is `&&` or `||` contributes one
138
+ decision point; `a && b && c` contributes 2.
139
+ """
140
+ node_types = ["binary_expression"]
141
+ examples = ["java-boolop-catch-001"]
142
+
143
+ [[ruling]]
144
+ id = "CC-JAVA-0004"
145
+ metric = "cyclomatic"
146
+ language = "java"
147
+ title = "Loops count +1"
148
+ statement = "Each `for_statement` / `enhanced_for_statement` / `while_statement` / `do_statement` contributes one decision point."
149
+ node_types = ["for_statement", "enhanced_for_statement", "while_statement", "do_statement"]
150
+ examples = ["java-loops-001"]
151
+
152
+ [[ruling]]
153
+ id = "CC-JAVA-0005"
154
+ metric = "cyclomatic"
155
+ language = "java"
156
+ title = "catch clauses count +1 each"
157
+ statement = "Each `catch_clause` contributes one decision point."
158
+ node_types = ["catch_clause"]
159
+ examples = ["java-boolop-catch-001"]
160
+
161
+ [[ruling]]
162
+ id = "CC-JAVA-0006"
163
+ metric = "cyclomatic"
164
+ language = "java"
165
+ title = "case labels count +1 each; default does not"
166
+ statement = """
167
+ Each `switch_label` beginning with `case` contributes one decision point; a
168
+ `default` label does not (it is the fall-through path, like an else).
169
+ """
170
+ node_types = ["switch_label"]
171
+ examples = ["java-switch-001"]
172
+
@@ -0,0 +1,20 @@
1
+ # HAL-*: Halstead metrics.
2
+
3
+ [[ruling]]
4
+ id = "HAL-ALL-0001"
5
+ metric = "halstead"
6
+ language = "all"
7
+ title = "Lexical Halstead: token-class harvesting (a declared approximation)"
8
+ statement = """
9
+ Operators are OPERATOR, KEYWORD, and PUNCT tokens; operands are IDENTIFIER,
10
+ NUMBER, and STRING tokens, from the unified lexical stream. n1/n2 are distinct
11
+ counts, N1/N2 total counts; V = N*log2(n); D = (n1/2)*(N2/n2); E = D*V.
12
+ Absolute Halstead values are implementation-defined across all tools — only
13
+ trends and ratios are stable — and every emitted value carries the
14
+ halstead-approximation diagnostic (ARCHITECTURE.md §13). This lexical convention is
15
+ deliberately uniform cross-language; its divergence from AST-harvest
16
+ implementations (radon, the stdlib reference lane) is declared, not classified
17
+ against a differential oracle: every value carries the halstead-approximation
18
+ diagnostic (there is no Halstead differential lane, unlike cyclomatic/cognitive).
19
+ """
20
+ examples = ["py-cc-boolop-001"]