@kernlang/review-python 3.1.6 → 3.1.8
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.
- package/dist/mapper.js +65 -18
- package/package.json +3 -3
- package/src/mapper.ts +80 -70
- package/tests/bilingual-v2.test.ts +13 -13
- package/tests/bilingual.test.ts +10 -10
package/dist/mapper.js
CHANGED
|
@@ -4,22 +4,68 @@
|
|
|
4
4
|
* Maps Python syntax → universal KERN concepts.
|
|
5
5
|
* Phase 1: error_raise, error_handle, effect
|
|
6
6
|
*/
|
|
7
|
+
import { conceptId, conceptSpan } from '@kernlang/core';
|
|
7
8
|
import Parser from 'tree-sitter';
|
|
8
9
|
import Python from 'tree-sitter-python';
|
|
9
|
-
import { conceptId, conceptSpan } from '@kernlang/core';
|
|
10
10
|
const EXTRACTOR_VERSION = '1.0.0';
|
|
11
11
|
// ── Network call patterns ────────────────────────────────────────────────
|
|
12
12
|
const NETWORK_MODULES = new Set(['requests', 'httpx', 'aiohttp', 'urllib']);
|
|
13
13
|
const NETWORK_METHODS = new Set(['get', 'post', 'put', 'patch', 'delete', 'head', 'options', 'request', 'fetch']);
|
|
14
14
|
const DB_MODULES = new Set(['psycopg2', 'asyncpg', 'pymongo', 'sqlalchemy', 'django']);
|
|
15
|
-
const DB_METHODS = new Set([
|
|
16
|
-
|
|
15
|
+
const DB_METHODS = new Set([
|
|
16
|
+
'execute',
|
|
17
|
+
'executemany',
|
|
18
|
+
'fetchone',
|
|
19
|
+
'fetchall',
|
|
20
|
+
'fetchmany',
|
|
21
|
+
'query',
|
|
22
|
+
'find',
|
|
23
|
+
'find_one',
|
|
24
|
+
'insert_one',
|
|
25
|
+
'insert_many',
|
|
26
|
+
'update_one',
|
|
27
|
+
'delete_one',
|
|
28
|
+
]);
|
|
29
|
+
const _FS_FUNCTIONS = new Set(['open', 'read', 'write', 'readlines', 'writelines']);
|
|
17
30
|
const STDLIB_MODULES = new Set([
|
|
18
|
-
'os',
|
|
19
|
-
'
|
|
20
|
-
'
|
|
21
|
-
'
|
|
22
|
-
'
|
|
31
|
+
'os',
|
|
32
|
+
'sys',
|
|
33
|
+
'json',
|
|
34
|
+
're',
|
|
35
|
+
'math',
|
|
36
|
+
'datetime',
|
|
37
|
+
'time',
|
|
38
|
+
'logging',
|
|
39
|
+
'argparse',
|
|
40
|
+
'collections',
|
|
41
|
+
'itertools',
|
|
42
|
+
'functools',
|
|
43
|
+
'pathlib',
|
|
44
|
+
'shutil',
|
|
45
|
+
'subprocess',
|
|
46
|
+
'threading',
|
|
47
|
+
'multiprocessing',
|
|
48
|
+
'abc',
|
|
49
|
+
'typing',
|
|
50
|
+
'io',
|
|
51
|
+
'pickle',
|
|
52
|
+
'random',
|
|
53
|
+
'hashlib',
|
|
54
|
+
'hmac',
|
|
55
|
+
'base64',
|
|
56
|
+
'csv',
|
|
57
|
+
'sqlite3',
|
|
58
|
+
'zlib',
|
|
59
|
+
'gzip',
|
|
60
|
+
'tarfile',
|
|
61
|
+
'zipfile',
|
|
62
|
+
'enum',
|
|
63
|
+
'struct',
|
|
64
|
+
'tempfile',
|
|
65
|
+
'unittest',
|
|
66
|
+
'urllib',
|
|
67
|
+
'uuid',
|
|
68
|
+
'xml',
|
|
23
69
|
]);
|
|
24
70
|
// ── Parser setup ─────────────────────────────────────────────────────────
|
|
25
71
|
let parser = null;
|
|
@@ -75,7 +121,7 @@ function extractErrorRaise(root, source, filePath, nodes) {
|
|
|
75
121
|
function extractErrorHandle(root, source, filePath, nodes) {
|
|
76
122
|
// except clauses
|
|
77
123
|
walkNodes(root, 'except_clause', (node) => {
|
|
78
|
-
const block = node.children.find(c => c.type === 'block');
|
|
124
|
+
const block = node.children.find((c) => c.type === 'block');
|
|
79
125
|
const disposition = classifyPythonDisposition(block, source);
|
|
80
126
|
const errorVar = extractExceptVar(node);
|
|
81
127
|
nodes.push({
|
|
@@ -161,7 +207,8 @@ function extractEffects(root, source, filePath, nodes) {
|
|
|
161
207
|
return;
|
|
162
208
|
}
|
|
163
209
|
// DB: cursor.execute(), db.query(), etc.
|
|
164
|
-
if (DB_METHODS.has(methodName) &&
|
|
210
|
+
if (DB_METHODS.has(methodName) &&
|
|
211
|
+
(DB_MODULES.has(objName) || /cursor|conn|db|session|collection/i.test(objName))) {
|
|
165
212
|
nodes.push({
|
|
166
213
|
id: conceptId(filePath, 'effect', node.startIndex),
|
|
167
214
|
kind: 'effect',
|
|
@@ -209,7 +256,7 @@ function extractEntrypoints(root, source, filePath, nodes) {
|
|
|
209
256
|
// 1. Route decorators: @app.route, @app.get, @router.post, etc.
|
|
210
257
|
// tree-sitter Python wraps decorated functions in 'decorated_definition'
|
|
211
258
|
walkNodes(root, 'decorated_definition', (node) => {
|
|
212
|
-
const fnDef = node.children.find(c => c.type === 'function_definition');
|
|
259
|
+
const fnDef = node.children.find((c) => c.type === 'function_definition');
|
|
213
260
|
if (!fnDef)
|
|
214
261
|
return;
|
|
215
262
|
for (const child of node.children) {
|
|
@@ -233,7 +280,7 @@ function extractEntrypoints(root, source, filePath, nodes) {
|
|
|
233
280
|
payload: {
|
|
234
281
|
kind: 'entrypoint',
|
|
235
282
|
subtype: 'route',
|
|
236
|
-
name: nameNode ? nameNode.text :
|
|
283
|
+
name: nameNode ? nameNode.text : pathMatch?.[1] || 'anonymous',
|
|
237
284
|
httpMethod: method === 'ROUTE' ? undefined : method,
|
|
238
285
|
},
|
|
239
286
|
});
|
|
@@ -243,7 +290,7 @@ function extractEntrypoints(root, source, filePath, nodes) {
|
|
|
243
290
|
// 2. if __name__ == '__main__':
|
|
244
291
|
walkNodes(root, 'if_statement', (node) => {
|
|
245
292
|
const condition = node.childForFieldName('condition');
|
|
246
|
-
if (condition
|
|
293
|
+
if (condition?.text.includes('__name__') && condition.text.includes('__main__')) {
|
|
247
294
|
nodes.push({
|
|
248
295
|
id: conceptId(filePath, 'entrypoint', node.startIndex),
|
|
249
296
|
kind: 'entrypoint',
|
|
@@ -289,7 +336,7 @@ function extractGuards(root, source, filePath, nodes) {
|
|
|
289
336
|
// 2. Pydantic validation: BaseModel.model_validate()
|
|
290
337
|
walkNodes(root, 'call', (node) => {
|
|
291
338
|
const func = node.childForFieldName('function');
|
|
292
|
-
if (func
|
|
339
|
+
if (func?.text.includes('model_validate')) {
|
|
293
340
|
nodes.push({
|
|
294
341
|
id: conceptId(filePath, 'guard', node.startIndex),
|
|
295
342
|
kind: 'guard',
|
|
@@ -306,7 +353,7 @@ function extractGuards(root, source, filePath, nodes) {
|
|
|
306
353
|
walkNodes(root, 'if_statement', (node) => {
|
|
307
354
|
const cond = node.childForFieldName('condition');
|
|
308
355
|
if (cond && /\b(user|auth|request\.user)\b/.test(cond.text)) {
|
|
309
|
-
const block = node.namedChildren.find(c => c.type === 'block');
|
|
356
|
+
const block = node.namedChildren.find((c) => c.type === 'block');
|
|
310
357
|
if (block) {
|
|
311
358
|
const firstStmt = block.namedChildren[0];
|
|
312
359
|
if (firstStmt && (firstStmt.type === 'return_statement' || firstStmt.type === 'raise_statement')) {
|
|
@@ -482,14 +529,14 @@ function getContainerId(node, filePath) {
|
|
|
482
529
|
}
|
|
483
530
|
function extractRaiseType(node) {
|
|
484
531
|
// raise ValueError("...") → "ValueError"
|
|
485
|
-
const callNode = node.namedChildren.find(c => c.type === 'call');
|
|
532
|
+
const callNode = node.namedChildren.find((c) => c.type === 'call');
|
|
486
533
|
if (callNode) {
|
|
487
534
|
const func = callNode.childForFieldName('function');
|
|
488
535
|
if (func)
|
|
489
536
|
return func.text;
|
|
490
537
|
}
|
|
491
538
|
// raise ValueError → just identifier
|
|
492
|
-
const ident = node.namedChildren.find(c => c.type === 'identifier');
|
|
539
|
+
const ident = node.namedChildren.find((c) => c.type === 'identifier');
|
|
493
540
|
if (ident)
|
|
494
541
|
return ident.text;
|
|
495
542
|
return undefined;
|
|
@@ -514,7 +561,7 @@ function isInAsyncDef(node) {
|
|
|
514
561
|
while (parent) {
|
|
515
562
|
if (parent.type === 'function_definition') {
|
|
516
563
|
// Check for 'async' keyword before 'def'
|
|
517
|
-
return parent.children.some(c => c.type === 'async');
|
|
564
|
+
return parent.children.some((c) => c.type === 'async');
|
|
518
565
|
}
|
|
519
566
|
parent = parent.parent;
|
|
520
567
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@kernlang/review-python",
|
|
3
|
-
"version": "3.1.
|
|
3
|
+
"version": "3.1.8",
|
|
4
4
|
"description": "Python concept mapper for kern review — tree-sitter based",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -8,8 +8,8 @@
|
|
|
8
8
|
"dependencies": {
|
|
9
9
|
"tree-sitter": "^0.25.0",
|
|
10
10
|
"tree-sitter-python": "^0.25.0",
|
|
11
|
-
"@kernlang/
|
|
12
|
-
"@kernlang/
|
|
11
|
+
"@kernlang/review": "3.1.8",
|
|
12
|
+
"@kernlang/core": "3.1.8"
|
|
13
13
|
},
|
|
14
14
|
"devDependencies": {
|
|
15
15
|
"ts-morph": "^27.0.0",
|
package/src/mapper.ts
CHANGED
|
@@ -5,13 +5,10 @@
|
|
|
5
5
|
* Phase 1: error_raise, error_handle, effect
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
+
import type { ConceptEdge, ConceptMap, ConceptNode, ConceptSpan, ErrorHandlePayload } from '@kernlang/core';
|
|
9
|
+
import { conceptId, conceptSpan } from '@kernlang/core';
|
|
8
10
|
import Parser from 'tree-sitter';
|
|
9
11
|
import Python from 'tree-sitter-python';
|
|
10
|
-
import type {
|
|
11
|
-
ConceptMap, ConceptNode, ConceptEdge, ConceptSpan,
|
|
12
|
-
ErrorHandlePayload, EntrypointPayload, GuardPayload, StateMutationPayload, DependencyPayload,
|
|
13
|
-
} from '@kernlang/core';
|
|
14
|
-
import { conceptId, conceptSpan } from '@kernlang/core';
|
|
15
12
|
|
|
16
13
|
const EXTRACTOR_VERSION = '1.0.0';
|
|
17
14
|
|
|
@@ -21,16 +18,62 @@ const NETWORK_MODULES = new Set(['requests', 'httpx', 'aiohttp', 'urllib']);
|
|
|
21
18
|
const NETWORK_METHODS = new Set(['get', 'post', 'put', 'patch', 'delete', 'head', 'options', 'request', 'fetch']);
|
|
22
19
|
|
|
23
20
|
const DB_MODULES = new Set(['psycopg2', 'asyncpg', 'pymongo', 'sqlalchemy', 'django']);
|
|
24
|
-
const DB_METHODS = new Set([
|
|
21
|
+
const DB_METHODS = new Set([
|
|
22
|
+
'execute',
|
|
23
|
+
'executemany',
|
|
24
|
+
'fetchone',
|
|
25
|
+
'fetchall',
|
|
26
|
+
'fetchmany',
|
|
27
|
+
'query',
|
|
28
|
+
'find',
|
|
29
|
+
'find_one',
|
|
30
|
+
'insert_one',
|
|
31
|
+
'insert_many',
|
|
32
|
+
'update_one',
|
|
33
|
+
'delete_one',
|
|
34
|
+
]);
|
|
25
35
|
|
|
26
|
-
const
|
|
36
|
+
const _FS_FUNCTIONS = new Set(['open', 'read', 'write', 'readlines', 'writelines']);
|
|
27
37
|
|
|
28
38
|
const STDLIB_MODULES = new Set([
|
|
29
|
-
'os',
|
|
30
|
-
'
|
|
31
|
-
'
|
|
32
|
-
'
|
|
33
|
-
'
|
|
39
|
+
'os',
|
|
40
|
+
'sys',
|
|
41
|
+
'json',
|
|
42
|
+
're',
|
|
43
|
+
'math',
|
|
44
|
+
'datetime',
|
|
45
|
+
'time',
|
|
46
|
+
'logging',
|
|
47
|
+
'argparse',
|
|
48
|
+
'collections',
|
|
49
|
+
'itertools',
|
|
50
|
+
'functools',
|
|
51
|
+
'pathlib',
|
|
52
|
+
'shutil',
|
|
53
|
+
'subprocess',
|
|
54
|
+
'threading',
|
|
55
|
+
'multiprocessing',
|
|
56
|
+
'abc',
|
|
57
|
+
'typing',
|
|
58
|
+
'io',
|
|
59
|
+
'pickle',
|
|
60
|
+
'random',
|
|
61
|
+
'hashlib',
|
|
62
|
+
'hmac',
|
|
63
|
+
'base64',
|
|
64
|
+
'csv',
|
|
65
|
+
'sqlite3',
|
|
66
|
+
'zlib',
|
|
67
|
+
'gzip',
|
|
68
|
+
'tarfile',
|
|
69
|
+
'zipfile',
|
|
70
|
+
'enum',
|
|
71
|
+
'struct',
|
|
72
|
+
'tempfile',
|
|
73
|
+
'unittest',
|
|
74
|
+
'urllib',
|
|
75
|
+
'uuid',
|
|
76
|
+
'xml',
|
|
34
77
|
]);
|
|
35
78
|
|
|
36
79
|
// ── Parser setup ─────────────────────────────────────────────────────────
|
|
@@ -72,12 +115,7 @@ export function extractPythonConcepts(source: string, filePath: string): Concept
|
|
|
72
115
|
|
|
73
116
|
// ── error_raise ──────────────────────────────────────────────────────────
|
|
74
117
|
|
|
75
|
-
function extractErrorRaise(
|
|
76
|
-
root: Parser.SyntaxNode,
|
|
77
|
-
source: string,
|
|
78
|
-
filePath: string,
|
|
79
|
-
nodes: ConceptNode[],
|
|
80
|
-
): void {
|
|
118
|
+
function extractErrorRaise(root: Parser.SyntaxNode, source: string, filePath: string, nodes: ConceptNode[]): void {
|
|
81
119
|
// raise statements
|
|
82
120
|
walkNodes(root, 'raise_statement', (node) => {
|
|
83
121
|
const errorType = extractRaiseType(node);
|
|
@@ -100,15 +138,10 @@ function extractErrorRaise(
|
|
|
100
138
|
|
|
101
139
|
// ── error_handle ─────────────────────────────────────────────────────────
|
|
102
140
|
|
|
103
|
-
function extractErrorHandle(
|
|
104
|
-
root: Parser.SyntaxNode,
|
|
105
|
-
source: string,
|
|
106
|
-
filePath: string,
|
|
107
|
-
nodes: ConceptNode[],
|
|
108
|
-
): void {
|
|
141
|
+
function extractErrorHandle(root: Parser.SyntaxNode, source: string, filePath: string, nodes: ConceptNode[]): void {
|
|
109
142
|
// except clauses
|
|
110
143
|
walkNodes(root, 'except_clause', (node) => {
|
|
111
|
-
const block = node.children.find(c => c.type === 'block');
|
|
144
|
+
const block = node.children.find((c) => c.type === 'block');
|
|
112
145
|
const disposition = classifyPythonDisposition(block, source);
|
|
113
146
|
const errorVar = extractExceptVar(node);
|
|
114
147
|
|
|
@@ -180,12 +213,7 @@ function classifyPythonDisposition(
|
|
|
180
213
|
|
|
181
214
|
// ── effect ───────────────────────────────────────────────────────────────
|
|
182
215
|
|
|
183
|
-
function extractEffects(
|
|
184
|
-
root: Parser.SyntaxNode,
|
|
185
|
-
source: string,
|
|
186
|
-
filePath: string,
|
|
187
|
-
nodes: ConceptNode[],
|
|
188
|
-
): void {
|
|
216
|
+
function extractEffects(root: Parser.SyntaxNode, source: string, filePath: string, nodes: ConceptNode[]): void {
|
|
189
217
|
walkNodes(root, 'call', (node) => {
|
|
190
218
|
const funcNode = node.childForFieldName('function');
|
|
191
219
|
if (!funcNode) return;
|
|
@@ -215,7 +243,10 @@ function extractEffects(
|
|
|
215
243
|
}
|
|
216
244
|
|
|
217
245
|
// DB: cursor.execute(), db.query(), etc.
|
|
218
|
-
if (
|
|
246
|
+
if (
|
|
247
|
+
DB_METHODS.has(methodName) &&
|
|
248
|
+
(DB_MODULES.has(objName) || /cursor|conn|db|session|collection/i.test(objName))
|
|
249
|
+
) {
|
|
219
250
|
nodes.push({
|
|
220
251
|
id: conceptId(filePath, 'effect', node.startIndex),
|
|
221
252
|
kind: 'effect',
|
|
@@ -263,16 +294,11 @@ function extractEffects(
|
|
|
263
294
|
|
|
264
295
|
// ── entrypoint ──────────────────────────────────────────────────────────
|
|
265
296
|
|
|
266
|
-
function extractEntrypoints(
|
|
267
|
-
root: Parser.SyntaxNode,
|
|
268
|
-
source: string,
|
|
269
|
-
filePath: string,
|
|
270
|
-
nodes: ConceptNode[],
|
|
271
|
-
): void {
|
|
297
|
+
function extractEntrypoints(root: Parser.SyntaxNode, source: string, filePath: string, nodes: ConceptNode[]): void {
|
|
272
298
|
// 1. Route decorators: @app.route, @app.get, @router.post, etc.
|
|
273
299
|
// tree-sitter Python wraps decorated functions in 'decorated_definition'
|
|
274
300
|
walkNodes(root, 'decorated_definition', (node) => {
|
|
275
|
-
const fnDef = node.children.find(c => c.type === 'function_definition');
|
|
301
|
+
const fnDef = node.children.find((c) => c.type === 'function_definition');
|
|
276
302
|
if (!fnDef) return;
|
|
277
303
|
|
|
278
304
|
for (const child of node.children) {
|
|
@@ -297,7 +323,7 @@ function extractEntrypoints(
|
|
|
297
323
|
payload: {
|
|
298
324
|
kind: 'entrypoint',
|
|
299
325
|
subtype: 'route',
|
|
300
|
-
name: nameNode ? nameNode.text :
|
|
326
|
+
name: nameNode ? nameNode.text : pathMatch?.[1] || 'anonymous',
|
|
301
327
|
httpMethod: method === 'ROUTE' ? undefined : method,
|
|
302
328
|
},
|
|
303
329
|
});
|
|
@@ -308,7 +334,7 @@ function extractEntrypoints(
|
|
|
308
334
|
// 2. if __name__ == '__main__':
|
|
309
335
|
walkNodes(root, 'if_statement', (node) => {
|
|
310
336
|
const condition = node.childForFieldName('condition');
|
|
311
|
-
if (condition
|
|
337
|
+
if (condition?.text.includes('__name__') && condition.text.includes('__main__')) {
|
|
312
338
|
nodes.push({
|
|
313
339
|
id: conceptId(filePath, 'entrypoint', node.startIndex),
|
|
314
340
|
kind: 'entrypoint',
|
|
@@ -328,12 +354,7 @@ function extractEntrypoints(
|
|
|
328
354
|
|
|
329
355
|
// ── guard ───────────────────────────────────────────────────────────────
|
|
330
356
|
|
|
331
|
-
function extractGuards(
|
|
332
|
-
root: Parser.SyntaxNode,
|
|
333
|
-
source: string,
|
|
334
|
-
filePath: string,
|
|
335
|
-
nodes: ConceptNode[],
|
|
336
|
-
): void {
|
|
357
|
+
function extractGuards(root: Parser.SyntaxNode, source: string, filePath: string, nodes: ConceptNode[]): void {
|
|
337
358
|
// 1. Auth decorators (tree-sitter: decorated_definition → decorator + function_definition)
|
|
338
359
|
walkNodes(root, 'decorated_definition', (node) => {
|
|
339
360
|
for (const child of node.children) {
|
|
@@ -361,7 +382,7 @@ function extractGuards(
|
|
|
361
382
|
// 2. Pydantic validation: BaseModel.model_validate()
|
|
362
383
|
walkNodes(root, 'call', (node) => {
|
|
363
384
|
const func = node.childForFieldName('function');
|
|
364
|
-
if (func
|
|
385
|
+
if (func?.text.includes('model_validate')) {
|
|
365
386
|
nodes.push({
|
|
366
387
|
id: conceptId(filePath, 'guard', node.startIndex),
|
|
367
388
|
kind: 'guard',
|
|
@@ -379,7 +400,7 @@ function extractGuards(
|
|
|
379
400
|
walkNodes(root, 'if_statement', (node) => {
|
|
380
401
|
const cond = node.childForFieldName('condition');
|
|
381
402
|
if (cond && /\b(user|auth|request\.user)\b/.test(cond.text)) {
|
|
382
|
-
const block = node.namedChildren.find(c => c.type === 'block');
|
|
403
|
+
const block = node.namedChildren.find((c) => c.type === 'block');
|
|
383
404
|
if (block) {
|
|
384
405
|
const firstStmt = block.namedChildren[0];
|
|
385
406
|
if (firstStmt && (firstStmt.type === 'return_statement' || firstStmt.type === 'raise_statement')) {
|
|
@@ -401,12 +422,7 @@ function extractGuards(
|
|
|
401
422
|
|
|
402
423
|
// ── state_mutation ───────────────────────────────────────────────────────
|
|
403
424
|
|
|
404
|
-
function extractStateMutation(
|
|
405
|
-
root: Parser.SyntaxNode,
|
|
406
|
-
source: string,
|
|
407
|
-
filePath: string,
|
|
408
|
-
nodes: ConceptNode[],
|
|
409
|
-
): void {
|
|
425
|
+
function extractStateMutation(root: Parser.SyntaxNode, source: string, filePath: string, nodes: ConceptNode[]): void {
|
|
410
426
|
// Track global keyword usage
|
|
411
427
|
const globalVarsInFile = new Set<string>();
|
|
412
428
|
walkNodes(root, 'global_statement', (node) => {
|
|
@@ -471,12 +487,7 @@ function extractStateMutation(
|
|
|
471
487
|
|
|
472
488
|
// ── dependency ──────────────────────────────────────────────────────────
|
|
473
489
|
|
|
474
|
-
function extractDependencyEdges(
|
|
475
|
-
root: Parser.SyntaxNode,
|
|
476
|
-
source: string,
|
|
477
|
-
filePath: string,
|
|
478
|
-
edges: ConceptEdge[],
|
|
479
|
-
): void {
|
|
490
|
+
function extractDependencyEdges(root: Parser.SyntaxNode, source: string, filePath: string, edges: ConceptEdge[]): void {
|
|
480
491
|
const addDependency = (node: Parser.SyntaxNode, specifier: string): void => {
|
|
481
492
|
let subtype: 'stdlib' | 'external' | 'internal' = 'external';
|
|
482
493
|
if (specifier.startsWith('.')) {
|
|
@@ -529,11 +540,7 @@ function extractDependencyEdges(
|
|
|
529
540
|
|
|
530
541
|
// ── Tree-sitter Helpers ──────────────────────────────────────────────────
|
|
531
542
|
|
|
532
|
-
function walkNodes(
|
|
533
|
-
root: Parser.SyntaxNode,
|
|
534
|
-
type: string,
|
|
535
|
-
callback: (node: Parser.SyntaxNode) => void,
|
|
536
|
-
): void {
|
|
543
|
+
function walkNodes(root: Parser.SyntaxNode, type: string, callback: (node: Parser.SyntaxNode) => void): void {
|
|
537
544
|
const cursor = root.walk();
|
|
538
545
|
let reachedRoot = false;
|
|
539
546
|
while (true) {
|
|
@@ -543,7 +550,10 @@ function walkNodes(
|
|
|
543
550
|
if (cursor.gotoFirstChild()) continue;
|
|
544
551
|
if (cursor.gotoNextSibling()) continue;
|
|
545
552
|
while (true) {
|
|
546
|
-
if (!cursor.gotoParent()) {
|
|
553
|
+
if (!cursor.gotoParent()) {
|
|
554
|
+
reachedRoot = true;
|
|
555
|
+
break;
|
|
556
|
+
}
|
|
547
557
|
if (cursor.gotoNextSibling()) break;
|
|
548
558
|
}
|
|
549
559
|
if (reachedRoot) break;
|
|
@@ -579,13 +589,13 @@ function getContainerId(node: Parser.SyntaxNode, filePath: string): string | und
|
|
|
579
589
|
|
|
580
590
|
function extractRaiseType(node: Parser.SyntaxNode): string | undefined {
|
|
581
591
|
// raise ValueError("...") → "ValueError"
|
|
582
|
-
const callNode = node.namedChildren.find(c => c.type === 'call');
|
|
592
|
+
const callNode = node.namedChildren.find((c) => c.type === 'call');
|
|
583
593
|
if (callNode) {
|
|
584
594
|
const func = callNode.childForFieldName('function');
|
|
585
595
|
if (func) return func.text;
|
|
586
596
|
}
|
|
587
597
|
// raise ValueError → just identifier
|
|
588
|
-
const ident = node.namedChildren.find(c => c.type === 'identifier');
|
|
598
|
+
const ident = node.namedChildren.find((c) => c.type === 'identifier');
|
|
589
599
|
if (ident) return ident.text;
|
|
590
600
|
return undefined;
|
|
591
601
|
}
|
|
@@ -610,7 +620,7 @@ function isInAsyncDef(node: Parser.SyntaxNode): boolean {
|
|
|
610
620
|
while (parent) {
|
|
611
621
|
if (parent.type === 'function_definition') {
|
|
612
622
|
// Check for 'async' keyword before 'def'
|
|
613
|
-
return parent.children.some(c => c.type === 'async');
|
|
623
|
+
return parent.children.some((c) => c.type === 'async');
|
|
614
624
|
}
|
|
615
625
|
parent = parent.parent;
|
|
616
626
|
}
|
|
@@ -4,9 +4,9 @@
|
|
|
4
4
|
* Same concept, two languages, same shape.
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
-
import { extractPythonConcepts } from '../src/mapper.js';
|
|
8
7
|
import { extractTsConcepts } from '@kernlang/review';
|
|
9
8
|
import { Project } from 'ts-morph';
|
|
9
|
+
import { extractPythonConcepts } from '../src/mapper.js';
|
|
10
10
|
|
|
11
11
|
function tsSourceFile(source: string, filePath = 'test.ts') {
|
|
12
12
|
const project = new Project({ useInMemoryFileSystem: true, compilerOptions: { strict: true } });
|
|
@@ -19,7 +19,7 @@ describe('Bilingual: entrypoint', () => {
|
|
|
19
19
|
app.get('/users', (req, res) => { res.json([]); });
|
|
20
20
|
`);
|
|
21
21
|
const concepts = extractTsConcepts(sf, 'test.ts');
|
|
22
|
-
const ep = concepts.nodes.find(n => n.kind === 'entrypoint');
|
|
22
|
+
const ep = concepts.nodes.find((n) => n.kind === 'entrypoint');
|
|
23
23
|
expect(ep).toBeDefined();
|
|
24
24
|
expect(ep!.payload.kind).toBe('entrypoint');
|
|
25
25
|
if (ep!.payload.kind === 'entrypoint') {
|
|
@@ -37,7 +37,7 @@ def get_users():
|
|
|
37
37
|
return []
|
|
38
38
|
`;
|
|
39
39
|
const concepts = extractPythonConcepts(source, 'test.py');
|
|
40
|
-
const ep = concepts.nodes.find(n => n.kind === 'entrypoint');
|
|
40
|
+
const ep = concepts.nodes.find((n) => n.kind === 'entrypoint');
|
|
41
41
|
expect(ep).toBeDefined();
|
|
42
42
|
expect(ep!.payload.kind).toBe('entrypoint');
|
|
43
43
|
if (ep!.payload.kind === 'entrypoint') {
|
|
@@ -55,7 +55,7 @@ describe('Bilingual: guard', () => {
|
|
|
55
55
|
}
|
|
56
56
|
`);
|
|
57
57
|
const concepts = extractTsConcepts(sf, 'test.ts');
|
|
58
|
-
const guard = concepts.nodes.find(n => n.kind === 'guard');
|
|
58
|
+
const guard = concepts.nodes.find((n) => n.kind === 'guard');
|
|
59
59
|
expect(guard).toBeDefined();
|
|
60
60
|
expect(guard!.payload.kind).toBe('guard');
|
|
61
61
|
if (guard!.payload.kind === 'guard') {
|
|
@@ -72,7 +72,7 @@ def dashboard(request):
|
|
|
72
72
|
return render(request, 'dashboard.html')
|
|
73
73
|
`;
|
|
74
74
|
const concepts = extractPythonConcepts(source, 'test.py');
|
|
75
|
-
const guard = concepts.nodes.find(n => n.kind === 'guard');
|
|
75
|
+
const guard = concepts.nodes.find((n) => n.kind === 'guard');
|
|
76
76
|
expect(guard).toBeDefined();
|
|
77
77
|
expect(guard!.payload.kind).toBe('guard');
|
|
78
78
|
if (guard!.payload.kind === 'guard') {
|
|
@@ -90,7 +90,7 @@ describe('Bilingual: state_mutation', () => {
|
|
|
90
90
|
}
|
|
91
91
|
`);
|
|
92
92
|
const concepts = extractTsConcepts(sf, 'test.ts');
|
|
93
|
-
const mut = concepts.nodes.find(n => n.kind === 'state_mutation');
|
|
93
|
+
const mut = concepts.nodes.find((n) => n.kind === 'state_mutation');
|
|
94
94
|
expect(mut).toBeDefined();
|
|
95
95
|
if (mut!.payload.kind === 'state_mutation') {
|
|
96
96
|
expect(mut!.payload.scope).toBe('module');
|
|
@@ -106,7 +106,7 @@ class Counter:
|
|
|
106
106
|
self.count += 1
|
|
107
107
|
`;
|
|
108
108
|
const concepts = extractPythonConcepts(source, 'test.py');
|
|
109
|
-
const mut = concepts.nodes.find(n => n.kind === 'state_mutation');
|
|
109
|
+
const mut = concepts.nodes.find((n) => n.kind === 'state_mutation');
|
|
110
110
|
expect(mut).toBeDefined();
|
|
111
111
|
if (mut!.payload.kind === 'state_mutation') {
|
|
112
112
|
expect(mut!.payload.scope).toBe('module');
|
|
@@ -124,9 +124,9 @@ describe('Bilingual: dependency edges', () => {
|
|
|
124
124
|
const concepts = extractTsConcepts(sf, 'test.ts');
|
|
125
125
|
expect(concepts.edges.length).toBeGreaterThanOrEqual(3);
|
|
126
126
|
|
|
127
|
-
const external = concepts.edges.find(e => e.payload.kind === 'dependency' && e.payload.subtype === 'external');
|
|
128
|
-
const stdlib = concepts.edges.find(e => e.payload.kind === 'dependency' && e.payload.subtype === 'stdlib');
|
|
129
|
-
const internal = concepts.edges.find(e => e.payload.kind === 'dependency' && e.payload.subtype === 'internal');
|
|
127
|
+
const external = concepts.edges.find((e) => e.payload.kind === 'dependency' && e.payload.subtype === 'external');
|
|
128
|
+
const stdlib = concepts.edges.find((e) => e.payload.kind === 'dependency' && e.payload.subtype === 'stdlib');
|
|
129
|
+
const internal = concepts.edges.find((e) => e.payload.kind === 'dependency' && e.payload.subtype === 'internal');
|
|
130
130
|
|
|
131
131
|
expect(external).toBeDefined();
|
|
132
132
|
expect(stdlib).toBeDefined();
|
|
@@ -142,9 +142,9 @@ from .utils import helper
|
|
|
142
142
|
const concepts = extractPythonConcepts(source, 'test.py');
|
|
143
143
|
expect(concepts.edges.length).toBeGreaterThanOrEqual(3);
|
|
144
144
|
|
|
145
|
-
const external = concepts.edges.find(e => e.payload.kind === 'dependency' && e.payload.subtype === 'external');
|
|
146
|
-
const stdlib = concepts.edges.find(e => e.payload.kind === 'dependency' && e.payload.subtype === 'stdlib');
|
|
147
|
-
const internal = concepts.edges.find(e => e.payload.kind === 'dependency' && e.payload.subtype === 'internal');
|
|
145
|
+
const external = concepts.edges.find((e) => e.payload.kind === 'dependency' && e.payload.subtype === 'external');
|
|
146
|
+
const stdlib = concepts.edges.find((e) => e.payload.kind === 'dependency' && e.payload.subtype === 'stdlib');
|
|
147
|
+
const internal = concepts.edges.find((e) => e.payload.kind === 'dependency' && e.payload.subtype === 'internal');
|
|
148
148
|
|
|
149
149
|
expect(external).toBeDefined();
|
|
150
150
|
expect(stdlib).toBeDefined();
|
package/tests/bilingual.test.ts
CHANGED
|
@@ -4,9 +4,9 @@
|
|
|
4
4
|
* This is the proof that KERN concepts are universal.
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
-
import { extractPythonConcepts } from '../src/mapper.js';
|
|
8
7
|
import { extractTsConcepts, runConceptRules } from '@kernlang/review';
|
|
9
8
|
import { Project } from 'ts-morph';
|
|
9
|
+
import { extractPythonConcepts } from '../src/mapper.js';
|
|
10
10
|
|
|
11
11
|
function tsSourceFile(source: string, filePath = 'test.ts') {
|
|
12
12
|
const project = new Project({ useInMemoryFileSystem: true, compilerOptions: { strict: true } });
|
|
@@ -18,7 +18,7 @@ describe('Bilingual: ignored-error', () => {
|
|
|
18
18
|
const sf = tsSourceFile('try { doWork(); } catch (e) {}');
|
|
19
19
|
const concepts = extractTsConcepts(sf, 'test.ts');
|
|
20
20
|
const findings = runConceptRules(concepts, 'test.ts');
|
|
21
|
-
const f = findings.find(f => f.ruleId === 'ignored-error');
|
|
21
|
+
const f = findings.find((f) => f.ruleId === 'ignored-error');
|
|
22
22
|
expect(f).toBeDefined();
|
|
23
23
|
expect(f!.severity).toBe('error');
|
|
24
24
|
});
|
|
@@ -32,7 +32,7 @@ except:
|
|
|
32
32
|
`;
|
|
33
33
|
const concepts = extractPythonConcepts(source, 'test.py');
|
|
34
34
|
const findings = runConceptRules(concepts, 'test.py');
|
|
35
|
-
const f = findings.find(f => f.ruleId === 'ignored-error');
|
|
35
|
+
const f = findings.find((f) => f.ruleId === 'ignored-error');
|
|
36
36
|
expect(f).toBeDefined();
|
|
37
37
|
expect(f!.severity).toBe('error');
|
|
38
38
|
});
|
|
@@ -46,7 +46,7 @@ except Exception as e:
|
|
|
46
46
|
`;
|
|
47
47
|
const concepts = extractPythonConcepts(source, 'test.py');
|
|
48
48
|
const findings = runConceptRules(concepts, 'test.py');
|
|
49
|
-
const f = findings.find(f => f.ruleId === 'ignored-error');
|
|
49
|
+
const f = findings.find((f) => f.ruleId === 'ignored-error');
|
|
50
50
|
expect(f).toBeDefined();
|
|
51
51
|
});
|
|
52
52
|
|
|
@@ -54,7 +54,7 @@ except Exception as e:
|
|
|
54
54
|
const sf = tsSourceFile('try { doWork(); } catch (e) { throw new AppError(e); }');
|
|
55
55
|
const concepts = extractTsConcepts(sf, 'test.ts');
|
|
56
56
|
const findings = runConceptRules(concepts, 'test.ts');
|
|
57
|
-
const f = findings.find(f => f.ruleId === 'ignored-error');
|
|
57
|
+
const f = findings.find((f) => f.ruleId === 'ignored-error');
|
|
58
58
|
expect(f).toBeUndefined();
|
|
59
59
|
});
|
|
60
60
|
|
|
@@ -67,7 +67,7 @@ except Exception as e:
|
|
|
67
67
|
`;
|
|
68
68
|
const concepts = extractPythonConcepts(source, 'test.py');
|
|
69
69
|
const findings = runConceptRules(concepts, 'test.py');
|
|
70
|
-
const f = findings.find(f => f.ruleId === 'ignored-error');
|
|
70
|
+
const f = findings.find((f) => f.ruleId === 'ignored-error');
|
|
71
71
|
expect(f).toBeUndefined();
|
|
72
72
|
});
|
|
73
73
|
});
|
|
@@ -79,8 +79,8 @@ describe('Bilingual: concept parity', () => {
|
|
|
79
79
|
|
|
80
80
|
const pyConcepts = extractPythonConcepts('def fail():\n raise ValueError("boom")', 'test.py');
|
|
81
81
|
|
|
82
|
-
const tsRaise = tsConcepts.nodes.find(n => n.kind === 'error_raise');
|
|
83
|
-
const pyRaise = pyConcepts.nodes.find(n => n.kind === 'error_raise');
|
|
82
|
+
const tsRaise = tsConcepts.nodes.find((n) => n.kind === 'error_raise');
|
|
83
|
+
const pyRaise = pyConcepts.nodes.find((n) => n.kind === 'error_raise');
|
|
84
84
|
|
|
85
85
|
expect(tsRaise).toBeDefined();
|
|
86
86
|
expect(pyRaise).toBeDefined();
|
|
@@ -98,8 +98,8 @@ describe('Bilingual: concept parity', () => {
|
|
|
98
98
|
|
|
99
99
|
const pyConcepts = extractPythonConcepts('def get_data():\n requests.get("/api")', 'test.py');
|
|
100
100
|
|
|
101
|
-
const tsEffect = tsConcepts.nodes.find(n => n.kind === 'effect');
|
|
102
|
-
const pyEffect = pyConcepts.nodes.find(n => n.kind === 'effect');
|
|
101
|
+
const tsEffect = tsConcepts.nodes.find((n) => n.kind === 'effect');
|
|
102
|
+
const pyEffect = pyConcepts.nodes.find((n) => n.kind === 'effect');
|
|
103
103
|
|
|
104
104
|
expect(tsEffect).toBeDefined();
|
|
105
105
|
expect(pyEffect).toBeDefined();
|