@generaltranslation/python-extractor 0.0.0 → 0.0.1-alpha.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.
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { PYTHON_DECLARE_STATIC, PYTHON_DECLARE_VAR } from './constants.js';
|
|
2
2
|
import { resolveFunctionInCurrentFile, resolveFunctionInFile, } from './resolveFunctionVariants.js';
|
|
3
|
+
import { extractImports } from './extractImports.js';
|
|
3
4
|
import { resolveImportPath } from './resolveImport.js';
|
|
4
5
|
import { declareVar } from 'generaltranslation/internal';
|
|
5
6
|
/**
|
|
@@ -236,25 +237,10 @@ async function resolveStaticBinaryOperator(node, ctx) {
|
|
|
236
237
|
ctx.errors.push(`${locationStr(node)}: binary operator missing operands`);
|
|
237
238
|
return null;
|
|
238
239
|
}
|
|
239
|
-
//
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
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`);
|
|
240
|
+
// Verify it's a + operator
|
|
241
|
+
const operator = node.childForFieldName('operator');
|
|
242
|
+
if (operator && operator.text !== '+') {
|
|
243
|
+
ctx.errors.push(`${locationStr(node)}: unsupported binary operator "${operator.text}" in static expression`);
|
|
258
244
|
return null;
|
|
259
245
|
}
|
|
260
246
|
const leftNode = await resolveStaticValue(left, ctx);
|
|
@@ -351,35 +337,25 @@ async function resolveFunctionCall(callNode, ctx) {
|
|
|
351
337
|
}
|
|
352
338
|
const funcName = funcNode.text;
|
|
353
339
|
// Expression parser callback for resolving return expressions.
|
|
354
|
-
//
|
|
355
|
-
//
|
|
356
|
-
const
|
|
357
|
-
|
|
340
|
+
// Receives the actual rootNode and filePath from whichever file
|
|
341
|
+
// the function is defined in (handles re-exports correctly).
|
|
342
|
+
const exprParser = (node, targetRootNode, targetFilePath) => {
|
|
343
|
+
const targetImports = extractImportsFromRoot(targetRootNode, ctx.imports);
|
|
344
|
+
return resolveStaticValue(node, {
|
|
345
|
+
rootNode: targetRootNode,
|
|
346
|
+
imports: targetImports,
|
|
347
|
+
filePath: targetFilePath,
|
|
348
|
+
errors: ctx.errors,
|
|
349
|
+
});
|
|
358
350
|
};
|
|
359
351
|
// Try resolving in current file
|
|
360
|
-
const localResult = await resolveFunctionInCurrentFile(funcName, ctx.rootNode,
|
|
352
|
+
const localResult = await resolveFunctionInCurrentFile(funcName, ctx.rootNode, ctx.filePath, exprParser);
|
|
361
353
|
if (localResult)
|
|
362
354
|
return localResult;
|
|
363
|
-
// Try resolving from imports
|
|
355
|
+
// Try resolving from imports (follows re-export chains automatically)
|
|
364
356
|
const importInfo = findImportForName(funcName, ctx);
|
|
365
357
|
if (importInfo) {
|
|
366
|
-
|
|
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
|
-
});
|
|
358
|
+
const result = await resolveFunctionInFile(importInfo.originalName, importInfo.filePath, exprParser);
|
|
383
359
|
if (result)
|
|
384
360
|
return result;
|
|
385
361
|
}
|
|
@@ -387,55 +363,27 @@ async function resolveFunctionCall(callNode, ctx) {
|
|
|
387
363
|
return null;
|
|
388
364
|
}
|
|
389
365
|
/**
|
|
390
|
-
* Extracts import aliases from a target file's root node.
|
|
366
|
+
* Extracts GT import aliases from a target file's root node.
|
|
391
367
|
* Merges with parent imports for GT package functions (declare_var, etc.)
|
|
392
368
|
* that may not be imported in the target file.
|
|
393
369
|
*/
|
|
394
370
|
function extractImportsFromRoot(rootNode, parentImports) {
|
|
395
|
-
//
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
//
|
|
399
|
-
//
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
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
|
-
}
|
|
371
|
+
// Extract GT-only imports from the target file using the same
|
|
372
|
+
// filtering logic as the main extractImports (filters by GT packages)
|
|
373
|
+
const fileImports = extractImports(rootNode);
|
|
374
|
+
// Carry over GT declare_* imports from the calling context
|
|
375
|
+
// (in case the helper file doesn't import them directly)
|
|
376
|
+
const parentDeclareImports = parentImports.filter((imp) => imp.originalName === PYTHON_DECLARE_STATIC ||
|
|
377
|
+
imp.originalName === PYTHON_DECLARE_VAR);
|
|
378
|
+
// Deduplicate: prefer the target file's own imports over parent's
|
|
379
|
+
const seen = new Set(fileImports.map((imp) => imp.localName));
|
|
380
|
+
const merged = [...fileImports];
|
|
381
|
+
for (const imp of parentDeclareImports) {
|
|
382
|
+
if (!seen.has(imp.localName)) {
|
|
383
|
+
merged.push(imp);
|
|
436
384
|
}
|
|
437
385
|
}
|
|
438
|
-
return
|
|
386
|
+
return merged;
|
|
439
387
|
}
|
|
440
388
|
/**
|
|
441
389
|
* Resolves the argument of a declare_var() call.
|
|
@@ -4,17 +4,23 @@ import type { StringNode } from './stringNode.js';
|
|
|
4
4
|
* Callback to parse a return expression into a StringNode.
|
|
5
5
|
* Provided by the caller so function resolution doesn't need to know about
|
|
6
6
|
* declare_var, declare_static, imports, etc.
|
|
7
|
+
*
|
|
8
|
+
* @param node - The return expression AST node
|
|
9
|
+
* @param rootNode - The root AST node of the file containing the function
|
|
10
|
+
* @param filePath - The absolute path of the file containing the function
|
|
7
11
|
*/
|
|
8
|
-
export type ExpressionParser = (node: SyntaxNode, rootNode: SyntaxNode) => Promise<StringNode | null>;
|
|
12
|
+
export type ExpressionParser = (node: SyntaxNode, rootNode: SyntaxNode, filePath: string) => Promise<StringNode | null>;
|
|
9
13
|
/**
|
|
10
14
|
* Resolves all return values of a function defined in the current file's AST.
|
|
11
15
|
* Uses the provided expression parser to handle complex return expressions
|
|
12
16
|
* (concat, declare_var, etc.).
|
|
13
17
|
*/
|
|
14
|
-
export declare function resolveFunctionInCurrentFile(functionName: string, rootNode: SyntaxNode, parseExpr: ExpressionParser): Promise<StringNode | null>;
|
|
18
|
+
export declare function resolveFunctionInCurrentFile(functionName: string, rootNode: SyntaxNode, filePath: string, parseExpr: ExpressionParser): Promise<StringNode | null>;
|
|
15
19
|
/**
|
|
16
20
|
* Resolves all return values of a function defined in an external file.
|
|
21
|
+
* Follows re-export chains: if the function isn't defined in the target file
|
|
22
|
+
* but is imported from another module, follows the import to the source.
|
|
17
23
|
* Results are cached by filePath::functionName.
|
|
18
24
|
*/
|
|
19
|
-
export declare function resolveFunctionInFile(functionName: string, filePath: string, parseExpr: ExpressionParser): Promise<StringNode | null>;
|
|
25
|
+
export declare function resolveFunctionInFile(functionName: string, filePath: string, parseExpr: ExpressionParser, visited?: Set<string>): Promise<StringNode | null>;
|
|
20
26
|
export declare function clearFunctionCache(): void;
|
|
@@ -1,26 +1,36 @@
|
|
|
1
1
|
import fs from 'node:fs';
|
|
2
2
|
import { getParser } from './parser.js';
|
|
3
|
+
import { resolveImportPath } from './resolveImport.js';
|
|
3
4
|
const crossFileCache = new Map();
|
|
4
5
|
/**
|
|
5
6
|
* Resolves all return values of a function defined in the current file's AST.
|
|
6
7
|
* Uses the provided expression parser to handle complex return expressions
|
|
7
8
|
* (concat, declare_var, etc.).
|
|
8
9
|
*/
|
|
9
|
-
export async function resolveFunctionInCurrentFile(functionName, rootNode, parseExpr) {
|
|
10
|
+
export async function resolveFunctionInCurrentFile(functionName, rootNode, filePath, parseExpr) {
|
|
10
11
|
const funcDef = findFunctionDefinition(rootNode, functionName);
|
|
11
12
|
if (!funcDef)
|
|
12
13
|
return null;
|
|
13
|
-
return extractReturnVariants(funcDef, rootNode, parseExpr);
|
|
14
|
+
return extractReturnVariants(funcDef, rootNode, filePath, parseExpr);
|
|
14
15
|
}
|
|
15
16
|
/**
|
|
16
17
|
* Resolves all return values of a function defined in an external file.
|
|
18
|
+
* Follows re-export chains: if the function isn't defined in the target file
|
|
19
|
+
* but is imported from another module, follows the import to the source.
|
|
17
20
|
* Results are cached by filePath::functionName.
|
|
18
21
|
*/
|
|
19
|
-
export async function resolveFunctionInFile(functionName, filePath, parseExpr) {
|
|
22
|
+
export async function resolveFunctionInFile(functionName, filePath, parseExpr, visited) {
|
|
20
23
|
const cacheKey = `${filePath}::${functionName}`;
|
|
21
24
|
if (crossFileCache.has(cacheKey)) {
|
|
22
25
|
return crossFileCache.get(cacheKey);
|
|
23
26
|
}
|
|
27
|
+
// Prevent infinite re-export loops
|
|
28
|
+
const visitedSet = visited ?? new Set();
|
|
29
|
+
if (visitedSet.has(cacheKey)) {
|
|
30
|
+
crossFileCache.set(cacheKey, null);
|
|
31
|
+
return null;
|
|
32
|
+
}
|
|
33
|
+
visitedSet.add(cacheKey);
|
|
24
34
|
let source;
|
|
25
35
|
try {
|
|
26
36
|
source = fs.readFileSync(filePath, 'utf8');
|
|
@@ -35,9 +45,21 @@ export async function resolveFunctionInFile(functionName, filePath, parseExpr) {
|
|
|
35
45
|
crossFileCache.set(cacheKey, null);
|
|
36
46
|
return null;
|
|
37
47
|
}
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
48
|
+
// Try to find function definition in this file
|
|
49
|
+
const result = await resolveFunctionInCurrentFile(functionName, tree.rootNode, filePath, parseExpr);
|
|
50
|
+
if (result) {
|
|
51
|
+
crossFileCache.set(cacheKey, result);
|
|
52
|
+
return result;
|
|
53
|
+
}
|
|
54
|
+
// Function not defined here — check for re-exports
|
|
55
|
+
const reExportInfo = findReExport(functionName, tree.rootNode, filePath);
|
|
56
|
+
if (reExportInfo) {
|
|
57
|
+
const reResult = await resolveFunctionInFile(reExportInfo.originalName, reExportInfo.filePath, parseExpr, visitedSet);
|
|
58
|
+
crossFileCache.set(cacheKey, reResult);
|
|
59
|
+
return reResult;
|
|
60
|
+
}
|
|
61
|
+
crossFileCache.set(cacheKey, null);
|
|
62
|
+
return null;
|
|
41
63
|
}
|
|
42
64
|
/**
|
|
43
65
|
* Finds a top-level function_definition by name in the AST.
|
|
@@ -70,7 +92,7 @@ function findFunctionDefinition(rootNode, name) {
|
|
|
70
92
|
* Extracts all return values from a function body and parses them into StringNodes.
|
|
71
93
|
* Skips nested function definitions.
|
|
72
94
|
*/
|
|
73
|
-
async function extractReturnVariants(funcDef, rootNode, parseExpr) {
|
|
95
|
+
async function extractReturnVariants(funcDef, rootNode, filePath, parseExpr) {
|
|
74
96
|
const body = funcDef.childForFieldName('body');
|
|
75
97
|
if (!body)
|
|
76
98
|
return null;
|
|
@@ -81,7 +103,7 @@ async function extractReturnVariants(funcDef, rootNode, parseExpr) {
|
|
|
81
103
|
// Parse each return expression into a StringNode
|
|
82
104
|
const nodes = [];
|
|
83
105
|
for (const expr of returnExprs) {
|
|
84
|
-
const node = await parseExpr(expr, rootNode);
|
|
106
|
+
const node = await parseExpr(expr, rootNode, filePath);
|
|
85
107
|
if (node)
|
|
86
108
|
nodes.push(node);
|
|
87
109
|
}
|
|
@@ -117,6 +139,62 @@ function collectReturnExpressions(node, results) {
|
|
|
117
139
|
collectReturnExpressions(child, results);
|
|
118
140
|
}
|
|
119
141
|
}
|
|
142
|
+
/**
|
|
143
|
+
* Checks if a function name is re-exported from another module in the given file.
|
|
144
|
+
* e.g., `from static_test import get_gender` makes `get_gender` a re-export.
|
|
145
|
+
*/
|
|
146
|
+
function findReExport(functionName, rootNode, currentFilePath) {
|
|
147
|
+
for (let i = 0; i < rootNode.childCount; i++) {
|
|
148
|
+
const node = rootNode.child(i);
|
|
149
|
+
if (!node || node.type !== 'import_from_statement')
|
|
150
|
+
continue;
|
|
151
|
+
const moduleName = getModuleName(node);
|
|
152
|
+
if (!moduleName)
|
|
153
|
+
continue;
|
|
154
|
+
for (let j = 0; j < node.childCount; j++) {
|
|
155
|
+
const child = node.child(j);
|
|
156
|
+
if (!child)
|
|
157
|
+
continue;
|
|
158
|
+
if (child.type === 'dotted_name' && child.text !== moduleName) {
|
|
159
|
+
if (child.text === functionName) {
|
|
160
|
+
const resolved = resolveImportPath(moduleName, currentFilePath);
|
|
161
|
+
if (resolved) {
|
|
162
|
+
return { originalName: functionName, filePath: resolved };
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
else if (child.type === 'aliased_import') {
|
|
167
|
+
const nameNode = child.childForFieldName('name');
|
|
168
|
+
const aliasNode = child.childForFieldName('alias');
|
|
169
|
+
const alias = aliasNode?.text ?? nameNode?.text;
|
|
170
|
+
if (alias === functionName && nameNode) {
|
|
171
|
+
const resolved = resolveImportPath(moduleName, currentFilePath);
|
|
172
|
+
if (resolved) {
|
|
173
|
+
return { originalName: nameNode.text, filePath: resolved };
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
return null;
|
|
180
|
+
}
|
|
181
|
+
function getModuleName(importNode) {
|
|
182
|
+
const moduleNode = importNode.childForFieldName('module_name');
|
|
183
|
+
if (moduleNode)
|
|
184
|
+
return moduleNode.text;
|
|
185
|
+
for (let i = 0; i < importNode.childCount; i++) {
|
|
186
|
+
const child = importNode.child(i);
|
|
187
|
+
if (!child)
|
|
188
|
+
continue;
|
|
189
|
+
if (child.type === 'import')
|
|
190
|
+
break;
|
|
191
|
+
if (child.type === 'dotted_name')
|
|
192
|
+
return child.text;
|
|
193
|
+
if (child.type === 'relative_import')
|
|
194
|
+
return child.text;
|
|
195
|
+
}
|
|
196
|
+
return undefined;
|
|
197
|
+
}
|
|
120
198
|
export function clearFunctionCache() {
|
|
121
199
|
crossFileCache.clear();
|
|
122
200
|
}
|
package/dist/resolveImport.js
CHANGED
|
@@ -34,8 +34,13 @@ function doResolve(moduleName, currentFilePath) {
|
|
|
34
34
|
baseDir = path.dirname(baseDir);
|
|
35
35
|
}
|
|
36
36
|
const remainder = moduleName.slice(dotCount);
|
|
37
|
-
if (!remainder)
|
|
37
|
+
if (!remainder) {
|
|
38
|
+
// Bare dot import (e.g., "from . import X") — resolve to __init__.py
|
|
39
|
+
const initPath = path.join(baseDir, '__init__.py');
|
|
40
|
+
if (fs.existsSync(initPath))
|
|
41
|
+
return initPath;
|
|
38
42
|
return null;
|
|
43
|
+
}
|
|
39
44
|
return resolveModulePath(baseDir, remainder);
|
|
40
45
|
}
|
|
41
46
|
// Absolute import: dotted or simple name
|