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.
- codecaliper/__init__.py +64 -0
- codecaliper/_version.py +1 -0
- codecaliper/api.py +446 -0
- codecaliper/canonical.py +130 -0
- codecaliper/cli.py +231 -0
- codecaliper/errors.py +27 -0
- codecaliper/languages/__init__.py +44 -0
- codecaliper/languages/base.py +162 -0
- codecaliper/languages/java.py +192 -0
- codecaliper/languages/python.py +179 -0
- codecaliper/metrics/__init__.py +0 -0
- codecaliper/metrics/base.py +55 -0
- codecaliper/metrics/cognitive.py +111 -0
- codecaliper/metrics/cyclomatic.py +62 -0
- codecaliper/metrics/halstead.py +55 -0
- codecaliper/metrics/loc.py +62 -0
- codecaliper/metrics/mi.py +31 -0
- codecaliper/model.py +132 -0
- codecaliper/py.typed +0 -0
- codecaliper/readability/__init__.py +13 -0
- codecaliper/readability/base.py +11 -0
- codecaliper/readability/bw2010.py +156 -0
- codecaliper/readability/granularity.py +210 -0
- codecaliper/readability/retrain.py +67 -0
- codecaliper/spec/__init__.py +3 -0
- codecaliper/spec/registry.py +112 -0
- codecaliper/spec/rulings/bw.toml +156 -0
- codecaliper/spec/rulings/cognitive.toml +124 -0
- codecaliper/spec/rulings/core.toml +87 -0
- codecaliper/spec/rulings/cyclomatic.toml +172 -0
- codecaliper/spec/rulings/halstead.toml +20 -0
- codecaliper/spec/rulings/index.toml +54 -0
- codecaliper/spec/rulings/loc.toml +50 -0
- codecaliper/spec/rulings/mi.toml +15 -0
- codecaliper/spec/rulings/tokenization.toml +191 -0
- codecaliper/spec/validated_grammars.toml +6 -0
- codecaliper/syntax/__init__.py +0 -0
- codecaliper/syntax/_treesitter.py +179 -0
- codecaliper/syntax/grammars.py +45 -0
- codecaliper/syntax/tokens.py +165 -0
- codecaliper-0.1.0.dist-info/METADATA +180 -0
- codecaliper-0.1.0.dist-info/RECORD +46 -0
- codecaliper-0.1.0.dist-info/WHEEL +4 -0
- codecaliper-0.1.0.dist-info/entry_points.txt +2 -0
- codecaliper-0.1.0.dist-info/licenses/LICENSE +21 -0
- 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"]
|