@generaltranslation/python-extractor 0.2.20 → 0.2.21

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/constants.js CHANGED
@@ -1,21 +1,26 @@
1
- export const PYTHON_GT_PACKAGES = ['gt_flask', 'gt_fastapi'];
2
- export const PYTHON_GT_DEPENDENCIES = ['gt-flask', 'gt-fastapi'];
3
- export const PYTHON_T_FUNCTION = 't';
4
- export const PYTHON_MSG_FUNCTION = 'msg';
5
- export const PYTHON_DERIVE = 'derive';
1
+ //#region src/constants.ts
2
+ const PYTHON_GT_PACKAGES = ["gt_flask", "gt_fastapi"];
3
+ const PYTHON_GT_DEPENDENCIES = ["gt-flask", "gt-fastapi"];
4
+ const PYTHON_T_FUNCTION = "t";
5
+ const PYTHON_MSG_FUNCTION = "msg";
6
+ const PYTHON_DERIVE = "derive";
6
7
  /** @deprecated Use PYTHON_DERIVE instead */
7
- export const PYTHON_DECLARE_STATIC = 'declare_static';
8
- export const PYTHON_DECLARE_VAR = 'declare_var';
8
+ const PYTHON_DECLARE_STATIC = "declare_static";
9
+ const PYTHON_DECLARE_VAR = "declare_var";
9
10
  /** These imported names are tracked (translation functions + derive helpers) */
10
- export const PYTHON_TRANSLATION_FUNCTIONS = [
11
- 't',
12
- 'msg',
13
- 'derive',
14
- 'declare_static',
15
- 'declare_var',
11
+ const PYTHON_TRANSLATION_FUNCTIONS = [
12
+ "t",
13
+ "msg",
14
+ "derive",
15
+ "declare_static",
16
+ "declare_var"
16
17
  ];
17
- export const PYTHON_METADATA_KWARGS = {
18
- _id: 'id',
19
- _context: 'context',
20
- _max_chars: 'maxChars',
18
+ const PYTHON_METADATA_KWARGS = {
19
+ _id: "id",
20
+ _context: "context",
21
+ _max_chars: "maxChars"
21
22
  };
23
+ //#endregion
24
+ export { PYTHON_DECLARE_STATIC, PYTHON_DECLARE_VAR, PYTHON_DERIVE, PYTHON_GT_DEPENDENCIES, PYTHON_GT_PACKAGES, PYTHON_METADATA_KWARGS, PYTHON_MSG_FUNCTION, PYTHON_TRANSLATION_FUNCTIONS, PYTHON_T_FUNCTION };
25
+
26
+ //# sourceMappingURL=constants.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"constants.js","names":[],"sources":["../src/constants.ts"],"sourcesContent":["export const PYTHON_GT_PACKAGES = ['gt_flask', 'gt_fastapi'] as const;\nexport const PYTHON_GT_DEPENDENCIES = ['gt-flask', 'gt-fastapi'] as const;\nexport const PYTHON_T_FUNCTION = 't';\nexport const PYTHON_MSG_FUNCTION = 'msg';\nexport const PYTHON_DERIVE = 'derive';\n/** @deprecated Use PYTHON_DERIVE instead */\nexport const PYTHON_DECLARE_STATIC = 'declare_static';\nexport const PYTHON_DECLARE_VAR = 'declare_var';\n/** These imported names are tracked (translation functions + derive helpers) */\nexport const PYTHON_TRANSLATION_FUNCTIONS = [\n 't',\n 'msg',\n 'derive',\n 'declare_static',\n 'declare_var',\n] as const;\nexport const PYTHON_METADATA_KWARGS = {\n _id: 'id',\n _context: 'context',\n _max_chars: 'maxChars',\n} as const;\n"],"mappings":";AAAA,MAAa,qBAAqB,CAAC,YAAY,aAAa;AAC5D,MAAa,yBAAyB,CAAC,YAAY,aAAa;AAChE,MAAa,oBAAoB;AACjC,MAAa,sBAAsB;AACnC,MAAa,gBAAgB;;AAE7B,MAAa,wBAAwB;AACrC,MAAa,qBAAqB;;AAElC,MAAa,+BAA+B;CAC1C;CACA;CACA;CACA;CACA;CACD;AACD,MAAa,yBAAyB;CACpC,KAAK;CACL,UAAU;CACV,YAAY;CACb"}
@@ -1,288 +1,205 @@
1
- import { PYTHON_METADATA_KWARGS, PYTHON_DERIVE, PYTHON_DECLARE_STATIC, PYTHON_DECLARE_VAR, } from './constants.js';
2
- import { containsStaticCalls, parseStringExpression, } from './parseStringExpression.js';
3
- import { nodeToStrings } from './stringNode.js';
4
- import { indexVars } from 'generaltranslation/internal';
5
- import { randomUUID } from 'node:crypto';
1
+ import { PYTHON_METADATA_KWARGS } from "./constants.js";
2
+ import { containsStaticCalls, parseStringExpression } from "./parseStringExpression.js";
3
+ import { nodeToStrings } from "./stringNode.js";
4
+ import { indexVars } from "generaltranslation/internal";
5
+ import { randomUUID } from "node:crypto";
6
+ //#region src/extractCalls.ts
6
7
  /**
7
- * Extracts translation function calls from a Python AST.
8
- * Walks all `call` nodes and checks if they reference a tracked import.
9
- */
10
- export async function extractCalls(rootNode, imports, filePath) {
11
- const calls = [];
12
- const errors = [];
13
- const warnings = [];
14
- // Only track t/msg as translation functions (not derive/declare_static/declare_var)
15
- const trackedNames = new Set(imports
16
- .filter((imp) => imp.originalName !== PYTHON_DERIVE &&
17
- imp.originalName !== PYTHON_DECLARE_STATIC &&
18
- imp.originalName !== PYTHON_DECLARE_VAR)
19
- .map((imp) => imp.localName));
20
- if (trackedNames.size === 0)
21
- return { calls, errors, warnings };
22
- await walkCalls(rootNode, trackedNames, imports, filePath, calls, errors, warnings);
23
- return { calls, errors, warnings };
8
+ * Extracts translation function calls from a Python AST.
9
+ * Walks all `call` nodes and checks if they reference a tracked import.
10
+ */
11
+ async function extractCalls(rootNode, imports, filePath) {
12
+ const calls = [];
13
+ const errors = [];
14
+ const warnings = [];
15
+ const trackedNames = new Set(imports.filter((imp) => imp.originalName !== "derive" && imp.originalName !== "declare_static" && imp.originalName !== "declare_var").map((imp) => imp.localName));
16
+ if (trackedNames.size === 0) return {
17
+ calls,
18
+ errors,
19
+ warnings
20
+ };
21
+ await walkCalls(rootNode, trackedNames, imports, filePath, calls, errors, warnings);
22
+ return {
23
+ calls,
24
+ errors,
25
+ warnings
26
+ };
24
27
  }
25
28
  async function walkCalls(node, trackedNames, imports, filePath, calls, errors, warnings) {
26
- if (node.type === 'call') {
27
- const funcNode = node.childForFieldName('function');
28
- if (funcNode &&
29
- funcNode.type === 'identifier' &&
30
- trackedNames.has(funcNode.text)) {
31
- await processCall(node, imports, filePath, calls, errors, warnings);
32
- }
33
- }
34
- for (let i = 0; i < node.childCount; i++) {
35
- const child = node.child(i);
36
- if (child)
37
- await walkCalls(child, trackedNames, imports, filePath, calls, errors, warnings);
38
- }
29
+ if (node.type === "call") {
30
+ const funcNode = node.childForFieldName("function");
31
+ if (funcNode && funcNode.type === "identifier" && trackedNames.has(funcNode.text)) await processCall(node, imports, filePath, calls, errors, warnings);
32
+ }
33
+ for (let i = 0; i < node.childCount; i++) {
34
+ const child = node.child(i);
35
+ if (child) await walkCalls(child, trackedNames, imports, filePath, calls, errors, warnings);
36
+ }
39
37
  }
40
38
  async function processCall(callNode, imports, filePath, calls, errors, _warnings) {
41
- const argsNode = callNode.childForFieldName('arguments');
42
- if (!argsNode) {
43
- errors.push(`${locationStr(callNode)}: translation call has no arguments`);
44
- return;
45
- }
46
- // Find first positional argument (skip punctuation)
47
- let firstArg = null;
48
- for (let i = 0; i < argsNode.childCount; i++) {
49
- const child = argsNode.child(i);
50
- if (child &&
51
- child.type !== '(' &&
52
- child.type !== ')' &&
53
- child.type !== ',' &&
54
- child.type !== 'keyword_argument') {
55
- firstArg = child;
56
- break;
57
- }
58
- }
59
- if (!firstArg) {
60
- errors.push(`${locationStr(callNode)}: translation call has no positional argument`);
61
- return;
62
- }
63
- // Check if this expression contains declare_static/declare_var
64
- const hasStaticHelpers = (firstArg.type === 'string' &&
65
- isFString(firstArg) &&
66
- containsStaticCalls(firstArg, imports)) ||
67
- (firstArg.type === 'binary_operator' &&
68
- containsStaticCalls(firstArg, imports)) ||
69
- (firstArg.type === 'call' && containsStaticCalls(firstArg, imports)) ||
70
- (firstArg.type === 'parenthesized_expression' &&
71
- containsStaticCalls(firstArg, imports));
72
- if (hasStaticHelpers) {
73
- // Compound expression path: parse into StringNode tree
74
- const rootNode = callNode.tree?.rootNode;
75
- if (!rootNode) {
76
- errors.push(`${locationStr(callNode)}: could not access AST root`);
77
- return;
78
- }
79
- const stringNode = await parseStringExpression(firstArg, {
80
- rootNode,
81
- imports,
82
- filePath,
83
- errors,
84
- });
85
- if (!stringNode)
86
- return;
87
- const strings = nodeToStrings(stringNode).map(indexVars);
88
- if (strings.length === 0) {
89
- errors.push(`${locationStr(callNode)}: no string variants produced`);
90
- return;
91
- }
92
- // Extract static metadata and check for derive in _context
93
- const metadata = extractKwargs(argsNode, errors, callNode, imports);
94
- const contextVariants = await extractDeriveContext(argsNode, imports, filePath, rootNode, errors);
95
- const staticId = `static-temp-id-${randomUUID()}`;
96
- if (contextVariants) {
97
- // Cross-product: content variants × context variants
98
- for (const source of strings) {
99
- for (const context of contextVariants) {
100
- calls.push({
101
- source,
102
- ...metadata,
103
- context,
104
- staticId,
105
- line: callNode.startPosition.row + 1,
106
- column: callNode.startPosition.column,
107
- });
108
- }
109
- }
110
- }
111
- else {
112
- for (const source of strings) {
113
- calls.push({
114
- source,
115
- ...metadata,
116
- staticId,
117
- line: callNode.startPosition.row + 1,
118
- column: callNode.startPosition.column,
119
- });
120
- }
121
- }
122
- return;
123
- }
124
- // Simple path: validate first argument is a plain string literal
125
- if (firstArg.type !== 'string') {
126
- if (firstArg.type === 'identifier') {
127
- errors.push(`${locationStr(callNode)}: translation call uses a variable "${firstArg.text}" instead of a string literal`);
128
- }
129
- else if (firstArg.type === 'concatenated_string') {
130
- errors.push(`${locationStr(callNode)}: translation call uses concatenated strings — use a single string literal`);
131
- }
132
- else {
133
- errors.push(`${locationStr(callNode)}: translation call first argument must be a string literal, got "${firstArg.type}"`);
134
- }
135
- return;
136
- }
137
- // Check for f-strings (without declare_static/declare_var)
138
- if (isFString(firstArg)) {
139
- errors.push(`${locationStr(callNode)}: translation call uses an f-string — use a plain string literal or derive()/declare_var()`);
140
- return;
141
- }
142
- const source = extractStringContent(firstArg);
143
- if (source === undefined) {
144
- errors.push(`${locationStr(callNode)}: could not extract string content`);
145
- return;
146
- }
147
- // Extract keyword arguments and check for derive in _context
148
- const metadata = extractKwargs(argsNode, errors, callNode, imports);
149
- const rootNode = callNode.tree?.rootNode;
150
- const contextVariants = rootNode
151
- ? await extractDeriveContext(argsNode, imports, filePath, rootNode, errors)
152
- : null;
153
- if (contextVariants) {
154
- const staticId = `static-temp-id-${randomUUID()}`;
155
- for (const context of contextVariants) {
156
- calls.push({
157
- source,
158
- ...metadata,
159
- context,
160
- staticId,
161
- line: callNode.startPosition.row + 1,
162
- column: callNode.startPosition.column,
163
- });
164
- }
165
- }
166
- else {
167
- calls.push({
168
- source,
169
- ...metadata,
170
- line: callNode.startPosition.row + 1,
171
- column: callNode.startPosition.column,
172
- });
173
- }
39
+ var _callNode$tree2;
40
+ const argsNode = callNode.childForFieldName("arguments");
41
+ if (!argsNode) {
42
+ errors.push(`${locationStr(callNode)}: translation call has no arguments`);
43
+ return;
44
+ }
45
+ let firstArg = null;
46
+ for (let i = 0; i < argsNode.childCount; i++) {
47
+ const child = argsNode.child(i);
48
+ if (child && child.type !== "(" && child.type !== ")" && child.type !== "," && child.type !== "keyword_argument") {
49
+ firstArg = child;
50
+ break;
51
+ }
52
+ }
53
+ if (!firstArg) {
54
+ errors.push(`${locationStr(callNode)}: translation call has no positional argument`);
55
+ return;
56
+ }
57
+ if (firstArg.type === "string" && isFString(firstArg) && containsStaticCalls(firstArg, imports) || firstArg.type === "binary_operator" && containsStaticCalls(firstArg, imports) || firstArg.type === "call" && containsStaticCalls(firstArg, imports) || firstArg.type === "parenthesized_expression" && containsStaticCalls(firstArg, imports)) {
58
+ var _callNode$tree;
59
+ const rootNode = (_callNode$tree = callNode.tree) === null || _callNode$tree === void 0 ? void 0 : _callNode$tree.rootNode;
60
+ if (!rootNode) {
61
+ errors.push(`${locationStr(callNode)}: could not access AST root`);
62
+ return;
63
+ }
64
+ const stringNode = await parseStringExpression(firstArg, {
65
+ rootNode,
66
+ imports,
67
+ filePath,
68
+ errors
69
+ });
70
+ if (!stringNode) return;
71
+ const strings = nodeToStrings(stringNode).map(indexVars);
72
+ if (strings.length === 0) {
73
+ errors.push(`${locationStr(callNode)}: no string variants produced`);
74
+ return;
75
+ }
76
+ const metadata = extractKwargs(argsNode, errors, callNode, imports);
77
+ const contextVariants = await extractDeriveContext(argsNode, imports, filePath, rootNode, errors);
78
+ const staticId = `static-temp-id-${randomUUID()}`;
79
+ if (contextVariants) for (const source of strings) for (const context of contextVariants) calls.push({
80
+ source,
81
+ ...metadata,
82
+ context,
83
+ staticId,
84
+ line: callNode.startPosition.row + 1,
85
+ column: callNode.startPosition.column
86
+ });
87
+ else for (const source of strings) calls.push({
88
+ source,
89
+ ...metadata,
90
+ staticId,
91
+ line: callNode.startPosition.row + 1,
92
+ column: callNode.startPosition.column
93
+ });
94
+ return;
95
+ }
96
+ if (firstArg.type !== "string") {
97
+ if (firstArg.type === "identifier") errors.push(`${locationStr(callNode)}: translation call uses a variable "${firstArg.text}" instead of a string literal`);
98
+ else if (firstArg.type === "concatenated_string") errors.push(`${locationStr(callNode)}: translation call uses concatenated strings — use a single string literal`);
99
+ else errors.push(`${locationStr(callNode)}: translation call first argument must be a string literal, got "${firstArg.type}"`);
100
+ return;
101
+ }
102
+ if (isFString(firstArg)) {
103
+ errors.push(`${locationStr(callNode)}: translation call uses an f-string — use a plain string literal or derive()/declare_var()`);
104
+ return;
105
+ }
106
+ const source = extractStringContent(firstArg);
107
+ if (source === void 0) {
108
+ errors.push(`${locationStr(callNode)}: could not extract string content`);
109
+ return;
110
+ }
111
+ const metadata = extractKwargs(argsNode, errors, callNode, imports);
112
+ const rootNode = (_callNode$tree2 = callNode.tree) === null || _callNode$tree2 === void 0 ? void 0 : _callNode$tree2.rootNode;
113
+ const contextVariants = rootNode ? await extractDeriveContext(argsNode, imports, filePath, rootNode, errors) : null;
114
+ if (contextVariants) {
115
+ const staticId = `static-temp-id-${randomUUID()}`;
116
+ for (const context of contextVariants) calls.push({
117
+ source,
118
+ ...metadata,
119
+ context,
120
+ staticId,
121
+ line: callNode.startPosition.row + 1,
122
+ column: callNode.startPosition.column
123
+ });
124
+ } else calls.push({
125
+ source,
126
+ ...metadata,
127
+ line: callNode.startPosition.row + 1,
128
+ column: callNode.startPosition.column
129
+ });
174
130
  }
175
131
  function extractKwargs(argsNode, errors, callNode, imports) {
176
- const result = {};
177
- for (let i = 0; i < argsNode.childCount; i++) {
178
- const child = argsNode.child(i);
179
- if (!child || child.type !== 'keyword_argument')
180
- continue;
181
- const nameNode = child.childForFieldName('name');
182
- const valueNode = child.childForFieldName('value');
183
- if (!nameNode || !valueNode)
184
- continue;
185
- const kwargName = nameNode.text;
186
- const metadataKey = PYTHON_METADATA_KWARGS[kwargName];
187
- if (!metadataKey)
188
- continue;
189
- if (metadataKey === 'maxChars') {
190
- if (valueNode.type === 'integer') {
191
- result.maxChars = parseInt(valueNode.text, 10);
192
- }
193
- else {
194
- errors.push(`${locationStr(callNode)}: _max_chars must be an integer literal`);
195
- }
196
- }
197
- else {
198
- if (valueNode.type === 'string' && !isFString(valueNode)) {
199
- const value = extractStringContent(valueNode);
200
- if (value !== undefined) {
201
- if (metadataKey === 'id')
202
- result.id = value;
203
- else if (metadataKey === 'context')
204
- result.context = value;
205
- }
206
- }
207
- else if (metadataKey === 'context' &&
208
- imports &&
209
- containsStaticCalls(valueNode, imports)) {
210
- // _context contains derive() — skip error, caller handles derivation
211
- }
212
- else {
213
- errors.push(`${locationStr(callNode)}: _${metadataKey} must be a string literal`);
214
- }
215
- }
216
- }
217
- return result;
132
+ const result = {};
133
+ for (let i = 0; i < argsNode.childCount; i++) {
134
+ const child = argsNode.child(i);
135
+ if (!child || child.type !== "keyword_argument") continue;
136
+ const nameNode = child.childForFieldName("name");
137
+ const valueNode = child.childForFieldName("value");
138
+ if (!nameNode || !valueNode) continue;
139
+ const metadataKey = PYTHON_METADATA_KWARGS[nameNode.text];
140
+ if (!metadataKey) continue;
141
+ if (metadataKey === "maxChars") if (valueNode.type === "integer") result.maxChars = parseInt(valueNode.text, 10);
142
+ else errors.push(`${locationStr(callNode)}: _max_chars must be an integer literal`);
143
+ else if (valueNode.type === "string" && !isFString(valueNode)) {
144
+ const value = extractStringContent(valueNode);
145
+ if (value !== void 0) {
146
+ if (metadataKey === "id") result.id = value;
147
+ else if (metadataKey === "context") result.context = value;
148
+ }
149
+ } else if (metadataKey === "context" && imports && containsStaticCalls(valueNode, imports)) {} else errors.push(`${locationStr(callNode)}: _${metadataKey} must be a string literal`);
150
+ }
151
+ return result;
218
152
  }
219
153
  /**
220
- * Finds the _context keyword argument node and, if it contains a derive() call,
221
- * parses it into context variants. Returns null if _context is static or absent.
222
- */
154
+ * Finds the _context keyword argument node and, if it contains a derive() call,
155
+ * parses it into context variants. Returns null if _context is static or absent.
156
+ */
223
157
  async function extractDeriveContext(argsNode, imports, filePath, rootNode, errors) {
224
- // Find _context kwarg
225
- for (let i = 0; i < argsNode.childCount; i++) {
226
- const child = argsNode.child(i);
227
- if (!child || child.type !== 'keyword_argument')
228
- continue;
229
- const nameNode = child.childForFieldName('name');
230
- const valueNode = child.childForFieldName('value');
231
- if (!nameNode || !valueNode)
232
- continue;
233
- if (nameNode.text !== '_context')
234
- continue;
235
- // Check if value contains derive()
236
- if (!containsStaticCalls(valueNode, imports))
237
- return null;
238
- const contextNode = await parseStringExpression(valueNode, {
239
- rootNode,
240
- imports,
241
- filePath,
242
- errors,
243
- });
244
- if (!contextNode)
245
- return null;
246
- return nodeToStrings(contextNode);
247
- }
248
- return null;
158
+ for (let i = 0; i < argsNode.childCount; i++) {
159
+ const child = argsNode.child(i);
160
+ if (!child || child.type !== "keyword_argument") continue;
161
+ const nameNode = child.childForFieldName("name");
162
+ const valueNode = child.childForFieldName("value");
163
+ if (!nameNode || !valueNode) continue;
164
+ if (nameNode.text !== "_context") continue;
165
+ if (!containsStaticCalls(valueNode, imports)) return null;
166
+ const contextNode = await parseStringExpression(valueNode, {
167
+ rootNode,
168
+ imports,
169
+ filePath,
170
+ errors
171
+ });
172
+ if (!contextNode) return null;
173
+ return nodeToStrings(contextNode);
174
+ }
175
+ return null;
249
176
  }
250
177
  function isFString(stringNode) {
251
- // Check if string_start begins with 'f' or 'F'
252
- for (let i = 0; i < stringNode.childCount; i++) {
253
- const child = stringNode.child(i);
254
- if (child && child.type === 'string_start') {
255
- return /^[fF]/.test(child.text);
256
- }
257
- // Also check for interpolation children (hallmark of f-strings)
258
- if (child && child.type === 'interpolation') {
259
- return true;
260
- }
261
- }
262
- return false;
178
+ for (let i = 0; i < stringNode.childCount; i++) {
179
+ const child = stringNode.child(i);
180
+ if (child && child.type === "string_start") return /^[fF]/.test(child.text);
181
+ if (child && child.type === "interpolation") return true;
182
+ }
183
+ return false;
263
184
  }
264
185
  function extractStringContent(stringNode) {
265
- // Look for string_content child
266
- for (let i = 0; i < stringNode.childCount; i++) {
267
- const child = stringNode.child(i);
268
- if (child && child.type === 'string_content') {
269
- return child.text;
270
- }
271
- }
272
- // Empty string — no string_content child, but has string_start and string_end
273
- let hasStart = false;
274
- let hasEnd = false;
275
- for (let i = 0; i < stringNode.childCount; i++) {
276
- const child = stringNode.child(i);
277
- if (child?.type === 'string_start')
278
- hasStart = true;
279
- if (child?.type === 'string_end')
280
- hasEnd = true;
281
- }
282
- if (hasStart && hasEnd)
283
- return '';
284
- return undefined;
186
+ for (let i = 0; i < stringNode.childCount; i++) {
187
+ const child = stringNode.child(i);
188
+ if (child && child.type === "string_content") return child.text;
189
+ }
190
+ let hasStart = false;
191
+ let hasEnd = false;
192
+ for (let i = 0; i < stringNode.childCount; i++) {
193
+ const child = stringNode.child(i);
194
+ if ((child === null || child === void 0 ? void 0 : child.type) === "string_start") hasStart = true;
195
+ if ((child === null || child === void 0 ? void 0 : child.type) === "string_end") hasEnd = true;
196
+ }
197
+ if (hasStart && hasEnd) return "";
285
198
  }
286
199
  function locationStr(node) {
287
- return `line ${node.startPosition.row + 1}, col ${node.startPosition.column}`;
200
+ return `line ${node.startPosition.row + 1}, col ${node.startPosition.column}`;
288
201
  }
202
+ //#endregion
203
+ export { extractCalls };
204
+
205
+ //# sourceMappingURL=extractCalls.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"extractCalls.js","names":[],"sources":["../src/extractCalls.ts"],"sourcesContent":["import type { SyntaxNode } from './parser.js';\nimport type { ImportAlias } from './extractImports.js';\nimport {\n PYTHON_METADATA_KWARGS,\n PYTHON_DERIVE,\n PYTHON_DECLARE_STATIC,\n PYTHON_DECLARE_VAR,\n} from './constants.js';\nimport {\n containsStaticCalls,\n parseStringExpression,\n} from './parseStringExpression.js';\nimport { nodeToStrings } from './stringNode.js';\nimport { indexVars } from 'generaltranslation/internal';\nimport { randomUUID } from 'node:crypto';\n\nexport type RawTranslationCall = {\n source: string;\n id?: string;\n context?: string;\n maxChars?: number;\n staticId?: string;\n line: number;\n column: number;\n};\n\n/**\n * Extracts translation function calls from a Python AST.\n * Walks all `call` nodes and checks if they reference a tracked import.\n */\nexport async function extractCalls(\n rootNode: SyntaxNode,\n imports: ImportAlias[],\n filePath: string\n): Promise<{\n calls: RawTranslationCall[];\n errors: string[];\n warnings: string[];\n}> {\n const calls: RawTranslationCall[] = [];\n const errors: string[] = [];\n const warnings: string[] = [];\n\n // Only track t/msg as translation functions (not derive/declare_static/declare_var)\n const trackedNames = new Set(\n imports\n .filter(\n (imp) =>\n imp.originalName !== PYTHON_DERIVE &&\n imp.originalName !== PYTHON_DECLARE_STATIC &&\n imp.originalName !== PYTHON_DECLARE_VAR\n )\n .map((imp) => imp.localName)\n );\n if (trackedNames.size === 0) return { calls, errors, warnings };\n\n await walkCalls(\n rootNode,\n trackedNames,\n imports,\n filePath,\n calls,\n errors,\n warnings\n );\n\n return { calls, errors, warnings };\n}\n\nasync function walkCalls(\n node: SyntaxNode,\n trackedNames: Set<string>,\n imports: ImportAlias[],\n filePath: string,\n calls: RawTranslationCall[],\n errors: string[],\n warnings: string[]\n): Promise<void> {\n if (node.type === 'call') {\n const funcNode = node.childForFieldName('function');\n if (\n funcNode &&\n funcNode.type === 'identifier' &&\n trackedNames.has(funcNode.text)\n ) {\n await processCall(node, imports, filePath, calls, errors, warnings);\n }\n }\n\n for (let i = 0; i < node.childCount; i++) {\n const child = node.child(i);\n if (child)\n await walkCalls(\n child,\n trackedNames,\n imports,\n filePath,\n calls,\n errors,\n warnings\n );\n }\n}\n\nasync function processCall(\n callNode: SyntaxNode,\n imports: ImportAlias[],\n filePath: string,\n calls: RawTranslationCall[],\n errors: string[],\n _warnings: string[]\n): Promise<void> {\n const argsNode = callNode.childForFieldName('arguments');\n if (!argsNode) {\n errors.push(`${locationStr(callNode)}: translation call has no arguments`);\n return;\n }\n\n // Find first positional argument (skip punctuation)\n let firstArg: SyntaxNode | null = null;\n for (let i = 0; i < argsNode.childCount; i++) {\n const child = argsNode.child(i);\n if (\n child &&\n child.type !== '(' &&\n child.type !== ')' &&\n child.type !== ',' &&\n child.type !== 'keyword_argument'\n ) {\n firstArg = child;\n break;\n }\n }\n\n if (!firstArg) {\n errors.push(\n `${locationStr(callNode)}: translation call has no positional argument`\n );\n return;\n }\n\n // Check if this expression contains declare_static/declare_var\n const hasStaticHelpers =\n (firstArg.type === 'string' &&\n isFString(firstArg) &&\n containsStaticCalls(firstArg, imports)) ||\n (firstArg.type === 'binary_operator' &&\n containsStaticCalls(firstArg, imports)) ||\n (firstArg.type === 'call' && containsStaticCalls(firstArg, imports)) ||\n (firstArg.type === 'parenthesized_expression' &&\n containsStaticCalls(firstArg, imports));\n\n if (hasStaticHelpers) {\n // Compound expression path: parse into StringNode tree\n const rootNode = callNode.tree?.rootNode;\n if (!rootNode) {\n errors.push(`${locationStr(callNode)}: could not access AST root`);\n return;\n }\n\n const stringNode = await parseStringExpression(firstArg, {\n rootNode,\n imports,\n filePath,\n errors,\n });\n\n if (!stringNode) return;\n\n const strings = nodeToStrings(stringNode).map(indexVars);\n if (strings.length === 0) {\n errors.push(`${locationStr(callNode)}: no string variants produced`);\n return;\n }\n\n // Extract static metadata and check for derive in _context\n const metadata = extractKwargs(argsNode, errors, callNode, imports);\n const contextVariants = await extractDeriveContext(\n argsNode,\n imports,\n filePath,\n rootNode,\n errors\n );\n\n const staticId = `static-temp-id-${randomUUID()}`;\n\n if (contextVariants) {\n // Cross-product: content variants × context variants\n for (const source of strings) {\n for (const context of contextVariants) {\n calls.push({\n source,\n ...metadata,\n context,\n staticId,\n line: callNode.startPosition.row + 1,\n column: callNode.startPosition.column,\n });\n }\n }\n } else {\n for (const source of strings) {\n calls.push({\n source,\n ...metadata,\n staticId,\n line: callNode.startPosition.row + 1,\n column: callNode.startPosition.column,\n });\n }\n }\n return;\n }\n\n // Simple path: validate first argument is a plain string literal\n if (firstArg.type !== 'string') {\n if (firstArg.type === 'identifier') {\n errors.push(\n `${locationStr(callNode)}: translation call uses a variable \"${firstArg.text}\" instead of a string literal`\n );\n } else if (firstArg.type === 'concatenated_string') {\n errors.push(\n `${locationStr(callNode)}: translation call uses concatenated strings — use a single string literal`\n );\n } else {\n errors.push(\n `${locationStr(callNode)}: translation call first argument must be a string literal, got \"${firstArg.type}\"`\n );\n }\n return;\n }\n\n // Check for f-strings (without declare_static/declare_var)\n if (isFString(firstArg)) {\n errors.push(\n `${locationStr(callNode)}: translation call uses an f-string — use a plain string literal or derive()/declare_var()`\n );\n return;\n }\n\n const source = extractStringContent(firstArg);\n if (source === undefined) {\n errors.push(`${locationStr(callNode)}: could not extract string content`);\n return;\n }\n\n // Extract keyword arguments and check for derive in _context\n const metadata = extractKwargs(argsNode, errors, callNode, imports);\n\n const rootNode = callNode.tree?.rootNode;\n const contextVariants = rootNode\n ? await extractDeriveContext(argsNode, imports, filePath, rootNode, errors)\n : null;\n\n if (contextVariants) {\n const staticId = `static-temp-id-${randomUUID()}`;\n for (const context of contextVariants) {\n calls.push({\n source,\n ...metadata,\n context,\n staticId,\n line: callNode.startPosition.row + 1,\n column: callNode.startPosition.column,\n });\n }\n } else {\n calls.push({\n source,\n ...metadata,\n line: callNode.startPosition.row + 1,\n column: callNode.startPosition.column,\n });\n }\n}\n\nfunction extractKwargs(\n argsNode: SyntaxNode,\n errors: string[],\n callNode: SyntaxNode,\n imports?: ImportAlias[]\n): { id?: string; context?: string; maxChars?: number } {\n const result: { id?: string; context?: string; maxChars?: number } = {};\n\n for (let i = 0; i < argsNode.childCount; i++) {\n const child = argsNode.child(i);\n if (!child || child.type !== 'keyword_argument') continue;\n\n const nameNode = child.childForFieldName('name');\n const valueNode = child.childForFieldName('value');\n if (!nameNode || !valueNode) continue;\n\n const kwargName = nameNode.text;\n const metadataKey = (\n PYTHON_METADATA_KWARGS as Record<string, string | undefined>\n )[kwargName];\n if (!metadataKey) continue;\n\n if (metadataKey === 'maxChars') {\n if (valueNode.type === 'integer') {\n result.maxChars = parseInt(valueNode.text, 10);\n } else {\n errors.push(\n `${locationStr(callNode)}: _max_chars must be an integer literal`\n );\n }\n } else {\n if (valueNode.type === 'string' && !isFString(valueNode)) {\n const value = extractStringContent(valueNode);\n if (value !== undefined) {\n if (metadataKey === 'id') result.id = value;\n else if (metadataKey === 'context') result.context = value;\n }\n } else if (\n metadataKey === 'context' &&\n imports &&\n containsStaticCalls(valueNode, imports)\n ) {\n // _context contains derive() — skip error, caller handles derivation\n } else {\n errors.push(\n `${locationStr(callNode)}: _${metadataKey} must be a string literal`\n );\n }\n }\n }\n\n return result;\n}\n\n/**\n * Finds the _context keyword argument node and, if it contains a derive() call,\n * parses it into context variants. Returns null if _context is static or absent.\n */\nasync function extractDeriveContext(\n argsNode: SyntaxNode,\n imports: ImportAlias[],\n filePath: string,\n rootNode: SyntaxNode,\n errors: string[]\n): Promise<string[] | null> {\n // Find _context kwarg\n for (let i = 0; i < argsNode.childCount; i++) {\n const child = argsNode.child(i);\n if (!child || child.type !== 'keyword_argument') continue;\n\n const nameNode = child.childForFieldName('name');\n const valueNode = child.childForFieldName('value');\n if (!nameNode || !valueNode) continue;\n if (nameNode.text !== '_context') continue;\n\n // Check if value contains derive()\n if (!containsStaticCalls(valueNode, imports)) return null;\n\n const contextNode = await parseStringExpression(valueNode, {\n rootNode,\n imports,\n filePath,\n errors,\n });\n if (!contextNode) return null;\n\n return nodeToStrings(contextNode);\n }\n return null;\n}\n\nfunction isFString(stringNode: SyntaxNode): boolean {\n // Check if string_start begins with 'f' or 'F'\n for (let i = 0; i < stringNode.childCount; i++) {\n const child = stringNode.child(i);\n if (child && child.type === 'string_start') {\n return /^[fF]/.test(child.text);\n }\n // Also check for interpolation children (hallmark of f-strings)\n if (child && child.type === 'interpolation') {\n return true;\n }\n }\n return false;\n}\n\nfunction extractStringContent(stringNode: SyntaxNode): string | undefined {\n // Look for string_content child\n for (let i = 0; i < stringNode.childCount; i++) {\n const child = stringNode.child(i);\n if (child && child.type === 'string_content') {\n return child.text;\n }\n }\n\n // Empty string — no string_content child, but has string_start and string_end\n let hasStart = false;\n let hasEnd = false;\n for (let i = 0; i < stringNode.childCount; i++) {\n const child = stringNode.child(i);\n if (child?.type === 'string_start') hasStart = true;\n if (child?.type === 'string_end') hasEnd = true;\n }\n if (hasStart && hasEnd) return '';\n\n return undefined;\n}\n\nfunction locationStr(node: SyntaxNode): string {\n return `line ${node.startPosition.row + 1}, col ${node.startPosition.column}`;\n}\n"],"mappings":"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"}