@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.
@@ -1,1064 +1,803 @@
1
- import fs from 'node:fs';
2
- import { getParser } from './parser.js';
3
- import { PYTHON_DERIVE, PYTHON_DECLARE_STATIC, PYTHON_DECLARE_VAR, } from './constants.js';
4
- import { resolveFunctionInCurrentFile, resolveFunctionInFile, } from './resolveFunctionVariants.js';
5
- import { extractImports } from './extractImports.js';
6
- import { resolveImportPath } from './resolveImport.js';
7
- import { declareVar } from 'generaltranslation/internal';
1
+ import "./constants.js";
2
+ import { getParser } from "./parser.js";
3
+ import { resolveImportPath } from "./resolveImport.js";
4
+ import { resolveFunctionInCurrentFile, resolveFunctionInFile } from "./resolveFunctionVariants.js";
5
+ import { extractImports } from "./extractImports.js";
6
+ import fs from "node:fs";
7
+ import { declareVar } from "generaltranslation/internal";
8
+ //#region src/parseStringExpression.ts
8
9
  /**
9
- * Returns true if the original import name is derive() or declare_static() (deprecated).
10
- */
10
+ * Returns true if the original import name is derive() or declare_static() (deprecated).
11
+ */
11
12
  function isDeriveFunction(originalName) {
12
- return (originalName === PYTHON_DERIVE || originalName === PYTHON_DECLARE_STATIC);
13
+ return originalName === "derive" || originalName === "declare_static";
13
14
  }
14
15
  /**
15
- * Checks if an expression contains derive/declare_static or declare_var calls.
16
- */
17
- export function containsStaticCalls(node, imports) {
18
- const staticNames = getDeriveImportNames(imports);
19
- if (staticNames.size === 0)
20
- return false;
21
- return hasDeriveCallRecursive(node, staticNames);
16
+ * Checks if an expression contains derive/declare_static or declare_var calls.
17
+ */
18
+ function containsStaticCalls(node, imports) {
19
+ const staticNames = getDeriveImportNames(imports);
20
+ if (staticNames.size === 0) return false;
21
+ return hasDeriveCallRecursive(node, staticNames);
22
22
  }
23
23
  function hasDeriveCallRecursive(node, names) {
24
- if (node.type === 'call') {
25
- const funcNode = node.childForFieldName('function');
26
- if (funcNode &&
27
- funcNode.type === 'identifier' &&
28
- names.has(funcNode.text)) {
29
- return true;
30
- }
31
- }
32
- for (let i = 0; i < node.childCount; i++) {
33
- const child = node.child(i);
34
- if (child && hasDeriveCallRecursive(child, names))
35
- return true;
36
- }
37
- return false;
24
+ if (node.type === "call") {
25
+ const funcNode = node.childForFieldName("function");
26
+ if (funcNode && funcNode.type === "identifier" && names.has(funcNode.text)) return true;
27
+ }
28
+ for (let i = 0; i < node.childCount; i++) {
29
+ const child = node.child(i);
30
+ if (child && hasDeriveCallRecursive(child, names)) return true;
31
+ }
32
+ return false;
38
33
  }
39
34
  /**
40
- * Parses the first argument of t() into a StringNode tree.
41
- * Handles: plain strings, f-strings with derive/declare_var,
42
- * binary + concatenation, and standalone derive calls.
43
- */
44
- export async function parseStringExpression(node, ctx) {
45
- // Parenthesized expression: unwrap and recurse
46
- if (node.type === 'parenthesized_expression') {
47
- for (let i = 0; i < node.childCount; i++) {
48
- const child = node.child(i);
49
- if (child && child.type !== '(' && child.type !== ')') {
50
- return parseStringExpression(child, ctx);
51
- }
52
- }
53
- return null;
54
- }
55
- // Plain string (no f-string)
56
- if (node.type === 'string' && !isFString(node)) {
57
- const content = extractStringContent(node);
58
- if (content === undefined)
59
- return null;
60
- return { type: 'text', text: content };
61
- }
62
- // F-string with interpolations
63
- if (node.type === 'string' && isFString(node)) {
64
- return parseFString(node, ctx);
65
- }
66
- // Binary operator: string concatenation with +
67
- if (node.type === 'binary_operator') {
68
- return parseBinaryOperator(node, ctx);
69
- }
70
- // Standalone call: derive/declare_static(...)
71
- if (node.type === 'call') {
72
- const funcNode = node.childForFieldName('function');
73
- if (funcNode && funcNode.type === 'identifier') {
74
- const originalName = getOriginalImportName(funcNode.text, ctx.imports);
75
- if (isDeriveFunction(originalName)) {
76
- return resolveDeclareStaticArg(node, ctx);
77
- }
78
- if (originalName === PYTHON_DECLARE_VAR) {
79
- return resolveDeclareVarArg(node, ctx);
80
- }
81
- }
82
- }
83
- ctx.errors.push(`${locationStr(node)}: unsupported expression type "${node.type}" in translation call`);
84
- return null;
35
+ * Parses the first argument of t() into a StringNode tree.
36
+ * Handles: plain strings, f-strings with derive/declare_var,
37
+ * binary + concatenation, and standalone derive calls.
38
+ */
39
+ async function parseStringExpression(node, ctx) {
40
+ if (node.type === "parenthesized_expression") {
41
+ for (let i = 0; i < node.childCount; i++) {
42
+ const child = node.child(i);
43
+ if (child && child.type !== "(" && child.type !== ")") return parseStringExpression(child, ctx);
44
+ }
45
+ return null;
46
+ }
47
+ if (node.type === "string" && !isFString(node)) {
48
+ const content = extractStringContent(node);
49
+ if (content === void 0) return null;
50
+ return {
51
+ type: "text",
52
+ text: content
53
+ };
54
+ }
55
+ if (node.type === "string" && isFString(node)) return parseFString(node, ctx);
56
+ if (node.type === "binary_operator") return parseBinaryOperator(node, ctx);
57
+ if (node.type === "call") {
58
+ const funcNode = node.childForFieldName("function");
59
+ if (funcNode && funcNode.type === "identifier") {
60
+ const originalName = getOriginalImportName(funcNode.text, ctx.imports);
61
+ if (isDeriveFunction(originalName)) return resolveDeclareStaticArg(node, ctx);
62
+ if (originalName === "declare_var") return resolveDeclareVarArg(node, ctx);
63
+ }
64
+ }
65
+ ctx.errors.push(`${locationStr(node)}: unsupported expression type "${node.type}" in translation call`);
66
+ return null;
85
67
  }
86
68
  /**
87
- * Parses an f-string into a StringNode tree.
88
- * string_content → text nodes, interpolation → check for derive/declare_var
89
- */
69
+ * Parses an f-string into a StringNode tree.
70
+ * string_content → text nodes, interpolation → check for derive/declare_var
71
+ */
90
72
  async function parseFString(node, ctx) {
91
- const parts = [];
92
- for (let i = 0; i < node.childCount; i++) {
93
- const child = node.child(i);
94
- if (!child)
95
- continue;
96
- if (child.type === 'string_content') {
97
- if (child.text.length > 0) {
98
- parts.push({ type: 'text', text: child.text });
99
- }
100
- continue;
101
- }
102
- if (child.type === 'interpolation') {
103
- const result = await parseInterpolation(child, ctx);
104
- if (result) {
105
- parts.push(result);
106
- }
107
- continue;
108
- }
109
- // Skip string_start, string_end, etc.
110
- }
111
- if (parts.length === 0)
112
- return { type: 'text', text: '' };
113
- if (parts.length === 1)
114
- return parts[0];
115
- return { type: 'sequence', nodes: parts };
73
+ const parts = [];
74
+ for (let i = 0; i < node.childCount; i++) {
75
+ const child = node.child(i);
76
+ if (!child) continue;
77
+ if (child.type === "string_content") {
78
+ if (child.text.length > 0) parts.push({
79
+ type: "text",
80
+ text: child.text
81
+ });
82
+ continue;
83
+ }
84
+ if (child.type === "interpolation") {
85
+ const result = await parseInterpolation(child, ctx);
86
+ if (result) parts.push(result);
87
+ continue;
88
+ }
89
+ }
90
+ if (parts.length === 0) return {
91
+ type: "text",
92
+ text: ""
93
+ };
94
+ if (parts.length === 1) return parts[0];
95
+ return {
96
+ type: "sequence",
97
+ nodes: parts
98
+ };
116
99
  }
117
100
  /**
118
- * Parses an interpolation within an f-string.
119
- * Must be a derive() or declare_var() call.
120
- */
101
+ * Parses an interpolation within an f-string.
102
+ * Must be a derive() or declare_var() call.
103
+ */
121
104
  async function parseInterpolation(interpNode, ctx) {
122
- // Find the expression inside the interpolation (skip { and })
123
- let expr = null;
124
- for (let i = 0; i < interpNode.childCount; i++) {
125
- const child = interpNode.child(i);
126
- if (child &&
127
- child.type !== '{' &&
128
- child.type !== '}' &&
129
- child.type !== 'type_conversion' &&
130
- child.type !== 'format_specifier') {
131
- expr = child;
132
- break;
133
- }
134
- }
135
- if (!expr) {
136
- ctx.errors.push(`${locationStr(interpNode)}: empty interpolation in f-string`);
137
- return null;
138
- }
139
- if (expr.type === 'call') {
140
- const funcNode = expr.childForFieldName('function');
141
- if (funcNode && funcNode.type === 'identifier') {
142
- const originalName = getOriginalImportName(funcNode.text, ctx.imports);
143
- if (isDeriveFunction(originalName)) {
144
- return resolveDeclareStaticArg(expr, ctx);
145
- }
146
- if (originalName === PYTHON_DECLARE_VAR) {
147
- return resolveDeclareVarArg(expr, ctx);
148
- }
149
- }
150
- }
151
- // Not a derive/declare_var call — error
152
- ctx.errors.push(`${locationStr(interpNode)}: f-string interpolation must use derive() or declare_var(), got "${expr.text}"`);
153
- return null;
105
+ let expr = null;
106
+ for (let i = 0; i < interpNode.childCount; i++) {
107
+ const child = interpNode.child(i);
108
+ if (child && child.type !== "{" && child.type !== "}" && child.type !== "type_conversion" && child.type !== "format_specifier") {
109
+ expr = child;
110
+ break;
111
+ }
112
+ }
113
+ if (!expr) {
114
+ ctx.errors.push(`${locationStr(interpNode)}: empty interpolation in f-string`);
115
+ return null;
116
+ }
117
+ if (expr.type === "call") {
118
+ const funcNode = expr.childForFieldName("function");
119
+ if (funcNode && funcNode.type === "identifier") {
120
+ const originalName = getOriginalImportName(funcNode.text, ctx.imports);
121
+ if (isDeriveFunction(originalName)) return resolveDeclareStaticArg(expr, ctx);
122
+ if (originalName === "declare_var") return resolveDeclareVarArg(expr, ctx);
123
+ }
124
+ }
125
+ ctx.errors.push(`${locationStr(interpNode)}: f-string interpolation must use derive() or declare_var(), got "${expr.text}"`);
126
+ return null;
154
127
  }
155
128
  /**
156
- * Parses binary + concatenation into a sequence node.
157
- */
129
+ * Parses binary + concatenation into a sequence node.
130
+ */
158
131
  async function parseBinaryOperator(node, ctx) {
159
- const left = node.childForFieldName('left');
160
- const operator = node.childForFieldName('operator');
161
- const right = node.childForFieldName('right');
162
- if (!left || !right) {
163
- ctx.errors.push(`${locationStr(node)}: binary operator missing operands`);
164
- return null;
165
- }
166
- // Verify it's a + operator
167
- if (operator && operator.text !== '+') {
168
- ctx.errors.push(`${locationStr(node)}: unsupported binary operator "${operator.text}" in translation call`);
169
- return null;
170
- }
171
- const leftNode = await parseStringExpression(left, ctx);
172
- const rightNode = await parseStringExpression(right, ctx);
173
- if (!leftNode || !rightNode)
174
- return null;
175
- // Flatten nested sequences
176
- const parts = [];
177
- if (leftNode.type === 'sequence') {
178
- parts.push(...leftNode.nodes);
179
- }
180
- else {
181
- parts.push(leftNode);
182
- }
183
- if (rightNode.type === 'sequence') {
184
- parts.push(...rightNode.nodes);
185
- }
186
- else {
187
- parts.push(rightNode);
188
- }
189
- return { type: 'sequence', nodes: parts };
132
+ const left = node.childForFieldName("left");
133
+ const operator = node.childForFieldName("operator");
134
+ const right = node.childForFieldName("right");
135
+ if (!left || !right) {
136
+ ctx.errors.push(`${locationStr(node)}: binary operator missing operands`);
137
+ return null;
138
+ }
139
+ if (operator && operator.text !== "+") {
140
+ ctx.errors.push(`${locationStr(node)}: unsupported binary operator "${operator.text}" in translation call`);
141
+ return null;
142
+ }
143
+ const leftNode = await parseStringExpression(left, ctx);
144
+ const rightNode = await parseStringExpression(right, ctx);
145
+ if (!leftNode || !rightNode) return null;
146
+ const parts = [];
147
+ if (leftNode.type === "sequence") parts.push(...leftNode.nodes);
148
+ else parts.push(leftNode);
149
+ if (rightNode.type === "sequence") parts.push(...rightNode.nodes);
150
+ else parts.push(rightNode);
151
+ return {
152
+ type: "sequence",
153
+ nodes: parts
154
+ };
190
155
  }
191
156
  /**
192
- * Resolves the argument of a derive() call into a StringNode.
193
- * Handles: string literals, ternary expressions, function calls.
194
- */
157
+ * Resolves the argument of a derive() call into a StringNode.
158
+ * Handles: string literals, ternary expressions, function calls.
159
+ */
195
160
  async function resolveDeclareStaticArg(callNode, ctx) {
196
- const arg = getFirstPositionalArg(callNode);
197
- if (!arg) {
198
- ctx.errors.push(`${locationStr(callNode)}: derive() / declare_static() requires an argument`);
199
- return null;
200
- }
201
- return resolveStaticValue(arg, ctx);
161
+ const arg = getFirstPositionalArg(callNode);
162
+ if (!arg) {
163
+ ctx.errors.push(`${locationStr(callNode)}: derive() / declare_static() requires an argument`);
164
+ return null;
165
+ }
166
+ return resolveStaticValue(arg, ctx);
202
167
  }
203
168
  /**
204
- * Resolves a value expression that should produce string variants.
205
- * Handles: string literals, ternary, function calls, binary concat,
206
- * and declare_var() calls (nested inside derive).
207
- */
169
+ * Resolves a value expression that should produce string variants.
170
+ * Handles: string literals, ternary, function calls, binary concat,
171
+ * and declare_var() calls (nested inside derive).
172
+ */
208
173
  async function resolveStaticValue(node, ctx) {
209
- // Parenthesized expression: unwrap and recurse
210
- if (node.type === 'parenthesized_expression') {
211
- for (let i = 0; i < node.childCount; i++) {
212
- const child = node.child(i);
213
- if (child && child.type !== '(' && child.type !== ')') {
214
- return resolveStaticValue(child, ctx);
215
- }
216
- }
217
- return null;
218
- }
219
- // String literal
220
- if (node.type === 'string' && !isFString(node)) {
221
- const content = extractStringContent(node);
222
- if (content === undefined)
223
- return null;
224
- return { type: 'text', text: content };
225
- }
226
- // Ternary / conditional expression: "day" if cond else "night"
227
- if (node.type === 'conditional_expression') {
228
- return resolveConditional(node, ctx);
229
- }
230
- // Binary operator: string concatenation with +
231
- if (node.type === 'binary_operator') {
232
- return resolveStaticBinaryOperator(node, ctx);
233
- }
234
- // Function call could be a user function or declare_var()
235
- if (node.type === 'call') {
236
- const funcNode = node.childForFieldName('function');
237
- if (funcNode && funcNode.type === 'identifier') {
238
- const originalName = getOriginalImportName(funcNode.text, ctx.imports);
239
- if (originalName === PYTHON_DECLARE_VAR) {
240
- return resolveDeclareVarArg(node, ctx);
241
- }
242
- }
243
- return resolveFunctionCall(node, ctx);
244
- }
245
- // Identifier: resolve to its assigned value
246
- if (node.type === 'identifier') {
247
- const result = await resolveIdentifier(node, ctx);
248
- if (result)
249
- return result;
250
- ctx.errors.push(`${locationStr(node)}: could not resolve identifier "${node.text}" to a static value`);
251
- return null;
252
- }
253
- // Subscript: dictionary access like LABELS[score] — returns all values as choices
254
- if (node.type === 'subscript') {
255
- return resolveSubscript(node, ctx);
256
- }
257
- // Attribute: dictionary access like obj.attr — returns the specific value
258
- if (node.type === 'attribute') {
259
- return resolveAttribute(node, ctx);
260
- }
261
- ctx.errors.push(`${locationStr(node)}: unsupported derive() argument type "${node.type}"`);
262
- return null;
174
+ if (node.type === "parenthesized_expression") {
175
+ for (let i = 0; i < node.childCount; i++) {
176
+ const child = node.child(i);
177
+ if (child && child.type !== "(" && child.type !== ")") return resolveStaticValue(child, ctx);
178
+ }
179
+ return null;
180
+ }
181
+ if (node.type === "string" && !isFString(node)) {
182
+ const content = extractStringContent(node);
183
+ if (content === void 0) return null;
184
+ return {
185
+ type: "text",
186
+ text: content
187
+ };
188
+ }
189
+ if (node.type === "conditional_expression") return resolveConditional(node, ctx);
190
+ if (node.type === "binary_operator") return resolveStaticBinaryOperator(node, ctx);
191
+ if (node.type === "call") {
192
+ const funcNode = node.childForFieldName("function");
193
+ if (funcNode && funcNode.type === "identifier") {
194
+ if (getOriginalImportName(funcNode.text, ctx.imports) === "declare_var") return resolveDeclareVarArg(node, ctx);
195
+ }
196
+ return resolveFunctionCall(node, ctx);
197
+ }
198
+ if (node.type === "identifier") {
199
+ const result = await resolveIdentifier(node, ctx);
200
+ if (result) return result;
201
+ ctx.errors.push(`${locationStr(node)}: could not resolve identifier "${node.text}" to a static value`);
202
+ return null;
203
+ }
204
+ if (node.type === "subscript") return resolveSubscript(node, ctx);
205
+ if (node.type === "attribute") return resolveAttribute(node, ctx);
206
+ ctx.errors.push(`${locationStr(node)}: unsupported derive() argument type "${node.type}"`);
207
+ return null;
263
208
  }
264
209
  /**
265
- * Handles binary + concatenation within a static value context.
266
- */
210
+ * Handles binary + concatenation within a static value context.
211
+ */
267
212
  async function resolveStaticBinaryOperator(node, ctx) {
268
- const left = node.childForFieldName('left');
269
- const right = node.childForFieldName('right');
270
- if (!left || !right) {
271
- ctx.errors.push(`${locationStr(node)}: binary operator missing operands`);
272
- return null;
273
- }
274
- // Verify it's a + operator
275
- const operator = node.childForFieldName('operator');
276
- if (operator && operator.text !== '+') {
277
- ctx.errors.push(`${locationStr(node)}: unsupported binary operator "${operator.text}" in static expression`);
278
- return null;
279
- }
280
- const leftNode = await resolveStaticValue(left, ctx);
281
- const rightNode = await resolveStaticValue(right, ctx);
282
- if (!leftNode || !rightNode)
283
- return null;
284
- // Flatten nested sequences
285
- const parts = [];
286
- if (leftNode.type === 'sequence') {
287
- parts.push(...leftNode.nodes);
288
- }
289
- else {
290
- parts.push(leftNode);
291
- }
292
- if (rightNode.type === 'sequence') {
293
- parts.push(...rightNode.nodes);
294
- }
295
- else {
296
- parts.push(rightNode);
297
- }
298
- return { type: 'sequence', nodes: parts };
213
+ const left = node.childForFieldName("left");
214
+ const right = node.childForFieldName("right");
215
+ if (!left || !right) {
216
+ ctx.errors.push(`${locationStr(node)}: binary operator missing operands`);
217
+ return null;
218
+ }
219
+ const operator = node.childForFieldName("operator");
220
+ if (operator && operator.text !== "+") {
221
+ ctx.errors.push(`${locationStr(node)}: unsupported binary operator "${operator.text}" in static expression`);
222
+ return null;
223
+ }
224
+ const leftNode = await resolveStaticValue(left, ctx);
225
+ const rightNode = await resolveStaticValue(right, ctx);
226
+ if (!leftNode || !rightNode) return null;
227
+ const parts = [];
228
+ if (leftNode.type === "sequence") parts.push(...leftNode.nodes);
229
+ else parts.push(leftNode);
230
+ if (rightNode.type === "sequence") parts.push(...rightNode.nodes);
231
+ else parts.push(rightNode);
232
+ return {
233
+ type: "sequence",
234
+ nodes: parts
235
+ };
299
236
  }
300
237
  /**
301
- * Resolves a Python conditional expression (ternary):
302
- * "day" if cond else "night"
303
- * tree-sitter: conditional_expression → [consequent, if, condition, else, alternate]
304
- */
238
+ * Resolves a Python conditional expression (ternary):
239
+ * "day" if cond else "night"
240
+ * tree-sitter: conditional_expression → [consequent, if, condition, else, alternate]
241
+ */
305
242
  async function resolveConditional(node, ctx) {
306
- // In Python's tree-sitter, the conditional_expression fields are:
307
- // body = consequent (the value if true)
308
- // condition = the test
309
- // alternative = the else value (named 'alternative' field)
310
- // But field names vary by tree-sitter version. Let's use positional children.
311
- // Structure: consequent "if" condition "else" alternative
312
- // The tree-sitter Python grammar uses named children:
313
- // body (first expression), if keyword, condition, else keyword, alternative
314
- // But let's find them by field name first, then fall back to positional.
315
- // Try using children directly: first non-keyword child is consequent,
316
- // child after "else" keyword is alternate
317
- let consequent = null;
318
- let alternate = null;
319
- let seenElse = false;
320
- for (let i = 0; i < node.childCount; i++) {
321
- const child = node.child(i);
322
- if (!child)
323
- continue;
324
- if (child.type === 'if')
325
- continue;
326
- if (child.type === 'else') {
327
- seenElse = true;
328
- continue;
329
- }
330
- if (!seenElse && !consequent) {
331
- consequent = child;
332
- }
333
- else if (seenElse && !alternate) {
334
- alternate = child;
335
- }
336
- }
337
- if (!consequent || !alternate) {
338
- ctx.errors.push(`${locationStr(node)}: could not parse conditional expression`);
339
- return null;
340
- }
341
- // Recursively resolve both branches (handles nested ternaries)
342
- const consequentNode = await resolveStaticValue(consequent, ctx);
343
- const alternateNode = await resolveStaticValue(alternate, ctx);
344
- if (!consequentNode || !alternateNode)
345
- return null;
346
- // Flatten choices
347
- const branches = [];
348
- if (consequentNode.type === 'choice') {
349
- branches.push(...consequentNode.nodes);
350
- }
351
- else {
352
- branches.push(consequentNode);
353
- }
354
- if (alternateNode.type === 'choice') {
355
- branches.push(...alternateNode.nodes);
356
- }
357
- else {
358
- branches.push(alternateNode);
359
- }
360
- return { type: 'choice', nodes: branches };
243
+ let consequent = null;
244
+ let alternate = null;
245
+ let seenElse = false;
246
+ for (let i = 0; i < node.childCount; i++) {
247
+ const child = node.child(i);
248
+ if (!child) continue;
249
+ if (child.type === "if") continue;
250
+ if (child.type === "else") {
251
+ seenElse = true;
252
+ continue;
253
+ }
254
+ if (!seenElse && !consequent) consequent = child;
255
+ else if (seenElse && !alternate) alternate = child;
256
+ }
257
+ if (!consequent || !alternate) {
258
+ ctx.errors.push(`${locationStr(node)}: could not parse conditional expression`);
259
+ return null;
260
+ }
261
+ const consequentNode = await resolveStaticValue(consequent, ctx);
262
+ const alternateNode = await resolveStaticValue(alternate, ctx);
263
+ if (!consequentNode || !alternateNode) return null;
264
+ const branches = [];
265
+ if (consequentNode.type === "choice") branches.push(...consequentNode.nodes);
266
+ else branches.push(consequentNode);
267
+ if (alternateNode.type === "choice") branches.push(...alternateNode.nodes);
268
+ else branches.push(alternateNode);
269
+ return {
270
+ type: "choice",
271
+ nodes: branches
272
+ };
361
273
  }
362
274
  /**
363
- * Resolves a function call to its string return variants.
364
- * Looks up the function locally, then in imported files.
365
- */
275
+ * Resolves a function call to its string return variants.
276
+ * Looks up the function locally, then in imported files.
277
+ */
366
278
  async function resolveFunctionCall(callNode, ctx) {
367
- const funcNode = callNode.childForFieldName('function');
368
- if (!funcNode || funcNode.type !== 'identifier') {
369
- ctx.errors.push(`${locationStr(callNode)}: cannot resolve non-identifier function call`);
370
- return null;
371
- }
372
- const funcName = funcNode.text;
373
- // Expression parser callback for resolving return expressions.
374
- // Receives the actual rootNode and filePath from whichever file
375
- // the function is defined in (handles re-exports correctly).
376
- const exprParser = (node, targetRootNode, targetFilePath) => {
377
- const targetImports = extractImportsFromRoot(targetRootNode, ctx.imports);
378
- return resolveStaticValue(node, {
379
- rootNode: targetRootNode,
380
- imports: targetImports,
381
- filePath: targetFilePath,
382
- errors: ctx.errors,
383
- });
384
- };
385
- // Try resolving in current file
386
- const localResult = await resolveFunctionInCurrentFile(funcName, ctx.rootNode, ctx.filePath, exprParser);
387
- if (localResult)
388
- return localResult;
389
- // Try resolving from imports (follows re-export chains automatically)
390
- const importInfo = findImportForName(funcName, ctx);
391
- if (importInfo) {
392
- const result = await resolveFunctionInFile(importInfo.originalName, importInfo.filePath, exprParser);
393
- if (result)
394
- return result;
395
- }
396
- ctx.errors.push(`${locationStr(callNode)}: could not resolve function "${funcName}" to string return values`);
397
- return null;
279
+ const funcNode = callNode.childForFieldName("function");
280
+ if (!funcNode || funcNode.type !== "identifier") {
281
+ ctx.errors.push(`${locationStr(callNode)}: cannot resolve non-identifier function call`);
282
+ return null;
283
+ }
284
+ const funcName = funcNode.text;
285
+ const exprParser = (node, targetRootNode, targetFilePath) => {
286
+ return resolveStaticValue(node, {
287
+ rootNode: targetRootNode,
288
+ imports: extractImportsFromRoot(targetRootNode, ctx.imports),
289
+ filePath: targetFilePath,
290
+ errors: ctx.errors
291
+ });
292
+ };
293
+ const localResult = await resolveFunctionInCurrentFile(funcName, ctx.rootNode, ctx.filePath, exprParser);
294
+ if (localResult) return localResult;
295
+ const importInfo = findImportForName(funcName, ctx);
296
+ if (importInfo) {
297
+ const result = await resolveFunctionInFile(importInfo.originalName, importInfo.filePath, exprParser);
298
+ if (result) return result;
299
+ }
300
+ ctx.errors.push(`${locationStr(callNode)}: could not resolve function "${funcName}" to string return values`);
301
+ return null;
398
302
  }
399
303
  /**
400
- * Extracts GT import aliases from a target file's root node.
401
- * Merges with parent imports for GT package functions (declare_var, etc.)
402
- * that may not be imported in the target file.
403
- */
304
+ * Extracts GT import aliases from a target file's root node.
305
+ * Merges with parent imports for GT package functions (declare_var, etc.)
306
+ * that may not be imported in the target file.
307
+ */
404
308
  function extractImportsFromRoot(rootNode, parentImports) {
405
- // Extract GT-only imports from the target file using the same
406
- // filtering logic as the main extractImports (filters by GT packages)
407
- const fileImports = extractImports(rootNode);
408
- // Carry over GT declare_* imports from the calling context
409
- // (in case the helper file doesn't import them directly)
410
- const parentDeclareImports = parentImports.filter((imp) => isDeriveFunction(imp.originalName) ||
411
- imp.originalName === PYTHON_DECLARE_VAR);
412
- // Deduplicate: prefer the target file's own imports over parent's
413
- const seen = new Set(fileImports.map((imp) => imp.localName));
414
- const merged = [...fileImports];
415
- for (const imp of parentDeclareImports) {
416
- if (!seen.has(imp.localName)) {
417
- merged.push(imp);
418
- }
419
- }
420
- return merged;
309
+ const fileImports = extractImports(rootNode);
310
+ const parentDeclareImports = parentImports.filter((imp) => isDeriveFunction(imp.originalName) || imp.originalName === "declare_var");
311
+ const seen = new Set(fileImports.map((imp) => imp.localName));
312
+ const merged = [...fileImports];
313
+ for (const imp of parentDeclareImports) if (!seen.has(imp.localName)) merged.push(imp);
314
+ return merged;
421
315
  }
422
316
  /**
423
- * Resolves the argument of a declare_var() call.
424
- * Produces ICU placeholder text using declareVar from generaltranslation.
425
- */
317
+ * Resolves the argument of a declare_var() call.
318
+ * Produces ICU placeholder text using declareVar from generaltranslation.
319
+ */
426
320
  async function resolveDeclareVarArg(callNode, ctx) {
427
- const argsNode = callNode.childForFieldName('arguments');
428
- if (!argsNode) {
429
- ctx.errors.push(`${locationStr(callNode)}: declare_var() requires arguments`);
430
- return null;
431
- }
432
- // Get the first positional arg (the variable - we use empty string since it's runtime)
433
- const firstArg = getFirstPositionalArg(callNode);
434
- if (!firstArg) {
435
- ctx.errors.push(`${locationStr(callNode)}: declare_var() requires a variable argument`);
436
- return null;
437
- }
438
- // Extract optional _name kwarg
439
- let nameOption;
440
- for (let i = 0; i < argsNode.childCount; i++) {
441
- const child = argsNode.child(i);
442
- if (!child || child.type !== 'keyword_argument')
443
- continue;
444
- const nameNode = child.childForFieldName('name');
445
- const valueNode = child.childForFieldName('value');
446
- if (!nameNode || !valueNode)
447
- continue;
448
- if (nameNode.text === '_name') {
449
- if (valueNode.type === 'string' && !isFString(valueNode)) {
450
- nameOption = extractStringContent(valueNode);
451
- }
452
- }
453
- }
454
- // Use declareVar with empty string for the runtime variable value
455
- const options = nameOption ? { $name: nameOption } : undefined;
456
- const icuText = declareVar('', options);
457
- return { type: 'text', text: icuText };
321
+ const argsNode = callNode.childForFieldName("arguments");
322
+ if (!argsNode) {
323
+ ctx.errors.push(`${locationStr(callNode)}: declare_var() requires arguments`);
324
+ return null;
325
+ }
326
+ if (!getFirstPositionalArg(callNode)) {
327
+ ctx.errors.push(`${locationStr(callNode)}: declare_var() requires a variable argument`);
328
+ return null;
329
+ }
330
+ let nameOption;
331
+ for (let i = 0; i < argsNode.childCount; i++) {
332
+ const child = argsNode.child(i);
333
+ if (!child || child.type !== "keyword_argument") continue;
334
+ const nameNode = child.childForFieldName("name");
335
+ const valueNode = child.childForFieldName("value");
336
+ if (!nameNode || !valueNode) continue;
337
+ if (nameNode.text === "_name") {
338
+ if (valueNode.type === "string" && !isFString(valueNode)) nameOption = extractStringContent(valueNode);
339
+ }
340
+ }
341
+ return {
342
+ type: "text",
343
+ text: declareVar("", nameOption ? { $name: nameOption } : void 0)
344
+ };
458
345
  }
459
- // ===== Constant / Dictionary Resolution ===== //
460
346
  /**
461
- * Finds a top-level assignment `name = <value>` in the given root node.
462
- * Returns the right-hand side (value) node, or null if not found.
463
- */
347
+ * Finds a top-level assignment `name = <value>` in the given root node.
348
+ * Returns the right-hand side (value) node, or null if not found.
349
+ */
464
350
  function findConstantAssignment(name, rootNode) {
465
- for (let i = 0; i < rootNode.childCount; i++) {
466
- const child = rootNode.child(i);
467
- if (!child || child.type !== 'expression_statement')
468
- continue;
469
- const expr = child.child(0);
470
- if (!expr || expr.type !== 'assignment')
471
- continue;
472
- const left = expr.childForFieldName('left');
473
- const right = expr.childForFieldName('right');
474
- if (left?.type === 'identifier' && left.text === name && right) {
475
- return right;
476
- }
477
- }
478
- return null;
351
+ for (let i = 0; i < rootNode.childCount; i++) {
352
+ const child = rootNode.child(i);
353
+ if (!child || child.type !== "expression_statement") continue;
354
+ const expr = child.child(0);
355
+ if (!expr || expr.type !== "assignment") continue;
356
+ const left = expr.childForFieldName("left");
357
+ const right = expr.childForFieldName("right");
358
+ if ((left === null || left === void 0 ? void 0 : left.type) === "identifier" && left.text === name && right) return right;
359
+ }
360
+ return null;
479
361
  }
480
362
  /**
481
- * Guard against infinite recursion when resolving identifier chains.
482
- * Tracks variable names currently being resolved to detect circular references.
483
- */
484
- const resolvingIdentifiers = new Set();
363
+ * Guard against infinite recursion when resolving identifier chains.
364
+ * Tracks variable names currently being resolved to detect circular references.
365
+ */
366
+ const resolvingIdentifiers = /* @__PURE__ */ new Set();
485
367
  /**
486
- * Resolves an identifier to its static value by finding the assignment
487
- * in the current file or cross-file via imports.
488
- */
368
+ * Resolves an identifier to its static value by finding the assignment
369
+ * in the current file or cross-file via imports.
370
+ */
489
371
  async function resolveIdentifier(node, ctx) {
490
- const name = node.text;
491
- // Guard against circular references (e.g., x = y; y = x)
492
- const guardKey = `${ctx.filePath}::${name}`;
493
- if (resolvingIdentifiers.has(guardKey)) {
494
- return null;
495
- }
496
- resolvingIdentifiers.add(guardKey);
497
- try {
498
- // Try local assignment first
499
- const localValue = findConstantAssignment(name, ctx.rootNode);
500
- if (localValue) {
501
- return await resolveStaticValue(localValue, ctx);
502
- }
503
- // Try cross-file via imports
504
- const importInfo = findImportForName(name, ctx);
505
- if (importInfo) {
506
- let source;
507
- try {
508
- source = fs.readFileSync(importInfo.filePath, 'utf8');
509
- }
510
- catch {
511
- return null;
512
- }
513
- const parser = await getParser();
514
- const tree = parser.parse(source);
515
- if (!tree)
516
- return null;
517
- const externalValue = findConstantAssignment(importInfo.originalName, tree.rootNode);
518
- if (externalValue) {
519
- const externalImports = extractImportsFromRoot(tree.rootNode, ctx.imports);
520
- return await resolveStaticValue(externalValue, {
521
- rootNode: tree.rootNode,
522
- imports: externalImports,
523
- filePath: importInfo.filePath,
524
- errors: ctx.errors,
525
- });
526
- }
527
- }
528
- return null;
529
- }
530
- finally {
531
- resolvingIdentifiers.delete(guardKey);
532
- }
372
+ const name = node.text;
373
+ const guardKey = `${ctx.filePath}::${name}`;
374
+ if (resolvingIdentifiers.has(guardKey)) return null;
375
+ resolvingIdentifiers.add(guardKey);
376
+ try {
377
+ const localValue = findConstantAssignment(name, ctx.rootNode);
378
+ if (localValue) return await resolveStaticValue(localValue, ctx);
379
+ const importInfo = findImportForName(name, ctx);
380
+ if (importInfo) {
381
+ let source;
382
+ try {
383
+ source = fs.readFileSync(importInfo.filePath, "utf8");
384
+ } catch {
385
+ return null;
386
+ }
387
+ const tree = (await getParser()).parse(source);
388
+ if (!tree) return null;
389
+ const externalValue = findConstantAssignment(importInfo.originalName, tree.rootNode);
390
+ if (externalValue) {
391
+ const externalImports = extractImportsFromRoot(tree.rootNode, ctx.imports);
392
+ return await resolveStaticValue(externalValue, {
393
+ rootNode: tree.rootNode,
394
+ imports: externalImports,
395
+ filePath: importInfo.filePath,
396
+ errors: ctx.errors
397
+ });
398
+ }
399
+ }
400
+ return null;
401
+ } finally {
402
+ resolvingIdentifiers.delete(guardKey);
403
+ }
533
404
  }
534
405
  /**
535
- * Finds a dictionary assignment and returns the dictionary node.
536
- * Searches locally first, then cross-file via imports.
537
- * Returns the dictionary node and the context (rootNode, filePath, imports)
538
- * for resolving values within it.
539
- */
406
+ * Finds a dictionary assignment and returns the dictionary node.
407
+ * Searches locally first, then cross-file via imports.
408
+ * Returns the dictionary node and the context (rootNode, filePath, imports)
409
+ * for resolving values within it.
410
+ */
540
411
  async function findDictionaryAssignment(name, ctx) {
541
- // Try local assignment
542
- const localValue = findConstantAssignment(name, ctx.rootNode);
543
- if (localValue &&
544
- (localValue.type === 'dictionary' || localValue.type === 'list')) {
545
- return { dictNode: localValue, valueCtx: ctx };
546
- }
547
- // Try cross-file
548
- const importInfo = findImportForName(name, ctx);
549
- if (importInfo) {
550
- let source;
551
- try {
552
- source = fs.readFileSync(importInfo.filePath, 'utf8');
553
- }
554
- catch {
555
- return null;
556
- }
557
- const parser = await getParser();
558
- const tree = parser.parse(source);
559
- if (!tree)
560
- return null;
561
- const externalValue = findConstantAssignment(importInfo.originalName, tree.rootNode);
562
- if (externalValue &&
563
- (externalValue.type === 'dictionary' || externalValue.type === 'list')) {
564
- const externalImports = extractImportsFromRoot(tree.rootNode, ctx.imports);
565
- return {
566
- dictNode: externalValue,
567
- valueCtx: {
568
- rootNode: tree.rootNode,
569
- imports: externalImports,
570
- filePath: importInfo.filePath,
571
- errors: ctx.errors,
572
- },
573
- };
574
- }
575
- }
576
- return null;
412
+ const localValue = findConstantAssignment(name, ctx.rootNode);
413
+ if (localValue && (localValue.type === "dictionary" || localValue.type === "list")) return {
414
+ dictNode: localValue,
415
+ valueCtx: ctx
416
+ };
417
+ const importInfo = findImportForName(name, ctx);
418
+ if (importInfo) {
419
+ let source;
420
+ try {
421
+ source = fs.readFileSync(importInfo.filePath, "utf8");
422
+ } catch {
423
+ return null;
424
+ }
425
+ const tree = (await getParser()).parse(source);
426
+ if (!tree) return null;
427
+ const externalValue = findConstantAssignment(importInfo.originalName, tree.rootNode);
428
+ if (externalValue && (externalValue.type === "dictionary" || externalValue.type === "list")) {
429
+ const externalImports = extractImportsFromRoot(tree.rootNode, ctx.imports);
430
+ return {
431
+ dictNode: externalValue,
432
+ valueCtx: {
433
+ rootNode: tree.rootNode,
434
+ imports: externalImports,
435
+ filePath: importInfo.filePath,
436
+ errors: ctx.errors
437
+ }
438
+ };
439
+ }
440
+ }
441
+ return null;
577
442
  }
578
443
  /**
579
- * Collects all key-value entries from a dictionary node,
580
- * including entries from spread sources (**base).
581
- */
444
+ * Collects all key-value entries from a dictionary node,
445
+ * including entries from spread sources (**base).
446
+ */
582
447
  async function collectDictEntries(dictNode, ctx) {
583
- const entries = [];
584
- for (let i = 0; i < dictNode.childCount; i++) {
585
- const child = dictNode.child(i);
586
- if (!child)
587
- continue;
588
- if (child.type === 'pair') {
589
- const keyNode = child.childForFieldName('key');
590
- const valueNode = child.childForFieldName('value');
591
- if (!valueNode)
592
- continue;
593
- let key = null;
594
- if (keyNode) {
595
- if (keyNode.type === 'string' && !isFString(keyNode)) {
596
- key = extractStringContent(keyNode) ?? null;
597
- }
598
- else if (keyNode.type === 'identifier') {
599
- key = keyNode.text;
600
- }
601
- else if (keyNode.type === 'integer') {
602
- key = keyNode.text;
603
- }
604
- }
605
- entries.push({ key, valueNode });
606
- }
607
- else if (child.type === 'dictionary_splat') {
608
- // Get the spread source expression (child after **)
609
- let splatExpr = null;
610
- for (let j = 0; j < child.childCount; j++) {
611
- const splatChild = child.child(j);
612
- if (splatChild && splatChild.type !== '**') {
613
- splatExpr = splatChild;
614
- break;
615
- }
616
- }
617
- if (!splatExpr || splatExpr.type !== 'identifier')
618
- continue;
619
- const name = splatExpr.text;
620
- // Try local first
621
- const localDict = findConstantAssignment(name, ctx.rootNode);
622
- if (localDict && localDict.type === 'dictionary') {
623
- entries.push(...(await collectDictEntries(localDict, ctx)));
624
- }
625
- else {
626
- // Try cross-file
627
- const importInfo = findImportForName(name, ctx);
628
- if (importInfo) {
629
- let source;
630
- try {
631
- source = fs.readFileSync(importInfo.filePath, 'utf8');
632
- }
633
- catch {
634
- continue;
635
- }
636
- const parser = await getParser();
637
- const tree = parser.parse(source);
638
- if (!tree)
639
- continue;
640
- const externalValue = findConstantAssignment(importInfo.originalName, tree.rootNode);
641
- if (externalValue && externalValue.type === 'dictionary') {
642
- const externalImports = extractImportsFromRoot(tree.rootNode, ctx.imports);
643
- const externalCtx = {
644
- rootNode: tree.rootNode,
645
- imports: externalImports,
646
- filePath: importInfo.filePath,
647
- errors: ctx.errors,
648
- };
649
- entries.push(...(await collectDictEntries(externalValue, externalCtx)));
650
- }
651
- }
652
- }
653
- }
654
- }
655
- return entries;
448
+ const entries = [];
449
+ for (let i = 0; i < dictNode.childCount; i++) {
450
+ const child = dictNode.child(i);
451
+ if (!child) continue;
452
+ if (child.type === "pair") {
453
+ const keyNode = child.childForFieldName("key");
454
+ const valueNode = child.childForFieldName("value");
455
+ if (!valueNode) continue;
456
+ let key = null;
457
+ if (keyNode) {
458
+ if (keyNode.type === "string" && !isFString(keyNode)) key = extractStringContent(keyNode) ?? null;
459
+ else if (keyNode.type === "identifier") key = keyNode.text;
460
+ else if (keyNode.type === "integer") key = keyNode.text;
461
+ }
462
+ entries.push({
463
+ key,
464
+ valueNode
465
+ });
466
+ } else if (child.type === "dictionary_splat") {
467
+ let splatExpr = null;
468
+ for (let j = 0; j < child.childCount; j++) {
469
+ const splatChild = child.child(j);
470
+ if (splatChild && splatChild.type !== "**") {
471
+ splatExpr = splatChild;
472
+ break;
473
+ }
474
+ }
475
+ if (!splatExpr || splatExpr.type !== "identifier") continue;
476
+ const name = splatExpr.text;
477
+ const localDict = findConstantAssignment(name, ctx.rootNode);
478
+ if (localDict && localDict.type === "dictionary") entries.push(...await collectDictEntries(localDict, ctx));
479
+ else {
480
+ const importInfo = findImportForName(name, ctx);
481
+ if (importInfo) {
482
+ let source;
483
+ try {
484
+ source = fs.readFileSync(importInfo.filePath, "utf8");
485
+ } catch {
486
+ continue;
487
+ }
488
+ const tree = (await getParser()).parse(source);
489
+ if (!tree) continue;
490
+ const externalValue = findConstantAssignment(importInfo.originalName, tree.rootNode);
491
+ if (externalValue && externalValue.type === "dictionary") {
492
+ const externalImports = extractImportsFromRoot(tree.rootNode, ctx.imports);
493
+ const externalCtx = {
494
+ rootNode: tree.rootNode,
495
+ imports: externalImports,
496
+ filePath: importInfo.filePath,
497
+ errors: ctx.errors
498
+ };
499
+ entries.push(...await collectDictEntries(externalValue, externalCtx));
500
+ }
501
+ }
502
+ }
503
+ }
504
+ }
505
+ return entries;
656
506
  }
657
507
  /**
658
- * Collects all elements from a list node as DictEntry[] with index as key.
659
- * Handles list_splat (*spread).
660
- */
508
+ * Collects all elements from a list node as DictEntry[] with index as key.
509
+ * Handles list_splat (*spread).
510
+ */
661
511
  async function collectListEntries(listNode, ctx) {
662
- const entries = [];
663
- let index = 0;
664
- for (let i = 0; i < listNode.childCount; i++) {
665
- const child = listNode.child(i);
666
- if (!child)
667
- continue;
668
- // Skip brackets and commas
669
- if (child.type === '[' || child.type === ']' || child.type === ',')
670
- continue;
671
- if (child.type === 'list_splat') {
672
- // *base spread resolve the source identifier
673
- let splatExpr = null;
674
- for (let j = 0; j < child.childCount; j++) {
675
- const sc = child.child(j);
676
- if (sc && sc.type !== '*') {
677
- splatExpr = sc;
678
- break;
679
- }
680
- }
681
- if (!splatExpr || splatExpr.type !== 'identifier')
682
- continue;
683
- const localList = findConstantAssignment(splatExpr.text, ctx.rootNode);
684
- if (localList && localList.type === 'list') {
685
- const spreadEntries = await collectListEntries(localList, ctx);
686
- for (const e of spreadEntries) {
687
- entries.push({ key: String(index++), valueNode: e.valueNode });
688
- }
689
- }
690
- else {
691
- // Try cross-file
692
- const importInfo = findImportForName(splatExpr.text, ctx);
693
- if (importInfo) {
694
- let source;
695
- try {
696
- source = fs.readFileSync(importInfo.filePath, 'utf8');
697
- }
698
- catch {
699
- continue;
700
- }
701
- const parser = await getParser();
702
- const tree = parser.parse(source);
703
- if (!tree)
704
- continue;
705
- const externalValue = findConstantAssignment(importInfo.originalName, tree.rootNode);
706
- if (externalValue && externalValue.type === 'list') {
707
- const externalImports = extractImportsFromRoot(tree.rootNode, ctx.imports);
708
- const externalCtx = {
709
- rootNode: tree.rootNode,
710
- imports: externalImports,
711
- filePath: importInfo.filePath,
712
- errors: ctx.errors,
713
- };
714
- const spreadEntries = await collectListEntries(externalValue, externalCtx);
715
- for (const e of spreadEntries) {
716
- entries.push({ key: String(index++), valueNode: e.valueNode });
717
- }
718
- }
719
- }
720
- }
721
- continue;
722
- }
723
- // Regular element — any expression
724
- entries.push({ key: String(index), valueNode: child });
725
- index++;
726
- }
727
- return entries;
512
+ const entries = [];
513
+ let index = 0;
514
+ for (let i = 0; i < listNode.childCount; i++) {
515
+ const child = listNode.child(i);
516
+ if (!child) continue;
517
+ if (child.type === "[" || child.type === "]" || child.type === ",") continue;
518
+ if (child.type === "list_splat") {
519
+ let splatExpr = null;
520
+ for (let j = 0; j < child.childCount; j++) {
521
+ const sc = child.child(j);
522
+ if (sc && sc.type !== "*") {
523
+ splatExpr = sc;
524
+ break;
525
+ }
526
+ }
527
+ if (!splatExpr || splatExpr.type !== "identifier") continue;
528
+ const localList = findConstantAssignment(splatExpr.text, ctx.rootNode);
529
+ if (localList && localList.type === "list") {
530
+ const spreadEntries = await collectListEntries(localList, ctx);
531
+ for (const e of spreadEntries) entries.push({
532
+ key: String(index++),
533
+ valueNode: e.valueNode
534
+ });
535
+ } else {
536
+ const importInfo = findImportForName(splatExpr.text, ctx);
537
+ if (importInfo) {
538
+ let source;
539
+ try {
540
+ source = fs.readFileSync(importInfo.filePath, "utf8");
541
+ } catch {
542
+ continue;
543
+ }
544
+ const tree = (await getParser()).parse(source);
545
+ if (!tree) continue;
546
+ const externalValue = findConstantAssignment(importInfo.originalName, tree.rootNode);
547
+ if (externalValue && externalValue.type === "list") {
548
+ const externalImports = extractImportsFromRoot(tree.rootNode, ctx.imports);
549
+ const spreadEntries = await collectListEntries(externalValue, {
550
+ rootNode: tree.rootNode,
551
+ imports: externalImports,
552
+ filePath: importInfo.filePath,
553
+ errors: ctx.errors
554
+ });
555
+ for (const e of spreadEntries) entries.push({
556
+ key: String(index++),
557
+ valueNode: e.valueNode
558
+ });
559
+ }
560
+ }
561
+ }
562
+ continue;
563
+ }
564
+ entries.push({
565
+ key: String(index),
566
+ valueNode: child
567
+ });
568
+ index++;
569
+ }
570
+ return entries;
728
571
  }
729
572
  /**
730
- * Resolves an expression to dictionary AST node(s).
731
- * Handles identifier, subscript chains, and attribute chains.
732
- */
573
+ * Resolves an expression to dictionary AST node(s).
574
+ * Handles identifier, subscript chains, and attribute chains.
575
+ */
733
576
  async function resolveToDictNodes(node, ctx) {
734
- // Case 1: identifier — base case
735
- if (node.type === 'identifier') {
736
- const result = await findDictionaryAssignment(node.text, ctx);
737
- if (result)
738
- return [result];
739
- return [];
740
- }
741
- // Case 2: subscript (e.g., D["a"] in D["a"]["x"])
742
- if (node.type === 'subscript') {
743
- const valueNode = node.childForFieldName('value');
744
- if (!valueNode)
745
- return [];
746
- const parentDicts = await resolveToDictNodes(valueNode, ctx);
747
- if (parentDicts.length === 0)
748
- return [];
749
- const subscriptKey = node.childForFieldName('subscript');
750
- if (!subscriptKey)
751
- return [];
752
- // Check if key is a static string literal
753
- const isStaticKey = subscriptKey.type === 'string' && !isFString(subscriptKey);
754
- const staticKeyValue = isStaticKey
755
- ? extractStringContent(subscriptKey)
756
- : null;
757
- const isStaticIntKey = subscriptKey.type === 'integer';
758
- const staticIntKeyValue = isStaticIntKey ? subscriptKey.text : null;
759
- const results = [];
760
- for (const parent of parentDicts) {
761
- const entries = parent.dictNode.type === 'list'
762
- ? await collectListEntries(parent.dictNode, parent.valueCtx)
763
- : await collectDictEntries(parent.dictNode, parent.valueCtx);
764
- if (staticKeyValue != null || staticIntKeyValue != null) {
765
- const keyToMatch = staticKeyValue ?? staticIntKeyValue;
766
- // Static: narrow to matching keys
767
- for (const entry of entries) {
768
- if (entry.key === keyToMatch &&
769
- (entry.valueNode.type === 'dictionary' ||
770
- entry.valueNode.type === 'list')) {
771
- results.push({
772
- dictNode: entry.valueNode,
773
- valueCtx: parent.valueCtx,
774
- });
775
- }
776
- }
777
- }
778
- else {
779
- // Dynamic: collect ALL entries whose values are dicts/lists
780
- for (const entry of entries) {
781
- if (entry.valueNode.type === 'dictionary' ||
782
- entry.valueNode.type === 'list') {
783
- results.push({
784
- dictNode: entry.valueNode,
785
- valueCtx: parent.valueCtx,
786
- });
787
- }
788
- }
789
- }
790
- }
791
- return results;
792
- }
793
- // Case 3: attribute (e.g., D.a in D.a.x)
794
- if (node.type === 'attribute') {
795
- const objectNode = node.childForFieldName('object');
796
- const attrNode = node.childForFieldName('attribute');
797
- if (!objectNode || !attrNode)
798
- return [];
799
- const parentDicts = await resolveToDictNodes(objectNode, ctx);
800
- if (parentDicts.length === 0)
801
- return [];
802
- const attrName = attrNode.text;
803
- const results = [];
804
- for (const parent of parentDicts) {
805
- const entries = await collectDictEntries(parent.dictNode, parent.valueCtx);
806
- for (const entry of entries) {
807
- if (entry.key === attrName &&
808
- (entry.valueNode.type === 'dictionary' ||
809
- entry.valueNode.type === 'list')) {
810
- results.push({
811
- dictNode: entry.valueNode,
812
- valueCtx: parent.valueCtx,
813
- });
814
- }
815
- }
816
- }
817
- return results;
818
- }
819
- return [];
577
+ if (node.type === "identifier") {
578
+ const result = await findDictionaryAssignment(node.text, ctx);
579
+ if (result) return [result];
580
+ return [];
581
+ }
582
+ if (node.type === "subscript") {
583
+ const valueNode = node.childForFieldName("value");
584
+ if (!valueNode) return [];
585
+ const parentDicts = await resolveToDictNodes(valueNode, ctx);
586
+ if (parentDicts.length === 0) return [];
587
+ const subscriptKey = node.childForFieldName("subscript");
588
+ if (!subscriptKey) return [];
589
+ const staticKeyValue = subscriptKey.type === "string" && !isFString(subscriptKey) ? extractStringContent(subscriptKey) : null;
590
+ const staticIntKeyValue = subscriptKey.type === "integer" ? subscriptKey.text : null;
591
+ const results = [];
592
+ for (const parent of parentDicts) {
593
+ const entries = parent.dictNode.type === "list" ? await collectListEntries(parent.dictNode, parent.valueCtx) : await collectDictEntries(parent.dictNode, parent.valueCtx);
594
+ if (staticKeyValue != null || staticIntKeyValue != null) {
595
+ const keyToMatch = staticKeyValue ?? staticIntKeyValue;
596
+ for (const entry of entries) if (entry.key === keyToMatch && (entry.valueNode.type === "dictionary" || entry.valueNode.type === "list")) results.push({
597
+ dictNode: entry.valueNode,
598
+ valueCtx: parent.valueCtx
599
+ });
600
+ } else for (const entry of entries) if (entry.valueNode.type === "dictionary" || entry.valueNode.type === "list") results.push({
601
+ dictNode: entry.valueNode,
602
+ valueCtx: parent.valueCtx
603
+ });
604
+ }
605
+ return results;
606
+ }
607
+ if (node.type === "attribute") {
608
+ const objectNode = node.childForFieldName("object");
609
+ const attrNode = node.childForFieldName("attribute");
610
+ if (!objectNode || !attrNode) return [];
611
+ const parentDicts = await resolveToDictNodes(objectNode, ctx);
612
+ if (parentDicts.length === 0) return [];
613
+ const attrName = attrNode.text;
614
+ const results = [];
615
+ for (const parent of parentDicts) {
616
+ const entries = await collectDictEntries(parent.dictNode, parent.valueCtx);
617
+ for (const entry of entries) if (entry.key === attrName && (entry.valueNode.type === "dictionary" || entry.valueNode.type === "list")) results.push({
618
+ dictNode: entry.valueNode,
619
+ valueCtx: parent.valueCtx
620
+ });
621
+ }
622
+ return results;
623
+ }
624
+ return [];
820
625
  }
821
626
  /**
822
- * Resolves a subscript expression (e.g., `LABELS[score]` or `D["a"]["x"]`)
823
- * by extracting values from the resolved dictionary.
824
- * Supports nested access chains and spread resolution.
825
- */
627
+ * Resolves a subscript expression (e.g., `LABELS[score]` or `D["a"]["x"]`)
628
+ * by extracting values from the resolved dictionary.
629
+ * Supports nested access chains and spread resolution.
630
+ */
826
631
  async function resolveSubscript(node, ctx) {
827
- const valueNode = node.childForFieldName('value');
828
- if (!valueNode) {
829
- ctx.errors.push(`${locationStr(node)}: subscript missing value`);
830
- return null;
831
- }
832
- // Resolve the object to dict node(s) — supports nesting
833
- const dicts = await resolveToDictNodes(valueNode, ctx);
834
- if (dicts.length === 0) {
835
- ctx.errors.push(`${locationStr(node)}: could not find dictionary or list for "${valueNode.text}"`);
836
- return null;
837
- }
838
- const subscriptKey = node.childForFieldName('subscript');
839
- const isStaticStringKey = subscriptKey?.type === 'string' && !isFString(subscriptKey);
840
- const staticStringKeyValue = isStaticStringKey
841
- ? extractStringContent(subscriptKey)
842
- : null;
843
- const isStaticIntKey = subscriptKey?.type === 'integer';
844
- const staticIntKeyValue = isStaticIntKey ? subscriptKey.text : null;
845
- const staticKeyValue = staticStringKeyValue ?? staticIntKeyValue;
846
- const branches = [];
847
- for (const { dictNode, valueCtx } of dicts) {
848
- const entries = dictNode.type === 'list'
849
- ? await collectListEntries(dictNode, valueCtx)
850
- : await collectDictEntries(dictNode, valueCtx);
851
- if (staticKeyValue != null) {
852
- // Static key: resolve matching values (no break — collect all for spread overrides)
853
- for (const entry of entries) {
854
- if (entry.key === staticKeyValue) {
855
- const resolved = await resolveStaticValue(entry.valueNode, valueCtx);
856
- if (resolved) {
857
- if (resolved.type === 'choice') {
858
- branches.push(...resolved.nodes);
859
- }
860
- else {
861
- branches.push(resolved);
862
- }
863
- }
864
- }
865
- }
866
- }
867
- else {
868
- // Dynamic key: extract ALL values
869
- for (const entry of entries) {
870
- const resolved = await resolveStaticValue(entry.valueNode, valueCtx);
871
- if (resolved) {
872
- if (resolved.type === 'choice') {
873
- branches.push(...resolved.nodes);
874
- }
875
- else {
876
- branches.push(resolved);
877
- }
878
- }
879
- }
880
- }
881
- }
882
- if (branches.length === 0) {
883
- ctx.errors.push(`${locationStr(node)}: collection has no resolvable values`);
884
- return null;
885
- }
886
- if (branches.length === 1)
887
- return branches[0];
888
- return { type: 'choice', nodes: branches };
632
+ const valueNode = node.childForFieldName("value");
633
+ if (!valueNode) {
634
+ ctx.errors.push(`${locationStr(node)}: subscript missing value`);
635
+ return null;
636
+ }
637
+ const dicts = await resolveToDictNodes(valueNode, ctx);
638
+ if (dicts.length === 0) {
639
+ ctx.errors.push(`${locationStr(node)}: could not find dictionary or list for "${valueNode.text}"`);
640
+ return null;
641
+ }
642
+ const subscriptKey = node.childForFieldName("subscript");
643
+ const staticStringKeyValue = (subscriptKey === null || subscriptKey === void 0 ? void 0 : subscriptKey.type) === "string" && !isFString(subscriptKey) ? extractStringContent(subscriptKey) : null;
644
+ const staticIntKeyValue = (subscriptKey === null || subscriptKey === void 0 ? void 0 : subscriptKey.type) === "integer" ? subscriptKey.text : null;
645
+ const staticKeyValue = staticStringKeyValue ?? staticIntKeyValue;
646
+ const branches = [];
647
+ for (const { dictNode, valueCtx } of dicts) {
648
+ const entries = dictNode.type === "list" ? await collectListEntries(dictNode, valueCtx) : await collectDictEntries(dictNode, valueCtx);
649
+ if (staticKeyValue != null) {
650
+ for (const entry of entries) if (entry.key === staticKeyValue) {
651
+ const resolved = await resolveStaticValue(entry.valueNode, valueCtx);
652
+ if (resolved) if (resolved.type === "choice") branches.push(...resolved.nodes);
653
+ else branches.push(resolved);
654
+ }
655
+ } else for (const entry of entries) {
656
+ const resolved = await resolveStaticValue(entry.valueNode, valueCtx);
657
+ if (resolved) if (resolved.type === "choice") branches.push(...resolved.nodes);
658
+ else branches.push(resolved);
659
+ }
660
+ }
661
+ if (branches.length === 0) {
662
+ ctx.errors.push(`${locationStr(node)}: collection has no resolvable values`);
663
+ return null;
664
+ }
665
+ if (branches.length === 1) return branches[0];
666
+ return {
667
+ type: "choice",
668
+ nodes: branches
669
+ };
889
670
  }
890
671
  /**
891
- * Resolves an attribute access expression (e.g., `obj.attr` or `obj.a.b`)
892
- * by finding the specific dictionary pair with a matching key.
893
- * Supports nested access chains and spread resolution.
894
- */
672
+ * Resolves an attribute access expression (e.g., `obj.attr` or `obj.a.b`)
673
+ * by finding the specific dictionary pair with a matching key.
674
+ * Supports nested access chains and spread resolution.
675
+ */
895
676
  async function resolveAttribute(node, ctx) {
896
- const objectNode = node.childForFieldName('object');
897
- const attrNode = node.childForFieldName('attribute');
898
- if (!objectNode || !attrNode) {
899
- ctx.errors.push(`${locationStr(node)}: attribute access missing object or attribute`);
900
- return null;
901
- }
902
- const attrName = attrNode.text;
903
- // Resolve the object to dict node(s) — supports nesting
904
- const dicts = await resolveToDictNodes(objectNode, ctx);
905
- if (dicts.length === 0) {
906
- ctx.errors.push(`${locationStr(node)}: could not find dictionary or list for "${objectNode.text}"`);
907
- return null;
908
- }
909
- const branches = [];
910
- for (const { dictNode, valueCtx } of dicts) {
911
- const entries = await collectDictEntries(dictNode, valueCtx);
912
- for (const entry of entries) {
913
- if (entry.key === attrName) {
914
- const resolved = await resolveStaticValue(entry.valueNode, valueCtx);
915
- if (resolved) {
916
- if (resolved.type === 'choice') {
917
- branches.push(...resolved.nodes);
918
- }
919
- else {
920
- branches.push(resolved);
921
- }
922
- }
923
- }
924
- }
925
- }
926
- if (branches.length === 0) {
927
- ctx.errors.push(`${locationStr(node)}: could not find key "${attrName}" in dictionary or list`);
928
- return null;
929
- }
930
- if (branches.length === 1)
931
- return branches[0];
932
- return { type: 'choice', nodes: branches };
677
+ const objectNode = node.childForFieldName("object");
678
+ const attrNode = node.childForFieldName("attribute");
679
+ if (!objectNode || !attrNode) {
680
+ ctx.errors.push(`${locationStr(node)}: attribute access missing object or attribute`);
681
+ return null;
682
+ }
683
+ const attrName = attrNode.text;
684
+ const dicts = await resolveToDictNodes(objectNode, ctx);
685
+ if (dicts.length === 0) {
686
+ ctx.errors.push(`${locationStr(node)}: could not find dictionary or list for "${objectNode.text}"`);
687
+ return null;
688
+ }
689
+ const branches = [];
690
+ for (const { dictNode, valueCtx } of dicts) {
691
+ const entries = await collectDictEntries(dictNode, valueCtx);
692
+ for (const entry of entries) if (entry.key === attrName) {
693
+ const resolved = await resolveStaticValue(entry.valueNode, valueCtx);
694
+ if (resolved) if (resolved.type === "choice") branches.push(...resolved.nodes);
695
+ else branches.push(resolved);
696
+ }
697
+ }
698
+ if (branches.length === 0) {
699
+ ctx.errors.push(`${locationStr(node)}: could not find key "${attrName}" in dictionary or list`);
700
+ return null;
701
+ }
702
+ if (branches.length === 1) return branches[0];
703
+ return {
704
+ type: "choice",
705
+ nodes: branches
706
+ };
933
707
  }
934
- // ===== Helpers ===== //
935
708
  function getFirstPositionalArg(callNode) {
936
- const argsNode = callNode.childForFieldName('arguments');
937
- if (!argsNode)
938
- return null;
939
- for (let i = 0; i < argsNode.childCount; i++) {
940
- const child = argsNode.child(i);
941
- if (child &&
942
- child.type !== '(' &&
943
- child.type !== ')' &&
944
- child.type !== ',' &&
945
- child.type !== 'keyword_argument') {
946
- return child;
947
- }
948
- }
949
- return null;
709
+ const argsNode = callNode.childForFieldName("arguments");
710
+ if (!argsNode) return null;
711
+ for (let i = 0; i < argsNode.childCount; i++) {
712
+ const child = argsNode.child(i);
713
+ if (child && child.type !== "(" && child.type !== ")" && child.type !== "," && child.type !== "keyword_argument") return child;
714
+ }
715
+ return null;
950
716
  }
951
717
  function getOriginalImportName(localName, imports) {
952
- for (const imp of imports) {
953
- if (imp.localName === localName) {
954
- return imp.originalName;
955
- }
956
- }
957
- return null;
718
+ for (const imp of imports) if (imp.localName === localName) return imp.originalName;
719
+ return null;
958
720
  }
959
721
  function getDeriveImportNames(imports) {
960
- const names = new Set();
961
- for (const imp of imports) {
962
- if (isDeriveFunction(imp.originalName) ||
963
- imp.originalName === PYTHON_DECLARE_VAR) {
964
- names.add(imp.localName);
965
- }
966
- }
967
- return names;
722
+ const names = /* @__PURE__ */ new Set();
723
+ for (const imp of imports) if (isDeriveFunction(imp.originalName) || imp.originalName === "declare_var") names.add(imp.localName);
724
+ return names;
968
725
  }
969
726
  /**
970
- * Finds import info for a given local name (for cross-file function resolution).
971
- * Only looks at non-GT imports (user function imports).
972
- */
727
+ * Finds import info for a given local name (for cross-file function resolution).
728
+ * Only looks at non-GT imports (user function imports).
729
+ */
973
730
  function findImportForName(localName, ctx) {
974
- // Walk the AST to find import_from_statement nodes
975
- for (let i = 0; i < ctx.rootNode.childCount; i++) {
976
- const node = ctx.rootNode.child(i);
977
- if (!node || node.type !== 'import_from_statement')
978
- continue;
979
- const moduleName = getModuleName(node);
980
- if (!moduleName)
981
- continue;
982
- // Check all imported names in this statement
983
- for (let j = 0; j < node.childCount; j++) {
984
- const child = node.child(j);
985
- if (!child)
986
- continue;
987
- if (child.type === 'aliased_import') {
988
- const nameNode = child.childForFieldName('name');
989
- const aliasNode = child.childForFieldName('alias');
990
- const importedName = nameNode?.text;
991
- const alias = aliasNode?.text ?? importedName;
992
- if (alias === localName && importedName) {
993
- const filePath = resolveImportPath(moduleName, ctx.filePath);
994
- if (filePath) {
995
- return { originalName: importedName, filePath };
996
- }
997
- }
998
- }
999
- else if (child.type === 'dotted_name') {
1000
- if (child.text === moduleName)
1001
- continue; // Skip module name itself
1002
- if (child.text === localName) {
1003
- const filePath = resolveImportPath(moduleName, ctx.filePath);
1004
- if (filePath) {
1005
- return { originalName: localName, filePath };
1006
- }
1007
- }
1008
- }
1009
- }
1010
- }
1011
- return null;
731
+ for (let i = 0; i < ctx.rootNode.childCount; i++) {
732
+ const node = ctx.rootNode.child(i);
733
+ if (!node || node.type !== "import_from_statement") continue;
734
+ const moduleName = getModuleName(node);
735
+ if (!moduleName) continue;
736
+ for (let j = 0; j < node.childCount; j++) {
737
+ const child = node.child(j);
738
+ if (!child) continue;
739
+ if (child.type === "aliased_import") {
740
+ const nameNode = child.childForFieldName("name");
741
+ const aliasNode = child.childForFieldName("alias");
742
+ const importedName = nameNode === null || nameNode === void 0 ? void 0 : nameNode.text;
743
+ if (((aliasNode === null || aliasNode === void 0 ? void 0 : aliasNode.text) ?? importedName) === localName && importedName) {
744
+ const filePath = resolveImportPath(moduleName, ctx.filePath);
745
+ if (filePath) return {
746
+ originalName: importedName,
747
+ filePath
748
+ };
749
+ }
750
+ } else if (child.type === "dotted_name") {
751
+ if (child.text === moduleName) continue;
752
+ if (child.text === localName) {
753
+ const filePath = resolveImportPath(moduleName, ctx.filePath);
754
+ if (filePath) return {
755
+ originalName: localName,
756
+ filePath
757
+ };
758
+ }
759
+ }
760
+ }
761
+ }
762
+ return null;
1012
763
  }
1013
764
  function getModuleName(importNode) {
1014
- const moduleNode = importNode.childForFieldName('module_name');
1015
- if (moduleNode)
1016
- return moduleNode.text;
1017
- for (let i = 0; i < importNode.childCount; i++) {
1018
- const child = importNode.child(i);
1019
- if (!child)
1020
- continue;
1021
- if (child.type === 'import')
1022
- break;
1023
- if (child.type === 'dotted_name')
1024
- return child.text;
1025
- if (child.type === 'relative_import')
1026
- return child.text;
1027
- }
1028
- return undefined;
765
+ const moduleNode = importNode.childForFieldName("module_name");
766
+ if (moduleNode) return moduleNode.text;
767
+ for (let i = 0; i < importNode.childCount; i++) {
768
+ const child = importNode.child(i);
769
+ if (!child) continue;
770
+ if (child.type === "import") break;
771
+ if (child.type === "dotted_name") return child.text;
772
+ if (child.type === "relative_import") return child.text;
773
+ }
1029
774
  }
1030
775
  function isFString(stringNode) {
1031
- for (let i = 0; i < stringNode.childCount; i++) {
1032
- const child = stringNode.child(i);
1033
- if (child && child.type === 'string_start') {
1034
- return /^[fF]/.test(child.text);
1035
- }
1036
- if (child && child.type === 'interpolation') {
1037
- return true;
1038
- }
1039
- }
1040
- return false;
776
+ for (let i = 0; i < stringNode.childCount; i++) {
777
+ const child = stringNode.child(i);
778
+ if (child && child.type === "string_start") return /^[fF]/.test(child.text);
779
+ if (child && child.type === "interpolation") return true;
780
+ }
781
+ return false;
1041
782
  }
1042
783
  function extractStringContent(stringNode) {
1043
- for (let i = 0; i < stringNode.childCount; i++) {
1044
- const child = stringNode.child(i);
1045
- if (child && child.type === 'string_content') {
1046
- return child.text;
1047
- }
1048
- }
1049
- let hasStart = false;
1050
- let hasEnd = false;
1051
- for (let i = 0; i < stringNode.childCount; i++) {
1052
- const child = stringNode.child(i);
1053
- if (child?.type === 'string_start')
1054
- hasStart = true;
1055
- if (child?.type === 'string_end')
1056
- hasEnd = true;
1057
- }
1058
- if (hasStart && hasEnd)
1059
- return '';
1060
- return undefined;
784
+ for (let i = 0; i < stringNode.childCount; i++) {
785
+ const child = stringNode.child(i);
786
+ if (child && child.type === "string_content") return child.text;
787
+ }
788
+ let hasStart = false;
789
+ let hasEnd = false;
790
+ for (let i = 0; i < stringNode.childCount; i++) {
791
+ const child = stringNode.child(i);
792
+ if ((child === null || child === void 0 ? void 0 : child.type) === "string_start") hasStart = true;
793
+ if ((child === null || child === void 0 ? void 0 : child.type) === "string_end") hasEnd = true;
794
+ }
795
+ if (hasStart && hasEnd) return "";
1061
796
  }
1062
797
  function locationStr(node) {
1063
- return `line ${node.startPosition.row + 1}, col ${node.startPosition.column}`;
798
+ return `line ${node.startPosition.row + 1}, col ${node.startPosition.column}`;
1064
799
  }
800
+ //#endregion
801
+ export { containsStaticCalls, parseStringExpression };
802
+
803
+ //# sourceMappingURL=parseStringExpression.js.map