@generaltranslation/python-extractor 0.0.0

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.
@@ -0,0 +1,607 @@
1
+ import { PYTHON_DECLARE_STATIC, PYTHON_DECLARE_VAR } from './constants.js';
2
+ import { resolveFunctionInCurrentFile, resolveFunctionInFile, } from './resolveFunctionVariants.js';
3
+ import { resolveImportPath } from './resolveImport.js';
4
+ import { declareVar } from 'generaltranslation/internal';
5
+ /**
6
+ * Checks if an expression contains declare_static or declare_var calls.
7
+ */
8
+ export function containsStaticCalls(node, imports) {
9
+ const staticNames = getStaticImportNames(imports);
10
+ if (staticNames.size === 0)
11
+ return false;
12
+ return hasStaticCallRecursive(node, staticNames);
13
+ }
14
+ function hasStaticCallRecursive(node, names) {
15
+ if (node.type === 'call') {
16
+ const funcNode = node.childForFieldName('function');
17
+ if (funcNode &&
18
+ funcNode.type === 'identifier' &&
19
+ names.has(funcNode.text)) {
20
+ return true;
21
+ }
22
+ }
23
+ for (let i = 0; i < node.childCount; i++) {
24
+ const child = node.child(i);
25
+ if (child && hasStaticCallRecursive(child, names))
26
+ return true;
27
+ }
28
+ return false;
29
+ }
30
+ /**
31
+ * Parses the first argument of t() into a StringNode tree.
32
+ * Handles: plain strings, f-strings with declare_static/declare_var,
33
+ * binary + concatenation, and standalone declare_static calls.
34
+ */
35
+ export async function parseStringExpression(node, ctx) {
36
+ // Plain string (no f-string)
37
+ if (node.type === 'string' && !isFString(node)) {
38
+ const content = extractStringContent(node);
39
+ if (content === undefined)
40
+ return null;
41
+ return { type: 'text', text: content };
42
+ }
43
+ // F-string with interpolations
44
+ if (node.type === 'string' && isFString(node)) {
45
+ return parseFString(node, ctx);
46
+ }
47
+ // Binary operator: string concatenation with +
48
+ if (node.type === 'binary_operator') {
49
+ return parseBinaryOperator(node, ctx);
50
+ }
51
+ // Standalone call: declare_static(...)
52
+ if (node.type === 'call') {
53
+ const funcNode = node.childForFieldName('function');
54
+ if (funcNode && funcNode.type === 'identifier') {
55
+ const originalName = getOriginalImportName(funcNode.text, ctx.imports);
56
+ if (originalName === PYTHON_DECLARE_STATIC) {
57
+ return resolveDeclareStaticArg(node, ctx);
58
+ }
59
+ if (originalName === PYTHON_DECLARE_VAR) {
60
+ return resolveDeclareVarArg(node, ctx);
61
+ }
62
+ }
63
+ }
64
+ ctx.errors.push(`${locationStr(node)}: unsupported expression type "${node.type}" in translation call`);
65
+ return null;
66
+ }
67
+ /**
68
+ * Parses an f-string into a StringNode tree.
69
+ * string_content → text nodes, interpolation → check for declare_static/declare_var
70
+ */
71
+ async function parseFString(node, ctx) {
72
+ const parts = [];
73
+ for (let i = 0; i < node.childCount; i++) {
74
+ const child = node.child(i);
75
+ if (!child)
76
+ continue;
77
+ if (child.type === 'string_content') {
78
+ if (child.text.length > 0) {
79
+ parts.push({ type: 'text', text: child.text });
80
+ }
81
+ continue;
82
+ }
83
+ if (child.type === 'interpolation') {
84
+ const result = await parseInterpolation(child, ctx);
85
+ if (result) {
86
+ parts.push(result);
87
+ }
88
+ continue;
89
+ }
90
+ // Skip string_start, string_end, etc.
91
+ }
92
+ if (parts.length === 0)
93
+ return { type: 'text', text: '' };
94
+ if (parts.length === 1)
95
+ return parts[0];
96
+ return { type: 'sequence', nodes: parts };
97
+ }
98
+ /**
99
+ * Parses an interpolation within an f-string.
100
+ * Must be a declare_static() or declare_var() call.
101
+ */
102
+ async function parseInterpolation(interpNode, ctx) {
103
+ // Find the expression inside the interpolation (skip { and })
104
+ let expr = null;
105
+ for (let i = 0; i < interpNode.childCount; i++) {
106
+ const child = interpNode.child(i);
107
+ if (child &&
108
+ child.type !== '{' &&
109
+ child.type !== '}' &&
110
+ child.type !== 'type_conversion' &&
111
+ child.type !== 'format_specifier') {
112
+ expr = child;
113
+ break;
114
+ }
115
+ }
116
+ if (!expr) {
117
+ ctx.errors.push(`${locationStr(interpNode)}: empty interpolation in f-string`);
118
+ return null;
119
+ }
120
+ if (expr.type === 'call') {
121
+ const funcNode = expr.childForFieldName('function');
122
+ if (funcNode && funcNode.type === 'identifier') {
123
+ const originalName = getOriginalImportName(funcNode.text, ctx.imports);
124
+ if (originalName === PYTHON_DECLARE_STATIC) {
125
+ return resolveDeclareStaticArg(expr, ctx);
126
+ }
127
+ if (originalName === PYTHON_DECLARE_VAR) {
128
+ return resolveDeclareVarArg(expr, ctx);
129
+ }
130
+ }
131
+ }
132
+ // Not a declare_static/declare_var call — error
133
+ ctx.errors.push(`${locationStr(interpNode)}: f-string interpolation must use declare_static() or declare_var(), got "${expr.text}"`);
134
+ return null;
135
+ }
136
+ /**
137
+ * Parses binary + concatenation into a sequence node.
138
+ */
139
+ async function parseBinaryOperator(node, ctx) {
140
+ const left = node.childForFieldName('left');
141
+ const operator = node.childForFieldName('operator');
142
+ const right = node.childForFieldName('right');
143
+ if (!left || !right) {
144
+ ctx.errors.push(`${locationStr(node)}: binary operator missing operands`);
145
+ return null;
146
+ }
147
+ // Verify it's a + operator
148
+ if (operator && operator.text !== '+') {
149
+ ctx.errors.push(`${locationStr(node)}: unsupported binary operator "${operator.text}" in translation call`);
150
+ return null;
151
+ }
152
+ const leftNode = await parseStringExpression(left, ctx);
153
+ const rightNode = await parseStringExpression(right, ctx);
154
+ if (!leftNode || !rightNode)
155
+ return null;
156
+ // Flatten nested sequences
157
+ const parts = [];
158
+ if (leftNode.type === 'sequence') {
159
+ parts.push(...leftNode.nodes);
160
+ }
161
+ else {
162
+ parts.push(leftNode);
163
+ }
164
+ if (rightNode.type === 'sequence') {
165
+ parts.push(...rightNode.nodes);
166
+ }
167
+ else {
168
+ parts.push(rightNode);
169
+ }
170
+ return { type: 'sequence', nodes: parts };
171
+ }
172
+ /**
173
+ * Resolves the argument of a declare_static() call into a StringNode.
174
+ * Handles: string literals, ternary expressions, function calls.
175
+ */
176
+ async function resolveDeclareStaticArg(callNode, ctx) {
177
+ const arg = getFirstPositionalArg(callNode);
178
+ if (!arg) {
179
+ ctx.errors.push(`${locationStr(callNode)}: declare_static() requires an argument`);
180
+ return null;
181
+ }
182
+ return resolveStaticValue(arg, ctx);
183
+ }
184
+ /**
185
+ * Resolves a value expression that should produce string variants.
186
+ * Handles: string literals, ternary, function calls, binary concat,
187
+ * and declare_var() calls (nested inside declare_static).
188
+ */
189
+ async function resolveStaticValue(node, ctx) {
190
+ // Parenthesized expression: unwrap and recurse
191
+ if (node.type === 'parenthesized_expression') {
192
+ for (let i = 0; i < node.childCount; i++) {
193
+ const child = node.child(i);
194
+ if (child && child.type !== '(' && child.type !== ')') {
195
+ return resolveStaticValue(child, ctx);
196
+ }
197
+ }
198
+ return null;
199
+ }
200
+ // String literal
201
+ if (node.type === 'string' && !isFString(node)) {
202
+ const content = extractStringContent(node);
203
+ if (content === undefined)
204
+ return null;
205
+ return { type: 'text', text: content };
206
+ }
207
+ // Ternary / conditional expression: "day" if cond else "night"
208
+ if (node.type === 'conditional_expression') {
209
+ return resolveConditional(node, ctx);
210
+ }
211
+ // Binary operator: string concatenation with +
212
+ if (node.type === 'binary_operator') {
213
+ return resolveStaticBinaryOperator(node, ctx);
214
+ }
215
+ // Function call — could be a user function or declare_var()
216
+ if (node.type === 'call') {
217
+ const funcNode = node.childForFieldName('function');
218
+ if (funcNode && funcNode.type === 'identifier') {
219
+ const originalName = getOriginalImportName(funcNode.text, ctx.imports);
220
+ if (originalName === PYTHON_DECLARE_VAR) {
221
+ return resolveDeclareVarArg(node, ctx);
222
+ }
223
+ }
224
+ return resolveFunctionCall(node, ctx);
225
+ }
226
+ ctx.errors.push(`${locationStr(node)}: unsupported declare_static argument type "${node.type}"`);
227
+ return null;
228
+ }
229
+ /**
230
+ * Handles binary + concatenation within a static value context.
231
+ */
232
+ async function resolveStaticBinaryOperator(node, ctx) {
233
+ const left = node.childForFieldName('left');
234
+ const right = node.childForFieldName('right');
235
+ if (!left || !right) {
236
+ ctx.errors.push(`${locationStr(node)}: binary operator missing operands`);
237
+ return null;
238
+ }
239
+ // Check for + operator (tree-sitter may not expose operator as a named field)
240
+ // Look for a non-expression child that is '+'
241
+ let isPlus = true;
242
+ for (let i = 0; i < node.childCount; i++) {
243
+ const child = node.child(i);
244
+ if (child &&
245
+ child.type !== 'identifier' &&
246
+ child.type !== 'string' &&
247
+ child.type !== 'call' &&
248
+ child.type !== 'binary_operator' &&
249
+ child.type !== 'conditional_expression' &&
250
+ child.type !== 'parenthesized_expression') {
251
+ if (child.text !== '+') {
252
+ isPlus = false;
253
+ }
254
+ }
255
+ }
256
+ if (!isPlus) {
257
+ ctx.errors.push(`${locationStr(node)}: unsupported binary operator in static expression`);
258
+ return null;
259
+ }
260
+ const leftNode = await resolveStaticValue(left, ctx);
261
+ const rightNode = await resolveStaticValue(right, ctx);
262
+ if (!leftNode || !rightNode)
263
+ return null;
264
+ // Flatten nested sequences
265
+ const parts = [];
266
+ if (leftNode.type === 'sequence') {
267
+ parts.push(...leftNode.nodes);
268
+ }
269
+ else {
270
+ parts.push(leftNode);
271
+ }
272
+ if (rightNode.type === 'sequence') {
273
+ parts.push(...rightNode.nodes);
274
+ }
275
+ else {
276
+ parts.push(rightNode);
277
+ }
278
+ return { type: 'sequence', nodes: parts };
279
+ }
280
+ /**
281
+ * Resolves a Python conditional expression (ternary):
282
+ * "day" if cond else "night"
283
+ * tree-sitter: conditional_expression → [consequent, if, condition, else, alternate]
284
+ */
285
+ async function resolveConditional(node, ctx) {
286
+ // In Python's tree-sitter, the conditional_expression fields are:
287
+ // body = consequent (the value if true)
288
+ // condition = the test
289
+ // alternative = the else value (named 'alternative' field)
290
+ // But field names vary by tree-sitter version. Let's use positional children.
291
+ // Structure: consequent "if" condition "else" alternative
292
+ // The tree-sitter Python grammar uses named children:
293
+ // body (first expression), if keyword, condition, else keyword, alternative
294
+ // But let's find them by field name first, then fall back to positional.
295
+ // Try using children directly: first non-keyword child is consequent,
296
+ // child after "else" keyword is alternate
297
+ let consequent = null;
298
+ let alternate = null;
299
+ let seenElse = false;
300
+ for (let i = 0; i < node.childCount; i++) {
301
+ const child = node.child(i);
302
+ if (!child)
303
+ continue;
304
+ if (child.type === 'if')
305
+ continue;
306
+ if (child.type === 'else') {
307
+ seenElse = true;
308
+ continue;
309
+ }
310
+ if (!seenElse && !consequent) {
311
+ consequent = child;
312
+ }
313
+ else if (seenElse && !alternate) {
314
+ alternate = child;
315
+ }
316
+ }
317
+ if (!consequent || !alternate) {
318
+ ctx.errors.push(`${locationStr(node)}: could not parse conditional expression`);
319
+ return null;
320
+ }
321
+ // Recursively resolve both branches (handles nested ternaries)
322
+ const consequentNode = await resolveStaticValue(consequent, ctx);
323
+ const alternateNode = await resolveStaticValue(alternate, ctx);
324
+ if (!consequentNode || !alternateNode)
325
+ return null;
326
+ // Flatten choices
327
+ const branches = [];
328
+ if (consequentNode.type === 'choice') {
329
+ branches.push(...consequentNode.nodes);
330
+ }
331
+ else {
332
+ branches.push(consequentNode);
333
+ }
334
+ if (alternateNode.type === 'choice') {
335
+ branches.push(...alternateNode.nodes);
336
+ }
337
+ else {
338
+ branches.push(alternateNode);
339
+ }
340
+ return { type: 'choice', nodes: branches };
341
+ }
342
+ /**
343
+ * Resolves a function call to its string return variants.
344
+ * Looks up the function locally, then in imported files.
345
+ */
346
+ async function resolveFunctionCall(callNode, ctx) {
347
+ const funcNode = callNode.childForFieldName('function');
348
+ if (!funcNode || funcNode.type !== 'identifier') {
349
+ ctx.errors.push(`${locationStr(callNode)}: cannot resolve non-identifier function call`);
350
+ return null;
351
+ }
352
+ const funcName = funcNode.text;
353
+ // Expression parser callback for resolving return expressions.
354
+ // For local functions, use the current context's imports.
355
+ // For cross-file, build context from the target file's imports.
356
+ const makeExprParser = (targetCtx) => {
357
+ return (node, _rootNode) => resolveStaticValue(node, targetCtx);
358
+ };
359
+ // Try resolving in current file
360
+ const localResult = await resolveFunctionInCurrentFile(funcName, ctx.rootNode, makeExprParser(ctx));
361
+ if (localResult)
362
+ return localResult;
363
+ // Try resolving from imports
364
+ const importInfo = findImportForName(funcName, ctx);
365
+ if (importInfo) {
366
+ // Build a context for the target file
367
+ const targetCtx = {
368
+ rootNode: ctx.rootNode, // Will be replaced by the actual target rootNode inside resolveFunctionInFile
369
+ imports: ctx.imports, // Use caller's imports as fallback
370
+ filePath: importInfo.filePath,
371
+ errors: ctx.errors,
372
+ };
373
+ const result = await resolveFunctionInFile(importInfo.originalName, importInfo.filePath, async (node, targetRootNode) => {
374
+ // Build proper context using target file's root and imports
375
+ const targetImports = extractImportsFromRoot(targetRootNode, ctx.imports);
376
+ return resolveStaticValue(node, {
377
+ rootNode: targetRootNode,
378
+ imports: targetImports,
379
+ filePath: importInfo.filePath,
380
+ errors: ctx.errors,
381
+ });
382
+ });
383
+ if (result)
384
+ return result;
385
+ }
386
+ ctx.errors.push(`${locationStr(callNode)}: could not resolve function "${funcName}" to string return values`);
387
+ return null;
388
+ }
389
+ /**
390
+ * Extracts import aliases from a target file's root node.
391
+ * Merges with parent imports for GT package functions (declare_var, etc.)
392
+ * that may not be imported in the target file.
393
+ */
394
+ function extractImportsFromRoot(rootNode, parentImports) {
395
+ // Import extractImports dynamically to avoid issues
396
+ const result = [];
397
+ // Carry over GT package imports from the calling context
398
+ // (declare_var, declare_static may be used without importing in helper files
399
+ // if passed through function calls, but the name resolution still needs them)
400
+ for (const imp of parentImports) {
401
+ if (imp.originalName === PYTHON_DECLARE_STATIC ||
402
+ imp.originalName === PYTHON_DECLARE_VAR) {
403
+ result.push(imp);
404
+ }
405
+ }
406
+ // Also check the target file's own imports for GT functions
407
+ for (let i = 0; i < rootNode.childCount; i++) {
408
+ const node = rootNode.child(i);
409
+ if (!node || node.type !== 'import_from_statement')
410
+ continue;
411
+ const moduleName = getModuleName(node);
412
+ if (!moduleName)
413
+ continue;
414
+ for (let j = 0; j < node.childCount; j++) {
415
+ const child = node.child(j);
416
+ if (!child)
417
+ continue;
418
+ if (child.type === 'dotted_name' && child.text !== moduleName) {
419
+ result.push({
420
+ localName: child.text,
421
+ originalName: child.text,
422
+ packageName: moduleName,
423
+ });
424
+ }
425
+ else if (child.type === 'aliased_import') {
426
+ const nameNode = child.childForFieldName('name');
427
+ const aliasNode = child.childForFieldName('alias');
428
+ if (nameNode) {
429
+ result.push({
430
+ localName: aliasNode?.text ?? nameNode.text,
431
+ originalName: nameNode.text,
432
+ packageName: moduleName,
433
+ });
434
+ }
435
+ }
436
+ }
437
+ }
438
+ return result;
439
+ }
440
+ /**
441
+ * Resolves the argument of a declare_var() call.
442
+ * Produces ICU placeholder text using declareVar from generaltranslation.
443
+ */
444
+ async function resolveDeclareVarArg(callNode, ctx) {
445
+ const argsNode = callNode.childForFieldName('arguments');
446
+ if (!argsNode) {
447
+ ctx.errors.push(`${locationStr(callNode)}: declare_var() requires arguments`);
448
+ return null;
449
+ }
450
+ // Get the first positional arg (the variable - we use empty string since it's runtime)
451
+ const firstArg = getFirstPositionalArg(callNode);
452
+ if (!firstArg) {
453
+ ctx.errors.push(`${locationStr(callNode)}: declare_var() requires a variable argument`);
454
+ return null;
455
+ }
456
+ // Extract optional _name kwarg
457
+ let nameOption;
458
+ for (let i = 0; i < argsNode.childCount; i++) {
459
+ const child = argsNode.child(i);
460
+ if (!child || child.type !== 'keyword_argument')
461
+ continue;
462
+ const nameNode = child.childForFieldName('name');
463
+ const valueNode = child.childForFieldName('value');
464
+ if (!nameNode || !valueNode)
465
+ continue;
466
+ if (nameNode.text === '_name') {
467
+ if (valueNode.type === 'string' && !isFString(valueNode)) {
468
+ nameOption = extractStringContent(valueNode);
469
+ }
470
+ }
471
+ }
472
+ // Use declareVar with empty string for the runtime variable value
473
+ const options = nameOption ? { $name: nameOption } : undefined;
474
+ const icuText = declareVar('', options);
475
+ return { type: 'text', text: icuText };
476
+ }
477
+ // ===== Helpers ===== //
478
+ function getFirstPositionalArg(callNode) {
479
+ const argsNode = callNode.childForFieldName('arguments');
480
+ if (!argsNode)
481
+ return null;
482
+ for (let i = 0; i < argsNode.childCount; i++) {
483
+ const child = argsNode.child(i);
484
+ if (child &&
485
+ child.type !== '(' &&
486
+ child.type !== ')' &&
487
+ child.type !== ',' &&
488
+ child.type !== 'keyword_argument') {
489
+ return child;
490
+ }
491
+ }
492
+ return null;
493
+ }
494
+ function getOriginalImportName(localName, imports) {
495
+ for (const imp of imports) {
496
+ if (imp.localName === localName) {
497
+ return imp.originalName;
498
+ }
499
+ }
500
+ return null;
501
+ }
502
+ function getStaticImportNames(imports) {
503
+ const names = new Set();
504
+ for (const imp of imports) {
505
+ if (imp.originalName === PYTHON_DECLARE_STATIC ||
506
+ imp.originalName === PYTHON_DECLARE_VAR) {
507
+ names.add(imp.localName);
508
+ }
509
+ }
510
+ return names;
511
+ }
512
+ /**
513
+ * Finds import info for a given local name (for cross-file function resolution).
514
+ * Only looks at non-GT imports (user function imports).
515
+ */
516
+ function findImportForName(localName, ctx) {
517
+ // Walk the AST to find import_from_statement nodes
518
+ for (let i = 0; i < ctx.rootNode.childCount; i++) {
519
+ const node = ctx.rootNode.child(i);
520
+ if (!node || node.type !== 'import_from_statement')
521
+ continue;
522
+ const moduleName = getModuleName(node);
523
+ if (!moduleName)
524
+ continue;
525
+ // Check all imported names in this statement
526
+ for (let j = 0; j < node.childCount; j++) {
527
+ const child = node.child(j);
528
+ if (!child)
529
+ continue;
530
+ if (child.type === 'aliased_import') {
531
+ const nameNode = child.childForFieldName('name');
532
+ const aliasNode = child.childForFieldName('alias');
533
+ const importedName = nameNode?.text;
534
+ const alias = aliasNode?.text ?? importedName;
535
+ if (alias === localName && importedName) {
536
+ const filePath = resolveImportPath(moduleName, ctx.filePath);
537
+ if (filePath) {
538
+ return { originalName: importedName, filePath };
539
+ }
540
+ }
541
+ }
542
+ else if (child.type === 'dotted_name') {
543
+ if (child.text === moduleName)
544
+ continue; // Skip module name itself
545
+ if (child.text === localName) {
546
+ const filePath = resolveImportPath(moduleName, ctx.filePath);
547
+ if (filePath) {
548
+ return { originalName: localName, filePath };
549
+ }
550
+ }
551
+ }
552
+ }
553
+ }
554
+ return null;
555
+ }
556
+ function getModuleName(importNode) {
557
+ const moduleNode = importNode.childForFieldName('module_name');
558
+ if (moduleNode)
559
+ return moduleNode.text;
560
+ for (let i = 0; i < importNode.childCount; i++) {
561
+ const child = importNode.child(i);
562
+ if (!child)
563
+ continue;
564
+ if (child.type === 'import')
565
+ break;
566
+ if (child.type === 'dotted_name')
567
+ return child.text;
568
+ if (child.type === 'relative_import')
569
+ return child.text;
570
+ }
571
+ return undefined;
572
+ }
573
+ function isFString(stringNode) {
574
+ for (let i = 0; i < stringNode.childCount; i++) {
575
+ const child = stringNode.child(i);
576
+ if (child && child.type === 'string_start') {
577
+ return /^[fF]/.test(child.text);
578
+ }
579
+ if (child && child.type === 'interpolation') {
580
+ return true;
581
+ }
582
+ }
583
+ return false;
584
+ }
585
+ function extractStringContent(stringNode) {
586
+ for (let i = 0; i < stringNode.childCount; i++) {
587
+ const child = stringNode.child(i);
588
+ if (child && child.type === 'string_content') {
589
+ return child.text;
590
+ }
591
+ }
592
+ let hasStart = false;
593
+ let hasEnd = false;
594
+ for (let i = 0; i < stringNode.childCount; i++) {
595
+ const child = stringNode.child(i);
596
+ if (child?.type === 'string_start')
597
+ hasStart = true;
598
+ if (child?.type === 'string_end')
599
+ hasEnd = true;
600
+ }
601
+ if (hasStart && hasEnd)
602
+ return '';
603
+ return undefined;
604
+ }
605
+ function locationStr(node) {
606
+ return `line ${node.startPosition.row + 1}, col ${node.startPosition.column}`;
607
+ }
@@ -0,0 +1,8 @@
1
+ import { Parser } from 'web-tree-sitter';
2
+ /**
3
+ * Lazily initializes and returns a singleton tree-sitter Parser
4
+ * configured for Python.
5
+ */
6
+ export declare function getParser(): Promise<Parser>;
7
+ export type { Parser };
8
+ export { type Tree, type Node as SyntaxNode } from 'web-tree-sitter';
package/dist/parser.js ADDED
@@ -0,0 +1,22 @@
1
+ import { Parser, Language } from 'web-tree-sitter';
2
+ import { createRequire } from 'module';
3
+ let parserPromise = null;
4
+ /**
5
+ * Lazily initializes and returns a singleton tree-sitter Parser
6
+ * configured for Python.
7
+ */
8
+ export function getParser() {
9
+ if (!parserPromise) {
10
+ parserPromise = initParser();
11
+ }
12
+ return parserPromise;
13
+ }
14
+ async function initParser() {
15
+ await Parser.init();
16
+ const parser = new Parser();
17
+ const require = createRequire(import.meta.url);
18
+ const wasmPath = require.resolve('tree-sitter-python/tree-sitter-python.wasm');
19
+ const Python = await Language.load(wasmPath);
20
+ parser.setLanguage(Python);
21
+ return parser;
22
+ }
@@ -0,0 +1,20 @@
1
+ import type { SyntaxNode } from './parser.js';
2
+ import type { StringNode } from './stringNode.js';
3
+ /**
4
+ * Callback to parse a return expression into a StringNode.
5
+ * Provided by the caller so function resolution doesn't need to know about
6
+ * declare_var, declare_static, imports, etc.
7
+ */
8
+ export type ExpressionParser = (node: SyntaxNode, rootNode: SyntaxNode) => Promise<StringNode | null>;
9
+ /**
10
+ * Resolves all return values of a function defined in the current file's AST.
11
+ * Uses the provided expression parser to handle complex return expressions
12
+ * (concat, declare_var, etc.).
13
+ */
14
+ export declare function resolveFunctionInCurrentFile(functionName: string, rootNode: SyntaxNode, parseExpr: ExpressionParser): Promise<StringNode | null>;
15
+ /**
16
+ * Resolves all return values of a function defined in an external file.
17
+ * Results are cached by filePath::functionName.
18
+ */
19
+ export declare function resolveFunctionInFile(functionName: string, filePath: string, parseExpr: ExpressionParser): Promise<StringNode | null>;
20
+ export declare function clearFunctionCache(): void;