deslop-js 0.0.11 → 0.0.12

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.
Files changed (3) hide show
  1. package/dist/index.cjs +238 -60
  2. package/dist/index.mjs +238 -60
  3. package/package.json +1 -1
package/dist/index.cjs CHANGED
@@ -1078,10 +1078,22 @@ const containsCallOrPromiseSurface = (node, recursionDepth = 0) => {
1078
1078
  }
1079
1079
  return false;
1080
1080
  };
1081
+ const unwrapParenthesizedExpression = (node) => {
1082
+ let current = node;
1083
+ while (current.type === "ParenthesizedExpression") {
1084
+ const inner = current.expression;
1085
+ if (!inner || !isOxcAstNode(inner)) return current;
1086
+ current = inner;
1087
+ }
1088
+ return current;
1089
+ };
1081
1090
  const isSimpleReturnArgument = (argumentNode) => {
1082
1091
  if (!isOxcAstNode(argumentNode)) return false;
1083
- if (argumentNode.type === "BlockStatement") return false;
1084
- if (argumentNode.type === "ObjectExpression") return false;
1092
+ const unwrapped = unwrapParenthesizedExpression(argumentNode);
1093
+ if (unwrapped.type === "BlockStatement") return false;
1094
+ if (unwrapped.type === "ObjectExpression") return false;
1095
+ if (unwrapped.type === "JSXElement") return false;
1096
+ if (unwrapped.type === "JSXFragment") return false;
1085
1097
  return true;
1086
1098
  };
1087
1099
  const detectBlockArrowSingleReturn = (functionNode) => {
@@ -1135,9 +1147,34 @@ const detectRedundantAwaitReturn = (functionNode) => {
1135
1147
  };
1136
1148
  };
1137
1149
  const isAsyncFunction = (functionNode) => Boolean(functionNode.async);
1138
- const detectUselessAsync = (functionNode) => {
1150
+ const containsPromiseTypeReference = (node, recursionDepth = 0) => {
1151
+ if (recursionDepth > 30) return false;
1152
+ if (!isOxcAstNode(node)) return false;
1153
+ if (node.type === "TSTypeReference") {
1154
+ const typeName = node.typeName;
1155
+ if (typeName?.name === "Promise") return true;
1156
+ if (typeName?.right?.name === "Promise") return true;
1157
+ }
1158
+ for (const value of Object.values(node)) if (Array.isArray(value)) {
1159
+ for (const element of value) if (containsPromiseTypeReference(element, recursionDepth + 1)) return true;
1160
+ } else if (isOxcAstNode(value)) {
1161
+ if (containsPromiseTypeReference(value, recursionDepth + 1)) return true;
1162
+ }
1163
+ return false;
1164
+ };
1165
+ const hasExplicitPromiseReturnType = (functionNode) => {
1166
+ const returnType = functionNode.returnType;
1167
+ if (!returnType || !isOxcAstNode(returnType)) return false;
1168
+ const annotation = returnType.typeAnnotation;
1169
+ if (!annotation || !isOxcAstNode(annotation)) return false;
1170
+ return containsPromiseTypeReference(annotation);
1171
+ };
1172
+ const detectUselessAsync = (functionNode, context) => {
1139
1173
  if (!isAsyncFunction(functionNode)) return void 0;
1140
1174
  if (functionNode.type === "ClassDeclaration" || functionNode.type === "MethodDefinition") return;
1175
+ if (context.isMethodContext) return void 0;
1176
+ if (context.isInlineCallback) return void 0;
1177
+ if (hasExplicitPromiseReturnType(functionNode)) return void 0;
1141
1178
  const bodyNode = functionNode.body;
1142
1179
  if (!isOxcAstNode(bodyNode)) return void 0;
1143
1180
  if (containsAwaitExpression(bodyNode)) return void 0;
@@ -1149,14 +1186,14 @@ const detectUselessAsync = (functionNode) => {
1149
1186
  suggestion: "drop `async` (caller's existing `await` keeps the type identical) or add an explicit return type"
1150
1187
  };
1151
1188
  };
1152
- const detectSimplifiableFunctionPatterns = (functionNode) => {
1189
+ const detectSimplifiableFunctionPatterns = (functionNode, context = {}) => {
1153
1190
  if (!isOxcAstNode(functionNode)) return [];
1154
1191
  const findings = [];
1155
1192
  const blockArrow = detectBlockArrowSingleReturn(functionNode);
1156
1193
  if (blockArrow) findings.push(blockArrow);
1157
1194
  const awaitReturn = detectRedundantAwaitReturn(functionNode);
1158
1195
  if (awaitReturn) findings.push(awaitReturn);
1159
- const uselessAsync = detectUselessAsync(functionNode);
1196
+ const uselessAsync = detectUselessAsync(functionNode, context);
1160
1197
  if (uselessAsync) findings.push(uselessAsync);
1161
1198
  return findings;
1162
1199
  };
@@ -1169,9 +1206,12 @@ const inferFunctionName = (functionNode, parentContext) => {
1169
1206
  if (declaredId?.name) return declaredId.name;
1170
1207
  return parentContext;
1171
1208
  };
1172
- const visitFunctionAndDescend = (functionNode, captures, contextName, recursionDepth) => {
1209
+ const visitFunctionAndDescend = (functionNode, captures, contextName, recursionDepth, isMethodContext, isInlineCallback) => {
1173
1210
  const functionName = inferFunctionName(functionNode, contextName);
1174
- const detections = detectSimplifiableFunctionPatterns(functionNode);
1211
+ const detections = detectSimplifiableFunctionPatterns(functionNode, {
1212
+ isMethodContext,
1213
+ isInlineCallback
1214
+ });
1175
1215
  for (const detection of detections) captures.push({
1176
1216
  kind: detection.kind,
1177
1217
  functionName,
@@ -1184,10 +1224,13 @@ const visitFunctionAndDescend = (functionNode, captures, contextName, recursionD
1184
1224
  const parameters = functionNode.params ?? [];
1185
1225
  for (const parameter of parameters) if (isOxcAstNode(parameter)) walkForFunctions(parameter, captures, functionName, recursionDepth + 1);
1186
1226
  };
1227
+ const isObjectMethodShorthand = (node) => (node.type === "Property" || node.type === "ObjectProperty") && node.method === true;
1228
+ const isObjectPropertyAssignment = (node) => (node.type === "Property" || node.type === "ObjectProperty") && node.method !== true;
1229
+ const isCallOrNewExpression = (node) => node.type === "CallExpression" || node.type === "NewExpression";
1187
1230
  const walkForFunctions = (node, captures, contextName, recursionDepth = 0) => {
1188
1231
  if (recursionDepth > 200) return;
1189
1232
  if (looksLikeFunction(node)) {
1190
- visitFunctionAndDescend(node, captures, contextName, recursionDepth);
1233
+ visitFunctionAndDescend(node, captures, contextName, recursionDepth, false, false);
1191
1234
  return;
1192
1235
  }
1193
1236
  let nextContext = contextName;
@@ -1203,6 +1246,35 @@ const walkForFunctions = (node, captures, contextName, recursionDepth = 0) => {
1203
1246
  const className = getIdentifierName(node.id);
1204
1247
  if (className) nextContext = className;
1205
1248
  }
1249
+ if (node.type === "MethodDefinition" || isObjectMethodShorthand(node)) {
1250
+ const methodValue = node.value;
1251
+ if (methodValue && isOxcAstNode(methodValue) && looksLikeFunction(methodValue)) {
1252
+ visitFunctionAndDescend(methodValue, captures, getIdentifierName(node.key) ?? nextContext, recursionDepth + 1, true, false);
1253
+ const keyNode = node.key;
1254
+ if (keyNode && isOxcAstNode(keyNode) && node.computed) walkForFunctions(keyNode, captures, nextContext, recursionDepth + 1);
1255
+ return;
1256
+ }
1257
+ }
1258
+ if (isObjectPropertyAssignment(node)) {
1259
+ const propertyValue = node.value;
1260
+ if (propertyValue && isOxcAstNode(propertyValue) && looksLikeFunction(propertyValue)) {
1261
+ visitFunctionAndDescend(propertyValue, captures, getIdentifierName(node.key) ?? nextContext, recursionDepth + 1, false, true);
1262
+ const keyNode = node.key;
1263
+ if (keyNode && isOxcAstNode(keyNode) && node.computed) walkForFunctions(keyNode, captures, nextContext, recursionDepth + 1);
1264
+ return;
1265
+ }
1266
+ }
1267
+ if (isCallOrNewExpression(node)) {
1268
+ const callee = node.callee;
1269
+ if (callee && isOxcAstNode(callee)) walkForFunctions(callee, captures, nextContext, recursionDepth + 1);
1270
+ const callArguments = node.arguments ?? [];
1271
+ for (const argument of callArguments) {
1272
+ if (!isOxcAstNode(argument)) continue;
1273
+ if (looksLikeFunction(argument)) visitFunctionAndDescend(argument, captures, nextContext, recursionDepth + 1, false, true);
1274
+ else walkForFunctions(argument, captures, nextContext, recursionDepth + 1);
1275
+ }
1276
+ return;
1277
+ }
1206
1278
  for (const value of Object.values(node)) if (Array.isArray(value)) {
1207
1279
  for (const element of value) if (isOxcAstNode(element)) walkForFunctions(element, captures, nextContext, recursionDepth + 1);
1208
1280
  } else if (isOxcAstNode(value)) walkForFunctions(value, captures, nextContext, recursionDepth + 1);
@@ -1535,10 +1607,15 @@ const CSS_EXTENSIONS = [
1535
1607
  ];
1536
1608
  const CSS_IMPORT_PATTERN = /@import\s+(?:url\()?['"]([^'"]+)['"]\)?/g;
1537
1609
  const SCSS_USE_FORWARD_PATTERN = /@(?:use|forward)\s+['"]([^'"]+)['"]/g;
1610
+ const TAILWIND_PLUGIN_REFERENCE_PATTERN = /@(?:plugin|reference|config)\s+['"]([^'"]+)['"]/g;
1538
1611
  const parseCssImports = (filePath) => {
1539
1612
  const sourceText = (0, node_fs.readFileSync)(filePath, "utf-8");
1540
1613
  const imports = [];
1541
- const patterns = [CSS_IMPORT_PATTERN, SCSS_USE_FORWARD_PATTERN];
1614
+ const patterns = [
1615
+ CSS_IMPORT_PATTERN,
1616
+ SCSS_USE_FORWARD_PATTERN,
1617
+ TAILWIND_PLUGIN_REFERENCE_PATTERN
1618
+ ];
1542
1619
  for (const pattern of patterns) {
1543
1620
  let match;
1544
1621
  pattern.lastIndex = 0;
@@ -1561,6 +1638,7 @@ const parseCssImports = (filePath) => {
1561
1638
  memberAccesses: [],
1562
1639
  wholeObjectUses: [],
1563
1640
  localIdentifierReferences: [],
1641
+ referencedFilenames: [],
1564
1642
  redundantTypePatterns: [],
1565
1643
  identityWrappers: [],
1566
1644
  typeDefinitionHashes: [],
@@ -1600,6 +1678,7 @@ const createEmptyParsedSource = () => ({
1600
1678
  memberAccesses: [],
1601
1679
  wholeObjectUses: [],
1602
1680
  localIdentifierReferences: [],
1681
+ referencedFilenames: [],
1603
1682
  redundantTypePatterns: [],
1604
1683
  identityWrappers: [],
1605
1684
  typeDefinitionHashes: [],
@@ -1743,6 +1822,7 @@ const parseSourceFile = (filePath) => {
1743
1822
  ...createEmptyParsedSource(),
1744
1823
  imports,
1745
1824
  exports,
1825
+ referencedFilenames: extractReferencedFilenames(sourceText),
1746
1826
  errors: [...earlyErrors, new ParseError({
1747
1827
  code: "parse-recovered",
1748
1828
  severity: "info",
@@ -1755,6 +1835,7 @@ const parseSourceFile = (filePath) => {
1755
1835
  ...createEmptyParsedSource(),
1756
1836
  imports,
1757
1837
  exports,
1838
+ referencedFilenames: extractReferencedFilenames(sourceText),
1758
1839
  errors: [...earlyErrors, new ParseError({
1759
1840
  code: "parse-failed",
1760
1841
  message: "oxc-parser returned no program body",
@@ -1807,50 +1888,63 @@ const parseSourceFile = (filePath) => {
1807
1888
  safeWalk("collectDryPatterns", () => {
1808
1889
  collectDryPatterns(program.body, sourceText, redundantTypePatterns, identityWrappers, typeDefinitionHashes);
1809
1890
  }, void 0);
1891
+ const inlineTypeLiterals = safeWalk("collectInlineTypeLiterals", () => collectInlineTypeLiterals(program.body), []).map((capture) => ({
1892
+ structuralHash: capture.structuralHash,
1893
+ memberCount: capture.memberCount,
1894
+ preview: capture.preview,
1895
+ context: capture.context,
1896
+ nearestName: capture.nearestName,
1897
+ line: getLineFromOffset(sourceText, capture.startOffset),
1898
+ column: getColumnFromOffset(sourceText, capture.startOffset)
1899
+ }));
1900
+ const simplifiableFunctions = safeWalk("collectSimplifiableFunctions", () => collectSimplifiableFunctions(program.body), []).map((capture) => ({
1901
+ kind: capture.kind,
1902
+ functionName: capture.functionName,
1903
+ line: getLineFromOffset(sourceText, capture.startOffset),
1904
+ column: getColumnFromOffset(sourceText, capture.startOffset),
1905
+ reason: capture.reason,
1906
+ suggestion: capture.suggestion
1907
+ }));
1908
+ const simplifiableExpressions = safeWalk("collectSimplifiableExpressions", () => collectSimplifiableExpressions(program.body), []).map((capture) => ({
1909
+ kind: capture.kind,
1910
+ snippet: capture.snippet,
1911
+ line: getLineFromOffset(sourceText, capture.startOffset),
1912
+ column: getColumnFromOffset(sourceText, capture.startOffset),
1913
+ reason: capture.reason,
1914
+ suggestion: capture.suggestion
1915
+ }));
1916
+ const duplicateConstantCandidates = safeWalk("collectDuplicateConstantCandidates", () => collectDuplicateConstantCandidates(program.body), []).map((capture) => ({
1917
+ constantName: capture.constantName,
1918
+ literalHash: capture.literalHash,
1919
+ literalPreview: capture.literalPreview,
1920
+ line: getLineFromOffset(sourceText, capture.startOffset),
1921
+ column: getColumnFromOffset(sourceText, capture.startOffset)
1922
+ }));
1810
1923
  return {
1811
1924
  imports,
1812
1925
  exports,
1813
1926
  memberAccesses,
1814
1927
  wholeObjectUses,
1815
1928
  localIdentifierReferences,
1929
+ referencedFilenames: extractReferencedFilenames(sourceText),
1816
1930
  redundantTypePatterns,
1817
1931
  identityWrappers,
1818
1932
  typeDefinitionHashes,
1819
- inlineTypeLiterals: safeWalk("collectInlineTypeLiterals", () => collectInlineTypeLiterals(program.body), []).map((capture) => ({
1820
- structuralHash: capture.structuralHash,
1821
- memberCount: capture.memberCount,
1822
- preview: capture.preview,
1823
- context: capture.context,
1824
- nearestName: capture.nearestName,
1825
- line: getLineFromOffset(sourceText, capture.startOffset),
1826
- column: getColumnFromOffset(sourceText, capture.startOffset)
1827
- })),
1828
- simplifiableFunctions: safeWalk("collectSimplifiableFunctions", () => collectSimplifiableFunctions(program.body), []).map((capture) => ({
1829
- kind: capture.kind,
1830
- functionName: capture.functionName,
1831
- line: getLineFromOffset(sourceText, capture.startOffset),
1832
- column: getColumnFromOffset(sourceText, capture.startOffset),
1833
- reason: capture.reason,
1834
- suggestion: capture.suggestion
1835
- })),
1836
- simplifiableExpressions: safeWalk("collectSimplifiableExpressions", () => collectSimplifiableExpressions(program.body), []).map((capture) => ({
1837
- kind: capture.kind,
1838
- snippet: capture.snippet,
1839
- line: getLineFromOffset(sourceText, capture.startOffset),
1840
- column: getColumnFromOffset(sourceText, capture.startOffset),
1841
- reason: capture.reason,
1842
- suggestion: capture.suggestion
1843
- })),
1844
- duplicateConstantCandidates: safeWalk("collectDuplicateConstantCandidates", () => collectDuplicateConstantCandidates(program.body), []).map((capture) => ({
1845
- constantName: capture.constantName,
1846
- literalHash: capture.literalHash,
1847
- literalPreview: capture.literalPreview,
1848
- line: getLineFromOffset(sourceText, capture.startOffset),
1849
- column: getColumnFromOffset(sourceText, capture.startOffset)
1850
- })),
1933
+ inlineTypeLiterals,
1934
+ simplifiableFunctions,
1935
+ simplifiableExpressions,
1936
+ duplicateConstantCandidates,
1851
1937
  errors: [...earlyErrors, ...detectorErrors]
1852
1938
  };
1853
1939
  };
1940
+ const REFERENCED_FILENAME_LITERAL_PATTERN = /(?<![./@\w-])(?:["'`])([a-z][\w-]*\.(?:ts|tsx|js|jsx|mts|mjs|cts|cjs))(?:["'`])/g;
1941
+ const extractReferencedFilenames = (sourceText) => {
1942
+ const captured = /* @__PURE__ */ new Set();
1943
+ REFERENCED_FILENAME_LITERAL_PATTERN.lastIndex = 0;
1944
+ let match;
1945
+ while ((match = REFERENCED_FILENAME_LITERAL_PATTERN.exec(sourceText)) !== null) captured.add(match[1]);
1946
+ return [...captured];
1947
+ };
1854
1948
  const collectDryPatterns = (bodyNodes, sourceText, redundantTypePatterns, identityWrappers, typeDefinitionHashes) => {
1855
1949
  for (const statement of bodyNodes) inspectStatement(statement, sourceText, redundantTypePatterns, identityWrappers, typeDefinitionHashes);
1856
1950
  };
@@ -3452,8 +3546,11 @@ const resolveEntries = async (config) => {
3452
3546
  }
3453
3547
  const frameworkEntries = detectFrameworkEntries(absoluteRoot);
3454
3548
  const entryEligiblePackages = workspacePackages.filter(isEntryEligible);
3549
+ const monorepoRootForEntries = findMonorepoRoot(absoluteRoot);
3550
+ const ancestorPackageJsonRoots = monorepoRootForEntries && monorepoRootForEntries !== absoluteRoot ? [monorepoRootForEntries] : [];
3455
3551
  const scriptEntries = extractScriptEntries(absoluteRoot);
3456
3552
  for (const workspacePackage of entryEligiblePackages) scriptEntries.push(...extractScriptEntries(workspacePackage.directory));
3553
+ for (const ancestorRoot of ancestorPackageJsonRoots) for (const entryPath of extractScriptEntries(ancestorRoot)) if (entryPath.startsWith(`${absoluteRoot}/`)) scriptEntries.push(entryPath);
3457
3554
  const webpackEntries = extractWebpackEntryPoints(absoluteRoot);
3458
3555
  for (const workspacePackage of entryEligiblePackages) webpackEntries.push(...extractWebpackEntryPoints(workspacePackage.directory));
3459
3556
  const viteEntries = extractViteEntryPoints(absoluteRoot);
@@ -6527,6 +6624,7 @@ const buildDependencyGraph = (inputs) => {
6527
6624
  memberAccesses: input.parsed.memberAccesses,
6528
6625
  wholeObjectUses: input.parsed.wholeObjectUses,
6529
6626
  localIdentifierReferences: input.parsed.localIdentifierReferences,
6627
+ referencedFilenames: input.parsed.referencedFilenames,
6530
6628
  redundantTypePatterns: input.parsed.redundantTypePatterns,
6531
6629
  identityWrappers: input.parsed.identityWrappers,
6532
6630
  typeDefinitionHashes: input.parsed.typeDefinitionHashes,
@@ -6854,6 +6952,21 @@ const hasExcludedExtension = (filePath) => {
6854
6952
  return EXCLUDED_EXTENSIONS.has(filePath.slice(lastDot));
6855
6953
  };
6856
6954
  const isExcludedByPattern = (filePath) => TEST_FILE_PATTERN.test(filePath) || EXCLUDED_DIRECTORY_PATTERN.test(filePath) || CONFIG_FILE_PATTERN.test(filePath);
6955
+ /**
6956
+ * Files the parser couldn't analyze (minified bundles, oversized files, binaries)
6957
+ * have no detectable imports — they're effectively opaque. Flagging them as
6958
+ * "unused" is a false positive because we can't see who imports them, and they
6959
+ * may be static assets, generated bundles, or build artifacts that get loaded
6960
+ * outside the JS module graph (HTML `<script src>`, `vite-plugin-string`, etc.).
6961
+ * The parser already records a `file-minified`/`file-too-large`/`file-binary`
6962
+ * info-level entry in `analysisErrors`, which is the actionable signal.
6963
+ */
6964
+ const PARSE_OPAQUE_ERROR_CODES = new Set([
6965
+ "file-minified",
6966
+ "file-too-large",
6967
+ "file-binary"
6968
+ ]);
6969
+ const isOpaqueToAnalysis = (module) => module.parseErrors.some((parseError) => parseError.code && PARSE_OPAQUE_ERROR_CODES.has(parseError.code));
6857
6970
  const detectOrphanFiles = (graph) => {
6858
6971
  const unusedFiles = [];
6859
6972
  for (const module of graph.modules) {
@@ -6863,6 +6976,7 @@ const detectOrphanFiles = (graph) => {
6863
6976
  if (module.isConfigFile) continue;
6864
6977
  if (hasExcludedExtension(module.fileId.path)) continue;
6865
6978
  if (isExcludedByPattern(module.fileId.path)) continue;
6979
+ if (isOpaqueToAnalysis(module)) continue;
6866
6980
  if (isBarrelWithReachableSources(module, graph)) continue;
6867
6981
  if (hasReachableDirectImporter(module.fileId.index, graph)) continue;
6868
6982
  unusedFiles.push({ path: module.fileId.path });
@@ -7193,13 +7307,13 @@ const detectStalePackages = (graph, config) => {
7193
7307
  const declaredNames = new Set(declaredDependencies.keys());
7194
7308
  const usedPackageNames = collectUsedPackages(graph);
7195
7309
  const monorepoRoot = findMonorepoRoot(config.rootDir);
7196
- const nodeModulesRoot = monorepoRoot ?? config.rootDir;
7310
+ const nodeModulesSearchRoots = monorepoRoot && monorepoRoot !== config.rootDir ? [config.rootDir, monorepoRoot] : [config.rootDir];
7197
7311
  const allPackageJsonPaths = discoverAllPackageJsonPaths(config.rootDir);
7198
7312
  if (monorepoRoot) {
7199
7313
  const monorepoPackageJson = (0, node_path.join)(monorepoRoot, "package.json");
7200
7314
  if (!allPackageJsonPaths.includes(monorepoPackageJson) && (0, node_fs.existsSync)(monorepoPackageJson)) allPackageJsonPaths.push(monorepoPackageJson);
7201
7315
  }
7202
- const binToPackage = buildBinToPackageMap(nodeModulesRoot, declaredNames);
7316
+ const binToPackage = buildBinToPackageMap(nodeModulesSearchRoots, declaredNames);
7203
7317
  for (const workspacePackageJsonPath of allPackageJsonPaths) {
7204
7318
  const scriptReferenced = collectScriptReferencedPackages(workspacePackageJsonPath, declaredNames, binToPackage);
7205
7319
  for (const packageName of scriptReferenced) usedPackageNames.add(packageName);
@@ -7245,7 +7359,7 @@ const detectStalePackages = (graph, config) => {
7245
7359
  if ("react-dom" in peerDeps && declaredDependencies.get("react-dom") === true) usedPackageNames.add("react-dom");
7246
7360
  } catch {}
7247
7361
  }
7248
- const peerSatisfied = collectPeerSatisfiedPackages(nodeModulesRoot, declaredNames, usedPackageNames);
7362
+ const peerSatisfied = collectPeerSatisfiedPackages(nodeModulesSearchRoots, declaredNames, usedPackageNames);
7249
7363
  for (const packageName of peerSatisfied) usedPackageNames.add(packageName);
7250
7364
  const staticPeerSatisfied = collectStaticPeerSatisfiedPackages(declaredNames, usedPackageNames);
7251
7365
  for (const packageName of staticPeerSatisfied) usedPackageNames.add(packageName);
@@ -7291,14 +7405,14 @@ const hasJsxFiles = (graph) => graph.modules.some((module) => {
7291
7405
  const filePath = module.fileId.path;
7292
7406
  return filePath.endsWith(".tsx") || filePath.endsWith(".jsx");
7293
7407
  });
7294
- const collectPeerSatisfiedPackages = (rootDir, declaredNames, confirmedUsedNames) => {
7408
+ const collectPeerSatisfiedPackages = (nodeModulesSearchRoots, declaredNames, confirmedUsedNames) => {
7295
7409
  const peerSatisfied = /* @__PURE__ */ new Set();
7296
- const nodeModulesDir = (0, node_path.join)(rootDir, "node_modules");
7297
7410
  for (const installedName of declaredNames) {
7298
7411
  if (!confirmedUsedNames.has(installedName)) continue;
7299
- const packageJsonPath = installedName.startsWith("@") ? (0, node_path.join)(nodeModulesDir, ...installedName.split("/"), "package.json") : (0, node_path.join)(nodeModulesDir, installedName, "package.json");
7412
+ const installedPackageJsonPath = findInstalledPackageJsonPath(installedName, nodeModulesSearchRoots);
7413
+ if (!installedPackageJsonPath) continue;
7300
7414
  try {
7301
- const content = (0, node_fs.readFileSync)(packageJsonPath, "utf-8");
7415
+ const content = (0, node_fs.readFileSync)(installedPackageJsonPath, "utf-8");
7302
7416
  const peerDeps = JSON.parse(content).peerDependencies;
7303
7417
  if (peerDeps && typeof peerDeps === "object") {
7304
7418
  for (const peerName of Object.keys(peerDeps)) if (declaredNames.has(peerName)) peerSatisfied.add(peerName);
@@ -7309,6 +7423,12 @@ const collectPeerSatisfiedPackages = (rootDir, declaredNames, confirmedUsedNames
7309
7423
  }
7310
7424
  return peerSatisfied;
7311
7425
  };
7426
+ const findInstalledPackageJsonPath = (packageName, nodeModulesSearchRoots) => {
7427
+ for (const searchRoot of nodeModulesSearchRoots) {
7428
+ const candidatePath = packageName.startsWith("@") ? (0, node_path.join)(searchRoot, "node_modules", ...packageName.split("/"), "package.json") : (0, node_path.join)(searchRoot, "node_modules", packageName, "package.json");
7429
+ if ((0, node_fs.existsSync)(candidatePath)) return candidatePath;
7430
+ }
7431
+ };
7312
7432
  const STATIC_PEER_DEPENDENCY_MAP = {
7313
7433
  "@apollo/client": ["graphql"],
7314
7434
  "@docusaurus/core": ["@mdx-js/react"],
@@ -7413,11 +7533,12 @@ const ENV_WRAPPER_BINARY_SET = new Set([
7413
7533
  "env-cmd"
7414
7534
  ]);
7415
7535
  const INLINE_ENV_VAR_PATTERN = /^[A-Z_][A-Z0-9_]*=/;
7416
- const buildBinToPackageMap = (rootDir, declaredNames) => {
7536
+ const buildBinToPackageMap = (nodeModulesSearchRoots, declaredNames) => {
7417
7537
  const binToPackage = /* @__PURE__ */ new Map();
7418
7538
  for (const [binary, packageName] of Object.entries(CLI_BINARY_TO_PACKAGE)) binToPackage.set(binary, packageName);
7419
7539
  for (const packageName of declaredNames) {
7420
- const packageBinJsonPath = packageName.startsWith("@") ? (0, node_path.join)(rootDir, "node_modules", ...packageName.split("/"), "package.json") : (0, node_path.join)(rootDir, "node_modules", packageName, "package.json");
7540
+ const packageBinJsonPath = findInstalledPackageJsonPath(packageName, nodeModulesSearchRoots);
7541
+ if (!packageBinJsonPath) continue;
7421
7542
  try {
7422
7543
  const binContent = (0, node_fs.readFileSync)(packageBinJsonPath, "utf-8");
7423
7544
  const binPackageJson = JSON.parse(binContent);
@@ -8127,7 +8248,7 @@ const detectDuplicateImports = (graph) => {
8127
8248
  const findings = [];
8128
8249
  for (const module of graph.modules) {
8129
8250
  if (module.isDeclarationFile) continue;
8130
- const specifierToOccurrences = /* @__PURE__ */ new Map();
8251
+ const groupedByKindAndSpecifier = /* @__PURE__ */ new Map();
8131
8252
  for (const importInfo of module.imports) {
8132
8253
  if (importInfo.isSideEffect) continue;
8133
8254
  if (importInfo.isDynamic) continue;
@@ -8138,18 +8259,21 @@ const detectDuplicateImports = (graph) => {
8138
8259
  importedNames: importInfo.importedNames.map((binding) => binding.isNamespace ? `* as ${binding.alias ?? ""}` : binding.alias ?? binding.name),
8139
8260
  isTypeOnly: importInfo.isTypeOnly
8140
8261
  };
8141
- const existing = specifierToOccurrences.get(importInfo.specifier);
8262
+ const groupKey = `${importInfo.isTypeOnly ? "type" : "value"}:${importInfo.specifier}`;
8263
+ const existing = groupedByKindAndSpecifier.get(groupKey);
8142
8264
  if (existing) existing.push(occurrence);
8143
- else specifierToOccurrences.set(importInfo.specifier, [occurrence]);
8265
+ else groupedByKindAndSpecifier.set(groupKey, [occurrence]);
8144
8266
  }
8145
- for (const [specifier, occurrences] of specifierToOccurrences) {
8267
+ for (const [groupKey, occurrences] of groupedByKindAndSpecifier) {
8146
8268
  if (occurrences.length < 2) continue;
8269
+ const specifier = groupKey.slice(groupKey.indexOf(":") + 1);
8270
+ const kindLabel = groupKey.startsWith("type:") ? "type-only " : "";
8147
8271
  findings.push({
8148
8272
  path: module.fileId.path,
8149
8273
  specifier,
8150
8274
  occurrences,
8151
8275
  confidence: "high",
8152
- reason: `"${specifier}" is imported ${occurrences.length} times in this file — merge into a single statement`
8276
+ reason: `"${specifier}" is imported ${occurrences.length} times in this file as ${kindLabel}imports — merge into a single statement`
8153
8277
  });
8154
8278
  }
8155
8279
  }
@@ -8244,6 +8368,7 @@ const detectDuplicateConstants = (graph) => {
8244
8368
  const uniqueFilePaths = new Set(bucket.occurrences.map((occurrence) => occurrence.path));
8245
8369
  if (uniqueFilePaths.size < 3) continue;
8246
8370
  const uniqueNames = new Set(bucket.occurrences.map((occurrence) => occurrence.constantName));
8371
+ if (uniqueNames.size > 1 && hasDistinctUnitSuffixes([...uniqueNames])) continue;
8247
8372
  findings.push({
8248
8373
  literalHash,
8249
8374
  literalPreview: bucket.literalPreview,
@@ -8254,6 +8379,29 @@ const detectDuplicateConstants = (graph) => {
8254
8379
  }
8255
8380
  return findings;
8256
8381
  };
8382
+ const TRAILING_NAME_TOKEN_PATTERN = /_([A-Z][A-Z0-9]*)$/;
8383
+ const extractTrailingNameToken = (constantName) => {
8384
+ const match = constantName.match(TRAILING_NAME_TOKEN_PATTERN);
8385
+ return match ? match[1] : void 0;
8386
+ };
8387
+ /**
8388
+ * AGENTS.md requires magic numbers to use trailing unit suffixes (`_MS`, `_PX`,
8389
+ * `_TOKENS`, `_WIDTH`, …). When same-value constants carry DIFFERENT trailing
8390
+ * tokens (e.g. `STEP_DELAY_MS = 1000` vs `MINIMUM_TOKENS = 1000`), they
8391
+ * represent semantically distinct quantities that cannot be consolidated —
8392
+ * flagging them as duplicates is misleading. Constants sharing the same
8393
+ * trailing token (e.g. `CACHE_INTERVAL_MS` + `RECONNECT_DELAY_MS`, both `_MS`)
8394
+ * stay flagged because they are at least same-unit and might be extractable.
8395
+ */
8396
+ const hasDistinctUnitSuffixes = (constantNames) => {
8397
+ const trailingTokens = /* @__PURE__ */ new Set();
8398
+ for (const name of constantNames) {
8399
+ const token = extractTrailingNameToken(name);
8400
+ if (!token) return false;
8401
+ trailingTokens.add(token);
8402
+ }
8403
+ return trailingTokens.size > 1;
8404
+ };
8257
8405
  const detectSimplifiableExpressions = (graph) => {
8258
8406
  const findings = [];
8259
8407
  for (const module of graph.modules) {
@@ -9353,6 +9501,39 @@ const generateReport = (graph, config) => {
9353
9501
  //#region src/index.ts
9354
9502
  const STYLE_EXTENSIONS = [".css", ".scss"];
9355
9503
  const REACT_NATIVE_ENABLERS = ["react-native", "expo"];
9504
+ const basenameFromPath = (filePath) => {
9505
+ const lastSlashIndex = filePath.lastIndexOf("/");
9506
+ return lastSlashIndex === -1 ? filePath : filePath.slice(lastSlashIndex + 1);
9507
+ };
9508
+ /**
9509
+ * Dynamic registry pattern: many codebases use a central "schema/registry"
9510
+ * module that lists tool/command/page filenames as string literals, then a
9511
+ * runner spawns them via `path.resolve(dir, file)` or `import()`. Static
9512
+ * analysis can't follow the indirection, so those targets get falsely
9513
+ * flagged as unused.
9514
+ *
9515
+ * Heuristic: if a parsed string literal exactly matches the basename of
9516
+ * exactly one file in the project, treat that file as an entry point.
9517
+ * Uniqueness guards against false-positives from common names like
9518
+ * `index.ts` matching dozens of unrelated files.
9519
+ */
9520
+ const markFilenameRegistryEntries = (moduleGraph) => {
9521
+ const basenameToModuleIndex = /* @__PURE__ */ new Map();
9522
+ for (const module of moduleGraph.modules) {
9523
+ const basename = basenameFromPath(module.fileId.path);
9524
+ const existing = basenameToModuleIndex.get(basename);
9525
+ if (existing === void 0) basenameToModuleIndex.set(basename, module.fileId.index);
9526
+ else if (existing !== "ambiguous") basenameToModuleIndex.set(basename, "ambiguous");
9527
+ }
9528
+ for (const module of moduleGraph.modules) for (const referencedFilename of module.referencedFilenames) {
9529
+ const targetIndex = basenameToModuleIndex.get(referencedFilename);
9530
+ if (typeof targetIndex !== "number") continue;
9531
+ const targetModule = moduleGraph.modules[targetIndex];
9532
+ if (!targetModule || targetModule.isEntryPoint) continue;
9533
+ if (targetModule.fileId.index === module.fileId.index) continue;
9534
+ targetModule.isEntryPoint = true;
9535
+ }
9536
+ };
9356
9537
  const detectReactNative = (rootDir, workspacePackages) => {
9357
9538
  const directoriesToCheck = [rootDir, ...workspacePackages.map((workspacePackage) => workspacePackage.directory)];
9358
9539
  for (const directory of directoriesToCheck) {
@@ -9510,11 +9691,7 @@ const analyze = async (config) => {
9510
9691
  }));
9511
9692
  }
9512
9693
  const absoluteRoot = (0, node_path.resolve)(config.rootDir);
9513
- const outputDirectoryExclusions = OUTPUT_DIRECTORIES.flatMap((outputDirectory) => {
9514
- const exclusions = [`${absoluteRoot}/${outputDirectory}/**`];
9515
- for (const workspacePackage of workspacePackages) exclusions.push(`${workspacePackage.directory}/${outputDirectory}/**`);
9516
- return exclusions;
9517
- });
9694
+ const outputDirectoryExclusions = OUTPUT_DIRECTORIES.flatMap((outputDirectory) => [`${absoluteRoot}/${outputDirectory}/**`, `${absoluteRoot}/**/${outputDirectory}/**`]);
9518
9695
  const allExclusionPatterns = [
9519
9696
  ...workspaceDiscovery.excludedDirectories.map((directory) => `${directory}/**`),
9520
9697
  ...frameworkIgnorePatterns,
@@ -9714,6 +9891,7 @@ const analyze = async (config) => {
9714
9891
  detail: describeUnknownError(reExportError)
9715
9892
  }));
9716
9893
  }
9894
+ markFilenameRegistryEntries(moduleGraph);
9717
9895
  try {
9718
9896
  traceReachability(moduleGraph);
9719
9897
  } catch (reachabilityError) {
package/dist/index.mjs CHANGED
@@ -1047,10 +1047,22 @@ const containsCallOrPromiseSurface = (node, recursionDepth = 0) => {
1047
1047
  }
1048
1048
  return false;
1049
1049
  };
1050
+ const unwrapParenthesizedExpression = (node) => {
1051
+ let current = node;
1052
+ while (current.type === "ParenthesizedExpression") {
1053
+ const inner = current.expression;
1054
+ if (!inner || !isOxcAstNode(inner)) return current;
1055
+ current = inner;
1056
+ }
1057
+ return current;
1058
+ };
1050
1059
  const isSimpleReturnArgument = (argumentNode) => {
1051
1060
  if (!isOxcAstNode(argumentNode)) return false;
1052
- if (argumentNode.type === "BlockStatement") return false;
1053
- if (argumentNode.type === "ObjectExpression") return false;
1061
+ const unwrapped = unwrapParenthesizedExpression(argumentNode);
1062
+ if (unwrapped.type === "BlockStatement") return false;
1063
+ if (unwrapped.type === "ObjectExpression") return false;
1064
+ if (unwrapped.type === "JSXElement") return false;
1065
+ if (unwrapped.type === "JSXFragment") return false;
1054
1066
  return true;
1055
1067
  };
1056
1068
  const detectBlockArrowSingleReturn = (functionNode) => {
@@ -1104,9 +1116,34 @@ const detectRedundantAwaitReturn = (functionNode) => {
1104
1116
  };
1105
1117
  };
1106
1118
  const isAsyncFunction = (functionNode) => Boolean(functionNode.async);
1107
- const detectUselessAsync = (functionNode) => {
1119
+ const containsPromiseTypeReference = (node, recursionDepth = 0) => {
1120
+ if (recursionDepth > 30) return false;
1121
+ if (!isOxcAstNode(node)) return false;
1122
+ if (node.type === "TSTypeReference") {
1123
+ const typeName = node.typeName;
1124
+ if (typeName?.name === "Promise") return true;
1125
+ if (typeName?.right?.name === "Promise") return true;
1126
+ }
1127
+ for (const value of Object.values(node)) if (Array.isArray(value)) {
1128
+ for (const element of value) if (containsPromiseTypeReference(element, recursionDepth + 1)) return true;
1129
+ } else if (isOxcAstNode(value)) {
1130
+ if (containsPromiseTypeReference(value, recursionDepth + 1)) return true;
1131
+ }
1132
+ return false;
1133
+ };
1134
+ const hasExplicitPromiseReturnType = (functionNode) => {
1135
+ const returnType = functionNode.returnType;
1136
+ if (!returnType || !isOxcAstNode(returnType)) return false;
1137
+ const annotation = returnType.typeAnnotation;
1138
+ if (!annotation || !isOxcAstNode(annotation)) return false;
1139
+ return containsPromiseTypeReference(annotation);
1140
+ };
1141
+ const detectUselessAsync = (functionNode, context) => {
1108
1142
  if (!isAsyncFunction(functionNode)) return void 0;
1109
1143
  if (functionNode.type === "ClassDeclaration" || functionNode.type === "MethodDefinition") return;
1144
+ if (context.isMethodContext) return void 0;
1145
+ if (context.isInlineCallback) return void 0;
1146
+ if (hasExplicitPromiseReturnType(functionNode)) return void 0;
1110
1147
  const bodyNode = functionNode.body;
1111
1148
  if (!isOxcAstNode(bodyNode)) return void 0;
1112
1149
  if (containsAwaitExpression(bodyNode)) return void 0;
@@ -1118,14 +1155,14 @@ const detectUselessAsync = (functionNode) => {
1118
1155
  suggestion: "drop `async` (caller's existing `await` keeps the type identical) or add an explicit return type"
1119
1156
  };
1120
1157
  };
1121
- const detectSimplifiableFunctionPatterns = (functionNode) => {
1158
+ const detectSimplifiableFunctionPatterns = (functionNode, context = {}) => {
1122
1159
  if (!isOxcAstNode(functionNode)) return [];
1123
1160
  const findings = [];
1124
1161
  const blockArrow = detectBlockArrowSingleReturn(functionNode);
1125
1162
  if (blockArrow) findings.push(blockArrow);
1126
1163
  const awaitReturn = detectRedundantAwaitReturn(functionNode);
1127
1164
  if (awaitReturn) findings.push(awaitReturn);
1128
- const uselessAsync = detectUselessAsync(functionNode);
1165
+ const uselessAsync = detectUselessAsync(functionNode, context);
1129
1166
  if (uselessAsync) findings.push(uselessAsync);
1130
1167
  return findings;
1131
1168
  };
@@ -1138,9 +1175,12 @@ const inferFunctionName = (functionNode, parentContext) => {
1138
1175
  if (declaredId?.name) return declaredId.name;
1139
1176
  return parentContext;
1140
1177
  };
1141
- const visitFunctionAndDescend = (functionNode, captures, contextName, recursionDepth) => {
1178
+ const visitFunctionAndDescend = (functionNode, captures, contextName, recursionDepth, isMethodContext, isInlineCallback) => {
1142
1179
  const functionName = inferFunctionName(functionNode, contextName);
1143
- const detections = detectSimplifiableFunctionPatterns(functionNode);
1180
+ const detections = detectSimplifiableFunctionPatterns(functionNode, {
1181
+ isMethodContext,
1182
+ isInlineCallback
1183
+ });
1144
1184
  for (const detection of detections) captures.push({
1145
1185
  kind: detection.kind,
1146
1186
  functionName,
@@ -1153,10 +1193,13 @@ const visitFunctionAndDescend = (functionNode, captures, contextName, recursionD
1153
1193
  const parameters = functionNode.params ?? [];
1154
1194
  for (const parameter of parameters) if (isOxcAstNode(parameter)) walkForFunctions(parameter, captures, functionName, recursionDepth + 1);
1155
1195
  };
1196
+ const isObjectMethodShorthand = (node) => (node.type === "Property" || node.type === "ObjectProperty") && node.method === true;
1197
+ const isObjectPropertyAssignment = (node) => (node.type === "Property" || node.type === "ObjectProperty") && node.method !== true;
1198
+ const isCallOrNewExpression = (node) => node.type === "CallExpression" || node.type === "NewExpression";
1156
1199
  const walkForFunctions = (node, captures, contextName, recursionDepth = 0) => {
1157
1200
  if (recursionDepth > 200) return;
1158
1201
  if (looksLikeFunction(node)) {
1159
- visitFunctionAndDescend(node, captures, contextName, recursionDepth);
1202
+ visitFunctionAndDescend(node, captures, contextName, recursionDepth, false, false);
1160
1203
  return;
1161
1204
  }
1162
1205
  let nextContext = contextName;
@@ -1172,6 +1215,35 @@ const walkForFunctions = (node, captures, contextName, recursionDepth = 0) => {
1172
1215
  const className = getIdentifierName(node.id);
1173
1216
  if (className) nextContext = className;
1174
1217
  }
1218
+ if (node.type === "MethodDefinition" || isObjectMethodShorthand(node)) {
1219
+ const methodValue = node.value;
1220
+ if (methodValue && isOxcAstNode(methodValue) && looksLikeFunction(methodValue)) {
1221
+ visitFunctionAndDescend(methodValue, captures, getIdentifierName(node.key) ?? nextContext, recursionDepth + 1, true, false);
1222
+ const keyNode = node.key;
1223
+ if (keyNode && isOxcAstNode(keyNode) && node.computed) walkForFunctions(keyNode, captures, nextContext, recursionDepth + 1);
1224
+ return;
1225
+ }
1226
+ }
1227
+ if (isObjectPropertyAssignment(node)) {
1228
+ const propertyValue = node.value;
1229
+ if (propertyValue && isOxcAstNode(propertyValue) && looksLikeFunction(propertyValue)) {
1230
+ visitFunctionAndDescend(propertyValue, captures, getIdentifierName(node.key) ?? nextContext, recursionDepth + 1, false, true);
1231
+ const keyNode = node.key;
1232
+ if (keyNode && isOxcAstNode(keyNode) && node.computed) walkForFunctions(keyNode, captures, nextContext, recursionDepth + 1);
1233
+ return;
1234
+ }
1235
+ }
1236
+ if (isCallOrNewExpression(node)) {
1237
+ const callee = node.callee;
1238
+ if (callee && isOxcAstNode(callee)) walkForFunctions(callee, captures, nextContext, recursionDepth + 1);
1239
+ const callArguments = node.arguments ?? [];
1240
+ for (const argument of callArguments) {
1241
+ if (!isOxcAstNode(argument)) continue;
1242
+ if (looksLikeFunction(argument)) visitFunctionAndDescend(argument, captures, nextContext, recursionDepth + 1, false, true);
1243
+ else walkForFunctions(argument, captures, nextContext, recursionDepth + 1);
1244
+ }
1245
+ return;
1246
+ }
1175
1247
  for (const value of Object.values(node)) if (Array.isArray(value)) {
1176
1248
  for (const element of value) if (isOxcAstNode(element)) walkForFunctions(element, captures, nextContext, recursionDepth + 1);
1177
1249
  } else if (isOxcAstNode(value)) walkForFunctions(value, captures, nextContext, recursionDepth + 1);
@@ -1504,10 +1576,15 @@ const CSS_EXTENSIONS = [
1504
1576
  ];
1505
1577
  const CSS_IMPORT_PATTERN = /@import\s+(?:url\()?['"]([^'"]+)['"]\)?/g;
1506
1578
  const SCSS_USE_FORWARD_PATTERN = /@(?:use|forward)\s+['"]([^'"]+)['"]/g;
1579
+ const TAILWIND_PLUGIN_REFERENCE_PATTERN = /@(?:plugin|reference|config)\s+['"]([^'"]+)['"]/g;
1507
1580
  const parseCssImports = (filePath) => {
1508
1581
  const sourceText = readFileSync(filePath, "utf-8");
1509
1582
  const imports = [];
1510
- const patterns = [CSS_IMPORT_PATTERN, SCSS_USE_FORWARD_PATTERN];
1583
+ const patterns = [
1584
+ CSS_IMPORT_PATTERN,
1585
+ SCSS_USE_FORWARD_PATTERN,
1586
+ TAILWIND_PLUGIN_REFERENCE_PATTERN
1587
+ ];
1511
1588
  for (const pattern of patterns) {
1512
1589
  let match;
1513
1590
  pattern.lastIndex = 0;
@@ -1530,6 +1607,7 @@ const parseCssImports = (filePath) => {
1530
1607
  memberAccesses: [],
1531
1608
  wholeObjectUses: [],
1532
1609
  localIdentifierReferences: [],
1610
+ referencedFilenames: [],
1533
1611
  redundantTypePatterns: [],
1534
1612
  identityWrappers: [],
1535
1613
  typeDefinitionHashes: [],
@@ -1569,6 +1647,7 @@ const createEmptyParsedSource = () => ({
1569
1647
  memberAccesses: [],
1570
1648
  wholeObjectUses: [],
1571
1649
  localIdentifierReferences: [],
1650
+ referencedFilenames: [],
1572
1651
  redundantTypePatterns: [],
1573
1652
  identityWrappers: [],
1574
1653
  typeDefinitionHashes: [],
@@ -1712,6 +1791,7 @@ const parseSourceFile = (filePath) => {
1712
1791
  ...createEmptyParsedSource(),
1713
1792
  imports,
1714
1793
  exports,
1794
+ referencedFilenames: extractReferencedFilenames(sourceText),
1715
1795
  errors: [...earlyErrors, new ParseError({
1716
1796
  code: "parse-recovered",
1717
1797
  severity: "info",
@@ -1724,6 +1804,7 @@ const parseSourceFile = (filePath) => {
1724
1804
  ...createEmptyParsedSource(),
1725
1805
  imports,
1726
1806
  exports,
1807
+ referencedFilenames: extractReferencedFilenames(sourceText),
1727
1808
  errors: [...earlyErrors, new ParseError({
1728
1809
  code: "parse-failed",
1729
1810
  message: "oxc-parser returned no program body",
@@ -1776,50 +1857,63 @@ const parseSourceFile = (filePath) => {
1776
1857
  safeWalk("collectDryPatterns", () => {
1777
1858
  collectDryPatterns(program.body, sourceText, redundantTypePatterns, identityWrappers, typeDefinitionHashes);
1778
1859
  }, void 0);
1860
+ const inlineTypeLiterals = safeWalk("collectInlineTypeLiterals", () => collectInlineTypeLiterals(program.body), []).map((capture) => ({
1861
+ structuralHash: capture.structuralHash,
1862
+ memberCount: capture.memberCount,
1863
+ preview: capture.preview,
1864
+ context: capture.context,
1865
+ nearestName: capture.nearestName,
1866
+ line: getLineFromOffset(sourceText, capture.startOffset),
1867
+ column: getColumnFromOffset(sourceText, capture.startOffset)
1868
+ }));
1869
+ const simplifiableFunctions = safeWalk("collectSimplifiableFunctions", () => collectSimplifiableFunctions(program.body), []).map((capture) => ({
1870
+ kind: capture.kind,
1871
+ functionName: capture.functionName,
1872
+ line: getLineFromOffset(sourceText, capture.startOffset),
1873
+ column: getColumnFromOffset(sourceText, capture.startOffset),
1874
+ reason: capture.reason,
1875
+ suggestion: capture.suggestion
1876
+ }));
1877
+ const simplifiableExpressions = safeWalk("collectSimplifiableExpressions", () => collectSimplifiableExpressions(program.body), []).map((capture) => ({
1878
+ kind: capture.kind,
1879
+ snippet: capture.snippet,
1880
+ line: getLineFromOffset(sourceText, capture.startOffset),
1881
+ column: getColumnFromOffset(sourceText, capture.startOffset),
1882
+ reason: capture.reason,
1883
+ suggestion: capture.suggestion
1884
+ }));
1885
+ const duplicateConstantCandidates = safeWalk("collectDuplicateConstantCandidates", () => collectDuplicateConstantCandidates(program.body), []).map((capture) => ({
1886
+ constantName: capture.constantName,
1887
+ literalHash: capture.literalHash,
1888
+ literalPreview: capture.literalPreview,
1889
+ line: getLineFromOffset(sourceText, capture.startOffset),
1890
+ column: getColumnFromOffset(sourceText, capture.startOffset)
1891
+ }));
1779
1892
  return {
1780
1893
  imports,
1781
1894
  exports,
1782
1895
  memberAccesses,
1783
1896
  wholeObjectUses,
1784
1897
  localIdentifierReferences,
1898
+ referencedFilenames: extractReferencedFilenames(sourceText),
1785
1899
  redundantTypePatterns,
1786
1900
  identityWrappers,
1787
1901
  typeDefinitionHashes,
1788
- inlineTypeLiterals: safeWalk("collectInlineTypeLiterals", () => collectInlineTypeLiterals(program.body), []).map((capture) => ({
1789
- structuralHash: capture.structuralHash,
1790
- memberCount: capture.memberCount,
1791
- preview: capture.preview,
1792
- context: capture.context,
1793
- nearestName: capture.nearestName,
1794
- line: getLineFromOffset(sourceText, capture.startOffset),
1795
- column: getColumnFromOffset(sourceText, capture.startOffset)
1796
- })),
1797
- simplifiableFunctions: safeWalk("collectSimplifiableFunctions", () => collectSimplifiableFunctions(program.body), []).map((capture) => ({
1798
- kind: capture.kind,
1799
- functionName: capture.functionName,
1800
- line: getLineFromOffset(sourceText, capture.startOffset),
1801
- column: getColumnFromOffset(sourceText, capture.startOffset),
1802
- reason: capture.reason,
1803
- suggestion: capture.suggestion
1804
- })),
1805
- simplifiableExpressions: safeWalk("collectSimplifiableExpressions", () => collectSimplifiableExpressions(program.body), []).map((capture) => ({
1806
- kind: capture.kind,
1807
- snippet: capture.snippet,
1808
- line: getLineFromOffset(sourceText, capture.startOffset),
1809
- column: getColumnFromOffset(sourceText, capture.startOffset),
1810
- reason: capture.reason,
1811
- suggestion: capture.suggestion
1812
- })),
1813
- duplicateConstantCandidates: safeWalk("collectDuplicateConstantCandidates", () => collectDuplicateConstantCandidates(program.body), []).map((capture) => ({
1814
- constantName: capture.constantName,
1815
- literalHash: capture.literalHash,
1816
- literalPreview: capture.literalPreview,
1817
- line: getLineFromOffset(sourceText, capture.startOffset),
1818
- column: getColumnFromOffset(sourceText, capture.startOffset)
1819
- })),
1902
+ inlineTypeLiterals,
1903
+ simplifiableFunctions,
1904
+ simplifiableExpressions,
1905
+ duplicateConstantCandidates,
1820
1906
  errors: [...earlyErrors, ...detectorErrors]
1821
1907
  };
1822
1908
  };
1909
+ const REFERENCED_FILENAME_LITERAL_PATTERN = /(?<![./@\w-])(?:["'`])([a-z][\w-]*\.(?:ts|tsx|js|jsx|mts|mjs|cts|cjs))(?:["'`])/g;
1910
+ const extractReferencedFilenames = (sourceText) => {
1911
+ const captured = /* @__PURE__ */ new Set();
1912
+ REFERENCED_FILENAME_LITERAL_PATTERN.lastIndex = 0;
1913
+ let match;
1914
+ while ((match = REFERENCED_FILENAME_LITERAL_PATTERN.exec(sourceText)) !== null) captured.add(match[1]);
1915
+ return [...captured];
1916
+ };
1823
1917
  const collectDryPatterns = (bodyNodes, sourceText, redundantTypePatterns, identityWrappers, typeDefinitionHashes) => {
1824
1918
  for (const statement of bodyNodes) inspectStatement(statement, sourceText, redundantTypePatterns, identityWrappers, typeDefinitionHashes);
1825
1919
  };
@@ -3421,8 +3515,11 @@ const resolveEntries = async (config) => {
3421
3515
  }
3422
3516
  const frameworkEntries = detectFrameworkEntries(absoluteRoot);
3423
3517
  const entryEligiblePackages = workspacePackages.filter(isEntryEligible);
3518
+ const monorepoRootForEntries = findMonorepoRoot(absoluteRoot);
3519
+ const ancestorPackageJsonRoots = monorepoRootForEntries && monorepoRootForEntries !== absoluteRoot ? [monorepoRootForEntries] : [];
3424
3520
  const scriptEntries = extractScriptEntries(absoluteRoot);
3425
3521
  for (const workspacePackage of entryEligiblePackages) scriptEntries.push(...extractScriptEntries(workspacePackage.directory));
3522
+ for (const ancestorRoot of ancestorPackageJsonRoots) for (const entryPath of extractScriptEntries(ancestorRoot)) if (entryPath.startsWith(`${absoluteRoot}/`)) scriptEntries.push(entryPath);
3426
3523
  const webpackEntries = extractWebpackEntryPoints(absoluteRoot);
3427
3524
  for (const workspacePackage of entryEligiblePackages) webpackEntries.push(...extractWebpackEntryPoints(workspacePackage.directory));
3428
3525
  const viteEntries = extractViteEntryPoints(absoluteRoot);
@@ -6496,6 +6593,7 @@ const buildDependencyGraph = (inputs) => {
6496
6593
  memberAccesses: input.parsed.memberAccesses,
6497
6594
  wholeObjectUses: input.parsed.wholeObjectUses,
6498
6595
  localIdentifierReferences: input.parsed.localIdentifierReferences,
6596
+ referencedFilenames: input.parsed.referencedFilenames,
6499
6597
  redundantTypePatterns: input.parsed.redundantTypePatterns,
6500
6598
  identityWrappers: input.parsed.identityWrappers,
6501
6599
  typeDefinitionHashes: input.parsed.typeDefinitionHashes,
@@ -6823,6 +6921,21 @@ const hasExcludedExtension = (filePath) => {
6823
6921
  return EXCLUDED_EXTENSIONS.has(filePath.slice(lastDot));
6824
6922
  };
6825
6923
  const isExcludedByPattern = (filePath) => TEST_FILE_PATTERN.test(filePath) || EXCLUDED_DIRECTORY_PATTERN.test(filePath) || CONFIG_FILE_PATTERN.test(filePath);
6924
+ /**
6925
+ * Files the parser couldn't analyze (minified bundles, oversized files, binaries)
6926
+ * have no detectable imports — they're effectively opaque. Flagging them as
6927
+ * "unused" is a false positive because we can't see who imports them, and they
6928
+ * may be static assets, generated bundles, or build artifacts that get loaded
6929
+ * outside the JS module graph (HTML `<script src>`, `vite-plugin-string`, etc.).
6930
+ * The parser already records a `file-minified`/`file-too-large`/`file-binary`
6931
+ * info-level entry in `analysisErrors`, which is the actionable signal.
6932
+ */
6933
+ const PARSE_OPAQUE_ERROR_CODES = new Set([
6934
+ "file-minified",
6935
+ "file-too-large",
6936
+ "file-binary"
6937
+ ]);
6938
+ const isOpaqueToAnalysis = (module) => module.parseErrors.some((parseError) => parseError.code && PARSE_OPAQUE_ERROR_CODES.has(parseError.code));
6826
6939
  const detectOrphanFiles = (graph) => {
6827
6940
  const unusedFiles = [];
6828
6941
  for (const module of graph.modules) {
@@ -6832,6 +6945,7 @@ const detectOrphanFiles = (graph) => {
6832
6945
  if (module.isConfigFile) continue;
6833
6946
  if (hasExcludedExtension(module.fileId.path)) continue;
6834
6947
  if (isExcludedByPattern(module.fileId.path)) continue;
6948
+ if (isOpaqueToAnalysis(module)) continue;
6835
6949
  if (isBarrelWithReachableSources(module, graph)) continue;
6836
6950
  if (hasReachableDirectImporter(module.fileId.index, graph)) continue;
6837
6951
  unusedFiles.push({ path: module.fileId.path });
@@ -7162,13 +7276,13 @@ const detectStalePackages = (graph, config) => {
7162
7276
  const declaredNames = new Set(declaredDependencies.keys());
7163
7277
  const usedPackageNames = collectUsedPackages(graph);
7164
7278
  const monorepoRoot = findMonorepoRoot(config.rootDir);
7165
- const nodeModulesRoot = monorepoRoot ?? config.rootDir;
7279
+ const nodeModulesSearchRoots = monorepoRoot && monorepoRoot !== config.rootDir ? [config.rootDir, monorepoRoot] : [config.rootDir];
7166
7280
  const allPackageJsonPaths = discoverAllPackageJsonPaths(config.rootDir);
7167
7281
  if (monorepoRoot) {
7168
7282
  const monorepoPackageJson = join(monorepoRoot, "package.json");
7169
7283
  if (!allPackageJsonPaths.includes(monorepoPackageJson) && existsSync(monorepoPackageJson)) allPackageJsonPaths.push(monorepoPackageJson);
7170
7284
  }
7171
- const binToPackage = buildBinToPackageMap(nodeModulesRoot, declaredNames);
7285
+ const binToPackage = buildBinToPackageMap(nodeModulesSearchRoots, declaredNames);
7172
7286
  for (const workspacePackageJsonPath of allPackageJsonPaths) {
7173
7287
  const scriptReferenced = collectScriptReferencedPackages(workspacePackageJsonPath, declaredNames, binToPackage);
7174
7288
  for (const packageName of scriptReferenced) usedPackageNames.add(packageName);
@@ -7214,7 +7328,7 @@ const detectStalePackages = (graph, config) => {
7214
7328
  if ("react-dom" in peerDeps && declaredDependencies.get("react-dom") === true) usedPackageNames.add("react-dom");
7215
7329
  } catch {}
7216
7330
  }
7217
- const peerSatisfied = collectPeerSatisfiedPackages(nodeModulesRoot, declaredNames, usedPackageNames);
7331
+ const peerSatisfied = collectPeerSatisfiedPackages(nodeModulesSearchRoots, declaredNames, usedPackageNames);
7218
7332
  for (const packageName of peerSatisfied) usedPackageNames.add(packageName);
7219
7333
  const staticPeerSatisfied = collectStaticPeerSatisfiedPackages(declaredNames, usedPackageNames);
7220
7334
  for (const packageName of staticPeerSatisfied) usedPackageNames.add(packageName);
@@ -7260,14 +7374,14 @@ const hasJsxFiles = (graph) => graph.modules.some((module) => {
7260
7374
  const filePath = module.fileId.path;
7261
7375
  return filePath.endsWith(".tsx") || filePath.endsWith(".jsx");
7262
7376
  });
7263
- const collectPeerSatisfiedPackages = (rootDir, declaredNames, confirmedUsedNames) => {
7377
+ const collectPeerSatisfiedPackages = (nodeModulesSearchRoots, declaredNames, confirmedUsedNames) => {
7264
7378
  const peerSatisfied = /* @__PURE__ */ new Set();
7265
- const nodeModulesDir = join(rootDir, "node_modules");
7266
7379
  for (const installedName of declaredNames) {
7267
7380
  if (!confirmedUsedNames.has(installedName)) continue;
7268
- const packageJsonPath = installedName.startsWith("@") ? join(nodeModulesDir, ...installedName.split("/"), "package.json") : join(nodeModulesDir, installedName, "package.json");
7381
+ const installedPackageJsonPath = findInstalledPackageJsonPath(installedName, nodeModulesSearchRoots);
7382
+ if (!installedPackageJsonPath) continue;
7269
7383
  try {
7270
- const content = readFileSync(packageJsonPath, "utf-8");
7384
+ const content = readFileSync(installedPackageJsonPath, "utf-8");
7271
7385
  const peerDeps = JSON.parse(content).peerDependencies;
7272
7386
  if (peerDeps && typeof peerDeps === "object") {
7273
7387
  for (const peerName of Object.keys(peerDeps)) if (declaredNames.has(peerName)) peerSatisfied.add(peerName);
@@ -7278,6 +7392,12 @@ const collectPeerSatisfiedPackages = (rootDir, declaredNames, confirmedUsedNames
7278
7392
  }
7279
7393
  return peerSatisfied;
7280
7394
  };
7395
+ const findInstalledPackageJsonPath = (packageName, nodeModulesSearchRoots) => {
7396
+ for (const searchRoot of nodeModulesSearchRoots) {
7397
+ const candidatePath = packageName.startsWith("@") ? join(searchRoot, "node_modules", ...packageName.split("/"), "package.json") : join(searchRoot, "node_modules", packageName, "package.json");
7398
+ if (existsSync(candidatePath)) return candidatePath;
7399
+ }
7400
+ };
7281
7401
  const STATIC_PEER_DEPENDENCY_MAP = {
7282
7402
  "@apollo/client": ["graphql"],
7283
7403
  "@docusaurus/core": ["@mdx-js/react"],
@@ -7382,11 +7502,12 @@ const ENV_WRAPPER_BINARY_SET = new Set([
7382
7502
  "env-cmd"
7383
7503
  ]);
7384
7504
  const INLINE_ENV_VAR_PATTERN = /^[A-Z_][A-Z0-9_]*=/;
7385
- const buildBinToPackageMap = (rootDir, declaredNames) => {
7505
+ const buildBinToPackageMap = (nodeModulesSearchRoots, declaredNames) => {
7386
7506
  const binToPackage = /* @__PURE__ */ new Map();
7387
7507
  for (const [binary, packageName] of Object.entries(CLI_BINARY_TO_PACKAGE)) binToPackage.set(binary, packageName);
7388
7508
  for (const packageName of declaredNames) {
7389
- const packageBinJsonPath = packageName.startsWith("@") ? join(rootDir, "node_modules", ...packageName.split("/"), "package.json") : join(rootDir, "node_modules", packageName, "package.json");
7509
+ const packageBinJsonPath = findInstalledPackageJsonPath(packageName, nodeModulesSearchRoots);
7510
+ if (!packageBinJsonPath) continue;
7390
7511
  try {
7391
7512
  const binContent = readFileSync(packageBinJsonPath, "utf-8");
7392
7513
  const binPackageJson = JSON.parse(binContent);
@@ -8096,7 +8217,7 @@ const detectDuplicateImports = (graph) => {
8096
8217
  const findings = [];
8097
8218
  for (const module of graph.modules) {
8098
8219
  if (module.isDeclarationFile) continue;
8099
- const specifierToOccurrences = /* @__PURE__ */ new Map();
8220
+ const groupedByKindAndSpecifier = /* @__PURE__ */ new Map();
8100
8221
  for (const importInfo of module.imports) {
8101
8222
  if (importInfo.isSideEffect) continue;
8102
8223
  if (importInfo.isDynamic) continue;
@@ -8107,18 +8228,21 @@ const detectDuplicateImports = (graph) => {
8107
8228
  importedNames: importInfo.importedNames.map((binding) => binding.isNamespace ? `* as ${binding.alias ?? ""}` : binding.alias ?? binding.name),
8108
8229
  isTypeOnly: importInfo.isTypeOnly
8109
8230
  };
8110
- const existing = specifierToOccurrences.get(importInfo.specifier);
8231
+ const groupKey = `${importInfo.isTypeOnly ? "type" : "value"}:${importInfo.specifier}`;
8232
+ const existing = groupedByKindAndSpecifier.get(groupKey);
8111
8233
  if (existing) existing.push(occurrence);
8112
- else specifierToOccurrences.set(importInfo.specifier, [occurrence]);
8234
+ else groupedByKindAndSpecifier.set(groupKey, [occurrence]);
8113
8235
  }
8114
- for (const [specifier, occurrences] of specifierToOccurrences) {
8236
+ for (const [groupKey, occurrences] of groupedByKindAndSpecifier) {
8115
8237
  if (occurrences.length < 2) continue;
8238
+ const specifier = groupKey.slice(groupKey.indexOf(":") + 1);
8239
+ const kindLabel = groupKey.startsWith("type:") ? "type-only " : "";
8116
8240
  findings.push({
8117
8241
  path: module.fileId.path,
8118
8242
  specifier,
8119
8243
  occurrences,
8120
8244
  confidence: "high",
8121
- reason: `"${specifier}" is imported ${occurrences.length} times in this file — merge into a single statement`
8245
+ reason: `"${specifier}" is imported ${occurrences.length} times in this file as ${kindLabel}imports — merge into a single statement`
8122
8246
  });
8123
8247
  }
8124
8248
  }
@@ -8213,6 +8337,7 @@ const detectDuplicateConstants = (graph) => {
8213
8337
  const uniqueFilePaths = new Set(bucket.occurrences.map((occurrence) => occurrence.path));
8214
8338
  if (uniqueFilePaths.size < 3) continue;
8215
8339
  const uniqueNames = new Set(bucket.occurrences.map((occurrence) => occurrence.constantName));
8340
+ if (uniqueNames.size > 1 && hasDistinctUnitSuffixes([...uniqueNames])) continue;
8216
8341
  findings.push({
8217
8342
  literalHash,
8218
8343
  literalPreview: bucket.literalPreview,
@@ -8223,6 +8348,29 @@ const detectDuplicateConstants = (graph) => {
8223
8348
  }
8224
8349
  return findings;
8225
8350
  };
8351
+ const TRAILING_NAME_TOKEN_PATTERN = /_([A-Z][A-Z0-9]*)$/;
8352
+ const extractTrailingNameToken = (constantName) => {
8353
+ const match = constantName.match(TRAILING_NAME_TOKEN_PATTERN);
8354
+ return match ? match[1] : void 0;
8355
+ };
8356
+ /**
8357
+ * AGENTS.md requires magic numbers to use trailing unit suffixes (`_MS`, `_PX`,
8358
+ * `_TOKENS`, `_WIDTH`, …). When same-value constants carry DIFFERENT trailing
8359
+ * tokens (e.g. `STEP_DELAY_MS = 1000` vs `MINIMUM_TOKENS = 1000`), they
8360
+ * represent semantically distinct quantities that cannot be consolidated —
8361
+ * flagging them as duplicates is misleading. Constants sharing the same
8362
+ * trailing token (e.g. `CACHE_INTERVAL_MS` + `RECONNECT_DELAY_MS`, both `_MS`)
8363
+ * stay flagged because they are at least same-unit and might be extractable.
8364
+ */
8365
+ const hasDistinctUnitSuffixes = (constantNames) => {
8366
+ const trailingTokens = /* @__PURE__ */ new Set();
8367
+ for (const name of constantNames) {
8368
+ const token = extractTrailingNameToken(name);
8369
+ if (!token) return false;
8370
+ trailingTokens.add(token);
8371
+ }
8372
+ return trailingTokens.size > 1;
8373
+ };
8226
8374
  const detectSimplifiableExpressions = (graph) => {
8227
8375
  const findings = [];
8228
8376
  for (const module of graph.modules) {
@@ -9322,6 +9470,39 @@ const generateReport = (graph, config) => {
9322
9470
  //#region src/index.ts
9323
9471
  const STYLE_EXTENSIONS = [".css", ".scss"];
9324
9472
  const REACT_NATIVE_ENABLERS = ["react-native", "expo"];
9473
+ const basenameFromPath = (filePath) => {
9474
+ const lastSlashIndex = filePath.lastIndexOf("/");
9475
+ return lastSlashIndex === -1 ? filePath : filePath.slice(lastSlashIndex + 1);
9476
+ };
9477
+ /**
9478
+ * Dynamic registry pattern: many codebases use a central "schema/registry"
9479
+ * module that lists tool/command/page filenames as string literals, then a
9480
+ * runner spawns them via `path.resolve(dir, file)` or `import()`. Static
9481
+ * analysis can't follow the indirection, so those targets get falsely
9482
+ * flagged as unused.
9483
+ *
9484
+ * Heuristic: if a parsed string literal exactly matches the basename of
9485
+ * exactly one file in the project, treat that file as an entry point.
9486
+ * Uniqueness guards against false-positives from common names like
9487
+ * `index.ts` matching dozens of unrelated files.
9488
+ */
9489
+ const markFilenameRegistryEntries = (moduleGraph) => {
9490
+ const basenameToModuleIndex = /* @__PURE__ */ new Map();
9491
+ for (const module of moduleGraph.modules) {
9492
+ const basename = basenameFromPath(module.fileId.path);
9493
+ const existing = basenameToModuleIndex.get(basename);
9494
+ if (existing === void 0) basenameToModuleIndex.set(basename, module.fileId.index);
9495
+ else if (existing !== "ambiguous") basenameToModuleIndex.set(basename, "ambiguous");
9496
+ }
9497
+ for (const module of moduleGraph.modules) for (const referencedFilename of module.referencedFilenames) {
9498
+ const targetIndex = basenameToModuleIndex.get(referencedFilename);
9499
+ if (typeof targetIndex !== "number") continue;
9500
+ const targetModule = moduleGraph.modules[targetIndex];
9501
+ if (!targetModule || targetModule.isEntryPoint) continue;
9502
+ if (targetModule.fileId.index === module.fileId.index) continue;
9503
+ targetModule.isEntryPoint = true;
9504
+ }
9505
+ };
9325
9506
  const detectReactNative = (rootDir, workspacePackages) => {
9326
9507
  const directoriesToCheck = [rootDir, ...workspacePackages.map((workspacePackage) => workspacePackage.directory)];
9327
9508
  for (const directory of directoriesToCheck) {
@@ -9479,11 +9660,7 @@ const analyze = async (config) => {
9479
9660
  }));
9480
9661
  }
9481
9662
  const absoluteRoot = resolve(config.rootDir);
9482
- const outputDirectoryExclusions = OUTPUT_DIRECTORIES.flatMap((outputDirectory) => {
9483
- const exclusions = [`${absoluteRoot}/${outputDirectory}/**`];
9484
- for (const workspacePackage of workspacePackages) exclusions.push(`${workspacePackage.directory}/${outputDirectory}/**`);
9485
- return exclusions;
9486
- });
9663
+ const outputDirectoryExclusions = OUTPUT_DIRECTORIES.flatMap((outputDirectory) => [`${absoluteRoot}/${outputDirectory}/**`, `${absoluteRoot}/**/${outputDirectory}/**`]);
9487
9664
  const allExclusionPatterns = [
9488
9665
  ...workspaceDiscovery.excludedDirectories.map((directory) => `${directory}/**`),
9489
9666
  ...frameworkIgnorePatterns,
@@ -9683,6 +9860,7 @@ const analyze = async (config) => {
9683
9860
  detail: describeUnknownError(reExportError)
9684
9861
  }));
9685
9862
  }
9863
+ markFilenameRegistryEntries(moduleGraph);
9686
9864
  try {
9687
9865
  traceReachability(moduleGraph);
9688
9866
  } catch (reachabilityError) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "deslop-js",
3
- "version": "0.0.11",
3
+ "version": "0.0.12",
4
4
  "description": "Deslop JavaScript code",
5
5
  "keywords": [
6
6
  "dead-code",