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.
- package/dist/index.cjs +238 -60
- package/dist/index.mjs +238 -60
- 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
|
-
|
|
1084
|
-
if (
|
|
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
|
|
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 = [
|
|
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
|
|
1820
|
-
|
|
1821
|
-
|
|
1822
|
-
|
|
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
|
|
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(
|
|
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(
|
|
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 = (
|
|
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
|
|
7412
|
+
const installedPackageJsonPath = findInstalledPackageJsonPath(installedName, nodeModulesSearchRoots);
|
|
7413
|
+
if (!installedPackageJsonPath) continue;
|
|
7300
7414
|
try {
|
|
7301
|
-
const content = (0, node_fs.readFileSync)(
|
|
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 = (
|
|
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 =
|
|
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
|
|
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
|
|
8262
|
+
const groupKey = `${importInfo.isTypeOnly ? "type" : "value"}:${importInfo.specifier}`;
|
|
8263
|
+
const existing = groupedByKindAndSpecifier.get(groupKey);
|
|
8142
8264
|
if (existing) existing.push(occurrence);
|
|
8143
|
-
else
|
|
8265
|
+
else groupedByKindAndSpecifier.set(groupKey, [occurrence]);
|
|
8144
8266
|
}
|
|
8145
|
-
for (const [
|
|
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
|
-
|
|
1053
|
-
if (
|
|
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
|
|
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 = [
|
|
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
|
|
1789
|
-
|
|
1790
|
-
|
|
1791
|
-
|
|
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
|
|
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(
|
|
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(
|
|
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 = (
|
|
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
|
|
7381
|
+
const installedPackageJsonPath = findInstalledPackageJsonPath(installedName, nodeModulesSearchRoots);
|
|
7382
|
+
if (!installedPackageJsonPath) continue;
|
|
7269
7383
|
try {
|
|
7270
|
-
const content = readFileSync(
|
|
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 = (
|
|
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 =
|
|
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
|
|
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
|
|
8231
|
+
const groupKey = `${importInfo.isTypeOnly ? "type" : "value"}:${importInfo.specifier}`;
|
|
8232
|
+
const existing = groupedByKindAndSpecifier.get(groupKey);
|
|
8111
8233
|
if (existing) existing.push(occurrence);
|
|
8112
|
-
else
|
|
8234
|
+
else groupedByKindAndSpecifier.set(groupKey, [occurrence]);
|
|
8113
8235
|
}
|
|
8114
|
-
for (const [
|
|
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) {
|