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
codecaliper/cli.py ADDED
@@ -0,0 +1,231 @@
1
+ """The codecaliper CLI (stdlib argparse — zero extra dependencies).
2
+
3
+ codecaliper FILE... [--lang auto|python|java] [--json|--csv] ...
4
+ codecaliper spec version | list | show RULING-ID
5
+ codecaliper env # the calibration plate
6
+ codecaliper cite [--format ...] # methods-section template
7
+
8
+ Diagnostics go to stderr, data to stdout — pipeline-safe. Exit codes:
9
+ 0 success, 1 usage/internal error, 2 completed with error-severity diagnostics.
10
+ """
11
+
12
+ from __future__ import annotations
13
+
14
+ import argparse
15
+ import platform
16
+ import sys
17
+
18
+ from codecaliper._version import __version__
19
+ from codecaliper.errors import CodecaliperError
20
+
21
+
22
+ class _Parser(argparse.ArgumentParser):
23
+ """argparse exits 2 on usage errors; our contract reserves 2 for
24
+ 'completed with error-severity diagnostics', so usage errors exit 1."""
25
+
26
+ def error(self, message: str) -> None: # type: ignore[override]
27
+ self.print_usage(sys.stderr)
28
+ print(f"{self.prog}: error: {message}", file=sys.stderr)
29
+ raise SystemExit(1)
30
+
31
+
32
+ def main(argv: list[str] | None = None) -> int:
33
+ args = argv if argv is not None else sys.argv[1:]
34
+ if args and args[0] in ("spec", "env", "cite"):
35
+ return _dispatch_subcommand(args)
36
+ return _cmd_measure(args)
37
+
38
+
39
+ def _measure_parser() -> argparse.ArgumentParser:
40
+ p = _Parser(
41
+ prog="codecaliper",
42
+ description="A cross-language code readability + complexity measurement instrument.",
43
+ )
44
+ p.add_argument("files", nargs="+", metavar="FILE")
45
+ p.add_argument("--lang", default="auto", choices=["auto", "python", "java"])
46
+ fmt = p.add_mutually_exclusive_group()
47
+ fmt.add_argument("--json", action="store_true", help="JSON output (default)")
48
+ fmt.add_argument("--csv", action="store_true", help="wide-format CSV output")
49
+ p.add_argument("--metrics", default=",".join(
50
+ ("cyclomatic", "cognitive", "halstead", "maintainability_index", "loc")))
51
+ p.add_argument("--no-readability", action="store_true")
52
+ p.add_argument("--bw-granularity", default="file",
53
+ choices=["snippet", "function", "file"])
54
+ p.add_argument("--sonar-compat", action="store_true",
55
+ help="cognitive complexity in sonar-compat mode (default: whitepaper)")
56
+ p.add_argument("--explain", action="store_true",
57
+ help="attach per-increment ruling traces (RulingTrace)")
58
+ p.add_argument("--strict", action="store_true",
59
+ help="error exit on parse errors instead of measure-and-label")
60
+ p.add_argument("-o", "--output", default=None)
61
+ p.add_argument("--version", action="version", version=f"codecaliper {__version__}")
62
+ return p
63
+
64
+
65
+ def _cmd_measure(argv: list[str]) -> int:
66
+ from codecaliper import canonical
67
+ from codecaliper.api import Session
68
+ from codecaliper.errors import StrictParseError
69
+ from codecaliper.languages import detect_language
70
+
71
+ opts = _measure_parser().parse_args(argv)
72
+ try:
73
+ session = Session(
74
+ metrics=tuple(m for m in opts.metrics.split(",") if m),
75
+ readability=() if opts.no_readability else ("bw2010",),
76
+ granularity=opts.bw_granularity,
77
+ cognitive_mode="sonar-compat" if opts.sonar_compat else "whitepaper",
78
+ explain=opts.explain,
79
+ strict=opts.strict,
80
+ )
81
+ except CodecaliperError as exc:
82
+ print(f"codecaliper: error: {exc}", file=sys.stderr)
83
+ return 1
84
+ reports = []
85
+ had_error_diag = False
86
+ try:
87
+ for path in opts.files:
88
+ lang = detect_language(path) if opts.lang == "auto" else opts.lang
89
+ rep = session.measure_file(path, language=lang)
90
+ reports.append(rep)
91
+ for d in rep.diagnostics:
92
+ print(f"{path}: {d.severity}: {d.code}: {d.message}", file=sys.stderr)
93
+ if d.severity == "error":
94
+ had_error_diag = True
95
+ except StrictParseError as exc:
96
+ # --strict: an error-severity condition, per the documented exit contract
97
+ print(f"codecaliper: error: {exc}", file=sys.stderr)
98
+ return 2
99
+ except (CodecaliperError, OSError) as exc:
100
+ print(f"codecaliper: error: {exc}", file=sys.stderr)
101
+ return 1
102
+
103
+ if opts.csv:
104
+ out = canonical.to_csv(reports)
105
+ else:
106
+ out = "".join(canonical.to_json(r) for r in reports)
107
+ if opts.output:
108
+ try:
109
+ with open(opts.output, "w", encoding="utf-8") as f:
110
+ f.write(out)
111
+ except OSError as exc:
112
+ print(f"codecaliper: error: {exc}", file=sys.stderr)
113
+ return 1
114
+ else:
115
+ sys.stdout.write(out)
116
+ return 2 if had_error_diag else 0
117
+
118
+
119
+ def _dispatch_subcommand(argv: list[str]) -> int:
120
+ cmd, rest = argv[0], argv[1:]
121
+ if cmd == "spec":
122
+ return _cmd_spec(rest)
123
+ if cmd == "env":
124
+ return _cmd_env()
125
+ if cmd == "cite":
126
+ return _cmd_cite(rest)
127
+ return 1 # pragma: no cover
128
+
129
+
130
+ def _cmd_spec(argv: list[str]) -> int:
131
+ from codecaliper.spec import iter_rulings, ruling, spec_version
132
+
133
+ p = _Parser(prog="codecaliper spec")
134
+ sub = p.add_subparsers(dest="action", required=True)
135
+ sub.add_parser("version")
136
+ lst = sub.add_parser("list")
137
+ lst.add_argument("--metric", default=None)
138
+ lst.add_argument("--lang", default=None)
139
+ show = sub.add_parser("show")
140
+ show.add_argument("ruling_id")
141
+ opts = p.parse_args(argv)
142
+
143
+ if opts.action == "version":
144
+ print(spec_version())
145
+ elif opts.action == "list":
146
+ for r in iter_rulings(metric=opts.metric, language=opts.lang):
147
+ mode = f" [{r.mode}]" if r.mode else ""
148
+ print(f"{r.id} {r.status:<10} {r.title}{mode}")
149
+ elif opts.action == "show":
150
+ try:
151
+ r = ruling(opts.ruling_id)
152
+ except CodecaliperError as exc:
153
+ print(f"codecaliper: error: {exc}", file=sys.stderr)
154
+ return 1
155
+ print(f"{r.id} — {r.title}")
156
+ print(f"metric: {r.metric} language: {r.language} status: {r.status}"
157
+ + (f" mode: {r.mode}" if r.mode else ""))
158
+ if r.superseded_by:
159
+ print(f"superseded by: {r.superseded_by}")
160
+ print(f"since spec: {r.since_spec}")
161
+ if r.node_types:
162
+ print(f"node types: {', '.join(r.node_types)}")
163
+ if r.examples:
164
+ print(f"normative cases: {', '.join(r.examples)}")
165
+ print()
166
+ print(r.statement)
167
+ return 0
168
+
169
+
170
+ def _cmd_env() -> int:
171
+ """The calibration plate — what bug reports and papers should quote."""
172
+ from codecaliper.languages import available_languages, get_adapter
173
+ from codecaliper.spec import spec_version
174
+
175
+ print(f"codecaliper {__version__}")
176
+ print(f"spec {spec_version()}")
177
+ print(f"python {platform.python_version()} ({platform.system()} {platform.machine()})")
178
+ try:
179
+ import importlib.metadata
180
+
181
+ print(f"tree-sitter {importlib.metadata.version('tree-sitter')} (binding)")
182
+ except Exception: # noqa: BLE001 - env report must never crash
183
+ print("tree-sitter binding: not installed")
184
+ for lang in available_languages():
185
+ try:
186
+ info = get_adapter(lang).grammar_info()
187
+ flag = "calibrated" if info.validated else "UNVALIDATED"
188
+ print(f"{info.package} {info.version} (ABI {info.abi_version}, {flag})")
189
+ except CodecaliperError as exc:
190
+ print(f"{lang}: grammar unavailable: {exc}")
191
+ return 0
192
+
193
+
194
+ def _cmd_cite(argv: list[str]) -> int:
195
+ from codecaliper.languages import available_languages, get_adapter
196
+ from codecaliper.spec import spec_version
197
+
198
+ p = _Parser(prog="codecaliper cite")
199
+ p.add_argument("--format", default="text", choices=["text", "bibtex"])
200
+ opts = p.parse_args(argv)
201
+ grammars = []
202
+ for lang in available_languages():
203
+ try:
204
+ info = get_adapter(lang).grammar_info()
205
+ grammars.append(f"{info.package} {info.version}")
206
+ except CodecaliperError:
207
+ pass
208
+ if opts.format == "bibtex":
209
+ print(
210
+ "@software{codecaliper,\n"
211
+ " author = {Ji, Yuxiang},\n"
212
+ " title = {codecaliper: a cross-language code readability and "
213
+ "complexity measurement instrument},\n"
214
+ f" version = {{{__version__}}},\n"
215
+ f" note = {{spec {spec_version()}; grammars: {', '.join(grammars)}}},\n"
216
+ " url = {https://github.com/KurathSec/codecaliper}\n"
217
+ "}"
218
+ )
219
+ else:
220
+ print(
221
+ f"Metrics were computed with codecaliper {__version__} under mapping "
222
+ f"specification {spec_version()} (grammars: {', '.join(grammars)}); "
223
+ "cognitive complexity in whitepaper mode unless stated; readability "
224
+ "vectors are the raw Buse-Weimer 2010 feature set, with granularity "
225
+ "and extrapolation as labelled in the output."
226
+ )
227
+ return 0
228
+
229
+
230
+ if __name__ == "__main__": # pragma: no cover
231
+ raise SystemExit(main())
codecaliper/errors.py ADDED
@@ -0,0 +1,27 @@
1
+ """Typed error taxonomy. Nothing else escapes the public facade."""
2
+
3
+ from __future__ import annotations
4
+
5
+
6
+ class CodecaliperError(Exception):
7
+ """Base class for all codecaliper errors."""
8
+
9
+
10
+ class UnsupportedLanguageError(CodecaliperError):
11
+ """The requested language has no registered adapter."""
12
+
13
+
14
+ class LanguageDetectionError(CodecaliperError):
15
+ """--lang auto could not map the file extension; never guess (spec CORE-ALL-0005)."""
16
+
17
+
18
+ class GrammarLoadError(CodecaliperError):
19
+ """A tree-sitter grammar wheel could not be imported or wrapped."""
20
+
21
+
22
+ class SpecError(CodecaliperError):
23
+ """The ruling registry is unreadable, or code cited a phantom ruling ID."""
24
+
25
+
26
+ class StrictParseError(CodecaliperError):
27
+ """--strict was set and the parse tree contains ERROR/MISSING nodes (CORE-ALL-0002)."""
@@ -0,0 +1,44 @@
1
+ """Language adapter registry — a plain dict, deliberately (ARCHITECTURE.md §14:
2
+ entry-point plugin discovery is deferred until a third-party adapter exists)."""
3
+
4
+ from __future__ import annotations
5
+
6
+ from functools import cache
7
+
8
+ from codecaliper.errors import LanguageDetectionError, UnsupportedLanguageError
9
+ from codecaliper.languages.base import LanguageAdapter
10
+ from codecaliper.spec import require
11
+
12
+ _BUILTIN = ("python", "java")
13
+
14
+ R_LANG_AUTO = require("CORE-ALL-0005") # extension map, never a guess
15
+
16
+
17
+ @cache
18
+ def get_adapter(name: str) -> LanguageAdapter:
19
+ if name == "python":
20
+ from codecaliper.languages.python import PythonAdapter
21
+
22
+ return PythonAdapter()
23
+ if name == "java":
24
+ from codecaliper.languages.java import JavaAdapter
25
+
26
+ return JavaAdapter()
27
+ raise UnsupportedLanguageError(
28
+ f"no adapter for language {name!r}; available: {', '.join(_BUILTIN)}"
29
+ )
30
+
31
+
32
+ def available_languages() -> tuple[str, ...]:
33
+ return _BUILTIN
34
+
35
+
36
+ def detect_language(path: str) -> str:
37
+ """--lang auto: extension map with an explicit error — never a guess."""
38
+ lower = path.lower()
39
+ for name in _BUILTIN:
40
+ if any(lower.endswith(ext) for ext in get_adapter(name).file_extensions):
41
+ return name
42
+ raise LanguageDetectionError(
43
+ f"cannot detect language of {path!r} from its extension; pass --lang explicitly"
44
+ )
@@ -0,0 +1,162 @@
1
+ """The LanguageAdapter contract.
2
+
3
+ Metric engines never see tree-sitter node-type strings — only :class:`NodeClass`
4
+ and :class:`~codecaliper.syntax.tokens.TokenKind`. Per-language differences live
5
+ entirely in the adapters' tables and a few named hook methods, every one citing
6
+ a ruling ID; the grammar-integrity and spec-coverage tests hold the tables to
7
+ the compiled grammar and the spec (ARCHITECTURE.md §5).
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ from dataclasses import dataclass, field
13
+ from enum import Enum
14
+ from typing import TYPE_CHECKING
15
+
16
+ from codecaliper.model import GrammarInfo, Span
17
+
18
+ if TYPE_CHECKING: # pragma: no cover
19
+ from codecaliper.syntax._treesitter import Node, Parser, Tree
20
+
21
+
22
+ class NodeClass(Enum):
23
+ BRANCH = "branch" # if heads
24
+ ELIF_CONTINUATION = "elif" # elif / else-if arms (chain-flattened)
25
+ ELSE_CLAUSE = "else" # if-attached else
26
+ TERNARY = "ternary"
27
+ SWITCH = "switch" # match / switch head
28
+ CASE_LABEL = "case"
29
+ LOOP = "loop"
30
+ CATCH = "catch"
31
+ BOOL_OP = "boolop"
32
+ JUMP_LABEL = "labeled-jump" # break/continue LABEL — flat +1 (COG-JAVA-0001)
33
+ COMPREHENSION_GUARD = "comp-guard"
34
+ LAMBDA = "lambda"
35
+ FUNCTION_DEF = "function"
36
+ CLASS_DEF = "class"
37
+ NESTING_ONLY = "nesting-only" # try / with — no increment, no cognitive nesting
38
+
39
+
40
+ @dataclass(frozen=True, slots=True)
41
+ class Classified:
42
+ node_class: NodeClass
43
+ rulings: tuple[str, ...]
44
+ new_sequence: bool = True # BOOL_OP only: starts a new like-operator sequence (COG-ALL-0003)
45
+
46
+
47
+ @dataclass(frozen=True, slots=True)
48
+ class FunctionUnit:
49
+ name: str
50
+ qualified_name: str
51
+ node: Node
52
+ span: Span
53
+
54
+
55
+ def span_of(node: Node) -> Span:
56
+ return Span(
57
+ start_line=node.start_point[0] + 1,
58
+ start_col=node.start_point[1],
59
+ end_line=node.end_point[0] + 1,
60
+ end_col=node.end_point[1],
61
+ )
62
+
63
+
64
+ @dataclass
65
+ class LanguageAdapter:
66
+ """Base adapter: concrete adapters supply tables and small hooks."""
67
+
68
+ name: str = ""
69
+ file_extensions: tuple[str, ...] = ()
70
+
71
+ # --- token tables (BW + Halstead + LOC share them; BW-ALL-0006) ---
72
+ keywords: frozenset[str] = frozenset()
73
+ soft_keywords: frozenset[str] = frozenset() # classify as IDENTIFIER (BW-PY-0001)
74
+ keyword_leaf_types: frozenset[str] = frozenset()
75
+ identifier_types: frozenset[str] = frozenset()
76
+ operator_leaf_types: frozenset[str] = frozenset() # named leaves that are OPERATOR tokens
77
+ number_types: frozenset[str] = frozenset()
78
+ string_types: frozenset[str] = frozenset()
79
+ comment_types: frozenset[str] = frozenset()
80
+ atomic_types: frozenset[str] = frozenset()
81
+ arithmetic_ops: frozenset[str] = frozenset()
82
+ comparison_ops: frozenset[str] = frozenset()
83
+ assignment_ops: frozenset[str] = frozenset()
84
+ branch_keywords: frozenset[str] = frozenset()
85
+ loop_keywords: frozenset[str] = frozenset()
86
+
87
+ # Construct-specific tokenization rulings that only govern when their node
88
+ # type actually lexes (node_type -> ruling id) — cited per-occurrence, like
89
+ # TOK-ALL-0007, NOT statically the way the atomic-string rulings are.
90
+ conditional_token_rulings: dict[str, str] = field(default_factory=dict)
91
+
92
+ # --- structural tables ---
93
+ node_class_map: dict[str, tuple[NodeClass, tuple[str, ...]]] = field(default_factory=dict)
94
+ statement_types: frozenset[str] = frozenset()
95
+ function_def_types: frozenset[str] = frozenset()
96
+ class_def_types: frozenset[str] = frozenset()
97
+
98
+ _grammar: object = None
99
+ _parser: Parser | None = None
100
+ _grammar_info: GrammarInfo | None = None
101
+
102
+ # --- parsing ---
103
+ def _ensure_loaded(self) -> None:
104
+ if self._parser is None:
105
+ from codecaliper.syntax import grammars
106
+
107
+ self._grammar, self._parser, self._grammar_info = grammars.load(self.name)
108
+
109
+ def parse(self, source: bytes) -> Tree:
110
+ self._ensure_loaded()
111
+ assert self._parser is not None
112
+ return self._parser.parse(source)
113
+
114
+ def grammar_info(self) -> GrammarInfo:
115
+ self._ensure_loaded()
116
+ assert self._grammar_info is not None
117
+ return self._grammar_info
118
+
119
+ def grammar(self) -> object:
120
+ self._ensure_loaded()
121
+ return self._grammar
122
+
123
+ # --- classification (table lookup; hooks in subclasses for context cases) ---
124
+ def classify(self, node: Node) -> Classified | None:
125
+ entry = self.node_class_map.get(node.type)
126
+ if entry is None:
127
+ return None
128
+ return Classified(entry[0], entry[1])
129
+
130
+ # --- hooks ---
131
+ def function_name(self, node: Node) -> str:
132
+ name = node.child_by_field_name("name")
133
+ if name is None:
134
+ return ""
135
+ return (name.text or b"").decode("utf-8", errors="replace")
136
+
137
+ def call_name(self, node: Node) -> str | None:
138
+ """Simple callee name for the recursion heuristic (COG-ALL-0005), or None."""
139
+ return None
140
+
141
+ def function_units(self, tree: Tree) -> list[FunctionUnit]:
142
+ from codecaliper.syntax._treesitter import is_opaque, walk
143
+
144
+ units: list[FunctionUnit] = []
145
+ # enclosing-name stack for qualified names, driven by node spans
146
+ stack: list[tuple[int, str]] = [] # (end_byte, name)
147
+ opaque_until = -1 # skip everything inside an ERROR subtree (CORE-ALL-0002)
148
+ for node, _depth in walk(tree):
149
+ if node.start_byte < opaque_until:
150
+ continue
151
+ if is_opaque(node):
152
+ opaque_until = node.end_byte
153
+ continue
154
+ while stack and node.start_byte >= stack[-1][0]:
155
+ stack.pop()
156
+ if node.type in self.function_def_types or node.type in self.class_def_types:
157
+ name = self.function_name(node)
158
+ if node.type in self.function_def_types:
159
+ qual = ".".join([n for _, n in stack] + [name])
160
+ units.append(FunctionUnit(name, qual, node, span_of(node)))
161
+ stack.append((node.end_byte, name))
162
+ return units
@@ -0,0 +1,192 @@
1
+ """Java adapter: tables + contextual hooks, every row citing a ruling.
2
+
3
+ Verified against tree-sitter-java 0.23.5 node kinds. Java has no else_clause
4
+ node — an `else` is an anonymous token and the else-if chain lives in the
5
+ if_statement's `alternative` field, so both are handled by hooks here.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ from typing import TYPE_CHECKING
11
+
12
+ from codecaliper.languages.base import Classified, LanguageAdapter, NodeClass
13
+ from codecaliper.spec import require
14
+
15
+ if TYPE_CHECKING: # pragma: no cover
16
+ from codecaliper.syntax._treesitter import Node
17
+
18
+ R_CC_IF = require("CC-JAVA-0001")
19
+ R_CC_TERNARY = require("CC-JAVA-0002")
20
+ R_CC_BOOLOP = require("CC-JAVA-0003")
21
+ R_CC_LOOP = require("CC-JAVA-0004")
22
+ R_CC_CATCH = require("CC-JAVA-0005")
23
+ R_CC_CASE = require("CC-JAVA-0006")
24
+ R_COG_STRUCT = require("COG-ALL-0001")
25
+ R_COG_HYBRID = require("COG-ALL-0002")
26
+ R_COG_BOOLSEQ = require("COG-ALL-0003")
27
+ R_COG_NESTING = require("COG-ALL-0004")
28
+ R_COG_JUMP = require("COG-JAVA-0001")
29
+ R_NESTED_UNITS = require("CORE-ALL-0003")
30
+ R_TOK_UNDERSCORE = require("TOK-JAVA-0002")
31
+
32
+ # The JLS reserved words except `_`, which is ruled an identifier token
33
+ # (TOK-JAVA-0002, mirroring Python's `_`); true/false/null arrive via
34
+ # keyword_leaf_types (BW-JAVA-0001). Contextual keywords (record, sealed,
35
+ # yield, ...) are identifiers, like every other non-reserved word token
36
+ # (TOK-ALL-0007).
37
+ _JAVA_KEYWORDS = frozenset(
38
+ """abstract assert boolean break byte case catch char class const continue
39
+ default do double else enum extends final finally float for goto if
40
+ implements import instanceof int interface long native new package private
41
+ protected public return short static strictfp super switch synchronized
42
+ this throw throws transient try void volatile while""".split()
43
+ )
44
+
45
+ _NODE_CLASS_MAP: dict[str, tuple[NodeClass, tuple[str, ...]]] = {
46
+ "ternary_expression": (NodeClass.TERNARY, (R_CC_TERNARY, R_COG_STRUCT)),
47
+ "for_statement": (NodeClass.LOOP, (R_CC_LOOP, R_COG_STRUCT)),
48
+ "enhanced_for_statement": (NodeClass.LOOP, (R_CC_LOOP, R_COG_STRUCT)),
49
+ "while_statement": (NodeClass.LOOP, (R_CC_LOOP, R_COG_STRUCT)),
50
+ "do_statement": (NodeClass.LOOP, (R_CC_LOOP, R_COG_STRUCT)),
51
+ "catch_clause": (NodeClass.CATCH, (R_CC_CATCH, R_COG_STRUCT)),
52
+ "switch_expression": (NodeClass.SWITCH, (R_COG_STRUCT,)),
53
+ "lambda_expression": (NodeClass.LAMBDA, (R_COG_NESTING,)),
54
+ "method_declaration": (NodeClass.FUNCTION_DEF, (R_NESTED_UNITS, R_COG_NESTING)),
55
+ "constructor_declaration": (NodeClass.FUNCTION_DEF, (R_NESTED_UNITS, R_COG_NESTING)),
56
+ "compact_constructor_declaration": (NodeClass.FUNCTION_DEF, (R_NESTED_UNITS, R_COG_NESTING)),
57
+ "class_declaration": (NodeClass.CLASS_DEF, (R_NESTED_UNITS,)),
58
+ "interface_declaration": (NodeClass.CLASS_DEF, (R_NESTED_UNITS,)),
59
+ "enum_declaration": (NodeClass.CLASS_DEF, (R_NESTED_UNITS,)),
60
+ "record_declaration": (NodeClass.CLASS_DEF, (R_NESTED_UNITS,)),
61
+ "try_statement": (NodeClass.NESTING_ONLY, ()),
62
+ "try_with_resources_statement": (NodeClass.NESTING_ONLY, ()),
63
+ "synchronized_statement": (NodeClass.NESTING_ONLY, ()),
64
+ }
65
+
66
+
67
+ class JavaAdapter(LanguageAdapter):
68
+ def __init__(self) -> None:
69
+ super().__init__(
70
+ name="java",
71
+ file_extensions=(".java",),
72
+ keywords=_JAVA_KEYWORDS,
73
+ keyword_leaf_types=frozenset({"true", "false", "null_literal"}),
74
+ # underscore_pattern: `_` as an unnamed variable declarator is the
75
+ # same identifier token as `_` in pattern/lambda positions
76
+ # (TOK-JAVA-0002)
77
+ identifier_types=frozenset(
78
+ {"identifier", "type_identifier", "underscore_pattern"}
79
+ ),
80
+ # TOK-JAVA-0002 governs only when an underscore_pattern actually
81
+ # lexes; cited per-occurrence, not statically (unlike TOK-JAVA-0001)
82
+ conditional_token_rulings={"underscore_pattern": R_TOK_UNDERSCORE},
83
+ number_types=frozenset(
84
+ {
85
+ "decimal_integer_literal", "hex_integer_literal",
86
+ "octal_integer_literal", "binary_integer_literal",
87
+ "decimal_floating_point_literal", "hex_floating_point_literal",
88
+ }
89
+ ),
90
+ string_types=frozenset({"string_literal", "character_literal"}),
91
+ comment_types=frozenset({"line_comment", "block_comment"}),
92
+ atomic_types=frozenset({"string_literal", "character_literal"}), # TOK-JAVA-0001
93
+ arithmetic_ops=frozenset({"+", "-", "*", "/", "%"}),
94
+ comparison_ops=frozenset({"==", "!=", "<", ">", "<=", ">="}),
95
+ assignment_ops=frozenset(
96
+ {"=", "+=", "-=", "*=", "/=", "%=", "&=", "|=", "^=", "<<=", ">>=", ">>>="}
97
+ ),
98
+ branch_keywords=frozenset({"if"}), # BW-JAVA-0002
99
+ loop_keywords=frozenset({"for", "while", "do"}), # BW-JAVA-0002
100
+ node_class_map=dict(_NODE_CLASS_MAP),
101
+ statement_types=frozenset(
102
+ {
103
+ "local_variable_declaration", "expression_statement", "return_statement",
104
+ "if_statement", "for_statement", "enhanced_for_statement",
105
+ "while_statement", "do_statement", "try_statement",
106
+ "try_with_resources_statement", "switch_expression", "break_statement",
107
+ "continue_statement", "throw_statement", "method_declaration",
108
+ "constructor_declaration", "class_declaration", "field_declaration",
109
+ "assert_statement", "yield_statement", "labeled_statement",
110
+ "synchronized_statement",
111
+ # declarations (LOC-ALL-0004 covers declarations too)
112
+ "package_declaration", "import_declaration", "interface_declaration",
113
+ "enum_declaration", "record_declaration", "annotation_type_declaration",
114
+ "annotation_type_element_declaration", "constant_declaration",
115
+ "static_initializer", "compact_constructor_declaration",
116
+ "explicit_constructor_invocation", "module_declaration",
117
+ "requires_module_directive", "exports_module_directive",
118
+ "opens_module_directive", "uses_module_directive",
119
+ "provides_module_directive",
120
+ }
121
+ ),
122
+ function_def_types=frozenset(
123
+ {"method_declaration", "constructor_declaration",
124
+ "compact_constructor_declaration"}
125
+ ),
126
+ class_def_types=frozenset(
127
+ {"class_declaration", "interface_declaration", "enum_declaration",
128
+ "record_declaration"}
129
+ ),
130
+ )
131
+
132
+ def classify(self, node: Node) -> Classified | None:
133
+ t = node.type
134
+ if t == "if_statement":
135
+ # CC-JAVA-0001 + COG-ALL-0002: an if that is the `alternative` of
136
+ # another if is an else-if arm — chain-flattened for cognitive.
137
+ parent = node.parent
138
+ if (
139
+ parent is not None
140
+ and parent.type == "if_statement"
141
+ and parent.child_by_field_name("alternative") == node
142
+ ):
143
+ return Classified(NodeClass.ELIF_CONTINUATION, (R_CC_IF, R_COG_HYBRID))
144
+ return Classified(NodeClass.BRANCH, (R_CC_IF, R_COG_STRUCT))
145
+ if t == "else":
146
+ # Anonymous `else` token: hybrid +1 unless it introduces an else-if
147
+ # (then the nested if_statement counts instead — no double count).
148
+ parent = node.parent
149
+ if parent is not None and parent.type == "if_statement":
150
+ alt = parent.child_by_field_name("alternative")
151
+ if alt is not None and alt.type != "if_statement":
152
+ return Classified(NodeClass.ELSE_CLAUSE, (R_COG_HYBRID,))
153
+ return None
154
+ if t == "binary_expression":
155
+ op = node.child_by_field_name("operator")
156
+ op_type = op.type if op is not None else ""
157
+ if op_type not in ("&&", "||"):
158
+ return None
159
+ parent = node.parent
160
+ same_as_parent = False
161
+ if parent is not None and parent.type == "binary_expression":
162
+ pop = parent.child_by_field_name("operator")
163
+ same_as_parent = pop is not None and pop.type == op_type
164
+ return Classified(
165
+ NodeClass.BOOL_OP, (R_CC_BOOLOP, R_COG_BOOLSEQ), new_sequence=not same_as_parent
166
+ )
167
+ if t == "switch_label":
168
+ # CC-JAVA-0006: `case` labels count; `default` does not.
169
+ first = node.children[0] if node.children else None
170
+ if first is not None and first.type == "case":
171
+ return Classified(NodeClass.CASE_LABEL, (R_CC_CASE,))
172
+ return None
173
+ if t in ("break_statement", "continue_statement"):
174
+ # COG-JAVA-0001: only LABELED jumps increment (break LABEL /
175
+ # continue LABEL); a bare break/continue contributes nothing.
176
+ if any(child.type == "identifier" for child in node.children):
177
+ return Classified(NodeClass.JUMP_LABEL, (R_COG_JUMP,))
178
+ return None
179
+ return super().classify(node)
180
+
181
+ def call_name(self, node: Node) -> str | None:
182
+ """COG-ALL-0005 receiver rule: bare calls plus this-receiver calls only —
183
+ a same-named call on another receiver is not recursion."""
184
+ if node.type != "method_invocation":
185
+ return None
186
+ obj = node.child_by_field_name("object")
187
+ if obj is not None and obj.type != "this":
188
+ return None
189
+ name = node.child_by_field_name("name")
190
+ if name is not None:
191
+ return (name.text or b"").decode("utf-8", errors="replace")
192
+ return None