@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.
@@ -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
- // 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`);
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
- // 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);
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, makeExprParser(ctx));
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
- // 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
- });
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
- // 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
- }
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 result;
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
- const result = await resolveFunctionInCurrentFile(functionName, tree.rootNode, parseExpr);
39
- crossFileCache.set(cacheKey, result);
40
- return result;
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
  }
@@ -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
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@generaltranslation/python-extractor",
3
- "version": "0.0.0",
3
+ "version": "0.1.0",
4
4
  "description": "Python source code extraction for General Translation",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",