react-i18next-scanner 0.1.5 → 0.1.6

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.
@@ -154,106 +154,157 @@ function getJsxAttribute(attributes, attributeName) {
154
154
  }
155
155
  return undefined;
156
156
  }
157
+ function unwrapExpression(expression) {
158
+ let current = expression;
159
+ while (true) {
160
+ if (ts.isParenthesizedExpression(current)) {
161
+ current = current.expression;
162
+ continue;
163
+ }
164
+ if (ts.isAsExpression(current)) {
165
+ current = current.expression;
166
+ continue;
167
+ }
168
+ if (ts.isTypeAssertionExpression(current)) {
169
+ current = current.expression;
170
+ continue;
171
+ }
172
+ if (ts.isNonNullExpression(current)) {
173
+ current = current.expression;
174
+ continue;
175
+ }
176
+ if (ts.isCallExpression(current) &&
177
+ ts.isIdentifier(current.expression) &&
178
+ current.expression.text === "String" &&
179
+ current.arguments[0]) {
180
+ current = current.arguments[0];
181
+ continue;
182
+ }
183
+ return current;
184
+ }
185
+ }
157
186
  function extractPropertyChain(expression) {
158
- if (ts.isIdentifier(expression)) {
187
+ const unwrapped = unwrapExpression(expression);
188
+ if (ts.isIdentifier(unwrapped)) {
159
189
  return {
160
- root: expression.text,
190
+ root: unwrapped.text,
161
191
  path: [],
162
192
  };
163
193
  }
164
- if (ts.isPropertyAccessExpression(expression)) {
165
- const left = extractPropertyChain(expression.expression);
194
+ if (ts.isPropertyAccessExpression(unwrapped)) {
195
+ const left = extractPropertyChain(unwrapped.expression);
166
196
  if (!left) {
167
197
  return null;
168
198
  }
169
199
  return {
170
200
  root: left.root,
171
- path: [...left.path, expression.name.text],
201
+ path: [...left.path, unwrapped.name.text],
172
202
  };
173
203
  }
174
- if (ts.isElementAccessExpression(expression)) {
175
- const left = extractPropertyChain(expression.expression);
176
- if (!left || !expression.argumentExpression) {
204
+ if (ts.isElementAccessExpression(unwrapped)) {
205
+ const left = extractPropertyChain(unwrapped.expression);
206
+ if (!left) {
177
207
  return null;
178
208
  }
179
- if (ts.isStringLiteral(expression.argumentExpression) ||
180
- ts.isNoSubstitutionTemplateLiteral(expression.argumentExpression)) {
209
+ const argument = unwrapped.argumentExpression;
210
+ if (argument &&
211
+ (ts.isStringLiteral(argument) ||
212
+ ts.isNoSubstitutionTemplateLiteral(argument))) {
181
213
  return {
182
214
  root: left.root,
183
- path: [...left.path, expression.argumentExpression.text],
215
+ path: [...left.path, argument.text],
184
216
  };
185
217
  }
218
+ return {
219
+ root: left.root,
220
+ path: [...left.path, "*"],
221
+ };
186
222
  }
187
223
  return null;
188
224
  }
189
- function resolvePropertyChainToTranslationKey(expression, currentFilePath, importBindings, objectRegistry, availableFiles) {
190
- const chain = extractPropertyChain(expression);
191
- if (!chain) {
192
- return null;
225
+ function resolvePropertyChainToTranslationKeys(expression, currentFilePath, importBindings, objectRegistry, availableFiles) {
226
+ const access = resolveImportedObjectAccess(expression, currentFilePath, importBindings, objectRegistry, availableFiles);
227
+ if (!access) {
228
+ return new Set();
193
229
  }
194
- const binding = importBindings.get(chain.root);
195
- if (!binding) {
196
- return null;
197
- }
198
- const importedFilePath = resolveImportSource(currentFilePath, binding.source, availableFiles);
199
- if (!importedFilePath) {
200
- return null;
230
+ const objectMap = objectRegistry.get(`${access.importedFilePath}::${access.importedName}`);
231
+ if (!objectMap || access.path.length === 0) {
232
+ return new Set();
201
233
  }
202
- const objectMap = objectRegistry.get(`${importedFilePath}::${binding.importedName}`);
203
- if (!objectMap) {
204
- return null;
205
- }
206
- const propertyPath = chain.path.join(".");
207
- if (!propertyPath) {
208
- return null;
209
- }
210
- return objectMap.get(propertyPath) ?? null;
234
+ return resolveObjectValuesByPath(objectMap, access.path);
211
235
  }
212
236
  function collectLocalAliases(sourceFile, currentFilePath, importBindings, objectRegistry, availableFiles) {
213
- const aliases = new Map();
237
+ const objectAliases = new Map();
238
+ const keyAliases = new Map();
214
239
  function visit(node) {
215
240
  if (ts.isVariableDeclaration(node) &&
216
241
  ts.isIdentifier(node.name) &&
217
242
  node.initializer) {
218
- const resolvedKey = resolvePropertyChainToTranslationKey(node.initializer, currentFilePath, importBindings, objectRegistry, availableFiles);
219
- if (resolvedKey) {
220
- aliases.set(node.name.text, resolvedKey);
243
+ const initializer = unwrapExpression(node.initializer);
244
+ const importedAccess = resolveImportedObjectAccess(initializer, currentFilePath, importBindings, objectRegistry, availableFiles);
245
+ if (importedAccess) {
246
+ const keys = resolvePropertyChainToTranslationKeys(initializer, currentFilePath, importBindings, objectRegistry, availableFiles);
247
+ if (keys.size > 0) {
248
+ keyAliases.set(node.name.text, keys);
249
+ }
250
+ else {
251
+ objectAliases.set(node.name.text, importedAccess);
252
+ }
221
253
  }
222
254
  }
223
255
  ts.forEachChild(node, visit);
224
256
  }
225
257
  visit(sourceFile);
226
- return aliases;
258
+ return { objectAliases, keyAliases };
259
+ }
260
+ function resolveAliasPropertyAccessToTranslationKeys(expression, objectAliases, keyAliases, objectRegistry) {
261
+ const unwrapped = unwrapExpression(expression);
262
+ if (ts.isIdentifier(unwrapped)) {
263
+ return keyAliases.get(unwrapped.text) ?? new Set();
264
+ }
265
+ const chain = extractPropertyChain(unwrapped);
266
+ if (!chain) {
267
+ return new Set();
268
+ }
269
+ const objectAlias = objectAliases.get(chain.root);
270
+ if (!objectAlias) {
271
+ return new Set();
272
+ }
273
+ const objectMap = objectRegistry.get(`${objectAlias.importedFilePath}::${objectAlias.importedName}`);
274
+ if (!objectMap) {
275
+ return new Set();
276
+ }
277
+ const fullPath = [...objectAlias.path, ...chain.path];
278
+ return resolveObjectValuesByPath(objectMap, fullPath);
227
279
  }
228
280
  export async function collectResolvedImportedObjectTranslationKeys(filePaths) {
281
+ const normalizedFilePaths = filePaths.map((filePath) => path.resolve(filePath));
229
282
  const sourceFiles = new Map();
230
- const availableFiles = new Set(filePaths);
283
+ const availableFiles = new Set(normalizedFilePaths);
231
284
  const resolvedKeys = new Set();
232
- for (const filePath of filePaths) {
285
+ for (const filePath of normalizedFilePaths) {
233
286
  const content = await fs.readFile(filePath, "utf-8");
234
287
  sourceFiles.set(filePath, createSourceFile(filePath, content));
235
288
  }
236
- const objectRegistry = buildObjectRegistry(filePaths, sourceFiles);
237
- for (const filePath of filePaths) {
289
+ const objectRegistry = buildObjectRegistry(normalizedFilePaths, sourceFiles);
290
+ for (const filePath of normalizedFilePaths) {
238
291
  const sourceFile = sourceFiles.get(filePath);
239
292
  if (!sourceFile) {
240
293
  continue;
241
294
  }
242
295
  const importBindings = collectImportBindings(sourceFile);
243
- const localAliases = collectLocalAliases(sourceFile, filePath, importBindings, objectRegistry, availableFiles);
296
+ const { objectAliases, keyAliases } = collectLocalAliases(sourceFile, filePath, importBindings, objectRegistry, availableFiles);
244
297
  function visit(node) {
245
298
  if (ts.isCallExpression(node) && isTranslationCall(node)) {
246
299
  const firstArg = node.arguments[0];
247
300
  if (firstArg) {
248
- const directResolved = resolvePropertyChainToTranslationKey(firstArg, filePath, importBindings, objectRegistry, availableFiles);
249
- if (directResolved) {
250
- resolvedKeys.add(directResolved);
301
+ const directResolved = resolvePropertyChainToTranslationKeys(firstArg, filePath, importBindings, objectRegistry, availableFiles);
302
+ for (const key of directResolved) {
303
+ resolvedKeys.add(key);
251
304
  }
252
- if (ts.isIdentifier(firstArg)) {
253
- const aliasResolved = localAliases.get(firstArg.text);
254
- if (aliasResolved) {
255
- resolvedKeys.add(aliasResolved);
256
- }
305
+ const aliasResolved = resolveAliasPropertyAccessToTranslationKeys(firstArg, objectAliases, keyAliases, objectRegistry);
306
+ for (const key of aliasResolved) {
307
+ resolvedKeys.add(key);
257
308
  }
258
309
  }
259
310
  }
@@ -273,18 +324,57 @@ export async function collectResolvedImportedObjectTranslationKeys(filePaths) {
273
324
  return;
274
325
  }
275
326
  const expr = i18nKeyAttr.initializer.expression;
276
- const directResolved = resolvePropertyChainToTranslationKey(expr, filePath, importBindings, objectRegistry, availableFiles);
277
- if (directResolved) {
278
- resolvedKeys.add(directResolved);
327
+ const directResolved = resolvePropertyChainToTranslationKeys(expr, filePath, importBindings, objectRegistry, availableFiles);
328
+ for (const key of directResolved) {
329
+ resolvedKeys.add(key);
279
330
  }
280
- if (ts.isIdentifier(expr)) {
281
- const aliasResolved = localAliases.get(expr.text);
282
- if (aliasResolved) {
283
- resolvedKeys.add(aliasResolved);
284
- }
331
+ const aliasResolved = resolveAliasPropertyAccessToTranslationKeys(expr, objectAliases, keyAliases, objectRegistry);
332
+ for (const key of aliasResolved) {
333
+ resolvedKeys.add(key);
285
334
  }
286
335
  }
287
336
  visit(sourceFile);
288
337
  }
289
338
  return resolvedKeys;
290
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
+ return {
376
+ importedFilePath,
377
+ importedName: binding.importedName,
378
+ path: chain.path,
379
+ };
380
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "react-i18next-scanner",
3
- "version": "0.1.5",
3
+ "version": "0.1.6",
4
4
  "description": "CLI tool to scan React projects for missing and unused react-i18next translation keys",
5
5
  "bin": "./dist/cli/cli.js",
6
6
  "scripts": {