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,54 @@
|
|
|
1
|
+
# The versioned metric-to-syntax mapping specification (ARCHITECTURE.md §3).
|
|
2
|
+
# SPEC semver is independent of the package version:
|
|
3
|
+
# patch = editorial; minor = new rulings, no existing number changes;
|
|
4
|
+
# major = any change that alters a number for any existing corpus case
|
|
5
|
+
# (including a grammar pin bump that does so).
|
|
6
|
+
[spec]
|
|
7
|
+
version = "1.1.0"
|
|
8
|
+
|
|
9
|
+
[[changelog]]
|
|
10
|
+
version = "1.1.0"
|
|
11
|
+
summary = """
|
|
12
|
+
Completions, no existing corpus value changes (MINOR): COG-JAVA-0001 added
|
|
13
|
+
(labeled break/continue = flat +1 in both modes — the whitepaper's fundamental
|
|
14
|
+
increment was silently missing for Java); TOK-ALL-0007 added (anonymous
|
|
15
|
+
word tokens outside the keyword table are identifiers — Python `__future__`
|
|
16
|
+
and Java contextual keywords previously fell through to OPERATOR, except
|
|
17
|
+
`yield`, which the Java keyword table wrongly reserved, contradicting
|
|
18
|
+
BW-JAVA-0001, so it lexed as KEYWORD); TOK-PY-0002 supersedes TOK-PY-0001
|
|
19
|
+
(`concatenated_string` is descended: implicit-concatenation parts are separate
|
|
20
|
+
STRING tokens and an interleaved comment is a COMMENT token — the superseded
|
|
21
|
+
ruling swallowed such comments into one string token, undercounting comment
|
|
22
|
+
lines); TOK-JAVA-0002 added (Java `_` is an IDENTIFIER token in every
|
|
23
|
+
position — `underscore_pattern` declarator uses previously lexed as OTHER,
|
|
24
|
+
while pattern/lambda uses already lexed as identifiers); TOK-PY-0003 added
|
|
25
|
+
(Python `...`, the named `ellipsis` leaf, lexes as an OPERATOR token and is
|
|
26
|
+
therefore visible to Halstead — previously it fell through to OTHER,
|
|
27
|
+
undercounting `.pyi`-shaped stub bodies); the lloc statement
|
|
28
|
+
tables were completed to match LOC-ALL-0004's stated scope (Python:
|
|
29
|
+
future_import_statement, type_alias_statement, legacy print/exec; Java:
|
|
30
|
+
package/import/interface/enum/record/annotation-type/constant declarations,
|
|
31
|
+
static initializers, compact constructors, explicit constructor invocations,
|
|
32
|
+
module declarations and directives). Numeric changes on inputs containing
|
|
33
|
+
those constructs only (including a Halstead change on `...`-bearing inputs);
|
|
34
|
+
every new ruling ships with its corpus case.
|
|
35
|
+
"""
|
|
36
|
+
|
|
37
|
+
[[changelog]]
|
|
38
|
+
version = "1.0.0"
|
|
39
|
+
summary = """
|
|
40
|
+
First calibrated evolution, arbitrated by the pre-registered BW faithfulness
|
|
41
|
+
experiment (validation/bw_faithfulness/derived/arbitration_report.md):
|
|
42
|
+
TOK-ALL-0006 supersedes TOK-ALL-0004 (indentation tab = 8, was 1 — under
|
|
43
|
+
tab=1 the indentation/readability correlation contradicted BW Fig. 9);
|
|
44
|
+
BW-ALL-0007 added (BW token-family features use the full lexical stream,
|
|
45
|
+
ERROR regions included — the construct is lexical; coverage on the original
|
|
46
|
+
dataset 29/100 clean parses -> 100/100 full token streams). The operator-class
|
|
47
|
+
arbitration returned a NULL result: BW-ALL-0006 unchanged. Numeric changes:
|
|
48
|
+
indentation features on tab-indented lines; BW token features on parse-error
|
|
49
|
+
inputs. Every active ruling is corpus-covered (the 1.0 gate).
|
|
50
|
+
"""
|
|
51
|
+
|
|
52
|
+
[[changelog]]
|
|
53
|
+
version = "0.1.0"
|
|
54
|
+
summary = "Seed ruling set: core policies, tokenization, CC/COG/HAL/MI/LOC, BW feature delimitation for Python + Java."
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
# LOC-*: line counting.
|
|
2
|
+
|
|
3
|
+
[[ruling]]
|
|
4
|
+
id = "LOC-ALL-0001"
|
|
5
|
+
metric = "loc"
|
|
6
|
+
language = "all"
|
|
7
|
+
title = "Physical and blank lines"
|
|
8
|
+
statement = """
|
|
9
|
+
physical_lines = number of lines of the normalized text (a trailing final
|
|
10
|
+
newline does not create an extra empty line). blank_lines = lines whose content
|
|
11
|
+
strips to empty.
|
|
12
|
+
"""
|
|
13
|
+
examples = ["py-multiline-string-001", "py-blank-lines-001"]
|
|
14
|
+
|
|
15
|
+
[[ruling]]
|
|
16
|
+
id = "LOC-ALL-0002"
|
|
17
|
+
metric = "loc"
|
|
18
|
+
language = "all"
|
|
19
|
+
title = "sloc: lines covered by at least one code token"
|
|
20
|
+
statement = """
|
|
21
|
+
sloc counts lines covered by the span of at least one non-comment token. A
|
|
22
|
+
multi-line string is code: every line it spans counts. (This is where the old
|
|
23
|
+
regex lane provably fails — ARCHITECTURE.md §6.)
|
|
24
|
+
"""
|
|
25
|
+
examples = ["py-multiline-string-001"]
|
|
26
|
+
|
|
27
|
+
[[ruling]]
|
|
28
|
+
id = "LOC-ALL-0003"
|
|
29
|
+
metric = "loc"
|
|
30
|
+
language = "all"
|
|
31
|
+
title = "comment_lines: lines covered by at least one comment token"
|
|
32
|
+
statement = """
|
|
33
|
+
comment_lines counts lines covered by the span of at least one comment token;
|
|
34
|
+
a multi-line block comment counts on EVERY line it spans. A line containing
|
|
35
|
+
both code and a trailing comment counts toward both sloc and comment_lines
|
|
36
|
+
(the two are coverage measures, not a partition).
|
|
37
|
+
"""
|
|
38
|
+
examples = ["java-block-comment-001", "py-multiline-string-001"]
|
|
39
|
+
|
|
40
|
+
[[ruling]]
|
|
41
|
+
id = "LOC-ALL-0004"
|
|
42
|
+
metric = "loc"
|
|
43
|
+
language = "all"
|
|
44
|
+
title = "lloc: statement-classified nodes"
|
|
45
|
+
statement = """
|
|
46
|
+
lloc counts nodes in the adapter's statement table (simple statements plus
|
|
47
|
+
compound-statement headers plus declarations). The table is part of the spec
|
|
48
|
+
surface and validated against the grammar.
|
|
49
|
+
"""
|
|
50
|
+
examples = ["py-cc-boolop-001", "java-elseif-001"]
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# MI-*: maintainability index.
|
|
2
|
+
|
|
3
|
+
[[ruling]]
|
|
4
|
+
id = "MI-ALL-0001"
|
|
5
|
+
metric = "maintainability_index"
|
|
6
|
+
language = "all"
|
|
7
|
+
title = "MI variant: no comment term, clamped to 0-100"
|
|
8
|
+
statement = """
|
|
9
|
+
MI = clamp_0_100((171 - 5.2*ln(V) - 0.23*CC - 16.2*ln(SLOC)) * 100/171), the
|
|
10
|
+
Visual Studio / radon convention without the comment term. MI is DERIVED from
|
|
11
|
+
Halstead volume, cyclomatic complexity and SLOC — it is not independent of CC
|
|
12
|
+
(ARCHITECTURE.md §13); every MI value carries a typed derived_from and a standing
|
|
13
|
+
mi-contains-cc diagnostic.
|
|
14
|
+
"""
|
|
15
|
+
examples = ["py-cc-boolop-001"]
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
# TOK-*: input normalization and the unified lexical token stream.
|
|
2
|
+
# These change emitted numbers, so they are spec, not implementation detail.
|
|
3
|
+
|
|
4
|
+
[[ruling]]
|
|
5
|
+
id = "TOK-ALL-0001"
|
|
6
|
+
metric = "tokenization"
|
|
7
|
+
language = "all"
|
|
8
|
+
title = "Input is decoded as UTF-8 with replacement"
|
|
9
|
+
statement = """
|
|
10
|
+
Byte input is decoded as UTF-8; undecodable bytes are replaced (U+FFFD) and an
|
|
11
|
+
encoding-replaced diagnostic is attached. str input is used as-is.
|
|
12
|
+
"""
|
|
13
|
+
examples = ["py-tok-normalize-001"]
|
|
14
|
+
|
|
15
|
+
[[ruling]]
|
|
16
|
+
id = "TOK-ALL-0002"
|
|
17
|
+
metric = "tokenization"
|
|
18
|
+
language = "all"
|
|
19
|
+
title = "A leading UTF-8 BOM is stripped"
|
|
20
|
+
statement = """
|
|
21
|
+
A leading U+FEFF is stripped before measurement (with a bom-stripped
|
|
22
|
+
diagnostic); otherwise it would pollute line-1 character and length features.
|
|
23
|
+
"""
|
|
24
|
+
examples = ["py-tok-normalize-001"]
|
|
25
|
+
|
|
26
|
+
[[ruling]]
|
|
27
|
+
id = "TOK-ALL-0003"
|
|
28
|
+
metric = "tokenization"
|
|
29
|
+
language = "all"
|
|
30
|
+
title = "Line endings are normalized to LF"
|
|
31
|
+
statement = """
|
|
32
|
+
CRLF and lone CR are normalized to LF before measurement, so character-level
|
|
33
|
+
features are platform-stable.
|
|
34
|
+
"""
|
|
35
|
+
examples = ["py-tok-normalize-001"]
|
|
36
|
+
|
|
37
|
+
[[ruling]]
|
|
38
|
+
id = "TOK-ALL-0004"
|
|
39
|
+
metric = "tokenization"
|
|
40
|
+
language = "all"
|
|
41
|
+
title = "A tab counts as one indentation character (provisional)"
|
|
42
|
+
status = "superseded"
|
|
43
|
+
superseded_by = "TOK-ALL-0006"
|
|
44
|
+
statement = """
|
|
45
|
+
Indentation is measured as the count of leading whitespace characters; a tab
|
|
46
|
+
counts as 1. PROVISIONAL: the BW faithfulness pipeline (§6.3) will arbitrate
|
|
47
|
+
tab semantics empirically; any change supersedes this ruling.
|
|
48
|
+
"""
|
|
49
|
+
|
|
50
|
+
[[ruling]]
|
|
51
|
+
id = "TOK-ALL-0005"
|
|
52
|
+
metric = "tokenization"
|
|
53
|
+
language = "all"
|
|
54
|
+
title = "Token line attribution"
|
|
55
|
+
statement = """
|
|
56
|
+
Every lexical token is attributed to the physical line of its start position.
|
|
57
|
+
A multi-line token (string, block comment) is ONE token on its start line for
|
|
58
|
+
token-count features; line-coverage features (sloc, comment lines) use its full
|
|
59
|
+
span instead.
|
|
60
|
+
"""
|
|
61
|
+
|
|
62
|
+
[[ruling]]
|
|
63
|
+
id = "TOK-PY-0001"
|
|
64
|
+
metric = "tokenization"
|
|
65
|
+
language = "python"
|
|
66
|
+
title = "Python atomic tokens: strings are consumed whole"
|
|
67
|
+
status = "superseded"
|
|
68
|
+
superseded_by = "TOK-PY-0002"
|
|
69
|
+
statement = """
|
|
70
|
+
`string` and `concatenated_string` subtrees are consumed as single STRING
|
|
71
|
+
tokens. F-string interpolation contents therefore do NOT contribute token-level
|
|
72
|
+
features (identifiers/operators inside interpolations are invisible). This
|
|
73
|
+
diverges from CPython 3.12+ tokenize (PEP 701), which tokenizes interpolation
|
|
74
|
+
contents; the divergence vs the stdlib reference extractor is classified, and
|
|
75
|
+
uniformity with Java strings is preferred.
|
|
76
|
+
"""
|
|
77
|
+
node_types = ["string", "concatenated_string"]
|
|
78
|
+
|
|
79
|
+
[[ruling]]
|
|
80
|
+
id = "TOK-PY-0002"
|
|
81
|
+
metric = "tokenization"
|
|
82
|
+
language = "python"
|
|
83
|
+
title = "Python atomic tokens: strings are consumed whole; implicit concatenation is descended"
|
|
84
|
+
since_spec = "1.1.0"
|
|
85
|
+
statement = """
|
|
86
|
+
`string` subtrees are consumed as single STRING tokens. F-string interpolation
|
|
87
|
+
contents therefore do NOT contribute token-level features (identifiers and
|
|
88
|
+
operators inside interpolations are invisible); this diverges from CPython
|
|
89
|
+
3.12+ tokenize (PEP 701), the divergence vs the stdlib reference extractor is
|
|
90
|
+
classified, and uniformity with Java strings is preferred. A
|
|
91
|
+
`concatenated_string` (implicit concatenation) is NOT atomic: it is descended,
|
|
92
|
+
so each string part is its own STRING token and a comment interleaved between
|
|
93
|
+
parts is an ordinary COMMENT token — matching CPython tokenize. Supersedes
|
|
94
|
+
TOK-PY-0001, which consumed `concatenated_string` whole and thereby silently
|
|
95
|
+
swallowed interleaved comments (undercounting comment lines and counting
|
|
96
|
+
comment-only lines inside the concatenation as code).
|
|
97
|
+
"""
|
|
98
|
+
node_types = ["string", "concatenated_string"]
|
|
99
|
+
examples = ["py-concat-string-001", "py-multiline-string-001"]
|
|
100
|
+
|
|
101
|
+
[[ruling]]
|
|
102
|
+
id = "TOK-PY-0003"
|
|
103
|
+
metric = "tokenization"
|
|
104
|
+
language = "python"
|
|
105
|
+
title = "Python `...` is an operator token"
|
|
106
|
+
since_spec = "1.1.0"
|
|
107
|
+
statement = """
|
|
108
|
+
The `...` literal (the grammar's named `ellipsis` leaf) lexes as an OPERATOR
|
|
109
|
+
token. This matches CPython tokenize (which emits an OP token for `...`) and
|
|
110
|
+
Java's varargs `...` (also OPERATOR), and keeps `...` visible to Halstead as
|
|
111
|
+
an operator — consistent with Python's other singleton literals (None/True/
|
|
112
|
+
False count as Halstead operators via keyword_leaf_types). Before 1.1.0 the
|
|
113
|
+
named `ellipsis` leaf fell through to OTHER and was invisible to Halstead
|
|
114
|
+
(systematically undercounting `.pyi` stub bodies, which are mostly `...`). BW
|
|
115
|
+
token-family features are unaffected either way: `...` matches none of the
|
|
116
|
+
counted operator spellings.
|
|
117
|
+
"""
|
|
118
|
+
node_types = ["ellipsis"]
|
|
119
|
+
examples = ["py-ellipsis-001"]
|
|
120
|
+
|
|
121
|
+
[[ruling]]
|
|
122
|
+
id = "TOK-JAVA-0001"
|
|
123
|
+
metric = "tokenization"
|
|
124
|
+
language = "java"
|
|
125
|
+
title = "Java atomic tokens: string and character literals"
|
|
126
|
+
statement = """
|
|
127
|
+
`string_literal` and `character_literal` subtrees are consumed as single STRING
|
|
128
|
+
tokens (text blocks included).
|
|
129
|
+
"""
|
|
130
|
+
node_types = ["string_literal", "character_literal"]
|
|
131
|
+
|
|
132
|
+
[[ruling]]
|
|
133
|
+
id = "TOK-JAVA-0002"
|
|
134
|
+
metric = "tokenization"
|
|
135
|
+
language = "java"
|
|
136
|
+
title = "Java `_` is an identifier token in every position"
|
|
137
|
+
since_spec = "1.1.0"
|
|
138
|
+
statement = """
|
|
139
|
+
The reserved word `_` (JLS §3.9, reserved since SE 9) lexes as an IDENTIFIER
|
|
140
|
+
token in every syntactic position. The grammar already yields `identifier`
|
|
141
|
+
nodes for `_` in switch patterns and lambda parameters; the
|
|
142
|
+
`underscore_pattern` node it yields for unnamed variable declarators is
|
|
143
|
+
classified identically, so the same source token cannot silently change token
|
|
144
|
+
class with syntactic position (before 1.1.0 it lexed as OTHER there —
|
|
145
|
+
invisible to BW token-family features and Halstead). Mirrors BW-PY-0001,
|
|
146
|
+
which counts Python's soft-keyword `_` as an identifier. BW-JAVA-0001's
|
|
147
|
+
keyword table deliberately omits `_`.
|
|
148
|
+
"""
|
|
149
|
+
node_types = ["underscore_pattern"]
|
|
150
|
+
examples = ["java-underscore-001"]
|
|
151
|
+
|
|
152
|
+
[[ruling]]
|
|
153
|
+
id = "TOK-ALL-0007"
|
|
154
|
+
metric = "tokenization"
|
|
155
|
+
language = "all"
|
|
156
|
+
title = "Anonymous word tokens outside the keyword table are identifiers"
|
|
157
|
+
since_spec = "1.1.0"
|
|
158
|
+
statement = """
|
|
159
|
+
A grammar-anonymous leaf token whose text is word-shaped (letter/underscore
|
|
160
|
+
start, word characters, internal hyphens allowed) and not in the language's
|
|
161
|
+
keyword table is an IDENTIFIER token: Python's `__future__`, Java's contextual
|
|
162
|
+
keywords (record, sealed, non-sealed, permits, when, yield, module directives).
|
|
163
|
+
This matches the reference tokenizers, which yield NAME for all of them, and
|
|
164
|
+
keeps the keyword tables exactly the reserved-word sets (BW-PY-0001,
|
|
165
|
+
BW-JAVA-0001). Before 1.1.0 these tokens fell through to OPERATOR — except
|
|
166
|
+
Java's `yield`, which was wrongly in the keyword table (contradicting
|
|
167
|
+
BW-JAVA-0001's stated set) and lexed as KEYWORD.
|
|
168
|
+
"""
|
|
169
|
+
examples = ["py-future-import-001", "java-contextual-001"]
|
|
170
|
+
|
|
171
|
+
[[ruling]]
|
|
172
|
+
id = "TOK-ALL-0006"
|
|
173
|
+
metric = "tokenization"
|
|
174
|
+
language = "all"
|
|
175
|
+
title = "A tab counts as 8 indentation characters (arbitrated)"
|
|
176
|
+
since_spec = "1.0.0"
|
|
177
|
+
statement = """
|
|
178
|
+
Indentation features count leading whitespace with a tab as 8 characters and a
|
|
179
|
+
space as 1. Supersedes TOK-ALL-0004 (tab = 1), whose PROVISIONAL marker was
|
|
180
|
+
resolved by the pre-registered 32-cell BW faithfulness arbitration
|
|
181
|
+
(validation/bw_faithfulness/derived/arbitration_report.md, 2026-07): under
|
|
182
|
+
tab = 1 the avg_indentation correlation with the human readability scores is
|
|
183
|
+
zero (rho +0.001), contradicting the paper's Fig. 9; any expansion >= 2 fixes
|
|
184
|
+
the sign in both extraction modes, and 8 won the pre-registered AUC tie-break
|
|
185
|
+
(honest caveat, per the independent verification: 8-vs-4 is a deterministic
|
|
186
|
+
convention pick within statistical noise at n=100 — both deliver the
|
|
187
|
+
Fig. 9-matching rho ~ -0.25; 2 does not). Scope: indentation features only;
|
|
188
|
+
line-length and space-count features remain raw character counts (a candidate
|
|
189
|
+
for future arbitration, noted in the report).
|
|
190
|
+
"""
|
|
191
|
+
examples = ["py-tok-normalize-001"]
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
# The grammar versions this release is calibrated against (ARCHITECTURE.md §10).
|
|
2
|
+
# A deviating installed version still runs, but every report is stamped
|
|
3
|
+
# GrammarInfo.validated=false plus an unvalidated-grammar diagnostic.
|
|
4
|
+
[grammars]
|
|
5
|
+
python = "0.25.0"
|
|
6
|
+
java = "0.23.5"
|
|
File without changes
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
"""The tree-sitter quarantine — the ONLY module that imports ``tree_sitter``.
|
|
2
|
+
|
|
3
|
+
py-tree-sitter breaks its API across 0.x minors (0.22 removed
|
|
4
|
+
``Language(path, name)``; 0.23 removed the Parser setters; 0.24/0.25 moved
|
|
5
|
+
query execution to ``QueryCursor`` and deprecated ``Language.version`` for
|
|
6
|
+
``abi_version``). Confining the import here bounds every future breakage to one
|
|
7
|
+
file (ARCHITECTURE.md §4). The MVP deliberately avoids the Query API entirely —
|
|
8
|
+
cursor walks suffice.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
import importlib
|
|
14
|
+
import importlib.metadata
|
|
15
|
+
from collections.abc import Iterator
|
|
16
|
+
from typing import Any
|
|
17
|
+
|
|
18
|
+
from tree_sitter import Language, Node, Parser, Tree
|
|
19
|
+
|
|
20
|
+
from codecaliper.errors import GrammarLoadError
|
|
21
|
+
|
|
22
|
+
__all__ = ["Language", "Node", "Parser", "Tree", "load_language", "make_parser",
|
|
23
|
+
"walk", "leaves", "node_kinds", "count_error_nodes"]
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def load_language(grammar_module: str) -> tuple[Language, str, int]:
|
|
27
|
+
"""Import a grammar wheel and wrap its PyCapsule.
|
|
28
|
+
|
|
29
|
+
Returns ``(language, installed_version, abi_version)``.
|
|
30
|
+
"""
|
|
31
|
+
try:
|
|
32
|
+
mod = importlib.import_module(grammar_module)
|
|
33
|
+
lang = Language(mod.language())
|
|
34
|
+
except Exception as exc: # noqa: BLE001 — surface as our typed error
|
|
35
|
+
raise GrammarLoadError(f"cannot load grammar module {grammar_module!r}: {exc}") from exc
|
|
36
|
+
dist = grammar_module.replace("_", "-")
|
|
37
|
+
try:
|
|
38
|
+
version = importlib.metadata.version(dist)
|
|
39
|
+
except importlib.metadata.PackageNotFoundError:
|
|
40
|
+
version = "unknown"
|
|
41
|
+
# version -> abi_version shim (abi_version since py-tree-sitter 0.25)
|
|
42
|
+
abi: int | None = getattr(lang, "abi_version", None)
|
|
43
|
+
if abi is None: # pragma: no cover - older binding shim
|
|
44
|
+
abi = int(getattr(lang, "version", 0))
|
|
45
|
+
return lang, version, int(abi)
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def make_parser(language: Language) -> Parser:
|
|
49
|
+
return Parser(language)
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def walk(tree: Tree) -> Iterator[tuple[Node, int]]:
|
|
53
|
+
"""Cursor-based preorder ``(node, depth)`` — O(1) memory, no recursion limit,
|
|
54
|
+
deterministic order."""
|
|
55
|
+
cursor = tree.walk()
|
|
56
|
+
depth = 0
|
|
57
|
+
while True:
|
|
58
|
+
node = cursor.node
|
|
59
|
+
assert node is not None # a fresh walk cursor always sits on a node
|
|
60
|
+
yield node, depth
|
|
61
|
+
if cursor.goto_first_child():
|
|
62
|
+
depth += 1
|
|
63
|
+
continue
|
|
64
|
+
while True:
|
|
65
|
+
if cursor.goto_next_sibling():
|
|
66
|
+
break
|
|
67
|
+
if not cursor.goto_parent():
|
|
68
|
+
return
|
|
69
|
+
depth -= 1
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def is_opaque(node: Node) -> bool:
|
|
73
|
+
"""ERROR/MISSING subtrees are opaque to all measurement (CORE-ALL-0002)."""
|
|
74
|
+
return node.type == "ERROR" or node.is_missing
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def leaves(tree: Tree, atomic_types: frozenset[str]) -> Iterator[Node]:
|
|
78
|
+
"""Terminal-token stream: descend to leaves, but consume node types in
|
|
79
|
+
``atomic_types`` (e.g. Python ``string`` with its start/content/end
|
|
80
|
+
children) as single opaque tokens (TOK-PY-0002 / TOK-JAVA-0001).
|
|
81
|
+
|
|
82
|
+
ERROR/MISSING subtrees yield nothing and are not descended into
|
|
83
|
+
(CORE-ALL-0002): their tokens must not feed Halstead/LOC or (on clean
|
|
84
|
+
parses) BW. On parse errors, BW token-family features instead use
|
|
85
|
+
:func:`leaves_lexical`, which descends into ERROR subtrees (BW-ALL-0007).
|
|
86
|
+
"""
|
|
87
|
+
cursor = tree.walk()
|
|
88
|
+
while True:
|
|
89
|
+
node = cursor.node
|
|
90
|
+
assert node is not None # a fresh walk cursor always sits on a node
|
|
91
|
+
descend = False
|
|
92
|
+
if not is_opaque(node):
|
|
93
|
+
if node.child_count == 0 or node.type in atomic_types:
|
|
94
|
+
yield node
|
|
95
|
+
else:
|
|
96
|
+
descend = True
|
|
97
|
+
if descend and cursor.goto_first_child():
|
|
98
|
+
continue
|
|
99
|
+
while True:
|
|
100
|
+
if cursor.goto_next_sibling():
|
|
101
|
+
break
|
|
102
|
+
if not cursor.goto_parent():
|
|
103
|
+
return
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def leaves_lexical(tree: Tree, atomic_types: frozenset[str]) -> Iterator[Node]:
|
|
107
|
+
"""Full lexical leaf stream: like :func:`leaves`, but DESCENDS into ERROR
|
|
108
|
+
subtrees, yielding the tokens tree-sitter recovered inside them — the BW
|
|
109
|
+
construct is lexical and must see fragment tokens (BW-ALL-0007). MISSING
|
|
110
|
+
nodes are zero-width fabrications and still yield nothing; a childless
|
|
111
|
+
ERROR node (raw untokenized bytes) also yields nothing."""
|
|
112
|
+
cursor = tree.walk()
|
|
113
|
+
while True:
|
|
114
|
+
node = cursor.node
|
|
115
|
+
assert node is not None # a fresh walk cursor always sits on a node
|
|
116
|
+
descend = False
|
|
117
|
+
if not node.is_missing:
|
|
118
|
+
if node.type == "ERROR":
|
|
119
|
+
descend = node.child_count > 0
|
|
120
|
+
elif node.child_count == 0 or node.type in atomic_types:
|
|
121
|
+
yield node
|
|
122
|
+
else:
|
|
123
|
+
descend = True
|
|
124
|
+
if descend and cursor.goto_first_child():
|
|
125
|
+
continue
|
|
126
|
+
while True:
|
|
127
|
+
if cursor.goto_next_sibling():
|
|
128
|
+
break
|
|
129
|
+
if not cursor.goto_parent():
|
|
130
|
+
return
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def node_kinds(language: Language) -> frozenset[str]:
|
|
134
|
+
"""Grammar introspection: every node-kind string of the COMPILED grammar.
|
|
135
|
+
|
|
136
|
+
Used by tests/test_grammar_integrity.py to validate adapter tables and
|
|
137
|
+
ruling node_types against the actually-installed grammar, so a grammar node
|
|
138
|
+
rename fails loudly instead of silently zeroing metrics.
|
|
139
|
+
"""
|
|
140
|
+
count: int = language.node_kind_count
|
|
141
|
+
kinds = set()
|
|
142
|
+
for i in range(count):
|
|
143
|
+
kind = language.node_kind_for_id(i)
|
|
144
|
+
if kind is not None:
|
|
145
|
+
kinds.add(kind)
|
|
146
|
+
return frozenset(kinds)
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
def count_error_nodes(tree: Tree) -> int:
|
|
150
|
+
"""Number of ERROR/MISSING nodes (parse-error policy CORE-ALL-0002)."""
|
|
151
|
+
n = 0
|
|
152
|
+
for node, _ in walk(tree):
|
|
153
|
+
if node.type == "ERROR" or node.is_missing:
|
|
154
|
+
n += 1
|
|
155
|
+
return n
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def node_text(node: Node, source: bytes) -> str:
|
|
159
|
+
return source[node.start_byte : node.end_byte].decode("utf-8", errors="replace")
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
def field_name_of(node: Node) -> str | None:
|
|
163
|
+
"""The field name this node occupies in its parent, if any."""
|
|
164
|
+
parent = node.parent
|
|
165
|
+
if parent is None:
|
|
166
|
+
return None
|
|
167
|
+
# tree-sitter exposes this via the parent's field iteration; the portable
|
|
168
|
+
# way across binding versions is child_by_field_name comparison.
|
|
169
|
+
for field in ("alternative", "consequence", "condition", "operator", "body", "name"):
|
|
170
|
+
try:
|
|
171
|
+
if parent.child_by_field_name(field) == node:
|
|
172
|
+
return field
|
|
173
|
+
except Exception: # pragma: no cover - defensive against binding churn
|
|
174
|
+
return None
|
|
175
|
+
return None
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
def language_of_parser(parser: Parser) -> Any: # pragma: no cover - debugging aid
|
|
179
|
+
return parser.language
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
"""Grammar loading and the calibration check (ARCHITECTURE.md §10).
|
|
2
|
+
|
|
3
|
+
A deviating installed grammar version never refuses to run — it is stamped
|
|
4
|
+
``GrammarInfo.validated=False`` and every report carries an
|
|
5
|
+
``unvalidated-grammar`` diagnostic. Run and label, never refuse, never silently
|
|
6
|
+
trust.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import sys
|
|
12
|
+
from functools import cache, lru_cache
|
|
13
|
+
from importlib import resources
|
|
14
|
+
|
|
15
|
+
from codecaliper.model import GrammarInfo
|
|
16
|
+
from codecaliper.syntax import _treesitter as ts
|
|
17
|
+
|
|
18
|
+
if sys.version_info >= (3, 11):
|
|
19
|
+
import tomllib
|
|
20
|
+
else: # pragma: no cover - py3.10 fallback
|
|
21
|
+
import tomli as tomllib
|
|
22
|
+
|
|
23
|
+
_GRAMMAR_MODULES = {"python": "tree_sitter_python", "java": "tree_sitter_java"}
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
@lru_cache(maxsize=1)
|
|
27
|
+
def validated_versions() -> dict[str, str]:
|
|
28
|
+
ref = resources.files("codecaliper.spec") / "validated_grammars.toml"
|
|
29
|
+
with ref.open("rb") as f:
|
|
30
|
+
return dict(tomllib.load(f)["grammars"])
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
@cache
|
|
34
|
+
def load(language_name: str) -> tuple[ts.Language, ts.Parser, GrammarInfo]:
|
|
35
|
+
module = _GRAMMAR_MODULES[language_name]
|
|
36
|
+
lang, version, abi = ts.load_language(module)
|
|
37
|
+
parser = ts.make_parser(lang)
|
|
38
|
+
info = GrammarInfo(
|
|
39
|
+
language=language_name,
|
|
40
|
+
package=module.replace("_", "-"),
|
|
41
|
+
version=version,
|
|
42
|
+
abi_version=abi,
|
|
43
|
+
validated=(validated_versions().get(language_name) == version),
|
|
44
|
+
)
|
|
45
|
+
return lang, parser, info
|