deslop-js 0.0.12 → 0.0.14-dev.a6825a1

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 CHANGED
@@ -33,10 +33,10 @@ let fast_glob = require("fast-glob");
33
33
  fast_glob = __toESM(fast_glob, 1);
34
34
  let node_fs_promises = require("node:fs/promises");
35
35
  let oxc_parser = require("oxc-parser");
36
- let oxc_resolver = require("oxc-resolver");
37
- let minimatch = require("minimatch");
38
36
  let typescript = require("typescript");
39
37
  typescript = __toESM(typescript, 1);
38
+ let oxc_resolver = require("oxc-resolver");
39
+ let minimatch = require("minimatch");
40
40
 
41
41
  //#region src/constants.ts
42
42
  const DEFAULT_EXTENSIONS = [
@@ -250,6 +250,7 @@ const BUILTIN_MODULES = new Set([
250
250
  ]);
251
251
  const PLATFORM_SUFFIXES = [
252
252
  ".web",
253
+ ".react-native",
253
254
  ".native",
254
255
  ".ios",
255
256
  ".android",
@@ -257,6 +258,7 @@ const PLATFORM_SUFFIXES = [
257
258
  ".windows",
258
259
  ".macos",
259
260
  ".any",
261
+ ".react-server",
260
262
  ".server",
261
263
  ".client"
262
264
  ];
@@ -952,7 +954,7 @@ const visitFunctionParameters = (parameters, captures, functionName) => {
952
954
  inspectTypeAnnotation(parameter.typeAnnotation, captures, "function-parameter", functionName ? `${functionName}(${parameterIdentifierName ?? "?"})` : parameterIdentifierName);
953
955
  }
954
956
  };
955
- const visitFunctionLike = (functionNode, captures, functionName) => {
957
+ const visitFunctionLike$1 = (functionNode, captures, functionName) => {
956
958
  const parameters = functionNode.params;
957
959
  visitFunctionParameters(parameters, captures, functionName);
958
960
  const returnTypeNode = functionNode.returnType;
@@ -967,7 +969,7 @@ const visitVariableDeclaration = (declarationNode, captures, enclosingName) => {
967
969
  const declarationName = getIdentifierName(declarator.id);
968
970
  inspectTypeAnnotation(declarator.typeAnnotation ?? (declarator.id && isOxcAstNode(declarator.id) ? declarator.id.typeAnnotation : void 0), captures, "variable-annotation", declarationName);
969
971
  const initializerNode = declarator.init;
970
- if (isOxcAstNode(initializerNode)) if (initializerNode.type === "ArrowFunctionExpression" || initializerNode.type === "FunctionExpression") visitFunctionLike(initializerNode, captures, declarationName ?? enclosingName);
972
+ if (isOxcAstNode(initializerNode)) if (initializerNode.type === "ArrowFunctionExpression" || initializerNode.type === "FunctionExpression") visitFunctionLike$1(initializerNode, captures, declarationName ?? enclosingName);
971
973
  else walkExpressionForInlineTypes(initializerNode, captures, declarationName ?? enclosingName);
972
974
  }
973
975
  };
@@ -979,7 +981,7 @@ const walkBodyForInlineTypes = (bodyNode, captures, enclosingName, recursionDept
979
981
  for (const statement of statements) {
980
982
  if (!isOxcAstNode(statement)) continue;
981
983
  if (statement.type === "VariableDeclaration") visitVariableDeclaration(statement, captures, enclosingName);
982
- else if (statement.type === "FunctionDeclaration") visitFunctionLike(statement, captures, getIdentifierName(statement.id) ?? enclosingName);
984
+ else if (statement.type === "FunctionDeclaration") visitFunctionLike$1(statement, captures, getIdentifierName(statement.id) ?? enclosingName);
983
985
  else if (statement.type === "TSTypeAliasDeclaration") {
984
986
  const typeAliasName = getIdentifierName(statement.id);
985
987
  captureIfTypeLiteral(statement.typeAnnotation, captures, "local-type-alias", typeAliasName);
@@ -992,7 +994,7 @@ const walkExpressionForInlineTypes = (expressionNode, captures, enclosingName, r
992
994
  if (recursionDepth > 200) return;
993
995
  if (!isOxcAstNode(expressionNode)) return;
994
996
  if (expressionNode.type === "ArrowFunctionExpression" || expressionNode.type === "FunctionExpression") {
995
- visitFunctionLike(expressionNode, captures, enclosingName);
997
+ visitFunctionLike$1(expressionNode, captures, enclosingName);
996
998
  return;
997
999
  }
998
1000
  for (const value of Object.values(expressionNode)) if (Array.isArray(value)) for (const element of value) walkExpressionForInlineTypes(element, captures, enclosingName, recursionDepth + 1);
@@ -1003,7 +1005,7 @@ const visitTopLevelStatement = (statementNode, captures) => {
1003
1005
  const innerNode = statementNode.type === "ExportNamedDeclaration" || statementNode.type === "ExportDefaultDeclaration" ? statementNode.declaration ?? statementNode : statementNode;
1004
1006
  const targetNode = isOxcAstNode(innerNode) ? innerNode : statementNode;
1005
1007
  if (targetNode.type === "FunctionDeclaration") {
1006
- visitFunctionLike(targetNode, captures, getIdentifierName(targetNode.id));
1008
+ visitFunctionLike$1(targetNode, captures, getIdentifierName(targetNode.id));
1007
1009
  return;
1008
1010
  }
1009
1011
  if (targetNode.type === "VariableDeclaration") {
@@ -1023,7 +1025,7 @@ const visitTopLevelStatement = (statementNode, captures) => {
1023
1025
  }
1024
1026
  if (memberCandidate.type === "MethodDefinition" || memberCandidate.type === "TSAbstractMethodDefinition") {
1025
1027
  const methodValue = memberCandidate.value;
1026
- if (isOxcAstNode(methodValue)) visitFunctionLike(methodValue, captures, qualifiedName);
1028
+ if (isOxcAstNode(methodValue)) visitFunctionLike$1(methodValue, captures, qualifiedName);
1027
1029
  }
1028
1030
  }
1029
1031
  return;
@@ -1573,10 +1575,24 @@ const extractMdxImportsExports = (sourceText) => {
1573
1575
  return statements.join("\n");
1574
1576
  };
1575
1577
  const ASTRO_FRONTMATTER_PATTERN = /^---\r?\n([\s\S]*?)\r?\n---/;
1576
- const extractAstroFrontmatter = (sourceText) => {
1578
+ const ASTRO_SCRIPT_TAG_PATTERN = /<script\b([^>]*?)\/>|<script\b([^>]*)>([\s\S]*?)<\/script>/gi;
1579
+ const ASTRO_SCRIPT_SRC_ATTRIBUTE_PATTERN = /\bsrc\s*=\s*["']([^"']+)["']/i;
1580
+ const extractAstroSources = (sourceText) => {
1581
+ const sections = [];
1577
1582
  const frontmatterMatch = sourceText.match(ASTRO_FRONTMATTER_PATTERN);
1578
- if (!frontmatterMatch) return "";
1579
- return frontmatterMatch[1];
1583
+ if (frontmatterMatch) sections.push(frontmatterMatch[1]);
1584
+ ASTRO_SCRIPT_TAG_PATTERN.lastIndex = 0;
1585
+ let scriptMatch;
1586
+ while ((scriptMatch = ASTRO_SCRIPT_TAG_PATTERN.exec(sourceText)) !== null) {
1587
+ const selfClosingAttributes = scriptMatch[1];
1588
+ const pairedAttributes = scriptMatch[2];
1589
+ const attributes = selfClosingAttributes ?? pairedAttributes ?? "";
1590
+ const body = selfClosingAttributes === void 0 ? scriptMatch[3] ?? "" : "";
1591
+ const srcMatch = attributes.match(ASTRO_SCRIPT_SRC_ATTRIBUTE_PATTERN);
1592
+ if (srcMatch) sections.push(`import ${JSON.stringify(srcMatch[1])};`);
1593
+ if (body) sections.push(body);
1594
+ }
1595
+ return sections.join("\n");
1580
1596
  };
1581
1597
  const VUE_SCRIPT_PATTERN = /<script[^>]*(?:lang=["'](?:ts|tsx)["'][^>]*)?>([\s\S]*?)<\/script>/gi;
1582
1598
  const extractVueScriptContent = (sourceText) => {
@@ -1794,7 +1810,8 @@ const parseSourceFile = (filePath) => {
1794
1810
  const isAstro = filePath.endsWith(".astro");
1795
1811
  const isVue = filePath.endsWith(".vue");
1796
1812
  const isSvelte = filePath.endsWith(".svelte");
1797
- const textToParse = isMdx ? extractMdxImportsExports(sourceText) : isAstro ? extractAstroFrontmatter(sourceText) : isVue ? extractVueScriptContent(sourceText) : isSvelte ? extractSvelteScriptContent(sourceText) : sourceText;
1813
+ const isPreprocessed = isMdx || isAstro || isVue || isSvelte;
1814
+ const textToParse = isMdx ? extractMdxImportsExports(sourceText) : isAstro ? extractAstroSources(sourceText) : isVue ? extractVueScriptContent(sourceText) : isSvelte ? extractSvelteScriptContent(sourceText) : sourceText;
1798
1815
  const parseFileName = isMdx || isAstro || isVue || isSvelte ? filePath.replace(/\.(mdx|astro|vue|svelte)$/, ".tsx") : filePath;
1799
1816
  let result;
1800
1817
  try {
@@ -1818,7 +1835,7 @@ const parseSourceFile = (filePath) => {
1818
1835
  if (tsxResult.errors.length === 0) result = tsxResult;
1819
1836
  }
1820
1837
  } catch {}
1821
- if (result.errors.length > 0) return {
1838
+ if (result.errors.length > 0 && !isPreprocessed) return {
1822
1839
  ...createEmptyParsedSource(),
1823
1840
  imports,
1824
1841
  exports,
@@ -1830,6 +1847,12 @@ const parseSourceFile = (filePath) => {
1830
1847
  path: filePath
1831
1848
  })]
1832
1849
  };
1850
+ if (result.errors.length > 0) earlyErrors.push(new ParseError({
1851
+ code: "parse-recovered-partial",
1852
+ severity: "info",
1853
+ message: `oxc-parser reported ${result.errors.length} syntax issue(s) in extracted ${isAstro ? "Astro" : isVue ? "Vue" : isSvelte ? "Svelte" : "MDX"} sources; continuing with partial AST`,
1854
+ path: filePath
1855
+ }));
1833
1856
  const program = result.program;
1834
1857
  if (!program?.body) return {
1835
1858
  ...createEmptyParsedSource(),
@@ -3122,6 +3145,7 @@ const EXPO_ENTRY_PATTERNS = [
3122
3145
  ];
3123
3146
  const EXPO_ROUTER_ENTRY_PATTERNS = [
3124
3147
  "app/**/*.{ts,tsx,js,jsx}",
3148
+ "src/app/**/*.{ts,tsx,js,jsx}",
3125
3149
  "app.config.{ts,js,mjs,cjs}",
3126
3150
  "metro.config.{ts,js,mjs,cjs}",
3127
3151
  "babel.config.{ts,js,mjs,cjs}"
@@ -3164,6 +3188,196 @@ const discoverMobileEntryPoints = (directory) => {
3164
3188
  }
3165
3189
  };
3166
3190
 
3191
+ //#endregion
3192
+ //#region src/collect/expo-config-plugin-entries.ts
3193
+ const EXPO_CONFIG_FILE_GLOBS = ["app.config.{ts,mts,cts,js,mjs,cjs}", "app.json"];
3194
+ const NESTED_EXPO_CONFIG_FILE_GLOBS = [
3195
+ ...EXPO_CONFIG_FILE_GLOBS,
3196
+ "**/app.config.{ts,mts,cts,js,mjs,cjs}",
3197
+ "**/app.json"
3198
+ ];
3199
+ const EXPO_REACT_NATIVE_DEPENDENCIES = new Set(["expo", "react-native"]);
3200
+ const EXPO_PLUGIN_RESOLVABLE_EXTENSIONS = SOURCE_EXTENSIONS$3.map((sourceExtension) => `.${sourceExtension}`);
3201
+ const isRecord = (value) => typeof value === "object" && value !== null;
3202
+ const isExpoOrReactNativeWorkspace = (dependencies) => [...EXPO_REACT_NATIVE_DEPENDENCIES].some((dependencyName) => dependencyName in dependencies);
3203
+ const isLocalExpoPluginPath = (value) => (value.startsWith("./") || value.startsWith("../")) && !value.includes("*") && !value.includes("?");
3204
+ const isFile = (filePath) => {
3205
+ try {
3206
+ return (0, node_fs.statSync)(filePath).isFile();
3207
+ } catch {
3208
+ return false;
3209
+ }
3210
+ };
3211
+ const resolveExpoPluginPath = (configDirectory, pluginPath) => {
3212
+ const candidatePath = (0, node_path.resolve)(configDirectory, pluginPath);
3213
+ if (isFile(candidatePath)) return candidatePath;
3214
+ for (const extension of EXPO_PLUGIN_RESOLVABLE_EXTENSIONS) {
3215
+ const candidatePathWithExtension = `${candidatePath}${extension}`;
3216
+ if (isFile(candidatePathWithExtension)) return candidatePathWithExtension;
3217
+ }
3218
+ for (const extension of EXPO_PLUGIN_RESOLVABLE_EXTENSIONS) {
3219
+ const indexCandidatePath = (0, node_path.join)(candidatePath, `index${extension}`);
3220
+ if (isFile(indexCandidatePath)) return indexCandidatePath;
3221
+ }
3222
+ };
3223
+ const addExpoPluginEntry = (entries, rootDirectory, configDirectory, pluginPath) => {
3224
+ if (!isLocalExpoPluginPath(pluginPath)) return;
3225
+ const resolvedPath = resolveExpoPluginPath(configDirectory, pluginPath);
3226
+ if (!resolvedPath) return;
3227
+ const relativePath = (0, node_path.relative)(rootDirectory, resolvedPath);
3228
+ if (relativePath !== "" && (relativePath.startsWith("..") || (0, node_path.isAbsolute)(relativePath))) return;
3229
+ entries.add(resolvedPath);
3230
+ };
3231
+ const getPropertyName = (name) => {
3232
+ if (typescript.default.isIdentifier(name) || typescript.default.isStringLiteral(name) || typescript.default.isNumericLiteral(name)) return name.text;
3233
+ };
3234
+ const unwrapExpression = (expression) => {
3235
+ let currentExpression = expression;
3236
+ while (typescript.default.isParenthesizedExpression(currentExpression)) currentExpression = currentExpression.expression;
3237
+ return currentExpression;
3238
+ };
3239
+ const collectExpoPluginPathsFromArray = (array, entries, rootDirectory, configDirectory) => {
3240
+ for (const element of array.elements) {
3241
+ if (typescript.default.isStringLiteral(element) || typescript.default.isNoSubstitutionTemplateLiteral(element)) {
3242
+ addExpoPluginEntry(entries, rootDirectory, configDirectory, element.text);
3243
+ continue;
3244
+ }
3245
+ if (typescript.default.isArrayLiteralExpression(element)) {
3246
+ const [pluginName] = element.elements;
3247
+ if (pluginName && (typescript.default.isStringLiteral(pluginName) || typescript.default.isNoSubstitutionTemplateLiteral(pluginName))) addExpoPluginEntry(entries, rootDirectory, configDirectory, pluginName.text);
3248
+ }
3249
+ }
3250
+ };
3251
+ const collectExpoPluginPathsFromConfigObject = (objectLiteral, entries, rootDirectory, configDirectory) => {
3252
+ for (const property of objectLiteral.properties) if (typescript.default.isPropertyAssignment(property) && getPropertyName(property.name) === "plugins" && typescript.default.isArrayLiteralExpression(property.initializer)) collectExpoPluginPathsFromArray(property.initializer, entries, rootDirectory, configDirectory);
3253
+ };
3254
+ const collectReturnedExpoConfigPluginPaths = (body, entries, rootDirectory, configDirectory) => {
3255
+ if (!typescript.default.isBlock(body)) {
3256
+ const expression = unwrapExpression(body);
3257
+ if (typescript.default.isObjectLiteralExpression(expression)) collectExpoPluginPathsFromConfigObject(expression, entries, rootDirectory, configDirectory);
3258
+ return;
3259
+ }
3260
+ const visit = (node) => {
3261
+ if (typescript.default.isFunctionDeclaration(node) || typescript.default.isFunctionExpression(node) || typescript.default.isArrowFunction(node)) return;
3262
+ if (typescript.default.isReturnStatement(node) && node.expression) {
3263
+ const expression = unwrapExpression(node.expression);
3264
+ if (typescript.default.isObjectLiteralExpression(expression)) collectExpoPluginPathsFromConfigObject(expression, entries, rootDirectory, configDirectory);
3265
+ return;
3266
+ }
3267
+ typescript.default.forEachChild(node, visit);
3268
+ };
3269
+ visit(body);
3270
+ };
3271
+ const collectExpoPluginPathsFromConfigExpression = (expression, entries, rootDirectory, configDirectory, bindings, seenIdentifiers = /* @__PURE__ */ new Set()) => {
3272
+ const configExpression = unwrapExpression(expression);
3273
+ if (typescript.default.isObjectLiteralExpression(configExpression)) {
3274
+ collectExpoPluginPathsFromConfigObject(configExpression, entries, rootDirectory, configDirectory);
3275
+ return;
3276
+ }
3277
+ if (typescript.default.isIdentifier(configExpression)) {
3278
+ if (seenIdentifiers.has(configExpression.text)) return;
3279
+ seenIdentifiers.add(configExpression.text);
3280
+ const boundExpression = bindings.expressions.get(configExpression.text);
3281
+ if (boundExpression) {
3282
+ collectExpoPluginPathsFromConfigExpression(boundExpression, entries, rootDirectory, configDirectory, bindings, seenIdentifiers);
3283
+ return;
3284
+ }
3285
+ const boundFunction = bindings.functions.get(configExpression.text);
3286
+ if (boundFunction?.body) collectReturnedExpoConfigPluginPaths(boundFunction.body, entries, rootDirectory, configDirectory);
3287
+ return;
3288
+ }
3289
+ if (typescript.default.isArrowFunction(configExpression)) {
3290
+ collectReturnedExpoConfigPluginPaths(configExpression.body, entries, rootDirectory, configDirectory);
3291
+ return;
3292
+ }
3293
+ if (typescript.default.isFunctionExpression(configExpression) && configExpression.body) collectReturnedExpoConfigPluginPaths(configExpression.body, entries, rootDirectory, configDirectory);
3294
+ };
3295
+ const hasDefaultExportModifier = (node) => Boolean(typescript.default.canHaveModifiers(node) && typescript.default.getModifiers(node)?.some((modifier) => modifier.kind === typescript.default.SyntaxKind.DefaultKeyword));
3296
+ const isModuleExportsAssignmentTarget = (node) => typescript.default.isPropertyAccessExpression(node) && typescript.default.isIdentifier(node.expression) && node.expression.text === "module" && node.name.text === "exports";
3297
+ const collectStaticConfigBindings = (sourceFile) => {
3298
+ const expressions = /* @__PURE__ */ new Map();
3299
+ const functions = /* @__PURE__ */ new Map();
3300
+ for (const statement of sourceFile.statements) {
3301
+ if (typescript.default.isVariableStatement(statement)) {
3302
+ for (const declaration of statement.declarationList.declarations) if (typescript.default.isIdentifier(declaration.name) && declaration.initializer) expressions.set(declaration.name.text, declaration.initializer);
3303
+ continue;
3304
+ }
3305
+ if (typescript.default.isFunctionDeclaration(statement) && statement.name) functions.set(statement.name.text, statement);
3306
+ }
3307
+ return {
3308
+ expressions,
3309
+ functions
3310
+ };
3311
+ };
3312
+ const collectExpoPluginPathsFromAppConfig = (configPath, entries, rootDirectory) => {
3313
+ const extension = (0, node_path.extname)(configPath);
3314
+ const sourceFile = typescript.default.createSourceFile(configPath, (0, node_fs.readFileSync)(configPath, "utf8"), typescript.default.ScriptTarget.Latest, true, extension === ".ts" || extension === ".mts" || extension === ".cts" ? typescript.default.ScriptKind.TS : typescript.default.ScriptKind.JS);
3315
+ const configDirectory = (0, node_path.dirname)(configPath);
3316
+ const bindings = collectStaticConfigBindings(sourceFile);
3317
+ const visit = (node) => {
3318
+ if (typescript.default.isExportAssignment(node)) {
3319
+ collectExpoPluginPathsFromConfigExpression(node.expression, entries, rootDirectory, configDirectory, bindings);
3320
+ return;
3321
+ }
3322
+ if (typescript.default.isFunctionDeclaration(node) && hasDefaultExportModifier(node) && node.body) {
3323
+ collectReturnedExpoConfigPluginPaths(node.body, entries, rootDirectory, configDirectory);
3324
+ return;
3325
+ }
3326
+ if (typescript.default.isBinaryExpression(node) && node.operatorToken.kind === typescript.default.SyntaxKind.EqualsToken && isModuleExportsAssignmentTarget(node.left)) {
3327
+ collectExpoPluginPathsFromConfigExpression(node.right, entries, rootDirectory, configDirectory, bindings);
3328
+ return;
3329
+ }
3330
+ typescript.default.forEachChild(node, visit);
3331
+ };
3332
+ visit(sourceFile);
3333
+ };
3334
+ const collectPluginPathsFromJsonValue = (value) => {
3335
+ if (!Array.isArray(value)) return [];
3336
+ const pluginPaths = [];
3337
+ for (const plugin of value) {
3338
+ if (typeof plugin === "string") {
3339
+ pluginPaths.push(plugin);
3340
+ continue;
3341
+ }
3342
+ if (Array.isArray(plugin) && typeof plugin[0] === "string") pluginPaths.push(plugin[0]);
3343
+ }
3344
+ return pluginPaths;
3345
+ };
3346
+ const collectExpoPluginPathsFromAppJson = (configPath, entries, rootDirectory) => {
3347
+ const parsedJson = JSON.parse((0, node_fs.readFileSync)(configPath, "utf8"));
3348
+ const configDirectory = (0, node_path.dirname)(configPath);
3349
+ if (!isRecord(parsedJson)) return;
3350
+ const expoConfig = parsedJson.expo;
3351
+ const expoPluginPaths = isRecord(expoConfig) ? collectPluginPathsFromJsonValue(expoConfig.plugins) : [];
3352
+ for (const pluginPath of [...expoPluginPaths, ...collectPluginPathsFromJsonValue(parsedJson.plugins)]) addExpoPluginEntry(entries, rootDirectory, configDirectory, pluginPath);
3353
+ };
3354
+ const collectExpoPluginPathsFromConfig = (configPath, entries, rootDirectory) => {
3355
+ try {
3356
+ if ((0, node_path.basename)(configPath) === "app.json") {
3357
+ collectExpoPluginPathsFromAppJson(configPath, entries, rootDirectory);
3358
+ return;
3359
+ }
3360
+ collectExpoPluginPathsFromAppConfig(configPath, entries, rootDirectory);
3361
+ } catch {}
3362
+ };
3363
+ const extractExpoConfigPluginEntries = (directory, dependencies, rootDirectory = directory, includeNestedConfigs = true) => {
3364
+ if (!isExpoOrReactNativeWorkspace(dependencies)) return [];
3365
+ const entries = /* @__PURE__ */ new Set();
3366
+ const configPaths = fast_glob.default.sync(includeNestedConfigs ? NESTED_EXPO_CONFIG_FILE_GLOBS : EXPO_CONFIG_FILE_GLOBS, {
3367
+ cwd: directory,
3368
+ absolute: true,
3369
+ onlyFiles: true,
3370
+ ignore: [
3371
+ "**/node_modules/**",
3372
+ "**/dist/**",
3373
+ "**/build/**"
3374
+ ],
3375
+ deep: 6
3376
+ });
3377
+ for (const configPath of configPaths) collectExpoPluginPathsFromConfig(configPath, entries, rootDirectory);
3378
+ return [...entries];
3379
+ };
3380
+
3167
3381
  //#endregion
3168
3382
  //#region src/resolver/source-path.ts
3169
3383
  const SOURCE_EXTENSIONS$1 = [
@@ -3204,6 +3418,8 @@ const resolveSourcePath = (distPath, directory) => {
3204
3418
  const sourceCandidate = (0, node_path.resolve)(directory, withoutExtension + sourceExtension);
3205
3419
  if ((0, node_fs.existsSync)(sourceCandidate)) return sourceCandidate;
3206
3420
  }
3421
+ const indexPrefixedCandidate = resolveWithIndexPrefix(withoutExtension, directory);
3422
+ if (indexPrefixedCandidate) return indexPrefixedCandidate;
3207
3423
  }
3208
3424
  if (matchesOutputDirectory(relativeToDist)) for (const stem of SOURCE_INDEX_FALLBACK_STEMS) for (const sourceExtension of SOURCE_EXTENSIONS$1) {
3209
3425
  const fallbackCandidate = (0, node_path.resolve)(directory, stem + sourceExtension);
@@ -3217,6 +3433,15 @@ const resolveSourcePath = (distPath, directory) => {
3217
3433
  }
3218
3434
  const indexCandidate = (0, node_path.resolve)(directory, withoutExtension, "index.ts");
3219
3435
  if ((0, node_fs.existsSync)(indexCandidate)) return indexCandidate;
3436
+ const indexPrefixedCandidate = resolveWithIndexPrefix(withoutExtension, directory);
3437
+ if (indexPrefixedCandidate) return indexPrefixedCandidate;
3438
+ }
3439
+ };
3440
+ const resolveWithIndexPrefix = (stemPath, directory) => {
3441
+ const indexPrefixedStem = `${(0, node_path.dirname)(stemPath)}/index.${(0, node_path.basename)(stemPath)}`;
3442
+ for (const sourceExtension of SOURCE_EXTENSIONS$1) {
3443
+ const candidate = (0, node_path.resolve)(directory, indexPrefixedStem + sourceExtension);
3444
+ if ((0, node_fs.existsSync)(candidate)) return candidate;
3220
3445
  }
3221
3446
  };
3222
3447
 
@@ -3460,6 +3685,10 @@ const extractSectionsModuleEntries = (directory) => {
3460
3685
  return [...entries];
3461
3686
  };
3462
3687
 
3688
+ //#endregion
3689
+ //#region src/utils/to-posix-path.ts
3690
+ const toPosixPath = (filePath) => filePath.replace(/\\/g, "/");
3691
+
3463
3692
  //#endregion
3464
3693
  //#region src/collect/entries.ts
3465
3694
  const collectSourceFiles = async (config) => {
@@ -3576,6 +3805,11 @@ const resolveEntries = async (config) => {
3576
3805
  for (const workspacePackage of entryEligiblePackages) tsConfigIncludeEntries.push(...extractTsConfigIncludeFilesEntries(workspacePackage.directory));
3577
3806
  const configStringEntries = extractConfigStringReferencedEntries(absoluteRoot);
3578
3807
  for (const workspacePackage of entryEligiblePackages) configStringEntries.push(...extractConfigStringReferencedEntries(workspacePackage.directory));
3808
+ const expoConfigPluginEntries = extractExpoConfigPluginEntries(absoluteRoot, readPackageJsonDependencies((0, node_path.join)(absoluteRoot, "package.json")), absoluteRoot, false);
3809
+ for (const workspacePackage of entryEligiblePackages) {
3810
+ const workspacePackageDependencies = readPackageJsonDependencies((0, node_path.join)(workspacePackage.directory, "package.json"));
3811
+ expoConfigPluginEntries.push(...extractExpoConfigPluginEntries(workspacePackage.directory, workspacePackageDependencies, absoluteRoot));
3812
+ }
3579
3813
  const sectionsModuleEntries = extractSectionsModuleEntries(absoluteRoot);
3580
3814
  const wranglerEntries = extractWranglerEntries(absoluteRoot);
3581
3815
  for (const workspacePackage of entryEligiblePackages) wranglerEntries.push(...extractWranglerEntries(workspacePackage.directory));
@@ -3586,7 +3820,7 @@ const resolveEntries = async (config) => {
3586
3820
  const testRunnerDiscovery = discoverTestRunnerEntryPoints(absoluteRoot, entryEligiblePackages);
3587
3821
  const toolingDiscovery = discoverToolingEntryPoints(absoluteRoot, entryEligiblePackages);
3588
3822
  const ciEntries = extractCiWorkflowEntries(absoluteRoot);
3589
- const testEntries = [...new Set([...testRunnerDiscovery.entryFiles, ...testSetupEntries])];
3823
+ const testEntries = [...new Set([...testRunnerDiscovery.entryFiles, ...testSetupEntries].map(toPosixPath))];
3590
3824
  const testEntryPathSet = new Set(testEntries);
3591
3825
  return {
3592
3826
  productionEntries: [...new Set([
@@ -3604,14 +3838,15 @@ const resolveEntries = async (config) => {
3604
3838
  ...webWorkerEntries,
3605
3839
  ...tsConfigIncludeEntries,
3606
3840
  ...configStringEntries,
3841
+ ...expoConfigPluginEntries,
3607
3842
  ...sectionsModuleEntries,
3608
3843
  ...wranglerEntries,
3609
3844
  ...pluginFileEntries,
3610
3845
  ...toolingDiscovery.entryFiles,
3611
3846
  ...ciEntries
3612
- ])].filter((entryPath) => !testEntryPathSet.has(entryPath)),
3847
+ ].map(toPosixPath))].filter((entryPath) => !testEntryPathSet.has(entryPath)),
3613
3848
  testEntries,
3614
- alwaysUsedFiles: [...new Set([...toolingDiscovery.alwaysUsedFiles, ...testRunnerDiscovery.alwaysUsedFiles])]
3849
+ alwaysUsedFiles: [...new Set([...toolingDiscovery.alwaysUsedFiles, ...testRunnerDiscovery.alwaysUsedFiles].map(toPosixPath))]
3615
3850
  };
3616
3851
  };
3617
3852
  const DEFAULT_INDEX_PATTERNS = [
@@ -3823,6 +4058,7 @@ const SCRIPT_MULTIPLEXERS = new Set([
3823
4058
  "lerna",
3824
4059
  "ultra"
3825
4060
  ]);
4061
+ const TSCONFIG_PROJECT_FLAGS = new Set(["--project", "-p"]);
3826
4062
  const CONFIG_LIKE_FLAGS = new Set([
3827
4063
  "--config",
3828
4064
  "-c",
@@ -3968,7 +4204,8 @@ const extractScriptFileArguments = (scriptCommand, directory) => {
3968
4204
  const configPath = tokens[tokenIndex + 1].replace(/^['"]|['"]$/g, "");
3969
4205
  if (looksLikeFilePath(configPath)) {
3970
4206
  const absoluteConfigPath = (0, node_path.resolve)(directory, configPath);
3971
- if ((0, node_fs.existsSync)(absoluteConfigPath)) entries.push(absoluteConfigPath);
4207
+ if ((0, node_fs.existsSync)(absoluteConfigPath)) if (TSCONFIG_PROJECT_FLAGS.has(token) && TSCONFIG_PROJECT_PATTERN.test(absoluteConfigPath)) entries.push(...expandTsConfigProjectEntries(absoluteConfigPath));
4208
+ else entries.push(absoluteConfigPath);
3972
4209
  }
3973
4210
  tokenIndex++;
3974
4211
  }
@@ -3977,9 +4214,11 @@ const extractScriptFileArguments = (scriptCommand, directory) => {
3977
4214
  const equalsIndex = token.indexOf("=");
3978
4215
  if (equalsIndex > 0 && CONFIG_LIKE_FLAGS.has(token.slice(0, equalsIndex))) {
3979
4216
  const configValue = token.slice(equalsIndex + 1);
4217
+ const flagName = token.slice(0, equalsIndex);
3980
4218
  if (configValue && looksLikeFilePath(configValue)) {
3981
4219
  const absoluteConfigPath = (0, node_path.resolve)(directory, configValue);
3982
- if ((0, node_fs.existsSync)(absoluteConfigPath)) entries.push(absoluteConfigPath);
4220
+ if ((0, node_fs.existsSync)(absoluteConfigPath)) if (TSCONFIG_PROJECT_FLAGS.has(flagName) && TSCONFIG_PROJECT_PATTERN.test(absoluteConfigPath)) entries.push(...expandTsConfigProjectEntries(absoluteConfigPath));
4221
+ else entries.push(absoluteConfigPath);
3983
4222
  }
3984
4223
  continue;
3985
4224
  }
@@ -4282,6 +4521,7 @@ const extractScriptTagsFromHtmlFile = (htmlFilePath) => {
4282
4521
  return entries;
4283
4522
  };
4284
4523
  const TSCONFIG_FILENAME_GLOBS = ["tsconfig.json", "tsconfig.*.json"];
4524
+ const TSCONFIG_PROJECT_PATTERN = /(?:^|[\\/])tsconfig(?:\.[^.]+)?\.json$/;
4285
4525
  const stripJsoncCommentsLocal = (sourceText) => {
4286
4526
  let result = "";
4287
4527
  let insideString = false;
@@ -4353,6 +4593,34 @@ const extractTsConfigIncludeFilesEntries = (directory) => {
4353
4593
  } catch {}
4354
4594
  return entries;
4355
4595
  };
4596
+ const expandTsConfigProjectEntries = (tsconfigAbsolutePath) => {
4597
+ const entries = [];
4598
+ try {
4599
+ const cleaned = stripJsoncCommentsLocal((0, node_fs.readFileSync)(tsconfigAbsolutePath, "utf-8"));
4600
+ const tsconfigJson = JSON.parse(cleaned);
4601
+ const tsconfigDir = (0, node_path.dirname)(tsconfigAbsolutePath);
4602
+ if (Array.isArray(tsconfigJson.files)) for (const fileItem of tsconfigJson.files) {
4603
+ if (typeof fileItem !== "string") continue;
4604
+ const candidatePath = (0, node_path.resolve)(tsconfigDir, fileItem);
4605
+ if ((0, node_fs.existsSync)(candidatePath)) entries.push(candidatePath);
4606
+ }
4607
+ if (Array.isArray(tsconfigJson.include)) for (const includePattern of tsconfigJson.include) {
4608
+ if (typeof includePattern !== "string") continue;
4609
+ const expandedFiles = fast_glob.default.sync(includePattern, {
4610
+ cwd: tsconfigDir,
4611
+ absolute: true,
4612
+ onlyFiles: true,
4613
+ ignore: [
4614
+ "**/node_modules/**",
4615
+ "**/dist/**",
4616
+ "**/build/**"
4617
+ ]
4618
+ });
4619
+ entries.push(...expandedFiles);
4620
+ }
4621
+ } catch {}
4622
+ return entries;
4623
+ };
4356
4624
  const WRANGLER_TOML_MAIN_PATTERN = /^\s*main\s*=\s*['"]([^'"\n]+)['"]/m;
4357
4625
  const WRANGLER_JSON_MAIN_PATTERN = /"main"\s*:\s*"([^"]+)"/;
4358
4626
  const WRANGLER_SERVICE_BINDINGS_PATTERN = /entry_point\s*=\s*['"]([^'"\n]+)['"]/g;
@@ -4871,6 +5139,9 @@ const TEST_FRAMEWORK_PATTERNS = [
4871
5139
  alwaysUsed: ["cypress.config.{ts,js}", "cypress.config.*.{ts,js}"]
4872
5140
  }
4873
5141
  ];
5142
+ const JS_TS_COMPONENT_EXTENSIONS = "{ts,tsx,js,jsx}";
5143
+ const INERTIA_COMPONENT_EXTENSIONS = "{ts,tsx,js,jsx,vue,svelte}";
5144
+ const VIKE_ROUTE_EXTENSIONS = "{ts,tsx,js,jsx,md,mdx}";
4874
5145
  const FRAMEWORK_PATTERNS = [
4875
5146
  {
4876
5147
  enablers: ["storybook"],
@@ -4990,6 +5261,50 @@ const FRAMEWORK_PATTERNS = [
4990
5261
  ],
4991
5262
  alwaysUsed: ["angular.json", "**/karma.conf.js"]
4992
5263
  },
5264
+ {
5265
+ enablers: [
5266
+ "@inertiajs/react",
5267
+ "@inertiajs/inertia-react",
5268
+ "@inertiajs/vue3",
5269
+ "@inertiajs/inertia-vue3",
5270
+ "@inertiajs/svelte",
5271
+ "@inertiajs/inertia-svelte",
5272
+ "@inertiajs/inertia"
5273
+ ],
5274
+ enablerPrefixes: [],
5275
+ entryPatterns: [
5276
+ `resources/js/app.${INERTIA_COMPONENT_EXTENSIONS}`,
5277
+ `resources/js/App.${INERTIA_COMPONENT_EXTENSIONS}`,
5278
+ `resources/js/Pages/**/*.${INERTIA_COMPONENT_EXTENSIONS}`,
5279
+ `resources/js/pages/**/*.${INERTIA_COMPONENT_EXTENSIONS}`,
5280
+ `app/frontend/Pages/**/*.${INERTIA_COMPONENT_EXTENSIONS}`,
5281
+ `app/frontend/pages/**/*.${INERTIA_COMPONENT_EXTENSIONS}`,
5282
+ `app/frontend/entrypoints/**/*.${INERTIA_COMPONENT_EXTENSIONS}`,
5283
+ `app/javascript/Pages/**/*.${INERTIA_COMPONENT_EXTENSIONS}`,
5284
+ `app/javascript/pages/**/*.${INERTIA_COMPONENT_EXTENSIONS}`,
5285
+ `frontend/src/Pages/**/*.${INERTIA_COMPONENT_EXTENSIONS}`,
5286
+ `frontend/src/pages/**/*.${INERTIA_COMPONENT_EXTENSIONS}`,
5287
+ `inertia/Pages/**/*.${INERTIA_COMPONENT_EXTENSIONS}`,
5288
+ `inertia/pages/**/*.${INERTIA_COMPONENT_EXTENSIONS}`,
5289
+ `src/app.${INERTIA_COMPONENT_EXTENSIONS}`,
5290
+ `src/App.${INERTIA_COMPONENT_EXTENSIONS}`,
5291
+ `src/Pages/**/*.${INERTIA_COMPONENT_EXTENSIONS}`,
5292
+ `src/pages/**/*.${INERTIA_COMPONENT_EXTENSIONS}`
5293
+ ],
5294
+ alwaysUsed: []
5295
+ },
5296
+ {
5297
+ enablers: ["@redwoodjs/router", "@redwoodjs/web"],
5298
+ enablerPrefixes: [],
5299
+ entryPatterns: [
5300
+ `web/src/App.${JS_TS_COMPONENT_EXTENSIONS}`,
5301
+ `web/src/Routes.${JS_TS_COMPONENT_EXTENSIONS}`,
5302
+ `web/src/index.${JS_TS_COMPONENT_EXTENSIONS}`,
5303
+ `web/src/layouts/**/*.${JS_TS_COMPONENT_EXTENSIONS}`,
5304
+ `web/src/pages/**/*.${JS_TS_COMPONENT_EXTENSIONS}`
5305
+ ],
5306
+ alwaysUsed: []
5307
+ },
4993
5308
  {
4994
5309
  enablers: ["react-scripts", "react-app-rewired"],
4995
5310
  enablerPrefixes: [],
@@ -5235,6 +5550,48 @@ const FRAMEWORK_PATTERNS = [
5235
5550
  ],
5236
5551
  alwaysUsed: ["tsr.config.json", "app.config.{ts,js}"]
5237
5552
  },
5553
+ {
5554
+ enablers: ["waku"],
5555
+ enablerPrefixes: [],
5556
+ entryPatterns: [
5557
+ `src/pages/**/*.${JS_TS_COMPONENT_EXTENSIONS}`,
5558
+ `src/waku.client.${JS_TS_COMPONENT_EXTENSIONS}`,
5559
+ `src/waku.server.${JS_TS_COMPONENT_EXTENSIONS}`
5560
+ ],
5561
+ alwaysUsed: []
5562
+ },
5563
+ {
5564
+ enablers: ["vike", "vite-plugin-ssr"],
5565
+ enablerPrefixes: [],
5566
+ entryPatterns: [
5567
+ `pages/**/*.${VIKE_ROUTE_EXTENSIONS}`,
5568
+ `renderer/**/*.${JS_TS_COMPONENT_EXTENSIONS}`,
5569
+ `src/pages/**/*.${VIKE_ROUTE_EXTENSIONS}`,
5570
+ `src/renderer/**/*.${JS_TS_COMPONENT_EXTENSIONS}`
5571
+ ],
5572
+ alwaysUsed: []
5573
+ },
5574
+ {
5575
+ enablers: ["rakkasjs"],
5576
+ enablerPrefixes: [],
5577
+ entryPatterns: [
5578
+ `src/client.${JS_TS_COMPONENT_EXTENSIONS}`,
5579
+ `src/server.${JS_TS_COMPONENT_EXTENSIONS}`,
5580
+ `src/routes/**/*.${JS_TS_COMPONENT_EXTENSIONS}`
5581
+ ],
5582
+ alwaysUsed: []
5583
+ },
5584
+ {
5585
+ enablers: [
5586
+ "@module-federation/enhanced",
5587
+ "@module-federation/node",
5588
+ "@module-federation/vite",
5589
+ "@originjs/vite-plugin-federation"
5590
+ ],
5591
+ enablerPrefixes: [],
5592
+ entryPatterns: ["federation.config.{ts,js,mjs,cjs,mts,cts}", "module-federation.config.{ts,js,mjs,cjs,mts,cts}"],
5593
+ alwaysUsed: []
5594
+ },
5238
5595
  {
5239
5596
  enablers: [
5240
5597
  "vite",
@@ -5446,7 +5803,7 @@ const FRAMEWORK_PATTERNS = [
5446
5803
  "app/_layout.{ts,tsx,js,jsx}",
5447
5804
  "app/index.{ts,tsx,js,jsx}"
5448
5805
  ],
5449
- alwaysUsed: ["app.json", "app.config.{ts,js}"]
5806
+ alwaysUsed: ["app.json", "app.config.{ts,mts,cts,js,mjs,cjs}"]
5450
5807
  },
5451
5808
  {
5452
5809
  enablers: ["wrangler"],
@@ -5665,7 +6022,8 @@ const FRAMEWORK_SCRIPT_BINARIES = {
5665
6022
  nuxt: ["nuxt"],
5666
6023
  astro: ["astro"],
5667
6024
  gatsby: ["gatsby"],
5668
- remix: ["remix"],
6025
+ "@remix-run/dev": ["remix"],
6026
+ "@react-router/dev": ["react-router"],
5669
6027
  "@sveltejs/kit": ["svelte-kit", "vite-svelte-kit"],
5670
6028
  "@docusaurus/core": ["docusaurus"],
5671
6029
  "@angular/core": ["ng"],
@@ -5734,7 +6092,7 @@ const discoverToolingEntryPoints = (rootDir, workspacePackages) => {
5734
6092
  const scriptDetectedEnablers = detectFrameworkFromScripts(readPackageScripts(directory));
5735
6093
  const mergedDependencies = { ...workspaceDependencies };
5736
6094
  if (directory === rootDir) Object.assign(mergedDependencies, rootDependencies);
5737
- else for (const enabler of scriptDetectedEnablers) if (enabler in monorepoRootDeps || enabler in rootDependencies) mergedDependencies[enabler] = "*";
6095
+ for (const enabler of scriptDetectedEnablers) if (enabler in workspaceDependencies || enabler in rootDependencies || enabler in monorepoRootDeps) mergedDependencies[enabler] = "*";
5738
6096
  const activatedPatterns = [];
5739
6097
  const activatedAlwaysUsed = [];
5740
6098
  for (const plugin of FRAMEWORK_PATTERNS) if (isToolingPluginEnabled(plugin, mergedDependencies)) {
@@ -5783,6 +6141,40 @@ const discoverToolingEntryPoints = (rootDir, workspacePackages) => {
5783
6141
  };
5784
6142
  };
5785
6143
 
6144
+ //#endregion
6145
+ //#region src/utils/is-platform-builtin-or-virtual.ts
6146
+ const BUILTIN_SUBPATH_NODE_MODULES = new Set([
6147
+ "fs",
6148
+ "dns",
6149
+ "stream",
6150
+ "readline",
6151
+ "timers",
6152
+ "util",
6153
+ "test",
6154
+ "assert",
6155
+ "inspector",
6156
+ "path"
6157
+ ]);
6158
+ /**
6159
+ * True for module specifiers that don't correspond to a real on-disk
6160
+ * package — Node / Bun / Cloudflare / Sass built-ins, the Deno `std`
6161
+ * bare specifier, and Vite `virtual:` modules — so they aren't mistakenly
6162
+ * surfaced as `unused-dependency` or `unresolved-import`.
6163
+ */
6164
+ const isPlatformBuiltinOrVirtualSpecifier = (specifier) => {
6165
+ if (specifier.startsWith("virtual:")) return true;
6166
+ if (specifier === "bun" || specifier.startsWith("bun:")) return true;
6167
+ if (specifier.startsWith("cloudflare:")) return true;
6168
+ if (specifier.startsWith("sass:")) return true;
6169
+ if (specifier === "std" || specifier.startsWith("std/")) return true;
6170
+ const stripped = specifier.startsWith("node:") ? specifier.slice(5) : specifier;
6171
+ const slashIndex = stripped.indexOf("/");
6172
+ if (slashIndex === -1) return BUILTIN_MODULES.has(stripped);
6173
+ const baseName = stripped.slice(0, slashIndex);
6174
+ if (!BUILTIN_MODULES.has(baseName)) return false;
6175
+ return BUILTIN_SUBPATH_NODE_MODULES.has(baseName);
6176
+ };
6177
+
5786
6178
  //#endregion
5787
6179
  //#region src/resolver/resolve.ts
5788
6180
  const fileExistsCache = /* @__PURE__ */ new Map();
@@ -6419,9 +6811,10 @@ const createResolver = (config, workspacePackages = [], options = {}) => {
6419
6811
  try {
6420
6812
  const resolverResult = activeResolver.sync(fromDir, cleanedSpecifier);
6421
6813
  if (resolverResult.path) {
6422
- const isInsideNodeModules = resolverResult.path.includes("/node_modules/");
6814
+ const normalizedResolvedPath = toPosixPath(resolverResult.path);
6815
+ const isInsideNodeModules = normalizedResolvedPath.includes("/node_modules/");
6423
6816
  return {
6424
- resolvedPath: isInsideNodeModules ? void 0 : resolverResult.path,
6817
+ resolvedPath: isInsideNodeModules ? void 0 : normalizedResolvedPath,
6425
6818
  isExternal: isInsideNodeModules,
6426
6819
  packageName: isInsideNodeModules ? extractPackageNameFromSpecifier(cleanedSpecifier) : void 0
6427
6820
  };
@@ -6536,7 +6929,14 @@ const createResolver = (config, workspacePackages = [], options = {}) => {
6536
6929
  resolveResultCache.set(cacheKey, unresolvedResult);
6537
6930
  return unresolvedResult;
6538
6931
  };
6539
- return { resolveModule };
6932
+ const resolveModuleWithPosixPath = (specifier, fromFile) => {
6933
+ const resolved = resolveModule(specifier, fromFile);
6934
+ return resolved.resolvedPath ? {
6935
+ ...resolved,
6936
+ resolvedPath: toPosixPath(resolved.resolvedPath)
6937
+ } : resolved;
6938
+ };
6939
+ return { resolveModule: resolveModuleWithPosixPath };
6540
6940
  };
6541
6941
  const stripJsonComments = (content) => {
6542
6942
  let result = "";
@@ -6577,21 +6977,7 @@ const stripJsonComments = (content) => {
6577
6977
  }
6578
6978
  return result.replace(/,(\s*[}\]])/g, "$1");
6579
6979
  };
6580
- const BUILTIN_SUBPATH_MODULES = new Set([
6581
- "fs",
6582
- "dns",
6583
- "stream",
6584
- "readline",
6585
- "timers",
6586
- "util"
6587
- ]);
6588
- const isBuiltinModule = (specifier) => {
6589
- if (specifier.startsWith("node:")) return true;
6590
- const baseName = specifier.split("/")[0];
6591
- if (!BUILTIN_MODULES.has(baseName)) return false;
6592
- if (!specifier.includes("/")) return true;
6593
- return BUILTIN_SUBPATH_MODULES.has(baseName);
6594
- };
6980
+ const isBuiltinModule = (specifier) => isPlatformBuiltinOrVirtualSpecifier(specifier);
6595
6981
  const isBareSpecifier = (specifier) => !specifier.startsWith(".") && !specifier.startsWith("/");
6596
6982
  const extractPackageNameFromSpecifier = (specifier) => {
6597
6983
  if (specifier.startsWith("node:")) return specifier.slice(5).split("/")[0];
@@ -6616,7 +7002,7 @@ const isConfigFile = (filePath) => {
6616
7002
  //#region src/linker/build.ts
6617
7003
  const buildDependencyGraph = (inputs) => {
6618
7004
  const fileIdMap = /* @__PURE__ */ new Map();
6619
- for (const input of inputs) fileIdMap.set(input.fileId.path, input.fileId.index);
7005
+ for (const input of inputs) fileIdMap.set(toPosixPath(input.fileId.path), input.fileId.index);
6620
7006
  const modules = inputs.map((input) => ({
6621
7007
  fileId: input.fileId,
6622
7008
  imports: input.parsed.imports,
@@ -6662,7 +7048,7 @@ const buildDependencyGraph = (inputs) => {
6662
7048
  const sourceDir = node_path.default.dirname(input.fileId.path);
6663
7049
  const globPattern = importInfo.specifier;
6664
7050
  for (const [filePath] of fileIdMap) {
6665
- const relativePath = node_path.default.relative(sourceDir, filePath);
7051
+ const relativePath = toPosixPath(node_path.default.relative(sourceDir, filePath));
6666
7052
  if ((0, minimatch.minimatch)(relativePath.startsWith(".") ? relativePath : `./${relativePath}`, globPattern)) {
6667
7053
  const targetIndex = fileIdMap.get(filePath);
6668
7054
  if (targetIndex !== void 0) addEdge(sourceIndex, targetIndex, []);
@@ -6672,7 +7058,7 @@ const buildDependencyGraph = (inputs) => {
6672
7058
  }
6673
7059
  const resolved = input.resolvedImports.get(importInfo.specifier);
6674
7060
  if (!resolved?.resolvedPath) continue;
6675
- const targetIndex = fileIdMap.get(resolved.resolvedPath);
7061
+ const targetIndex = fileIdMap.get(toPosixPath(resolved.resolvedPath));
6676
7062
  if (targetIndex === void 0) continue;
6677
7063
  addEdge(sourceIndex, targetIndex, importInfo.importedNames.map((importedName) => ({
6678
7064
  importedName: importedName.name,
@@ -6687,7 +7073,7 @@ const buildDependencyGraph = (inputs) => {
6687
7073
  if (!exportInfo.isReExport || !exportInfo.reExportSource) continue;
6688
7074
  const resolved = input.resolvedImports.get(exportInfo.reExportSource);
6689
7075
  if (!resolved?.resolvedPath) continue;
6690
- const targetIndex = fileIdMap.get(resolved.resolvedPath);
7076
+ const targetIndex = fileIdMap.get(toPosixPath(resolved.resolvedPath));
6691
7077
  if (targetIndex === void 0) continue;
6692
7078
  const exportedName = exportInfo.isNamespaceReExport ? "*" : exportInfo.name;
6693
7079
  const originalName = exportInfo.isNamespaceReExport ? "*" : exportInfo.reExportOriginalName ?? exportInfo.name;
@@ -8476,61 +8862,2413 @@ const detectDuplicateInlineTypes = (graph) => {
8476
8862
  };
8477
8863
 
8478
8864
  //#endregion
8479
- //#region src/utils/run-safe-detector.ts
8480
- const runSafeDetector = (input) => {
8481
- try {
8482
- return input.detector();
8483
- } catch (caughtError) {
8484
- input.errorSink.push(new DetectorError({
8485
- module: input.module,
8486
- message: `${input.detectorName} threw ${input.contextDescription}`,
8487
- detail: describeUnknownError(caughtError)
8865
+ //#region src/report/cross-file-duplicate-exports.ts
8866
+ const buildReExportSourceSets = (graph) => {
8867
+ const reExportSources = /* @__PURE__ */ new Map();
8868
+ for (const edge of graph.edges) {
8869
+ if (!edge.isReExportEdge) continue;
8870
+ const existing = reExportSources.get(edge.source);
8871
+ if (existing) existing.add(edge.target);
8872
+ else reExportSources.set(edge.source, new Set([edge.target]));
8873
+ }
8874
+ return reExportSources;
8875
+ };
8876
+ /**
8877
+ * Two duplicate-export files "share a common importer" when there exists a
8878
+ * third file that imports from both, OR one duplicate file imports another.
8879
+ * This filters out coincidental duplicates among unrelated leaf modules
8880
+ * (SvelteKit/Next.js route files, scripts in different parts of a monorepo,
8881
+ * etc.) that happen to export the same name but can never be confused at any
8882
+ * import site.
8883
+ */
8884
+ const hasCommonImporter = (moduleIndices, graph) => {
8885
+ if (moduleIndices.length <= 1) return false;
8886
+ const duplicateModuleSet = new Set(moduleIndices);
8887
+ const importerOwner = /* @__PURE__ */ new Map();
8888
+ for (const moduleIndex of moduleIndices) {
8889
+ const importers = graph.reverseEdges.get(moduleIndex) ?? [];
8890
+ for (const importerIndex of importers) {
8891
+ if (duplicateModuleSet.has(importerIndex)) return true;
8892
+ const previousOwner = importerOwner.get(importerIndex);
8893
+ if (previousOwner === void 0) importerOwner.set(importerIndex, moduleIndex);
8894
+ else if (previousOwner !== moduleIndex) return true;
8895
+ }
8896
+ }
8897
+ return false;
8898
+ };
8899
+ /**
8900
+ * Cross-file duplicate exports: the same exported name lives in 2+ files.
8901
+ *
8902
+ * Filters applied (to keep the rule actionable):
8903
+ * - default exports are skipped (every module gets one and it's not actionable)
8904
+ * - re-export chains are pruned: if module A re-exports `Foo` from module B,
8905
+ * the (A, B) pair is one chain, not two real declarations
8906
+ * - TypeScript value/type namespace split: `export const X` and `export type X`
8907
+ * in the same file are distinct in TS's value/type namespaces; same name in a
8908
+ * value file and a type file is not a true duplicate either
8909
+ * - common-importer filter: only report duplicates where two of the duplicate
8910
+ * files share an importer or one imports another, so unrelated route files in
8911
+ * different parts of a repo don't get flagged
8912
+ */
8913
+ const detectCrossFileDuplicateExports = (graph) => {
8914
+ const reExportSources = buildReExportSourceSets(graph);
8915
+ const exportEntriesByName = /* @__PURE__ */ new Map();
8916
+ for (const module of graph.modules) {
8917
+ if (!module.isReachable) continue;
8918
+ if (module.isDeclarationFile) continue;
8919
+ if (module.isEntryPoint) continue;
8920
+ for (const exportInfo of module.exports) {
8921
+ if (exportInfo.isDefault) continue;
8922
+ if (exportInfo.isSynthetic) continue;
8923
+ if (exportInfo.name === "*") continue;
8924
+ if (exportInfo.isReExport) continue;
8925
+ const entry = {
8926
+ moduleIndex: module.fileId.index,
8927
+ path: module.fileId.path,
8928
+ line: exportInfo.line,
8929
+ column: exportInfo.column,
8930
+ isTypeOnly: exportInfo.isTypeOnly
8931
+ };
8932
+ const existing = exportEntriesByName.get(exportInfo.name);
8933
+ if (existing) existing.push(entry);
8934
+ else exportEntriesByName.set(exportInfo.name, [entry]);
8935
+ }
8936
+ }
8937
+ const findings = [];
8938
+ const sortedEntries = [...exportEntriesByName.entries()].sort(([nameA], [nameB]) => nameA.localeCompare(nameB));
8939
+ for (const [name, entries] of sortedEntries) {
8940
+ if (entries.length <= 1) continue;
8941
+ const hasValueExport = entries.some((entry) => !entry.isTypeOnly);
8942
+ const hasTypeExport = entries.some((entry) => entry.isTypeOnly);
8943
+ if (hasValueExport && hasTypeExport) {
8944
+ const valueModuleIndices = new Set(entries.filter((entry) => !entry.isTypeOnly).map((entry) => entry.moduleIndex));
8945
+ const typeModuleIndices = new Set(entries.filter((entry) => entry.isTypeOnly).map((entry) => entry.moduleIndex));
8946
+ if (valueModuleIndices.size <= 1 && typeModuleIndices.size <= 1) continue;
8947
+ }
8948
+ const moduleIndexSet = new Set(entries.map((entry) => entry.moduleIndex));
8949
+ const independentEntries = entries.filter((entry) => {
8950
+ const sources = reExportSources.get(entry.moduleIndex);
8951
+ if (!sources) return true;
8952
+ for (const sourceIndex of sources) if (moduleIndexSet.has(sourceIndex)) return false;
8953
+ return true;
8954
+ });
8955
+ if (independentEntries.length <= 1) continue;
8956
+ if (!hasCommonImporter(independentEntries.map((entry) => entry.moduleIndex), graph)) continue;
8957
+ const locations = independentEntries.map((entry) => ({
8958
+ path: entry.path,
8959
+ line: entry.line,
8960
+ column: entry.column,
8961
+ isTypeOnly: entry.isTypeOnly
8488
8962
  }));
8489
- return input.fallback;
8963
+ findings.push({
8964
+ name,
8965
+ locations,
8966
+ confidence: "medium",
8967
+ reason: `"${name}" is exported from ${locations.length} files that share a common importer — consumers may import the wrong one`
8968
+ });
8490
8969
  }
8970
+ return findings;
8491
8971
  };
8492
8972
 
8493
8973
  //#endregion
8494
- //#region src/semantic/program.ts
8495
- const failureFor = (reason, message, options = { rootDir: "" }) => {
8974
+ //#region src/utils/compute-line-starts.ts
8975
+ const LINE_FEED_CHAR_CODE = 10;
8976
+ const computeLineStarts = (sourceText) => {
8977
+ const lineStarts = [0];
8978
+ for (let charIndex = 0; charIndex < sourceText.length; charIndex++) if (sourceText.charCodeAt(charIndex) === LINE_FEED_CHAR_CODE) lineStarts.push(charIndex + 1);
8979
+ return lineStarts;
8980
+ };
8981
+
8982
+ //#endregion
8983
+ //#region src/utils/offset-to-line-column.ts
8984
+ const offsetToLineColumn = (byteOffset, lineStarts) => {
8985
+ let lowIndex = 0;
8986
+ let highIndex = lineStarts.length - 1;
8987
+ while (lowIndex < highIndex) {
8988
+ const middleIndex = lowIndex + highIndex + 1 >>> 1;
8989
+ if (lineStarts[middleIndex] <= byteOffset) lowIndex = middleIndex;
8990
+ else highIndex = middleIndex - 1;
8991
+ }
8496
8992
  return {
8497
- reason,
8498
- message,
8499
- error: new TypeScriptError({
8500
- code: {
8501
- "no-tsconfig": "tsconfig-not-found",
8502
- "tsconfig-parse-error": "tsconfig-parse-failed",
8503
- "program-creation-failed": "ts-program-creation-failed",
8504
- "too-many-files": "ts-program-too-large",
8505
- "typescript-load-failed": "ts-not-loadable"
8506
- }[reason],
8507
- severity: reason === "no-tsconfig" ? "info" : "warning",
8508
- message,
8509
- path: options.rootDir || void 0,
8510
- detail: options.detail
8511
- })
8993
+ line: lowIndex + 1,
8994
+ column: byteOffset - lineStarts[lowIndex]
8512
8995
  };
8513
8996
  };
8514
- const findNearestTsconfig = (rootDir, explicitPath) => {
8515
- if (explicitPath) {
8516
- const absoluteExplicit = (0, node_path.resolve)(rootDir, explicitPath);
8517
- if ((0, node_fs.existsSync)(absoluteExplicit)) return absoluteExplicit;
8518
- return;
8519
- }
8520
- for (const candidateName of DEFAULT_SEMANTIC_TSCONFIG_NAMES) {
8521
- const candidatePath = (0, node_path.resolve)(rootDir, candidateName);
8522
- if ((0, node_fs.existsSync)(candidatePath)) return candidatePath;
8997
+
8998
+ //#endregion
8999
+ //#region src/duplicate-blocks/concatenate.ts
9000
+ const SENTINEL_FILE_INDEX = Number.MAX_SAFE_INTEGER;
9001
+ /**
9002
+ * Rank-reduce token hashes to dense 0..K-1 integers and concatenate every
9003
+ * file's reduced sequence with a unique negative sentinel between files. Dense
9004
+ * ranks shrink the suffix-array's bucket counters from ~4 billion to a few
9005
+ * thousand (the standard prefix-doubling speedup), and negative sentinels
9006
+ * guarantee no real-token suffix can match across a file boundary.
9007
+ */
9008
+ const rankReduceAndConcatenate = (filesHashedTokens) => {
9009
+ const uniqueHashes = /* @__PURE__ */ new Set();
9010
+ for (const fileTokens of filesHashedTokens) for (const hashedToken of fileTokens) uniqueHashes.add(hashedToken.hash);
9011
+ const sortedUniqueHashes = [...uniqueHashes].sort((leftHash, rightHash) => leftHash - rightHash);
9012
+ const hashToRank = /* @__PURE__ */ new Map();
9013
+ for (let rankIndex = 0; rankIndex < sortedUniqueHashes.length; rankIndex++) hashToRank.set(sortedUniqueHashes[rankIndex], rankIndex + 1);
9014
+ const sequenceLength = filesHashedTokens.reduce((runningSum, fileTokens) => runningSum + fileTokens.length, 0) + Math.max(0, filesHashedTokens.length - 1);
9015
+ const tokenSequence = new Array(sequenceLength);
9016
+ const fileOf = new Array(sequenceLength);
9017
+ const fileOffsets = new Array(filesHashedTokens.length);
9018
+ let writeCursor = 0;
9019
+ let nextSentinelValue = -1;
9020
+ for (let fileIndex = 0; fileIndex < filesHashedTokens.length; fileIndex++) {
9021
+ fileOffsets[fileIndex] = writeCursor;
9022
+ const fileTokens = filesHashedTokens[fileIndex];
9023
+ for (const hashedToken of fileTokens) {
9024
+ tokenSequence[writeCursor] = hashToRank.get(hashedToken.hash) ?? 0;
9025
+ fileOf[writeCursor] = fileIndex;
9026
+ writeCursor++;
9027
+ }
9028
+ if (fileIndex < filesHashedTokens.length - 1) {
9029
+ tokenSequence[writeCursor] = nextSentinelValue;
9030
+ fileOf[writeCursor] = SENTINEL_FILE_INDEX;
9031
+ writeCursor++;
9032
+ nextSentinelValue--;
9033
+ }
8523
9034
  }
9035
+ return {
9036
+ tokenSequence,
9037
+ fileOf,
9038
+ fileOffsets
9039
+ };
8524
9040
  };
8525
- const createSemanticContext = (rootDir, tsconfigPath) => {
8526
- const resolvedTsconfigPath = findNearestTsconfig(rootDir, tsconfigPath);
8527
- if (!resolvedTsconfigPath) return {
8528
- ok: false,
8529
- failure: failureFor("no-tsconfig", `No tsconfig found under ${rootDir}`, { rootDir })
9041
+ const SENTINEL_FILE_MARKER = SENTINEL_FILE_INDEX;
9042
+
9043
+ //#endregion
9044
+ //#region src/duplicate-blocks/extract.ts
9045
+ const buildRawBlock = (suffixArray, fileOf, fileOffsets, filesTokenCounts, intervalBegin, intervalEnd, tokenLength) => {
9046
+ const candidateInstances = [];
9047
+ for (let suffixIndex = intervalBegin; suffixIndex < intervalEnd; suffixIndex++) {
9048
+ const startPosition = suffixArray[suffixIndex];
9049
+ const fileIndex = fileOf[startPosition];
9050
+ if (fileIndex === SENTINEL_FILE_MARKER) continue;
9051
+ const tokenOffsetWithinFile = startPosition - fileOffsets[fileIndex];
9052
+ if (tokenOffsetWithinFile + tokenLength > filesTokenCounts[fileIndex]) continue;
9053
+ candidateInstances.push({
9054
+ fileIndex,
9055
+ tokenOffsetWithinFile
9056
+ });
9057
+ }
9058
+ if (candidateInstances.length < 2) return void 0;
9059
+ candidateInstances.sort((leftInstance, rightInstance) => {
9060
+ if (leftInstance.fileIndex !== rightInstance.fileIndex) return leftInstance.fileIndex - rightInstance.fileIndex;
9061
+ return leftInstance.tokenOffsetWithinFile - rightInstance.tokenOffsetWithinFile;
9062
+ });
9063
+ const dedupedInstances = [];
9064
+ for (const instance of candidateInstances) {
9065
+ const lastInstance = dedupedInstances[dedupedInstances.length - 1];
9066
+ if (lastInstance !== void 0 && lastInstance.fileIndex === instance.fileIndex && instance.tokenOffsetWithinFile < lastInstance.tokenOffsetWithinFile + tokenLength) continue;
9067
+ dedupedInstances.push(instance);
9068
+ }
9069
+ if (dedupedInstances.length < 2) return void 0;
9070
+ return {
9071
+ instances: dedupedInstances,
9072
+ tokenLength
8530
9073
  };
8531
- let configFileContent;
8532
- try {
8533
- configFileContent = typescript.default.readConfigFile(resolvedTsconfigPath, typescript.default.sys.readFile);
9074
+ };
9075
+ /**
9076
+ * Walks `lcpArray` with a monotone stack to materialize every maximal
9077
+ * interval `[i, j]` whose minimum LCP is >= `minTokens`. Within-file
9078
+ * overlapping occurrences are dropped (keep the earliest non-overlapping
9079
+ * prefix), and any block left with fewer than two occurrences is discarded.
9080
+ */
9081
+ const extractRawDuplicateBlocks = (suffixArray, lcpArray, fileOf, fileOffsets, filesTokenCounts, minTokens) => {
9082
+ const sequenceLength = suffixArray.length;
9083
+ if (sequenceLength < 2) return [];
9084
+ const rawBlocks = [];
9085
+ const monotoneStack = [];
9086
+ for (let scanIndex = 1; scanIndex <= sequenceLength; scanIndex++) {
9087
+ const currentLcp = scanIndex < sequenceLength ? lcpArray[scanIndex] : 0;
9088
+ let intervalStart = scanIndex;
9089
+ while (monotoneStack.length > 0 && monotoneStack[monotoneStack.length - 1].lcpValue > currentLcp) {
9090
+ const popped = monotoneStack.pop();
9091
+ intervalStart = popped.startIndex;
9092
+ if (popped.lcpValue >= minTokens) {
9093
+ const candidate = buildRawBlock(suffixArray, fileOf, fileOffsets, filesTokenCounts, intervalStart - 1, scanIndex, popped.lcpValue);
9094
+ if (candidate) rawBlocks.push(candidate);
9095
+ }
9096
+ }
9097
+ if (scanIndex < sequenceLength) monotoneStack.push({
9098
+ lcpValue: currentLcp,
9099
+ startIndex: intervalStart
9100
+ });
9101
+ }
9102
+ return rawBlocks;
9103
+ };
9104
+
9105
+ //#endregion
9106
+ //#region src/duplicate-blocks/clusters.ts
9107
+ const baseName = (filePath) => {
9108
+ const trailingSlashIndex = filePath.lastIndexOf("/");
9109
+ return trailingSlashIndex === -1 ? filePath : filePath.slice(trailingSlashIndex + 1);
9110
+ };
9111
+ const buildSuggestions = (files, blocks, totalDuplicatedLines) => {
9112
+ const fileBaseNames = files.map((filePath) => baseName(filePath));
9113
+ if (files.length >= 2 && totalDuplicatedLines >= 50) {
9114
+ const estimatedSavings = blocks.reduce((runningSum, block) => runningSum + block.lineCount * Math.max(0, block.instances.length - 1), 0);
9115
+ return [{
9116
+ kind: "extract-module",
9117
+ description: `Extract ${blocks.length} shared duplicate block${blocks.length === 1 ? "" : "s"} (${totalDuplicatedLines} lines) from ${fileBaseNames.join(", ")} into a shared module`,
9118
+ estimatedSavings
9119
+ }];
9120
+ }
9121
+ return blocks.map((block) => ({
9122
+ kind: "extract-function",
9123
+ description: `Extract shared function (${block.lineCount} lines) from ${fileBaseNames.join(", ")}`,
9124
+ estimatedSavings: block.lineCount * Math.max(0, block.instances.length - 1)
9125
+ }));
9126
+ };
9127
+ const groupDuplicateBlocksIntoClusters = (duplicateBlocks) => {
9128
+ if (duplicateBlocks.length === 0) return [];
9129
+ const fileSetKeyToBucket = /* @__PURE__ */ new Map();
9130
+ for (const block of duplicateBlocks) {
9131
+ const sortedFiles = [...new Set(block.instances.map((instance) => instance.path))].sort();
9132
+ const fileSetKey = sortedFiles.join("|");
9133
+ const existing = fileSetKeyToBucket.get(fileSetKey);
9134
+ if (existing) existing.blocks.push(block);
9135
+ else fileSetKeyToBucket.set(fileSetKey, {
9136
+ files: sortedFiles,
9137
+ blocks: [block]
9138
+ });
9139
+ }
9140
+ const clusters = [];
9141
+ for (const bucket of fileSetKeyToBucket.values()) {
9142
+ const totalDuplicatedLines = bucket.blocks.reduce((runningSum, block) => runningSum + block.lineCount, 0);
9143
+ const totalDuplicatedTokens = bucket.blocks.reduce((runningSum, block) => runningSum + block.tokenCount, 0);
9144
+ clusters.push({
9145
+ files: bucket.files,
9146
+ groups: bucket.blocks,
9147
+ totalDuplicatedLines,
9148
+ totalDuplicatedTokens,
9149
+ suggestions: buildSuggestions(bucket.files, bucket.blocks, totalDuplicatedLines)
9150
+ });
9151
+ }
9152
+ clusters.sort((leftCluster, rightCluster) => {
9153
+ if (leftCluster.totalDuplicatedLines !== rightCluster.totalDuplicatedLines) return rightCluster.totalDuplicatedLines - leftCluster.totalDuplicatedLines;
9154
+ return rightCluster.groups.length - leftCluster.groups.length;
9155
+ });
9156
+ return clusters;
9157
+ };
9158
+
9159
+ //#endregion
9160
+ //#region src/duplicate-blocks/shadowed-directory-pairs.ts
9161
+ const splitDirectoryAndFile = (filePath) => {
9162
+ const trailingSlashIndex = filePath.lastIndexOf("/");
9163
+ if (trailingSlashIndex === -1) return {
9164
+ directory: "",
9165
+ baseName: filePath
9166
+ };
9167
+ return {
9168
+ directory: filePath.slice(0, trailingSlashIndex + 1),
9169
+ baseName: filePath.slice(trailingSlashIndex + 1)
9170
+ };
9171
+ };
9172
+ const toRelative = (filePath, rootDir) => {
9173
+ if (filePath.startsWith(rootDir + "/")) return filePath.slice(rootDir.length + 1);
9174
+ if (filePath === rootDir) return "";
9175
+ return filePath;
9176
+ };
9177
+ /**
9178
+ * Collapse N two-file duplicate-block clusters that share the same
9179
+ * `(directoryA, directoryB)` and matching basenames into a single
9180
+ * `ShadowedDirectoryPair` finding — the directories themselves drifted
9181
+ * (e.g. `src/` vs `deno/lib/`, a fork, a copy-paste of a route tree).
9182
+ */
9183
+ const detectShadowedDirectoryPairs = (duplicateBlockClusters, rootDir) => {
9184
+ const directoryPairBuckets = /* @__PURE__ */ new Map();
9185
+ for (const cluster of duplicateBlockClusters) {
9186
+ if (cluster.files.length !== 2) continue;
9187
+ const [firstFile, secondFile] = cluster.files;
9188
+ const firstSplit = splitDirectoryAndFile(toRelative(firstFile, rootDir));
9189
+ const secondSplit = splitDirectoryAndFile(toRelative(secondFile, rootDir));
9190
+ if (firstSplit.baseName !== secondSplit.baseName) continue;
9191
+ const [smallerDirectory, largerDirectory] = firstSplit.directory <= secondSplit.directory ? [firstSplit.directory, secondSplit.directory] : [secondSplit.directory, firstSplit.directory];
9192
+ const pairKey = `${smallerDirectory}::${largerDirectory}`;
9193
+ const entry = {
9194
+ baseName: firstSplit.baseName,
9195
+ duplicatedLines: cluster.totalDuplicatedLines
9196
+ };
9197
+ const existing = directoryPairBuckets.get(pairKey);
9198
+ if (existing) existing.push(entry);
9199
+ else directoryPairBuckets.set(pairKey, [entry]);
9200
+ }
9201
+ const shadowedDirectoryPairs = [];
9202
+ for (const [pairKey, entries] of directoryPairBuckets) {
9203
+ if (entries.length < 3) continue;
9204
+ const [directoryA, directoryB] = pairKey.split("::");
9205
+ const sharedBaseNames = [...new Set(entries.map((entry) => entry.baseName))].sort();
9206
+ const totalDuplicatedLines = entries.reduce((runningSum, entry) => runningSum + entry.duplicatedLines, 0);
9207
+ shadowedDirectoryPairs.push({
9208
+ directoryA,
9209
+ directoryB,
9210
+ sharedFiles: sharedBaseNames,
9211
+ totalDuplicatedLines
9212
+ });
9213
+ }
9214
+ shadowedDirectoryPairs.sort((leftPair, rightPair) => rightPair.totalDuplicatedLines - leftPair.totalDuplicatedLines);
9215
+ return shadowedDirectoryPairs;
9216
+ };
9217
+
9218
+ //#endregion
9219
+ //#region src/duplicate-blocks/normalize.ts
9220
+ /**
9221
+ * 32-bit FNV-1a. Collisions are tolerable: ties are broken back to the
9222
+ * original (path, offset) tuples downstream, so a rare collision inflates a
9223
+ * duplicate block with one extra spurious instance at worst.
9224
+ */
9225
+ const FNV_OFFSET_BASIS = 2166136261;
9226
+ const FNV_PRIME = 16777619;
9227
+ const hashString = (input) => {
9228
+ let hash = FNV_OFFSET_BASIS;
9229
+ for (let charIndex = 0; charIndex < input.length; charIndex++) {
9230
+ hash ^= input.charCodeAt(charIndex);
9231
+ hash = Math.imul(hash, FNV_PRIME);
9232
+ }
9233
+ return hash >>> 0;
9234
+ };
9235
+ const resolveNormalization = (mode) => {
9236
+ if (mode === "strict") return {
9237
+ ignoreIdentifiers: false,
9238
+ ignoreStringValues: false,
9239
+ ignoreNumericValues: false
9240
+ };
9241
+ return {
9242
+ ignoreIdentifiers: true,
9243
+ ignoreStringValues: true,
9244
+ ignoreNumericValues: true
9245
+ };
9246
+ };
9247
+ const hashSourceToken = (sourceToken, normalization) => {
9248
+ switch (sourceToken.kind) {
9249
+ case "node-enter": return hashString(`n:${sourceToken.payload}`);
9250
+ case "identifier": return normalization.ignoreIdentifiers ? hashString("id:*") : hashString(`id:${sourceToken.payload}`);
9251
+ case "string-literal": return normalization.ignoreStringValues ? hashString("s:*") : hashString(`s:${sourceToken.payload}`);
9252
+ case "numeric-literal": return normalization.ignoreNumericValues ? hashString("num:*") : hashString(`num:${sourceToken.payload}`);
9253
+ case "boolean-literal": return hashString(`b:${sourceToken.payload}`);
9254
+ case "null-literal": return hashString("null");
9255
+ case "template-literal": return hashString("tpl");
9256
+ case "regexp-literal": return hashString("re");
9257
+ default: return hashString("?");
9258
+ }
9259
+ };
9260
+ const normalizeAndHashTokens = (tokens, mode) => {
9261
+ const normalization = resolveNormalization(mode);
9262
+ const hashedTokens = new Array(tokens.length);
9263
+ for (let tokenIndex = 0; tokenIndex < tokens.length; tokenIndex++) hashedTokens[tokenIndex] = {
9264
+ hash: hashSourceToken(tokens[tokenIndex], normalization),
9265
+ originalIndex: tokenIndex
9266
+ };
9267
+ return hashedTokens;
9268
+ };
9269
+
9270
+ //#endregion
9271
+ //#region src/duplicate-blocks/suffix-array.ts
9272
+ /**
9273
+ * Prefix-doubling suffix array with two-pass radix sort, O(N log N).
9274
+ *
9275
+ * Negative values in `tokenSequence` (file-separator sentinels emitted by
9276
+ * `rankReduceAndConcatenate`) are shifted up so all ranks are >= 0. The
9277
+ * shift preserves the property that sentinels sort before all real ranks,
9278
+ * which is what stops cross-file suffix matches.
9279
+ */
9280
+ const buildSuffixArray = (tokenSequence) => {
9281
+ const sequenceLength = tokenSequence.length;
9282
+ if (sequenceLength === 0) return [];
9283
+ let minimumValue = 0;
9284
+ for (let scanIndex = 0; scanIndex < sequenceLength; scanIndex++) if (tokenSequence[scanIndex] < minimumValue) minimumValue = tokenSequence[scanIndex];
9285
+ let currentRanks = new Array(sequenceLength);
9286
+ for (let scanIndex = 0; scanIndex < sequenceLength; scanIndex++) currentRanks[scanIndex] = tokenSequence[scanIndex] - minimumValue;
9287
+ let suffixArray = new Array(sequenceLength);
9288
+ for (let positionIndex = 0; positionIndex < sequenceLength; positionIndex++) suffixArray[positionIndex] = positionIndex;
9289
+ let nextRanks = new Array(sequenceLength);
9290
+ let scratchSuffixArray = new Array(sequenceLength);
9291
+ let maximumRank = 0;
9292
+ for (let scanIndex = 0; scanIndex < sequenceLength; scanIndex++) if (currentRanks[scanIndex] > maximumRank) maximumRank = currentRanks[scanIndex];
9293
+ let stride = 1;
9294
+ while (stride < sequenceLength) {
9295
+ const bucketCount = maximumRank + 2;
9296
+ const buckets = new Array(bucketCount + 1).fill(0);
9297
+ for (let suffixIndex = 0; suffixIndex < sequenceLength; suffixIndex++) {
9298
+ const startPosition = suffixArray[suffixIndex];
9299
+ const secondaryKey = startPosition + stride < sequenceLength ? currentRanks[startPosition + stride] + 1 : 0;
9300
+ buckets[secondaryKey]++;
9301
+ }
9302
+ let prefixSum = 0;
9303
+ for (let bucketIndex = 0; bucketIndex < buckets.length; bucketIndex++) {
9304
+ const bucketCountValue = buckets[bucketIndex];
9305
+ buckets[bucketIndex] = prefixSum;
9306
+ prefixSum += bucketCountValue;
9307
+ }
9308
+ for (let suffixIndex = 0; suffixIndex < sequenceLength; suffixIndex++) {
9309
+ const startPosition = suffixArray[suffixIndex];
9310
+ const secondaryKey = startPosition + stride < sequenceLength ? currentRanks[startPosition + stride] + 1 : 0;
9311
+ scratchSuffixArray[buckets[secondaryKey]] = startPosition;
9312
+ buckets[secondaryKey]++;
9313
+ }
9314
+ for (let bucketIndex = 0; bucketIndex < buckets.length; bucketIndex++) buckets[bucketIndex] = 0;
9315
+ for (let suffixIndex = 0; suffixIndex < sequenceLength; suffixIndex++) {
9316
+ const startPosition = scratchSuffixArray[suffixIndex];
9317
+ buckets[currentRanks[startPosition]]++;
9318
+ }
9319
+ prefixSum = 0;
9320
+ for (let bucketIndex = 0; bucketIndex < buckets.length; bucketIndex++) {
9321
+ const bucketCountValue = buckets[bucketIndex];
9322
+ buckets[bucketIndex] = prefixSum;
9323
+ prefixSum += bucketCountValue;
9324
+ }
9325
+ for (let suffixIndex = 0; suffixIndex < sequenceLength; suffixIndex++) {
9326
+ const startPosition = scratchSuffixArray[suffixIndex];
9327
+ suffixArray[buckets[currentRanks[startPosition]]] = startPosition;
9328
+ buckets[currentRanks[startPosition]]++;
9329
+ }
9330
+ nextRanks[suffixArray[0]] = 0;
9331
+ for (let suffixIndex = 1; suffixIndex < sequenceLength; suffixIndex++) {
9332
+ const previousStart = suffixArray[suffixIndex - 1];
9333
+ const currentStart = suffixArray[suffixIndex];
9334
+ const previousSecondary = previousStart + stride < sequenceLength ? currentRanks[previousStart + stride] : -1;
9335
+ const currentSecondary = currentStart + stride < sequenceLength ? currentRanks[currentStart + stride] : -1;
9336
+ const isSameBucket = currentRanks[previousStart] === currentRanks[currentStart] && previousSecondary === currentSecondary;
9337
+ nextRanks[currentStart] = nextRanks[previousStart] + (isSameBucket ? 0 : 1);
9338
+ }
9339
+ const newMaximumRank = nextRanks[suffixArray[sequenceLength - 1]];
9340
+ [currentRanks, nextRanks] = [nextRanks, currentRanks];
9341
+ if (newMaximumRank === sequenceLength - 1) break;
9342
+ maximumRank = newMaximumRank;
9343
+ stride *= 2;
9344
+ }
9345
+ return suffixArray;
9346
+ };
9347
+ /**
9348
+ * Kasai's O(N) longest-common-prefix array. The `>= 0` check inside the inner
9349
+ * loop is the only non-textbook bit: it prevents a real-token LCP from
9350
+ * accidentally crossing a sentinel boundary (sentinels are negative).
9351
+ */
9352
+ const buildLcpArray = (tokenSequence, suffixArray) => {
9353
+ const sequenceLength = tokenSequence.length;
9354
+ const inverseSuffixArray = new Array(sequenceLength);
9355
+ for (let arrayIndex = 0; arrayIndex < sequenceLength; arrayIndex++) inverseSuffixArray[suffixArray[arrayIndex]] = arrayIndex;
9356
+ const lcpArray = new Array(sequenceLength).fill(0);
9357
+ let runningLcp = 0;
9358
+ for (let positionIndex = 0; positionIndex < sequenceLength; positionIndex++) {
9359
+ if (inverseSuffixArray[positionIndex] === 0) {
9360
+ runningLcp = 0;
9361
+ continue;
9362
+ }
9363
+ const previousStart = suffixArray[inverseSuffixArray[positionIndex] - 1];
9364
+ while (positionIndex + runningLcp < sequenceLength && previousStart + runningLcp < sequenceLength && tokenSequence[positionIndex + runningLcp] === tokenSequence[previousStart + runningLcp] && tokenSequence[positionIndex + runningLcp] >= 0) runningLcp++;
9365
+ lcpArray[inverseSuffixArray[positionIndex]] = runningLcp;
9366
+ if (runningLcp > 0) runningLcp--;
9367
+ }
9368
+ return lcpArray;
9369
+ };
9370
+
9371
+ //#endregion
9372
+ //#region src/utils/is-ast-node.ts
9373
+ const isAstNode = (candidate) => typeof candidate === "object" && candidate !== null && "type" in candidate;
9374
+
9375
+ //#endregion
9376
+ //#region src/duplicate-blocks/token-visitor.ts
9377
+ const NODES_DROPPED_FROM_TOKEN_STREAM = new Set([
9378
+ "ImportDeclaration",
9379
+ "ExportAllDeclaration",
9380
+ "TSTypeAnnotation",
9381
+ "TSTypeAliasDeclaration",
9382
+ "TSInterfaceDeclaration",
9383
+ "TSTypeParameterDeclaration",
9384
+ "TSTypeParameterInstantiation",
9385
+ "TSTypeReference",
9386
+ "TSAnyKeyword",
9387
+ "TSUnknownKeyword",
9388
+ "TSStringKeyword",
9389
+ "TSNumberKeyword",
9390
+ "TSBooleanKeyword",
9391
+ "TSVoidKeyword",
9392
+ "TSUndefinedKeyword",
9393
+ "TSNullKeyword",
9394
+ "TSNeverKeyword",
9395
+ "TSUnionType",
9396
+ "TSIntersectionType",
9397
+ "TSLiteralType",
9398
+ "TSArrayType",
9399
+ "TSTupleType",
9400
+ "TSTypeLiteral",
9401
+ "TSPropertySignature",
9402
+ "TSMethodSignature",
9403
+ "TSCallSignatureDeclaration",
9404
+ "TSConstructSignatureDeclaration",
9405
+ "TSIndexSignature",
9406
+ "TSConditionalType",
9407
+ "TSMappedType",
9408
+ "TSInferType",
9409
+ "TSImportType",
9410
+ "TSQualifiedName",
9411
+ "TSTypeOperator",
9412
+ "TSTypePredicate",
9413
+ "TSFunctionType",
9414
+ "TSConstructorType"
9415
+ ]);
9416
+ const visitChildrenRaw = (node, visit) => {
9417
+ if (!isAstNode(node)) return;
9418
+ for (const key of Object.keys(node)) {
9419
+ if (key === "type" || key === "start" || key === "end" || key === "loc" || key === "range") continue;
9420
+ const value = node[key];
9421
+ if (Array.isArray(value)) for (const item of value) visit(item);
9422
+ else if (value !== null && typeof value === "object") visit(value);
9423
+ }
9424
+ };
9425
+ const safeNumberOrZero = (candidate) => typeof candidate === "number" ? candidate : 0;
9426
+ /**
9427
+ * Walk an oxc AST and emit a flat token stream suitable for suffix-array-based
9428
+ * duplicate-block detection. Two structurally-identical regions of code produce the same
9429
+ * token sequence (modulo identifier/literal-value normalization, applied later
9430
+ * in `normalize.ts`).
9431
+ *
9432
+ * Implementation note: rather than a hand-written keyword/operator lexer-style
9433
+ * visitor, we walk the AST generically and emit one `node-enter` token per
9434
+ * visited node. This trades a slightly different token-density profile for
9435
+ * less code. AST-shape tokens still distinguish
9436
+ * `function add(a, b) { return a + b }` from `const add = (a, b) => a + b`.
9437
+ * Identifiers and value literals get dedicated tokens so semantic-mode
9438
+ * normalization can blind them.
9439
+ *
9440
+ * Imports and type-only constructs are dropped to keep import-block boilerplate
9441
+ * and ambient type declarations from inflating the noise floor.
9442
+ */
9443
+ const tokenizeAst = (program) => {
9444
+ const tokens = [];
9445
+ const visit = (node) => {
9446
+ if (!isAstNode(node)) return;
9447
+ const nodeType = node.type;
9448
+ if (NODES_DROPPED_FROM_TOKEN_STREAM.has(nodeType)) return;
9449
+ const start = safeNumberOrZero(node.start);
9450
+ const end = safeNumberOrZero(node.end);
9451
+ if (nodeType === "Identifier" || nodeType === "PrivateIdentifier") {
9452
+ const identifierName = node.name;
9453
+ tokens.push({
9454
+ kind: "identifier",
9455
+ payload: typeof identifierName === "string" ? identifierName : "",
9456
+ start,
9457
+ end
9458
+ });
9459
+ return;
9460
+ }
9461
+ if (nodeType === "Literal") {
9462
+ const literalValue = node.value;
9463
+ if (typeof literalValue === "string") tokens.push({
9464
+ kind: "string-literal",
9465
+ payload: literalValue,
9466
+ start,
9467
+ end
9468
+ });
9469
+ else if (typeof literalValue === "number") tokens.push({
9470
+ kind: "numeric-literal",
9471
+ payload: String(literalValue),
9472
+ start,
9473
+ end
9474
+ });
9475
+ else if (typeof literalValue === "boolean") tokens.push({
9476
+ kind: "boolean-literal",
9477
+ payload: literalValue ? "true" : "false",
9478
+ start,
9479
+ end
9480
+ });
9481
+ else if (literalValue === null) tokens.push({
9482
+ kind: "null-literal",
9483
+ payload: "null",
9484
+ start,
9485
+ end
9486
+ });
9487
+ else if (node.regex) tokens.push({
9488
+ kind: "regexp-literal",
9489
+ payload: "regex",
9490
+ start,
9491
+ end
9492
+ });
9493
+ else tokens.push({
9494
+ kind: "node-enter",
9495
+ payload: nodeType,
9496
+ start,
9497
+ end
9498
+ });
9499
+ return;
9500
+ }
9501
+ if (nodeType === "TemplateLiteral") {
9502
+ tokens.push({
9503
+ kind: "template-literal",
9504
+ payload: "tpl",
9505
+ start,
9506
+ end
9507
+ });
9508
+ visitChildrenRaw(node, visit);
9509
+ return;
9510
+ }
9511
+ tokens.push({
9512
+ kind: "node-enter",
9513
+ payload: nodeType,
9514
+ start,
9515
+ end
9516
+ });
9517
+ visitChildrenRaw(node, visit);
9518
+ };
9519
+ visit(program);
9520
+ return tokens;
9521
+ };
9522
+
9523
+ //#endregion
9524
+ //#region src/duplicate-blocks/index.ts
9525
+ const isBinaryFile = (sourceText) => {
9526
+ const sampleEnd = Math.min(sourceText.length, BINARY_DETECTION_SAMPLE_BYTES);
9527
+ let nullByteCount = 0;
9528
+ for (let charIndex = 0; charIndex < sampleEnd; charIndex++) if (sourceText.charCodeAt(charIndex) === 0) {
9529
+ nullByteCount++;
9530
+ if (nullByteCount >= 4) return true;
9531
+ }
9532
+ return false;
9533
+ };
9534
+ const isMinifiedSource = (sourceText) => {
9535
+ if (sourceText.length < 5e3) return false;
9536
+ const lineCount = (sourceText.match(/\n/g)?.length ?? 0) + 1;
9537
+ return sourceText.length / lineCount > 500;
9538
+ };
9539
+ const tokenizeFile = (filePath) => {
9540
+ let sourceStat;
9541
+ try {
9542
+ sourceStat = (0, node_fs.statSync)(filePath);
9543
+ } catch {
9544
+ return;
9545
+ }
9546
+ if (sourceStat.size > 2e6) return void 0;
9547
+ let sourceText;
9548
+ try {
9549
+ sourceText = (0, node_fs.readFileSync)(filePath, "utf-8");
9550
+ } catch {
9551
+ return;
9552
+ }
9553
+ if (sourceText.length === 0) return void 0;
9554
+ if (isBinaryFile(sourceText)) return void 0;
9555
+ if (isMinifiedSource(sourceText)) return void 0;
9556
+ let parseResult;
9557
+ try {
9558
+ parseResult = (0, oxc_parser.parseSync)(filePath, sourceText);
9559
+ } catch {
9560
+ return;
9561
+ }
9562
+ const sourceTokens = tokenizeAst(parseResult.program);
9563
+ if (sourceTokens.length === 0) return void 0;
9564
+ const lineStarts = computeLineStarts(sourceText);
9565
+ return {
9566
+ path: filePath,
9567
+ sourceTokens,
9568
+ lineStarts,
9569
+ lineCount: lineStarts.length
9570
+ };
9571
+ };
9572
+ const buildCloneInstance = (rawInstance, tokenLength, tokenizedFiles) => {
9573
+ const file = tokenizedFiles[rawInstance.fileIndex];
9574
+ const firstToken = file.sourceTokens[rawInstance.tokenOffsetWithinFile];
9575
+ const lastToken = file.sourceTokens[rawInstance.tokenOffsetWithinFile + tokenLength - 1];
9576
+ const startSpan = offsetToLineColumn(firstToken.start, file.lineStarts);
9577
+ const endSpan = offsetToLineColumn(lastToken.end, file.lineStarts);
9578
+ return {
9579
+ path: file.path,
9580
+ startLine: startSpan.line,
9581
+ endLine: endSpan.line,
9582
+ startColumn: startSpan.column,
9583
+ endColumn: endSpan.column
9584
+ };
9585
+ };
9586
+ const directoryOf = (filePath) => (0, node_path.dirname)(filePath);
9587
+ const filterRawBlocksToReportableDuplicates = (rawBlocks, tokenizedFiles, config) => {
9588
+ const duplicateBlocks = [];
9589
+ for (const rawBlock of rawBlocks) {
9590
+ const instances = rawBlock.instances.map((rawInstance) => buildCloneInstance(rawInstance, rawBlock.tokenLength, tokenizedFiles));
9591
+ let lineCount = 0;
9592
+ for (const instance of instances) {
9593
+ const instanceLineCount = instance.endLine - instance.startLine + 1;
9594
+ if (instanceLineCount > lineCount) lineCount = instanceLineCount;
9595
+ }
9596
+ if (lineCount < config.minLines) continue;
9597
+ if (instances.length < config.minOccurrences) continue;
9598
+ if (config.skipLocal) {
9599
+ if (new Set(instances.map((instance) => directoryOf(instance.path))).size < 2) continue;
9600
+ }
9601
+ const distinctFiles = new Set(instances.map((instance) => instance.path));
9602
+ const confidence = distinctFiles.size >= 2 ? "high" : "medium";
9603
+ duplicateBlocks.push({
9604
+ instances,
9605
+ tokenCount: rawBlock.tokenLength,
9606
+ lineCount,
9607
+ confidence,
9608
+ reason: distinctFiles.size >= 2 ? `${instances.length} occurrences spanning ${distinctFiles.size} files (≥${rawBlock.tokenLength} tokens, ${lineCount} lines)` : `${instances.length} occurrences within a single file (≥${rawBlock.tokenLength} tokens, ${lineCount} lines)`
9609
+ });
9610
+ }
9611
+ const maximalBlocks = dropBlocksSubsumedByLongerSibling(duplicateBlocks);
9612
+ maximalBlocks.sort((firstClone, secondClone) => {
9613
+ if (firstClone.lineCount !== secondClone.lineCount) return secondClone.lineCount - firstClone.lineCount;
9614
+ return secondClone.tokenCount - firstClone.tokenCount;
9615
+ });
9616
+ return maximalBlocks;
9617
+ };
9618
+ /**
9619
+ * The suffix-array + LCP-interval scan emits one block per LCP interval, but
9620
+ * nested intervals routinely yield the same set of source spans at multiple
9621
+ * lengths (the same maximal repeat reported at L, L-1, L-2, …). Drop any
9622
+ * block whose every instance is spatially contained inside some other block's
9623
+ * matching instance — that other block is strictly more informative.
9624
+ *
9625
+ * O(N²) worst-case, but N here is post-filter blocks (typically <1000 even on
9626
+ * large monorepos), and the early-exit on instance-count mismatch keeps it
9627
+ * tight in practice.
9628
+ */
9629
+ const dropBlocksSubsumedByLongerSibling = (blocks) => {
9630
+ const sorted = [...blocks].sort((firstBlock, secondBlock) => {
9631
+ if (firstBlock.tokenCount !== secondBlock.tokenCount) return secondBlock.tokenCount - firstBlock.tokenCount;
9632
+ return secondBlock.lineCount - firstBlock.lineCount;
9633
+ });
9634
+ const survivors = [];
9635
+ for (const candidate of sorted) {
9636
+ let subsumed = false;
9637
+ for (const survivor of survivors) {
9638
+ if (survivor.instances.length !== candidate.instances.length) continue;
9639
+ if (allInstancesContainedIn(candidate, survivor)) {
9640
+ subsumed = true;
9641
+ break;
9642
+ }
9643
+ }
9644
+ if (!subsumed) survivors.push(candidate);
9645
+ }
9646
+ return survivors;
9647
+ };
9648
+ const allInstancesContainedIn = (candidate, longer) => {
9649
+ for (const candidateInstance of candidate.instances) {
9650
+ let matched = false;
9651
+ for (const longerInstance of longer.instances) if (candidateInstance.path === longerInstance.path && isSpanContained(candidateInstance, longerInstance)) {
9652
+ matched = true;
9653
+ break;
9654
+ }
9655
+ if (!matched) return false;
9656
+ }
9657
+ return true;
9658
+ };
9659
+ const isSpanContained = (inner, outer) => {
9660
+ const innerStartsAfterOuter = inner.startLine > outer.startLine || inner.startLine === outer.startLine && inner.startColumn >= outer.startColumn;
9661
+ const innerEndsBeforeOuter = inner.endLine < outer.endLine || inner.endLine === outer.endLine && inner.endColumn <= outer.endColumn;
9662
+ return innerStartsAfterOuter && innerEndsBeforeOuter;
9663
+ };
9664
+ /**
9665
+ * Token-based duplicate block detector.
9666
+ *
9667
+ * Pipeline:
9668
+ * 1. Tokenize each file with the AST visitor in `token-visitor.ts`
9669
+ * 2. Hash + normalize tokens with the chosen detection mode
9670
+ * 3. Concatenate every file's hashed tokens with unique negative sentinels
9671
+ * 4. Build a suffix array (prefix doubling + radix sort) and LCP array
9672
+ * 5. Stack-based LCP-interval scan extracts maximal duplicate blocks
9673
+ * 6. Filter on min-tokens / min-lines / min-occurrences / skip-local
9674
+ * 7. Group clones into families; collapse N two-file families with matching
9675
+ * basenames into a `ShadowedDirectoryPair` finding
9676
+ *
9677
+ * Returns empty arrays when `config.enabled` is false.
9678
+ */
9679
+ const detectDuplicateBlocks = (graph, config, rootDir) => {
9680
+ if (!config || !config.enabled) return {
9681
+ duplicateBlocks: [],
9682
+ duplicateBlockClusters: [],
9683
+ shadowedDirectoryPairs: []
9684
+ };
9685
+ const tokenizedFiles = [];
9686
+ for (const module of graph.modules) {
9687
+ if (module.isDeclarationFile) continue;
9688
+ if (module.isConfigFile) continue;
9689
+ const tokenizedFile = tokenizeFile(module.fileId.path);
9690
+ if (!tokenizedFile) continue;
9691
+ tokenizedFiles.push(tokenizedFile);
9692
+ }
9693
+ if (tokenizedFiles.length === 0) return {
9694
+ duplicateBlocks: [],
9695
+ duplicateBlockClusters: [],
9696
+ shadowedDirectoryPairs: []
9697
+ };
9698
+ const filesHashedTokens = tokenizedFiles.map((file) => normalizeAndHashTokens(file.sourceTokens, config.mode));
9699
+ const filesTokenCounts = filesHashedTokens.map((fileTokens) => fileTokens.length);
9700
+ if (!filesTokenCounts.some((count) => count >= config.minTokens)) return {
9701
+ duplicateBlocks: [],
9702
+ duplicateBlockClusters: [],
9703
+ shadowedDirectoryPairs: []
9704
+ };
9705
+ const concatenation = rankReduceAndConcatenate(filesHashedTokens);
9706
+ if (concatenation.tokenSequence.length === 0) return {
9707
+ duplicateBlocks: [],
9708
+ duplicateBlockClusters: [],
9709
+ shadowedDirectoryPairs: []
9710
+ };
9711
+ const suffixArray = buildSuffixArray(concatenation.tokenSequence);
9712
+ const duplicateBlocks = filterRawBlocksToReportableDuplicates(extractRawDuplicateBlocks(suffixArray, buildLcpArray(concatenation.tokenSequence, suffixArray), concatenation.fileOf, concatenation.fileOffsets, filesTokenCounts, config.minTokens), tokenizedFiles, config);
9713
+ const duplicateBlockClusters = groupDuplicateBlocksIntoClusters(duplicateBlocks);
9714
+ return {
9715
+ duplicateBlocks,
9716
+ duplicateBlockClusters,
9717
+ shadowedDirectoryPairs: detectShadowedDirectoryPairs(duplicateBlockClusters, rootDir)
9718
+ };
9719
+ };
9720
+
9721
+ //#endregion
9722
+ //#region src/report/re-export-cycles.ts
9723
+ /**
9724
+ * Reports cycles in the subgraph of `isReExportEdge` edges only. These are
9725
+ * a strict subset of `circularDependencies` but worth separating: every
9726
+ * general cycle can have a legitimate bidirectional-collaboration reason,
9727
+ * but a re-export cycle has none — it always tanks tree-shaking and risks
9728
+ * the "Cannot access X before initialization" TDZ runtime error.
9729
+ */
9730
+ const detectReExportCycles = (graph) => {
9731
+ const adjacency = Array.from({ length: graph.modules.length }, () => []);
9732
+ const reExportTargetSets = Array.from({ length: graph.modules.length }, () => /* @__PURE__ */ new Set());
9733
+ for (const edge of graph.edges) {
9734
+ if (!edge.isReExportEdge) continue;
9735
+ if (edge.target >= graph.modules.length) continue;
9736
+ if (reExportTargetSets[edge.source].has(edge.target)) continue;
9737
+ reExportTargetSets[edge.source].add(edge.target);
9738
+ adjacency[edge.source].push(edge.target);
9739
+ }
9740
+ const sccComponents = computeStronglyConnectedComponents(adjacency);
9741
+ const findings = [];
9742
+ for (const component of sccComponents) {
9743
+ if (component.length === 1) {
9744
+ const onlyNode = component[0];
9745
+ if (!adjacency[onlyNode].includes(onlyNode)) continue;
9746
+ const filePath = graph.modules[onlyNode].fileId.path;
9747
+ findings.push({
9748
+ files: [filePath],
9749
+ kind: "self-loop",
9750
+ confidence: "high",
9751
+ reason: `${filePath} re-exports from itself — the barrel imports its own root, which breaks bundler tree-shaking and risks TDZ runtime errors`
9752
+ });
9753
+ continue;
9754
+ }
9755
+ const sortedFiles = component.map((moduleIndex) => graph.modules[moduleIndex].fileId.path).sort();
9756
+ findings.push({
9757
+ files: sortedFiles,
9758
+ kind: "multi-node",
9759
+ confidence: "high",
9760
+ reason: `${sortedFiles.length} modules form a re-export cycle — refactor consumers to import from the leaf module instead of the barrel`
9761
+ });
9762
+ }
9763
+ findings.sort((firstFinding, secondFinding) => firstFinding.files[0].localeCompare(secondFinding.files[0]));
9764
+ return findings;
9765
+ };
9766
+ /**
9767
+ * Iterative Tarjan's SCC. Singleton components are returned too so the
9768
+ * caller can distinguish a real self-loop from a node with no edges.
9769
+ */
9770
+ const computeStronglyConnectedComponents = (adjacency) => {
9771
+ const nodeCount = adjacency.length;
9772
+ if (nodeCount === 0) return [];
9773
+ const indices = new Array(nodeCount).fill(-1);
9774
+ const lowLinks = new Array(nodeCount).fill(0);
9775
+ const onStack = new Array(nodeCount).fill(false);
9776
+ const tarjanStack = [];
9777
+ const components = [];
9778
+ let nextIndex = 0;
9779
+ for (let startNode = 0; startNode < nodeCount; startNode++) {
9780
+ if (indices[startNode] !== -1) continue;
9781
+ const dfsStack = [{
9782
+ node: startNode,
9783
+ successorPosition: 0
9784
+ }];
9785
+ indices[startNode] = nextIndex;
9786
+ lowLinks[startNode] = nextIndex;
9787
+ nextIndex++;
9788
+ onStack[startNode] = true;
9789
+ tarjanStack.push(startNode);
9790
+ while (dfsStack.length > 0) {
9791
+ const frame = dfsStack[dfsStack.length - 1];
9792
+ const successors = adjacency[frame.node];
9793
+ if (frame.successorPosition < successors.length) {
9794
+ const successorNode = successors[frame.successorPosition];
9795
+ frame.successorPosition++;
9796
+ if (indices[successorNode] === -1) {
9797
+ indices[successorNode] = nextIndex;
9798
+ lowLinks[successorNode] = nextIndex;
9799
+ nextIndex++;
9800
+ onStack[successorNode] = true;
9801
+ tarjanStack.push(successorNode);
9802
+ dfsStack.push({
9803
+ node: successorNode,
9804
+ successorPosition: 0
9805
+ });
9806
+ } else if (onStack[successorNode]) {
9807
+ if (indices[successorNode] < lowLinks[frame.node]) lowLinks[frame.node] = indices[successorNode];
9808
+ }
9809
+ } else {
9810
+ if (lowLinks[frame.node] === indices[frame.node]) {
9811
+ const component = [];
9812
+ let popped;
9813
+ do {
9814
+ popped = tarjanStack.pop();
9815
+ onStack[popped] = false;
9816
+ component.push(popped);
9817
+ } while (popped !== frame.node);
9818
+ components.push(component);
9819
+ }
9820
+ dfsStack.pop();
9821
+ if (dfsStack.length > 0) {
9822
+ const parent = dfsStack[dfsStack.length - 1];
9823
+ if (lowLinks[frame.node] < lowLinks[parent.node]) lowLinks[parent.node] = lowLinks[frame.node];
9824
+ }
9825
+ }
9826
+ }
9827
+ }
9828
+ return components;
9829
+ };
9830
+
9831
+ //#endregion
9832
+ //#region src/report/feature-flags.ts
9833
+ const BUILTIN_SDK_PATTERNS = [
9834
+ {
9835
+ functionName: "useFlag",
9836
+ nameArgIndex: 0,
9837
+ provider: "LaunchDarkly"
9838
+ },
9839
+ {
9840
+ functionName: "useLDFlag",
9841
+ nameArgIndex: 0,
9842
+ provider: "LaunchDarkly"
9843
+ },
9844
+ {
9845
+ functionName: "useFeatureFlag",
9846
+ nameArgIndex: 0,
9847
+ provider: "LaunchDarkly"
9848
+ },
9849
+ {
9850
+ functionName: "variation",
9851
+ nameArgIndex: 0,
9852
+ provider: "LaunchDarkly"
9853
+ },
9854
+ {
9855
+ functionName: "boolVariation",
9856
+ nameArgIndex: 0,
9857
+ provider: "LaunchDarkly"
9858
+ },
9859
+ {
9860
+ functionName: "stringVariation",
9861
+ nameArgIndex: 0,
9862
+ provider: "LaunchDarkly"
9863
+ },
9864
+ {
9865
+ functionName: "numberVariation",
9866
+ nameArgIndex: 0,
9867
+ provider: "LaunchDarkly"
9868
+ },
9869
+ {
9870
+ functionName: "jsonVariation",
9871
+ nameArgIndex: 0,
9872
+ provider: "LaunchDarkly"
9873
+ },
9874
+ {
9875
+ functionName: "useGate",
9876
+ nameArgIndex: 0,
9877
+ provider: "Statsig"
9878
+ },
9879
+ {
9880
+ functionName: "checkGate",
9881
+ nameArgIndex: 0,
9882
+ provider: "Statsig"
9883
+ },
9884
+ {
9885
+ functionName: "useExperiment",
9886
+ nameArgIndex: 0,
9887
+ provider: "Statsig"
9888
+ },
9889
+ {
9890
+ functionName: "useConfig",
9891
+ nameArgIndex: 0,
9892
+ provider: "Statsig"
9893
+ },
9894
+ {
9895
+ functionName: "isEnabled",
9896
+ nameArgIndex: 0,
9897
+ provider: "Unleash"
9898
+ },
9899
+ {
9900
+ functionName: "getVariant",
9901
+ nameArgIndex: 0,
9902
+ provider: "Unleash"
9903
+ },
9904
+ {
9905
+ functionName: "isOn",
9906
+ nameArgIndex: 0,
9907
+ provider: "GrowthBook"
9908
+ },
9909
+ {
9910
+ functionName: "isOff",
9911
+ nameArgIndex: 0,
9912
+ provider: "GrowthBook"
9913
+ },
9914
+ {
9915
+ functionName: "getFeatureValue",
9916
+ nameArgIndex: 0,
9917
+ provider: "GrowthBook"
9918
+ },
9919
+ {
9920
+ functionName: "getTreatment",
9921
+ nameArgIndex: 0,
9922
+ provider: "Split"
9923
+ },
9924
+ {
9925
+ functionName: "useFeatureFlagEnabled",
9926
+ nameArgIndex: 0,
9927
+ provider: "PostHog"
9928
+ },
9929
+ {
9930
+ functionName: "useFeatureFlagPayload",
9931
+ nameArgIndex: 0,
9932
+ provider: "PostHog"
9933
+ },
9934
+ {
9935
+ functionName: "useFeatureFlagVariantKey",
9936
+ nameArgIndex: 0,
9937
+ provider: "PostHog"
9938
+ },
9939
+ {
9940
+ functionName: "getFeatureFlagPayload",
9941
+ nameArgIndex: 0,
9942
+ provider: "PostHog"
9943
+ },
9944
+ {
9945
+ functionName: "getValueAsync",
9946
+ nameArgIndex: 0,
9947
+ provider: "ConfigCat"
9948
+ },
9949
+ {
9950
+ functionName: "getValueDetailsAsync",
9951
+ nameArgIndex: 0,
9952
+ provider: "ConfigCat"
9953
+ },
9954
+ {
9955
+ functionName: "hasFeature",
9956
+ nameArgIndex: 0,
9957
+ provider: "Flagsmith"
9958
+ },
9959
+ {
9960
+ functionName: "useDecision",
9961
+ nameArgIndex: 0,
9962
+ provider: "Optimizely"
9963
+ },
9964
+ {
9965
+ functionName: "getFeatureVariable",
9966
+ nameArgIndex: 0,
9967
+ provider: "Optimizely"
9968
+ },
9969
+ {
9970
+ functionName: "getFeatureVariableBoolean",
9971
+ nameArgIndex: 0,
9972
+ provider: "Optimizely"
9973
+ },
9974
+ {
9975
+ functionName: "getFeatureVariableString",
9976
+ nameArgIndex: 0,
9977
+ provider: "Optimizely"
9978
+ },
9979
+ {
9980
+ functionName: "getFeatureVariableInteger",
9981
+ nameArgIndex: 0,
9982
+ provider: "Optimizely"
9983
+ },
9984
+ {
9985
+ functionName: "getFeatureVariableDouble",
9986
+ nameArgIndex: 0,
9987
+ provider: "Optimizely"
9988
+ },
9989
+ {
9990
+ functionName: "getFeatureVariableJson",
9991
+ nameArgIndex: 0,
9992
+ provider: "Optimizely"
9993
+ },
9994
+ {
9995
+ functionName: "getFeatureVariableJSON",
9996
+ nameArgIndex: 0,
9997
+ provider: "Optimizely"
9998
+ },
9999
+ {
10000
+ functionName: "getStringAssignment",
10001
+ nameArgIndex: 0,
10002
+ provider: "Eppo"
10003
+ },
10004
+ {
10005
+ functionName: "getBooleanAssignment",
10006
+ nameArgIndex: 0,
10007
+ provider: "Eppo"
10008
+ },
10009
+ {
10010
+ functionName: "getNumericAssignment",
10011
+ nameArgIndex: 0,
10012
+ provider: "Eppo"
10013
+ },
10014
+ {
10015
+ functionName: "getIntegerAssignment",
10016
+ nameArgIndex: 0,
10017
+ provider: "Eppo"
10018
+ },
10019
+ {
10020
+ functionName: "getJSONAssignment",
10021
+ nameArgIndex: 0,
10022
+ provider: "Eppo"
10023
+ }
10024
+ ];
10025
+ const VERCEL_FLAGS_FUNCTION_NAMES = new Set(["flag", "evaluate"]);
10026
+ const BUILTIN_ENV_PREFIXES = [
10027
+ "FEATURE_",
10028
+ "NEXT_PUBLIC_FEATURE_",
10029
+ "NEXT_PUBLIC_ENABLE_",
10030
+ "REACT_APP_FEATURE_",
10031
+ "REACT_APP_ENABLE_",
10032
+ "VITE_FEATURE_",
10033
+ "VITE_ENABLE_",
10034
+ "NUXT_PUBLIC_FEATURE_",
10035
+ "ENABLE_",
10036
+ "FF_",
10037
+ "FLAG_",
10038
+ "TOGGLE_"
10039
+ ];
10040
+ const CONFIG_OBJECT_KEYWORDS = new Set([
10041
+ "feature",
10042
+ "features",
10043
+ "featureflags",
10044
+ "featureflag",
10045
+ "flag",
10046
+ "flags",
10047
+ "toggle",
10048
+ "toggles"
10049
+ ]);
10050
+ const getStaticName = (node) => {
10051
+ if (!isAstNode(node)) return void 0;
10052
+ if (node.type === "Identifier" || node.type === "PrivateIdentifier") {
10053
+ const identifierName = node.name;
10054
+ return typeof identifierName === "string" ? identifierName : void 0;
10055
+ }
10056
+ if (node.type === "Literal") {
10057
+ const literalValue = node.value;
10058
+ return typeof literalValue === "string" ? literalValue : void 0;
10059
+ }
10060
+ };
10061
+ const extractStringArgument = (callArguments, argumentIndex) => {
10062
+ if (!Array.isArray(callArguments)) return void 0;
10063
+ const argumentNode = callArguments[argumentIndex];
10064
+ if (!isAstNode(argumentNode)) return void 0;
10065
+ if (argumentNode.type === "Literal") {
10066
+ const literalValue = argumentNode.value;
10067
+ return typeof literalValue === "string" ? literalValue : void 0;
10068
+ }
10069
+ if (argumentNode.type === "ObjectExpression") {
10070
+ const properties = argumentNode.properties;
10071
+ if (!Array.isArray(properties)) return void 0;
10072
+ for (const property of properties) {
10073
+ if (!isAstNode(property)) continue;
10074
+ if (property.type !== "Property") continue;
10075
+ const propertyKey = getStaticName(property.key);
10076
+ if (propertyKey !== "key" && propertyKey !== "name") continue;
10077
+ const propertyValueName = getStaticName(property.value);
10078
+ if (propertyValueName !== void 0) return propertyValueName;
10079
+ }
10080
+ }
10081
+ };
10082
+ const extractProcessEnvName = (memberExpression) => {
10083
+ if (!isAstNode(memberExpression)) return void 0;
10084
+ if (memberExpression.type !== "MemberExpression" && memberExpression.type !== "StaticMemberExpression") return;
10085
+ const propertyName = getStaticName(memberExpression.property);
10086
+ if (propertyName === void 0) return void 0;
10087
+ const objectNode = memberExpression.object;
10088
+ if (!isAstNode(objectNode)) return void 0;
10089
+ if (objectNode.type !== "MemberExpression" && objectNode.type !== "StaticMemberExpression") return;
10090
+ const innerObjectName = getStaticName(objectNode.object);
10091
+ const innerPropertyName = getStaticName(objectNode.property);
10092
+ if (innerObjectName === "process" && innerPropertyName === "env") return propertyName;
10093
+ };
10094
+ const isFlagEnvName = (envName, extraEnvPrefixes) => {
10095
+ for (const prefix of BUILTIN_ENV_PREFIXES) if (envName.startsWith(prefix)) return true;
10096
+ for (const prefix of extraEnvPrefixes) if (envName.startsWith(prefix)) return true;
10097
+ return false;
10098
+ };
10099
+ const collectVercelFlagsImports = (programNode) => {
10100
+ const localNames = /* @__PURE__ */ new Set();
10101
+ if (!isAstNode(programNode)) return localNames;
10102
+ const body = programNode.body;
10103
+ if (!Array.isArray(body)) return localNames;
10104
+ for (const statement of body) {
10105
+ if (!isAstNode(statement)) continue;
10106
+ if (statement.type !== "ImportDeclaration") continue;
10107
+ const sourceLiteral = statement.source;
10108
+ const sourceValue = isAstNode(sourceLiteral) ? sourceLiteral.value : void 0;
10109
+ if (typeof sourceValue !== "string") continue;
10110
+ if (!(sourceValue === "flags" || sourceValue.startsWith("flags/") || sourceValue === "@vercel/flags" || sourceValue.startsWith("@vercel/flags/"))) continue;
10111
+ const specifiers = statement.specifiers;
10112
+ if (!Array.isArray(specifiers)) continue;
10113
+ for (const specifier of specifiers) {
10114
+ if (!isAstNode(specifier)) continue;
10115
+ if (specifier.type === "ImportSpecifier") {
10116
+ const imported = specifier.imported;
10117
+ const local = specifier.local;
10118
+ const importedName = getStaticName(imported);
10119
+ const localName = getStaticName(local);
10120
+ if (importedName && VERCEL_FLAGS_FUNCTION_NAMES.has(importedName) && localName) localNames.add(localName);
10121
+ }
10122
+ }
10123
+ }
10124
+ return localNames;
10125
+ };
10126
+ const visitChildrenWithGuard = (node, visitor) => {
10127
+ if (!isAstNode(node)) return;
10128
+ for (const key of Object.keys(node)) {
10129
+ if (key === "type" || key === "start" || key === "end" || key === "loc" || key === "range") continue;
10130
+ const value = node[key];
10131
+ if (Array.isArray(value)) for (const item of value) visitor(item);
10132
+ else if (value !== null && typeof value === "object") visitor(value);
10133
+ }
10134
+ };
10135
+ const recordFlag = (context, flagName, kind, byteOffset, sdkProvider) => {
10136
+ const { line, column } = offsetToLineColumn(byteOffset, context.lineStarts);
10137
+ context.results.push({
10138
+ path: context.filePath,
10139
+ name: flagName,
10140
+ kind,
10141
+ line,
10142
+ column,
10143
+ sdkProvider,
10144
+ guardLineStart: context.guard?.startLine,
10145
+ guardLineEnd: context.guard?.endLine,
10146
+ guardsDeadCode: false
10147
+ });
10148
+ };
10149
+ const visitNode$1 = (node, context) => {
10150
+ if (!isAstNode(node)) return;
10151
+ if (node.type === "IfStatement") {
10152
+ const start = node.start;
10153
+ const end = node.end;
10154
+ const guard = typeof start === "number" && typeof end === "number" ? {
10155
+ startLine: offsetToLineColumn(start, context.lineStarts).line,
10156
+ endLine: offsetToLineColumn(end, context.lineStarts).line
10157
+ } : void 0;
10158
+ const previousGuard = context.guard;
10159
+ context.guard = guard;
10160
+ visitNode$1(node.test, context);
10161
+ context.guard = previousGuard;
10162
+ visitNode$1(node.consequent, context);
10163
+ visitNode$1(node.alternate, context);
10164
+ return;
10165
+ }
10166
+ if (node.type === "ConditionalExpression") {
10167
+ const start = node.start;
10168
+ const end = node.end;
10169
+ const guard = typeof start === "number" && typeof end === "number" ? {
10170
+ startLine: offsetToLineColumn(start, context.lineStarts).line,
10171
+ endLine: offsetToLineColumn(end, context.lineStarts).line
10172
+ } : void 0;
10173
+ const previousGuard = context.guard;
10174
+ context.guard = guard;
10175
+ visitNode$1(node.test, context);
10176
+ context.guard = previousGuard;
10177
+ visitNode$1(node.consequent, context);
10178
+ visitNode$1(node.alternate, context);
10179
+ return;
10180
+ }
10181
+ visitFlagPatternsInExpression(node, context);
10182
+ visitChildrenWithGuard(node, (child) => visitNode$1(child, context));
10183
+ };
10184
+ const visitFlagPatternsInExpression = (node, context) => {
10185
+ if (!isAstNode(node)) return;
10186
+ if (node.type === "MemberExpression" || node.type === "StaticMemberExpression") {
10187
+ const envName = extractProcessEnvName(node);
10188
+ if (envName !== void 0 && isFlagEnvName(envName, context.envPrefixes)) {
10189
+ const start = node.start;
10190
+ if (typeof start === "number") recordFlag(context, envName, "env-var", start, void 0);
10191
+ } else if (context.detectConfigObjects) {
10192
+ const objectName = getStaticName(node.object);
10193
+ const propertyName = getStaticName(node.property);
10194
+ if (objectName && propertyName) {
10195
+ if (CONFIG_OBJECT_KEYWORDS.has(objectName.toLowerCase()) || CONFIG_OBJECT_KEYWORDS.has(propertyName.toLowerCase())) {
10196
+ const start = node.start;
10197
+ if (typeof start === "number") recordFlag(context, `${objectName}.${propertyName}`, "config-object", start, void 0);
10198
+ }
10199
+ }
10200
+ }
10201
+ }
10202
+ if (node.type === "CallExpression") {
10203
+ const callee = node.callee;
10204
+ let functionName;
10205
+ let calleeIsMemberExpression = false;
10206
+ let receiverIsItselfMemberExpression = false;
10207
+ if (isAstNode(callee)) {
10208
+ if (callee.type === "Identifier") functionName = getStaticName(callee);
10209
+ else if (callee.type === "MemberExpression" || callee.type === "StaticMemberExpression") {
10210
+ calleeIsMemberExpression = true;
10211
+ functionName = getStaticName(callee.property);
10212
+ const receiver = callee.object;
10213
+ if (isAstNode(receiver) && (receiver.type === "MemberExpression" || receiver.type === "StaticMemberExpression")) receiverIsItselfMemberExpression = true;
10214
+ }
10215
+ }
10216
+ if (functionName !== void 0) {
10217
+ if (!calleeIsMemberExpression && context.vercelFlagsLocalNames.has(functionName)) {
10218
+ const callArguments = node.arguments;
10219
+ const flagName = extractStringArgument(callArguments, 0);
10220
+ if (flagName !== void 0) {
10221
+ const start = node.start;
10222
+ if (typeof start === "number") recordFlag(context, flagName, "sdk-call", start, "Vercel Flags");
10223
+ }
10224
+ return;
10225
+ }
10226
+ if (calleeIsMemberExpression && receiverIsItselfMemberExpression) return;
10227
+ for (const sdkPattern of context.sdkPatterns) {
10228
+ if (sdkPattern.functionName !== functionName) continue;
10229
+ const callArguments = node.arguments;
10230
+ const flagName = extractStringArgument(callArguments, sdkPattern.nameArgIndex);
10231
+ if (flagName === void 0) continue;
10232
+ const start = node.start;
10233
+ if (typeof start === "number") recordFlag(context, flagName, "sdk-call", start, sdkPattern.provider === "" ? void 0 : sdkPattern.provider);
10234
+ break;
10235
+ }
10236
+ }
10237
+ }
10238
+ };
10239
+ const buildSdkPatterns = (extraSdkFunctionNames) => {
10240
+ const merged = [...BUILTIN_SDK_PATTERNS];
10241
+ for (const extraName of extraSdkFunctionNames) merged.push({
10242
+ functionName: extraName,
10243
+ nameArgIndex: 0,
10244
+ provider: ""
10245
+ });
10246
+ return merged;
10247
+ };
10248
+ const detectFeatureFlags = (graph, config) => {
10249
+ if (!config?.enabled) return [];
10250
+ const sdkPatterns = buildSdkPatterns(config.extraSdkFunctionNames);
10251
+ const collectedFlags = [];
10252
+ for (const module of graph.modules) {
10253
+ if (module.isDeclarationFile) continue;
10254
+ if (module.isConfigFile) continue;
10255
+ let sourceText;
10256
+ try {
10257
+ sourceText = (0, node_fs.readFileSync)(module.fileId.path, "utf-8");
10258
+ } catch {
10259
+ continue;
10260
+ }
10261
+ let parseResult;
10262
+ try {
10263
+ parseResult = (0, oxc_parser.parseSync)(module.fileId.path, sourceText);
10264
+ } catch {
10265
+ continue;
10266
+ }
10267
+ const lineStarts = computeLineStarts(sourceText);
10268
+ const vercelFlagsLocalNames = collectVercelFlagsImports(parseResult.program);
10269
+ const visitContext = {
10270
+ filePath: module.fileId.path,
10271
+ lineStarts,
10272
+ results: [],
10273
+ envPrefixes: config.extraEnvPrefixes,
10274
+ sdkPatterns,
10275
+ detectConfigObjects: config.detectConfigObjects,
10276
+ vercelFlagsLocalNames,
10277
+ guard: void 0
10278
+ };
10279
+ visitNode$1(parseResult.program, visitContext);
10280
+ collectedFlags.push(...visitContext.results);
10281
+ }
10282
+ collectedFlags.sort((leftFlag, rightFlag) => {
10283
+ if (leftFlag.path !== rightFlag.path) return leftFlag.path.localeCompare(rightFlag.path);
10284
+ if (leftFlag.line !== rightFlag.line) return leftFlag.line - rightFlag.line;
10285
+ return leftFlag.column - rightFlag.column;
10286
+ });
10287
+ return collectedFlags;
10288
+ };
10289
+ /**
10290
+ * Mark each flag whose guard span overlaps an unused export as
10291
+ * `guardsDeadCode: true`.
10292
+ */
10293
+ const correlateFlagsWithDeadCode = (flags, scanResult) => {
10294
+ if (flags.length === 0 || scanResult.unusedExports.length === 0) return;
10295
+ const unusedByFile = /* @__PURE__ */ new Map();
10296
+ for (const unusedExport of scanResult.unusedExports) {
10297
+ const existing = unusedByFile.get(unusedExport.path);
10298
+ if (existing) existing.push(unusedExport.line);
10299
+ else unusedByFile.set(unusedExport.path, [unusedExport.line]);
10300
+ }
10301
+ for (const flag of flags) {
10302
+ if (flag.guardLineStart === void 0 || flag.guardLineEnd === void 0) continue;
10303
+ const linesInFile = unusedByFile.get(flag.path);
10304
+ if (!linesInFile) continue;
10305
+ const guardStart = flag.guardLineStart;
10306
+ const guardEnd = flag.guardLineEnd;
10307
+ for (const unusedLine of linesInFile) if (unusedLine >= guardStart && unusedLine <= guardEnd) {
10308
+ flag.guardsDeadCode = true;
10309
+ break;
10310
+ }
10311
+ }
10312
+ };
10313
+
10314
+ //#endregion
10315
+ //#region src/report/complexity.ts
10316
+ const incrementCyclomatic = (state) => {
10317
+ const topFrame = state.frameStack[state.frameStack.length - 1];
10318
+ if (topFrame) topFrame.cyclomaticComplexity++;
10319
+ };
10320
+ const incrementCognitiveWithNesting = (state) => {
10321
+ const topFrame = state.frameStack[state.frameStack.length - 1];
10322
+ if (topFrame) topFrame.cognitiveComplexity += 1 + topFrame.nestingLevel;
10323
+ };
10324
+ const incrementCognitiveFlat = (state) => {
10325
+ const topFrame = state.frameStack[state.frameStack.length - 1];
10326
+ if (topFrame) topFrame.cognitiveComplexity++;
10327
+ };
10328
+ const handleLogicalOperator = (operator, state) => {
10329
+ const topFrame = state.frameStack[state.frameStack.length - 1];
10330
+ if (!topFrame) return;
10331
+ if (topFrame.lastLogicalOperator === void 0) {
10332
+ topFrame.cognitiveComplexity++;
10333
+ topFrame.lastLogicalOperator = operator;
10334
+ return;
10335
+ }
10336
+ if (topFrame.lastLogicalOperator === operator) return;
10337
+ topFrame.cognitiveComplexity++;
10338
+ topFrame.lastLogicalOperator = operator;
10339
+ };
10340
+ const resetLogicalOperator = (state) => {
10341
+ const topFrame = state.frameStack[state.frameStack.length - 1];
10342
+ if (topFrame) topFrame.lastLogicalOperator = void 0;
10343
+ };
10344
+ const incrementNesting = (state) => {
10345
+ const topFrame = state.frameStack[state.frameStack.length - 1];
10346
+ if (topFrame) topFrame.nestingLevel++;
10347
+ };
10348
+ const decrementNesting = (state) => {
10349
+ const topFrame = state.frameStack[state.frameStack.length - 1];
10350
+ if (topFrame && topFrame.nestingLevel > 0) topFrame.nestingLevel--;
10351
+ };
10352
+ const countParameters = (parametersNode) => {
10353
+ if (!isAstNode(parametersNode)) return 0;
10354
+ const params = parametersNode;
10355
+ if (Array.isArray(params.params)) return params.params.length;
10356
+ if (Array.isArray(params.items)) return params.items.length;
10357
+ return 0;
10358
+ };
10359
+ const visitChildrenGeneric = (node, visitor) => {
10360
+ if (!isAstNode(node)) return;
10361
+ for (const key of Object.keys(node)) {
10362
+ if (key === "type" || key === "start" || key === "end" || key === "loc" || key === "range") continue;
10363
+ const value = node[key];
10364
+ if (Array.isArray(value)) for (const item of value) visitor(item);
10365
+ else if (value !== null && typeof value === "object") visitor(value);
10366
+ }
10367
+ };
10368
+ const pushFunctionFrame = (functionName, startOffset, endOffset, parameterCount, state) => {
10369
+ state.frameStack.push({
10370
+ functionName,
10371
+ startOffset,
10372
+ endOffset,
10373
+ cyclomaticComplexity: 1,
10374
+ cognitiveComplexity: 0,
10375
+ nestingLevel: 0,
10376
+ lastLogicalOperator: void 0,
10377
+ parameterCount
10378
+ });
10379
+ };
10380
+ const popFunctionFrame = (state) => {
10381
+ const completedFrame = state.frameStack.pop();
10382
+ if (!completedFrame) return;
10383
+ const { line, column } = offsetToLineColumn(completedFrame.startOffset, state.lineStarts);
10384
+ const endLine = offsetToLineColumn(completedFrame.endOffset, state.lineStarts).line;
10385
+ state.results.push({
10386
+ path: state.filePath,
10387
+ functionName: completedFrame.functionName,
10388
+ line,
10389
+ column,
10390
+ cyclomatic: completedFrame.cyclomaticComplexity,
10391
+ cognitive: completedFrame.cognitiveComplexity,
10392
+ lineCount: Math.max(1, endLine - line + 1),
10393
+ paramCount: completedFrame.parameterCount,
10394
+ confidence: "medium",
10395
+ reason: ""
10396
+ });
10397
+ };
10398
+ const visitFunctionLike = (node, kind, state) => {
10399
+ if (!isAstNode(node)) return;
10400
+ const functionName = state.pendingFunctionName ?? (() => {
10401
+ const idNode = node.id;
10402
+ const idName = isAstNode(idNode) ? idNode.name : void 0;
10403
+ return typeof idName === "string" ? idName : kind === "arrow" ? "<arrow>" : "<anonymous>";
10404
+ })();
10405
+ state.pendingFunctionName = void 0;
10406
+ const isNested = state.frameStack.length > 0;
10407
+ if (isNested) incrementNesting(state);
10408
+ const startOffset = node.start;
10409
+ const endOffset = node.end;
10410
+ const parameterCount = countParameters(node.params);
10411
+ pushFunctionFrame(functionName, typeof startOffset === "number" ? startOffset : 0, typeof endOffset === "number" ? endOffset : 0, parameterCount, state);
10412
+ visitChildrenGeneric(node, (child) => visitNode(child, state));
10413
+ popFunctionFrame(state);
10414
+ if (isNested) decrementNesting(state);
10415
+ };
10416
+ const visitNode = (node, state) => {
10417
+ if (!isAstNode(node)) return;
10418
+ switch (node.type) {
10419
+ case "FunctionDeclaration":
10420
+ case "FunctionExpression":
10421
+ case "MethodDefinition":
10422
+ if (node.type === "MethodDefinition") {
10423
+ const keyNode = node.key;
10424
+ const keyName = isAstNode(keyNode) ? keyNode.name ?? keyNode.value : void 0;
10425
+ if (typeof keyName === "string") state.pendingFunctionName = keyName;
10426
+ visitChildrenGeneric(node, (child) => visitNode(child, state));
10427
+ state.pendingFunctionName = void 0;
10428
+ return;
10429
+ }
10430
+ visitFunctionLike(node, "function", state);
10431
+ return;
10432
+ case "ArrowFunctionExpression":
10433
+ visitFunctionLike(node, "arrow", state);
10434
+ return;
10435
+ case "VariableDeclarator": {
10436
+ const declaratorId = node.id;
10437
+ const declaratorIdName = isAstNode(declaratorId) ? declaratorId.name : void 0;
10438
+ if (typeof declaratorIdName === "string") state.pendingFunctionName = declaratorIdName;
10439
+ visitChildrenGeneric(node, (child) => visitNode(child, state));
10440
+ state.pendingFunctionName = void 0;
10441
+ return;
10442
+ }
10443
+ case "PropertyDefinition": {
10444
+ const keyNode = node.key;
10445
+ const keyName = isAstNode(keyNode) ? keyNode.name : void 0;
10446
+ if (typeof keyName === "string") state.pendingFunctionName = keyName;
10447
+ visitChildrenGeneric(node, (child) => visitNode(child, state));
10448
+ state.pendingFunctionName = void 0;
10449
+ return;
10450
+ }
10451
+ case "IfStatement":
10452
+ incrementCyclomatic(state);
10453
+ incrementCognitiveWithNesting(state);
10454
+ incrementNesting(state);
10455
+ visitChildrenGeneric(node, (child) => visitNode(child, state));
10456
+ decrementNesting(state);
10457
+ resetLogicalOperator(state);
10458
+ return;
10459
+ case "ForStatement":
10460
+ case "ForInStatement":
10461
+ case "ForOfStatement":
10462
+ case "WhileStatement":
10463
+ case "DoWhileStatement":
10464
+ incrementCyclomatic(state);
10465
+ incrementCognitiveWithNesting(state);
10466
+ incrementNesting(state);
10467
+ visitChildrenGeneric(node, (child) => visitNode(child, state));
10468
+ decrementNesting(state);
10469
+ return;
10470
+ case "SwitchCase": {
10471
+ const testNode = node.test;
10472
+ if (testNode !== null && testNode !== void 0) {
10473
+ incrementCyclomatic(state);
10474
+ incrementCognitiveFlat(state);
10475
+ }
10476
+ visitChildrenGeneric(node, (child) => visitNode(child, state));
10477
+ return;
10478
+ }
10479
+ case "CatchClause":
10480
+ incrementCyclomatic(state);
10481
+ incrementCognitiveWithNesting(state);
10482
+ incrementNesting(state);
10483
+ visitChildrenGeneric(node, (child) => visitNode(child, state));
10484
+ decrementNesting(state);
10485
+ return;
10486
+ case "ConditionalExpression":
10487
+ incrementCyclomatic(state);
10488
+ incrementCognitiveWithNesting(state);
10489
+ visitChildrenGeneric(node, (child) => visitNode(child, state));
10490
+ return;
10491
+ case "LogicalExpression": {
10492
+ const operator = node.operator;
10493
+ if (operator === "&&" || operator === "||" || operator === "??") {
10494
+ incrementCyclomatic(state);
10495
+ handleLogicalOperator(operator, state);
10496
+ }
10497
+ visitChildrenGeneric(node, (child) => visitNode(child, state));
10498
+ return;
10499
+ }
10500
+ case "AssignmentExpression": {
10501
+ const operator = node.operator;
10502
+ if (operator === "&&=" || operator === "||=" || operator === "??=") incrementCyclomatic(state);
10503
+ visitChildrenGeneric(node, (child) => visitNode(child, state));
10504
+ return;
10505
+ }
10506
+ case "ChainExpression":
10507
+ incrementCyclomatic(state);
10508
+ visitChildrenGeneric(node, (child) => visitNode(child, state));
10509
+ return;
10510
+ default: visitChildrenGeneric(node, (child) => visitNode(child, state));
10511
+ }
10512
+ };
10513
+ const annotateConfidence = (finding, config) => {
10514
+ const breaches = [];
10515
+ if (finding.cyclomatic >= config.cyclomaticThreshold) breaches.push(`cyclomatic ${finding.cyclomatic} ≥ ${config.cyclomaticThreshold}`);
10516
+ if (finding.cognitive >= config.cognitiveThreshold) breaches.push(`cognitive ${finding.cognitive} ≥ ${config.cognitiveThreshold}`);
10517
+ if (finding.paramCount >= config.paramCountThreshold) breaches.push(`paramCount ${finding.paramCount} ≥ ${config.paramCountThreshold}`);
10518
+ if (finding.lineCount >= config.functionLineThreshold) breaches.push(`lineCount ${finding.lineCount} ≥ ${config.functionLineThreshold}`);
10519
+ return {
10520
+ confidence: breaches.length >= 2 ? "high" : "medium",
10521
+ reason: `${finding.functionName} breaches ${breaches.length} threshold${breaches.length === 1 ? "" : "s"}: ${breaches.join(", ")}`
10522
+ };
10523
+ };
10524
+ /**
10525
+ * Per-function cyclomatic + cognitive complexity.
10526
+ *
10527
+ * Cyclomatic (McCabe): 1 + decision points. Counts if/for/while/do/case/catch,
10528
+ * the ?: ternary, &&, ||, ??, &&=/||=/??=, and ?. (optional chaining).
10529
+ *
10530
+ * Cognitive (SonarSource): structural increments with nesting penalty.
10531
+ * Operator-sequence rule: a run of the same logical operator is +1 total;
10532
+ * each operator change adds another +1.
10533
+ *
10534
+ * Returns only functions whose metrics breach at least one threshold from
10535
+ * `config`. Threshold breach count tunes the `confidence` field.
10536
+ */
10537
+ const detectComplexHotspots = (graph, config) => {
10538
+ if (!config?.enabled) return [];
10539
+ const hotspotFindings = [];
10540
+ for (const module of graph.modules) {
10541
+ if (module.isDeclarationFile) continue;
10542
+ if (module.isConfigFile) continue;
10543
+ let sourceText;
10544
+ try {
10545
+ sourceText = (0, node_fs.readFileSync)(module.fileId.path, "utf-8");
10546
+ } catch {
10547
+ continue;
10548
+ }
10549
+ let parseResult;
10550
+ try {
10551
+ parseResult = (0, oxc_parser.parseSync)(module.fileId.path, sourceText);
10552
+ } catch {
10553
+ continue;
10554
+ }
10555
+ const visitState = {
10556
+ filePath: module.fileId.path,
10557
+ lineStarts: computeLineStarts(sourceText),
10558
+ results: [],
10559
+ frameStack: [],
10560
+ pendingFunctionName: void 0
10561
+ };
10562
+ visitNode(parseResult.program, visitState);
10563
+ for (const result of visitState.results) {
10564
+ if (!(result.cyclomatic >= config.cyclomaticThreshold || result.cognitive >= config.cognitiveThreshold || result.paramCount >= config.paramCountThreshold || result.lineCount >= config.functionLineThreshold)) continue;
10565
+ const annotated = annotateConfidence(result, config);
10566
+ hotspotFindings.push({
10567
+ ...result,
10568
+ confidence: annotated.confidence,
10569
+ reason: annotated.reason
10570
+ });
10571
+ }
10572
+ }
10573
+ hotspotFindings.sort((leftFinding, rightFinding) => {
10574
+ const leftScore = leftFinding.cyclomatic + leftFinding.cognitive;
10575
+ const rightScore = rightFinding.cyclomatic + rightFinding.cognitive;
10576
+ if (leftScore !== rightScore) return rightScore - leftScore;
10577
+ if (leftFinding.path !== rightFinding.path) return leftFinding.path.localeCompare(rightFinding.path);
10578
+ return leftFinding.line - rightFinding.line;
10579
+ });
10580
+ return hotspotFindings;
10581
+ };
10582
+
10583
+ //#endregion
10584
+ //#region src/report/private-type-leaks.ts
10585
+ const extractIdentifierName = (node) => {
10586
+ if (!isAstNode(node)) return void 0;
10587
+ if (node.type === "Identifier") {
10588
+ const identifierName = node.name;
10589
+ return typeof identifierName === "string" ? identifierName : void 0;
10590
+ }
10591
+ };
10592
+ const collectTypeReferenceNamesFromTypeNode = (typeNode, into) => {
10593
+ if (!isAstNode(typeNode)) return;
10594
+ if (typeNode.type === "TSTypeReference") {
10595
+ const referencedTypeName = typeNode.typeName;
10596
+ if (isAstNode(referencedTypeName) && referencedTypeName.type === "Identifier") {
10597
+ const name = referencedTypeName.name;
10598
+ if (typeof name === "string") into.add(name);
10599
+ }
10600
+ }
10601
+ for (const key of Object.keys(typeNode)) {
10602
+ if (key === "type" || key === "start" || key === "end") continue;
10603
+ const value = typeNode[key];
10604
+ if (Array.isArray(value)) for (const item of value) collectTypeReferenceNamesFromTypeNode(item, into);
10605
+ else if (value !== null && typeof value === "object") collectTypeReferenceNamesFromTypeNode(value, into);
10606
+ }
10607
+ };
10608
+ const isExportedDeclaration = (statement) => {
10609
+ if (!isAstNode(statement)) return false;
10610
+ return statement.type === "ExportNamedDeclaration" || statement.type === "ExportDefaultDeclaration";
10611
+ };
10612
+ const declarationOf = (statement) => {
10613
+ if (!isAstNode(statement)) return void 0;
10614
+ return statement.declaration;
10615
+ };
10616
+ const exportedNameOfDeclaration = (declarationNode) => {
10617
+ if (!isAstNode(declarationNode)) return void 0;
10618
+ if (declarationNode.type === "FunctionDeclaration" || declarationNode.type === "ClassDeclaration") return extractIdentifierName(declarationNode.id);
10619
+ if (declarationNode.type === "VariableDeclaration") {
10620
+ const declarators = declarationNode.declarations;
10621
+ if (Array.isArray(declarators) && declarators.length > 0) {
10622
+ const firstDeclarator = declarators[0];
10623
+ if (isAstNode(firstDeclarator)) return extractIdentifierName(firstDeclarator.id);
10624
+ }
10625
+ }
10626
+ if (declarationNode.type === "TSInterfaceDeclaration" || declarationNode.type === "TSTypeAliasDeclaration") return extractIdentifierName(declarationNode.id);
10627
+ };
10628
+ const collectFromFunctionLikeSignature = (functionLikeNode, exportName, collected) => {
10629
+ if (!isAstNode(functionLikeNode)) return;
10630
+ const params = functionLikeNode.params;
10631
+ if (Array.isArray(params)) for (const param of params) collectFromParameter(param, exportName, collected);
10632
+ const returnTypeAnnotation = functionLikeNode.returnType;
10633
+ if (isAstNode(returnTypeAnnotation)) {
10634
+ const annotation = returnTypeAnnotation.typeAnnotation;
10635
+ pushTypeReferences(annotation, exportName, collected, returnTypeAnnotation);
10636
+ }
10637
+ };
10638
+ const collectFromParameter = (parameterNode, exportName, collected) => {
10639
+ if (!isAstNode(parameterNode)) return;
10640
+ const annotation = parameterNode.typeAnnotation;
10641
+ if (isAstNode(annotation)) {
10642
+ const innerTypeNode = annotation.typeAnnotation;
10643
+ pushTypeReferences(innerTypeNode, exportName, collected, annotation);
10644
+ }
10645
+ };
10646
+ const pushTypeReferences = (typeNode, exportName, collected, spanFallbackNode) => {
10647
+ if (!isAstNode(typeNode)) return;
10648
+ const referencedTypeNames = /* @__PURE__ */ new Set();
10649
+ collectTypeReferenceNamesFromTypeNode(typeNode, referencedTypeNames);
10650
+ for (const referencedName of referencedTypeNames) {
10651
+ const offset = typeNode.start;
10652
+ const fallbackOffset = isAstNode(spanFallbackNode) && typeof spanFallbackNode.start === "number" ? spanFallbackNode.start : 0;
10653
+ collected.push({
10654
+ exportName,
10655
+ typeName: referencedName,
10656
+ byteOffset: typeof offset === "number" ? offset : fallbackOffset
10657
+ });
10658
+ }
10659
+ };
10660
+ const collectPublicSignatureReferences = (programNode) => {
10661
+ const collected = [];
10662
+ if (!isAstNode(programNode)) return collected;
10663
+ const programBody = programNode.body;
10664
+ if (!Array.isArray(programBody)) return collected;
10665
+ for (const statement of programBody) {
10666
+ if (!isExportedDeclaration(statement)) continue;
10667
+ const declarationNode = declarationOf(statement);
10668
+ if (declarationNode === void 0 || declarationNode === null) continue;
10669
+ const exportedName = exportedNameOfDeclaration(declarationNode);
10670
+ if (!exportedName) continue;
10671
+ if (isAstNode(declarationNode)) {
10672
+ if (declarationNode.type === "FunctionDeclaration" || declarationNode.type === "ArrowFunctionExpression" || declarationNode.type === "FunctionExpression") {
10673
+ collectFromFunctionLikeSignature(declarationNode, exportedName, collected);
10674
+ continue;
10675
+ }
10676
+ if (declarationNode.type === "VariableDeclaration") {
10677
+ const declarators = declarationNode.declarations;
10678
+ if (Array.isArray(declarators)) for (const declarator of declarators) {
10679
+ if (!isAstNode(declarator)) continue;
10680
+ const id = declarator.id;
10681
+ if (isAstNode(id)) {
10682
+ const annotation = id.typeAnnotation;
10683
+ if (isAstNode(annotation)) {
10684
+ const inner = annotation.typeAnnotation;
10685
+ pushTypeReferences(inner, exportedName, collected, annotation);
10686
+ }
10687
+ }
10688
+ const init = declarator.init;
10689
+ if (isAstNode(init) && (init.type === "ArrowFunctionExpression" || init.type === "FunctionExpression")) collectFromFunctionLikeSignature(init, exportedName, collected);
10690
+ }
10691
+ continue;
10692
+ }
10693
+ if (declarationNode.type === "ClassDeclaration") {
10694
+ const classBody = declarationNode.body;
10695
+ if (isAstNode(classBody)) {
10696
+ const members = classBody.body;
10697
+ if (Array.isArray(members)) for (const member of members) {
10698
+ if (!isAstNode(member)) continue;
10699
+ if (member.type === "MethodDefinition") {
10700
+ const value = member.value;
10701
+ collectFromFunctionLikeSignature(value, exportedName, collected);
10702
+ } else if (member.type === "PropertyDefinition") {
10703
+ const annotation = member.typeAnnotation;
10704
+ if (isAstNode(annotation)) {
10705
+ const inner = annotation.typeAnnotation;
10706
+ pushTypeReferences(inner, exportedName, collected, annotation);
10707
+ }
10708
+ }
10709
+ }
10710
+ }
10711
+ }
10712
+ }
10713
+ }
10714
+ return collected;
10715
+ };
10716
+ const collectLocalTypeNames = (programNode) => {
10717
+ const localTypeNames = /* @__PURE__ */ new Set();
10718
+ const exportedNames = /* @__PURE__ */ new Set();
10719
+ if (!isAstNode(programNode)) return {
10720
+ localTypeNames,
10721
+ exportedNames
10722
+ };
10723
+ const programBody = programNode.body;
10724
+ if (!Array.isArray(programBody)) return {
10725
+ localTypeNames,
10726
+ exportedNames
10727
+ };
10728
+ for (const statement of programBody) {
10729
+ if (!isAstNode(statement)) continue;
10730
+ if (statement.type === "TSInterfaceDeclaration" || statement.type === "TSTypeAliasDeclaration") {
10731
+ const name = extractIdentifierName(statement.id);
10732
+ if (name) localTypeNames.add(name);
10733
+ continue;
10734
+ }
10735
+ if (statement.type === "ExportNamedDeclaration") {
10736
+ const declarationNode = statement.declaration;
10737
+ if (isAstNode(declarationNode)) {
10738
+ if (declarationNode.type === "TSInterfaceDeclaration" || declarationNode.type === "TSTypeAliasDeclaration") {
10739
+ const name = extractIdentifierName(declarationNode.id);
10740
+ if (name) exportedNames.add(name);
10741
+ continue;
10742
+ }
10743
+ const declaredName = exportedNameOfDeclaration(declarationNode);
10744
+ if (declaredName) exportedNames.add(declaredName);
10745
+ }
10746
+ const specifiers = statement.specifiers;
10747
+ if (Array.isArray(specifiers)) for (const specifier of specifiers) {
10748
+ if (!isAstNode(specifier)) continue;
10749
+ if (specifier.type === "ExportSpecifier") {
10750
+ const exported = specifier.exported;
10751
+ const exportedNameValue = extractIdentifierName(exported);
10752
+ if (exportedNameValue) exportedNames.add(exportedNameValue);
10753
+ }
10754
+ }
10755
+ }
10756
+ }
10757
+ return {
10758
+ localTypeNames,
10759
+ exportedNames
10760
+ };
10761
+ };
10762
+ /**
10763
+ * Storybook CSF3 convention: a story file declares
10764
+ *
10765
+ * const meta = { ... } satisfies Meta<...>;
10766
+ * export default meta;
10767
+ * type Story = StoryObj<typeof meta>;
10768
+ * export const Primary: Story = { ... };
10769
+ *
10770
+ * `Story` is intentionally a local alias — consumers don't import it; the
10771
+ * Storybook runtime reads the default export. Flagging this as a leak
10772
+ * produces near-100% false positives on Storybook codebases, so skip story
10773
+ * files entirely. Storybook supports both common glob conventions — match
10774
+ * the `*.stories.{ext}` style as well as a bare `stories.{ext}` basename.
10775
+ */
10776
+ const STORYBOOK_STORY_BASENAME_PATTERN = /^(?:.*\.)?stories\.(?:[cm]?ts|[cm]?js|tsx|jsx)$/i;
10777
+ const isStorybookStoryFile = (filePath) => {
10778
+ const lastSlash = filePath.lastIndexOf("/");
10779
+ const basename = lastSlash === -1 ? filePath : filePath.slice(lastSlash + 1);
10780
+ return STORYBOOK_STORY_BASENAME_PATTERN.test(basename);
10781
+ };
10782
+ /**
10783
+ * Detect TypeScript "private type leak": an exported declaration's signature
10784
+ * references a type that was declared locally in the same module but is not
10785
+ * itself exported. Consumers of the export need that type to satisfy the
10786
+ * signature, but cannot import it.
10787
+ *
10788
+ * Skips declaration files (`.d.ts`) — they are pure type modules where this
10789
+ * pattern is the norm. Keeps it simple: doesn't try to chase aliased re-export
10790
+ * paths (deslop-js's broader resolver work covers that elsewhere); a leak
10791
+ * that's actually re-exported gets filtered out at the `exportedNames` set.
10792
+ */
10793
+ const detectPrivateTypeLeaks = (graph) => {
10794
+ const findings = [];
10795
+ for (const module of graph.modules) {
10796
+ if (module.isDeclarationFile) continue;
10797
+ if (module.isConfigFile) continue;
10798
+ if (!module.isReachable) continue;
10799
+ if (isStorybookStoryFile(module.fileId.path)) continue;
10800
+ let sourceText;
10801
+ try {
10802
+ sourceText = (0, node_fs.readFileSync)(module.fileId.path, "utf-8");
10803
+ } catch {
10804
+ continue;
10805
+ }
10806
+ let parseResult;
10807
+ try {
10808
+ parseResult = (0, oxc_parser.parseSync)(module.fileId.path, sourceText);
10809
+ } catch {
10810
+ continue;
10811
+ }
10812
+ const programNode = parseResult.program;
10813
+ const { localTypeNames, exportedNames } = collectLocalTypeNames(programNode);
10814
+ if (localTypeNames.size === 0) continue;
10815
+ const publicSignatureReferences = collectPublicSignatureReferences(programNode);
10816
+ if (publicSignatureReferences.length === 0) continue;
10817
+ const lineStarts = computeLineStarts(sourceText);
10818
+ const seenPairs = /* @__PURE__ */ new Set();
10819
+ for (const reference of publicSignatureReferences) {
10820
+ if (!localTypeNames.has(reference.typeName)) continue;
10821
+ if (exportedNames.has(reference.typeName)) continue;
10822
+ const pairKey = `${reference.exportName}::${reference.typeName}`;
10823
+ if (seenPairs.has(pairKey)) continue;
10824
+ seenPairs.add(pairKey);
10825
+ const { line, column } = offsetToLineColumn(reference.byteOffset, lineStarts);
10826
+ findings.push({
10827
+ path: module.fileId.path,
10828
+ exportName: reference.exportName,
10829
+ typeName: reference.typeName,
10830
+ line,
10831
+ column,
10832
+ confidence: "high",
10833
+ reason: `${reference.exportName}'s signature references ${reference.typeName}, declared locally but not exported — consumers can't satisfy the type without importing it`
10834
+ });
10835
+ }
10836
+ }
10837
+ findings.sort((leftLeak, rightLeak) => {
10838
+ if (leftLeak.path !== rightLeak.path) return leftLeak.path.localeCompare(rightLeak.path);
10839
+ return leftLeak.line - rightLeak.line;
10840
+ });
10841
+ return findings;
10842
+ };
10843
+
10844
+ //#endregion
10845
+ //#region src/report/typescript-smells.ts
10846
+ const parseSource = (filePath) => {
10847
+ let sourceText;
10848
+ try {
10849
+ sourceText = (0, node_fs.readFileSync)(filePath, "utf-8");
10850
+ } catch {
10851
+ return;
10852
+ }
10853
+ let parseResult;
10854
+ try {
10855
+ parseResult = (0, oxc_parser.parseSync)(filePath, sourceText);
10856
+ } catch {
10857
+ return;
10858
+ }
10859
+ const rawComments = parseResult.comments;
10860
+ const comments = Array.isArray(rawComments) ? rawComments.filter(isParsedSourceComment) : [];
10861
+ return {
10862
+ programNode: parseResult.program,
10863
+ sourceText,
10864
+ lineStarts: computeLineStarts(sourceText),
10865
+ comments
10866
+ };
10867
+ };
10868
+ const isParsedSourceComment = (candidate) => {
10869
+ if (typeof candidate !== "object" || candidate === null) return false;
10870
+ const fields = candidate;
10871
+ return (fields.type === "Line" || fields.type === "Block") && typeof fields.value === "string" && typeof fields.start === "number" && typeof fields.end === "number";
10872
+ };
10873
+ const sliceSnippet = (sourceText, start, end) => {
10874
+ const SNIPPET_BUDGET_CHARS = 80;
10875
+ const raw = sourceText.slice(start, Math.min(end, start + SNIPPET_BUDGET_CHARS)).replace(/\s+/g, " ").trim();
10876
+ return end - start > SNIPPET_BUDGET_CHARS ? `${raw}…` : raw;
10877
+ };
10878
+ const isAnyOrUnknownTypeAnnotation = (typeAnnotation) => {
10879
+ if (!isAstNode(typeAnnotation)) return void 0;
10880
+ if (typeAnnotation.type === "TSAnyKeyword") return "any";
10881
+ if (typeAnnotation.type === "TSUnknownKeyword") return "unknown";
10882
+ };
10883
+ const isLiteralLikeNonNull = (expression) => {
10884
+ if (!isAstNode(expression)) return false;
10885
+ if (expression.type === "Literal") return expression.value !== null;
10886
+ if (expression.type === "TemplateLiteral" || expression.type === "ArrayExpression" || expression.type === "ObjectExpression" || expression.type === "FunctionExpression" || expression.type === "ArrowFunctionExpression" || expression.type === "ClassExpression") return true;
10887
+ return false;
10888
+ };
10889
+ const collectUnnecessaryAssertionsInNode = (node, filePath, sourceText, lineStarts, results) => {
10890
+ if (!isAstNode(node)) return;
10891
+ if (node.type === "TSAsExpression" || node.type === "TSSatisfiesExpression") {
10892
+ const innerExpression = node.expression;
10893
+ const typeAnnotation = node.typeAnnotation;
10894
+ if (node.type === "TSAsExpression") {
10895
+ const outerKind = isAnyOrUnknownTypeAnnotation(typeAnnotation);
10896
+ if (outerKind === void 0 && isAstNode(innerExpression) && innerExpression.type === "TSAsExpression") {
10897
+ const innerTypeAnnotation = innerExpression.typeAnnotation;
10898
+ const innerKind = isAnyOrUnknownTypeAnnotation(innerTypeAnnotation);
10899
+ if (innerKind !== void 0) pushAssertion(node, "redundant-double-assertion", `\`x as ${innerKind} as T\` first widens to ${innerKind} just to assert to T — drop the intermediate ${innerKind} and assert directly`, "if you must assert, write `x as T` directly", filePath, sourceText, lineStarts, results);
10900
+ }
10901
+ if (outerKind === "any") pushAssertion(node, "assertion-to-any", "`as any` opts out of TypeScript's type system — narrow to a specific type or use `unknown`", "replace `as any` with the actual type, or use `as unknown as T` only when you genuinely need to discard the inferred type", filePath, sourceText, lineStarts, results);
10902
+ }
10903
+ }
10904
+ if (node.type === "TSTypeAssertion") pushAssertion(node, "angle-bracket-assertion", "`<T>x` style assertion is parsed as a JSX tag in `.tsx` and is deprecated in mixed-extension projects — prefer `x as T`", "rewrite `<T>x` as `x as T`", filePath, sourceText, lineStarts, results);
10905
+ if (node.type === "TSNonNullExpression") {
10906
+ const innerExpression = node.expression;
10907
+ if (isAstNode(innerExpression) && innerExpression.type === "TSNonNullExpression") pushAssertion(node, "double-non-null", "`x!!` is the non-null assertion applied twice — the second `!` is always a no-op", "drop one of the `!` operators", filePath, sourceText, lineStarts, results);
10908
+ else if (isLiteralLikeNonNull(innerExpression)) pushAssertion(node, "redundant-non-null-on-literal", "`!` after a literal / array / object / function expression is redundant — those values are never null", "remove the trailing `!`", filePath, sourceText, lineStarts, results);
10909
+ }
10910
+ };
10911
+ const pushAssertion = (node, kind, reason, suggestion, filePath, sourceText, lineStarts, results) => {
10912
+ if (!isAstNode(node)) return;
10913
+ const startOffset = node.start;
10914
+ const endOffset = node.end;
10915
+ if (typeof startOffset !== "number" || typeof endOffset !== "number") return;
10916
+ const { line, column } = offsetToLineColumn(startOffset, lineStarts);
10917
+ const isHighConfidenceKind = kind === "double-non-null" || kind === "redundant-non-null-on-literal" || kind === "redundant-double-assertion";
10918
+ results.push({
10919
+ path: filePath,
10920
+ kind,
10921
+ snippet: sliceSnippet(sourceText, startOffset, endOffset),
10922
+ line,
10923
+ column,
10924
+ confidence: isHighConfidenceKind ? "high" : "medium",
10925
+ reason,
10926
+ suggestion
10927
+ });
10928
+ };
10929
+ const visitForUnnecessaryAssertions = (node, filePath, sourceText, lineStarts, results) => {
10930
+ if (!isAstNode(node)) return;
10931
+ collectUnnecessaryAssertionsInNode(node, filePath, sourceText, lineStarts, results);
10932
+ for (const propertyKey of Object.keys(node)) {
10933
+ if (propertyKey === "type" || propertyKey === "start" || propertyKey === "end" || propertyKey === "loc" || propertyKey === "range") continue;
10934
+ const value = node[propertyKey];
10935
+ if (Array.isArray(value)) for (const item of value) visitForUnnecessaryAssertions(item, filePath, sourceText, lineStarts, results);
10936
+ else if (value !== null && typeof value === "object") visitForUnnecessaryAssertions(value, filePath, sourceText, lineStarts, results);
10937
+ }
10938
+ };
10939
+ const importExpressionSpecifier = (importExpression) => {
10940
+ if (!isAstNode(importExpression)) return void 0;
10941
+ if (importExpression.type !== "ImportExpression") return void 0;
10942
+ const sourceNode = importExpression.source;
10943
+ if (!isAstNode(sourceNode)) return void 0;
10944
+ if (sourceNode.type !== "Literal") return void 0;
10945
+ const literalValue = sourceNode.value;
10946
+ return typeof literalValue === "string" ? literalValue : void 0;
10947
+ };
10948
+ const findThenImportInExpressionStatement = (expressionNode) => {
10949
+ if (!isAstNode(expressionNode)) return void 0;
10950
+ if (expressionNode.type !== "CallExpression") return void 0;
10951
+ const callee = expressionNode.callee;
10952
+ if (!isAstNode(callee)) return void 0;
10953
+ if (callee.type !== "MemberExpression" && callee.type !== "StaticMemberExpression") return void 0;
10954
+ const propertyNode = callee.property;
10955
+ const propertyName = isAstNode(propertyNode) ? propertyNode.name : void 0;
10956
+ if (propertyName !== "then" && propertyName !== "catch" && propertyName !== "finally") return void 0;
10957
+ const objectNode = callee.object;
10958
+ const specifier = importExpressionSpecifier(objectNode);
10959
+ if (specifier === void 0) return void 0;
10960
+ return {
10961
+ importExpression: objectNode,
10962
+ specifier
10963
+ };
10964
+ };
10965
+ const findAwaitImportInExpression = (expressionNode) => {
10966
+ if (!isAstNode(expressionNode)) return void 0;
10967
+ if (expressionNode.type !== "AwaitExpression") return void 0;
10968
+ const argumentNode = expressionNode.argument;
10969
+ const specifier = importExpressionSpecifier(argumentNode);
10970
+ if (specifier === void 0) return void 0;
10971
+ return {
10972
+ importExpression: argumentNode,
10973
+ specifier
10974
+ };
10975
+ };
10976
+ const collectLazyImportsAtTopLevel = (programNode, filePath, lineStarts, results) => {
10977
+ if (!isAstNode(programNode)) return;
10978
+ const programBody = programNode.body;
10979
+ if (!Array.isArray(programBody)) return;
10980
+ for (const topLevelStatement of programBody) {
10981
+ if (!isAstNode(topLevelStatement)) continue;
10982
+ if (topLevelStatement.type === "VariableDeclaration") {
10983
+ const declarators = topLevelStatement.declarations;
10984
+ if (!Array.isArray(declarators)) continue;
10985
+ for (const declarator of declarators) {
10986
+ if (!isAstNode(declarator)) continue;
10987
+ const initializer = declarator.init;
10988
+ const awaitImport = findAwaitImportInExpression(initializer);
10989
+ if (awaitImport) recordLazyImport(awaitImport, "top-level-await-import", filePath, lineStarts, results);
10990
+ }
10991
+ continue;
10992
+ }
10993
+ if (topLevelStatement.type === "ExpressionStatement") {
10994
+ const innerExpression = topLevelStatement.expression;
10995
+ const awaitImport = findAwaitImportInExpression(innerExpression);
10996
+ if (awaitImport) {
10997
+ recordLazyImport(awaitImport, "top-level-await-import", filePath, lineStarts, results);
10998
+ continue;
10999
+ }
11000
+ const thenImport = findThenImportInExpressionStatement(innerExpression);
11001
+ if (thenImport) recordLazyImport(thenImport, "top-level-then-import", filePath, lineStarts, results);
11002
+ }
11003
+ }
11004
+ };
11005
+ const recordLazyImport = (match, kind, filePath, lineStarts, results) => {
11006
+ if (!isAstNode(match.importExpression)) return;
11007
+ const startOffset = match.importExpression.start;
11008
+ if (typeof startOffset !== "number") return;
11009
+ const { line, column } = offsetToLineColumn(startOffset, lineStarts);
11010
+ results.push({
11011
+ path: filePath,
11012
+ specifier: match.specifier,
11013
+ kind,
11014
+ line,
11015
+ column,
11016
+ confidence: kind === "top-level-await-import" ? "high" : "medium",
11017
+ reason: kind === "top-level-await-import" ? `top-level \`await import("${match.specifier}")\` runs synchronously before the module finishes loading anyway — there is no laziness benefit, prefer a static \`import\`` : `top-level \`import("${match.specifier}").then(...)\` runs at module evaluation — prefer a static \`import\` and a regular function call unless the dynamic-import contract is intentional`
11018
+ });
11019
+ };
11020
+ const buildPackageJsonTypeCache = () => {
11021
+ const directoryToType = /* @__PURE__ */ new Map();
11022
+ const resolveModuleType = (filePath) => {
11023
+ let currentDirectory = (0, node_path.dirname)((0, node_path.resolve)(filePath));
11024
+ const visitedDirectories = [];
11025
+ while (true) {
11026
+ visitedDirectories.push(currentDirectory);
11027
+ const cached = directoryToType.get(currentDirectory);
11028
+ if (cached !== void 0) {
11029
+ for (const visitedDirectory of visitedDirectories) directoryToType.set(visitedDirectory, cached);
11030
+ return cached;
11031
+ }
11032
+ const packageJsonPath = (0, node_path.join)(currentDirectory, "package.json");
11033
+ if ((0, node_fs.existsSync)(packageJsonPath)) try {
11034
+ const packageJson = JSON.parse((0, node_fs.readFileSync)(packageJsonPath, "utf-8"));
11035
+ const moduleType = packageJson.type === "module" ? "module" : packageJson.type === "commonjs" ? "commonjs" : void 0;
11036
+ for (const visitedDirectory of visitedDirectories) directoryToType.set(visitedDirectory, moduleType);
11037
+ return moduleType;
11038
+ } catch {
11039
+ for (const visitedDirectory of visitedDirectories) directoryToType.set(visitedDirectory, void 0);
11040
+ return;
11041
+ }
11042
+ const parentDirectory = (0, node_path.dirname)(currentDirectory);
11043
+ if (parentDirectory === currentDirectory) {
11044
+ for (const visitedDirectory of visitedDirectories) directoryToType.set(visitedDirectory, void 0);
11045
+ return;
11046
+ }
11047
+ currentDirectory = parentDirectory;
11048
+ }
11049
+ };
11050
+ return { resolveModuleType };
11051
+ };
11052
+ const isEsmFilePath = (filePath, typeCache) => {
11053
+ if (filePath.endsWith(".mts") || filePath.endsWith(".mjs")) return true;
11054
+ if (filePath.endsWith(".cts") || filePath.endsWith(".cjs")) return false;
11055
+ return typeCache.resolveModuleType(filePath) === "module";
11056
+ };
11057
+ const collectCommonjsInEsm = (programNode, filePath, sourceText, lineStarts, results) => {
11058
+ if (!isAstNode(programNode)) return;
11059
+ visitForCommonjs(programNode, filePath, sourceText, lineStarts, results);
11060
+ };
11061
+ const visitForCommonjs = (node, filePath, sourceText, lineStarts, results) => {
11062
+ if (!isAstNode(node)) return;
11063
+ if (node.type === "CallExpression") {
11064
+ const callee = node.callee;
11065
+ if (isAstNode(callee) && callee.type === "Identifier") {
11066
+ if (callee.name === "require") {
11067
+ const callArguments = node.arguments;
11068
+ if (Array.isArray(callArguments) && callArguments.length > 0) {
11069
+ const firstArgument = callArguments[0];
11070
+ if (isAstNode(firstArgument) && firstArgument.type === "Literal" && typeof firstArgument.value === "string") {
11071
+ const startOffset = node.start;
11072
+ const endOffset = node.end;
11073
+ if (typeof startOffset === "number" && typeof endOffset === "number") {
11074
+ const { line, column } = offsetToLineColumn(startOffset, lineStarts);
11075
+ results.push({
11076
+ path: filePath,
11077
+ kind: "require",
11078
+ line,
11079
+ column,
11080
+ confidence: "high",
11081
+ reason: "synchronous `require()` is unavailable in native ESM — use a static `import` or top-level `await import()`",
11082
+ snippet: sliceSnippet(sourceText, startOffset, endOffset)
11083
+ });
11084
+ }
11085
+ }
11086
+ }
11087
+ }
11088
+ }
11089
+ }
11090
+ if (node.type === "AssignmentExpression") {
11091
+ const leftSide = node.left;
11092
+ if (isAstNode(leftSide)) {
11093
+ if (leftSide.type === "MemberExpression" || leftSide.type === "StaticMemberExpression") {
11094
+ const objectNode = leftSide.object;
11095
+ const propertyNode = leftSide.property;
11096
+ const objectName = isAstNode(objectNode) ? objectNode.name : void 0;
11097
+ const propertyName = isAstNode(propertyNode) ? propertyNode.name : void 0;
11098
+ if (objectName === "module" && propertyName === "exports") {
11099
+ const startOffset = node.start;
11100
+ const endOffset = node.end;
11101
+ if (typeof startOffset === "number" && typeof endOffset === "number") {
11102
+ const { line, column } = offsetToLineColumn(startOffset, lineStarts);
11103
+ results.push({
11104
+ path: filePath,
11105
+ kind: "module-exports",
11106
+ line,
11107
+ column,
11108
+ confidence: "high",
11109
+ reason: "`module.exports = ...` is CommonJS — replace with `export default` or named `export` for ESM",
11110
+ snippet: sliceSnippet(sourceText, startOffset, endOffset)
11111
+ });
11112
+ }
11113
+ } else if (objectName === "exports") {
11114
+ const startOffset = node.start;
11115
+ const endOffset = node.end;
11116
+ if (typeof startOffset === "number" && typeof endOffset === "number") {
11117
+ const { line, column } = offsetToLineColumn(startOffset, lineStarts);
11118
+ results.push({
11119
+ path: filePath,
11120
+ kind: "exports-assignment",
11121
+ line,
11122
+ column,
11123
+ confidence: "high",
11124
+ reason: "`exports.x = ...` is CommonJS — replace with a named `export` for ESM",
11125
+ snippet: sliceSnippet(sourceText, startOffset, endOffset)
11126
+ });
11127
+ }
11128
+ }
11129
+ }
11130
+ }
11131
+ }
11132
+ for (const propertyKey of Object.keys(node)) {
11133
+ if (propertyKey === "type" || propertyKey === "start" || propertyKey === "end" || propertyKey === "loc" || propertyKey === "range") continue;
11134
+ const value = node[propertyKey];
11135
+ if (Array.isArray(value)) for (const item of value) visitForCommonjs(item, filePath, sourceText, lineStarts, results);
11136
+ else if (value !== null && typeof value === "object") visitForCommonjs(value, filePath, sourceText, lineStarts, results);
11137
+ }
11138
+ };
11139
+ const TS_IGNORE_LEADING = /^\s*@ts-ignore\b/;
11140
+ const TS_NOCHECK_LEADING = /^\s*@ts-nocheck\b/;
11141
+ const TS_EXPECT_ERROR_LEADING = /^\s*@ts-expect-error\b(.*)$/;
11142
+ const collectTypeScriptEscapeHatches = (comments, filePath, lineStarts, results) => {
11143
+ for (const comment of comments) {
11144
+ const commentBody = comment.type === "Block" ? comment.value.split("\n")[0] : comment.value;
11145
+ if (TS_IGNORE_LEADING.test(commentBody)) {
11146
+ pushEscapeHatch(comment.start, "ts-ignore", "`@ts-ignore` silently swallows the next line's type errors forever — use `@ts-expect-error` so the suppression breaks if the underlying error gets fixed", "rewrite as `@ts-expect-error <why this is okay>`", "high", filePath, lineStarts, results);
11147
+ continue;
11148
+ }
11149
+ if (TS_NOCHECK_LEADING.test(commentBody)) {
11150
+ pushEscapeHatch(comment.start, "ts-nocheck", "`@ts-nocheck` disables type checking for the entire file — fix the underlying types or scope the suppression to a specific line", "remove `@ts-nocheck` and address the underlying type errors, or use per-line `@ts-expect-error` with a justification", "medium", filePath, lineStarts, results);
11151
+ continue;
11152
+ }
11153
+ const expectErrorMatch = commentBody.match(TS_EXPECT_ERROR_LEADING);
11154
+ if (expectErrorMatch) {
11155
+ if ((expectErrorMatch[1] ?? "").trim().length === 0) pushEscapeHatch(comment.start, "ts-expect-error-without-explanation", "`@ts-expect-error` should be followed by a comment explaining why the next line legitimately produces a type error", "add a short justification: `// @ts-expect-error: <why this is okay>`", "low", filePath, lineStarts, results);
11156
+ }
11157
+ }
11158
+ };
11159
+ const pushEscapeHatch = (commentStartOffset, kind, reason, suggestion, confidence, filePath, lineStarts, results) => {
11160
+ const { line, column } = offsetToLineColumn(commentStartOffset, lineStarts);
11161
+ results.push({
11162
+ path: filePath,
11163
+ kind,
11164
+ line,
11165
+ column,
11166
+ confidence,
11167
+ reason,
11168
+ suggestion
11169
+ });
11170
+ };
11171
+ const isTypeScriptOrJsFile = (filePath) => filePath.endsWith(".ts") || filePath.endsWith(".tsx") || filePath.endsWith(".mts") || filePath.endsWith(".cts") || filePath.endsWith(".js") || filePath.endsWith(".jsx") || filePath.endsWith(".mjs") || filePath.endsWith(".cjs");
11172
+ const isTypeScriptFileExtension = (filePath) => filePath.endsWith(".ts") || filePath.endsWith(".tsx") || filePath.endsWith(".mts") || filePath.endsWith(".cts");
11173
+ const detectTypeScriptSmells = (graph) => {
11174
+ const unnecessaryAssertions = [];
11175
+ const lazyImportsAtTopLevel = [];
11176
+ const commonjsInEsm = [];
11177
+ const typeScriptEscapeHatches = [];
11178
+ const packageJsonTypeCache = buildPackageJsonTypeCache();
11179
+ for (const module of graph.modules) {
11180
+ if (module.isDeclarationFile) continue;
11181
+ const filePath = module.fileId.path;
11182
+ if (!isTypeScriptOrJsFile(filePath)) continue;
11183
+ const parsedSource = parseSource(filePath);
11184
+ if (!parsedSource) continue;
11185
+ if (isTypeScriptFileExtension(filePath)) {
11186
+ visitForUnnecessaryAssertions(parsedSource.programNode, filePath, parsedSource.sourceText, parsedSource.lineStarts, unnecessaryAssertions);
11187
+ collectTypeScriptEscapeHatches(parsedSource.comments, filePath, parsedSource.lineStarts, typeScriptEscapeHatches);
11188
+ }
11189
+ collectLazyImportsAtTopLevel(parsedSource.programNode, filePath, parsedSource.lineStarts, lazyImportsAtTopLevel);
11190
+ if (isEsmFilePath(filePath, packageJsonTypeCache)) collectCommonjsInEsm(parsedSource.programNode, filePath, parsedSource.sourceText, parsedSource.lineStarts, commonjsInEsm);
11191
+ }
11192
+ unnecessaryAssertions.sort((leftFinding, rightFinding) => {
11193
+ if (leftFinding.path !== rightFinding.path) return leftFinding.path.localeCompare(rightFinding.path);
11194
+ return leftFinding.line - rightFinding.line;
11195
+ });
11196
+ lazyImportsAtTopLevel.sort((leftFinding, rightFinding) => {
11197
+ if (leftFinding.path !== rightFinding.path) return leftFinding.path.localeCompare(rightFinding.path);
11198
+ return leftFinding.line - rightFinding.line;
11199
+ });
11200
+ commonjsInEsm.sort((leftFinding, rightFinding) => {
11201
+ if (leftFinding.path !== rightFinding.path) return leftFinding.path.localeCompare(rightFinding.path);
11202
+ return leftFinding.line - rightFinding.line;
11203
+ });
11204
+ typeScriptEscapeHatches.sort((leftFinding, rightFinding) => {
11205
+ if (leftFinding.path !== rightFinding.path) return leftFinding.path.localeCompare(rightFinding.path);
11206
+ return leftFinding.line - rightFinding.line;
11207
+ });
11208
+ return {
11209
+ unnecessaryAssertions,
11210
+ lazyImportsAtTopLevel,
11211
+ commonjsInEsm,
11212
+ typeScriptEscapeHatches
11213
+ };
11214
+ };
11215
+
11216
+ //#endregion
11217
+ //#region src/utils/run-safe-detector.ts
11218
+ const runSafeDetector = (input) => {
11219
+ try {
11220
+ return input.detector();
11221
+ } catch (caughtError) {
11222
+ input.errorSink.push(new DetectorError({
11223
+ module: input.module,
11224
+ message: `${input.detectorName} threw ${input.contextDescription}`,
11225
+ detail: describeUnknownError(caughtError)
11226
+ }));
11227
+ return input.fallback;
11228
+ }
11229
+ };
11230
+
11231
+ //#endregion
11232
+ //#region src/semantic/program.ts
11233
+ const failureFor = (reason, message, options = { rootDir: "" }) => {
11234
+ return {
11235
+ reason,
11236
+ message,
11237
+ error: new TypeScriptError({
11238
+ code: {
11239
+ "no-tsconfig": "tsconfig-not-found",
11240
+ "tsconfig-parse-error": "tsconfig-parse-failed",
11241
+ "program-creation-failed": "ts-program-creation-failed",
11242
+ "too-many-files": "ts-program-too-large",
11243
+ "typescript-load-failed": "ts-not-loadable"
11244
+ }[reason],
11245
+ severity: reason === "no-tsconfig" ? "info" : "warning",
11246
+ message,
11247
+ path: options.rootDir || void 0,
11248
+ detail: options.detail
11249
+ })
11250
+ };
11251
+ };
11252
+ const findNearestTsconfig = (rootDir, explicitPath) => {
11253
+ if (explicitPath) {
11254
+ const absoluteExplicit = (0, node_path.resolve)(rootDir, explicitPath);
11255
+ if ((0, node_fs.existsSync)(absoluteExplicit)) return absoluteExplicit;
11256
+ return;
11257
+ }
11258
+ for (const candidateName of DEFAULT_SEMANTIC_TSCONFIG_NAMES) {
11259
+ const candidatePath = (0, node_path.resolve)(rootDir, candidateName);
11260
+ if ((0, node_fs.existsSync)(candidatePath)) return candidatePath;
11261
+ }
11262
+ };
11263
+ const createSemanticContext = (rootDir, tsconfigPath) => {
11264
+ const resolvedTsconfigPath = findNearestTsconfig(rootDir, tsconfigPath);
11265
+ if (!resolvedTsconfigPath) return {
11266
+ ok: false,
11267
+ failure: failureFor("no-tsconfig", `No tsconfig found under ${rootDir}`, { rootDir })
11268
+ };
11269
+ let configFileContent;
11270
+ try {
11271
+ configFileContent = typescript.default.readConfigFile(resolvedTsconfigPath, typescript.default.sys.readFile);
8534
11272
  } catch (readError) {
8535
11273
  return {
8536
11274
  ok: false,
@@ -8915,6 +11653,55 @@ const detectUnusedEnumMembers = (graph, config, context, referenceIndex) => {
8915
11653
  return findings;
8916
11654
  };
8917
11655
 
11656
+ //#endregion
11657
+ //#region src/utils/is-framework-lifecycle-method.ts
11658
+ /**
11659
+ * Methods invoked by-name by React / Angular runtimes. Static "no caller"
11660
+ * analysis can't see those call sites, so without this allowlist
11661
+ * `unusedClassMembers` would fire on every component.
11662
+ */
11663
+ const FRAMEWORK_LIFECYCLE_METHODS = new Set([
11664
+ "render",
11665
+ "componentDidMount",
11666
+ "componentDidUpdate",
11667
+ "componentWillUnmount",
11668
+ "shouldComponentUpdate",
11669
+ "getSnapshotBeforeUpdate",
11670
+ "getDerivedStateFromProps",
11671
+ "getDerivedStateFromError",
11672
+ "componentDidCatch",
11673
+ "componentWillMount",
11674
+ "componentWillReceiveProps",
11675
+ "componentWillUpdate",
11676
+ "UNSAFE_componentWillMount",
11677
+ "UNSAFE_componentWillReceiveProps",
11678
+ "UNSAFE_componentWillUpdate",
11679
+ "getChildContext",
11680
+ "contextType",
11681
+ "ngOnInit",
11682
+ "ngOnDestroy",
11683
+ "ngOnChanges",
11684
+ "ngDoCheck",
11685
+ "ngAfterContentInit",
11686
+ "ngAfterContentChecked",
11687
+ "ngAfterViewInit",
11688
+ "ngAfterViewChecked",
11689
+ "ngAcceptInputType",
11690
+ "canActivate",
11691
+ "canDeactivate",
11692
+ "canActivateChild",
11693
+ "canMatch",
11694
+ "resolve",
11695
+ "intercept",
11696
+ "transform",
11697
+ "validate",
11698
+ "registerOnChange",
11699
+ "registerOnTouched",
11700
+ "writeValue",
11701
+ "setDisabledState"
11702
+ ]);
11703
+ const isFrameworkLifecycleMethod = (name) => FRAMEWORK_LIFECYCLE_METHODS.has(name);
11704
+
8918
11705
  //#endregion
8919
11706
  //#region src/semantic/unused-class-members.ts
8920
11707
  const isClassExported = (declaration) => {
@@ -9046,6 +11833,7 @@ const detectUnusedClassMembers = (graph, config, context, referenceIndex, decora
9046
11833
  if (memberHasExternalReference(memberSymbol, referenceIndex)) continue;
9047
11834
  const memberName = typescript.default.isIdentifier(member.name) ? member.name.text : member.name.getText(sourceFile);
9048
11835
  if (overriddenMemberNames.has(memberName)) continue;
11836
+ if (isFrameworkLifecycleMethod(memberName)) continue;
9049
11837
  const { line: zeroIndexedLine, character: zeroIndexedColumn } = sourceFile.getLineAndCharacterOfPosition(member.getStart(sourceFile));
9050
11838
  const line = zeroIndexedLine + 1;
9051
11839
  const column = zeroIndexedColumn + 1;
@@ -9446,6 +12234,22 @@ const generateReport = (graph, config) => {
9446
12234
  const simplifiableFunctions = config.reportRedundancy ? safeReportDetector("detectSimplifiableFunctions", () => detectSimplifiableFunctions(graph), [], errorSink) : [];
9447
12235
  const simplifiableExpressions = config.reportRedundancy ? safeReportDetector("detectSimplifiableExpressions", () => detectSimplifiableExpressions(graph), [], errorSink) : [];
9448
12236
  const duplicateConstants = config.reportRedundancy ? safeReportDetector("detectDuplicateConstants", () => detectDuplicateConstants(graph), [], errorSink) : [];
12237
+ const crossFileDuplicateExports = config.reportRedundancy ? safeReportDetector("detectCrossFileDuplicateExports", () => detectCrossFileDuplicateExports(graph), [], errorSink) : [];
12238
+ const duplicateBlockResult = safeReportDetector("detectDuplicateBlocks", () => detectDuplicateBlocks(graph, config.duplicateBlocks, config.rootDir), {
12239
+ duplicateBlocks: [],
12240
+ duplicateBlockClusters: [],
12241
+ shadowedDirectoryPairs: []
12242
+ }, errorSink);
12243
+ const reExportCycles = safeReportDetector("detectReExportCycles", () => detectReExportCycles(graph), [], errorSink);
12244
+ const featureFlags = safeReportDetector("detectFeatureFlags", () => detectFeatureFlags(graph, config.featureFlags), [], errorSink);
12245
+ const complexFunctions = safeReportDetector("detectComplexHotspots", () => detectComplexHotspots(graph, config.complexity), [], errorSink);
12246
+ const privateTypeLeaks = safeReportDetector("detectPrivateTypeLeaks", () => detectPrivateTypeLeaks(graph), [], errorSink);
12247
+ const typeScriptSmellsResult = safeReportDetector("detectTypeScriptSmells", () => detectTypeScriptSmells(graph), {
12248
+ unnecessaryAssertions: [],
12249
+ lazyImportsAtTopLevel: [],
12250
+ commonjsInEsm: [],
12251
+ typeScriptEscapeHatches: []
12252
+ }, errorSink);
9449
12253
  let semanticResult;
9450
12254
  try {
9451
12255
  semanticResult = runSemanticAnalysis(graph, config);
@@ -9470,6 +12274,7 @@ const generateReport = (graph, config) => {
9470
12274
  errorSink.push(semanticError);
9471
12275
  }
9472
12276
  const redundantAliases = config.reportRedundancy ? [...syntacticRedundantAliases, ...semanticResult.redundantAliases] : [];
12277
+ if (featureFlags.length > 0) correlateFlagsWithDeadCode(featureFlags, { unusedExports });
9473
12278
  const totalExports = graph.modules.reduce((exportCount, module) => exportCount + module.exports.filter((exportInfo) => !(exportInfo.name === "*" && exportInfo.isNamespaceReExport)).length, 0);
9474
12279
  return {
9475
12280
  unusedFiles,
@@ -9490,6 +12295,18 @@ const generateReport = (graph, config) => {
9490
12295
  simplifiableFunctions,
9491
12296
  simplifiableExpressions,
9492
12297
  duplicateConstants,
12298
+ crossFileDuplicateExports,
12299
+ duplicateBlocks: duplicateBlockResult.duplicateBlocks,
12300
+ duplicateBlockClusters: duplicateBlockResult.duplicateBlockClusters,
12301
+ shadowedDirectoryPairs: duplicateBlockResult.shadowedDirectoryPairs,
12302
+ reExportCycles,
12303
+ featureFlags,
12304
+ complexFunctions,
12305
+ privateTypeLeaks,
12306
+ unnecessaryAssertions: typeScriptSmellsResult.unnecessaryAssertions,
12307
+ lazyImportsAtTopLevel: typeScriptSmellsResult.lazyImportsAtTopLevel,
12308
+ commonjsInEsm: typeScriptSmellsResult.commonjsInEsm,
12309
+ typeScriptEscapeHatches: typeScriptSmellsResult.typeScriptEscapeHatches,
9493
12310
  analysisErrors: errorSink,
9494
12311
  totalFiles: graph.modules.length,
9495
12312
  totalExports,
@@ -9577,18 +12394,53 @@ const detectReactNative = (rootDir, workspacePackages) => {
9577
12394
  *
9578
12395
  * - `reportRedundancy: true` — on because redundancy findings are mostly
9579
12396
  * high-signal and the detectors carry their own confidence tiers.
12397
+ *
12398
+ * - `duplicateBlocks: undefined` — token-based copy-paste detection (suffix
12399
+ * array + LCP) is opt-in. It re-parses every source
12400
+ * file to emit a token stream and adds significant runtime to the scan.
12401
+ * Pass `duplicateBlocks: { enabled: true }` to turn it on.
9580
12402
  */
9581
12403
  const fillSemanticConfig = (semanticOverrides) => {
9582
- if (semanticOverrides === void 0) return void 0;
12404
+ const overrides = semanticOverrides ?? {};
12405
+ return {
12406
+ enabled: overrides.enabled ?? true,
12407
+ reportUnusedTypes: overrides.reportUnusedTypes ?? true,
12408
+ reportUnusedEnumMembers: overrides.reportUnusedEnumMembers ?? true,
12409
+ reportUnusedClassMembers: overrides.reportUnusedClassMembers ?? false,
12410
+ reportRedundantVariableAliases: overrides.reportRedundantVariableAliases ?? true,
12411
+ reportMisclassifiedDependencies: overrides.reportMisclassifiedDependencies ?? true,
12412
+ reportRoundTripAliases: overrides.reportRoundTripAliases ?? true,
12413
+ decoratorAllowlist: overrides.decoratorAllowlist ?? DEFAULT_SEMANTIC_DECORATOR_ALLOWLIST
12414
+ };
12415
+ };
12416
+ const fillDuplicateBlocksConfig = (duplicateBlocksOverrides) => {
12417
+ const overrides = duplicateBlocksOverrides ?? {};
12418
+ return {
12419
+ enabled: overrides.enabled ?? true,
12420
+ mode: overrides.mode ?? "semantic",
12421
+ minTokens: overrides.minTokens ?? 50,
12422
+ minLines: overrides.minLines ?? 5,
12423
+ minOccurrences: overrides.minOccurrences ?? 2,
12424
+ skipLocal: overrides.skipLocal ?? false
12425
+ };
12426
+ };
12427
+ const fillFeatureFlagsConfig = (flagsOverrides) => {
12428
+ const overrides = flagsOverrides ?? {};
12429
+ return {
12430
+ enabled: overrides.enabled ?? true,
12431
+ extraEnvPrefixes: overrides.extraEnvPrefixes ?? [],
12432
+ extraSdkFunctionNames: overrides.extraSdkFunctionNames ?? [],
12433
+ detectConfigObjects: overrides.detectConfigObjects ?? false
12434
+ };
12435
+ };
12436
+ const fillComplexityConfig = (complexityOverrides) => {
12437
+ const overrides = complexityOverrides ?? {};
9583
12438
  return {
9584
- enabled: semanticOverrides.enabled ?? false,
9585
- reportUnusedTypes: semanticOverrides.reportUnusedTypes ?? true,
9586
- reportUnusedEnumMembers: semanticOverrides.reportUnusedEnumMembers ?? true,
9587
- reportUnusedClassMembers: semanticOverrides.reportUnusedClassMembers ?? false,
9588
- reportRedundantVariableAliases: semanticOverrides.reportRedundantVariableAliases ?? true,
9589
- reportMisclassifiedDependencies: semanticOverrides.reportMisclassifiedDependencies ?? true,
9590
- reportRoundTripAliases: semanticOverrides.reportRoundTripAliases ?? true,
9591
- decoratorAllowlist: semanticOverrides.decoratorAllowlist ?? DEFAULT_SEMANTIC_DECORATOR_ALLOWLIST
12439
+ enabled: overrides.enabled ?? true,
12440
+ cyclomaticThreshold: overrides.cyclomaticThreshold ?? 10,
12441
+ cognitiveThreshold: overrides.cognitiveThreshold ?? 15,
12442
+ paramCountThreshold: overrides.paramCountThreshold ?? 5,
12443
+ functionLineThreshold: overrides.functionLineThreshold ?? 80
9592
12444
  };
9593
12445
  };
9594
12446
  const defineConfig = (options) => ({
@@ -9600,7 +12452,10 @@ const defineConfig = (options) => ({
9600
12452
  reportTypes: options.reportTypes ?? false,
9601
12453
  includeEntryExports: options.includeEntryExports ?? false,
9602
12454
  reportRedundancy: options.reportRedundancy ?? true,
9603
- semantic: fillSemanticConfig(options.semantic)
12455
+ semantic: fillSemanticConfig(options.semantic),
12456
+ duplicateBlocks: fillDuplicateBlocksConfig(options.duplicateBlocks),
12457
+ featureFlags: fillFeatureFlagsConfig(options.featureFlags),
12458
+ complexity: fillComplexityConfig(options.complexity)
9604
12459
  });
9605
12460
  const buildEmptyScanResult = (errors, elapsedMs) => ({
9606
12461
  unusedFiles: [],
@@ -9621,6 +12476,18 @@ const buildEmptyScanResult = (errors, elapsedMs) => ({
9621
12476
  simplifiableFunctions: [],
9622
12477
  simplifiableExpressions: [],
9623
12478
  duplicateConstants: [],
12479
+ crossFileDuplicateExports: [],
12480
+ duplicateBlocks: [],
12481
+ duplicateBlockClusters: [],
12482
+ shadowedDirectoryPairs: [],
12483
+ reExportCycles: [],
12484
+ featureFlags: [],
12485
+ complexFunctions: [],
12486
+ privateTypeLeaks: [],
12487
+ unnecessaryAssertions: [],
12488
+ lazyImportsAtTopLevel: [],
12489
+ commonjsInEsm: [],
12490
+ typeScriptEscapeHatches: [],
9624
12491
  analysisErrors: errors,
9625
12492
  totalFiles: 0,
9626
12493
  totalExports: 0,