@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.
- package/LICENSE.md +105 -0
- package/dist/constants.d.ts +13 -0
- package/dist/constants.js +18 -0
- package/dist/extractCalls.d.ts +20 -0
- package/dist/extractCalls.js +211 -0
- package/dist/extractImports.d.ts +18 -0
- package/dist/extractImports.js +86 -0
- package/dist/index.d.ts +9 -0
- package/dist/index.js +42 -0
- package/dist/parseStringExpression.d.ts +20 -0
- package/dist/parseStringExpression.js +607 -0
- package/dist/parser.d.ts +8 -0
- package/dist/parser.js +22 -0
- package/dist/resolveFunctionVariants.d.ts +20 -0
- package/dist/resolveFunctionVariants.js +122 -0
- package/dist/resolveImport.d.ts +11 -0
- package/dist/resolveImport.js +63 -0
- package/dist/stringNode.d.ts +21 -0
- package/dist/stringNode.js +51 -0
- package/dist/types.d.ts +16 -0
- package/dist/types.js +1 -0
- package/package.json +68 -0
|
@@ -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
|
+
}
|
package/dist/parser.d.ts
ADDED
|
@@ -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;
|