react-i18next-scanner 0.1.6 → 0.1.7
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.
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
// src/core/actions/writeResolvedImportedObjectUsagesReport.ts
|
|
2
|
+
import fs from "fs/promises";
|
|
3
|
+
import path from "path";
|
|
4
|
+
export async function writeResolvedImportedObjectUsagesReport(usages, outputDir) {
|
|
5
|
+
if (usages.length === 0) {
|
|
6
|
+
return;
|
|
7
|
+
}
|
|
8
|
+
const reportDir = outputDir ?? "reports";
|
|
9
|
+
await fs.mkdir(reportDir, { recursive: true });
|
|
10
|
+
const reportPath = path.join(reportDir, "resolved-imported-object-usages.json");
|
|
11
|
+
await fs.writeFile(reportPath, JSON.stringify(usages, null, 2), "utf-8");
|
|
12
|
+
}
|
|
@@ -20,6 +20,15 @@ function isProbablyTranslationKey(value) {
|
|
|
20
20
|
!normalized.includes("\r") &&
|
|
21
21
|
(normalized.includes(".") || normalized.includes(":")));
|
|
22
22
|
}
|
|
23
|
+
function getPropertyName(name) {
|
|
24
|
+
if (ts.isIdentifier(name)) {
|
|
25
|
+
return name.text;
|
|
26
|
+
}
|
|
27
|
+
if (ts.isStringLiteral(name) || ts.isNumericLiteral(name)) {
|
|
28
|
+
return name.text;
|
|
29
|
+
}
|
|
30
|
+
return null;
|
|
31
|
+
}
|
|
23
32
|
function flattenObjectLiteral(node, prefix = "", result = new Map()) {
|
|
24
33
|
for (const property of node.properties) {
|
|
25
34
|
if (!ts.isPropertyAssignment(property)) {
|
|
@@ -45,15 +54,6 @@ function flattenObjectLiteral(node, prefix = "", result = new Map()) {
|
|
|
45
54
|
}
|
|
46
55
|
return result;
|
|
47
56
|
}
|
|
48
|
-
function getPropertyName(name) {
|
|
49
|
-
if (ts.isIdentifier(name)) {
|
|
50
|
-
return name.text;
|
|
51
|
-
}
|
|
52
|
-
if (ts.isStringLiteral(name) || ts.isNumericLiteral(name)) {
|
|
53
|
-
return name.text;
|
|
54
|
-
}
|
|
55
|
-
return null;
|
|
56
|
-
}
|
|
57
57
|
function hasExportModifier(node) {
|
|
58
58
|
return !!node.modifiers?.some((modifier) => modifier.kind === ts.SyntaxKind.ExportKeyword);
|
|
59
59
|
}
|
|
@@ -103,7 +103,10 @@ function collectImportBindings(sourceFile) {
|
|
|
103
103
|
const importedName = element.propertyName
|
|
104
104
|
? element.propertyName.text
|
|
105
105
|
: element.name.text;
|
|
106
|
-
bindings.set(localName, {
|
|
106
|
+
bindings.set(localName, {
|
|
107
|
+
importedName,
|
|
108
|
+
source,
|
|
109
|
+
});
|
|
107
110
|
}
|
|
108
111
|
}
|
|
109
112
|
});
|
|
@@ -222,6 +225,47 @@ function extractPropertyChain(expression) {
|
|
|
222
225
|
}
|
|
223
226
|
return null;
|
|
224
227
|
}
|
|
228
|
+
function matchObjectPath(pattern, actual) {
|
|
229
|
+
if (pattern.length !== actual.length) {
|
|
230
|
+
return false;
|
|
231
|
+
}
|
|
232
|
+
return pattern.every((part, index) => {
|
|
233
|
+
return part === "*" || part === actual[index];
|
|
234
|
+
});
|
|
235
|
+
}
|
|
236
|
+
function resolveObjectValuesByPath(objectMap, pathPattern) {
|
|
237
|
+
const result = new Set();
|
|
238
|
+
for (const [objectPath, translationKey] of objectMap.entries()) {
|
|
239
|
+
const actualPath = objectPath.split(".");
|
|
240
|
+
if (matchObjectPath(pathPattern, actualPath)) {
|
|
241
|
+
result.add(translationKey);
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
return result;
|
|
245
|
+
}
|
|
246
|
+
function resolveImportedObjectAccess(expression, currentFilePath, importBindings, objectRegistry, availableFiles) {
|
|
247
|
+
const chain = extractPropertyChain(expression);
|
|
248
|
+
if (!chain) {
|
|
249
|
+
return null;
|
|
250
|
+
}
|
|
251
|
+
const binding = importBindings.get(chain.root);
|
|
252
|
+
if (!binding) {
|
|
253
|
+
return null;
|
|
254
|
+
}
|
|
255
|
+
const importedFilePath = resolveImportSource(currentFilePath, binding.source, availableFiles);
|
|
256
|
+
if (!importedFilePath) {
|
|
257
|
+
return null;
|
|
258
|
+
}
|
|
259
|
+
const objectMap = objectRegistry.get(`${importedFilePath}::${binding.importedName}`);
|
|
260
|
+
if (!objectMap) {
|
|
261
|
+
return null;
|
|
262
|
+
}
|
|
263
|
+
return {
|
|
264
|
+
importedFilePath,
|
|
265
|
+
importedName: binding.importedName,
|
|
266
|
+
path: chain.path,
|
|
267
|
+
};
|
|
268
|
+
}
|
|
225
269
|
function resolvePropertyChainToTranslationKeys(expression, currentFilePath, importBindings, objectRegistry, availableFiles) {
|
|
226
270
|
const access = resolveImportedObjectAccess(expression, currentFilePath, importBindings, objectRegistry, availableFiles);
|
|
227
271
|
if (!access) {
|
|
@@ -245,7 +289,10 @@ function collectLocalAliases(sourceFile, currentFilePath, importBindings, object
|
|
|
245
289
|
if (importedAccess) {
|
|
246
290
|
const keys = resolvePropertyChainToTranslationKeys(initializer, currentFilePath, importBindings, objectRegistry, availableFiles);
|
|
247
291
|
if (keys.size > 0) {
|
|
248
|
-
keyAliases.set(node.name.text,
|
|
292
|
+
keyAliases.set(node.name.text, {
|
|
293
|
+
keys,
|
|
294
|
+
access: importedAccess,
|
|
295
|
+
});
|
|
249
296
|
}
|
|
250
297
|
else {
|
|
251
298
|
objectAliases.set(node.name.text, importedAccess);
|
|
@@ -255,12 +302,15 @@ function collectLocalAliases(sourceFile, currentFilePath, importBindings, object
|
|
|
255
302
|
ts.forEachChild(node, visit);
|
|
256
303
|
}
|
|
257
304
|
visit(sourceFile);
|
|
258
|
-
return {
|
|
305
|
+
return {
|
|
306
|
+
objectAliases,
|
|
307
|
+
keyAliases,
|
|
308
|
+
};
|
|
259
309
|
}
|
|
260
310
|
function resolveAliasPropertyAccessToTranslationKeys(expression, objectAliases, keyAliases, objectRegistry) {
|
|
261
311
|
const unwrapped = unwrapExpression(expression);
|
|
262
312
|
if (ts.isIdentifier(unwrapped)) {
|
|
263
|
-
return keyAliases.get(unwrapped.text) ?? new Set();
|
|
313
|
+
return keyAliases.get(unwrapped.text)?.keys ?? new Set();
|
|
264
314
|
}
|
|
265
315
|
const chain = extractPropertyChain(unwrapped);
|
|
266
316
|
if (!chain) {
|
|
@@ -277,11 +327,62 @@ function resolveAliasPropertyAccessToTranslationKeys(expression, objectAliases,
|
|
|
277
327
|
const fullPath = [...objectAlias.path, ...chain.path];
|
|
278
328
|
return resolveObjectValuesByPath(objectMap, fullPath);
|
|
279
329
|
}
|
|
330
|
+
function resolveAliasObjectAccess(expression, objectAliases, keyAliases) {
|
|
331
|
+
const unwrapped = unwrapExpression(expression);
|
|
332
|
+
if (ts.isIdentifier(unwrapped)) {
|
|
333
|
+
return keyAliases.get(unwrapped.text)?.access ?? null;
|
|
334
|
+
}
|
|
335
|
+
const chain = extractPropertyChain(unwrapped);
|
|
336
|
+
if (!chain) {
|
|
337
|
+
return null;
|
|
338
|
+
}
|
|
339
|
+
const objectAlias = objectAliases.get(chain.root);
|
|
340
|
+
if (!objectAlias) {
|
|
341
|
+
return null;
|
|
342
|
+
}
|
|
343
|
+
return {
|
|
344
|
+
importedFilePath: objectAlias.importedFilePath,
|
|
345
|
+
importedName: objectAlias.importedName,
|
|
346
|
+
path: [...objectAlias.path, ...chain.path],
|
|
347
|
+
};
|
|
348
|
+
}
|
|
349
|
+
function formatAccessPath(importedName, pathParts) {
|
|
350
|
+
if (pathParts.length === 0) {
|
|
351
|
+
return importedName;
|
|
352
|
+
}
|
|
353
|
+
return `${importedName}.${pathParts.join(".")}`;
|
|
354
|
+
}
|
|
355
|
+
function addResolvedUsages(usages, usageIds, keys, params) {
|
|
356
|
+
for (const key of keys) {
|
|
357
|
+
const usageId = [
|
|
358
|
+
key,
|
|
359
|
+
params.sourceFilePath,
|
|
360
|
+
params.importedFilePath,
|
|
361
|
+
params.importedObjectName,
|
|
362
|
+
params.accessPath,
|
|
363
|
+
params.usageKind,
|
|
364
|
+
].join("::");
|
|
365
|
+
if (usageIds.has(usageId)) {
|
|
366
|
+
continue;
|
|
367
|
+
}
|
|
368
|
+
usageIds.add(usageId);
|
|
369
|
+
usages.push({
|
|
370
|
+
key,
|
|
371
|
+
sourceFilePath: params.sourceFilePath,
|
|
372
|
+
importedFilePath: params.importedFilePath,
|
|
373
|
+
importedObjectName: params.importedObjectName,
|
|
374
|
+
accessPath: params.accessPath,
|
|
375
|
+
usageKind: params.usageKind,
|
|
376
|
+
});
|
|
377
|
+
}
|
|
378
|
+
}
|
|
280
379
|
export async function collectResolvedImportedObjectTranslationKeys(filePaths) {
|
|
281
380
|
const normalizedFilePaths = filePaths.map((filePath) => path.resolve(filePath));
|
|
282
381
|
const sourceFiles = new Map();
|
|
283
382
|
const availableFiles = new Set(normalizedFilePaths);
|
|
284
383
|
const resolvedKeys = new Set();
|
|
384
|
+
const usages = [];
|
|
385
|
+
const usageIds = new Set();
|
|
285
386
|
for (const filePath of normalizedFilePaths) {
|
|
286
387
|
const content = await fs.readFile(filePath, "utf-8");
|
|
287
388
|
sourceFiles.set(filePath, createSourceFile(filePath, content));
|
|
@@ -294,27 +395,36 @@ export async function collectResolvedImportedObjectTranslationKeys(filePaths) {
|
|
|
294
395
|
}
|
|
295
396
|
const importBindings = collectImportBindings(sourceFile);
|
|
296
397
|
const { objectAliases, keyAliases } = collectLocalAliases(sourceFile, filePath, importBindings, objectRegistry, availableFiles);
|
|
297
|
-
function
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
if (firstArg) {
|
|
301
|
-
const directResolved = resolvePropertyChainToTranslationKeys(firstArg, filePath, importBindings, objectRegistry, availableFiles);
|
|
302
|
-
for (const key of directResolved) {
|
|
303
|
-
resolvedKeys.add(key);
|
|
304
|
-
}
|
|
305
|
-
const aliasResolved = resolveAliasPropertyAccessToTranslationKeys(firstArg, objectAliases, keyAliases, objectRegistry);
|
|
306
|
-
for (const key of aliasResolved) {
|
|
307
|
-
resolvedKeys.add(key);
|
|
308
|
-
}
|
|
309
|
-
}
|
|
398
|
+
function addKeys(keys) {
|
|
399
|
+
for (const key of keys) {
|
|
400
|
+
resolvedKeys.add(key);
|
|
310
401
|
}
|
|
311
|
-
|
|
312
|
-
|
|
402
|
+
}
|
|
403
|
+
function collectFromExpression(expression, usageKind) {
|
|
404
|
+
const directResolved = resolvePropertyChainToTranslationKeys(expression, filePath, importBindings, objectRegistry, availableFiles);
|
|
405
|
+
addKeys(directResolved);
|
|
406
|
+
const directAccess = resolveImportedObjectAccess(expression, filePath, importBindings, objectRegistry, availableFiles);
|
|
407
|
+
if (directAccess && directResolved.size > 0) {
|
|
408
|
+
addResolvedUsages(usages, usageIds, directResolved, {
|
|
409
|
+
sourceFilePath: filePath,
|
|
410
|
+
importedFilePath: directAccess.importedFilePath,
|
|
411
|
+
importedObjectName: directAccess.importedName,
|
|
412
|
+
accessPath: formatAccessPath(directAccess.importedName, directAccess.path),
|
|
413
|
+
usageKind,
|
|
414
|
+
});
|
|
313
415
|
}
|
|
314
|
-
|
|
315
|
-
|
|
416
|
+
const aliasResolved = resolveAliasPropertyAccessToTranslationKeys(expression, objectAliases, keyAliases, objectRegistry);
|
|
417
|
+
addKeys(aliasResolved);
|
|
418
|
+
const aliasAccess = resolveAliasObjectAccess(expression, objectAliases, keyAliases);
|
|
419
|
+
if (aliasAccess && aliasResolved.size > 0) {
|
|
420
|
+
addResolvedUsages(usages, usageIds, aliasResolved, {
|
|
421
|
+
sourceFilePath: filePath,
|
|
422
|
+
importedFilePath: aliasAccess.importedFilePath,
|
|
423
|
+
importedObjectName: aliasAccess.importedName,
|
|
424
|
+
accessPath: formatAccessPath(aliasAccess.importedName, aliasAccess.path),
|
|
425
|
+
usageKind,
|
|
426
|
+
});
|
|
316
427
|
}
|
|
317
|
-
ts.forEachChild(node, visit);
|
|
318
428
|
}
|
|
319
429
|
function collectFromTrans(attributes) {
|
|
320
430
|
const i18nKeyAttr = getJsxAttribute(attributes, "i18nKey");
|
|
@@ -323,58 +433,27 @@ export async function collectResolvedImportedObjectTranslationKeys(filePaths) {
|
|
|
323
433
|
!i18nKeyAttr.initializer.expression) {
|
|
324
434
|
return;
|
|
325
435
|
}
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
436
|
+
collectFromExpression(i18nKeyAttr.initializer.expression, "Trans");
|
|
437
|
+
}
|
|
438
|
+
function visit(node) {
|
|
439
|
+
if (ts.isCallExpression(node) && isTranslationCall(node)) {
|
|
440
|
+
const firstArg = node.arguments[0];
|
|
441
|
+
if (firstArg) {
|
|
442
|
+
collectFromExpression(firstArg, "t");
|
|
443
|
+
}
|
|
330
444
|
}
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
445
|
+
if (ts.isJsxSelfClosingElement(node) && isTransTagName(node.tagName)) {
|
|
446
|
+
collectFromTrans(node.attributes);
|
|
447
|
+
}
|
|
448
|
+
if (ts.isJsxOpeningElement(node) && isTransTagName(node.tagName)) {
|
|
449
|
+
collectFromTrans(node.attributes);
|
|
334
450
|
}
|
|
451
|
+
ts.forEachChild(node, visit);
|
|
335
452
|
}
|
|
336
453
|
visit(sourceFile);
|
|
337
454
|
}
|
|
338
|
-
return resolvedKeys;
|
|
339
|
-
}
|
|
340
|
-
function matchObjectPath(pattern, actual) {
|
|
341
|
-
if (pattern.length !== actual.length) {
|
|
342
|
-
return false;
|
|
343
|
-
}
|
|
344
|
-
return pattern.every((part, index) => {
|
|
345
|
-
return part === "*" || part === actual[index];
|
|
346
|
-
});
|
|
347
|
-
}
|
|
348
|
-
function resolveObjectValuesByPath(objectMap, pathPattern) {
|
|
349
|
-
const result = new Set();
|
|
350
|
-
for (const [objectPath, translationKey] of objectMap.entries()) {
|
|
351
|
-
const actualPath = objectPath.split(".");
|
|
352
|
-
if (matchObjectPath(pathPattern, actualPath)) {
|
|
353
|
-
result.add(translationKey);
|
|
354
|
-
}
|
|
355
|
-
}
|
|
356
|
-
return result;
|
|
357
|
-
}
|
|
358
|
-
function resolveImportedObjectAccess(expression, currentFilePath, importBindings, objectRegistry, availableFiles) {
|
|
359
|
-
const chain = extractPropertyChain(expression);
|
|
360
|
-
if (!chain) {
|
|
361
|
-
return null;
|
|
362
|
-
}
|
|
363
|
-
const binding = importBindings.get(chain.root);
|
|
364
|
-
if (!binding) {
|
|
365
|
-
return null;
|
|
366
|
-
}
|
|
367
|
-
const importedFilePath = resolveImportSource(currentFilePath, binding.source, availableFiles);
|
|
368
|
-
if (!importedFilePath) {
|
|
369
|
-
return null;
|
|
370
|
-
}
|
|
371
|
-
const objectMap = objectRegistry.get(`${importedFilePath}::${binding.importedName}`);
|
|
372
|
-
if (!objectMap) {
|
|
373
|
-
return null;
|
|
374
|
-
}
|
|
375
455
|
return {
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
path: chain.path,
|
|
456
|
+
keys: resolvedKeys,
|
|
457
|
+
usages,
|
|
379
458
|
};
|
|
380
459
|
}
|
|
@@ -246,7 +246,9 @@ export async function deepVerifyUnusedKeys(sourceFiles, unusedKeysByFile) {
|
|
|
246
246
|
const definitelyUnusedByFile = {};
|
|
247
247
|
const possiblyDynamicallyUsedByFile = {};
|
|
248
248
|
const evidenceList = await Promise.all(sourceFiles.map((file) => collectFileEvidence(file)));
|
|
249
|
-
const
|
|
249
|
+
const resolvedImportedObjectResult = await collectResolvedImportedObjectTranslationKeys(sourceFiles);
|
|
250
|
+
const resolvedImportedObjectKeys = resolvedImportedObjectResult.keys;
|
|
251
|
+
const resolvedImportedObjectUsages = resolvedImportedObjectResult.usages;
|
|
250
252
|
for (const [translationFilePath, unusedKeys] of Object.entries(unusedKeysByFile)) {
|
|
251
253
|
const definitelyUnused = [];
|
|
252
254
|
const possiblyDynamic = [];
|
|
@@ -264,5 +266,6 @@ export async function deepVerifyUnusedKeys(sourceFiles, unusedKeysByFile) {
|
|
|
264
266
|
return {
|
|
265
267
|
definitelyUnusedByFile,
|
|
266
268
|
possiblyDynamicallyUsedByFile,
|
|
269
|
+
resolvedImportedObjectUsages,
|
|
267
270
|
};
|
|
268
271
|
}
|
package/dist/core/scanner.js
CHANGED
|
@@ -7,6 +7,7 @@ import { writeDynamicTranslationReport } from "./actions/writeDynamicTranslation
|
|
|
7
7
|
import { syncMissingTranslations } from "./actions/syncMissingTranslations.js";
|
|
8
8
|
import { removeUnusedTranslations } from "./actions/removeUnusedTranslations.js";
|
|
9
9
|
import { writeUnusedTranslationsReport } from "./actions/writeUnusedTranslationsReport.js";
|
|
10
|
+
import { writeResolvedImportedObjectUsagesReport } from "./actions/writeResolvedImportedObjectUsageReport.js";
|
|
10
11
|
import { deepVerifyUnusedKeys } from "./analysis/deepVerifyUnusedKeys.js";
|
|
11
12
|
export async function runScanner(config) {
|
|
12
13
|
const files = await getFiles(config.srcPaths);
|
|
@@ -16,6 +17,7 @@ export async function runScanner(config) {
|
|
|
16
17
|
const jsonData = await loadJsonKeys(config.jsonDir);
|
|
17
18
|
const result = analyzeKeys(usedKeys, jsonData);
|
|
18
19
|
const verification = await deepVerifyUnusedKeys(files, result.unusedKeysByFile);
|
|
20
|
+
await writeResolvedImportedObjectUsagesReport(verification.resolvedImportedObjectUsages ?? [], config.outputDir);
|
|
19
21
|
await writeUnusedTranslationsReport(verification.definitelyUnusedByFile, jsonData, config.outputDir);
|
|
20
22
|
if (config.syncMissing) {
|
|
21
23
|
await syncMissingTranslations(result, jsonData, config.sortJson);
|