@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 +22 -17
- package/dist/constants.js.map +1 -0
- package/dist/extractCalls.js +188 -271
- package/dist/extractCalls.js.map +1 -0
- package/dist/extractImports.js +61 -76
- package/dist/extractImports.js.map +1 -0
- package/dist/index.js +50 -52
- package/dist/index.js.map +1 -0
- package/dist/parseStringExpression.js +722 -983
- package/dist/parseStringExpression.js.map +1 -0
- package/dist/parser.js +30 -37
- package/dist/parser.js.map +1 -0
- package/dist/resolveFunctionVariants.js +154 -181
- package/dist/resolveFunctionVariants.js.map +1 -0
- package/dist/resolveImport.js +48 -60
- package/dist/resolveImport.js.map +1 -0
- package/dist/stringNode.js +32 -46
- package/dist/stringNode.js.map +1 -0
- package/package.json +7 -4
package/dist/constants.js
CHANGED
|
@@ -1,21 +1,26 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
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
|
-
|
|
8
|
-
|
|
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
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
11
|
+
const PYTHON_TRANSLATION_FUNCTIONS = [
|
|
12
|
+
"t",
|
|
13
|
+
"msg",
|
|
14
|
+
"derive",
|
|
15
|
+
"declare_static",
|
|
16
|
+
"declare_var"
|
|
16
17
|
];
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
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"}
|
package/dist/extractCalls.js
CHANGED
|
@@ -1,288 +1,205 @@
|
|
|
1
|
-
import { PYTHON_METADATA_KWARGS
|
|
2
|
-
import { containsStaticCalls, parseStringExpression
|
|
3
|
-
import { nodeToStrings } from
|
|
4
|
-
import { indexVars } from
|
|
5
|
-
import { randomUUID } from
|
|
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
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
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
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
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
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
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
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
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
|
-
|
|
221
|
-
|
|
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
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
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
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
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
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
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
|
-
|
|
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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"}
|