@intlayer/lsp 8.11.4 → 9.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/esm/_virtual/_rolldown/runtime.mjs +36 -0
- package/dist/esm/callers/index.mjs +12 -0
- package/dist/esm/callers/index.mjs.map +1 -0
- package/dist/esm/callers/patterns.mjs +37 -0
- package/dist/esm/callers/patterns.mjs.map +1 -0
- package/dist/esm/dictionaryUtils.mjs +84 -0
- package/dist/esm/dictionaryUtils.mjs.map +1 -0
- package/dist/esm/findFieldInFile.mjs +22 -125
- package/dist/esm/findFieldInFile.mjs.map +1 -1
- package/dist/esm/findKeyAtOffset.mjs +20 -24
- package/dist/esm/findKeyAtOffset.mjs.map +1 -1
- package/dist/esm/findKeyInContentFile.mjs +259 -40
- package/dist/esm/findKeyInContentFile.mjs.map +1 -1
- package/dist/esm/findTCallAtOffset.mjs +33 -0
- package/dist/esm/findTCallAtOffset.mjs.map +1 -0
- package/dist/esm/findUsageFieldAtOffset.mjs +12 -51
- package/dist/esm/findUsageFieldAtOffset.mjs.map +1 -1
- package/dist/esm/index.mjs +343 -172
- package/dist/esm/index.mjs.map +1 -1
- package/dist/esm/oxcUtils.mjs +99 -29
- package/dist/esm/oxcUtils.mjs.map +1 -1
- package/dist/esm/usageAnalyzer.mjs +695 -0
- package/dist/esm/usageAnalyzer.mjs.map +1 -0
- package/dist/esm/utils.mjs +12 -1
- package/dist/types/callers/index.d.ts +5 -0
- package/dist/types/callers/index.d.ts.map +1 -0
- package/dist/types/callers/patterns.d.ts +30 -0
- package/dist/types/callers/patterns.d.ts.map +1 -0
- package/dist/types/dictionaryUtils.d.ts +11 -0
- package/dist/types/dictionaryUtils.d.ts.map +1 -0
- package/dist/types/findFieldInFile.d.ts +19 -22
- package/dist/types/findFieldInFile.d.ts.map +1 -1
- package/dist/types/findKeyAtOffset.d.ts +15 -12
- package/dist/types/findKeyAtOffset.d.ts.map +1 -1
- package/dist/types/findKeyInContentFile.d.ts +23 -21
- package/dist/types/findKeyInContentFile.d.ts.map +1 -1
- package/dist/types/findTCallAtOffset.d.ts +24 -0
- package/dist/types/findTCallAtOffset.d.ts.map +1 -0
- package/dist/types/findUsageFieldAtOffset.d.ts +14 -13
- package/dist/types/findUsageFieldAtOffset.d.ts.map +1 -1
- package/dist/types/index.d.ts +1 -1
- package/dist/types/oxcUtils.d.ts +44 -15
- package/dist/types/oxcUtils.d.ts.map +1 -1
- package/dist/types/usageAnalyzer.d.ts +141 -0
- package/dist/types/usageAnalyzer.d.ts.map +1 -0
- package/dist/types/utils.d.ts +15 -1
- package/package.json +11 -24
|
@@ -0,0 +1,695 @@
|
|
|
1
|
+
import { callers_exports } from "./callers/index.mjs";
|
|
2
|
+
import { buildParentMap, getObjectPropertyNode, getObjectPropertyString, getPropertyKeyName, getStaticStringValue, nodeEnd, nodeStart, parseText, walkAst } from "./oxcUtils.mjs";
|
|
3
|
+
|
|
4
|
+
//#region src/usageAnalyzer.ts
|
|
5
|
+
/**
|
|
6
|
+
* Property names that are framework accessors rather than content fields
|
|
7
|
+
* (react `.value`, vue refs `.value`, editor `.raw`/`.use`): they are skipped
|
|
8
|
+
* when building field paths.
|
|
9
|
+
*/
|
|
10
|
+
const ACCESSOR_PROPERTY_NAMES = /* @__PURE__ */ new Set([
|
|
11
|
+
"value",
|
|
12
|
+
"raw",
|
|
13
|
+
"use"
|
|
14
|
+
]);
|
|
15
|
+
/**
|
|
16
|
+
* Resolve a value source against a call expression's arguments.
|
|
17
|
+
*/
|
|
18
|
+
const readValueSource = (callNode, source) => {
|
|
19
|
+
const argumentsList = callNode["arguments"];
|
|
20
|
+
switch (source.from) {
|
|
21
|
+
case "argument": return getStaticStringValue(argumentsList?.[source.index]);
|
|
22
|
+
case "option": return getObjectPropertyString(argumentsList?.[source.argumentIndex], source.property);
|
|
23
|
+
case "fixed": return source.value;
|
|
24
|
+
case "path-first-segment": return null;
|
|
25
|
+
}
|
|
26
|
+
};
|
|
27
|
+
/** Read the first matching value from a list of sources. */
|
|
28
|
+
const readValueSources = (callNode, sources) => {
|
|
29
|
+
for (const source of sources ?? []) {
|
|
30
|
+
const value = readValueSource(callNode, source);
|
|
31
|
+
if (value !== null) return value;
|
|
32
|
+
}
|
|
33
|
+
return null;
|
|
34
|
+
};
|
|
35
|
+
/**
|
|
36
|
+
* Apply nested-namespace semantics: for descriptors flagged
|
|
37
|
+
* `nestedNamespace`, a dotted namespace is split at the first `.` into the
|
|
38
|
+
* dictionary key and a key-prefix path (next-intl:
|
|
39
|
+
* `useTranslations('about.counter')` → dictionary `about`, prefix
|
|
40
|
+
* `['counter']`).
|
|
41
|
+
*/
|
|
42
|
+
const splitNestedNamespace = (descriptor, rawNamespace) => {
|
|
43
|
+
if (!descriptor.nestedNamespace || !rawNamespace.includes(".")) return {
|
|
44
|
+
dictionaryKey: rawNamespace,
|
|
45
|
+
prefixPath: []
|
|
46
|
+
};
|
|
47
|
+
const segments = rawNamespace.split(".");
|
|
48
|
+
return {
|
|
49
|
+
dictionaryKey: segments[0],
|
|
50
|
+
prefixPath: segments.slice(1)
|
|
51
|
+
};
|
|
52
|
+
};
|
|
53
|
+
/**
|
|
54
|
+
* Return the caller name matched by a call/tagged-template callee: the bare
|
|
55
|
+
* identifier name, or the property name of a member expression when the
|
|
56
|
+
* descriptor allows method matching.
|
|
57
|
+
*/
|
|
58
|
+
const getCalleeName = (calleeNode) => {
|
|
59
|
+
if (!calleeNode) return null;
|
|
60
|
+
if (calleeNode["type"] === "Identifier") return {
|
|
61
|
+
name: calleeNode["name"],
|
|
62
|
+
isMethod: false
|
|
63
|
+
};
|
|
64
|
+
if (calleeNode["type"] === "MemberExpression" && calleeNode["computed"] !== true) {
|
|
65
|
+
const propertyNode = calleeNode["property"];
|
|
66
|
+
if (propertyNode?.["type"] === "Identifier") return {
|
|
67
|
+
name: propertyNode["name"],
|
|
68
|
+
isMethod: true
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
return null;
|
|
72
|
+
};
|
|
73
|
+
/**
|
|
74
|
+
* The message id of a self caller: either the first string argument or the
|
|
75
|
+
* `id` property of a descriptor-object first argument
|
|
76
|
+
* (`formatMessage({ id: 'home.title' })`, `i18n._({ id: '…' })`).
|
|
77
|
+
*/
|
|
78
|
+
const getSelfCallMessageId = (callNode) => {
|
|
79
|
+
const firstArgument = callNode["arguments"]?.[0];
|
|
80
|
+
if (!firstArgument) return null;
|
|
81
|
+
return getStaticStringValue(firstArgument) ?? getObjectPropertyString(firstArgument, "id");
|
|
82
|
+
};
|
|
83
|
+
/**
|
|
84
|
+
* Split a message id into dictionary key + field path according to the
|
|
85
|
+
* descriptor semantics.
|
|
86
|
+
*/
|
|
87
|
+
const resolveMessageId = (descriptor, fixedDictionaryKey, messageId) => {
|
|
88
|
+
if (fixedDictionaryKey !== null) return {
|
|
89
|
+
dictionaryKey: fixedDictionaryKey,
|
|
90
|
+
fieldPath: descriptor.flatKey ? [messageId] : messageId.split(".")
|
|
91
|
+
};
|
|
92
|
+
if (!descriptor.namespaceSources.some((source) => source.from === "path-first-segment")) return null;
|
|
93
|
+
const segments = messageId.split(".");
|
|
94
|
+
if (!segments[0]) return null;
|
|
95
|
+
return {
|
|
96
|
+
dictionaryKey: segments[0],
|
|
97
|
+
fieldPath: segments.slice(1)
|
|
98
|
+
};
|
|
99
|
+
};
|
|
100
|
+
/**
|
|
101
|
+
* i18next-style `ns:key` ids override the bound namespace:
|
|
102
|
+
* `t('about:title')` reads the `about` dictionary even when the translator
|
|
103
|
+
* was created with another namespace.
|
|
104
|
+
*/
|
|
105
|
+
const splitNamespacePrefixedId = (messageId) => {
|
|
106
|
+
const separatorIndex = messageId.indexOf(":");
|
|
107
|
+
if (separatorIndex <= 0) return {
|
|
108
|
+
namespaceOverride: null,
|
|
109
|
+
messagePath: messageId
|
|
110
|
+
};
|
|
111
|
+
return {
|
|
112
|
+
namespaceOverride: messageId.slice(0, separatorIndex),
|
|
113
|
+
messagePath: messageId.slice(separatorIndex + 1)
|
|
114
|
+
};
|
|
115
|
+
};
|
|
116
|
+
/** Unwrap `await expression` down to the awaited expression. */
|
|
117
|
+
const unwrapAwait = (node) => node?.["type"] === "AwaitExpression" ? node["argument"] : node;
|
|
118
|
+
/** Collect every module specifier imported by the program. */
|
|
119
|
+
const collectImportSources = (program) => {
|
|
120
|
+
const importSources = /* @__PURE__ */ new Set();
|
|
121
|
+
for (const statement of program["body"] ?? []) {
|
|
122
|
+
if (statement["type"] !== "ImportDeclaration") continue;
|
|
123
|
+
const sourceValue = statement["source"]?.["value"];
|
|
124
|
+
if (typeof sourceValue === "string") importSources.add(sourceValue);
|
|
125
|
+
}
|
|
126
|
+
return importSources;
|
|
127
|
+
};
|
|
128
|
+
/** Map each imported local name to its module specifier. */
|
|
129
|
+
const collectImportedLocalNames = (program) => {
|
|
130
|
+
const localNameToSource = /* @__PURE__ */ new Map();
|
|
131
|
+
for (const statement of program["body"] ?? []) {
|
|
132
|
+
if (statement["type"] !== "ImportDeclaration") continue;
|
|
133
|
+
const sourceValue = statement["source"]?.["value"];
|
|
134
|
+
if (typeof sourceValue !== "string") continue;
|
|
135
|
+
for (const specifier of statement["specifiers"] ?? []) {
|
|
136
|
+
const localName = specifier["local"]?.["name"];
|
|
137
|
+
if (typeof localName === "string") localNameToSource.set(localName, sourceValue);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
return localNameToSource;
|
|
141
|
+
};
|
|
142
|
+
/**
|
|
143
|
+
* Find the descriptor matching a call (or tagged template) callee among the
|
|
144
|
+
* active descriptors.
|
|
145
|
+
*/
|
|
146
|
+
const matchDescriptorForCallee = (state, calleeNode, filter) => {
|
|
147
|
+
const callee = getCalleeName(calleeNode);
|
|
148
|
+
if (!callee) return null;
|
|
149
|
+
for (const descriptor of state.activeDescriptors) {
|
|
150
|
+
if (descriptor.callerName !== callee.name) continue;
|
|
151
|
+
if (callee.isMethod && !descriptor.matchAsMethod) continue;
|
|
152
|
+
if (filter && !filter(descriptor)) continue;
|
|
153
|
+
return descriptor;
|
|
154
|
+
}
|
|
155
|
+
return null;
|
|
156
|
+
};
|
|
157
|
+
/** The module specifier a caller local name was imported from, if any. */
|
|
158
|
+
const getModuleSource = (state, descriptor) => state.importedLocalNames.get(descriptor.callerName) ?? void 0;
|
|
159
|
+
/**
|
|
160
|
+
* Recursively register a content destructure pattern: each property key is a
|
|
161
|
+
* field usage, each bound identifier becomes a content binding, and nested
|
|
162
|
+
* patterns extend the field path.
|
|
163
|
+
*
|
|
164
|
+
* const { searchInput: { text: alias } } = useIntlayer('key')
|
|
165
|
+
* → usages: ['searchInput'], ['searchInput', 'text']
|
|
166
|
+
* → binding: alias → ['searchInput', 'text']
|
|
167
|
+
*/
|
|
168
|
+
const collectContentDestructure = (state, pattern, dictionaryKey, basePath, descriptor, moduleSource) => {
|
|
169
|
+
for (const property of pattern["properties"] ?? []) {
|
|
170
|
+
const propertyType = property["type"];
|
|
171
|
+
if (propertyType !== "Property" && propertyType !== "ObjectProperty") continue;
|
|
172
|
+
const keyNode = property["key"];
|
|
173
|
+
const fieldName = getPropertyKeyName(keyNode);
|
|
174
|
+
if (!keyNode || !fieldName) continue;
|
|
175
|
+
const fieldPath = [...basePath, fieldName];
|
|
176
|
+
state.usages.push({
|
|
177
|
+
dictionaryKey,
|
|
178
|
+
fieldPath,
|
|
179
|
+
start: nodeStart(keyNode),
|
|
180
|
+
end: nodeEnd(keyNode),
|
|
181
|
+
kind: "destructure",
|
|
182
|
+
callerName: descriptor.callerName,
|
|
183
|
+
library: descriptor.library,
|
|
184
|
+
moduleSource
|
|
185
|
+
});
|
|
186
|
+
let valueNode = property["value"];
|
|
187
|
+
if (valueNode?.["type"] === "AssignmentPattern") valueNode = valueNode["left"];
|
|
188
|
+
if (valueNode?.["type"] === "Identifier") state.contentBindings.set(valueNode["name"], {
|
|
189
|
+
dictionaryKey,
|
|
190
|
+
basePath: fieldPath,
|
|
191
|
+
descriptor,
|
|
192
|
+
moduleSource
|
|
193
|
+
});
|
|
194
|
+
else if (valueNode?.["type"] === "ObjectPattern") collectContentDestructure(state, valueNode, dictionaryKey, fieldPath, descriptor, moduleSource);
|
|
195
|
+
}
|
|
196
|
+
};
|
|
197
|
+
/**
|
|
198
|
+
* Register bindings created by `const … = caller(…)` declarations, and record
|
|
199
|
+
* dictionary-level (`namespace`) usages for every matched caller call.
|
|
200
|
+
*/
|
|
201
|
+
const collectBindings = (state, program) => {
|
|
202
|
+
walkAst(program, (node) => {
|
|
203
|
+
if (node["type"] === "CallExpression") {
|
|
204
|
+
const descriptor = matchDescriptorForCallee(state, node["callee"], (candidate) => candidate.translationFunction !== "self");
|
|
205
|
+
if (descriptor) {
|
|
206
|
+
const rawNamespace = readValueSources(node, descriptor.namespaceSources);
|
|
207
|
+
if (rawNamespace) {
|
|
208
|
+
const { dictionaryKey } = splitNestedNamespace(descriptor, rawNamespace);
|
|
209
|
+
state.usages.push({
|
|
210
|
+
dictionaryKey,
|
|
211
|
+
fieldPath: [],
|
|
212
|
+
start: nodeStart(node),
|
|
213
|
+
end: nodeEnd(node),
|
|
214
|
+
kind: "namespace",
|
|
215
|
+
callerName: descriptor.callerName,
|
|
216
|
+
library: descriptor.library,
|
|
217
|
+
moduleSource: getModuleSource(state, descriptor)
|
|
218
|
+
});
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
return;
|
|
222
|
+
}
|
|
223
|
+
if (node["type"] !== "VariableDeclarator") return;
|
|
224
|
+
const initializer = unwrapAwait(node["init"]);
|
|
225
|
+
if (initializer?.["type"] !== "CallExpression") return;
|
|
226
|
+
const descriptor = matchDescriptorForCallee(state, initializer["callee"], (candidate) => candidate.translationFunction !== "self");
|
|
227
|
+
if (!descriptor) return;
|
|
228
|
+
const rawNamespace = readValueSources(initializer, descriptor.namespaceSources);
|
|
229
|
+
if (!rawNamespace && !descriptor.allowRootScope) return;
|
|
230
|
+
const namespaceSplit = rawNamespace ? splitNestedNamespace(descriptor, rawNamespace) : {
|
|
231
|
+
dictionaryKey: null,
|
|
232
|
+
prefixPath: []
|
|
233
|
+
};
|
|
234
|
+
const keyPrefixValue = readValueSources(initializer, descriptor.keyPrefixSources);
|
|
235
|
+
const keyPrefix = [...namespaceSplit.prefixPath, ...keyPrefixValue ? keyPrefixValue.split(".") : []];
|
|
236
|
+
const dictionaryKey = namespaceSplit.dictionaryKey;
|
|
237
|
+
const moduleSource = getModuleSource(state, descriptor);
|
|
238
|
+
const idNode = node["id"];
|
|
239
|
+
if (!idNode) return;
|
|
240
|
+
if (idNode["type"] === "Identifier") {
|
|
241
|
+
const variableName = idNode["name"];
|
|
242
|
+
if (descriptor.translationFunction === "return-value") state.translatorBindings.set(variableName, {
|
|
243
|
+
dictionaryKey,
|
|
244
|
+
keyPrefix,
|
|
245
|
+
descriptor,
|
|
246
|
+
moduleSource
|
|
247
|
+
});
|
|
248
|
+
else if (descriptor.translationFunction === "content" && dictionaryKey) state.contentBindings.set(variableName, {
|
|
249
|
+
dictionaryKey,
|
|
250
|
+
basePath: keyPrefix,
|
|
251
|
+
descriptor,
|
|
252
|
+
moduleSource
|
|
253
|
+
});
|
|
254
|
+
return;
|
|
255
|
+
}
|
|
256
|
+
if (idNode["type"] !== "ObjectPattern") return;
|
|
257
|
+
if (descriptor.translationFunction === "content") {
|
|
258
|
+
if (!dictionaryKey) return;
|
|
259
|
+
collectContentDestructure(state, idNode, dictionaryKey, keyPrefix, descriptor, moduleSource);
|
|
260
|
+
return;
|
|
261
|
+
}
|
|
262
|
+
if (descriptor.translationFunction !== "destructured-t") return;
|
|
263
|
+
for (const property of idNode["properties"] ?? []) {
|
|
264
|
+
const propertyType = property["type"];
|
|
265
|
+
if (propertyType !== "Property" && propertyType !== "ObjectProperty") continue;
|
|
266
|
+
const keyNode = property["key"];
|
|
267
|
+
const valueNode = property["value"];
|
|
268
|
+
const fieldName = getPropertyKeyName(keyNode);
|
|
269
|
+
const localName = valueNode?.["type"] === "Identifier" ? valueNode["name"] : null;
|
|
270
|
+
if (fieldName !== "t" || !localName) continue;
|
|
271
|
+
state.translatorBindings.set(localName, {
|
|
272
|
+
dictionaryKey,
|
|
273
|
+
keyPrefix,
|
|
274
|
+
descriptor,
|
|
275
|
+
moduleSource
|
|
276
|
+
});
|
|
277
|
+
}
|
|
278
|
+
});
|
|
279
|
+
};
|
|
280
|
+
/**
|
|
281
|
+
* Record `t('path.to.field')` calls on translator bindings, and self-caller
|
|
282
|
+
* calls (`formatMessage({ id })`, `i18n._('id')`).
|
|
283
|
+
*/
|
|
284
|
+
const collectCallUsages = (state, program) => {
|
|
285
|
+
walkAst(program, (node) => {
|
|
286
|
+
if (node["type"] === "TaggedTemplateExpression") {
|
|
287
|
+
collectTaggedTemplateUsage(state, node);
|
|
288
|
+
return;
|
|
289
|
+
}
|
|
290
|
+
if (node["type"] !== "CallExpression") return;
|
|
291
|
+
const callee = node["callee"];
|
|
292
|
+
if (callee?.["type"] === "Identifier") {
|
|
293
|
+
const binding = state.translatorBindings.get(callee["name"]);
|
|
294
|
+
if (binding) {
|
|
295
|
+
const argumentsList = node["arguments"];
|
|
296
|
+
const messageId = getStaticStringValue(argumentsList?.[0]);
|
|
297
|
+
if (messageId) {
|
|
298
|
+
const { namespaceOverride, messagePath } = splitNamespacePrefixedId(messageId);
|
|
299
|
+
let dictionaryKey = namespaceOverride ?? binding.dictionaryKey;
|
|
300
|
+
let fieldPath = namespaceOverride ? messagePath.split(".") : [...binding.keyPrefix, ...messagePath.split(".")];
|
|
301
|
+
if (dictionaryKey === null) {
|
|
302
|
+
const segments = messagePath.split(".");
|
|
303
|
+
dictionaryKey = segments[0] ?? null;
|
|
304
|
+
fieldPath = segments.slice(1);
|
|
305
|
+
}
|
|
306
|
+
if (dictionaryKey) state.usages.push({
|
|
307
|
+
dictionaryKey,
|
|
308
|
+
fieldPath,
|
|
309
|
+
start: nodeStart(node),
|
|
310
|
+
end: nodeEnd(node),
|
|
311
|
+
kind: "call",
|
|
312
|
+
callerName: binding.descriptor.callerName,
|
|
313
|
+
library: binding.descriptor.library,
|
|
314
|
+
moduleSource: binding.moduleSource
|
|
315
|
+
});
|
|
316
|
+
}
|
|
317
|
+
return;
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
const descriptor = matchDescriptorForCallee(state, callee, (candidate) => candidate.translationFunction === "self");
|
|
321
|
+
if (!descriptor) return;
|
|
322
|
+
const messageId = getSelfCallMessageId(node);
|
|
323
|
+
if (!messageId) return;
|
|
324
|
+
const fixedDictionaryKey = readValueSources(node, descriptor.namespaceSources);
|
|
325
|
+
const resolved = resolveMessageId(descriptor, fixedDictionaryKey, messageId);
|
|
326
|
+
if (!resolved) return;
|
|
327
|
+
state.usages.push({
|
|
328
|
+
...resolved,
|
|
329
|
+
start: nodeStart(node),
|
|
330
|
+
end: nodeEnd(node),
|
|
331
|
+
kind: "call",
|
|
332
|
+
callerName: descriptor.callerName,
|
|
333
|
+
library: descriptor.library,
|
|
334
|
+
moduleSource: getModuleSource(state, descriptor)
|
|
335
|
+
});
|
|
336
|
+
});
|
|
337
|
+
};
|
|
338
|
+
/**
|
|
339
|
+
* Lingui macro tagged template: ``t`Hello ${name}` `` — the catalog id is the
|
|
340
|
+
* template text with `{placeholder}` slots for each expression.
|
|
341
|
+
*/
|
|
342
|
+
const collectTaggedTemplateUsage = (state, node) => {
|
|
343
|
+
const descriptor = matchDescriptorForCallee(state, node["tag"], (candidate) => candidate.matchAsTaggedTemplate === true);
|
|
344
|
+
if (!descriptor) return;
|
|
345
|
+
const quasiNode = node["quasi"];
|
|
346
|
+
const quasis = quasiNode?.["quasis"] ?? [];
|
|
347
|
+
const expressions = quasiNode?.["expressions"] ?? [];
|
|
348
|
+
let messageId = "";
|
|
349
|
+
for (let index = 0; index < quasis.length; index++) {
|
|
350
|
+
const valueNode = quasis[index]?.["value"];
|
|
351
|
+
messageId += valueNode?.["cooked"] ?? valueNode?.["raw"] ?? "";
|
|
352
|
+
if (index < expressions.length) {
|
|
353
|
+
const expression = expressions[index];
|
|
354
|
+
const placeholder = expression["type"] === "Identifier" ? expression["name"] : String(index);
|
|
355
|
+
messageId += `{${placeholder}}`;
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
if (!messageId) return;
|
|
359
|
+
const fixedDictionaryKey = readValueSources(node, descriptor.namespaceSources);
|
|
360
|
+
const resolved = resolveMessageId(descriptor, fixedDictionaryKey, messageId);
|
|
361
|
+
if (!resolved) return;
|
|
362
|
+
state.usages.push({
|
|
363
|
+
...resolved,
|
|
364
|
+
start: nodeStart(node),
|
|
365
|
+
end: nodeEnd(node),
|
|
366
|
+
kind: "tagged-template",
|
|
367
|
+
callerName: descriptor.callerName,
|
|
368
|
+
library: descriptor.library,
|
|
369
|
+
moduleSource: getModuleSource(state, descriptor)
|
|
370
|
+
});
|
|
371
|
+
};
|
|
372
|
+
/** Read a static string JSX attribute from an opening element. */
|
|
373
|
+
const getJsxAttributeString = (openingElement, attributeName) => {
|
|
374
|
+
for (const attribute of openingElement["attributes"] ?? []) {
|
|
375
|
+
if (attribute["type"] !== "JSXAttribute") continue;
|
|
376
|
+
if (attribute["name"]?.["name"] !== attributeName) continue;
|
|
377
|
+
const valueNode = attribute["value"];
|
|
378
|
+
if (!valueNode) return null;
|
|
379
|
+
const directValue = getStaticStringValue(valueNode);
|
|
380
|
+
if (directValue !== null) return directValue;
|
|
381
|
+
if (valueNode["type"] === "JSXExpressionContainer") return getStaticStringValue(valueNode["expression"]);
|
|
382
|
+
return null;
|
|
383
|
+
}
|
|
384
|
+
return null;
|
|
385
|
+
};
|
|
386
|
+
/**
|
|
387
|
+
* Record JSX message components: `<FormattedMessage id="home.title" />`,
|
|
388
|
+
* `<Trans i18nKey="richText" />`, `<Trans id="…" />`.
|
|
389
|
+
*/
|
|
390
|
+
const collectJsxUsages = (state, program) => {
|
|
391
|
+
const translatorNamespaces = new Set([...state.translatorBindings.values()].map((binding) => binding.dictionaryKey).filter((dictionaryKey) => dictionaryKey !== null));
|
|
392
|
+
const fallbackNamespace = translatorNamespaces.size === 1 ? [...translatorNamespaces][0] : null;
|
|
393
|
+
walkAst(program, (node) => {
|
|
394
|
+
if (node["type"] !== "JSXOpeningElement") return;
|
|
395
|
+
const nameNode = node["name"];
|
|
396
|
+
if (nameNode?.["type"] !== "JSXIdentifier") return;
|
|
397
|
+
const elementName = nameNode["name"];
|
|
398
|
+
for (const descriptor of state.activeDescriptors) {
|
|
399
|
+
if (!descriptor.jsxIdAttribute) continue;
|
|
400
|
+
if (descriptor.callerName !== elementName) continue;
|
|
401
|
+
const messageId = getJsxAttributeString(node, descriptor.jsxIdAttribute);
|
|
402
|
+
if (messageId === null) continue;
|
|
403
|
+
const namespaceFromAttribute = descriptor.jsxNamespaceAttribute ? getJsxAttributeString(node, descriptor.jsxNamespaceAttribute) : null;
|
|
404
|
+
const fixedNamespace = descriptor.namespaceSources.find((source) => source.from === "fixed");
|
|
405
|
+
const dictionaryKey = namespaceFromAttribute ?? (fixedNamespace?.from === "fixed" ? fixedNamespace.value : null) ?? fallbackNamespace;
|
|
406
|
+
let resolved;
|
|
407
|
+
if (dictionaryKey) resolved = {
|
|
408
|
+
dictionaryKey,
|
|
409
|
+
fieldPath: descriptor.flatKey ? [messageId] : messageId.split(".")
|
|
410
|
+
};
|
|
411
|
+
else resolved = resolveMessageId(descriptor, null, messageId);
|
|
412
|
+
if (!resolved) continue;
|
|
413
|
+
state.usages.push({
|
|
414
|
+
...resolved,
|
|
415
|
+
start: nodeStart(node),
|
|
416
|
+
end: nodeEnd(node),
|
|
417
|
+
kind: "jsx",
|
|
418
|
+
callerName: descriptor.callerName,
|
|
419
|
+
library: descriptor.library,
|
|
420
|
+
moduleSource: getModuleSource(state, descriptor)
|
|
421
|
+
});
|
|
422
|
+
return;
|
|
423
|
+
}
|
|
424
|
+
});
|
|
425
|
+
};
|
|
426
|
+
/** Identifier positions that are declarations, not references. */
|
|
427
|
+
const isDeclarationPosition = (identifierNode, parentMap) => {
|
|
428
|
+
const parent = parentMap.get(identifierNode);
|
|
429
|
+
if (!parent) return false;
|
|
430
|
+
const parentType = parent["type"];
|
|
431
|
+
if (parentType === "VariableDeclarator" && parent["id"] === identifierNode) return true;
|
|
432
|
+
if ((parentType === "Property" || parentType === "ObjectProperty") && parentMap.get(parent)?.["type"] === "ObjectPattern") return true;
|
|
433
|
+
if (parentType === "ImportSpecifier" || parentType === "PropertyDefinition") return true;
|
|
434
|
+
if (parentType === "FunctionDeclaration" || parentType === "FunctionExpression" || parentType === "ArrowFunctionExpression") return true;
|
|
435
|
+
if ((parentType === "Property" || parentType === "ObjectProperty") && parent["key"] === identifierNode && parentMap.get(parent)?.["type"] === "ObjectExpression") return true;
|
|
436
|
+
return false;
|
|
437
|
+
};
|
|
438
|
+
/** Identifier used as the (non-computed) property name of a member access. */
|
|
439
|
+
const isPropertyNamePosition = (identifierNode, parentMap) => {
|
|
440
|
+
const parent = parentMap.get(identifierNode);
|
|
441
|
+
if (!parent) return false;
|
|
442
|
+
const parentType = parent["type"];
|
|
443
|
+
return parentType === "MemberExpression" && parent["property"] === identifierNode && parent["computed"] !== true || parentType === "JSXMemberExpression" && parent["property"] === identifierNode;
|
|
444
|
+
};
|
|
445
|
+
/**
|
|
446
|
+
* Record member-access / bare-reference usages of content bindings:
|
|
447
|
+
* `content.section.title`, `{title}` (destructured field reference),
|
|
448
|
+
* Svelte's `$content.title`, Vue's `content.value.title`.
|
|
449
|
+
*/
|
|
450
|
+
const collectMemberUsages = (state, program, parentMap) => {
|
|
451
|
+
if (state.contentBindings.size === 0) return;
|
|
452
|
+
walkAst(program, (node) => {
|
|
453
|
+
const nodeType = node["type"];
|
|
454
|
+
if (nodeType !== "Identifier" && nodeType !== "JSXIdentifier") return;
|
|
455
|
+
const rawName = node["name"];
|
|
456
|
+
const variableName = rawName.startsWith("$") ? rawName.slice(1) : rawName;
|
|
457
|
+
const binding = state.contentBindings.get(variableName);
|
|
458
|
+
if (!binding) return;
|
|
459
|
+
const parentType = parentMap.get(node)?.["type"];
|
|
460
|
+
if (parentType?.startsWith("TS") || parentType === "JSXAttribute") return;
|
|
461
|
+
if (isDeclarationPosition(node, parentMap)) return;
|
|
462
|
+
if (isPropertyNamePosition(node, parentMap)) return;
|
|
463
|
+
const fieldPath = [...binding.basePath];
|
|
464
|
+
const fieldSpans = fieldPath.map(() => ({
|
|
465
|
+
start: nodeStart(node),
|
|
466
|
+
end: nodeEnd(node)
|
|
467
|
+
}));
|
|
468
|
+
let currentNode = node;
|
|
469
|
+
while (true) {
|
|
470
|
+
const parent = parentMap.get(currentNode);
|
|
471
|
+
if (!parent) break;
|
|
472
|
+
const parentType = parent["type"];
|
|
473
|
+
if (parentType === "MemberExpression" && parent["object"] === currentNode && parent["computed"] !== true || parentType === "JSXMemberExpression" && parent["object"] === currentNode) {
|
|
474
|
+
const propertyNode = parent["property"];
|
|
475
|
+
const propertyName = propertyNode["name"];
|
|
476
|
+
if (!ACCESSOR_PROPERTY_NAMES.has(propertyName)) {
|
|
477
|
+
fieldPath.push(propertyName);
|
|
478
|
+
fieldSpans.push({
|
|
479
|
+
start: nodeStart(propertyNode),
|
|
480
|
+
end: nodeEnd(propertyNode)
|
|
481
|
+
});
|
|
482
|
+
}
|
|
483
|
+
currentNode = parent;
|
|
484
|
+
continue;
|
|
485
|
+
}
|
|
486
|
+
if (parentType === "MemberExpression" && parent["object"] === currentNode && parent["computed"] === true) {
|
|
487
|
+
const propertyValue = getStaticStringValue(parent["property"]);
|
|
488
|
+
if (propertyValue === null) break;
|
|
489
|
+
fieldPath.push(propertyValue);
|
|
490
|
+
fieldSpans.push({
|
|
491
|
+
start: nodeStart(parent["property"]),
|
|
492
|
+
end: nodeEnd(parent["property"])
|
|
493
|
+
});
|
|
494
|
+
currentNode = parent;
|
|
495
|
+
continue;
|
|
496
|
+
}
|
|
497
|
+
if (parentType === "CallExpression" && parent["callee"] === currentNode) {
|
|
498
|
+
currentNode = parent;
|
|
499
|
+
continue;
|
|
500
|
+
}
|
|
501
|
+
break;
|
|
502
|
+
}
|
|
503
|
+
state.usages.push({
|
|
504
|
+
dictionaryKey: binding.dictionaryKey,
|
|
505
|
+
fieldPath,
|
|
506
|
+
fieldSpans,
|
|
507
|
+
start: nodeStart(node),
|
|
508
|
+
end: nodeEnd(currentNode),
|
|
509
|
+
kind: "member",
|
|
510
|
+
callerName: binding.descriptor.callerName,
|
|
511
|
+
library: binding.descriptor.library,
|
|
512
|
+
moduleSource: binding.moduleSource,
|
|
513
|
+
isBareReference: fieldPath.length === binding.basePath.length
|
|
514
|
+
});
|
|
515
|
+
return true;
|
|
516
|
+
});
|
|
517
|
+
};
|
|
518
|
+
/**
|
|
519
|
+
* Analyse a source file and return every resolvable translation usage:
|
|
520
|
+
* base-intlayer content access and all compat-library forms (translation
|
|
521
|
+
* functions, self calls, JSX message components, tagged templates).
|
|
522
|
+
*
|
|
523
|
+
* Pure and self-contained (oxc parse only) — shared by the LSP handlers and
|
|
524
|
+
* the VS Code extension providers/decorations.
|
|
525
|
+
*
|
|
526
|
+
* @param text - Full source text (a script: TS/TSX or an extracted SFC block).
|
|
527
|
+
* @returns Usages ordered by start offset.
|
|
528
|
+
*/
|
|
529
|
+
const collectMessageUsages = (text) => {
|
|
530
|
+
const program = parseText(text);
|
|
531
|
+
if (!program) return [];
|
|
532
|
+
const fileImportSources = collectImportSources(program);
|
|
533
|
+
const state = {
|
|
534
|
+
usages: [],
|
|
535
|
+
translatorBindings: /* @__PURE__ */ new Map(),
|
|
536
|
+
contentBindings: /* @__PURE__ */ new Map(),
|
|
537
|
+
activeDescriptors: callers_exports.ALL_CALLERS.filter((descriptor) => (0, callers_exports.isCallerActive)(descriptor, fileImportSources)),
|
|
538
|
+
importedLocalNames: collectImportedLocalNames(program)
|
|
539
|
+
};
|
|
540
|
+
collectBindings(state, program);
|
|
541
|
+
collectCallUsages(state, program);
|
|
542
|
+
collectJsxUsages(state, program);
|
|
543
|
+
const parentMap = buildParentMap(program);
|
|
544
|
+
collectMemberUsages(state, program, parentMap);
|
|
545
|
+
return state.usages.sort((first, second) => first.start - second.start);
|
|
546
|
+
};
|
|
547
|
+
/**
|
|
548
|
+
* Resolve the dictionary key declared by a single call-expression node when
|
|
549
|
+
* it matches any registered non-self caller (`useIntlayer("key")`,
|
|
550
|
+
* `useI18n({ namespace })`, …). Used for on-the-fly checks where the full
|
|
551
|
+
* file analysis is not needed (e.g. completion context).
|
|
552
|
+
*
|
|
553
|
+
* @param callNode - A candidate CallExpression node.
|
|
554
|
+
*/
|
|
555
|
+
const matchCallNamespace = (callNode) => {
|
|
556
|
+
if (callNode["type"] !== "CallExpression") return null;
|
|
557
|
+
const callee = getCalleeName(callNode["callee"]);
|
|
558
|
+
if (!callee) return null;
|
|
559
|
+
for (const descriptor of callers_exports.ALL_CALLERS) {
|
|
560
|
+
if (descriptor.translationFunction === "self") continue;
|
|
561
|
+
if (descriptor.callerName !== callee.name) continue;
|
|
562
|
+
if (callee.isMethod && !descriptor.matchAsMethod) continue;
|
|
563
|
+
const namespaceValue = readValueSources(callNode, descriptor.namespaceSources);
|
|
564
|
+
if (namespaceValue !== null) return splitNestedNamespace(descriptor, namespaceValue).dictionaryKey;
|
|
565
|
+
}
|
|
566
|
+
return null;
|
|
567
|
+
};
|
|
568
|
+
/**
|
|
569
|
+
* The AST node holding the namespace string for a given value source, when
|
|
570
|
+
* that source is addressable in the call (positional argument or options
|
|
571
|
+
* property).
|
|
572
|
+
*/
|
|
573
|
+
const getNamespaceArgumentNode = (callNode, source) => {
|
|
574
|
+
const argumentsList = callNode["arguments"];
|
|
575
|
+
if (source.from === "argument") return argumentsList?.[source.index] ?? null;
|
|
576
|
+
if (source.from === "option") return getObjectPropertyNode(argumentsList?.[source.argumentIndex], source.property)?.["value"] ?? null;
|
|
577
|
+
return null;
|
|
578
|
+
};
|
|
579
|
+
/**
|
|
580
|
+
* Collect every call site with an addressable namespace argument (positional
|
|
581
|
+
* or options property) resolving to a static string. Powers the
|
|
582
|
+
* unknown-dictionary diagnostics and `findKeyAtOffset`.
|
|
583
|
+
*
|
|
584
|
+
* Self callers (`formatMessage`, lingui `t`/`_`) don't declare a namespace at
|
|
585
|
+
* the call site and are excluded.
|
|
586
|
+
*
|
|
587
|
+
* @param text - Full source text.
|
|
588
|
+
*/
|
|
589
|
+
const collectNamespaceReferences = (text) => {
|
|
590
|
+
const program = parseText(text);
|
|
591
|
+
if (!program) return [];
|
|
592
|
+
const fileImportSources = collectImportSources(program);
|
|
593
|
+
const activeDescriptors = callers_exports.ALL_CALLERS.filter((descriptor) => descriptor.translationFunction !== "self" && (0, callers_exports.isCallerActive)(descriptor, fileImportSources));
|
|
594
|
+
const references = [];
|
|
595
|
+
walkAst(program, (node) => {
|
|
596
|
+
if (node["type"] !== "CallExpression") return;
|
|
597
|
+
const calleeNode = node["callee"];
|
|
598
|
+
const callee = getCalleeName(calleeNode);
|
|
599
|
+
if (!callee) return;
|
|
600
|
+
for (const descriptor of activeDescriptors) {
|
|
601
|
+
if (descriptor.callerName !== callee.name) continue;
|
|
602
|
+
if (callee.isMethod && !descriptor.matchAsMethod) continue;
|
|
603
|
+
const calleeNameNode = callee.isMethod && calleeNode ? calleeNode["property"] : calleeNode;
|
|
604
|
+
for (const source of descriptor.namespaceSources) {
|
|
605
|
+
const namespaceNode = getNamespaceArgumentNode(node, source);
|
|
606
|
+
const namespaceValue = getStaticStringValue(namespaceNode ?? void 0);
|
|
607
|
+
if (!namespaceNode || namespaceValue === null) continue;
|
|
608
|
+
references.push({
|
|
609
|
+
dictionaryKey: splitNestedNamespace(descriptor, namespaceValue).dictionaryKey,
|
|
610
|
+
callerName: descriptor.callerName,
|
|
611
|
+
library: descriptor.library,
|
|
612
|
+
callStart: nodeStart(node),
|
|
613
|
+
callEnd: nodeEnd(node),
|
|
614
|
+
calleeStart: nodeStart(calleeNameNode),
|
|
615
|
+
calleeEnd: nodeEnd(calleeNameNode),
|
|
616
|
+
namespaceStart: nodeStart(namespaceNode),
|
|
617
|
+
namespaceEnd: nodeEnd(namespaceNode)
|
|
618
|
+
});
|
|
619
|
+
return;
|
|
620
|
+
}
|
|
621
|
+
}
|
|
622
|
+
});
|
|
623
|
+
return references;
|
|
624
|
+
};
|
|
625
|
+
/**
|
|
626
|
+
* Collect every variable bound to a dictionary in the file. Used by consumers
|
|
627
|
+
* that scan non-script regions (Vue/Angular templates) for references to
|
|
628
|
+
* those variables, or that need to know which dictionaries a file consumes.
|
|
629
|
+
*
|
|
630
|
+
* @param text - Full source text (a script: TS/TSX or an extracted SFC block).
|
|
631
|
+
*/
|
|
632
|
+
const collectCallerBindings = (text) => {
|
|
633
|
+
const program = parseText(text);
|
|
634
|
+
if (!program) return [];
|
|
635
|
+
const fileImportSources = collectImportSources(program);
|
|
636
|
+
const state = {
|
|
637
|
+
usages: [],
|
|
638
|
+
translatorBindings: /* @__PURE__ */ new Map(),
|
|
639
|
+
contentBindings: /* @__PURE__ */ new Map(),
|
|
640
|
+
activeDescriptors: callers_exports.ALL_CALLERS.filter((descriptor) => (0, callers_exports.isCallerActive)(descriptor, fileImportSources)),
|
|
641
|
+
importedLocalNames: collectImportedLocalNames(program)
|
|
642
|
+
};
|
|
643
|
+
collectBindings(state, program);
|
|
644
|
+
return [...[...state.contentBindings.entries()].map(([variableName, binding]) => ({
|
|
645
|
+
variableName,
|
|
646
|
+
dictionaryKey: binding.dictionaryKey,
|
|
647
|
+
basePath: binding.basePath,
|
|
648
|
+
bindingKind: "content",
|
|
649
|
+
callerName: binding.descriptor.callerName,
|
|
650
|
+
library: binding.descriptor.library
|
|
651
|
+
})), ...[...state.translatorBindings.entries()].filter(([, binding]) => binding.dictionaryKey !== null).map(([variableName, binding]) => ({
|
|
652
|
+
variableName,
|
|
653
|
+
dictionaryKey: binding.dictionaryKey,
|
|
654
|
+
basePath: binding.keyPrefix,
|
|
655
|
+
bindingKind: "translator",
|
|
656
|
+
callerName: binding.descriptor.callerName,
|
|
657
|
+
library: binding.descriptor.library
|
|
658
|
+
}))];
|
|
659
|
+
};
|
|
660
|
+
/**
|
|
661
|
+
* Find the most specific translation usage at `offset`.
|
|
662
|
+
*
|
|
663
|
+
* For member chains the field path is trimmed to the segment under the
|
|
664
|
+
* cursor: hovering `section` in `content.section.title` yields
|
|
665
|
+
* `['section']`, not the full chain.
|
|
666
|
+
*
|
|
667
|
+
* Dictionary-level `namespace` usages are only returned when no field-level
|
|
668
|
+
* usage matches the offset.
|
|
669
|
+
*
|
|
670
|
+
* @param text - Full source text.
|
|
671
|
+
* @param offset - Cursor offset in `text`.
|
|
672
|
+
*/
|
|
673
|
+
const findMessageUsageAtOffset = (text, offset) => {
|
|
674
|
+
const usages = collectMessageUsages(text);
|
|
675
|
+
let bestMatch = null;
|
|
676
|
+
for (const usage of usages) {
|
|
677
|
+
if (offset < usage.start || offset > usage.end) continue;
|
|
678
|
+
if (!bestMatch || bestMatch.kind === "namespace" && usage.kind !== "namespace" || usage.end - usage.start < bestMatch.end - bestMatch.start && !(bestMatch.kind !== "namespace" && usage.kind === "namespace")) bestMatch = usage;
|
|
679
|
+
}
|
|
680
|
+
if (!bestMatch) return null;
|
|
681
|
+
if (bestMatch.kind === "member" && bestMatch.fieldSpans) {
|
|
682
|
+
let lastIncludedIndex = -1;
|
|
683
|
+
for (let index = 0; index < bestMatch.fieldSpans.length; index++) if (offset >= bestMatch.fieldSpans[index].start) lastIncludedIndex = index;
|
|
684
|
+
return {
|
|
685
|
+
...bestMatch,
|
|
686
|
+
fieldPath: bestMatch.fieldPath.slice(0, lastIncludedIndex + 1),
|
|
687
|
+
fieldSpans: bestMatch.fieldSpans.slice(0, lastIncludedIndex + 1)
|
|
688
|
+
};
|
|
689
|
+
}
|
|
690
|
+
return bestMatch;
|
|
691
|
+
};
|
|
692
|
+
|
|
693
|
+
//#endregion
|
|
694
|
+
export { collectCallerBindings, collectMessageUsages, collectNamespaceReferences, findMessageUsageAtOffset, matchCallNamespace };
|
|
695
|
+
//# sourceMappingURL=usageAnalyzer.mjs.map
|