@generaltranslation/python-extractor 0.0.0 → 0.1.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/dist/extractCalls.js
CHANGED
|
@@ -65,7 +65,9 @@ async function processCall(callNode, imports, filePath, calls, errors, _warnings
|
|
|
65
65
|
containsStaticCalls(firstArg, imports)) ||
|
|
66
66
|
(firstArg.type === 'binary_operator' &&
|
|
67
67
|
containsStaticCalls(firstArg, imports)) ||
|
|
68
|
-
(firstArg.type === 'call' && containsStaticCalls(firstArg, imports))
|
|
68
|
+
(firstArg.type === 'call' && containsStaticCalls(firstArg, imports)) ||
|
|
69
|
+
(firstArg.type === 'parenthesized_expression' &&
|
|
70
|
+
containsStaticCalls(firstArg, imports));
|
|
69
71
|
if (hasStaticHelpers) {
|
|
70
72
|
// Compound expression path: parse into StringNode tree
|
|
71
73
|
const rootNode = callNode.tree?.rootNode;
|
|
@@ -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
|
/**
|
|
@@ -33,6 +34,16 @@ function hasStaticCallRecursive(node, names) {
|
|
|
33
34
|
* binary + concatenation, and standalone declare_static calls.
|
|
34
35
|
*/
|
|
35
36
|
export async function parseStringExpression(node, ctx) {
|
|
37
|
+
// Parenthesized expression: unwrap and recurse
|
|
38
|
+
if (node.type === 'parenthesized_expression') {
|
|
39
|
+
for (let i = 0; i < node.childCount; i++) {
|
|
40
|
+
const child = node.child(i);
|
|
41
|
+
if (child && child.type !== '(' && child.type !== ')') {
|
|
42
|
+
return parseStringExpression(child, ctx);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
return null;
|
|
46
|
+
}
|
|
36
47
|
// Plain string (no f-string)
|
|
37
48
|
if (node.type === 'string' && !isFString(node)) {
|
|
38
49
|
const content = extractStringContent(node);
|
|
@@ -236,25 +247,10 @@ async function resolveStaticBinaryOperator(node, ctx) {
|
|
|
236
247
|
ctx.errors.push(`${locationStr(node)}: binary operator missing operands`);
|
|
237
248
|
return null;
|
|
238
249
|
}
|
|
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`);
|
|
250
|
+
// Verify it's a + operator
|
|
251
|
+
const operator = node.childForFieldName('operator');
|
|
252
|
+
if (operator && operator.text !== '+') {
|
|
253
|
+
ctx.errors.push(`${locationStr(node)}: unsupported binary operator "${operator.text}" in static expression`);
|
|
258
254
|
return null;
|
|
259
255
|
}
|
|
260
256
|
const leftNode = await resolveStaticValue(left, ctx);
|
|
@@ -351,35 +347,25 @@ async function resolveFunctionCall(callNode, ctx) {
|
|
|
351
347
|
}
|
|
352
348
|
const funcName = funcNode.text;
|
|
353
349
|
// Expression parser callback for resolving return expressions.
|
|
354
|
-
//
|
|
355
|
-
//
|
|
356
|
-
const
|
|
357
|
-
|
|
350
|
+
// Receives the actual rootNode and filePath from whichever file
|
|
351
|
+
// the function is defined in (handles re-exports correctly).
|
|
352
|
+
const exprParser = (node, targetRootNode, targetFilePath) => {
|
|
353
|
+
const targetImports = extractImportsFromRoot(targetRootNode, ctx.imports);
|
|
354
|
+
return resolveStaticValue(node, {
|
|
355
|
+
rootNode: targetRootNode,
|
|
356
|
+
imports: targetImports,
|
|
357
|
+
filePath: targetFilePath,
|
|
358
|
+
errors: ctx.errors,
|
|
359
|
+
});
|
|
358
360
|
};
|
|
359
361
|
// Try resolving in current file
|
|
360
|
-
const localResult = await resolveFunctionInCurrentFile(funcName, ctx.rootNode,
|
|
362
|
+
const localResult = await resolveFunctionInCurrentFile(funcName, ctx.rootNode, ctx.filePath, exprParser);
|
|
361
363
|
if (localResult)
|
|
362
364
|
return localResult;
|
|
363
|
-
// Try resolving from imports
|
|
365
|
+
// Try resolving from imports (follows re-export chains automatically)
|
|
364
366
|
const importInfo = findImportForName(funcName, ctx);
|
|
365
367
|
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
|
-
});
|
|
368
|
+
const result = await resolveFunctionInFile(importInfo.originalName, importInfo.filePath, exprParser);
|
|
383
369
|
if (result)
|
|
384
370
|
return result;
|
|
385
371
|
}
|
|
@@ -387,55 +373,27 @@ async function resolveFunctionCall(callNode, ctx) {
|
|
|
387
373
|
return null;
|
|
388
374
|
}
|
|
389
375
|
/**
|
|
390
|
-
* Extracts import aliases from a target file's root node.
|
|
376
|
+
* Extracts GT import aliases from a target file's root node.
|
|
391
377
|
* Merges with parent imports for GT package functions (declare_var, etc.)
|
|
392
378
|
* that may not be imported in the target file.
|
|
393
379
|
*/
|
|
394
380
|
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
|
-
}
|
|
381
|
+
// Extract GT-only imports from the target file using the same
|
|
382
|
+
// filtering logic as the main extractImports (filters by GT packages)
|
|
383
|
+
const fileImports = extractImports(rootNode);
|
|
384
|
+
// Carry over GT declare_* imports from the calling context
|
|
385
|
+
// (in case the helper file doesn't import them directly)
|
|
386
|
+
const parentDeclareImports = parentImports.filter((imp) => imp.originalName === PYTHON_DECLARE_STATIC ||
|
|
387
|
+
imp.originalName === PYTHON_DECLARE_VAR);
|
|
388
|
+
// Deduplicate: prefer the target file's own imports over parent's
|
|
389
|
+
const seen = new Set(fileImports.map((imp) => imp.localName));
|
|
390
|
+
const merged = [...fileImports];
|
|
391
|
+
for (const imp of parentDeclareImports) {
|
|
392
|
+
if (!seen.has(imp.localName)) {
|
|
393
|
+
merged.push(imp);
|
|
436
394
|
}
|
|
437
395
|
}
|
|
438
|
-
return
|
|
396
|
+
return merged;
|
|
439
397
|
}
|
|
440
398
|
/**
|
|
441
399
|
* 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
|