codeecho 1.0.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.
- codeecho/.ignore +41 -0
- codeecho/__init__.py +22 -0
- codeecho/__main__.py +289 -0
- codeecho/db.py +289 -0
- codeecho/detector.py +246 -0
- codeecho/extractor.py +139 -0
- codeecho/fingerprint.py +38 -0
- codeecho/logging.ini +28 -0
- codeecho/models.py +52 -0
- codeecho/normalizer.py +519 -0
- codeecho/parser.py +94 -0
- codeecho/reporter/__init__.py +6 -0
- codeecho/reporter/html_reporter.py +212 -0
- codeecho/reporter/json_reporter.py +82 -0
- codeecho/scanner.py +109 -0
- codeecho-1.0.0.dist-info/METADATA +208 -0
- codeecho-1.0.0.dist-info/RECORD +20 -0
- codeecho-1.0.0.dist-info/WHEEL +4 -0
- codeecho-1.0.0.dist-info/entry_points.txt +3 -0
- codeecho-1.0.0.dist-info/licenses/LICENSE +21 -0
codeecho/normalizer.py
ADDED
|
@@ -0,0 +1,519 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Token extraction and Type-2 normalisation for code fragments.
|
|
3
|
+
|
|
4
|
+
Tokens are extracted from the raw fragment source text using a language-aware
|
|
5
|
+
regex-based tokeniser. This avoids direct tree-sitter AST node traversal,
|
|
6
|
+
which is unstable across tree-sitter 0.26+ Python bindings.
|
|
7
|
+
|
|
8
|
+
Identifiers and literals are replaced with sequential placeholders
|
|
9
|
+
(``ID_0``, ``ID_1``, ``LIT_0``, …) so that two fragments with the same structure
|
|
10
|
+
but different variable names still produce the same normalised token sequence.
|
|
11
|
+
|
|
12
|
+
:author: Ron Webb
|
|
13
|
+
:since: 1.0.0
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
import re
|
|
17
|
+
import logging
|
|
18
|
+
from dataclasses import dataclass, field
|
|
19
|
+
|
|
20
|
+
from tree_sitter import Node
|
|
21
|
+
|
|
22
|
+
_logger = logging.getLogger("codeecho.normalizer")
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
@dataclass
|
|
26
|
+
class NormalisationProfile:
|
|
27
|
+
"""Language-specific sets of identifier and literal node types."""
|
|
28
|
+
|
|
29
|
+
identifier_types: frozenset[str] = field(default_factory=frozenset)
|
|
30
|
+
literal_types: frozenset[str] = field(default_factory=frozenset)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
_PROFILES: dict[str, NormalisationProfile] = {
|
|
34
|
+
"Python": NormalisationProfile(
|
|
35
|
+
identifier_types=frozenset({"identifier", "type_identifier"}),
|
|
36
|
+
literal_types=frozenset(
|
|
37
|
+
{"string", "integer", "float", "true", "false", "none"}
|
|
38
|
+
),
|
|
39
|
+
),
|
|
40
|
+
"JavaScript": NormalisationProfile(
|
|
41
|
+
identifier_types=frozenset(
|
|
42
|
+
{"identifier", "property_identifier", "shorthand_property_identifier"}
|
|
43
|
+
),
|
|
44
|
+
literal_types=frozenset(
|
|
45
|
+
{"string", "number", "true", "false", "null", "template_string"}
|
|
46
|
+
),
|
|
47
|
+
),
|
|
48
|
+
"TypeScript": NormalisationProfile(
|
|
49
|
+
identifier_types=frozenset(
|
|
50
|
+
{
|
|
51
|
+
"identifier",
|
|
52
|
+
"property_identifier",
|
|
53
|
+
"type_identifier",
|
|
54
|
+
"shorthand_property_identifier",
|
|
55
|
+
}
|
|
56
|
+
),
|
|
57
|
+
literal_types=frozenset(
|
|
58
|
+
{"string", "number", "true", "false", "null", "template_string"}
|
|
59
|
+
),
|
|
60
|
+
),
|
|
61
|
+
"Java": NormalisationProfile(
|
|
62
|
+
identifier_types=frozenset({"identifier", "type_identifier"}),
|
|
63
|
+
literal_types=frozenset(
|
|
64
|
+
{
|
|
65
|
+
"string_literal",
|
|
66
|
+
"decimal_integer_literal",
|
|
67
|
+
"decimal_floating_point_literal",
|
|
68
|
+
"true",
|
|
69
|
+
"false",
|
|
70
|
+
"null_literal",
|
|
71
|
+
}
|
|
72
|
+
),
|
|
73
|
+
),
|
|
74
|
+
"Gosu": NormalisationProfile(
|
|
75
|
+
identifier_types=frozenset({"identifier", "type_identifier"}),
|
|
76
|
+
literal_types=frozenset(
|
|
77
|
+
{
|
|
78
|
+
"string_literal",
|
|
79
|
+
"decimal_integer_literal",
|
|
80
|
+
"decimal_floating_point_literal",
|
|
81
|
+
"true",
|
|
82
|
+
"false",
|
|
83
|
+
"null_literal",
|
|
84
|
+
}
|
|
85
|
+
),
|
|
86
|
+
),
|
|
87
|
+
"Go": NormalisationProfile(
|
|
88
|
+
identifier_types=frozenset(
|
|
89
|
+
{"identifier", "type_identifier", "field_identifier"}
|
|
90
|
+
),
|
|
91
|
+
literal_types=frozenset(
|
|
92
|
+
{
|
|
93
|
+
"interpreted_string_literal",
|
|
94
|
+
"raw_string_literal",
|
|
95
|
+
"int_literal",
|
|
96
|
+
"float_literal",
|
|
97
|
+
"true",
|
|
98
|
+
"false",
|
|
99
|
+
"nil",
|
|
100
|
+
}
|
|
101
|
+
),
|
|
102
|
+
),
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
_FALLBACK_PROFILE: NormalisationProfile = NormalisationProfile()
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
# ── Regex-based tokeniser ───────────────────────────────────────────────────
|
|
109
|
+
# Matches identifiers/keywords, number literals, quoted strings, and single
|
|
110
|
+
# non-whitespace characters (operators, punctuation).
|
|
111
|
+
_TOKEN_RE: re.Pattern[str] = re.compile(
|
|
112
|
+
r'"(?:[^"\\]|\\.)*"' # double-quoted string
|
|
113
|
+
r"|'(?:[^'\\]|\\.)*'" # single-quoted string
|
|
114
|
+
r"|`(?:[^`\\]|\\.)*`" # backtick string (JS/Go)
|
|
115
|
+
r"|//[^\n]*" # single-line comment
|
|
116
|
+
r"|/\*.*?\*/" # multi-line comment (non-greedy)
|
|
117
|
+
r"|[0-9]+(?:\.[0-9]+)?" # integer or float literal
|
|
118
|
+
r"|[a-zA-Z_$][a-zA-Z0-9_$]*" # identifier / keyword
|
|
119
|
+
r"|[^\s]", # any other single non-whitespace char
|
|
120
|
+
re.DOTALL,
|
|
121
|
+
)
|
|
122
|
+
|
|
123
|
+
# Per-language keyword sets used to distinguish identifiers from keywords
|
|
124
|
+
_KEYWORDS: dict[str, frozenset[str]] = {
|
|
125
|
+
"Python": frozenset(
|
|
126
|
+
{
|
|
127
|
+
"False",
|
|
128
|
+
"None",
|
|
129
|
+
"True",
|
|
130
|
+
"and",
|
|
131
|
+
"as",
|
|
132
|
+
"assert",
|
|
133
|
+
"async",
|
|
134
|
+
"await",
|
|
135
|
+
"break",
|
|
136
|
+
"class",
|
|
137
|
+
"continue",
|
|
138
|
+
"def",
|
|
139
|
+
"del",
|
|
140
|
+
"elif",
|
|
141
|
+
"else",
|
|
142
|
+
"except",
|
|
143
|
+
"finally",
|
|
144
|
+
"for",
|
|
145
|
+
"from",
|
|
146
|
+
"global",
|
|
147
|
+
"if",
|
|
148
|
+
"import",
|
|
149
|
+
"in",
|
|
150
|
+
"is",
|
|
151
|
+
"lambda",
|
|
152
|
+
"nonlocal",
|
|
153
|
+
"not",
|
|
154
|
+
"or",
|
|
155
|
+
"pass",
|
|
156
|
+
"raise",
|
|
157
|
+
"return",
|
|
158
|
+
"try",
|
|
159
|
+
"while",
|
|
160
|
+
"with",
|
|
161
|
+
"yield",
|
|
162
|
+
}
|
|
163
|
+
),
|
|
164
|
+
"JavaScript": frozenset(
|
|
165
|
+
{
|
|
166
|
+
"break",
|
|
167
|
+
"case",
|
|
168
|
+
"catch",
|
|
169
|
+
"class",
|
|
170
|
+
"const",
|
|
171
|
+
"continue",
|
|
172
|
+
"debugger",
|
|
173
|
+
"default",
|
|
174
|
+
"delete",
|
|
175
|
+
"do",
|
|
176
|
+
"else",
|
|
177
|
+
"export",
|
|
178
|
+
"extends",
|
|
179
|
+
"false",
|
|
180
|
+
"finally",
|
|
181
|
+
"for",
|
|
182
|
+
"function",
|
|
183
|
+
"if",
|
|
184
|
+
"import",
|
|
185
|
+
"in",
|
|
186
|
+
"instanceof",
|
|
187
|
+
"let",
|
|
188
|
+
"new",
|
|
189
|
+
"null",
|
|
190
|
+
"return",
|
|
191
|
+
"static",
|
|
192
|
+
"super",
|
|
193
|
+
"switch",
|
|
194
|
+
"this",
|
|
195
|
+
"throw",
|
|
196
|
+
"true",
|
|
197
|
+
"try",
|
|
198
|
+
"typeof",
|
|
199
|
+
"undefined",
|
|
200
|
+
"var",
|
|
201
|
+
"void",
|
|
202
|
+
"while",
|
|
203
|
+
"with",
|
|
204
|
+
"yield",
|
|
205
|
+
}
|
|
206
|
+
),
|
|
207
|
+
"TypeScript": frozenset(
|
|
208
|
+
{
|
|
209
|
+
"abstract",
|
|
210
|
+
"any",
|
|
211
|
+
"as",
|
|
212
|
+
"asserts",
|
|
213
|
+
"async",
|
|
214
|
+
"await",
|
|
215
|
+
"break",
|
|
216
|
+
"case",
|
|
217
|
+
"catch",
|
|
218
|
+
"class",
|
|
219
|
+
"const",
|
|
220
|
+
"continue",
|
|
221
|
+
"debugger",
|
|
222
|
+
"declare",
|
|
223
|
+
"default",
|
|
224
|
+
"delete",
|
|
225
|
+
"do",
|
|
226
|
+
"else",
|
|
227
|
+
"enum",
|
|
228
|
+
"export",
|
|
229
|
+
"extends",
|
|
230
|
+
"false",
|
|
231
|
+
"finally",
|
|
232
|
+
"for",
|
|
233
|
+
"from",
|
|
234
|
+
"function",
|
|
235
|
+
"if",
|
|
236
|
+
"implements",
|
|
237
|
+
"import",
|
|
238
|
+
"in",
|
|
239
|
+
"infer",
|
|
240
|
+
"instanceof",
|
|
241
|
+
"interface",
|
|
242
|
+
"is",
|
|
243
|
+
"keyof",
|
|
244
|
+
"let",
|
|
245
|
+
"module",
|
|
246
|
+
"namespace",
|
|
247
|
+
"never",
|
|
248
|
+
"new",
|
|
249
|
+
"null",
|
|
250
|
+
"of",
|
|
251
|
+
"override",
|
|
252
|
+
"private",
|
|
253
|
+
"protected",
|
|
254
|
+
"public",
|
|
255
|
+
"readonly",
|
|
256
|
+
"return",
|
|
257
|
+
"satisfies",
|
|
258
|
+
"static",
|
|
259
|
+
"super",
|
|
260
|
+
"switch",
|
|
261
|
+
"this",
|
|
262
|
+
"throw",
|
|
263
|
+
"true",
|
|
264
|
+
"try",
|
|
265
|
+
"type",
|
|
266
|
+
"typeof",
|
|
267
|
+
"undefined",
|
|
268
|
+
"unique",
|
|
269
|
+
"unknown",
|
|
270
|
+
"var",
|
|
271
|
+
"void",
|
|
272
|
+
"while",
|
|
273
|
+
"with",
|
|
274
|
+
"yield",
|
|
275
|
+
}
|
|
276
|
+
),
|
|
277
|
+
"Java": frozenset(
|
|
278
|
+
{
|
|
279
|
+
"abstract",
|
|
280
|
+
"assert",
|
|
281
|
+
"boolean",
|
|
282
|
+
"break",
|
|
283
|
+
"byte",
|
|
284
|
+
"case",
|
|
285
|
+
"catch",
|
|
286
|
+
"char",
|
|
287
|
+
"class",
|
|
288
|
+
"const",
|
|
289
|
+
"continue",
|
|
290
|
+
"default",
|
|
291
|
+
"do",
|
|
292
|
+
"double",
|
|
293
|
+
"else",
|
|
294
|
+
"enum",
|
|
295
|
+
"extends",
|
|
296
|
+
"final",
|
|
297
|
+
"finally",
|
|
298
|
+
"float",
|
|
299
|
+
"for",
|
|
300
|
+
"goto",
|
|
301
|
+
"if",
|
|
302
|
+
"implements",
|
|
303
|
+
"import",
|
|
304
|
+
"instanceof",
|
|
305
|
+
"int",
|
|
306
|
+
"interface",
|
|
307
|
+
"long",
|
|
308
|
+
"native",
|
|
309
|
+
"new",
|
|
310
|
+
"null",
|
|
311
|
+
"package",
|
|
312
|
+
"private",
|
|
313
|
+
"protected",
|
|
314
|
+
"public",
|
|
315
|
+
"return",
|
|
316
|
+
"short",
|
|
317
|
+
"static",
|
|
318
|
+
"strictfp",
|
|
319
|
+
"super",
|
|
320
|
+
"switch",
|
|
321
|
+
"synchronized",
|
|
322
|
+
"this",
|
|
323
|
+
"throw",
|
|
324
|
+
"throws",
|
|
325
|
+
"transient",
|
|
326
|
+
"true",
|
|
327
|
+
"try",
|
|
328
|
+
"var",
|
|
329
|
+
"void",
|
|
330
|
+
"volatile",
|
|
331
|
+
"while",
|
|
332
|
+
}
|
|
333
|
+
),
|
|
334
|
+
"Gosu": frozenset(
|
|
335
|
+
{
|
|
336
|
+
"abstract",
|
|
337
|
+
"as",
|
|
338
|
+
"assert",
|
|
339
|
+
"block",
|
|
340
|
+
"break",
|
|
341
|
+
"case",
|
|
342
|
+
"catch",
|
|
343
|
+
"class",
|
|
344
|
+
"classpath",
|
|
345
|
+
"continue",
|
|
346
|
+
"default",
|
|
347
|
+
"do",
|
|
348
|
+
"else",
|
|
349
|
+
"enum",
|
|
350
|
+
"erases",
|
|
351
|
+
"eval",
|
|
352
|
+
"exists",
|
|
353
|
+
"extends",
|
|
354
|
+
"false",
|
|
355
|
+
"final",
|
|
356
|
+
"finally",
|
|
357
|
+
"for",
|
|
358
|
+
"foreach",
|
|
359
|
+
"function",
|
|
360
|
+
"hiding",
|
|
361
|
+
"if",
|
|
362
|
+
"implements",
|
|
363
|
+
"import",
|
|
364
|
+
"in",
|
|
365
|
+
"index",
|
|
366
|
+
"interface",
|
|
367
|
+
"new",
|
|
368
|
+
"null",
|
|
369
|
+
"override",
|
|
370
|
+
"package",
|
|
371
|
+
"property",
|
|
372
|
+
"protected",
|
|
373
|
+
"public",
|
|
374
|
+
"readonly",
|
|
375
|
+
"return",
|
|
376
|
+
"static",
|
|
377
|
+
"super",
|
|
378
|
+
"switch",
|
|
379
|
+
"this",
|
|
380
|
+
"throw",
|
|
381
|
+
"throws",
|
|
382
|
+
"transient",
|
|
383
|
+
"true",
|
|
384
|
+
"try",
|
|
385
|
+
"unless",
|
|
386
|
+
"using",
|
|
387
|
+
"var",
|
|
388
|
+
"void",
|
|
389
|
+
"where",
|
|
390
|
+
"while",
|
|
391
|
+
}
|
|
392
|
+
),
|
|
393
|
+
"Go": frozenset(
|
|
394
|
+
{
|
|
395
|
+
"break",
|
|
396
|
+
"case",
|
|
397
|
+
"chan",
|
|
398
|
+
"const",
|
|
399
|
+
"continue",
|
|
400
|
+
"default",
|
|
401
|
+
"defer",
|
|
402
|
+
"else",
|
|
403
|
+
"fallthrough",
|
|
404
|
+
"false",
|
|
405
|
+
"for",
|
|
406
|
+
"func",
|
|
407
|
+
"go",
|
|
408
|
+
"goto",
|
|
409
|
+
"if",
|
|
410
|
+
"import",
|
|
411
|
+
"interface",
|
|
412
|
+
"map",
|
|
413
|
+
"nil",
|
|
414
|
+
"package",
|
|
415
|
+
"range",
|
|
416
|
+
"return",
|
|
417
|
+
"select",
|
|
418
|
+
"struct",
|
|
419
|
+
"switch",
|
|
420
|
+
"true",
|
|
421
|
+
"type",
|
|
422
|
+
"var",
|
|
423
|
+
}
|
|
424
|
+
),
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
# Patterns that indicate the token is a string/number literal
|
|
428
|
+
_STRING_RE: re.Pattern[str] = re.compile(r"""^["'`0-9]""")
|
|
429
|
+
|
|
430
|
+
|
|
431
|
+
def get_profile(language_name: str) -> NormalisationProfile:
|
|
432
|
+
"""Return the :class:`NormalisationProfile` for *language_name*, falling back to empty sets."""
|
|
433
|
+
return _PROFILES.get(language_name, _FALLBACK_PROFILE)
|
|
434
|
+
|
|
435
|
+
|
|
436
|
+
def _tokenise_text(text: str) -> list[str]:
|
|
437
|
+
"""Split *text* into a list of raw tokens using :data:`_TOKEN_RE`."""
|
|
438
|
+
return _TOKEN_RE.findall(text)
|
|
439
|
+
|
|
440
|
+
|
|
441
|
+
def _node_text(node: Node, source_bytes: bytes) -> str:
|
|
442
|
+
"""Return the UTF-8 decoded source text for the byte range covered by *node*."""
|
|
443
|
+
return source_bytes[node.start_byte : node.end_byte].decode(
|
|
444
|
+
"utf-8", errors="replace"
|
|
445
|
+
)
|
|
446
|
+
|
|
447
|
+
|
|
448
|
+
def tokenise_and_normalise(
|
|
449
|
+
text: str, language_name: str
|
|
450
|
+
) -> tuple[list[str], list[str]]:
|
|
451
|
+
"""Return ``(raw_tokens, normalised_tokens)`` for a fragment source *text* string.
|
|
452
|
+
|
|
453
|
+
Args:
|
|
454
|
+
text: Raw fragment source text (UTF-8 decoded).
|
|
455
|
+
language_name: Language whose keyword set governs normalisation.
|
|
456
|
+
|
|
457
|
+
Returns:
|
|
458
|
+
Raw token list and normalised token list where identifiers become
|
|
459
|
+
``ID_N`` and literals become ``LIT_N``.
|
|
460
|
+
"""
|
|
461
|
+
raw_tokens = _tokenise_text(text)
|
|
462
|
+
keywords = _KEYWORDS.get(language_name, frozenset())
|
|
463
|
+
return raw_tokens, _normalise(raw_tokens, keywords)
|
|
464
|
+
|
|
465
|
+
|
|
466
|
+
def extract_tokens(node: Node, source_bytes: bytes) -> list[str]:
|
|
467
|
+
"""Return a flat list of raw tokens for the source text covered by *node*.
|
|
468
|
+
|
|
469
|
+
Uses a regex-based tokeniser on the raw source slice — no tree traversal.
|
|
470
|
+
"""
|
|
471
|
+
return _tokenise_text(_node_text(node, source_bytes))
|
|
472
|
+
|
|
473
|
+
|
|
474
|
+
def extract_and_normalise(
|
|
475
|
+
node: Node, source_bytes: bytes, language_name: str
|
|
476
|
+
) -> tuple[list[str], list[str]]:
|
|
477
|
+
"""Return ``(raw_tokens, normalised_tokens)`` for the source covered by *node*.
|
|
478
|
+
|
|
479
|
+
Args:
|
|
480
|
+
node: Tree-sitter node; only its byte range is used (no child traversal).
|
|
481
|
+
source_bytes: Full source bytes of the file.
|
|
482
|
+
language_name: Language whose keyword set governs normalisation.
|
|
483
|
+
|
|
484
|
+
Returns:
|
|
485
|
+
Raw token list and normalised token list where identifiers become
|
|
486
|
+
``ID_N`` and literals become ``LIT_N``.
|
|
487
|
+
"""
|
|
488
|
+
return tokenise_and_normalise(_node_text(node, source_bytes), language_name)
|
|
489
|
+
|
|
490
|
+
|
|
491
|
+
def _normalise(raw_tokens: list[str], keywords: frozenset[str]) -> list[str]:
|
|
492
|
+
"""Build a normalised token list from *raw_tokens* using *keywords* for classification."""
|
|
493
|
+
norm_tokens: list[str] = []
|
|
494
|
+
id_map: dict[str, str] = {}
|
|
495
|
+
lit_count = 0
|
|
496
|
+
|
|
497
|
+
for token in raw_tokens:
|
|
498
|
+
if _is_comment(token):
|
|
499
|
+
continue # skip comments
|
|
500
|
+
if _is_identifier(token) and token not in keywords:
|
|
501
|
+
placeholder = id_map.setdefault(token, f"ID_{len(id_map)}")
|
|
502
|
+
norm_tokens.append(placeholder)
|
|
503
|
+
elif _STRING_RE.match(token):
|
|
504
|
+
norm_tokens.append(f"LIT_{lit_count}")
|
|
505
|
+
lit_count += 1
|
|
506
|
+
else:
|
|
507
|
+
norm_tokens.append(token)
|
|
508
|
+
|
|
509
|
+
return norm_tokens
|
|
510
|
+
|
|
511
|
+
|
|
512
|
+
def _is_identifier(token: str) -> bool:
|
|
513
|
+
"""Return True if *token* looks like an identifier (letter/underscore start)."""
|
|
514
|
+
return bool(token) and (token[0].isalpha() or token[0] == "_")
|
|
515
|
+
|
|
516
|
+
|
|
517
|
+
def _is_comment(token: str) -> bool:
|
|
518
|
+
"""Return True if *token* is a C-style single-line (``//``) or multi-line (``/* */``) comment."""
|
|
519
|
+
return token.startswith("//") or (token.startswith("/*") and token.endswith("*/"))
|
codeecho/parser.py
ADDED
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Tree-sitter parser wrapper providing one cached :class:`tree_sitter.Parser` per language.
|
|
3
|
+
|
|
4
|
+
Gosu (``.gs``) files are parsed using the Java grammar because Gosu is syntactically
|
|
5
|
+
Java-like (methods, classes, blocks).
|
|
6
|
+
|
|
7
|
+
:author: Ron Webb
|
|
8
|
+
:since: 1.0.0
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
import logging
|
|
12
|
+
from functools import lru_cache
|
|
13
|
+
|
|
14
|
+
from tree_sitter import Language, Parser, Tree
|
|
15
|
+
|
|
16
|
+
_logger = logging.getLogger("codeecho.parser")
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def _build_language(
|
|
20
|
+
language_name: str,
|
|
21
|
+
) -> Language | None: # pylint: disable=too-many-return-statements
|
|
22
|
+
"""Instantiate and return the tree-sitter :class:`Language` for *language_name*."""
|
|
23
|
+
match language_name:
|
|
24
|
+
case "Python":
|
|
25
|
+
import tree_sitter_python as m # pylint: disable=import-outside-toplevel
|
|
26
|
+
|
|
27
|
+
return Language(m.language())
|
|
28
|
+
case "JavaScript":
|
|
29
|
+
import tree_sitter_javascript as m # pylint: disable=import-outside-toplevel
|
|
30
|
+
|
|
31
|
+
return Language(m.language())
|
|
32
|
+
case "TypeScript" | "TSX":
|
|
33
|
+
import tree_sitter_typescript as m # pylint: disable=import-outside-toplevel
|
|
34
|
+
|
|
35
|
+
return Language(m.language_typescript())
|
|
36
|
+
case "Java" | "Gosu":
|
|
37
|
+
import tree_sitter_java as m # pylint: disable=import-outside-toplevel
|
|
38
|
+
|
|
39
|
+
return Language(m.language())
|
|
40
|
+
case "Go":
|
|
41
|
+
import tree_sitter_go as m # pylint: disable=import-outside-toplevel
|
|
42
|
+
|
|
43
|
+
return Language(m.language())
|
|
44
|
+
case _:
|
|
45
|
+
return None
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
@lru_cache(maxsize=None)
|
|
49
|
+
def get_language(language_name: str) -> Language | None:
|
|
50
|
+
"""Return a cached :class:`tree_sitter.Language` for *language_name*, or ``None`` on failure."""
|
|
51
|
+
try:
|
|
52
|
+
lang = _build_language(language_name)
|
|
53
|
+
except Exception as exc: # pylint: disable=broad-exception-caught
|
|
54
|
+
_logger.warning("Failed to load grammar for %s: %s", language_name, exc)
|
|
55
|
+
return None
|
|
56
|
+
if lang is None:
|
|
57
|
+
_logger.warning("No tree-sitter grammar for language: %s", language_name)
|
|
58
|
+
return lang
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
@lru_cache(maxsize=None)
|
|
62
|
+
def get_parser(language_name: str) -> Parser | None:
|
|
63
|
+
"""Return a cached :class:`tree_sitter.Parser` for *language_name*, or ``None`` on failure.
|
|
64
|
+
|
|
65
|
+
.. note::
|
|
66
|
+
The returned parser is NOT used directly for parsing; call :func:`parse` instead,
|
|
67
|
+
which creates a fresh :class:`~tree_sitter.Parser` instance per call to avoid
|
|
68
|
+
internal state corruption across files in tree-sitter 0.26+.
|
|
69
|
+
"""
|
|
70
|
+
lang = get_language(language_name)
|
|
71
|
+
if lang is None:
|
|
72
|
+
return None
|
|
73
|
+
return Parser(lang)
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def parse(source_bytes: bytes, language_name: str) -> Tree | None:
|
|
77
|
+
"""Parse *source_bytes* with the grammar for *language_name*.
|
|
78
|
+
|
|
79
|
+
A new :class:`~tree_sitter.Parser` is created for each call so that the
|
|
80
|
+
internal state of the C extension cannot carry over between files.
|
|
81
|
+
|
|
82
|
+
:param source_bytes: UTF-8-encoded source code.
|
|
83
|
+
:param language_name: Name as returned by :data:`codeecho.scanner.EXTENSION_TO_LANGUAGE`.
|
|
84
|
+
:returns: Parsed :class:`tree_sitter.Tree`, or ``None`` if the grammar is unavailable.
|
|
85
|
+
"""
|
|
86
|
+
lang = get_language(language_name)
|
|
87
|
+
if lang is None:
|
|
88
|
+
_logger.warning("Skipping parse — no grammar for %s", language_name)
|
|
89
|
+
return None
|
|
90
|
+
fresh_parser = Parser(lang)
|
|
91
|
+
tree = fresh_parser.parse(source_bytes)
|
|
92
|
+
if tree.root_node.has_error:
|
|
93
|
+
_logger.debug("Parse errors detected in %s source.", language_name)
|
|
94
|
+
return tree
|