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
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
|