@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
- // 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`);
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
- // 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);
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, makeExprParser(ctx));
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
- // 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
- });
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
- // 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
- }
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 result;
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
- 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.0.1-alpha.0",
4
4
  "description": "Python source code extraction for General Translation",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",