deslop-js 0.0.10 → 0.0.12

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -35,6 +35,8 @@ let node_fs_promises = require("node:fs/promises");
35
35
  let oxc_parser = require("oxc-parser");
36
36
  let oxc_resolver = require("oxc-resolver");
37
37
  let minimatch = require("minimatch");
38
+ let typescript = require("typescript");
39
+ typescript = __toESM(typescript, 1);
38
40
 
39
41
  //#region src/constants.ts
40
42
  const DEFAULT_EXTENSIONS = [
@@ -292,6 +294,173 @@ const RESOLVER_EXTENSIONS = [
292
294
  ".graphql",
293
295
  ".gql"
294
296
  ];
297
+ const SEMANTIC_MAX_PROGRAM_FILES = 5e3;
298
+ const MAX_PARSE_FILE_SIZE_BYTES = 2e6;
299
+ const MAX_ANALYSIS_ERRORS = 5e3;
300
+ const MAX_ERROR_DETAIL_LENGTH = 1e3;
301
+ const BINARY_DETECTION_SAMPLE_BYTES = 2048;
302
+ const MINIFIED_DETECTION_MIN_BYTES = 5e3;
303
+ /**
304
+ * Numeric literals below 1000 are dominated by indices, counters, small
305
+ * ranges, ports, percentages, and array sizes that coincide by accident
306
+ * (every `MAX_RETRIES = 3` is not a duplicate of every `LIMIT = 3`).
307
+ * 1000 admits real shared constants (timeouts in ms, byte sizes, polling
308
+ * intervals) without producing the noise floor that smaller magnitudes do.
309
+ * NOTE: even at 1000, the rule still produces medium-confidence false
310
+ * positives when constants share a value coincidentally with different
311
+ * names (e.g. `STEP_DELAY_MS` vs `MINIMUM_TOKENS`); the report explicitly
312
+ * downgrades those to `confidence: "medium"`.
313
+ */
314
+ const MIN_NUMERIC_LITERAL_MAGNITUDE_FOR_DUPLICATE = 1e3;
315
+ const DEFAULT_SEMANTIC_DECORATOR_ALLOWLIST = [
316
+ "Component",
317
+ "Injectable",
318
+ "NgModule",
319
+ "Pipe",
320
+ "Directive",
321
+ "Controller",
322
+ "Module",
323
+ "Resolver",
324
+ "Query",
325
+ "Mutation",
326
+ "Get",
327
+ "Post",
328
+ "Put",
329
+ "Patch",
330
+ "Delete",
331
+ "Head",
332
+ "Options",
333
+ "All",
334
+ "Sse",
335
+ "WebSocketGateway",
336
+ "SubscribeMessage"
337
+ ];
338
+ const DEFAULT_SEMANTIC_TSCONFIG_NAMES = [
339
+ "tsconfig.json",
340
+ "tsconfig.app.json",
341
+ "tsconfig.build.json",
342
+ "tsconfig.src.json",
343
+ "jsconfig.json"
344
+ ];
345
+
346
+ //#endregion
347
+ //#region src/errors.ts
348
+ const truncateDetail = (text) => {
349
+ if (text.length <= 1e3) return text;
350
+ return `${text.slice(0, MAX_ERROR_DETAIL_LENGTH)}… [truncated ${text.length - MAX_ERROR_DETAIL_LENGTH} chars]`;
351
+ };
352
+ const describeUnknownError = (caughtValue) => {
353
+ let rawText;
354
+ if (caughtValue instanceof Error) rawText = caughtValue.message || caughtValue.name || "unknown error";
355
+ else if (typeof caughtValue === "string") rawText = caughtValue;
356
+ else try {
357
+ rawText = JSON.stringify(caughtValue);
358
+ } catch {
359
+ rawText = String(caughtValue);
360
+ }
361
+ return truncateDetail(rawText ?? "");
362
+ };
363
+ var DeslopError = class DeslopError extends Error {
364
+ constructor(input) {
365
+ super(input.message);
366
+ this.name = "DeslopError";
367
+ this.code = input.code;
368
+ this.module = input.module;
369
+ this.severity = input.severity ?? "warning";
370
+ if (input.path !== void 0) this.path = input.path;
371
+ if (input.detail !== void 0) this.detail = input.detail;
372
+ }
373
+ toJSON() {
374
+ const payload = {
375
+ name: this.name,
376
+ code: this.code,
377
+ module: this.module,
378
+ severity: this.severity,
379
+ message: this.message
380
+ };
381
+ if (this.path !== void 0) payload.path = this.path;
382
+ if (this.detail !== void 0) payload.detail = this.detail;
383
+ return payload;
384
+ }
385
+ static fromCaught(input) {
386
+ return new DeslopError({
387
+ code: input.code,
388
+ module: input.module,
389
+ severity: input.severity,
390
+ message: input.message,
391
+ path: input.path,
392
+ detail: describeUnknownError(input.caught)
393
+ });
394
+ }
395
+ };
396
+ var ConfigError = class extends DeslopError {
397
+ constructor(input) {
398
+ super({
399
+ ...input,
400
+ code: input.code ?? "config-invalid",
401
+ module: "config",
402
+ severity: input.severity ?? "fatal"
403
+ });
404
+ this.name = "ConfigError";
405
+ }
406
+ };
407
+ var FileReadError = class extends DeslopError {
408
+ constructor(input) {
409
+ super({
410
+ ...input,
411
+ module: "parse"
412
+ });
413
+ this.name = "FileReadError";
414
+ }
415
+ };
416
+ var ParseError = class extends DeslopError {
417
+ constructor(input) {
418
+ super({
419
+ ...input,
420
+ module: "parse"
421
+ });
422
+ this.name = "ParseError";
423
+ }
424
+ };
425
+ var TypeScriptError = class extends DeslopError {
426
+ constructor(input) {
427
+ super({
428
+ ...input,
429
+ module: "semantic"
430
+ });
431
+ this.name = "TypeScriptError";
432
+ }
433
+ };
434
+ var WorkspaceError = class extends DeslopError {
435
+ constructor(input) {
436
+ super({
437
+ ...input,
438
+ module: "collect"
439
+ });
440
+ this.name = "WorkspaceError";
441
+ }
442
+ };
443
+ var ResolverError = class extends DeslopError {
444
+ constructor(input) {
445
+ super({
446
+ ...input,
447
+ code: input.code ?? "resolver-init-failed",
448
+ module: "resolver",
449
+ severity: input.severity ?? "fatal"
450
+ });
451
+ this.name = "ResolverError";
452
+ }
453
+ };
454
+ var DetectorError = class extends DeslopError {
455
+ constructor(input) {
456
+ super({
457
+ ...input,
458
+ code: input.code ?? "detector-failed",
459
+ module: input.module ?? "report"
460
+ });
461
+ this.name = "DetectorError";
462
+ }
463
+ };
295
464
 
296
465
  //#endregion
297
466
  //#region src/utils/line-column.ts
@@ -309,6 +478,1070 @@ const getColumnFromOffset = (source, offset) => {
309
478
  return column;
310
479
  };
311
480
 
481
+ //#endregion
482
+ //#region src/utils/extract-default-export-local-name.ts
483
+ const extractIdentifierFromCallArguments = (expression) => {
484
+ if (expression.type !== "CallExpression") return void 0;
485
+ for (const argument of expression.arguments) {
486
+ if (argument.type === "Identifier" && argument.name) return argument.name;
487
+ if (argument.type === "CallExpression") {
488
+ const nestedName = extractIdentifierFromCallArguments(argument);
489
+ if (nestedName) return nestedName;
490
+ }
491
+ }
492
+ if (expression.callee.type === "CallExpression") return extractIdentifierFromCallArguments(expression.callee);
493
+ };
494
+ const extractDefaultExportLocalName = (declaration) => {
495
+ if (!declaration) return void 0;
496
+ if (declaration.type === "Identifier" && declaration.name) return declaration.name;
497
+ if (declaration.type === "FunctionDeclaration" || declaration.type === "ClassDeclaration") return declaration.id?.name;
498
+ if (declaration.type === "CallExpression") return extractIdentifierFromCallArguments(declaration);
499
+ };
500
+
501
+ //#endregion
502
+ //#region src/utils/detect-redundant-type-pattern.ts
503
+ const isTypeNode = (value) => Boolean(value) && typeof value === "object" && typeof value.type === "string";
504
+ const isEmptyTypeLiteral = (node) => {
505
+ if (node.type !== "TSTypeLiteral") return false;
506
+ const members = node.members;
507
+ return Array.isArray(members) && members.length === 0;
508
+ };
509
+ const typeReferenceName = (node) => {
510
+ if (node.type !== "TSTypeReference") return void 0;
511
+ const typeName = node.typeName;
512
+ if (!typeName || typeName.type !== "Identifier") return void 0;
513
+ return typeName.name;
514
+ };
515
+ const isKeyofOfType = (candidate, expectedReferenceName) => {
516
+ if (candidate.type !== "TSTypeOperator") return false;
517
+ if (candidate.operator !== "keyof") return false;
518
+ const operand = candidate.typeAnnotation;
519
+ if (!operand) return false;
520
+ return typeReferenceName(operand) === expectedReferenceName;
521
+ };
522
+ const isNeverKeyword = (node) => node.type === "TSNeverKeyword";
523
+ const isLiterallyEqualByJson = (left, right) => {
524
+ const stripPositions = (key, value) => {
525
+ if (key === "start" || key === "end") return void 0;
526
+ return value;
527
+ };
528
+ return JSON.stringify(left, stripPositions) === JSON.stringify(right, stripPositions);
529
+ };
530
+ const detectIntersectionWithEmpty = (node) => {
531
+ if (node.type !== "TSIntersectionType") return void 0;
532
+ const operands = node.types;
533
+ if (!Array.isArray(operands) || operands.length < 2) return void 0;
534
+ if (!operands.some(isEmptyTypeLiteral)) return void 0;
535
+ return {
536
+ kind: "intersection-with-empty-object",
537
+ reason: "intersection with `{}` is a no-op; the empty object type does not constrain anything",
538
+ suggestion: "drop the `& {}` term"
539
+ };
540
+ };
541
+ const detectSelfUnion = (node) => {
542
+ if (node.type !== "TSUnionType") return void 0;
543
+ const operands = node.types;
544
+ if (!Array.isArray(operands) || operands.length < 2) return void 0;
545
+ for (let leftIndex = 0; leftIndex < operands.length; leftIndex++) for (let rightIndex = leftIndex + 1; rightIndex < operands.length; rightIndex++) if (isLiterallyEqualByJson(operands[leftIndex], operands[rightIndex])) return {
546
+ kind: "self-union",
547
+ reason: "union contains the same member twice",
548
+ suggestion: "deduplicate the union members"
549
+ };
550
+ };
551
+ const detectSelfIntersection = (node) => {
552
+ if (node.type !== "TSIntersectionType") return void 0;
553
+ const operands = node.types;
554
+ if (!Array.isArray(operands) || operands.length < 2) return void 0;
555
+ for (let leftIndex = 0; leftIndex < operands.length; leftIndex++) for (let rightIndex = leftIndex + 1; rightIndex < operands.length; rightIndex++) if (isLiterallyEqualByJson(operands[leftIndex], operands[rightIndex])) return {
556
+ kind: "self-intersection",
557
+ reason: "intersection contains the same operand twice",
558
+ suggestion: "deduplicate the intersection operands"
559
+ };
560
+ };
561
+ const detectNestedUtility = (node, utilityName, kind) => {
562
+ if (node.type !== "TSTypeReference") return void 0;
563
+ if (typeReferenceName(node) !== utilityName) return void 0;
564
+ const typeArguments = node.typeArguments;
565
+ if (!typeArguments) return void 0;
566
+ const params = typeArguments.params;
567
+ if (!Array.isArray(params) || params.length === 0) return void 0;
568
+ const firstArg = params[0];
569
+ if (firstArg.type !== "TSTypeReference") return void 0;
570
+ if (typeReferenceName(firstArg) !== utilityName) return void 0;
571
+ return {
572
+ kind,
573
+ reason: `${utilityName}<${utilityName}<T>> collapses to ${utilityName}<T>`,
574
+ suggestion: `flatten the nested ${utilityName}<...>`
575
+ };
576
+ };
577
+ const detectPickAllKeys = (node) => {
578
+ if (node.type !== "TSTypeReference") return void 0;
579
+ if (typeReferenceName(node) !== "Pick") return void 0;
580
+ const typeArguments = node.typeArguments;
581
+ if (!typeArguments) return void 0;
582
+ const params = typeArguments.params;
583
+ if (!Array.isArray(params) || params.length !== 2) return void 0;
584
+ const targetType = params[0];
585
+ const keys = params[1];
586
+ const targetName = typeReferenceName(targetType);
587
+ if (!targetName) return void 0;
588
+ if (!isKeyofOfType(keys, targetName)) return void 0;
589
+ return {
590
+ kind: "pick-all-keys",
591
+ reason: `Pick<${targetName}, keyof ${targetName}> is equivalent to ${targetName} itself`,
592
+ suggestion: `replace with ${targetName}`
593
+ };
594
+ };
595
+ const detectOmitNoKeys = (node) => {
596
+ if (node.type !== "TSTypeReference") return void 0;
597
+ if (typeReferenceName(node) !== "Omit") return void 0;
598
+ const typeArguments = node.typeArguments;
599
+ if (!typeArguments) return void 0;
600
+ const params = typeArguments.params;
601
+ if (!Array.isArray(params) || params.length !== 2) return void 0;
602
+ const targetType = params[0];
603
+ const keys = params[1];
604
+ const targetName = typeReferenceName(targetType);
605
+ if (!targetName) return void 0;
606
+ if (!isNeverKeyword(keys)) return void 0;
607
+ return {
608
+ kind: "omit-no-keys",
609
+ reason: `Omit<${targetName}, never> is equivalent to ${targetName} itself`,
610
+ suggestion: `replace with ${targetName}`
611
+ };
612
+ };
613
+ const isZodInferDeclarationMergingExtension = (parentExpression) => {
614
+ if (!parentExpression || parentExpression.type !== "MemberExpression") return false;
615
+ const propertyNode = parentExpression.property;
616
+ if (!propertyNode || propertyNode.type !== "Identifier") return false;
617
+ return propertyNode.name === "infer";
618
+ };
619
+ const isRadixStylePropsAliasExtension = (parentExpression) => {
620
+ if (!parentExpression || parentExpression.type !== "MemberExpression") return false;
621
+ const propertyNode = parentExpression.property;
622
+ if (!propertyNode || propertyNode.type !== "Identifier") return false;
623
+ return propertyNode.name === "Props";
624
+ };
625
+ const detectEmptyInterfaceExtendsOne = (declarationNode) => {
626
+ if (declarationNode.type !== "TSInterfaceDeclaration") return void 0;
627
+ const body = declarationNode.body;
628
+ if (!body || !Array.isArray(body.body) || body.body.length !== 0) return void 0;
629
+ const extendsClauses = declarationNode.extends;
630
+ if (!Array.isArray(extendsClauses) || extendsClauses.length !== 1) return void 0;
631
+ const declarationName = declarationNode.id?.name;
632
+ const parentExpression = extendsClauses[0]?.expression;
633
+ if (isZodInferDeclarationMergingExtension(parentExpression)) return void 0;
634
+ if (isRadixStylePropsAliasExtension(parentExpression)) return void 0;
635
+ const parentName = parentExpression && parentExpression.type === "Identifier" ? parentExpression.name : void 0;
636
+ return {
637
+ kind: "empty-interface-extends-one",
638
+ reason: `interface ${declarationName ?? "<anon>"} extends ${parentName ?? "<base>"} with no new members`,
639
+ suggestion: `replace with \`type ${declarationName ?? "X"} = ${parentName ?? "Base"}\``
640
+ };
641
+ };
642
+ const detectRedundantTypePatternForTypeAnnotation = (typeAnnotation) => {
643
+ if (!isTypeNode(typeAnnotation)) return void 0;
644
+ return detectIntersectionWithEmpty(typeAnnotation) ?? detectSelfUnion(typeAnnotation) ?? detectSelfIntersection(typeAnnotation) ?? detectNestedUtility(typeAnnotation, "Partial", "nested-partial") ?? detectNestedUtility(typeAnnotation, "Readonly", "nested-readonly") ?? detectNestedUtility(typeAnnotation, "Required", "nested-required") ?? detectPickAllKeys(typeAnnotation) ?? detectOmitNoKeys(typeAnnotation);
645
+ };
646
+ const detectRedundantInterfaceDeclaration = (declarationNode) => {
647
+ if (!isTypeNode(declarationNode)) return void 0;
648
+ return detectEmptyInterfaceExtendsOne(declarationNode);
649
+ };
650
+
651
+ //#endregion
652
+ //#region src/utils/oxc-ast-node.ts
653
+ const isOxcAstNode = (value) => Boolean(value) && typeof value === "object" && typeof value.type === "string";
654
+ const getNodeStringField = (node, key) => {
655
+ const value = node[key];
656
+ return typeof value === "string" ? value : void 0;
657
+ };
658
+ const getIdentifierName = (node) => {
659
+ if (!isOxcAstNode(node)) return void 0;
660
+ if (node.type !== "Identifier") return void 0;
661
+ return getNodeStringField(node, "name");
662
+ };
663
+
664
+ //#endregion
665
+ //#region src/utils/detect-identity-wrapper.ts
666
+ const getCalleeText = (calleeNode) => {
667
+ if (calleeNode.type === "Identifier") return getIdentifierName(calleeNode);
668
+ if (calleeNode.type === "MemberExpression") {
669
+ if (calleeNode.computed) return void 0;
670
+ const objectNode = calleeNode.object;
671
+ const propertyNode = calleeNode.property;
672
+ if (!objectNode || !propertyNode) return void 0;
673
+ const objectText = getCalleeText(objectNode);
674
+ const propertyText = propertyNode.type === "Identifier" ? propertyNode.name : void 0;
675
+ if (!objectText || !propertyText) return void 0;
676
+ return `${objectText}.${propertyText}`;
677
+ }
678
+ };
679
+ const collectParameterNames = (parameters) => {
680
+ const names = [];
681
+ let hasRest = false;
682
+ let hasDefault = false;
683
+ let restName;
684
+ for (const parameter of parameters) {
685
+ if (!isOxcAstNode(parameter)) return {
686
+ names,
687
+ hasRest: true,
688
+ hasDefault,
689
+ restName
690
+ };
691
+ if (parameter.type === "RestElement") {
692
+ const restArgument = parameter.argument;
693
+ if (restArgument && restArgument.type === "Identifier") {
694
+ hasRest = true;
695
+ restName = restArgument.name;
696
+ continue;
697
+ }
698
+ return {
699
+ names,
700
+ hasRest: true,
701
+ hasDefault,
702
+ restName
703
+ };
704
+ }
705
+ if (parameter.type === "AssignmentPattern") {
706
+ hasDefault = true;
707
+ return {
708
+ names,
709
+ hasRest,
710
+ hasDefault,
711
+ restName
712
+ };
713
+ }
714
+ if (parameter.type === "Identifier") {
715
+ names.push(parameter.name);
716
+ continue;
717
+ }
718
+ return {
719
+ names: [],
720
+ hasRest,
721
+ hasDefault,
722
+ restName
723
+ };
724
+ }
725
+ return {
726
+ names,
727
+ hasRest,
728
+ hasDefault,
729
+ restName
730
+ };
731
+ };
732
+ const argumentsMatchParameters = (callArguments, parameterNames, restName) => {
733
+ if (restName !== void 0) {
734
+ if (callArguments.length !== 1) return false;
735
+ const onlyArgument = callArguments[0];
736
+ if (!isOxcAstNode(onlyArgument)) return false;
737
+ if (onlyArgument.type !== "SpreadElement") return false;
738
+ const spreadArgumentNode = onlyArgument.argument;
739
+ return Boolean(spreadArgumentNode && spreadArgumentNode.type === "Identifier" && spreadArgumentNode.name === restName);
740
+ }
741
+ if (callArguments.length !== parameterNames.length) return false;
742
+ for (let argumentIndex = 0; argumentIndex < callArguments.length; argumentIndex++) {
743
+ const argumentNode = callArguments[argumentIndex];
744
+ if (!isOxcAstNode(argumentNode)) return false;
745
+ if (argumentNode.type !== "Identifier") return false;
746
+ if (argumentNode.name !== parameterNames[argumentIndex]) return false;
747
+ }
748
+ return true;
749
+ };
750
+ const extractCallExpressionFromBody = (bodyNode) => {
751
+ if (bodyNode.type === "CallExpression") return bodyNode;
752
+ if (bodyNode.type === "BlockStatement") {
753
+ const blockBody = bodyNode.body;
754
+ if (!Array.isArray(blockBody) || blockBody.length !== 1) return void 0;
755
+ const onlyStatement = blockBody[0];
756
+ if (!isOxcAstNode(onlyStatement)) return void 0;
757
+ if (onlyStatement.type !== "ReturnStatement") return void 0;
758
+ const returnedExpression = onlyStatement.argument;
759
+ if (!returnedExpression) return void 0;
760
+ if (returnedExpression.type !== "CallExpression") return void 0;
761
+ return returnedExpression;
762
+ }
763
+ };
764
+ const detectIdentityWrapperFromInitializer = (initializerNode, wrapperName) => {
765
+ if (!isOxcAstNode(initializerNode)) return void 0;
766
+ if (initializerNode.type !== "ArrowFunctionExpression" && initializerNode.type !== "FunctionExpression") return;
767
+ if (initializerNode.async) return void 0;
768
+ if (initializerNode.generator) return void 0;
769
+ const { names: parameterNames, hasRest, hasDefault, restName } = collectParameterNames(initializerNode.params ?? []);
770
+ if (hasDefault) return void 0;
771
+ const bodyNode = initializerNode.body;
772
+ if (!bodyNode) return void 0;
773
+ const callExpression = extractCallExpressionFromBody(bodyNode);
774
+ if (!callExpression) return void 0;
775
+ const calleeNode = callExpression.callee;
776
+ if (!calleeNode) return void 0;
777
+ const calleeText = getCalleeText(calleeNode);
778
+ if (!calleeText) return void 0;
779
+ if (calleeText === wrapperName) return void 0;
780
+ if (!argumentsMatchParameters(callExpression.arguments ?? [], parameterNames, hasRest ? restName : void 0)) return;
781
+ return { wrappedExpression: calleeText };
782
+ };
783
+
784
+ //#endregion
785
+ //#region src/utils/normalize-type-hash.ts
786
+ const POSITION_KEYS = new Set([
787
+ "start",
788
+ "end",
789
+ "loc",
790
+ "range"
791
+ ]);
792
+ const NOISY_KEYS = new Set([
793
+ "decorators",
794
+ "leadingComments",
795
+ "trailingComments",
796
+ "innerComments",
797
+ "directive",
798
+ "optional",
799
+ "computed",
800
+ "static",
801
+ "accessibility",
802
+ "declare",
803
+ "readonly"
804
+ ]);
805
+ const NAME_KEYS_TO_STRIP = new Set(["id"]);
806
+ const sanitizeNode = (input) => {
807
+ if (input === null || input === void 0) return input;
808
+ if (Array.isArray(input)) return input.map((element) => sanitizeNode(element));
809
+ if (typeof input !== "object") return input;
810
+ const record = input;
811
+ const cleaned = {};
812
+ for (const key of Object.keys(record)) {
813
+ if (POSITION_KEYS.has(key)) continue;
814
+ if (NOISY_KEYS.has(key)) continue;
815
+ if (NAME_KEYS_TO_STRIP.has(key)) continue;
816
+ cleaned[key] = sanitizeNode(record[key]);
817
+ }
818
+ if (cleaned.type === "TSTypeLiteral" && Array.isArray(cleaned.members)) cleaned.members = sortMembersByKey(cleaned.members);
819
+ if (cleaned.type === "TSInterfaceBody" && Array.isArray(cleaned.body)) cleaned.body = sortMembersByKey(cleaned.body);
820
+ return cleaned;
821
+ };
822
+ const extractMemberKey = (member) => {
823
+ if (!member || typeof member !== "object") return "";
824
+ const record = member;
825
+ if (record.key) {
826
+ const candidate = record.key.name ?? record.key.value;
827
+ if (candidate === void 0 || candidate === null) return "";
828
+ return String(candidate);
829
+ }
830
+ return `__${record.type ?? ""}__`;
831
+ };
832
+ const sortMembersByKey = (members) => {
833
+ const tagged = members.map((member) => ({
834
+ key: extractMemberKey(member),
835
+ member
836
+ }));
837
+ tagged.sort((leftEntry, rightEntry) => {
838
+ if (leftEntry.key < rightEntry.key) return -1;
839
+ if (leftEntry.key > rightEntry.key) return 1;
840
+ return 0;
841
+ });
842
+ return tagged.map((entry) => entry.member);
843
+ };
844
+ const normalizeTypeAstHash = (typeAnnotation) => {
845
+ const sanitized = sanitizeNode(typeAnnotation);
846
+ return JSON.stringify(sanitized);
847
+ };
848
+
849
+ //#endregion
850
+ //#region src/utils/collect-inline-type-literals.ts
851
+ const isTypeLiteralNode = (node) => node.type === "TSTypeLiteral";
852
+ const buildPreview = (typeLiteralNode) => {
853
+ const members = typeLiteralNode.members ?? [];
854
+ const propertyKeys = [];
855
+ for (const memberCandidate of members) {
856
+ if (!isOxcAstNode(memberCandidate)) continue;
857
+ if (memberCandidate.type !== "TSPropertySignature") continue;
858
+ const keyNode = memberCandidate.key;
859
+ const keyName = keyNode?.name ?? keyNode?.value;
860
+ if (keyName) propertyKeys.push(String(keyName));
861
+ }
862
+ propertyKeys.sort();
863
+ const truncatedKeys = propertyKeys.slice(0, 4);
864
+ const suffix = propertyKeys.length > 4 ? `, +${propertyKeys.length - 4} more` : "";
865
+ return `{ ${truncatedKeys.join(", ")}${suffix} }`;
866
+ };
867
+ const countPropertySignatures = (typeLiteralNode) => {
868
+ const members = typeLiteralNode.members ?? [];
869
+ let signatureCount = 0;
870
+ for (const memberCandidate of members) {
871
+ if (!isOxcAstNode(memberCandidate)) continue;
872
+ if (memberCandidate.type === "TSPropertySignature") signatureCount++;
873
+ }
874
+ return signatureCount;
875
+ };
876
+ const captureIfTypeLiteral = (candidateNode, captures, context, nearestName) => {
877
+ if (!isOxcAstNode(candidateNode)) return;
878
+ if (!isTypeLiteralNode(candidateNode)) return;
879
+ const memberCount = countPropertySignatures(candidateNode);
880
+ if (memberCount < 3) return;
881
+ captures.push({
882
+ structuralHash: `inline:${normalizeTypeAstHash(candidateNode)}`,
883
+ memberCount,
884
+ preview: buildPreview(candidateNode),
885
+ context,
886
+ nearestName,
887
+ startOffset: candidateNode.start ?? 0
888
+ });
889
+ };
890
+ const GENERIC_WRAPPERS_TO_RECURSE = new Set([
891
+ "Array",
892
+ "ReadonlyArray",
893
+ "Promise",
894
+ "Set",
895
+ "ReadonlySet",
896
+ "Map",
897
+ "ReadonlyMap",
898
+ "Record",
899
+ "Partial",
900
+ "Required",
901
+ "Readonly",
902
+ "NonNullable",
903
+ "Awaited"
904
+ ]);
905
+ const inspectAnyTypeNode = (candidateNode, captures, context, nearestName, recursionDepth) => {
906
+ if (!isOxcAstNode(candidateNode)) return;
907
+ if (recursionDepth > 6) return;
908
+ if (isTypeLiteralNode(candidateNode)) {
909
+ captureIfTypeLiteral(candidateNode, captures, context, nearestName);
910
+ const members = candidateNode.members ?? [];
911
+ for (const memberCandidate of members) {
912
+ if (!isOxcAstNode(memberCandidate)) continue;
913
+ if (memberCandidate.type !== "TSPropertySignature") continue;
914
+ const memberKey = memberCandidate.key?.name;
915
+ const nested = memberCandidate.typeAnnotation;
916
+ inspectAnyTypeNode(nested, captures, "interface-property", memberKey ?? nearestName, recursionDepth + 1);
917
+ }
918
+ return;
919
+ }
920
+ if (candidateNode.type === "TSTypeAnnotation") {
921
+ inspectAnyTypeNode(candidateNode.typeAnnotation, captures, context, nearestName, recursionDepth + 1);
922
+ return;
923
+ }
924
+ if (candidateNode.type === "TSArrayType") {
925
+ inspectAnyTypeNode(candidateNode.elementType, captures, context, nearestName, recursionDepth + 1);
926
+ return;
927
+ }
928
+ if (candidateNode.type === "TSUnionType" || candidateNode.type === "TSIntersectionType") {
929
+ const operands = candidateNode.types ?? [];
930
+ for (const operand of operands) inspectAnyTypeNode(operand, captures, context, nearestName, recursionDepth + 1);
931
+ return;
932
+ }
933
+ if (candidateNode.type === "TSTupleType") {
934
+ const elements = candidateNode.elementTypes ?? [];
935
+ for (const element of elements) inspectAnyTypeNode(element, captures, context, nearestName, recursionDepth + 1);
936
+ return;
937
+ }
938
+ if (candidateNode.type === "TSTypeReference") {
939
+ const referenceTypeName = candidateNode.typeName?.name;
940
+ const typeArguments = candidateNode.typeArguments;
941
+ if (referenceTypeName && typeArguments?.params && GENERIC_WRAPPERS_TO_RECURSE.has(referenceTypeName)) for (const param of typeArguments.params) inspectAnyTypeNode(param, captures, context, nearestName, recursionDepth + 1);
942
+ }
943
+ };
944
+ const inspectTypeAnnotation = (typeAnnotationNode, captures, context, nearestName) => {
945
+ inspectAnyTypeNode(typeAnnotationNode, captures, context, nearestName, 0);
946
+ };
947
+ const visitFunctionParameters = (parameters, captures, functionName) => {
948
+ if (!parameters) return;
949
+ for (const parameter of parameters) {
950
+ if (!isOxcAstNode(parameter)) continue;
951
+ const parameterIdentifierName = getIdentifierName(parameter);
952
+ inspectTypeAnnotation(parameter.typeAnnotation, captures, "function-parameter", functionName ? `${functionName}(${parameterIdentifierName ?? "?"})` : parameterIdentifierName);
953
+ }
954
+ };
955
+ const visitFunctionLike = (functionNode, captures, functionName) => {
956
+ const parameters = functionNode.params;
957
+ visitFunctionParameters(parameters, captures, functionName);
958
+ const returnTypeNode = functionNode.returnType;
959
+ if (returnTypeNode) inspectTypeAnnotation(returnTypeNode, captures, "function-return", functionName);
960
+ const bodyNode = functionNode.body;
961
+ if (bodyNode) walkBodyForInlineTypes(bodyNode, captures, functionName);
962
+ };
963
+ const visitVariableDeclaration = (declarationNode, captures, enclosingName) => {
964
+ const declarators = declarationNode.declarations ?? [];
965
+ for (const declarator of declarators) {
966
+ if (!isOxcAstNode(declarator)) continue;
967
+ const declarationName = getIdentifierName(declarator.id);
968
+ inspectTypeAnnotation(declarator.typeAnnotation ?? (declarator.id && isOxcAstNode(declarator.id) ? declarator.id.typeAnnotation : void 0), captures, "variable-annotation", declarationName);
969
+ const initializerNode = declarator.init;
970
+ if (isOxcAstNode(initializerNode)) if (initializerNode.type === "ArrowFunctionExpression" || initializerNode.type === "FunctionExpression") visitFunctionLike(initializerNode, captures, declarationName ?? enclosingName);
971
+ else walkExpressionForInlineTypes(initializerNode, captures, declarationName ?? enclosingName);
972
+ }
973
+ };
974
+ const walkBodyForInlineTypes = (bodyNode, captures, enclosingName, recursionDepth = 0) => {
975
+ if (recursionDepth > 200) return;
976
+ if (!isOxcAstNode(bodyNode)) return;
977
+ const statements = bodyNode.body ?? [];
978
+ if (!Array.isArray(statements)) return;
979
+ for (const statement of statements) {
980
+ if (!isOxcAstNode(statement)) continue;
981
+ if (statement.type === "VariableDeclaration") visitVariableDeclaration(statement, captures, enclosingName);
982
+ else if (statement.type === "FunctionDeclaration") visitFunctionLike(statement, captures, getIdentifierName(statement.id) ?? enclosingName);
983
+ else if (statement.type === "TSTypeAliasDeclaration") {
984
+ const typeAliasName = getIdentifierName(statement.id);
985
+ captureIfTypeLiteral(statement.typeAnnotation, captures, "local-type-alias", typeAliasName);
986
+ } else if (statement.type === "ReturnStatement") walkExpressionForInlineTypes(statement.argument, captures, enclosingName, recursionDepth + 1);
987
+ else if (statement.type === "BlockStatement") walkBodyForInlineTypes(statement, captures, enclosingName, recursionDepth + 1);
988
+ else if (statement.type === "ExpressionStatement") walkExpressionForInlineTypes(statement.expression, captures, enclosingName, recursionDepth + 1);
989
+ }
990
+ };
991
+ const walkExpressionForInlineTypes = (expressionNode, captures, enclosingName, recursionDepth = 0) => {
992
+ if (recursionDepth > 200) return;
993
+ if (!isOxcAstNode(expressionNode)) return;
994
+ if (expressionNode.type === "ArrowFunctionExpression" || expressionNode.type === "FunctionExpression") {
995
+ visitFunctionLike(expressionNode, captures, enclosingName);
996
+ return;
997
+ }
998
+ for (const value of Object.values(expressionNode)) if (Array.isArray(value)) for (const element of value) walkExpressionForInlineTypes(element, captures, enclosingName, recursionDepth + 1);
999
+ else if (isOxcAstNode(value)) walkExpressionForInlineTypes(value, captures, enclosingName, recursionDepth + 1);
1000
+ };
1001
+ const visitTopLevelStatement = (statementNode, captures) => {
1002
+ if (!isOxcAstNode(statementNode)) return;
1003
+ const innerNode = statementNode.type === "ExportNamedDeclaration" || statementNode.type === "ExportDefaultDeclaration" ? statementNode.declaration ?? statementNode : statementNode;
1004
+ const targetNode = isOxcAstNode(innerNode) ? innerNode : statementNode;
1005
+ if (targetNode.type === "FunctionDeclaration") {
1006
+ visitFunctionLike(targetNode, captures, getIdentifierName(targetNode.id));
1007
+ return;
1008
+ }
1009
+ if (targetNode.type === "VariableDeclaration") {
1010
+ visitVariableDeclaration(targetNode, captures, void 0);
1011
+ return;
1012
+ }
1013
+ if (targetNode.type === "ClassDeclaration") {
1014
+ const className = getIdentifierName(targetNode.id);
1015
+ const members = targetNode.body?.body ?? [];
1016
+ for (const memberCandidate of members) {
1017
+ if (!isOxcAstNode(memberCandidate)) continue;
1018
+ const memberKeyName = getIdentifierName(memberCandidate.key);
1019
+ const qualifiedName = className && memberKeyName ? `${className}.${memberKeyName}` : memberKeyName;
1020
+ if (memberCandidate.type === "PropertyDefinition") {
1021
+ inspectTypeAnnotation(memberCandidate.typeAnnotation, captures, "class-property", qualifiedName);
1022
+ continue;
1023
+ }
1024
+ if (memberCandidate.type === "MethodDefinition" || memberCandidate.type === "TSAbstractMethodDefinition") {
1025
+ const methodValue = memberCandidate.value;
1026
+ if (isOxcAstNode(methodValue)) visitFunctionLike(methodValue, captures, qualifiedName);
1027
+ }
1028
+ }
1029
+ return;
1030
+ }
1031
+ if (targetNode.type === "TSInterfaceDeclaration") {
1032
+ const interfaceName = getIdentifierName(targetNode.id);
1033
+ const interfaceMembers = targetNode.body?.body ?? [];
1034
+ for (const memberCandidate of interfaceMembers) {
1035
+ if (!isOxcAstNode(memberCandidate)) continue;
1036
+ if (memberCandidate.type !== "TSPropertySignature") continue;
1037
+ const memberKeyName = getIdentifierName(memberCandidate.key);
1038
+ const qualifiedName = interfaceName && memberKeyName ? `${interfaceName}.${memberKeyName}` : memberKeyName;
1039
+ inspectTypeAnnotation(memberCandidate.typeAnnotation, captures, "interface-property", qualifiedName);
1040
+ }
1041
+ }
1042
+ };
1043
+ const collectInlineTypeLiterals = (programBody) => {
1044
+ const captures = [];
1045
+ for (const statement of programBody) visitTopLevelStatement(statement, captures);
1046
+ return captures;
1047
+ };
1048
+
1049
+ //#endregion
1050
+ //#region src/utils/detect-simplifiable-function.ts
1051
+ const containsAwaitExpression = (node, recursionDepth = 0) => {
1052
+ if (recursionDepth > 30) return false;
1053
+ if (!isOxcAstNode(node)) return false;
1054
+ if (node.type === "AwaitExpression") return true;
1055
+ if (node.type === "FunctionDeclaration" || node.type === "FunctionExpression" || node.type === "ArrowFunctionExpression") return false;
1056
+ for (const value of Object.values(node)) if (Array.isArray(value)) {
1057
+ for (const element of value) if (containsAwaitExpression(element, recursionDepth + 1)) return true;
1058
+ } else if (isOxcAstNode(value)) {
1059
+ if (containsAwaitExpression(value, recursionDepth + 1)) return true;
1060
+ }
1061
+ return false;
1062
+ };
1063
+ const containsCallOrPromiseSurface = (node, recursionDepth = 0) => {
1064
+ if (recursionDepth > 30) return false;
1065
+ if (!isOxcAstNode(node)) return false;
1066
+ if (node.type === "FunctionDeclaration" || node.type === "FunctionExpression" || node.type === "ArrowFunctionExpression") return false;
1067
+ if (node.type === "CallExpression" || node.type === "NewExpression" || node.type === "TaggedTemplateExpression" || node.type === "ThrowStatement" || node.type === "YieldExpression") return true;
1068
+ if (node.type === "MemberExpression") {
1069
+ const objectNode = node.object;
1070
+ if (objectNode && isOxcAstNode(objectNode) && objectNode.type === "Identifier") {
1071
+ if (objectNode.name === "Promise") return true;
1072
+ }
1073
+ }
1074
+ for (const value of Object.values(node)) if (Array.isArray(value)) {
1075
+ for (const element of value) if (containsCallOrPromiseSurface(element, recursionDepth + 1)) return true;
1076
+ } else if (isOxcAstNode(value)) {
1077
+ if (containsCallOrPromiseSurface(value, recursionDepth + 1)) return true;
1078
+ }
1079
+ return false;
1080
+ };
1081
+ const unwrapParenthesizedExpression = (node) => {
1082
+ let current = node;
1083
+ while (current.type === "ParenthesizedExpression") {
1084
+ const inner = current.expression;
1085
+ if (!inner || !isOxcAstNode(inner)) return current;
1086
+ current = inner;
1087
+ }
1088
+ return current;
1089
+ };
1090
+ const isSimpleReturnArgument = (argumentNode) => {
1091
+ if (!isOxcAstNode(argumentNode)) return false;
1092
+ const unwrapped = unwrapParenthesizedExpression(argumentNode);
1093
+ if (unwrapped.type === "BlockStatement") return false;
1094
+ if (unwrapped.type === "ObjectExpression") return false;
1095
+ if (unwrapped.type === "JSXElement") return false;
1096
+ if (unwrapped.type === "JSXFragment") return false;
1097
+ return true;
1098
+ };
1099
+ const detectBlockArrowSingleReturn = (functionNode) => {
1100
+ if (functionNode.type !== "ArrowFunctionExpression") return void 0;
1101
+ if (functionNode.async) return void 0;
1102
+ const bodyNode = functionNode.body;
1103
+ if (!bodyNode || bodyNode.type !== "BlockStatement") return void 0;
1104
+ const statements = bodyNode.body ?? [];
1105
+ if (statements.length !== 1) return void 0;
1106
+ const onlyStatement = statements[0];
1107
+ if (!isOxcAstNode(onlyStatement)) return void 0;
1108
+ if (onlyStatement.type !== "ReturnStatement") return void 0;
1109
+ const returnArgument = onlyStatement.argument;
1110
+ if (!returnArgument) return void 0;
1111
+ if (!isSimpleReturnArgument(returnArgument)) return void 0;
1112
+ return {
1113
+ kind: "block-arrow-single-return",
1114
+ startOffset: functionNode.start ?? 0,
1115
+ reason: "arrow body is a single `return` statement; the block can be replaced by the expression directly",
1116
+ suggestion: "rewrite as `() => expression` without `{}`"
1117
+ };
1118
+ };
1119
+ const detectRedundantAwaitReturn = (functionNode) => {
1120
+ const bodyNode = functionNode.body;
1121
+ if (!bodyNode || bodyNode.type !== "BlockStatement") return void 0;
1122
+ const statements = bodyNode.body ?? [];
1123
+ if (statements.length < 2) return void 0;
1124
+ const penultimate = statements[statements.length - 2];
1125
+ const last = statements[statements.length - 1];
1126
+ if (!isOxcAstNode(penultimate) || !isOxcAstNode(last)) return void 0;
1127
+ if (penultimate.type !== "VariableDeclaration") return void 0;
1128
+ if (last.type !== "ReturnStatement") return void 0;
1129
+ const declarators = penultimate.declarations ?? [];
1130
+ if (declarators.length !== 1) return void 0;
1131
+ const declarator = declarators[0];
1132
+ if (!isOxcAstNode(declarator)) return void 0;
1133
+ const declaredIdentifier = declarator.id;
1134
+ const initializer = declarator.init;
1135
+ if (!declaredIdentifier?.name) return void 0;
1136
+ if (!isOxcAstNode(initializer)) return void 0;
1137
+ if (initializer.type !== "AwaitExpression") return void 0;
1138
+ const returnedArgument = last.argument;
1139
+ if (!isOxcAstNode(returnedArgument)) return void 0;
1140
+ if (returnedArgument.type !== "Identifier") return void 0;
1141
+ if (returnedArgument.name !== declaredIdentifier.name) return void 0;
1142
+ return {
1143
+ kind: "redundant-await-return",
1144
+ startOffset: penultimate.start ?? 0,
1145
+ reason: `\`const ${declaredIdentifier.name} = await …; return ${declaredIdentifier.name};\` can be \`return …;\` (the await is preserved by the implicit promise chain)`,
1146
+ suggestion: `replace the await/assign/return sequence with a single \`return await …\` or \`return …\` if no try/catch wraps it`
1147
+ };
1148
+ };
1149
+ const isAsyncFunction = (functionNode) => Boolean(functionNode.async);
1150
+ const containsPromiseTypeReference = (node, recursionDepth = 0) => {
1151
+ if (recursionDepth > 30) return false;
1152
+ if (!isOxcAstNode(node)) return false;
1153
+ if (node.type === "TSTypeReference") {
1154
+ const typeName = node.typeName;
1155
+ if (typeName?.name === "Promise") return true;
1156
+ if (typeName?.right?.name === "Promise") return true;
1157
+ }
1158
+ for (const value of Object.values(node)) if (Array.isArray(value)) {
1159
+ for (const element of value) if (containsPromiseTypeReference(element, recursionDepth + 1)) return true;
1160
+ } else if (isOxcAstNode(value)) {
1161
+ if (containsPromiseTypeReference(value, recursionDepth + 1)) return true;
1162
+ }
1163
+ return false;
1164
+ };
1165
+ const hasExplicitPromiseReturnType = (functionNode) => {
1166
+ const returnType = functionNode.returnType;
1167
+ if (!returnType || !isOxcAstNode(returnType)) return false;
1168
+ const annotation = returnType.typeAnnotation;
1169
+ if (!annotation || !isOxcAstNode(annotation)) return false;
1170
+ return containsPromiseTypeReference(annotation);
1171
+ };
1172
+ const detectUselessAsync = (functionNode, context) => {
1173
+ if (!isAsyncFunction(functionNode)) return void 0;
1174
+ if (functionNode.type === "ClassDeclaration" || functionNode.type === "MethodDefinition") return;
1175
+ if (context.isMethodContext) return void 0;
1176
+ if (context.isInlineCallback) return void 0;
1177
+ if (hasExplicitPromiseReturnType(functionNode)) return void 0;
1178
+ const bodyNode = functionNode.body;
1179
+ if (!isOxcAstNode(bodyNode)) return void 0;
1180
+ if (containsAwaitExpression(bodyNode)) return void 0;
1181
+ if (containsCallOrPromiseSurface(bodyNode)) return void 0;
1182
+ return {
1183
+ kind: "useless-async-no-await",
1184
+ startOffset: functionNode.start ?? 0,
1185
+ reason: "async function body contains no `await`, no function calls, and no Promise surface — the implicit Promise wrap is purely decorative",
1186
+ suggestion: "drop `async` (caller's existing `await` keeps the type identical) or add an explicit return type"
1187
+ };
1188
+ };
1189
+ const detectSimplifiableFunctionPatterns = (functionNode, context = {}) => {
1190
+ if (!isOxcAstNode(functionNode)) return [];
1191
+ const findings = [];
1192
+ const blockArrow = detectBlockArrowSingleReturn(functionNode);
1193
+ if (blockArrow) findings.push(blockArrow);
1194
+ const awaitReturn = detectRedundantAwaitReturn(functionNode);
1195
+ if (awaitReturn) findings.push(awaitReturn);
1196
+ const uselessAsync = detectUselessAsync(functionNode, context);
1197
+ if (uselessAsync) findings.push(uselessAsync);
1198
+ return findings;
1199
+ };
1200
+
1201
+ //#endregion
1202
+ //#region src/utils/collect-simplifiable-functions.ts
1203
+ const looksLikeFunction = (node) => node.type === "FunctionDeclaration" || node.type === "FunctionExpression" || node.type === "ArrowFunctionExpression";
1204
+ const inferFunctionName = (functionNode, parentContext) => {
1205
+ const declaredId = functionNode.id;
1206
+ if (declaredId?.name) return declaredId.name;
1207
+ return parentContext;
1208
+ };
1209
+ const visitFunctionAndDescend = (functionNode, captures, contextName, recursionDepth, isMethodContext, isInlineCallback) => {
1210
+ const functionName = inferFunctionName(functionNode, contextName);
1211
+ const detections = detectSimplifiableFunctionPatterns(functionNode, {
1212
+ isMethodContext,
1213
+ isInlineCallback
1214
+ });
1215
+ for (const detection of detections) captures.push({
1216
+ kind: detection.kind,
1217
+ functionName,
1218
+ startOffset: detection.startOffset,
1219
+ reason: detection.reason,
1220
+ suggestion: detection.suggestion
1221
+ });
1222
+ const bodyNode = functionNode.body;
1223
+ if (isOxcAstNode(bodyNode)) walkForFunctions(bodyNode, captures, functionName, recursionDepth + 1);
1224
+ const parameters = functionNode.params ?? [];
1225
+ for (const parameter of parameters) if (isOxcAstNode(parameter)) walkForFunctions(parameter, captures, functionName, recursionDepth + 1);
1226
+ };
1227
+ const isObjectMethodShorthand = (node) => (node.type === "Property" || node.type === "ObjectProperty") && node.method === true;
1228
+ const isObjectPropertyAssignment = (node) => (node.type === "Property" || node.type === "ObjectProperty") && node.method !== true;
1229
+ const isCallOrNewExpression = (node) => node.type === "CallExpression" || node.type === "NewExpression";
1230
+ const walkForFunctions = (node, captures, contextName, recursionDepth = 0) => {
1231
+ if (recursionDepth > 200) return;
1232
+ if (looksLikeFunction(node)) {
1233
+ visitFunctionAndDescend(node, captures, contextName, recursionDepth, false, false);
1234
+ return;
1235
+ }
1236
+ let nextContext = contextName;
1237
+ if (node.type === "VariableDeclarator") {
1238
+ const declaredName = getIdentifierName(node.id);
1239
+ if (declaredName) nextContext = declaredName;
1240
+ }
1241
+ if (node.type === "MethodDefinition" || node.type === "PropertyDefinition") {
1242
+ const propertyKeyName = getIdentifierName(node.key);
1243
+ if (propertyKeyName) nextContext = propertyKeyName;
1244
+ }
1245
+ if (node.type === "ClassDeclaration") {
1246
+ const className = getIdentifierName(node.id);
1247
+ if (className) nextContext = className;
1248
+ }
1249
+ if (node.type === "MethodDefinition" || isObjectMethodShorthand(node)) {
1250
+ const methodValue = node.value;
1251
+ if (methodValue && isOxcAstNode(methodValue) && looksLikeFunction(methodValue)) {
1252
+ visitFunctionAndDescend(methodValue, captures, getIdentifierName(node.key) ?? nextContext, recursionDepth + 1, true, false);
1253
+ const keyNode = node.key;
1254
+ if (keyNode && isOxcAstNode(keyNode) && node.computed) walkForFunctions(keyNode, captures, nextContext, recursionDepth + 1);
1255
+ return;
1256
+ }
1257
+ }
1258
+ if (isObjectPropertyAssignment(node)) {
1259
+ const propertyValue = node.value;
1260
+ if (propertyValue && isOxcAstNode(propertyValue) && looksLikeFunction(propertyValue)) {
1261
+ visitFunctionAndDescend(propertyValue, captures, getIdentifierName(node.key) ?? nextContext, recursionDepth + 1, false, true);
1262
+ const keyNode = node.key;
1263
+ if (keyNode && isOxcAstNode(keyNode) && node.computed) walkForFunctions(keyNode, captures, nextContext, recursionDepth + 1);
1264
+ return;
1265
+ }
1266
+ }
1267
+ if (isCallOrNewExpression(node)) {
1268
+ const callee = node.callee;
1269
+ if (callee && isOxcAstNode(callee)) walkForFunctions(callee, captures, nextContext, recursionDepth + 1);
1270
+ const callArguments = node.arguments ?? [];
1271
+ for (const argument of callArguments) {
1272
+ if (!isOxcAstNode(argument)) continue;
1273
+ if (looksLikeFunction(argument)) visitFunctionAndDescend(argument, captures, nextContext, recursionDepth + 1, false, true);
1274
+ else walkForFunctions(argument, captures, nextContext, recursionDepth + 1);
1275
+ }
1276
+ return;
1277
+ }
1278
+ for (const value of Object.values(node)) if (Array.isArray(value)) {
1279
+ for (const element of value) if (isOxcAstNode(element)) walkForFunctions(element, captures, nextContext, recursionDepth + 1);
1280
+ } else if (isOxcAstNode(value)) walkForFunctions(value, captures, nextContext, recursionDepth + 1);
1281
+ };
1282
+ const collectSimplifiableFunctions = (programBody) => {
1283
+ const captures = [];
1284
+ for (const statement of programBody) if (isOxcAstNode(statement)) walkForFunctions(statement, captures, void 0, 0);
1285
+ return captures;
1286
+ };
1287
+
1288
+ //#endregion
1289
+ //#region src/utils/collect-simplifiable-expressions.ts
1290
+ const memberAccessText = (node, depth = 0) => {
1291
+ if (depth > 6) return void 0;
1292
+ if (node.type === "Identifier") return node.name;
1293
+ if (node.type === "ThisExpression") return "this";
1294
+ if (node.type === "MemberExpression") {
1295
+ if (node.computed) return void 0;
1296
+ const objectNode = node.object;
1297
+ const propertyNode = node.property;
1298
+ if (!objectNode || !propertyNode) return void 0;
1299
+ const objectText = memberAccessText(objectNode, depth + 1);
1300
+ const propertyText = propertyNode.type === "Identifier" ? propertyNode.name : void 0;
1301
+ if (!objectText || !propertyText) return void 0;
1302
+ return `${objectText}.${propertyText}`;
1303
+ }
1304
+ };
1305
+ const isBooleanLiteral = (node, expected) => {
1306
+ if (node.type !== "Literal") return false;
1307
+ return node.value === expected;
1308
+ };
1309
+ const detectSelfFallbackTernary = (conditionalNode) => {
1310
+ if (conditionalNode.type !== "ConditionalExpression") return void 0;
1311
+ const testNode = conditionalNode.test;
1312
+ const consequentNode = conditionalNode.consequent;
1313
+ if (!testNode || !consequentNode) return void 0;
1314
+ const testText = memberAccessText(testNode);
1315
+ const consequentText = memberAccessText(consequentNode);
1316
+ if (!testText || !consequentText) return void 0;
1317
+ if (testText !== consequentText) return void 0;
1318
+ return {
1319
+ kind: "self-fallback-ternary",
1320
+ snippet: `${testText} ? ${consequentText} : ...`,
1321
+ startOffset: conditionalNode.start ?? 0,
1322
+ reason: `\`${testText} ? ${testText} : x\` is a self-fallback ternary`,
1323
+ suggestion: `use \`${testText} ?? x\` (nullish-only) or \`${testText} || x\` (falsy fallback) depending on intent`
1324
+ };
1325
+ };
1326
+ const detectTernaryReturnsBoolean = (conditionalNode) => {
1327
+ if (conditionalNode.type !== "ConditionalExpression") return void 0;
1328
+ const consequentNode = conditionalNode.consequent;
1329
+ const alternateNode = conditionalNode.alternate;
1330
+ if (!consequentNode || !alternateNode) return void 0;
1331
+ const isTrueFalse = isBooleanLiteral(consequentNode, true) && isBooleanLiteral(alternateNode, false);
1332
+ const isFalseTrue = isBooleanLiteral(consequentNode, false) && isBooleanLiteral(alternateNode, true);
1333
+ if (!isTrueFalse && !isFalseTrue) return void 0;
1334
+ return {
1335
+ kind: "ternary-returns-boolean",
1336
+ snippet: isTrueFalse ? "cond ? true : false" : "cond ? false : true",
1337
+ startOffset: conditionalNode.start ?? 0,
1338
+ reason: isTrueFalse ? "`cond ? true : false` collapses to `Boolean(cond)`" : "`cond ? false : true` collapses to `!cond`",
1339
+ suggestion: isTrueFalse ? "replace with `Boolean(cond)` or just `cond` when types match" : "replace with `!cond`"
1340
+ };
1341
+ };
1342
+ const isNullLiteral = (node) => node.type === "Literal" && node.value === null;
1343
+ const isUndefinedIdentifier = (node) => node.type === "Identifier" && node.name === "undefined";
1344
+ const detectNullishCoalescingWithNullish = (logicalNode) => {
1345
+ if (logicalNode.type !== "LogicalExpression") return void 0;
1346
+ if (logicalNode.operator !== "??") return void 0;
1347
+ const rightNode = logicalNode.right;
1348
+ if (!rightNode) return void 0;
1349
+ if (!(isNullLiteral(rightNode) || isUndefinedIdentifier(rightNode))) return void 0;
1350
+ const leftNode = logicalNode.left;
1351
+ const leftText = leftNode ? memberAccessText(leftNode) ?? "expr" : "expr";
1352
+ const rightLabel = isNullLiteral(rightNode) ? "null" : "undefined";
1353
+ return {
1354
+ kind: "nullish-coalescing-with-nullish",
1355
+ snippet: `${leftText} ?? ${rightLabel}`,
1356
+ startOffset: logicalNode.start ?? 0,
1357
+ reason: `\`x ?? ${rightLabel}\` looks like a no-op — but may be intentional when a caller's signature requires \`${rightLabel}\` (PropTypes, form-control onChange, etc.)`,
1358
+ suggestion: `if \`x\` is already \`T | ${rightLabel}\`, drop the \`?? ${rightLabel}\`; otherwise keep — the coercion changes the resolved type`
1359
+ };
1360
+ };
1361
+ const detectRedundantNullAndUndefinedCheck = (logicalNode) => {
1362
+ if (logicalNode.type !== "LogicalExpression") return void 0;
1363
+ if (logicalNode.operator !== "&&") return void 0;
1364
+ const leftNode = logicalNode.left;
1365
+ const rightNode = logicalNode.right;
1366
+ if (!leftNode || !rightNode) return void 0;
1367
+ if (leftNode.type !== "BinaryExpression" || rightNode.type !== "BinaryExpression") return void 0;
1368
+ const leftOp = leftNode.operator;
1369
+ const rightOp = rightNode.operator;
1370
+ if (leftOp !== "!==" || rightOp !== "!==") return void 0;
1371
+ const leftLeft = leftNode.left;
1372
+ const leftRight = leftNode.right;
1373
+ const rightLeft = rightNode.left;
1374
+ const rightRight = rightNode.right;
1375
+ if (!leftLeft || !leftRight || !rightLeft || !rightRight) return void 0;
1376
+ const leftLeftText = memberAccessText(leftLeft);
1377
+ const rightLeftText = memberAccessText(rightLeft);
1378
+ if (!leftLeftText || leftLeftText !== rightLeftText) return void 0;
1379
+ const leftRhsIsNull = isNullLiteral(leftRight);
1380
+ const leftRhsIsUndefined = isUndefinedIdentifier(leftRight);
1381
+ const rightRhsIsNull = isNullLiteral(rightRight);
1382
+ const rightRhsIsUndefined = isUndefinedIdentifier(rightRight);
1383
+ if (!(leftRhsIsNull && rightRhsIsUndefined || leftRhsIsUndefined && rightRhsIsNull)) return void 0;
1384
+ return {
1385
+ kind: "redundant-null-and-undefined-check",
1386
+ snippet: `${leftLeftText} !== null && ${leftLeftText} !== undefined`,
1387
+ startOffset: logicalNode.start ?? 0,
1388
+ reason: `\`x !== null && x !== undefined\` is equivalent to \`x != null\` (loose comparison checks both)`,
1389
+ suggestion: `replace with \`${leftLeftText} != null\``
1390
+ };
1391
+ };
1392
+ const detectDoubleBangBoolean = (unaryNode) => {
1393
+ if (unaryNode.type !== "UnaryExpression") return void 0;
1394
+ if (unaryNode.operator !== "!") return void 0;
1395
+ const inner = unaryNode.argument;
1396
+ if (!inner || inner.type !== "UnaryExpression") return void 0;
1397
+ if (inner.operator !== "!") return void 0;
1398
+ const coerced = inner.argument;
1399
+ if (!coerced) return void 0;
1400
+ const coercedText = memberAccessText(coerced) ?? "expr";
1401
+ return {
1402
+ kind: "double-bang-boolean",
1403
+ snippet: `!!${coercedText}`,
1404
+ startOffset: unaryNode.start ?? 0,
1405
+ reason: "`!!x` is a double-negation boolean coercion",
1406
+ suggestion: `replace with \`Boolean(${coercedText})\``
1407
+ };
1408
+ };
1409
+ const visit = (node, captures, depth) => {
1410
+ if (depth > 100) return;
1411
+ const conditionalCapture = detectSelfFallbackTernary(node) ?? detectTernaryReturnsBoolean(node);
1412
+ if (conditionalCapture) captures.push(conditionalCapture);
1413
+ const doubleBangCapture = detectDoubleBangBoolean(node);
1414
+ if (doubleBangCapture) captures.push(doubleBangCapture);
1415
+ const logicalCapture = detectNullishCoalescingWithNullish(node) ?? detectRedundantNullAndUndefinedCheck(node);
1416
+ if (logicalCapture) captures.push(logicalCapture);
1417
+ for (const value of Object.values(node)) if (Array.isArray(value)) {
1418
+ for (const element of value) if (isOxcAstNode(element)) visit(element, captures, depth + 1);
1419
+ } else if (isOxcAstNode(value)) visit(value, captures, depth + 1);
1420
+ };
1421
+ const collectSimplifiableExpressions = (programBody) => {
1422
+ const captures = [];
1423
+ for (const statement of programBody) if (isOxcAstNode(statement)) visit(statement, captures, 0);
1424
+ return captures;
1425
+ };
1426
+
1427
+ //#endregion
1428
+ //#region src/utils/collect-duplicate-constants.ts
1429
+ const FRAMEWORK_RESERVED_CONSTANT_NAMES = new Set([
1430
+ "dynamic",
1431
+ "dynamicParams",
1432
+ "revalidate",
1433
+ "runtime",
1434
+ "fetchCache",
1435
+ "preferredRegion",
1436
+ "maxDuration",
1437
+ "metadata",
1438
+ "viewport",
1439
+ "generateStaticParams",
1440
+ "generateMetadata",
1441
+ "config",
1442
+ "loader",
1443
+ "action",
1444
+ "links",
1445
+ "meta",
1446
+ "headers",
1447
+ "handle",
1448
+ "shouldRevalidate",
1449
+ "ErrorBoundary",
1450
+ "HydrateFallback",
1451
+ "Layout"
1452
+ ]);
1453
+ const isLiteralCandidate = (node) => {
1454
+ if (node.type === "Literal") {
1455
+ const value = node.value;
1456
+ if (typeof value === "string") {
1457
+ if (value.length < 8) return false;
1458
+ return true;
1459
+ }
1460
+ if (typeof value === "number") {
1461
+ if (!Number.isFinite(value)) return false;
1462
+ if (Math.abs(value) < 1e3) return false;
1463
+ return true;
1464
+ }
1465
+ return false;
1466
+ }
1467
+ if (node.type === "TemplateLiteral") {
1468
+ const expressions = node.expressions;
1469
+ if (Array.isArray(expressions) && expressions.length > 0) return false;
1470
+ const quasis = node.quasis;
1471
+ if (!Array.isArray(quasis) || quasis.length === 0) return false;
1472
+ return (quasis[0].value?.cooked ?? "").length >= 8;
1473
+ }
1474
+ if (node.type === "ArrayExpression") {
1475
+ const elements = node.elements ?? [];
1476
+ if (elements.length === 0) return false;
1477
+ for (const element of elements) {
1478
+ if (!isOxcAstNode(element)) return false;
1479
+ if (element.type !== "Literal") return false;
1480
+ }
1481
+ return true;
1482
+ }
1483
+ return false;
1484
+ };
1485
+ const hashLiteralNode = (node) => {
1486
+ if (node.type === "Literal") return `lit:${typeof node.value}:${JSON.stringify(node.value)}`;
1487
+ if (node.type === "TemplateLiteral") {
1488
+ const quasis = node.quasis ?? [];
1489
+ return `tpl:${JSON.stringify(quasis[0]?.value?.cooked ?? "")}`;
1490
+ }
1491
+ if (node.type === "ArrayExpression") return `arr:[${(node.elements ?? []).map((element) => {
1492
+ if (!isOxcAstNode(element)) return "?";
1493
+ if (element.type !== "Literal") return "?";
1494
+ return JSON.stringify(element.value);
1495
+ }).join(",")}]`;
1496
+ return "?";
1497
+ };
1498
+ const previewLiteralNode = (node) => {
1499
+ if (node.type === "Literal") {
1500
+ const value = node.value;
1501
+ if (typeof value === "string") return `"${value.length > 60 ? value.slice(0, 57) + "..." : value}"`;
1502
+ return String(value);
1503
+ }
1504
+ if (node.type === "TemplateLiteral") {
1505
+ const cooked = (node.quasis ?? [])[0]?.value?.cooked ?? "";
1506
+ return `\`${cooked.length > 60 ? cooked.slice(0, 57) + "..." : cooked}\``;
1507
+ }
1508
+ if (node.type === "ArrayExpression") {
1509
+ const elements = node.elements ?? [];
1510
+ return `[${elements.slice(0, 3).map((element) => isOxcAstNode(element) && element.type === "Literal" ? JSON.stringify(element.value) : "?").join(", ")}${elements.length > 3 ? `, +${elements.length - 3} more` : ""}]`;
1511
+ }
1512
+ return "<literal>";
1513
+ };
1514
+ const visitForConstants = (statementNode, candidates) => {
1515
+ if (!isOxcAstNode(statementNode)) return;
1516
+ const inner = (statementNode.type === "ExportNamedDeclaration" || statementNode.type === "ExportDefaultDeclaration") && statementNode.declaration ? statementNode.declaration : statementNode;
1517
+ if (!isOxcAstNode(inner)) return;
1518
+ if (inner.type !== "VariableDeclaration") return;
1519
+ if (inner.kind !== "const") return;
1520
+ const declarators = inner.declarations ?? [];
1521
+ for (const declarator of declarators) {
1522
+ if (!isOxcAstNode(declarator)) continue;
1523
+ const idNode = declarator.id;
1524
+ const initializerNode = declarator.init;
1525
+ if (!idNode || !initializerNode) continue;
1526
+ if (idNode.type !== "Identifier") continue;
1527
+ const constantName = idNode.name;
1528
+ if (!constantName) continue;
1529
+ if (FRAMEWORK_RESERVED_CONSTANT_NAMES.has(constantName)) continue;
1530
+ if (!isLiteralCandidate(initializerNode)) continue;
1531
+ candidates.push({
1532
+ constantName,
1533
+ literalHash: hashLiteralNode(initializerNode),
1534
+ literalPreview: previewLiteralNode(initializerNode),
1535
+ startOffset: declarator.start ?? inner.start ?? 0
1536
+ });
1537
+ }
1538
+ };
1539
+ const collectDuplicateConstantCandidates = (programBody) => {
1540
+ const candidates = [];
1541
+ for (const statement of programBody) visitForConstants(statement, candidates);
1542
+ return candidates;
1543
+ };
1544
+
312
1545
  //#endregion
313
1546
  //#region src/collect/parse.ts
314
1547
  const extractMdxImportsExports = (sourceText) => {
@@ -374,10 +1607,15 @@ const CSS_EXTENSIONS = [
374
1607
  ];
375
1608
  const CSS_IMPORT_PATTERN = /@import\s+(?:url\()?['"]([^'"]+)['"]\)?/g;
376
1609
  const SCSS_USE_FORWARD_PATTERN = /@(?:use|forward)\s+['"]([^'"]+)['"]/g;
1610
+ const TAILWIND_PLUGIN_REFERENCE_PATTERN = /@(?:plugin|reference|config)\s+['"]([^'"]+)['"]/g;
377
1611
  const parseCssImports = (filePath) => {
378
1612
  const sourceText = (0, node_fs.readFileSync)(filePath, "utf-8");
379
1613
  const imports = [];
380
- const patterns = [CSS_IMPORT_PATTERN, SCSS_USE_FORWARD_PATTERN];
1614
+ const patterns = [
1615
+ CSS_IMPORT_PATTERN,
1616
+ SCSS_USE_FORWARD_PATTERN,
1617
+ TAILWIND_PLUGIN_REFERENCE_PATTERN
1618
+ ];
381
1619
  for (const pattern of patterns) {
382
1620
  let match;
383
1621
  pattern.lastIndex = 0;
@@ -398,19 +1636,158 @@ const parseCssImports = (filePath) => {
398
1636
  imports,
399
1637
  exports: [],
400
1638
  memberAccesses: [],
401
- wholeObjectUses: []
1639
+ wholeObjectUses: [],
1640
+ localIdentifierReferences: [],
1641
+ referencedFilenames: [],
1642
+ redundantTypePatterns: [],
1643
+ identityWrappers: [],
1644
+ typeDefinitionHashes: [],
1645
+ inlineTypeLiterals: [],
1646
+ simplifiableFunctions: [],
1647
+ simplifiableExpressions: [],
1648
+ duplicateConstantCandidates: [],
1649
+ errors: []
402
1650
  };
403
1651
  };
404
1652
  const NON_JS_EXTENSIONS = [".graphql", ".gql"];
1653
+ const collectLocalIdentifierReferences = (statements) => {
1654
+ const references = [];
1655
+ const seenNames = /* @__PURE__ */ new Set();
1656
+ const visitNode = (node) => {
1657
+ if (!node || typeof node !== "object") return;
1658
+ const record = node;
1659
+ if (record.type === "Identifier" && typeof record.name === "string") {
1660
+ if (!seenNames.has(record.name)) {
1661
+ seenNames.add(record.name);
1662
+ references.push(record.name);
1663
+ }
1664
+ return;
1665
+ }
1666
+ for (const value of Object.values(record)) if (Array.isArray(value)) for (const innerValue of value) visitNode(innerValue);
1667
+ else if (value && typeof value === "object") visitNode(value);
1668
+ };
1669
+ for (const statement of statements) {
1670
+ if (statement.type === "ImportDeclaration" || statement.type === "ExportNamedDeclaration" || statement.type === "ExportDefaultDeclaration" || statement.type === "ExportAllDeclaration") continue;
1671
+ visitNode(statement);
1672
+ }
1673
+ return references;
1674
+ };
1675
+ const createEmptyParsedSource = () => ({
1676
+ imports: [],
1677
+ exports: [],
1678
+ memberAccesses: [],
1679
+ wholeObjectUses: [],
1680
+ localIdentifierReferences: [],
1681
+ referencedFilenames: [],
1682
+ redundantTypePatterns: [],
1683
+ identityWrappers: [],
1684
+ typeDefinitionHashes: [],
1685
+ inlineTypeLiterals: [],
1686
+ simplifiableFunctions: [],
1687
+ simplifiableExpressions: [],
1688
+ duplicateConstantCandidates: [],
1689
+ errors: []
1690
+ });
1691
+ const stripByteOrderMark = (sourceText) => {
1692
+ if (sourceText.charCodeAt(0) === 65279) return sourceText.slice(1);
1693
+ return sourceText;
1694
+ };
1695
+ const looksLikeBinaryContent = (sourceText) => {
1696
+ const sampleLength = Math.min(sourceText.length, BINARY_DETECTION_SAMPLE_BYTES);
1697
+ let nullByteCount = 0;
1698
+ for (let scanIndex = 0; scanIndex < sampleLength; scanIndex++) {
1699
+ if (sourceText.charCodeAt(scanIndex) === 0) nullByteCount++;
1700
+ if (nullByteCount > 4) return true;
1701
+ }
1702
+ return false;
1703
+ };
1704
+ const looksLikeMinifiedSource = (sourceText) => {
1705
+ if (sourceText.length < 5e3) return false;
1706
+ let newlineCount = 0;
1707
+ for (let scanIndex = 0; scanIndex < sourceText.length; scanIndex++) if (sourceText.charCodeAt(scanIndex) === 10) newlineCount++;
1708
+ return sourceText.length / (newlineCount + 1) > 500;
1709
+ };
1710
+ const safeReadSourceFile = (filePath, errors) => {
1711
+ try {
1712
+ const stats = (0, node_fs.statSync)(filePath);
1713
+ if (stats.size === 0) {
1714
+ errors.push(new FileReadError({
1715
+ code: "file-empty",
1716
+ severity: "info",
1717
+ message: "file is empty — nothing to analyze",
1718
+ path: filePath
1719
+ }));
1720
+ return;
1721
+ }
1722
+ if (stats.size > 2e6) {
1723
+ errors.push(new FileReadError({
1724
+ code: "file-too-large",
1725
+ message: `file size ${stats.size}B exceeds MAX_PARSE_FILE_SIZE_BYTES (${MAX_PARSE_FILE_SIZE_BYTES})`,
1726
+ path: filePath
1727
+ }));
1728
+ return;
1729
+ }
1730
+ } catch (statError) {
1731
+ errors.push(new FileReadError({
1732
+ code: "file-read-failed",
1733
+ message: "could not stat source file",
1734
+ path: filePath,
1735
+ detail: describeUnknownError(statError)
1736
+ }));
1737
+ return;
1738
+ }
1739
+ try {
1740
+ const sourceText = stripByteOrderMark((0, node_fs.readFileSync)(filePath, "utf-8"));
1741
+ if (looksLikeBinaryContent(sourceText)) {
1742
+ errors.push(new FileReadError({
1743
+ code: "file-binary",
1744
+ severity: "info",
1745
+ message: "file appears to be binary — skipping",
1746
+ path: filePath
1747
+ }));
1748
+ return;
1749
+ }
1750
+ if (looksLikeMinifiedSource(sourceText)) {
1751
+ errors.push(new FileReadError({
1752
+ code: "file-minified",
1753
+ severity: "info",
1754
+ message: "file appears to be a minified/bundled artifact — skipping redundancy analysis",
1755
+ path: filePath
1756
+ }));
1757
+ return;
1758
+ }
1759
+ return sourceText;
1760
+ } catch (readError) {
1761
+ errors.push(new FileReadError({
1762
+ code: "file-read-failed",
1763
+ message: "could not read source file",
1764
+ path: filePath,
1765
+ detail: describeUnknownError(readError)
1766
+ }));
1767
+ return;
1768
+ }
1769
+ };
405
1770
  const parseSourceFile = (filePath) => {
406
- if (CSS_EXTENSIONS.some((ext) => filePath.endsWith(ext))) return parseCssImports(filePath);
407
- if (NON_JS_EXTENSIONS.some((ext) => filePath.endsWith(ext))) return {
408
- imports: [],
409
- exports: [],
410
- memberAccesses: [],
411
- wholeObjectUses: []
1771
+ if (CSS_EXTENSIONS.some((ext) => filePath.endsWith(ext))) try {
1772
+ return parseCssImports(filePath);
1773
+ } catch (cssError) {
1774
+ return {
1775
+ ...createEmptyParsedSource(),
1776
+ errors: [new ParseError({
1777
+ code: "parse-failed",
1778
+ message: "CSS import parsing crashed",
1779
+ path: filePath,
1780
+ detail: describeUnknownError(cssError)
1781
+ })]
1782
+ };
1783
+ }
1784
+ if (NON_JS_EXTENSIONS.some((ext) => filePath.endsWith(ext))) return createEmptyParsedSource();
1785
+ const earlyErrors = [];
1786
+ const sourceText = safeReadSourceFile(filePath, earlyErrors);
1787
+ if (sourceText === void 0) return {
1788
+ ...createEmptyParsedSource(),
1789
+ errors: earlyErrors
412
1790
  };
413
- const sourceText = (0, node_fs.readFileSync)(filePath, "utf-8");
414
1791
  const imports = [];
415
1792
  const exports = [];
416
1793
  const isMdx = filePath.endsWith(".mdx");
@@ -419,51 +1796,223 @@ const parseSourceFile = (filePath) => {
419
1796
  const isSvelte = filePath.endsWith(".svelte");
420
1797
  const textToParse = isMdx ? extractMdxImportsExports(sourceText) : isAstro ? extractAstroFrontmatter(sourceText) : isVue ? extractVueScriptContent(sourceText) : isSvelte ? extractSvelteScriptContent(sourceText) : sourceText;
421
1798
  const parseFileName = isMdx || isAstro || isVue || isSvelte ? filePath.replace(/\.(mdx|astro|vue|svelte)$/, ".tsx") : filePath;
422
- let result = (0, oxc_parser.parseSync)(parseFileName, textToParse);
423
- const isPlainJsFile = parseFileName.endsWith(".js") || parseFileName.endsWith(".mjs") || parseFileName.endsWith(".cjs");
424
- if (result.errors.length > 0 && isPlainJsFile && result.errors.some((parseError) => {
425
- const errorMessage = String(parseError.message ?? "");
426
- return errorMessage.includes("JSX") || errorMessage.includes("Unexpected token");
427
- })) result = (0, oxc_parser.parseSync)(parseFileName.replace(/\.(m?js|cjs)$/, ".jsx"), textToParse);
1799
+ let result;
1800
+ try {
1801
+ result = (0, oxc_parser.parseSync)(parseFileName, textToParse);
1802
+ } catch (parseError) {
1803
+ return {
1804
+ ...createEmptyParsedSource(),
1805
+ errors: [...earlyErrors, new ParseError({
1806
+ code: "parse-failed",
1807
+ message: "oxc-parser threw during initial parse",
1808
+ path: filePath,
1809
+ detail: describeUnknownError(parseError)
1810
+ })]
1811
+ };
1812
+ }
1813
+ if ((parseFileName.endsWith(".js") || parseFileName.endsWith(".mjs") || parseFileName.endsWith(".cjs")) && result.errors.length > 0) try {
1814
+ const jsxResult = (0, oxc_parser.parseSync)(parseFileName.replace(/\.(m?js|cjs)$/, ".jsx"), textToParse);
1815
+ if (jsxResult.errors.length === 0) result = jsxResult;
1816
+ else {
1817
+ const tsxResult = (0, oxc_parser.parseSync)(parseFileName.replace(/\.(m?js|cjs)$/, ".tsx"), textToParse);
1818
+ if (tsxResult.errors.length === 0) result = tsxResult;
1819
+ }
1820
+ } catch {}
428
1821
  if (result.errors.length > 0) return {
1822
+ ...createEmptyParsedSource(),
429
1823
  imports,
430
1824
  exports,
431
- memberAccesses: [],
432
- wholeObjectUses: []
1825
+ referencedFilenames: extractReferencedFilenames(sourceText),
1826
+ errors: [...earlyErrors, new ParseError({
1827
+ code: "parse-recovered",
1828
+ severity: "info",
1829
+ message: `oxc-parser reported ${result.errors.length} syntax issue(s); skipping deep analysis for this file`,
1830
+ path: filePath
1831
+ })]
433
1832
  };
434
1833
  const program = result.program;
435
1834
  if (!program?.body) return {
1835
+ ...createEmptyParsedSource(),
436
1836
  imports,
437
1837
  exports,
438
- memberAccesses: [],
439
- wholeObjectUses: []
1838
+ referencedFilenames: extractReferencedFilenames(sourceText),
1839
+ errors: [...earlyErrors, new ParseError({
1840
+ code: "parse-failed",
1841
+ message: "oxc-parser returned no program body",
1842
+ path: filePath
1843
+ })]
440
1844
  };
441
- for (const node of program.body) switch (node.type) {
442
- case "ImportDeclaration":
443
- extractImportDeclaration(node, sourceText, imports);
444
- break;
445
- case "ExportNamedDeclaration":
446
- extractNamedExportDeclaration(node, sourceText, exports);
447
- break;
448
- case "ExportDefaultDeclaration":
449
- extractDefaultExportDeclaration(node, sourceText, exports);
450
- break;
451
- case "ExportAllDeclaration":
452
- extractExportAllDeclaration(node, sourceText, exports);
453
- break;
454
- }
455
- collectDynamicImports(program.body, sourceText, imports);
456
- const namespaceLocalNames = collectNamespaceLocalNames(imports);
457
- const memberAccesses = [];
458
- const wholeObjectUses = [];
459
- if (namespaceLocalNames.size > 0) collectMemberAccesses(program.body, namespaceLocalNames, memberAccesses, wholeObjectUses);
1845
+ const detectorErrors = [];
1846
+ const safeWalk = (walkerName, walker, fallback) => {
1847
+ try {
1848
+ return walker();
1849
+ } catch (walkError) {
1850
+ detectorErrors.push(new ParseError({
1851
+ code: "ast-walk-failed",
1852
+ message: `${walkerName} threw during AST traversal`,
1853
+ path: filePath,
1854
+ detail: describeUnknownError(walkError)
1855
+ }));
1856
+ return fallback;
1857
+ }
1858
+ };
1859
+ safeWalk("extractImportsAndExports", () => {
1860
+ for (const node of program.body) switch (node.type) {
1861
+ case "ImportDeclaration":
1862
+ extractImportDeclaration(node, sourceText, imports);
1863
+ break;
1864
+ case "ExportNamedDeclaration":
1865
+ extractNamedExportDeclaration(node, sourceText, exports);
1866
+ break;
1867
+ case "ExportDefaultDeclaration":
1868
+ extractDefaultExportDeclaration(node, sourceText, exports);
1869
+ break;
1870
+ case "ExportAllDeclaration":
1871
+ extractExportAllDeclaration(node, sourceText, exports);
1872
+ break;
1873
+ }
1874
+ }, void 0);
1875
+ safeWalk("collectDynamicImports", () => {
1876
+ collectDynamicImports(program.body, sourceText, imports);
1877
+ }, void 0);
1878
+ const namespaceLocalNames = collectNamespaceLocalNames(imports);
1879
+ const memberAccesses = [];
1880
+ const wholeObjectUses = [];
1881
+ if (namespaceLocalNames.size > 0) safeWalk("collectMemberAccesses", () => {
1882
+ collectMemberAccesses(program.body, namespaceLocalNames, memberAccesses, wholeObjectUses);
1883
+ }, void 0);
1884
+ const localIdentifierReferences = safeWalk("collectLocalIdentifierReferences", () => collectLocalIdentifierReferences(program.body), []);
1885
+ const redundantTypePatterns = [];
1886
+ const identityWrappers = [];
1887
+ const typeDefinitionHashes = [];
1888
+ safeWalk("collectDryPatterns", () => {
1889
+ collectDryPatterns(program.body, sourceText, redundantTypePatterns, identityWrappers, typeDefinitionHashes);
1890
+ }, void 0);
1891
+ const inlineTypeLiterals = safeWalk("collectInlineTypeLiterals", () => collectInlineTypeLiterals(program.body), []).map((capture) => ({
1892
+ structuralHash: capture.structuralHash,
1893
+ memberCount: capture.memberCount,
1894
+ preview: capture.preview,
1895
+ context: capture.context,
1896
+ nearestName: capture.nearestName,
1897
+ line: getLineFromOffset(sourceText, capture.startOffset),
1898
+ column: getColumnFromOffset(sourceText, capture.startOffset)
1899
+ }));
1900
+ const simplifiableFunctions = safeWalk("collectSimplifiableFunctions", () => collectSimplifiableFunctions(program.body), []).map((capture) => ({
1901
+ kind: capture.kind,
1902
+ functionName: capture.functionName,
1903
+ line: getLineFromOffset(sourceText, capture.startOffset),
1904
+ column: getColumnFromOffset(sourceText, capture.startOffset),
1905
+ reason: capture.reason,
1906
+ suggestion: capture.suggestion
1907
+ }));
1908
+ const simplifiableExpressions = safeWalk("collectSimplifiableExpressions", () => collectSimplifiableExpressions(program.body), []).map((capture) => ({
1909
+ kind: capture.kind,
1910
+ snippet: capture.snippet,
1911
+ line: getLineFromOffset(sourceText, capture.startOffset),
1912
+ column: getColumnFromOffset(sourceText, capture.startOffset),
1913
+ reason: capture.reason,
1914
+ suggestion: capture.suggestion
1915
+ }));
1916
+ const duplicateConstantCandidates = safeWalk("collectDuplicateConstantCandidates", () => collectDuplicateConstantCandidates(program.body), []).map((capture) => ({
1917
+ constantName: capture.constantName,
1918
+ literalHash: capture.literalHash,
1919
+ literalPreview: capture.literalPreview,
1920
+ line: getLineFromOffset(sourceText, capture.startOffset),
1921
+ column: getColumnFromOffset(sourceText, capture.startOffset)
1922
+ }));
460
1923
  return {
461
1924
  imports,
462
1925
  exports,
463
1926
  memberAccesses,
464
- wholeObjectUses
1927
+ wholeObjectUses,
1928
+ localIdentifierReferences,
1929
+ referencedFilenames: extractReferencedFilenames(sourceText),
1930
+ redundantTypePatterns,
1931
+ identityWrappers,
1932
+ typeDefinitionHashes,
1933
+ inlineTypeLiterals,
1934
+ simplifiableFunctions,
1935
+ simplifiableExpressions,
1936
+ duplicateConstantCandidates,
1937
+ errors: [...earlyErrors, ...detectorErrors]
465
1938
  };
466
1939
  };
1940
+ const REFERENCED_FILENAME_LITERAL_PATTERN = /(?<![./@\w-])(?:["'`])([a-z][\w-]*\.(?:ts|tsx|js|jsx|mts|mjs|cts|cjs))(?:["'`])/g;
1941
+ const extractReferencedFilenames = (sourceText) => {
1942
+ const captured = /* @__PURE__ */ new Set();
1943
+ REFERENCED_FILENAME_LITERAL_PATTERN.lastIndex = 0;
1944
+ let match;
1945
+ while ((match = REFERENCED_FILENAME_LITERAL_PATTERN.exec(sourceText)) !== null) captured.add(match[1]);
1946
+ return [...captured];
1947
+ };
1948
+ const collectDryPatterns = (bodyNodes, sourceText, redundantTypePatterns, identityWrappers, typeDefinitionHashes) => {
1949
+ for (const statement of bodyNodes) inspectStatement(statement, sourceText, redundantTypePatterns, identityWrappers, typeDefinitionHashes);
1950
+ };
1951
+ const inspectStatement = (statementNode, sourceText, redundantTypePatterns, identityWrappers, typeDefinitionHashes) => {
1952
+ let declarationOfInterest = statementNode;
1953
+ if (statementNode.type === "ExportNamedDeclaration" && statementNode.declaration) declarationOfInterest = statementNode.declaration;
1954
+ if (declarationOfInterest && typeof declarationOfInterest === "object") {
1955
+ const declarationNode = declarationOfInterest;
1956
+ if (declarationNode.type === "TSTypeAliasDeclaration") {
1957
+ const typeAliasName = declarationNode.id?.name;
1958
+ const typeAnnotation = declarationNode.typeAnnotation;
1959
+ const startOffset = declarationNode.start ?? 0;
1960
+ if (typeAliasName && typeAnnotation) {
1961
+ const redundantPattern = detectRedundantTypePatternForTypeAnnotation(typeAnnotation);
1962
+ if (redundantPattern) redundantTypePatterns.push({
1963
+ typeName: typeAliasName,
1964
+ kind: redundantPattern.kind,
1965
+ line: getLineFromOffset(sourceText, startOffset),
1966
+ column: getColumnFromOffset(sourceText, startOffset),
1967
+ reason: redundantPattern.reason,
1968
+ suggestion: redundantPattern.suggestion
1969
+ });
1970
+ typeDefinitionHashes.push({
1971
+ typeName: typeAliasName,
1972
+ structuralHash: `alias:${normalizeTypeAstHash(typeAnnotation)}`,
1973
+ line: getLineFromOffset(sourceText, startOffset),
1974
+ column: getColumnFromOffset(sourceText, startOffset)
1975
+ });
1976
+ }
1977
+ } else if (declarationNode.type === "TSInterfaceDeclaration") {
1978
+ const interfaceName = declarationNode.id?.name;
1979
+ const startOffset = declarationNode.start ?? 0;
1980
+ if (interfaceName) {
1981
+ const redundantPattern = detectRedundantInterfaceDeclaration(declarationNode);
1982
+ if (redundantPattern) redundantTypePatterns.push({
1983
+ typeName: interfaceName,
1984
+ kind: redundantPattern.kind,
1985
+ line: getLineFromOffset(sourceText, startOffset),
1986
+ column: getColumnFromOffset(sourceText, startOffset),
1987
+ reason: redundantPattern.reason,
1988
+ suggestion: redundantPattern.suggestion
1989
+ });
1990
+ const declarationCopy = {
1991
+ ...declarationNode,
1992
+ id: void 0
1993
+ };
1994
+ typeDefinitionHashes.push({
1995
+ typeName: interfaceName,
1996
+ structuralHash: `interface:${normalizeTypeAstHash(declarationCopy)}`,
1997
+ line: getLineFromOffset(sourceText, startOffset),
1998
+ column: getColumnFromOffset(sourceText, startOffset)
1999
+ });
2000
+ }
2001
+ } else if (declarationNode.type === "VariableDeclaration") for (const declarator of declarationNode.declarations ?? []) {
2002
+ const wrapperName = declarator.id?.name;
2003
+ const initializerNode = declarator.init;
2004
+ const startOffset = declarator.start ?? declarationNode.start ?? 0;
2005
+ if (!wrapperName || !initializerNode) continue;
2006
+ const wrapperDetection = detectIdentityWrapperFromInitializer(initializerNode, wrapperName);
2007
+ if (wrapperDetection) identityWrappers.push({
2008
+ wrapperName,
2009
+ wrappedExpression: wrapperDetection.wrappedExpression,
2010
+ line: getLineFromOffset(sourceText, startOffset),
2011
+ column: getColumnFromOffset(sourceText, startOffset)
2012
+ });
2013
+ }
2014
+ }
2015
+ };
467
2016
  const WHOLE_OBJECT_FUNCTION_NAMES = new Set([
468
2017
  "keys",
469
2018
  "values",
@@ -557,12 +2106,14 @@ const extractImportDeclaration = (node, sourceText, imports) => {
557
2106
  case "ImportSpecifier": {
558
2107
  const importedName = getModuleExportNameValue(specifierNode.imported);
559
2108
  const localName = specifierNode.local.name;
2109
+ const isSelfAlias = localName === importedName && specifierNode.imported.type === "Identifier" && specifierNode.imported.start !== specifierNode.local.start;
560
2110
  importedNames.push({
561
2111
  name: importedName,
562
2112
  alias: localName !== importedName ? localName : void 0,
563
2113
  isNamespace: false,
564
2114
  isDefault: importedName === "default",
565
- isTypeOnly: isTypeOnly || specifierNode.importKind === "type"
2115
+ isTypeOnly: isTypeOnly || specifierNode.importKind === "type",
2116
+ isRedundantAlias: isSelfAlias || void 0
566
2117
  });
567
2118
  break;
568
2119
  }
@@ -587,11 +2138,12 @@ const extractImportDeclaration = (node, sourceText, imports) => {
587
2138
  };
588
2139
  const extractNamedExportDeclaration = (node, sourceText, exports) => {
589
2140
  const isTypeOnly = node.exportKind === "type";
590
- const reExportSource = node.source?.value ?? void 0;
2141
+ const reExportSource = node.source?.value;
591
2142
  if (node.declaration) extractDeclarationNames(node.declaration, isTypeOnly, sourceText, exports, node.start);
592
2143
  for (const specifierNode of node.specifiers) {
593
2144
  const exportedName = getModuleExportNameValue(specifierNode.exported);
594
2145
  const localName = getModuleExportNameValue(specifierNode.local);
2146
+ const isSelfAlias = exportedName === localName && specifierNode.exported.type === "Identifier" && specifierNode.local.type === "Identifier" && specifierNode.exported.start !== specifierNode.local.start;
595
2147
  exports.push({
596
2148
  name: exportedName,
597
2149
  isDefault: exportedName === "default",
@@ -602,15 +2154,13 @@ const extractNamedExportDeclaration = (node, sourceText, exports) => {
602
2154
  reExportOriginalName: reExportSource !== void 0 ? localName : void 0,
603
2155
  isNamespaceReExport: false,
604
2156
  line: getLineFromOffset(sourceText, specifierNode.start ?? node.start),
605
- column: getColumnFromOffset(sourceText, specifierNode.start ?? node.start)
2157
+ column: getColumnFromOffset(sourceText, specifierNode.start ?? node.start),
2158
+ isRedundantAlias: isSelfAlias || void 0
606
2159
  });
607
2160
  }
608
2161
  };
609
2162
  const extractDefaultExportDeclaration = (node, sourceText, exports) => {
610
- const defaultExpression = node.declaration;
611
- let defaultExportLocalName;
612
- if (defaultExpression?.type === "Identifier" && typeof defaultExpression.name === "string") defaultExportLocalName = defaultExpression.name;
613
- else if ((defaultExpression?.type === "FunctionDeclaration" || defaultExpression?.type === "ClassDeclaration") && defaultExpression.id?.name) defaultExportLocalName = defaultExpression.id.name;
2163
+ const defaultExportLocalName = extractDefaultExportLocalName(node.declaration);
614
2164
  exports.push({
615
2165
  name: "default",
616
2166
  isDefault: true,
@@ -1718,6 +3268,198 @@ const findMonorepoRoot = (rootDir) => {
1718
3268
  }
1719
3269
  };
1720
3270
 
3271
+ //#endregion
3272
+ //#region src/utils/resolve-entry-with-extensions.ts
3273
+ const RESOLVABLE_EXTENSIONS = [
3274
+ ".ts",
3275
+ ".tsx",
3276
+ ".js",
3277
+ ".jsx",
3278
+ ".mjs",
3279
+ ".mts",
3280
+ ".cjs",
3281
+ ".cts"
3282
+ ];
3283
+ const resolveEntryWithExtensions = (basePath) => {
3284
+ if ((0, node_fs.existsSync)(basePath)) return basePath;
3285
+ for (const extension of RESOLVABLE_EXTENSIONS) {
3286
+ const withExtension = basePath + extension;
3287
+ if ((0, node_fs.existsSync)(withExtension)) return withExtension;
3288
+ }
3289
+ for (const extension of RESOLVABLE_EXTENSIONS) {
3290
+ const indexCandidate = (0, node_path.join)(basePath, `index${extension}`);
3291
+ if ((0, node_fs.existsSync)(indexCandidate)) return indexCandidate;
3292
+ }
3293
+ };
3294
+ const resolveEntryPathWithExtensions = (entryPath, rootDirectory) => {
3295
+ return resolveEntryWithExtensions((0, node_path.resolve)(rootDirectory, entryPath));
3296
+ };
3297
+
3298
+ //#endregion
3299
+ //#region src/collect/config-string-entries.ts
3300
+ const CONFIG_STRING_ENTRY_GLOBS = [
3301
+ "webpack.config.{js,ts,mjs,cjs}",
3302
+ "**/webpack*.config.{js,ts,mjs,cjs,babel.js}",
3303
+ "**/configs/webpack.config.{js,ts,mjs,cjs,babel.js}",
3304
+ "**/configs/webpack*.config.{js,ts,mjs,cjs,babel.js}",
3305
+ "jest.config.{js,ts,mjs,cjs,cts}",
3306
+ "**/jest.config.{js,ts,mjs,cjs,cts}",
3307
+ "vitest.config.{js,ts,mjs,mts}",
3308
+ "**/vitest.config.{js,ts,mjs,mts}",
3309
+ "**/vitest.*.config.{js,ts,mjs,mts}",
3310
+ "vite.config.{js,ts,mjs,mts}",
3311
+ "tailwind.config.{js,ts,cjs,mjs}",
3312
+ "**/tailwind.config.{js,ts,cjs,mjs}",
3313
+ "electron.vite.config.{js,ts,mjs}",
3314
+ "electron-builder.config.{js,ts,cjs}",
3315
+ "esbuild*.ts",
3316
+ "**/esbuild.entrypoints.ts",
3317
+ "metro.config.{js,ts}",
3318
+ "playwright.config.{js,ts}",
3319
+ "cypress.config.{js,ts}",
3320
+ "rollup.config.{js,ts,mjs,cjs}",
3321
+ "rollup.*.config.js",
3322
+ "**/.erb/configs/webpack*.config.{js,ts}",
3323
+ "**/.erb/configs/webpack.config.*.{js,ts}",
3324
+ "**/astro-tina-directive/register.js",
3325
+ "rspack.config.{js,ts,mjs,cjs}",
3326
+ "rsbuild.config.{js,ts,mjs,cjs}",
3327
+ "**/scripts/build.ts",
3328
+ "**/scripts/utils/createJestConfig.js"
3329
+ ];
3330
+ const CONFIG_RELATIVE_PATH_PATTERN = /['"`]((\.{1,2}\/|\.\.\/)[^'"`\n]+?|\.\/[^'"`\n]+?)['"`]/g;
3331
+ const JEST_ROOT_DIR_PATH_PATTERN = /<rootDir>\/([^'"`\n]+?)(?:['"`]|$)/g;
3332
+ const RESOLVE_CALL_PATH_PATTERN = /resolve\s*\(\s*['"`]([^'"`\n]+?)['"`]\s*\)/g;
3333
+ const PATH_JOIN_STRING_PATTERN = /path\.(?:join|resolve)\(\s*[^,]+,\s*['"`]([^'"`\n]+?)['"`]/g;
3334
+ const ENTRY_POINTS_STRING_PATTERN = /entryPoints:\s*\[\s*['"`]([^'"`\n]+?)['"`]/g;
3335
+ const ADD_PREAMBLE_PATTERN = /addPreamble\s*\(\s*['"`]([^'"`\n]+?)['"`]\s*\)/g;
3336
+ const ROLLUP_INPUT_PATTERN = /\binput\s*:\s*['"`]([^'"`\n]+?)['"`]/g;
3337
+ const VITEST_ENVIRONMENT_PATTERN = /environment\s*:\s*['"`](\.\/[^'"`\n]+?)['"`]/g;
3338
+ const ASTRO_ENTRYPOINT_PATTERN = /entrypoint\s*:\s*['"`](\.\/[^'"`\n]+?)['"`]/g;
3339
+ const WEBPACK_PATH_JOIN_ENTRY_PATTERN = /path\.join\(\s*[^,]+,\s*['"`]([^'"`\n]+?)['"`]\s*\)/g;
3340
+ const WEBPACK_RENDERER_PATH_JOIN_PATTERN = /path\.join\(\s*webpackPaths\.srcRendererPath\s*,\s*['"`]([^'"`\n]+?)['"`]\s*\)/g;
3341
+ const WEBPACK_MAIN_PATH_JOIN_PATTERN = /path\.join\(\s*webpackPaths\.srcMainPath\s*,\s*['"`]([^'"`\n]+?)['"`]\s*\)/g;
3342
+ const BARE_CONFIG_PATH_PATTERN = /['"`](config\/[^'"`\n]+?)['"`]/g;
3343
+ const stripModuleImportStatements = (content) => content.replace(/^\s*import\s+(?:type\s+)?[\s\S]*?\sfrom\s+['"`][^'"`\n]+['"`]\s*;?\s*$/gm, "").replace(/^\s*import\s+['"`][^'"`\n]+['"`]\s*;?\s*$/gm, "");
3344
+ const shouldSkipConfigPath = (rawPath) => {
3345
+ if (rawPath.includes("*") || rawPath.includes("?")) return true;
3346
+ if (rawPath.endsWith(".json") && !rawPath.includes("/src/")) return true;
3347
+ if (rawPath.startsWith("node:")) return true;
3348
+ if (rawPath.startsWith("@")) return true;
3349
+ return false;
3350
+ };
3351
+ const addResolvedConfigPath = (rawPath, configDirectory, projectRootDirectory, entries) => {
3352
+ if (shouldSkipConfigPath(rawPath)) return;
3353
+ const resolvedEntry = resolveEntryWithExtensions((0, node_path.resolve)(rawPath.startsWith(".") ? configDirectory : projectRootDirectory, rawPath.startsWith(".") ? rawPath : `./${rawPath}`));
3354
+ if (resolvedEntry) {
3355
+ entries.add(resolvedEntry);
3356
+ return;
3357
+ }
3358
+ if (rawPath.startsWith(".")) {
3359
+ const projectRootResolvedEntry = resolveEntryWithExtensions((0, node_path.resolve)(projectRootDirectory, rawPath));
3360
+ if (projectRootResolvedEntry) entries.add(projectRootResolvedEntry);
3361
+ }
3362
+ };
3363
+ const collectResolvedPathsFromStrings = (content, configDirectory, projectRootDirectory, entries) => {
3364
+ const contentWithoutImports = stripModuleImportStatements(content);
3365
+ const patterns = [
3366
+ CONFIG_RELATIVE_PATH_PATTERN,
3367
+ RESOLVE_CALL_PATH_PATTERN,
3368
+ PATH_JOIN_STRING_PATTERN,
3369
+ ENTRY_POINTS_STRING_PATTERN,
3370
+ ADD_PREAMBLE_PATTERN,
3371
+ ROLLUP_INPUT_PATTERN,
3372
+ VITEST_ENVIRONMENT_PATTERN,
3373
+ ASTRO_ENTRYPOINT_PATTERN,
3374
+ WEBPACK_PATH_JOIN_ENTRY_PATTERN,
3375
+ BARE_CONFIG_PATH_PATTERN
3376
+ ];
3377
+ for (const pattern of patterns) {
3378
+ let pathMatch;
3379
+ pattern.lastIndex = 0;
3380
+ while ((pathMatch = pattern.exec(contentWithoutImports)) !== null) addResolvedConfigPath(pathMatch[1], configDirectory, projectRootDirectory, entries);
3381
+ }
3382
+ let rendererEntryMatch;
3383
+ WEBPACK_RENDERER_PATH_JOIN_PATTERN.lastIndex = 0;
3384
+ while ((rendererEntryMatch = WEBPACK_RENDERER_PATH_JOIN_PATTERN.exec(contentWithoutImports)) !== null) addResolvedConfigPath(`src/renderer/${rendererEntryMatch[1]}`, configDirectory, projectRootDirectory, entries);
3385
+ let mainEntryMatch;
3386
+ WEBPACK_MAIN_PATH_JOIN_PATTERN.lastIndex = 0;
3387
+ while ((mainEntryMatch = WEBPACK_MAIN_PATH_JOIN_PATTERN.exec(contentWithoutImports)) !== null) addResolvedConfigPath(`src/main/${mainEntryMatch[1]}`, configDirectory, projectRootDirectory, entries);
3388
+ let rootDirMatch;
3389
+ JEST_ROOT_DIR_PATH_PATTERN.lastIndex = 0;
3390
+ while ((rootDirMatch = JEST_ROOT_DIR_PATH_PATTERN.exec(content)) !== null) addResolvedConfigPath(rootDirMatch[1], configDirectory, projectRootDirectory, entries);
3391
+ };
3392
+ const extractConfigStringReferencedEntries = (directory) => {
3393
+ const entries = /* @__PURE__ */ new Set();
3394
+ const configPaths = fast_glob.default.sync(CONFIG_STRING_ENTRY_GLOBS, {
3395
+ cwd: directory,
3396
+ absolute: true,
3397
+ onlyFiles: true,
3398
+ ignore: [
3399
+ "**/node_modules/**",
3400
+ "**/dist/**",
3401
+ "**/build/**"
3402
+ ],
3403
+ deep: 6
3404
+ });
3405
+ for (const configPath of configPaths) try {
3406
+ collectResolvedPathsFromStrings((0, node_fs.readFileSync)(configPath, "utf-8"), (0, node_path.dirname)(configPath), directory, entries);
3407
+ } catch {
3408
+ continue;
3409
+ }
3410
+ return [...entries];
3411
+ };
3412
+
3413
+ //#endregion
3414
+ //#region src/collect/sections-module-entries.ts
3415
+ const SECTIONS_FILE_GLOBS = ["sections.js", "**/sections.js"];
3416
+ const CALYPSO_MODULE_PATTERN = /module:\s*['"]calypso\/([^'"]+)['"]/g;
3417
+ const SECTION_BOOTSTRAP_SUFFIXES = [
3418
+ "",
3419
+ "/index",
3420
+ "/index.js",
3421
+ "/index.jsx",
3422
+ "/index.ts",
3423
+ "/index.tsx",
3424
+ "/main",
3425
+ "/controller",
3426
+ "/controller.js",
3427
+ "/controller.jsx"
3428
+ ];
3429
+ const addSectionModuleEntry = (modulePath, projectRootDirectory, entries) => {
3430
+ const moduleBasePath = (0, node_path.resolve)(projectRootDirectory, modulePath.replace(/^calypso\//, ""));
3431
+ for (const suffix of SECTION_BOOTSTRAP_SUFFIXES) {
3432
+ const resolvedEntry = resolveEntryWithExtensions(suffix ? `${moduleBasePath}${suffix}` : moduleBasePath);
3433
+ if (resolvedEntry) entries.add(resolvedEntry);
3434
+ }
3435
+ };
3436
+ const extractSectionsModuleEntries = (directory) => {
3437
+ const entries = /* @__PURE__ */ new Set();
3438
+ const sectionsFilePaths = fast_glob.default.sync(SECTIONS_FILE_GLOBS, {
3439
+ cwd: directory,
3440
+ absolute: true,
3441
+ onlyFiles: true,
3442
+ ignore: [
3443
+ "**/node_modules/**",
3444
+ "**/dist/**",
3445
+ "**/build/**"
3446
+ ],
3447
+ deep: 4
3448
+ });
3449
+ for (const sectionsFilePath of sectionsFilePaths) {
3450
+ if (!sectionsFilePath.endsWith("/client/sections.js")) continue;
3451
+ try {
3452
+ const content = (0, node_fs.readFileSync)(sectionsFilePath, "utf-8");
3453
+ let moduleMatch;
3454
+ CALYPSO_MODULE_PATTERN.lastIndex = 0;
3455
+ while ((moduleMatch = CALYPSO_MODULE_PATTERN.exec(content)) !== null) addSectionModuleEntry(moduleMatch[1], directory, entries);
3456
+ } catch {
3457
+ continue;
3458
+ }
3459
+ }
3460
+ return [...entries];
3461
+ };
3462
+
1721
3463
  //#endregion
1722
3464
  //#region src/collect/entries.ts
1723
3465
  const collectSourceFiles = async (config) => {
@@ -1804,8 +3546,11 @@ const resolveEntries = async (config) => {
1804
3546
  }
1805
3547
  const frameworkEntries = detectFrameworkEntries(absoluteRoot);
1806
3548
  const entryEligiblePackages = workspacePackages.filter(isEntryEligible);
3549
+ const monorepoRootForEntries = findMonorepoRoot(absoluteRoot);
3550
+ const ancestorPackageJsonRoots = monorepoRootForEntries && monorepoRootForEntries !== absoluteRoot ? [monorepoRootForEntries] : [];
1807
3551
  const scriptEntries = extractScriptEntries(absoluteRoot);
1808
3552
  for (const workspacePackage of entryEligiblePackages) scriptEntries.push(...extractScriptEntries(workspacePackage.directory));
3553
+ for (const ancestorRoot of ancestorPackageJsonRoots) for (const entryPath of extractScriptEntries(ancestorRoot)) if (entryPath.startsWith(`${absoluteRoot}/`)) scriptEntries.push(entryPath);
1809
3554
  const webpackEntries = extractWebpackEntryPoints(absoluteRoot);
1810
3555
  for (const workspacePackage of entryEligiblePackages) webpackEntries.push(...extractWebpackEntryPoints(workspacePackage.directory));
1811
3556
  const viteEntries = extractViteEntryPoints(absoluteRoot);
@@ -1829,6 +3574,9 @@ const resolveEntries = async (config) => {
1829
3574
  for (const workspacePackage of entryEligiblePackages) webWorkerEntries.push(...extractWebWorkerEntries(workspacePackage.directory));
1830
3575
  const tsConfigIncludeEntries = extractTsConfigIncludeFilesEntries(absoluteRoot);
1831
3576
  for (const workspacePackage of entryEligiblePackages) tsConfigIncludeEntries.push(...extractTsConfigIncludeFilesEntries(workspacePackage.directory));
3577
+ const configStringEntries = extractConfigStringReferencedEntries(absoluteRoot);
3578
+ for (const workspacePackage of entryEligiblePackages) configStringEntries.push(...extractConfigStringReferencedEntries(workspacePackage.directory));
3579
+ const sectionsModuleEntries = extractSectionsModuleEntries(absoluteRoot);
1832
3580
  const wranglerEntries = extractWranglerEntries(absoluteRoot);
1833
3581
  for (const workspacePackage of entryEligiblePackages) wranglerEntries.push(...extractWranglerEntries(workspacePackage.directory));
1834
3582
  const testSetupEntries = extractTestSetupFiles(absoluteRoot);
@@ -1855,6 +3603,8 @@ const resolveEntries = async (config) => {
1855
3603
  ...browserExtensionEntries,
1856
3604
  ...webWorkerEntries,
1857
3605
  ...tsConfigIncludeEntries,
3606
+ ...configStringEntries,
3607
+ ...sectionsModuleEntries,
1858
3608
  ...wranglerEntries,
1859
3609
  ...pluginFileEntries,
1860
3610
  ...toolingDiscovery.entryFiles,
@@ -1992,15 +3742,21 @@ const extractPackageJsonEntries = async (packageJsonPath) => {
1992
3742
  if (packageJson.exports) {
1993
3743
  const exportEntries = [];
1994
3744
  collectExportPaths(packageJson.exports, rootDir, exportEntries);
1995
- for (const exportEntry of exportEntries) if ((0, node_fs.existsSync)(exportEntry)) entries.push(exportEntry);
1996
- else {
1997
- const sourcePath = resolveSourcePath(exportEntry, rootDir);
1998
- if (sourcePath) entries.push(sourcePath);
1999
- else if (exportEntry.endsWith(".ts")) {
3745
+ for (const exportEntry of exportEntries) {
3746
+ const resolvedExportEntry = resolveEntryWithExtensions(exportEntry) ?? resolveEntryPathWithExtensions(exportEntry, rootDir) ?? resolveSourcePath(exportEntry, rootDir);
3747
+ if (resolvedExportEntry && (0, node_fs.existsSync)(resolvedExportEntry)) {
3748
+ entries.push(resolvedExportEntry);
3749
+ continue;
3750
+ }
3751
+ if (exportEntry.endsWith(".ts")) {
2000
3752
  const tsxFallback = exportEntry.replace(/\.ts$/, ".tsx");
2001
- if ((0, node_fs.existsSync)(tsxFallback)) entries.push(tsxFallback);
2002
- else entries.push(exportEntry);
2003
- } else entries.push(resolveEntryPath(exportEntry, rootDir));
3753
+ if ((0, node_fs.existsSync)(tsxFallback)) {
3754
+ entries.push(tsxFallback);
3755
+ continue;
3756
+ }
3757
+ }
3758
+ if ((0, node_fs.existsSync)(exportEntry)) entries.push(exportEntry);
3759
+ else entries.push(resolveEntryPath(exportEntry, rootDir));
2004
3760
  }
2005
3761
  }
2006
3762
  if (packageJson.bin) {
@@ -2009,9 +3765,52 @@ const extractPackageJsonEntries = async (packageJsonPath) => {
2009
3765
  for (const binPath of Object.values(packageJson.bin)) if (typeof binPath === "string") entries.push(resolveEntryPath(binPath, rootDir));
2010
3766
  }
2011
3767
  }
3768
+ if (Array.isArray(packageJson.sideEffects)) for (const sideEffectPattern of packageJson.sideEffects) {
3769
+ if (typeof sideEffectPattern !== "string") continue;
3770
+ const sourcePatterns = expandSideEffectGlobToSourcePatterns(sideEffectPattern);
3771
+ for (const sourcePattern of sourcePatterns) {
3772
+ const matchedSideEffectFiles = fast_glob.default.sync(sourcePattern, {
3773
+ cwd: rootDir,
3774
+ absolute: true,
3775
+ onlyFiles: true,
3776
+ ignore: [
3777
+ "**/node_modules/**",
3778
+ "**/dist/**",
3779
+ "**/build/**"
3780
+ ]
3781
+ });
3782
+ for (const matchedSideEffectFile of matchedSideEffectFiles) if (isImportableSourceFile(matchedSideEffectFile)) entries.push(matchedSideEffectFile);
3783
+ }
3784
+ }
3785
+ if (packageJson.build && typeof packageJson.build === "object") {
3786
+ const buildConfig = packageJson.build;
3787
+ if (Array.isArray(buildConfig.files)) for (const buildFileEntry of buildConfig.files) {
3788
+ if (typeof buildFileEntry !== "string") continue;
3789
+ if (buildFileEntry.includes("*")) continue;
3790
+ const resolvedBuildFile = resolveEntryWithExtensions((0, node_path.resolve)(rootDir, buildFileEntry)) ?? resolveEntryPathWithExtensions(buildFileEntry, rootDir);
3791
+ if (resolvedBuildFile && (0, node_fs.existsSync)(resolvedBuildFile)) entries.push(resolvedBuildFile);
3792
+ }
3793
+ }
3794
+ if (packageJson.jest && typeof packageJson.jest === "object") {
3795
+ const jestRootDirMatches = JSON.stringify(packageJson.jest).matchAll(/<rootDir>\/([^"\\]+)/g);
3796
+ for (const jestRootDirMatch of jestRootDirMatches) {
3797
+ const resolvedJestFile = resolveEntryPathWithExtensions(jestRootDirMatch[1], rootDir);
3798
+ if (resolvedJestFile && (0, node_fs.existsSync)(resolvedJestFile)) entries.push(resolvedJestFile);
3799
+ }
3800
+ }
2012
3801
  } catch {}
2013
3802
  return entries;
2014
3803
  };
3804
+ const expandSideEffectGlobToSourcePatterns = (pattern) => {
3805
+ const patterns = new Set([pattern]);
3806
+ if (pattern.endsWith(".js")) {
3807
+ patterns.add(pattern.replace(/\.js$/, ".ts"));
3808
+ patterns.add(pattern.replace(/\.js$/, ".tsx"));
3809
+ }
3810
+ if (pattern.includes("/lib/") || pattern.startsWith("lib/")) patterns.add(pattern.replace(/\blib\b/g, "src"));
3811
+ if (pattern.includes("/esm/") || pattern.startsWith("esm/")) patterns.add(pattern.replace(/\besm\b/g, "src"));
3812
+ return [...patterns];
3813
+ };
2015
3814
  const SHELL_OPERATORS_PATTERN = /\s*(?:&&|\|\||[;&|])\s*/;
2016
3815
  const SCRIPT_MULTIPLEXERS = new Set([
2017
3816
  "concurrently",
@@ -2389,23 +4188,6 @@ const WEBPACK_ENTRY_BLOCK_PATTERN = /entry\s*:\s*(?:\{[^}]*\}|\[[^\]]*\]|['"][^'
2389
4188
  const WEBPACK_ENTRY_FILE_PATTERN = /['"]([^'"]+)['"]/g;
2390
4189
  const WEBPACK_PATH_JOIN_PATTERN = /path\.(?:join|resolve)\(\s*__dirname\s*,\s*((?:['"][^'"]*['"]\s*,?\s*)+)\)/g;
2391
4190
  const REQUIRE_RESOLVE_PATTERN = /require\.resolve\(\s*['"]([^'"]+)['"]\s*\)/g;
2392
- const RESOLVABLE_EXTENSIONS = [
2393
- ".ts",
2394
- ".tsx",
2395
- ".js",
2396
- ".jsx",
2397
- ".mjs",
2398
- ".mts"
2399
- ];
2400
- const resolveEntryWithExtensions = (basePath) => {
2401
- if ((0, node_fs.existsSync)(basePath)) return basePath;
2402
- for (const extension of RESOLVABLE_EXTENSIONS) {
2403
- const withExtension = basePath + extension;
2404
- if ((0, node_fs.existsSync)(withExtension)) return withExtension;
2405
- }
2406
- const indexCandidates = RESOLVABLE_EXTENSIONS.map((extension) => (0, node_path.resolve)(basePath, `index${extension}`));
2407
- for (const candidate of indexCandidates) if ((0, node_fs.existsSync)(candidate)) return candidate;
2408
- };
2409
4191
  const extractWebpackEntryPoints = (directory) => {
2410
4192
  const entries = [];
2411
4193
  const webpackConfigPaths = fast_glob.default.sync([
@@ -2451,7 +4233,7 @@ const extractWebpackEntryPoints = (directory) => {
2451
4233
  while ((valueMatch = WEBPACK_ENTRY_FILE_PATTERN.exec(entryBlock)) !== null) {
2452
4234
  const entryPath = valueMatch[1];
2453
4235
  if (entryPath.startsWith("./") || entryPath.startsWith("../") || !entryPath.startsWith("/")) {
2454
- const resolvedEntry = resolveEntryWithExtensions((0, node_path.resolve)(directory, entryPath));
4236
+ const resolvedEntry = resolveEntryWithExtensions((0, node_path.resolve)(configDirectory, entryPath));
2455
4237
  if (resolvedEntry) entries.push(resolvedEntry);
2456
4238
  }
2457
4239
  }
@@ -2941,15 +4723,15 @@ const extractTestSetupFiles = (directory) => {
2941
4723
  const arrayContent = setupMatch[1];
2942
4724
  const singleValue = setupMatch[2];
2943
4725
  if (singleValue) {
2944
- const absolutePath = (0, node_path.resolve)(configDirectory, singleValue);
2945
- if ((0, node_fs.existsSync)(absolutePath)) entries.push(absolutePath);
4726
+ const resolvedPath = resolveEntryWithExtensions((0, node_path.resolve)(configDirectory, singleValue));
4727
+ if (resolvedPath) entries.push(resolvedPath);
2946
4728
  }
2947
4729
  if (arrayContent) {
2948
4730
  let pathMatch;
2949
4731
  SETUP_FILE_PATH_PATTERN.lastIndex = 0;
2950
4732
  while ((pathMatch = SETUP_FILE_PATH_PATTERN.exec(arrayContent)) !== null) {
2951
- const absolutePath = (0, node_path.resolve)(configDirectory, pathMatch[1]);
2952
- if ((0, node_fs.existsSync)(absolutePath)) entries.push(absolutePath);
4733
+ const resolvedPath = resolveEntryWithExtensions((0, node_path.resolve)(configDirectory, pathMatch[1]));
4734
+ if (resolvedPath) entries.push(resolvedPath);
2953
4735
  }
2954
4736
  }
2955
4737
  }
@@ -3686,13 +5468,16 @@ const FRAMEWORK_PATTERNS = [
3686
5468
  "electron-builder",
3687
5469
  "@electron-forge/cli",
3688
5470
  "electron-vite",
3689
- "electron-webpack"
5471
+ "electron-webpack",
5472
+ "electron-next"
3690
5473
  ],
3691
5474
  enablerPrefixes: ["@electron-forge/", "@electron/"],
3692
5475
  entryPatterns: [
3693
5476
  "src/main/**/*.{ts,tsx,js,jsx}",
3694
5477
  "src/preload/**/*.{ts,tsx,js,jsx}",
3695
- "electron/main.{ts,js}"
5478
+ "electron/main.{ts,js}",
5479
+ "main/index.{ts,tsx,js,jsx}",
5480
+ "renderer/pages/**/*.{ts,tsx,js,jsx}"
3696
5481
  ],
3697
5482
  alwaysUsed: [
3698
5483
  "electron-builder.{yml,yaml,json,json5,toml}",
@@ -4105,7 +5890,7 @@ const TSCONFIG_FILENAMES = [
4105
5890
  "tsconfig.base.json",
4106
5891
  "jsconfig.json"
4107
5892
  ];
4108
- const findNearestTsconfig = (fromDir, rootDir, monorepoRootDir) => {
5893
+ const findNearestTsconfig$1 = (fromDir, rootDir, monorepoRootDir) => {
4109
5894
  let currentDirectory = fromDir;
4110
5895
  const stopAt = monorepoRootDir ? (0, node_path.resolve)(monorepoRootDir) : (0, node_path.resolve)(rootDir);
4111
5896
  while (currentDirectory.length >= stopAt.length) {
@@ -4307,7 +6092,7 @@ const createResolver = (config, workspacePackages = [], options = {}) => {
4307
6092
  const fileDir = (0, node_path.dirname)(filePath);
4308
6093
  const cached = tsconfigPathCache.get(fileDir);
4309
6094
  if (cached !== void 0) return cached;
4310
- const tsconfigResult = findNearestTsconfig(fileDir, config.rootDir, options.monorepoRoot) ?? rootTsconfigPath;
6095
+ const tsconfigResult = findNearestTsconfig$1(fileDir, config.rootDir, options.monorepoRoot) ?? rootTsconfigPath;
4311
6096
  tsconfigPathCache.set(fileDir, tsconfigResult);
4312
6097
  return tsconfigResult;
4313
6098
  };
@@ -4731,6 +6516,18 @@ const createResolver = (config, workspacePackages = [], options = {}) => {
4731
6516
  resolveResultCache.set(cacheKey, resolvedResult);
4732
6517
  return resolvedResult;
4733
6518
  }
6519
+ if (cleanedSpecifier.startsWith(".")) {
6520
+ const relativeResolved = tryResolveFromDirectory(fromDir, cleanedSpecifier);
6521
+ if (relativeResolved && existsAsFile(relativeResolved)) {
6522
+ const resolvedResult = {
6523
+ resolvedPath: relativeResolved,
6524
+ isExternal: false,
6525
+ packageName: void 0
6526
+ };
6527
+ resolveResultCache.set(cacheKey, resolvedResult);
6528
+ return resolvedResult;
6529
+ }
6530
+ }
4734
6531
  const unresolvedResult = {
4735
6532
  resolvedPath: void 0,
4736
6533
  isExternal: false,
@@ -4826,6 +6623,16 @@ const buildDependencyGraph = (inputs) => {
4826
6623
  exports: input.parsed.exports,
4827
6624
  memberAccesses: input.parsed.memberAccesses,
4828
6625
  wholeObjectUses: input.parsed.wholeObjectUses,
6626
+ localIdentifierReferences: input.parsed.localIdentifierReferences,
6627
+ referencedFilenames: input.parsed.referencedFilenames,
6628
+ redundantTypePatterns: input.parsed.redundantTypePatterns,
6629
+ identityWrappers: input.parsed.identityWrappers,
6630
+ typeDefinitionHashes: input.parsed.typeDefinitionHashes,
6631
+ inlineTypeLiterals: input.parsed.inlineTypeLiterals,
6632
+ simplifiableFunctions: input.parsed.simplifiableFunctions,
6633
+ simplifiableExpressions: input.parsed.simplifiableExpressions,
6634
+ duplicateConstantCandidates: input.parsed.duplicateConstantCandidates,
6635
+ parseErrors: input.parsed.errors,
4829
6636
  isEntryPoint: input.isEntryPoint,
4830
6637
  isTestEntry: input.isTestEntry,
4831
6638
  isReachable: false,
@@ -5145,6 +6952,21 @@ const hasExcludedExtension = (filePath) => {
5145
6952
  return EXCLUDED_EXTENSIONS.has(filePath.slice(lastDot));
5146
6953
  };
5147
6954
  const isExcludedByPattern = (filePath) => TEST_FILE_PATTERN.test(filePath) || EXCLUDED_DIRECTORY_PATTERN.test(filePath) || CONFIG_FILE_PATTERN.test(filePath);
6955
+ /**
6956
+ * Files the parser couldn't analyze (minified bundles, oversized files, binaries)
6957
+ * have no detectable imports — they're effectively opaque. Flagging them as
6958
+ * "unused" is a false positive because we can't see who imports them, and they
6959
+ * may be static assets, generated bundles, or build artifacts that get loaded
6960
+ * outside the JS module graph (HTML `<script src>`, `vite-plugin-string`, etc.).
6961
+ * The parser already records a `file-minified`/`file-too-large`/`file-binary`
6962
+ * info-level entry in `analysisErrors`, which is the actionable signal.
6963
+ */
6964
+ const PARSE_OPAQUE_ERROR_CODES = new Set([
6965
+ "file-minified",
6966
+ "file-too-large",
6967
+ "file-binary"
6968
+ ]);
6969
+ const isOpaqueToAnalysis = (module) => module.parseErrors.some((parseError) => parseError.code && PARSE_OPAQUE_ERROR_CODES.has(parseError.code));
5148
6970
  const detectOrphanFiles = (graph) => {
5149
6971
  const unusedFiles = [];
5150
6972
  for (const module of graph.modules) {
@@ -5154,6 +6976,7 @@ const detectOrphanFiles = (graph) => {
5154
6976
  if (module.isConfigFile) continue;
5155
6977
  if (hasExcludedExtension(module.fileId.path)) continue;
5156
6978
  if (isExcludedByPattern(module.fileId.path)) continue;
6979
+ if (isOpaqueToAnalysis(module)) continue;
5157
6980
  if (isBarrelWithReachableSources(module, graph)) continue;
5158
6981
  if (hasReachableDirectImporter(module.fileId.index, graph)) continue;
5159
6982
  unusedFiles.push({ path: module.fileId.path });
@@ -5194,6 +7017,7 @@ const detectDeadExports = (graph, config) => {
5194
7017
  if (!config.reportTypes && exportInfo.isTypeOnly) continue;
5195
7018
  const usageKey = `${module.fileId.path}::${exportInfo.name}`;
5196
7019
  if (usageMap.has(usageKey)) continue;
7020
+ if (module.localIdentifierReferences.includes(exportInfo.name)) continue;
5197
7021
  if (!exportInfo.isDefault && defaultExportLinkedNames.has(exportInfo.name)) continue;
5198
7022
  unusedExports.push({
5199
7023
  path: module.fileId.path,
@@ -5227,6 +7051,11 @@ const buildUsageMap = (graph) => {
5227
7051
  else {
5228
7052
  const importName = symbol.isDefault ? "default" : symbol.importedName;
5229
7053
  markExportUsedRecursive(targetModule.fileId.path, importName, graph, sourceToTargetMap, usedExportKeys, /* @__PURE__ */ new Set());
7054
+ if (symbol.isDefault) {
7055
+ if (!targetModule.exports.some((exportInfo) => exportInfo.isDefault) && symbol.localName !== "default") {
7056
+ if (targetModule.exports.find((exportInfo) => exportInfo.name === symbol.localName)) markExportUsedRecursive(targetModule.fileId.path, symbol.localName, graph, sourceToTargetMap, usedExportKeys, /* @__PURE__ */ new Set());
7057
+ }
7058
+ }
5230
7059
  }
5231
7060
  }
5232
7061
  return usedExportKeys;
@@ -5437,6 +7266,7 @@ const matchesPackageImportReference = (content, packageName) => {
5437
7266
  new RegExp(`\\bfrom\\s+['"]${escapedPackageName}${subpathPattern}['"]`),
5438
7267
  new RegExp(`\\bimport\\s+(?:[^'";\\n]*?\\sfrom\\s+)?['"]${escapedPackageName}${subpathPattern}['"]`),
5439
7268
  new RegExp(`\\brequire\\s*\\(\\s*['"]${escapedPackageName}${subpathPattern}['"]\\s*\\)`),
7269
+ new RegExp(`\\brequire\\s*\\(\\s*\`${escapedPackageName}${subpathPattern}`),
5440
7270
  new RegExp(`\\bimport\\s*\\(\\s*['"]${escapedPackageName}${subpathPattern}['"]`)
5441
7271
  ].some((pattern) => pattern.test(content));
5442
7272
  };
@@ -5477,13 +7307,13 @@ const detectStalePackages = (graph, config) => {
5477
7307
  const declaredNames = new Set(declaredDependencies.keys());
5478
7308
  const usedPackageNames = collectUsedPackages(graph);
5479
7309
  const monorepoRoot = findMonorepoRoot(config.rootDir);
5480
- const nodeModulesRoot = monorepoRoot ?? config.rootDir;
7310
+ const nodeModulesSearchRoots = monorepoRoot && monorepoRoot !== config.rootDir ? [config.rootDir, monorepoRoot] : [config.rootDir];
5481
7311
  const allPackageJsonPaths = discoverAllPackageJsonPaths(config.rootDir);
5482
7312
  if (monorepoRoot) {
5483
7313
  const monorepoPackageJson = (0, node_path.join)(monorepoRoot, "package.json");
5484
7314
  if (!allPackageJsonPaths.includes(monorepoPackageJson) && (0, node_fs.existsSync)(monorepoPackageJson)) allPackageJsonPaths.push(monorepoPackageJson);
5485
7315
  }
5486
- const binToPackage = buildBinToPackageMap(nodeModulesRoot, declaredNames);
7316
+ const binToPackage = buildBinToPackageMap(nodeModulesSearchRoots, declaredNames);
5487
7317
  for (const workspacePackageJsonPath of allPackageJsonPaths) {
5488
7318
  const scriptReferenced = collectScriptReferencedPackages(workspacePackageJsonPath, declaredNames, binToPackage);
5489
7319
  for (const packageName of scriptReferenced) usedPackageNames.add(packageName);
@@ -5529,7 +7359,7 @@ const detectStalePackages = (graph, config) => {
5529
7359
  if ("react-dom" in peerDeps && declaredDependencies.get("react-dom") === true) usedPackageNames.add("react-dom");
5530
7360
  } catch {}
5531
7361
  }
5532
- const peerSatisfied = collectPeerSatisfiedPackages(nodeModulesRoot, declaredNames, usedPackageNames);
7362
+ const peerSatisfied = collectPeerSatisfiedPackages(nodeModulesSearchRoots, declaredNames, usedPackageNames);
5533
7363
  for (const packageName of peerSatisfied) usedPackageNames.add(packageName);
5534
7364
  const staticPeerSatisfied = collectStaticPeerSatisfiedPackages(declaredNames, usedPackageNames);
5535
7365
  for (const packageName of staticPeerSatisfied) usedPackageNames.add(packageName);
@@ -5575,14 +7405,14 @@ const hasJsxFiles = (graph) => graph.modules.some((module) => {
5575
7405
  const filePath = module.fileId.path;
5576
7406
  return filePath.endsWith(".tsx") || filePath.endsWith(".jsx");
5577
7407
  });
5578
- const collectPeerSatisfiedPackages = (rootDir, declaredNames, confirmedUsedNames) => {
7408
+ const collectPeerSatisfiedPackages = (nodeModulesSearchRoots, declaredNames, confirmedUsedNames) => {
5579
7409
  const peerSatisfied = /* @__PURE__ */ new Set();
5580
- const nodeModulesDir = (0, node_path.join)(rootDir, "node_modules");
5581
7410
  for (const installedName of declaredNames) {
5582
7411
  if (!confirmedUsedNames.has(installedName)) continue;
5583
- const packageJsonPath = installedName.startsWith("@") ? (0, node_path.join)(nodeModulesDir, ...installedName.split("/"), "package.json") : (0, node_path.join)(nodeModulesDir, installedName, "package.json");
7412
+ const installedPackageJsonPath = findInstalledPackageJsonPath(installedName, nodeModulesSearchRoots);
7413
+ if (!installedPackageJsonPath) continue;
5584
7414
  try {
5585
- const content = (0, node_fs.readFileSync)(packageJsonPath, "utf-8");
7415
+ const content = (0, node_fs.readFileSync)(installedPackageJsonPath, "utf-8");
5586
7416
  const peerDeps = JSON.parse(content).peerDependencies;
5587
7417
  if (peerDeps && typeof peerDeps === "object") {
5588
7418
  for (const peerName of Object.keys(peerDeps)) if (declaredNames.has(peerName)) peerSatisfied.add(peerName);
@@ -5593,6 +7423,12 @@ const collectPeerSatisfiedPackages = (rootDir, declaredNames, confirmedUsedNames
5593
7423
  }
5594
7424
  return peerSatisfied;
5595
7425
  };
7426
+ const findInstalledPackageJsonPath = (packageName, nodeModulesSearchRoots) => {
7427
+ for (const searchRoot of nodeModulesSearchRoots) {
7428
+ const candidatePath = packageName.startsWith("@") ? (0, node_path.join)(searchRoot, "node_modules", ...packageName.split("/"), "package.json") : (0, node_path.join)(searchRoot, "node_modules", packageName, "package.json");
7429
+ if ((0, node_fs.existsSync)(candidatePath)) return candidatePath;
7430
+ }
7431
+ };
5596
7432
  const STATIC_PEER_DEPENDENCY_MAP = {
5597
7433
  "@apollo/client": ["graphql"],
5598
7434
  "@docusaurus/core": ["@mdx-js/react"],
@@ -5686,7 +7522,9 @@ const CLI_BINARY_TO_PACKAGE = {
5686
7522
  };
5687
7523
  const CLI_BINARY_FALLBACK_PACKAGES = {
5688
7524
  babel: ["babel-cli"],
5689
- jest: ["jest-cli"]
7525
+ jest: ["jest-cli"],
7526
+ remark: ["remark-cli"],
7527
+ dumi: ["dumi"]
5690
7528
  };
5691
7529
  const ENV_WRAPPER_BINARY_SET = new Set([
5692
7530
  "cross-env",
@@ -5695,11 +7533,12 @@ const ENV_WRAPPER_BINARY_SET = new Set([
5695
7533
  "env-cmd"
5696
7534
  ]);
5697
7535
  const INLINE_ENV_VAR_PATTERN = /^[A-Z_][A-Z0-9_]*=/;
5698
- const buildBinToPackageMap = (rootDir, declaredNames) => {
7536
+ const buildBinToPackageMap = (nodeModulesSearchRoots, declaredNames) => {
5699
7537
  const binToPackage = /* @__PURE__ */ new Map();
5700
7538
  for (const [binary, packageName] of Object.entries(CLI_BINARY_TO_PACKAGE)) binToPackage.set(binary, packageName);
5701
7539
  for (const packageName of declaredNames) {
5702
- const packageBinJsonPath = packageName.startsWith("@") ? (0, node_path.join)(rootDir, "node_modules", ...packageName.split("/"), "package.json") : (0, node_path.join)(rootDir, "node_modules", packageName, "package.json");
7540
+ const packageBinJsonPath = findInstalledPackageJsonPath(packageName, nodeModulesSearchRoots);
7541
+ if (!packageBinJsonPath) continue;
5703
7542
  try {
5704
7543
  const binContent = (0, node_fs.readFileSync)(packageBinJsonPath, "utf-8");
5705
7544
  const binPackageJson = JSON.parse(binContent);
@@ -5791,7 +7630,12 @@ const CONFIG_FILE_GLOBS = [
5791
7630
  ".lintstagedrc.{js,cjs,mjs,json}",
5792
7631
  "commitlint.config.{js,cjs,mjs,ts}",
5793
7632
  ".commitlintrc.{js,cjs,mjs,json,yaml,yml}",
5794
- "tslint.json"
7633
+ "tslint.json",
7634
+ ".remarkrc",
7635
+ ".remarkrc.{js,cjs,mjs,json}",
7636
+ ".dumirc.ts",
7637
+ ".dumirc.js",
7638
+ "dumi.config.{ts,js}"
5795
7639
  ];
5796
7640
  const collectConfigReferencedPackages = (rootDir, graph, declaredNames) => {
5797
7641
  const referenced = /* @__PURE__ */ new Set();
@@ -5818,6 +7662,24 @@ const collectConfigReferencedPackages = (rootDir, graph, declaredNames) => {
5818
7662
  } catch {
5819
7663
  continue;
5820
7664
  }
7665
+ const documentationFiles = fast_glob.default.sync(["**/*.{mdx,md}"], {
7666
+ cwd: rootDir,
7667
+ absolute: true,
7668
+ onlyFiles: true,
7669
+ ignore: [
7670
+ "**/node_modules/**",
7671
+ "**/dist/**",
7672
+ "**/build/**",
7673
+ "**/CHANGELOG.md"
7674
+ ],
7675
+ deep: 6
7676
+ });
7677
+ for (const documentationPath of documentationFiles) try {
7678
+ const content = (0, node_fs.readFileSync)(documentationPath, "utf-8");
7679
+ for (const packageName of declaredNames) if (matchesPackageImportReference(content, packageName)) referenced.add(packageName);
7680
+ } catch {
7681
+ continue;
7682
+ }
5821
7683
  return referenced;
5822
7684
  };
5823
7685
  const PACKAGE_JSON_CONFIG_SECTIONS = [
@@ -6140,103 +8002,1495 @@ const findStronglyConnectedComponents = (adjacencyList) => {
6140
8002
  }
6141
8003
  }
6142
8004
  }
6143
- return components;
8005
+ return components;
8006
+ };
8007
+ const canonicalizeCycle = (cycle, graph) => {
8008
+ if (cycle.length === 0) return [];
8009
+ let minPosition = 0;
8010
+ let minPath = graph.modules[cycle[0]].fileId.path;
8011
+ for (let position = 1; position < cycle.length; position++) {
8012
+ const currentPath = graph.modules[cycle[position]].fileId.path;
8013
+ if (currentPath < minPath) {
8014
+ minPath = currentPath;
8015
+ minPosition = position;
8016
+ }
8017
+ }
8018
+ return [...cycle.slice(minPosition), ...cycle.slice(0, minPosition)];
8019
+ };
8020
+ const enumerateElementaryCycles = (componentNodes, adjacencyList, graph) => {
8021
+ if (componentNodes.length === 2) {
8022
+ const [nodeA, nodeB] = componentNodes;
8023
+ return [graph.modules[nodeA].fileId.path <= graph.modules[nodeB].fileId.path ? [nodeA, nodeB] : [nodeB, nodeA]];
8024
+ }
8025
+ const componentSet = new Set(componentNodes);
8026
+ const cycles = [];
8027
+ const seenKeys = /* @__PURE__ */ new Set();
8028
+ for (const startNode of componentNodes) {
8029
+ if (cycles.length >= 20) break;
8030
+ const visitedInThisSearch = /* @__PURE__ */ new Set();
8031
+ visitedInThisSearch.add(startNode);
8032
+ const pathStack = [startNode];
8033
+ const successorPositionStack = [0];
8034
+ while (pathStack.length > 0 && cycles.length < 20) {
8035
+ const currentNode = pathStack[pathStack.length - 1];
8036
+ const currentSuccessorPosition = successorPositionStack[successorPositionStack.length - 1];
8037
+ const successors = adjacencyList[currentNode].filter((successor) => componentSet.has(successor));
8038
+ if (currentSuccessorPosition < successors.length) {
8039
+ successorPositionStack[successorPositionStack.length - 1]++;
8040
+ const successor = successors[currentSuccessorPosition];
8041
+ if (successor === startNode) {
8042
+ const canonical = canonicalizeCycle([...pathStack], graph);
8043
+ const key = canonical.join(",");
8044
+ if (!seenKeys.has(key)) {
8045
+ seenKeys.add(key);
8046
+ cycles.push(canonical);
8047
+ }
8048
+ } else if (!visitedInThisSearch.has(successor)) {
8049
+ visitedInThisSearch.add(successor);
8050
+ pathStack.push(successor);
8051
+ successorPositionStack.push(0);
8052
+ }
8053
+ } else {
8054
+ visitedInThisSearch.delete(pathStack.pop());
8055
+ successorPositionStack.pop();
8056
+ }
8057
+ }
8058
+ }
8059
+ return cycles;
8060
+ };
8061
+ const detectCycles = (graph) => {
8062
+ const adjacencyList = buildAdjacencyList(graph);
8063
+ const components = findStronglyConnectedComponents(adjacencyList);
8064
+ const allCycles = [];
8065
+ const seenKeys = /* @__PURE__ */ new Set();
8066
+ const sortedComponents = [...components].sort((componentA, componentB) => componentA.length - componentB.length);
8067
+ for (const component of sortedComponents) {
8068
+ if (allCycles.length >= 200) break;
8069
+ if (component.length > 50) continue;
8070
+ const elementaryCycles = enumerateElementaryCycles(component, adjacencyList, graph);
8071
+ for (const cycle of elementaryCycles) {
8072
+ const key = cycle.join(",");
8073
+ if (!seenKeys.has(key)) {
8074
+ seenKeys.add(key);
8075
+ allCycles.push(cycle);
8076
+ }
8077
+ if (allCycles.length >= 200) break;
8078
+ }
8079
+ }
8080
+ allCycles.sort((cycleA, cycleB) => {
8081
+ const lengthDiff = cycleA.length - cycleB.length;
8082
+ if (lengthDiff !== 0) return lengthDiff;
8083
+ return graph.modules[cycleA[0]].fileId.path.localeCompare(graph.modules[cycleB[0]].fileId.path);
8084
+ });
8085
+ return allCycles.map((cycle) => ({ files: cycle.map((nodeIndex) => graph.modules[nodeIndex].fileId.path) }));
8086
+ };
8087
+
8088
+ //#endregion
8089
+ //#region src/report/redundancy.ts
8090
+ const isPlatformSpecificModulePath = (modulePath) => {
8091
+ const extensionIndex = modulePath.lastIndexOf(".");
8092
+ if (extensionIndex === -1) return false;
8093
+ const withoutExtension = modulePath.slice(0, extensionIndex);
8094
+ return PLATFORM_SUFFIXES.some((suffix) => withoutExtension.endsWith(suffix));
8095
+ };
8096
+ const platformStrippedBasePath = (modulePath) => {
8097
+ const extensionIndex = modulePath.lastIndexOf(".");
8098
+ if (extensionIndex === -1) return modulePath;
8099
+ const withoutExtension = modulePath.slice(0, extensionIndex);
8100
+ for (const suffix of PLATFORM_SUFFIXES) if (withoutExtension.endsWith(suffix)) return withoutExtension.slice(0, -suffix.length) + modulePath.slice(extensionIndex);
8101
+ return modulePath;
8102
+ };
8103
+ const buildPlatformSiblingGroupSizes = (graph) => {
8104
+ const baseToCount = /* @__PURE__ */ new Map();
8105
+ for (const module of graph.modules) {
8106
+ const base = platformStrippedBasePath(module.fileId.path);
8107
+ baseToCount.set(base, (baseToCount.get(base) ?? 0) + 1);
8108
+ }
8109
+ return baseToCount;
8110
+ };
8111
+ const detectUselessAliasedReExports = (graph) => {
8112
+ const findings = [];
8113
+ const moduleConsumerImportedNames = /* @__PURE__ */ new Map();
8114
+ const moduleConsumedWholesale = /* @__PURE__ */ new Set();
8115
+ const platformSiblingGroupSizes = buildPlatformSiblingGroupSizes(graph);
8116
+ for (const edge of graph.edges) {
8117
+ if (edge.isReExportEdge) {
8118
+ const reExportedSet = moduleConsumerImportedNames.get(edge.target);
8119
+ if (edge.reExportedNames.includes("*")) moduleConsumedWholesale.add(edge.target);
8120
+ if (reExportedSet) for (const reExportedName of edge.reExportedNames) reExportedSet.add(reExportedName);
8121
+ else moduleConsumerImportedNames.set(edge.target, new Set(edge.reExportedNames));
8122
+ continue;
8123
+ }
8124
+ if (edge.importedSymbols.length === 0) {
8125
+ moduleConsumedWholesale.add(edge.target);
8126
+ continue;
8127
+ }
8128
+ const importedSet = moduleConsumerImportedNames.get(edge.target);
8129
+ for (const symbol of edge.importedSymbols) {
8130
+ if (symbol.isNamespace || symbol.importedName === "*") {
8131
+ moduleConsumedWholesale.add(edge.target);
8132
+ continue;
8133
+ }
8134
+ const importedName = symbol.isDefault ? "default" : symbol.importedName;
8135
+ if (importedSet) importedSet.add(importedName);
8136
+ else moduleConsumerImportedNames.set(edge.target, new Set([importedName]));
8137
+ }
8138
+ }
8139
+ for (const module of graph.modules) {
8140
+ if (!module.isReachable) continue;
8141
+ if (module.isDeclarationFile) continue;
8142
+ if (moduleConsumedWholesale.has(module.fileId.index)) continue;
8143
+ if (isPlatformSpecificModulePath(module.fileId.path)) continue;
8144
+ const platformBase = platformStrippedBasePath(module.fileId.path);
8145
+ if ((platformSiblingGroupSizes.get(platformBase) ?? 0) > 1) continue;
8146
+ const consumerImportedNames = moduleConsumerImportedNames.get(module.fileId.index) ?? /* @__PURE__ */ new Set();
8147
+ for (const exportInfo of module.exports) {
8148
+ if (exportInfo.isSynthetic) continue;
8149
+ if (!exportInfo.isReExport) continue;
8150
+ if (!exportInfo.reExportOriginalName) continue;
8151
+ const exportedName = exportInfo.name;
8152
+ const originalName = exportInfo.reExportOriginalName;
8153
+ if (exportedName === originalName) continue;
8154
+ if (exportedName === "*") continue;
8155
+ if (originalName === "*") continue;
8156
+ if (originalName === "default") continue;
8157
+ if (exportInfo.isNamespaceReExport) continue;
8158
+ if (consumerImportedNames.has(exportedName)) continue;
8159
+ findings.push({
8160
+ path: module.fileId.path,
8161
+ kind: "reexport-aliased-not-used",
8162
+ name: exportedName,
8163
+ aliasedFrom: originalName,
8164
+ line: exportInfo.line,
8165
+ column: exportInfo.column,
8166
+ confidence: "medium",
8167
+ reason: `\`export { ${originalName} as ${exportedName} } from ...\` renames the symbol but no consumer imports it as \`${exportedName}\` — either drop the alias or have consumers use the new name`
8168
+ });
8169
+ }
8170
+ }
8171
+ return findings;
8172
+ };
8173
+ const detectRedundantAliases = (graph) => {
8174
+ const findings = [];
8175
+ for (const module of graph.modules) {
8176
+ if (module.isDeclarationFile) continue;
8177
+ if (!module.isReachable) continue;
8178
+ for (const importInfo of module.imports) for (const binding of importInfo.importedNames) {
8179
+ if (!binding.isRedundantAlias) continue;
8180
+ findings.push({
8181
+ path: module.fileId.path,
8182
+ kind: "import-self-alias",
8183
+ name: binding.name,
8184
+ aliasedFrom: binding.name,
8185
+ line: importInfo.line,
8186
+ column: importInfo.column,
8187
+ confidence: "high",
8188
+ reason: `\`import { ${binding.name} as ${binding.name} }\` aliases an identifier to its own name`
8189
+ });
8190
+ }
8191
+ for (const exportInfo of module.exports) {
8192
+ if (exportInfo.isSynthetic) continue;
8193
+ if (!exportInfo.isRedundantAlias) continue;
8194
+ const kind = exportInfo.isReExport ? "reexport-self-alias" : "export-self-alias";
8195
+ const sourceSuffix = exportInfo.reExportSource ? ` from "${exportInfo.reExportSource}"` : "";
8196
+ findings.push({
8197
+ path: module.fileId.path,
8198
+ kind,
8199
+ name: exportInfo.name,
8200
+ aliasedFrom: exportInfo.name,
8201
+ line: exportInfo.line,
8202
+ column: exportInfo.column,
8203
+ confidence: "high",
8204
+ reason: `\`export { ${exportInfo.name} as ${exportInfo.name} }${sourceSuffix}\` aliases an identifier to its own name`
8205
+ });
8206
+ }
8207
+ }
8208
+ return findings;
8209
+ };
8210
+ const detectDuplicateExports = (graph) => {
8211
+ const findings = [];
8212
+ for (const module of graph.modules) {
8213
+ if (module.isDeclarationFile) continue;
8214
+ const nameToOccurrences = /* @__PURE__ */ new Map();
8215
+ const nameHasReExport = /* @__PURE__ */ new Map();
8216
+ for (const exportInfo of module.exports) {
8217
+ if (exportInfo.isSynthetic) continue;
8218
+ if (exportInfo.name === "*" && exportInfo.isNamespaceReExport) continue;
8219
+ const occurrence = {
8220
+ line: exportInfo.line,
8221
+ column: exportInfo.column,
8222
+ reExportSource: exportInfo.reExportSource,
8223
+ isReExport: exportInfo.isReExport
8224
+ };
8225
+ const existing = nameToOccurrences.get(exportInfo.name);
8226
+ if (existing) existing.push(occurrence);
8227
+ else nameToOccurrences.set(exportInfo.name, [occurrence]);
8228
+ if (exportInfo.isReExport) nameHasReExport.set(exportInfo.name, true);
8229
+ }
8230
+ for (const [name, occurrences] of nameToOccurrences) {
8231
+ if (occurrences.length < 2) continue;
8232
+ if (!nameHasReExport.get(name)) continue;
8233
+ findings.push({
8234
+ path: module.fileId.path,
8235
+ name,
8236
+ occurrences,
8237
+ confidence: "high",
8238
+ reason: `"${name}" is exported ${occurrences.length} times from the same module`
8239
+ });
8240
+ }
8241
+ }
8242
+ return findings;
8243
+ };
8244
+
8245
+ //#endregion
8246
+ //#region src/report/dry-patterns.ts
8247
+ const detectDuplicateImports = (graph) => {
8248
+ const findings = [];
8249
+ for (const module of graph.modules) {
8250
+ if (module.isDeclarationFile) continue;
8251
+ const groupedByKindAndSpecifier = /* @__PURE__ */ new Map();
8252
+ for (const importInfo of module.imports) {
8253
+ if (importInfo.isSideEffect) continue;
8254
+ if (importInfo.isDynamic) continue;
8255
+ if (importInfo.isGlob) continue;
8256
+ const occurrence = {
8257
+ line: importInfo.line,
8258
+ column: importInfo.column,
8259
+ importedNames: importInfo.importedNames.map((binding) => binding.isNamespace ? `* as ${binding.alias ?? ""}` : binding.alias ?? binding.name),
8260
+ isTypeOnly: importInfo.isTypeOnly
8261
+ };
8262
+ const groupKey = `${importInfo.isTypeOnly ? "type" : "value"}:${importInfo.specifier}`;
8263
+ const existing = groupedByKindAndSpecifier.get(groupKey);
8264
+ if (existing) existing.push(occurrence);
8265
+ else groupedByKindAndSpecifier.set(groupKey, [occurrence]);
8266
+ }
8267
+ for (const [groupKey, occurrences] of groupedByKindAndSpecifier) {
8268
+ if (occurrences.length < 2) continue;
8269
+ const specifier = groupKey.slice(groupKey.indexOf(":") + 1);
8270
+ const kindLabel = groupKey.startsWith("type:") ? "type-only " : "";
8271
+ findings.push({
8272
+ path: module.fileId.path,
8273
+ specifier,
8274
+ occurrences,
8275
+ confidence: "high",
8276
+ reason: `"${specifier}" is imported ${occurrences.length} times in this file as ${kindLabel}imports — merge into a single statement`
8277
+ });
8278
+ }
8279
+ }
8280
+ return findings;
8281
+ };
8282
+ const detectRedundantTypePatterns = (graph) => {
8283
+ const findings = [];
8284
+ for (const module of graph.modules) {
8285
+ if (module.isDeclarationFile) continue;
8286
+ for (const parsedPattern of module.redundantTypePatterns) findings.push({
8287
+ path: module.fileId.path,
8288
+ typeName: parsedPattern.typeName,
8289
+ kind: parsedPattern.kind,
8290
+ line: parsedPattern.line,
8291
+ column: parsedPattern.column,
8292
+ confidence: "high",
8293
+ reason: parsedPattern.reason,
8294
+ suggestion: parsedPattern.suggestion
8295
+ });
8296
+ }
8297
+ return findings;
8298
+ };
8299
+ const detectIdentityWrappers = (graph) => {
8300
+ const findings = [];
8301
+ for (const module of graph.modules) {
8302
+ if (module.isDeclarationFile) continue;
8303
+ for (const parsedWrapper of module.identityWrappers) findings.push({
8304
+ path: module.fileId.path,
8305
+ wrapperName: parsedWrapper.wrapperName,
8306
+ wrappedExpression: parsedWrapper.wrappedExpression,
8307
+ line: parsedWrapper.line,
8308
+ column: parsedWrapper.column,
8309
+ confidence: "high",
8310
+ reason: `\`${parsedWrapper.wrapperName}\` is a thin wrapper that forwards every argument to \`${parsedWrapper.wrappedExpression}\` unchanged`
8311
+ });
8312
+ }
8313
+ return findings;
8314
+ };
8315
+ const detectDuplicateTypeDefinitions = (graph) => {
8316
+ const hashToInstances = /* @__PURE__ */ new Map();
8317
+ for (const module of graph.modules) {
8318
+ if (module.isDeclarationFile) continue;
8319
+ for (const typeHash of module.typeDefinitionHashes) {
8320
+ const instance = {
8321
+ path: module.fileId.path,
8322
+ typeName: typeHash.typeName,
8323
+ line: typeHash.line,
8324
+ column: typeHash.column
8325
+ };
8326
+ const existing = hashToInstances.get(typeHash.structuralHash);
8327
+ if (existing) existing.push(instance);
8328
+ else hashToInstances.set(typeHash.structuralHash, [instance]);
8329
+ }
8330
+ }
8331
+ const findings = [];
8332
+ for (const [structuralHash, instances] of hashToInstances) {
8333
+ if (instances.length < 2) continue;
8334
+ const uniquePaths = new Set(instances.map((instance) => instance.path));
8335
+ if (uniquePaths.size < 2) continue;
8336
+ const uniqueNames = new Set(instances.map((instance) => instance.typeName));
8337
+ const isAllSameName = uniqueNames.size === 1;
8338
+ findings.push({
8339
+ structuralHash,
8340
+ instances,
8341
+ confidence: isAllSameName ? "high" : "medium",
8342
+ reason: isAllSameName ? `${instances.length} identically-named type definitions of the same shape across ${uniquePaths.size} files — extract a shared definition` : `${instances.length} structurally-identical type definitions detected across ${uniquePaths.size} files under different names (${[...uniqueNames].join(", ")}) — confirm whether the rename is intentional`
8343
+ });
8344
+ }
8345
+ return findings;
8346
+ };
8347
+ const detectDuplicateConstants = (graph) => {
8348
+ const hashToBuckets = /* @__PURE__ */ new Map();
8349
+ for (const module of graph.modules) {
8350
+ if (module.isDeclarationFile) continue;
8351
+ for (const candidate of module.duplicateConstantCandidates) {
8352
+ const occurrence = {
8353
+ path: module.fileId.path,
8354
+ constantName: candidate.constantName,
8355
+ line: candidate.line,
8356
+ column: candidate.column
8357
+ };
8358
+ const existing = hashToBuckets.get(candidate.literalHash);
8359
+ if (existing) existing.occurrences.push(occurrence);
8360
+ else hashToBuckets.set(candidate.literalHash, {
8361
+ literalPreview: candidate.literalPreview,
8362
+ occurrences: [occurrence]
8363
+ });
8364
+ }
8365
+ }
8366
+ const findings = [];
8367
+ for (const [literalHash, bucket] of hashToBuckets) {
8368
+ const uniqueFilePaths = new Set(bucket.occurrences.map((occurrence) => occurrence.path));
8369
+ if (uniqueFilePaths.size < 3) continue;
8370
+ const uniqueNames = new Set(bucket.occurrences.map((occurrence) => occurrence.constantName));
8371
+ if (uniqueNames.size > 1 && hasDistinctUnitSuffixes([...uniqueNames])) continue;
8372
+ findings.push({
8373
+ literalHash,
8374
+ literalPreview: bucket.literalPreview,
8375
+ occurrences: bucket.occurrences,
8376
+ confidence: uniqueNames.size === 1 ? "high" : "medium",
8377
+ reason: uniqueNames.size === 1 ? `${bucket.occurrences.length} copies of \`const ${[...uniqueNames][0]} = ${bucket.literalPreview}\` across ${uniqueFilePaths.size} files — extract to a shared module` : `${bucket.occurrences.length} constants across ${uniqueFilePaths.size} files share the same literal value ${bucket.literalPreview} under different names (${[...uniqueNames].join(", ")}) — consider extracting`
8378
+ });
8379
+ }
8380
+ return findings;
8381
+ };
8382
+ const TRAILING_NAME_TOKEN_PATTERN = /_([A-Z][A-Z0-9]*)$/;
8383
+ const extractTrailingNameToken = (constantName) => {
8384
+ const match = constantName.match(TRAILING_NAME_TOKEN_PATTERN);
8385
+ return match ? match[1] : void 0;
8386
+ };
8387
+ /**
8388
+ * AGENTS.md requires magic numbers to use trailing unit suffixes (`_MS`, `_PX`,
8389
+ * `_TOKENS`, `_WIDTH`, …). When same-value constants carry DIFFERENT trailing
8390
+ * tokens (e.g. `STEP_DELAY_MS = 1000` vs `MINIMUM_TOKENS = 1000`), they
8391
+ * represent semantically distinct quantities that cannot be consolidated —
8392
+ * flagging them as duplicates is misleading. Constants sharing the same
8393
+ * trailing token (e.g. `CACHE_INTERVAL_MS` + `RECONNECT_DELAY_MS`, both `_MS`)
8394
+ * stay flagged because they are at least same-unit and might be extractable.
8395
+ */
8396
+ const hasDistinctUnitSuffixes = (constantNames) => {
8397
+ const trailingTokens = /* @__PURE__ */ new Set();
8398
+ for (const name of constantNames) {
8399
+ const token = extractTrailingNameToken(name);
8400
+ if (!token) return false;
8401
+ trailingTokens.add(token);
8402
+ }
8403
+ return trailingTokens.size > 1;
8404
+ };
8405
+ const detectSimplifiableExpressions = (graph) => {
8406
+ const findings = [];
8407
+ for (const module of graph.modules) {
8408
+ if (module.isDeclarationFile) continue;
8409
+ for (const parsedExpression of module.simplifiableExpressions) findings.push({
8410
+ path: module.fileId.path,
8411
+ kind: parsedExpression.kind,
8412
+ snippet: parsedExpression.snippet,
8413
+ line: parsedExpression.line,
8414
+ column: parsedExpression.column,
8415
+ confidence: parsedExpression.kind === "double-bang-boolean" || parsedExpression.kind === "ternary-returns-boolean" || parsedExpression.kind === "redundant-null-and-undefined-check" ? "high" : "medium",
8416
+ reason: parsedExpression.reason,
8417
+ suggestion: parsedExpression.suggestion
8418
+ });
8419
+ }
8420
+ return findings;
8421
+ };
8422
+ const detectSimplifiableFunctions = (graph) => {
8423
+ const findings = [];
8424
+ for (const module of graph.modules) {
8425
+ if (module.isDeclarationFile) continue;
8426
+ for (const parsedFunction of module.simplifiableFunctions) findings.push({
8427
+ path: module.fileId.path,
8428
+ kind: parsedFunction.kind,
8429
+ functionName: parsedFunction.functionName,
8430
+ line: parsedFunction.line,
8431
+ column: parsedFunction.column,
8432
+ confidence: parsedFunction.kind === "useless-async-no-await" ? "low" : "high",
8433
+ reason: parsedFunction.reason,
8434
+ suggestion: parsedFunction.suggestion
8435
+ });
8436
+ }
8437
+ return findings;
8438
+ };
8439
+ const detectDuplicateInlineTypes = (graph) => {
8440
+ const hashToOccurrences = /* @__PURE__ */ new Map();
8441
+ for (const module of graph.modules) {
8442
+ if (module.isDeclarationFile) continue;
8443
+ for (const inlineLiteral of module.inlineTypeLiterals) {
8444
+ const occurrence = {
8445
+ path: module.fileId.path,
8446
+ line: inlineLiteral.line,
8447
+ column: inlineLiteral.column,
8448
+ context: inlineLiteral.context,
8449
+ nearestName: inlineLiteral.nearestName
8450
+ };
8451
+ const existing = hashToOccurrences.get(inlineLiteral.structuralHash);
8452
+ if (existing) existing.occurrences.push(occurrence);
8453
+ else hashToOccurrences.set(inlineLiteral.structuralHash, {
8454
+ memberCount: inlineLiteral.memberCount,
8455
+ preview: inlineLiteral.preview,
8456
+ occurrences: [occurrence]
8457
+ });
8458
+ }
8459
+ }
8460
+ const findings = [];
8461
+ for (const [structuralHash, group] of hashToOccurrences) {
8462
+ if (group.occurrences.length < 2) continue;
8463
+ if (new Set(group.occurrences.map((occurrence) => `${occurrence.path}:${occurrence.line}`)).size < 2) continue;
8464
+ const uniquePaths = new Set(group.occurrences.map((occurrence) => occurrence.path));
8465
+ const confidence = uniquePaths.size >= 2 || group.memberCount >= 5 ? "medium" : "low";
8466
+ findings.push({
8467
+ structuralHash,
8468
+ memberCount: group.memberCount,
8469
+ preview: group.preview,
8470
+ occurrences: group.occurrences,
8471
+ confidence,
8472
+ reason: `inline object shape ${group.preview} appears at ${group.occurrences.length} sites across ${uniquePaths.size} file(s) — extract a named type`
8473
+ });
8474
+ }
8475
+ return findings;
8476
+ };
8477
+
8478
+ //#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)
8488
+ }));
8489
+ return input.fallback;
8490
+ }
8491
+ };
8492
+
8493
+ //#endregion
8494
+ //#region src/semantic/program.ts
8495
+ const failureFor = (reason, message, options = { rootDir: "" }) => {
8496
+ 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
+ })
8512
+ };
8513
+ };
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;
8523
+ }
8524
+ };
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 })
8530
+ };
8531
+ let configFileContent;
8532
+ try {
8533
+ configFileContent = typescript.default.readConfigFile(resolvedTsconfigPath, typescript.default.sys.readFile);
8534
+ } catch (readError) {
8535
+ return {
8536
+ ok: false,
8537
+ failure: failureFor("tsconfig-parse-error", "ts.readConfigFile threw", {
8538
+ rootDir: resolvedTsconfigPath,
8539
+ detail: describeUnknownError(readError)
8540
+ })
8541
+ };
8542
+ }
8543
+ if (configFileContent.error) return {
8544
+ ok: false,
8545
+ failure: failureFor("tsconfig-parse-error", typescript.default.flattenDiagnosticMessageText(configFileContent.error.messageText, "\n"), { rootDir: resolvedTsconfigPath })
8546
+ };
8547
+ let parsedCommandLine;
8548
+ try {
8549
+ parsedCommandLine = typescript.default.parseJsonConfigFileContent(configFileContent.config, typescript.default.sys, (0, node_path.dirname)(resolvedTsconfigPath), {
8550
+ noEmit: true,
8551
+ skipLibCheck: true,
8552
+ allowJs: true,
8553
+ isolatedModules: false
8554
+ }, resolvedTsconfigPath);
8555
+ } catch (parseError) {
8556
+ return {
8557
+ ok: false,
8558
+ failure: failureFor("tsconfig-parse-error", "ts.parseJsonConfigFileContent threw", {
8559
+ rootDir: resolvedTsconfigPath,
8560
+ detail: describeUnknownError(parseError)
8561
+ })
8562
+ };
8563
+ }
8564
+ if (parsedCommandLine.errors.length > 0) {
8565
+ const fatalErrors = parsedCommandLine.errors.filter((diagnostic) => diagnostic.category === typescript.default.DiagnosticCategory.Error);
8566
+ if (fatalErrors.length > 0 && parsedCommandLine.fileNames.length === 0) return {
8567
+ ok: false,
8568
+ failure: failureFor("tsconfig-parse-error", typescript.default.flattenDiagnosticMessageText(fatalErrors[0].messageText, "\n"), { rootDir: resolvedTsconfigPath })
8569
+ };
8570
+ }
8571
+ if (parsedCommandLine.fileNames.length > 5e3) return {
8572
+ ok: false,
8573
+ failure: failureFor("too-many-files", `Project has ${parsedCommandLine.fileNames.length} files, exceeds SEMANTIC_MAX_PROGRAM_FILES=${SEMANTIC_MAX_PROGRAM_FILES}`, { rootDir: resolvedTsconfigPath })
8574
+ };
8575
+ try {
8576
+ const program = typescript.default.createProgram({
8577
+ rootNames: parsedCommandLine.fileNames,
8578
+ options: parsedCommandLine.options,
8579
+ projectReferences: parsedCommandLine.projectReferences
8580
+ });
8581
+ return {
8582
+ ok: true,
8583
+ context: {
8584
+ program,
8585
+ checker: program.getTypeChecker(),
8586
+ rootSourceFiles: program.getSourceFiles().filter((sourceFile) => !sourceFile.isDeclarationFile || sourceFile.fileName.endsWith(".d.ts")),
8587
+ tsconfigPath: resolvedTsconfigPath
8588
+ }
8589
+ };
8590
+ } catch (programError) {
8591
+ return {
8592
+ ok: false,
8593
+ failure: failureFor("program-creation-failed", "ts.createProgram threw", {
8594
+ rootDir: resolvedTsconfigPath,
8595
+ detail: describeUnknownError(programError)
8596
+ })
8597
+ };
8598
+ }
8599
+ };
8600
+
8601
+ //#endregion
8602
+ //#region src/semantic/references.ts
8603
+ const canonicalKeyForSymbol = (symbol) => {
8604
+ return symbol.declarations?.[0] ?? symbol;
8605
+ };
8606
+ const isDeclarationNameIdentifier = (identifier) => {
8607
+ const parent = identifier.parent;
8608
+ if (!parent) return false;
8609
+ if ((typescript.default.isInterfaceDeclaration(parent) || typescript.default.isTypeAliasDeclaration(parent) || typescript.default.isClassDeclaration(parent) || typescript.default.isFunctionDeclaration(parent) || typescript.default.isEnumDeclaration(parent) || typescript.default.isModuleDeclaration(parent) || typescript.default.isVariableDeclaration(parent)) && parent.name === identifier) return true;
8610
+ if (typescript.default.isEnumMember(parent) && parent.name === identifier) return true;
8611
+ if (typescript.default.isPropertyDeclaration(parent) && parent.name === identifier) return true;
8612
+ if (typescript.default.isMethodDeclaration(parent) && parent.name === identifier) return true;
8613
+ if (typescript.default.isParameter(parent) && parent.name === identifier) return true;
8614
+ if (typescript.default.isBindingElement(parent) && parent.name === identifier) return true;
8615
+ return false;
8616
+ };
8617
+ const isExportSpecifierIdentifier = (identifier) => {
8618
+ const parent = identifier.parent;
8619
+ return Boolean(parent && typescript.default.isExportSpecifier(parent));
8620
+ };
8621
+ const isImportSpecifierIdentifier = (identifier) => {
8622
+ const parent = identifier.parent;
8623
+ if (!parent) return false;
8624
+ return typescript.default.isImportSpecifier(parent) || typescript.default.isImportClause(parent) || typescript.default.isNamespaceImport(parent);
8625
+ };
8626
+ const isInTypeContext = (identifier) => {
8627
+ let current = identifier.parent;
8628
+ let depth = 0;
8629
+ while (current && depth < 12) {
8630
+ if (typescript.default.isTypeReferenceNode(current) || typescript.default.isTypeQueryNode(current) || typescript.default.isTypeAliasDeclaration(current) || typescript.default.isInterfaceDeclaration(current) || typescript.default.isHeritageClause(current) || typescript.default.isImportTypeNode(current) || typescript.default.isTypePredicateNode(current) || typescript.default.isTypeOperatorNode(current) || typescript.default.isTypeLiteralNode(current) || typescript.default.isIndexedAccessTypeNode(current) || typescript.default.isMappedTypeNode(current) || typescript.default.isConditionalTypeNode(current) || typescript.default.isInferTypeNode(current)) return true;
8631
+ if (typescript.default.isExpressionStatement(current) || typescript.default.isBlock(current)) return false;
8632
+ current = current.parent;
8633
+ depth++;
8634
+ }
8635
+ return false;
8636
+ };
8637
+ const resolveSymbolForIdentifier = (identifier, checker) => {
8638
+ let symbol;
8639
+ try {
8640
+ symbol = checker.getSymbolAtLocation(identifier);
8641
+ } catch {
8642
+ return;
8643
+ }
8644
+ if (!symbol) return void 0;
8645
+ if (symbol.flags & typescript.default.SymbolFlags.Alias) try {
8646
+ return checker.getAliasedSymbol(symbol);
8647
+ } catch {
8648
+ return symbol;
8649
+ }
8650
+ return symbol;
8651
+ };
8652
+ const visitJsDocNodes = (node, visit) => {
8653
+ const jsDocContainer = node;
8654
+ if (!jsDocContainer.jsDoc) return;
8655
+ for (const jsDocNode of jsDocContainer.jsDoc) visit(jsDocNode);
8656
+ };
8657
+ const buildReferenceIndex = (program, checker) => {
8658
+ const keyedToReferences = /* @__PURE__ */ new Map();
8659
+ const recordIdentifier = (identifier, sourceFile) => {
8660
+ const resolvedSymbol = resolveSymbolForIdentifier(identifier, checker);
8661
+ if (!resolvedSymbol) return;
8662
+ const key = canonicalKeyForSymbol(resolvedSymbol);
8663
+ const site = {
8664
+ sourceFile,
8665
+ identifier,
8666
+ isDeclarationName: isDeclarationNameIdentifier(identifier),
8667
+ isExportSpecifier: isExportSpecifierIdentifier(identifier),
8668
+ isImportSpecifier: isImportSpecifierIdentifier(identifier),
8669
+ isTypeContext: isInTypeContext(identifier)
8670
+ };
8671
+ const existing = keyedToReferences.get(key);
8672
+ if (existing) existing.push(site);
8673
+ else keyedToReferences.set(key, [site]);
8674
+ };
8675
+ const visitNode = (node, sourceFile, recursionDepth) => {
8676
+ if (recursionDepth > 200) return;
8677
+ if (typescript.default.isIdentifier(node)) recordIdentifier(node, sourceFile);
8678
+ visitJsDocNodes(node, (jsDocNode) => visitNode(jsDocNode, sourceFile, recursionDepth + 1));
8679
+ node.forEachChild((child) => visitNode(child, sourceFile, recursionDepth + 1));
8680
+ };
8681
+ for (const sourceFile of program.getSourceFiles()) {
8682
+ if (sourceFile.isDeclarationFile) continue;
8683
+ visitNode(sourceFile, sourceFile, 0);
8684
+ }
8685
+ return {
8686
+ getReferences: (symbol) => keyedToReferences.get(canonicalKeyForSymbol(symbol)) ?? [],
8687
+ size: keyedToReferences.size
8688
+ };
8689
+ };
8690
+
8691
+ //#endregion
8692
+ //#region src/semantic/utils/source-file-lookup.ts
8693
+ const normalizeSourcePath = node_path.resolve;
8694
+ const buildSourceFileLookup = (program) => {
8695
+ const lookup = /* @__PURE__ */ new Map();
8696
+ for (const sourceFile of program.getSourceFiles()) {
8697
+ if (sourceFile.isDeclarationFile) continue;
8698
+ lookup.set(normalizeSourcePath(sourceFile.fileName), sourceFile);
8699
+ }
8700
+ return lookup;
8701
+ };
8702
+
8703
+ //#endregion
8704
+ //#region src/semantic/unused-types.ts
8705
+ const TYPE_DECLARATION_FLAGS = typescript.default.SymbolFlags.Interface | typescript.default.SymbolFlags.TypeAlias | typescript.default.SymbolFlags.Enum | typescript.default.SymbolFlags.ConstEnum | typescript.default.SymbolFlags.RegularEnum;
8706
+ const VALUE_DECLARATION_FLAGS = typescript.default.SymbolFlags.Variable | typescript.default.SymbolFlags.Function | typescript.default.SymbolFlags.Class | typescript.default.SymbolFlags.BlockScopedVariable | typescript.default.SymbolFlags.FunctionScopedVariable;
8707
+ const collectTypeExportCandidates = (graph, config) => {
8708
+ const candidates = [];
8709
+ for (const module of graph.modules) {
8710
+ if (!module.isReachable) continue;
8711
+ if (module.isDeclarationFile) continue;
8712
+ if (module.isEntryPoint && !config.includeEntryExports) continue;
8713
+ for (const exportInfo of module.exports) {
8714
+ if (exportInfo.isSynthetic) continue;
8715
+ if (!exportInfo.isTypeOnly) continue;
8716
+ if (exportInfo.isReExport) continue;
8717
+ if (exportInfo.name === "*") continue;
8718
+ candidates.push({
8719
+ module,
8720
+ exportName: exportInfo.name,
8721
+ line: exportInfo.line,
8722
+ column: exportInfo.column
8723
+ });
8724
+ }
8725
+ }
8726
+ return candidates;
8727
+ };
8728
+ const resolveExportSymbol = (sourceFile, exportName, checker) => {
8729
+ const moduleSymbol = checker.getSymbolAtLocation(sourceFile);
8730
+ if (!moduleSymbol) return void 0;
8731
+ const matchingExport = checker.getExportsOfModule(moduleSymbol).find((exportSymbol) => exportSymbol.name === exportName);
8732
+ if (!matchingExport) return void 0;
8733
+ if (matchingExport.flags & typescript.default.SymbolFlags.Alias) try {
8734
+ return checker.getAliasedSymbol(matchingExport);
8735
+ } catch {
8736
+ return matchingExport;
8737
+ }
8738
+ return matchingExport;
8739
+ };
8740
+ const isPureTypeSymbol = (symbol) => {
8741
+ const hasTypeFlags = (symbol.flags & TYPE_DECLARATION_FLAGS) !== 0;
8742
+ const hasValueFlags = (symbol.flags & VALUE_DECLARATION_FLAGS) !== 0;
8743
+ return hasTypeFlags && !hasValueFlags;
8744
+ };
8745
+ const classifyTypeKind = (symbol) => {
8746
+ if (symbol.flags & typescript.default.SymbolFlags.Interface) return "interface";
8747
+ if (symbol.flags & typescript.default.SymbolFlags.TypeAlias) return "type-alias";
8748
+ if (symbol.flags & (typescript.default.SymbolFlags.Enum | typescript.default.SymbolFlags.ConstEnum | typescript.default.SymbolFlags.RegularEnum)) return "enum-type";
8749
+ };
8750
+ const isReferenceMeaningful = (site) => {
8751
+ if (site.isDeclarationName) return false;
8752
+ return true;
8753
+ };
8754
+ const buildTrace = (candidate, meaningfulReferenceCount, totalReferenceCount, reExportSiteCount) => {
8755
+ return [
8756
+ `${candidate.module.fileId.path}:${candidate.line}:${candidate.column} declares "${candidate.exportName}"`,
8757
+ `total identifier references resolved to symbol: ${totalReferenceCount}`,
8758
+ `references excluding declaration site: ${meaningfulReferenceCount}`,
8759
+ `re-export specifier sites: ${reExportSiteCount}`
8760
+ ].slice(0, 5);
8761
+ };
8762
+ const detectUnusedTypes = (graph, config, context, referenceIndex) => {
8763
+ const findings = [];
8764
+ const candidates = collectTypeExportCandidates(graph, config);
8765
+ if (candidates.length === 0) return findings;
8766
+ const sourceFileLookup = buildSourceFileLookup(context.program);
8767
+ for (const candidate of candidates) {
8768
+ const sourceFile = sourceFileLookup.get(normalizeSourcePath(candidate.module.fileId.path));
8769
+ if (!sourceFile) continue;
8770
+ const exportSymbol = resolveExportSymbol(sourceFile, candidate.exportName, context.checker);
8771
+ if (!exportSymbol) continue;
8772
+ if (!isPureTypeSymbol(exportSymbol)) continue;
8773
+ const kind = classifyTypeKind(exportSymbol);
8774
+ if (!kind) continue;
8775
+ const allReferences = referenceIndex.getReferences(exportSymbol);
8776
+ const reExportSites = allReferences.filter((site) => site.isExportSpecifier);
8777
+ const meaningfulReferences = allReferences.filter(isReferenceMeaningful);
8778
+ if (meaningfulReferences.filter((site) => !site.isExportSpecifier).length > 0) continue;
8779
+ const declarations = exportSymbol.declarations ?? [];
8780
+ if (declarations.length > 1) {
8781
+ const declarationFiles = new Set(declarations.map((decl) => normalizeSourcePath(decl.getSourceFile().fileName)));
8782
+ if (declarationFiles.size > 1) {
8783
+ if (meaningfulReferences.some((site) => {
8784
+ const referenceFileName = normalizeSourcePath(site.sourceFile.fileName);
8785
+ return !declarationFiles.has(referenceFileName);
8786
+ })) continue;
8787
+ }
8788
+ }
8789
+ findings.push({
8790
+ path: candidate.module.fileId.path,
8791
+ name: candidate.exportName,
8792
+ line: candidate.line,
8793
+ column: candidate.column,
8794
+ kind,
8795
+ confidence: reExportSites.length > 0 ? "medium" : "high",
8796
+ reason: reExportSites.length > 0 ? `type "${candidate.exportName}" is only re-exported through ${reExportSites.length} barrel(s) and never used` : `type "${candidate.exportName}" has no references in the project`,
8797
+ trace: buildTrace(candidate, meaningfulReferences.length, allReferences.length, reExportSites.length)
8798
+ });
8799
+ }
8800
+ return findings;
8801
+ };
8802
+
8803
+ //#endregion
8804
+ //#region src/semantic/unused-enum-members.ts
8805
+ const collectEnumDeclarations = (graph, config, sourceFileLookup) => {
8806
+ const declarations = [];
8807
+ const visitTopLevel = (sourceFile, modulePath) => {
8808
+ for (const statement of sourceFile.statements) if (typescript.default.isEnumDeclaration(statement)) declarations.push({
8809
+ sourceFile,
8810
+ declaration: statement,
8811
+ modulePath
8812
+ });
8813
+ };
8814
+ for (const module of graph.modules) {
8815
+ if (!module.isReachable) continue;
8816
+ if (module.isDeclarationFile) continue;
8817
+ if (module.isEntryPoint && !config.includeEntryExports) continue;
8818
+ const sourceFile = sourceFileLookup.get(normalizeSourcePath(module.fileId.path));
8819
+ if (!sourceFile) continue;
8820
+ visitTopLevel(sourceFile, module.fileId.path);
8821
+ }
8822
+ return declarations;
8823
+ };
8824
+ const isStringLiteralEnum = (declaration) => {
8825
+ if (declaration.members.length === 0) return false;
8826
+ for (const member of declaration.members) {
8827
+ if (!member.initializer) return false;
8828
+ if (!typescript.default.isStringLiteral(member.initializer)) return false;
8829
+ }
8830
+ return true;
8831
+ };
8832
+ const isConstEnum = (declaration) => {
8833
+ const modifiers = typescript.default.canHaveModifiers(declaration) ? typescript.default.getModifiers(declaration) : void 0;
8834
+ if (!modifiers) return false;
8835
+ return modifiers.some((modifier) => modifier.kind === typescript.default.SyntaxKind.ConstKeyword);
8836
+ };
8837
+ const enumHasComputedAccess = (enumSymbol, referenceIndex) => {
8838
+ const references = referenceIndex.getReferences(enumSymbol);
8839
+ for (const referenceSite of references) {
8840
+ const parent = referenceSite.identifier.parent;
8841
+ if (!parent) continue;
8842
+ if (typescript.default.isElementAccessExpression(parent) && parent.expression === referenceSite.identifier) return true;
8843
+ }
8844
+ return false;
8845
+ };
8846
+ const enumHasWholeObjectUse = (enumSymbol, referenceIndex) => {
8847
+ const references = referenceIndex.getReferences(enumSymbol);
8848
+ for (const referenceSite of references) {
8849
+ if (referenceSite.isDeclarationName) continue;
8850
+ if (referenceSite.isExportSpecifier) continue;
8851
+ if (referenceSite.isImportSpecifier) continue;
8852
+ const parent = referenceSite.identifier.parent;
8853
+ if (!parent) continue;
8854
+ if (typescript.default.isPropertyAccessExpression(parent) && parent.expression === referenceSite.identifier) continue;
8855
+ if (typescript.default.isQualifiedName(parent) && parent.left === referenceSite.identifier) continue;
8856
+ if (typescript.default.isElementAccessExpression(parent) && parent.expression === referenceSite.identifier) continue;
8857
+ if (typescript.default.isTypeReferenceNode(parent)) continue;
8858
+ if (typescript.default.isTypeQueryNode(parent)) continue;
8859
+ return true;
8860
+ }
8861
+ return false;
8862
+ };
8863
+ const memberHasExternalReference$1 = (memberSymbol, referenceIndex) => {
8864
+ const references = referenceIndex.getReferences(memberSymbol);
8865
+ for (const referenceSite of references) {
8866
+ if (referenceSite.isDeclarationName) continue;
8867
+ return true;
8868
+ }
8869
+ return false;
8870
+ };
8871
+ const buildEnumMemberTrace = (enumName, memberName, declarationPath, line, column, hasComputedAccess, hasWholeObjectUse) => {
8872
+ const trace = [`${declarationPath}:${line}:${column} declares ${enumName}.${memberName}`, `no static \`${enumName}.${memberName}\` reference found in the project`];
8873
+ if (hasComputedAccess) trace.push(`${enumName}[...] computed access observed — confidence downgraded`);
8874
+ if (hasWholeObjectUse) trace.push(`${enumName} used as a whole value — confidence downgraded`);
8875
+ return trace.slice(0, 5);
8876
+ };
8877
+ const detectUnusedEnumMembers = (graph, config, context, referenceIndex) => {
8878
+ const findings = [];
8879
+ const enumDeclarations = collectEnumDeclarations(graph, config, buildSourceFileLookup(context.program));
8880
+ if (enumDeclarations.length === 0) return findings;
8881
+ const { checker } = context;
8882
+ for (const { sourceFile, declaration, modulePath } of enumDeclarations) {
8883
+ const enumSymbol = checker.getSymbolAtLocation(declaration.name);
8884
+ if (!enumSymbol) continue;
8885
+ const hasComputedAccess = enumHasComputedAccess(enumSymbol, referenceIndex);
8886
+ const hasWholeObjectUse = enumHasWholeObjectUse(enumSymbol, referenceIndex);
8887
+ const isPureStringEnum = isStringLiteralEnum(declaration);
8888
+ const isConst = isConstEnum(declaration);
8889
+ if (hasWholeObjectUse) continue;
8890
+ if (hasComputedAccess) continue;
8891
+ let confidence;
8892
+ if (isConst) confidence = "low";
8893
+ else if (isPureStringEnum) confidence = "high";
8894
+ else confidence = "medium";
8895
+ for (const member of declaration.members) {
8896
+ const memberSymbol = checker.getSymbolAtLocation(member.name);
8897
+ if (!memberSymbol) continue;
8898
+ if (memberHasExternalReference$1(memberSymbol, referenceIndex)) continue;
8899
+ const memberName = member.name.getText(sourceFile);
8900
+ const { line: zeroIndexedLine, character: zeroIndexedColumn } = sourceFile.getLineAndCharacterOfPosition(member.getStart(sourceFile));
8901
+ const line = zeroIndexedLine + 1;
8902
+ const column = zeroIndexedColumn + 1;
8903
+ findings.push({
8904
+ path: modulePath,
8905
+ enumName: declaration.name.text,
8906
+ memberName,
8907
+ line,
8908
+ column,
8909
+ confidence,
8910
+ reason: `${declaration.name.text}.${memberName} is declared but never referenced`,
8911
+ trace: buildEnumMemberTrace(declaration.name.text, memberName, modulePath, line, column, false, false)
8912
+ });
8913
+ }
8914
+ }
8915
+ return findings;
8916
+ };
8917
+
8918
+ //#endregion
8919
+ //#region src/semantic/unused-class-members.ts
8920
+ const isClassExported = (declaration) => {
8921
+ const modifiers = typescript.default.canHaveModifiers(declaration) ? typescript.default.getModifiers(declaration) : void 0;
8922
+ if (!modifiers) return false;
8923
+ return modifiers.some((modifier) => modifier.kind === typescript.default.SyntaxKind.ExportKeyword || modifier.kind === typescript.default.SyntaxKind.DefaultKeyword);
8924
+ };
8925
+ const collectClassDeclarations = (graph, config, sourceFileLookup) => {
8926
+ const contexts = [];
8927
+ for (const module of graph.modules) {
8928
+ if (!module.isReachable) continue;
8929
+ if (module.isDeclarationFile) continue;
8930
+ if (module.isEntryPoint && !config.includeEntryExports) continue;
8931
+ const sourceFile = sourceFileLookup.get(normalizeSourcePath(module.fileId.path));
8932
+ if (!sourceFile) continue;
8933
+ for (const statement of sourceFile.statements) {
8934
+ if (!typescript.default.isClassDeclaration(statement)) continue;
8935
+ if (!statement.name) continue;
8936
+ contexts.push({
8937
+ sourceFile,
8938
+ declaration: statement,
8939
+ modulePath: module.fileId.path,
8940
+ isExported: isClassExported(statement)
8941
+ });
8942
+ }
8943
+ }
8944
+ return contexts;
8945
+ };
8946
+ const buildSubclassMemberIndex = (classContexts, checker) => {
8947
+ const parentToOverriddenMemberNames = /* @__PURE__ */ new Map();
8948
+ const addOverrideNames = (parentSymbol, memberNames) => {
8949
+ const existing = parentToOverriddenMemberNames.get(parentSymbol);
8950
+ if (existing) for (const memberName of memberNames) existing.add(memberName);
8951
+ else parentToOverriddenMemberNames.set(parentSymbol, new Set(memberNames));
8952
+ };
8953
+ const collectMemberNames = (declaration) => {
8954
+ const names = [];
8955
+ for (const member of declaration.members) {
8956
+ if (!member.name || !typescript.default.isIdentifier(member.name)) continue;
8957
+ names.push(member.name.text);
8958
+ }
8959
+ return names;
8960
+ };
8961
+ for (const { declaration } of classContexts) {
8962
+ if (!declaration.heritageClauses) continue;
8963
+ for (const heritageClause of declaration.heritageClauses) {
8964
+ if (heritageClause.token !== typescript.default.SyntaxKind.ExtendsKeyword) continue;
8965
+ for (const heritageType of heritageClause.types) {
8966
+ const baseSymbol = checker.getSymbolAtLocation(heritageType.expression);
8967
+ if (!baseSymbol) continue;
8968
+ const resolvedBaseSymbol = baseSymbol.flags & typescript.default.SymbolFlags.Alias ? safeGetAliasedSymbol$1(baseSymbol, checker) : baseSymbol;
8969
+ if (!resolvedBaseSymbol) continue;
8970
+ addOverrideNames(resolvedBaseSymbol, collectMemberNames(declaration));
8971
+ }
8972
+ }
8973
+ }
8974
+ return { getOverridingMemberNames: (parentClassSymbol) => parentToOverriddenMemberNames.get(parentClassSymbol) ?? /* @__PURE__ */ new Set() };
6144
8975
  };
6145
- const canonicalizeCycle = (cycle, graph) => {
6146
- if (cycle.length === 0) return [];
6147
- let minPosition = 0;
6148
- let minPath = graph.modules[cycle[0]].fileId.path;
6149
- for (let position = 1; position < cycle.length; position++) {
6150
- const currentPath = graph.modules[cycle[position]].fileId.path;
6151
- if (currentPath < minPath) {
6152
- minPath = currentPath;
6153
- minPosition = position;
8976
+ const safeGetAliasedSymbol$1 = (symbol, checker) => {
8977
+ try {
8978
+ return checker.getAliasedSymbol(symbol);
8979
+ } catch {
8980
+ return;
8981
+ }
8982
+ };
8983
+ const isPrivateMember = (member) => {
8984
+ if (typescript.default.isPrivateIdentifier(member.name)) return true;
8985
+ const modifiers = typescript.default.canHaveModifiers(member) ? typescript.default.getModifiers(member) : void 0;
8986
+ if (!modifiers) return false;
8987
+ return modifiers.some((modifier) => modifier.kind === typescript.default.SyntaxKind.PrivateKeyword);
8988
+ };
8989
+ const isStaticMember = (member) => {
8990
+ const modifiers = typescript.default.canHaveModifiers(member) ? typescript.default.getModifiers(member) : void 0;
8991
+ if (!modifiers) return false;
8992
+ return modifiers.some((modifier) => modifier.kind === typescript.default.SyntaxKind.StaticKeyword);
8993
+ };
8994
+ const hasAllowedDecorator = (member, decoratorAllowlist) => {
8995
+ const decorators = typescript.default.canHaveDecorators(member) ? typescript.default.getDecorators(member) : void 0;
8996
+ if (!decorators || decorators.length === 0) return false;
8997
+ for (const decorator of decorators) {
8998
+ const expression = decorator.expression;
8999
+ let decoratorName;
9000
+ if (typescript.default.isIdentifier(expression)) decoratorName = expression.text;
9001
+ else if (typescript.default.isCallExpression(expression) && typescript.default.isIdentifier(expression.expression)) decoratorName = expression.expression.text;
9002
+ else if (typescript.default.isPropertyAccessExpression(expression) && typescript.default.isIdentifier(expression.name)) decoratorName = expression.name.text;
9003
+ if (decoratorName && decoratorAllowlist.has(decoratorName)) return true;
9004
+ }
9005
+ return false;
9006
+ };
9007
+ const classifyMemberKind = (member) => {
9008
+ if (typescript.default.isMethodDeclaration(member)) return "method";
9009
+ if (typescript.default.isPropertyDeclaration(member)) return "property";
9010
+ if (typescript.default.isGetAccessorDeclaration(member) || typescript.default.isSetAccessorDeclaration(member)) return "accessor";
9011
+ };
9012
+ const memberHasExternalReference = (memberSymbol, referenceIndex) => {
9013
+ const references = referenceIndex.getReferences(memberSymbol);
9014
+ for (const referenceSite of references) {
9015
+ if (referenceSite.isDeclarationName) continue;
9016
+ return true;
9017
+ }
9018
+ return false;
9019
+ };
9020
+ const buildClassMemberTrace = (className, memberName, modulePath, line, column, isOverriddenInSubclass, isExportedClass) => {
9021
+ const trace = [`${modulePath}:${line}:${column} declares ${className}.${memberName}`, `no \`${className}.${memberName}\` reference found outside the declaration`];
9022
+ if (isExportedClass) trace.push(`${className} is exported — confidence reduced for public-API safety`);
9023
+ if (isOverriddenInSubclass) trace.push(`subclass override observed — polymorphic call path possible`);
9024
+ return trace.slice(0, 5);
9025
+ };
9026
+ const detectUnusedClassMembers = (graph, config, context, referenceIndex, decoratorAllowlist) => {
9027
+ const findings = [];
9028
+ const classContexts = collectClassDeclarations(graph, config, buildSourceFileLookup(context.program));
9029
+ if (classContexts.length === 0) return findings;
9030
+ const { checker } = context;
9031
+ const decoratorAllowSet = new Set(decoratorAllowlist);
9032
+ const subclassMemberIndex = buildSubclassMemberIndex(classContexts, checker);
9033
+ for (const { sourceFile, declaration, modulePath, isExported } of classContexts) {
9034
+ const classSymbol = checker.getSymbolAtLocation(declaration.name);
9035
+ if (!classSymbol) continue;
9036
+ const overriddenMemberNames = subclassMemberIndex.getOverridingMemberNames(classSymbol);
9037
+ for (const member of declaration.members) {
9038
+ if (typescript.default.isConstructorDeclaration(member)) continue;
9039
+ if (!member.name) continue;
9040
+ const memberKind = classifyMemberKind(member);
9041
+ if (!memberKind) continue;
9042
+ if (isPrivateMember(member)) continue;
9043
+ if (hasAllowedDecorator(member, decoratorAllowSet)) continue;
9044
+ const memberSymbol = checker.getSymbolAtLocation(member.name);
9045
+ if (!memberSymbol) continue;
9046
+ if (memberHasExternalReference(memberSymbol, referenceIndex)) continue;
9047
+ const memberName = typescript.default.isIdentifier(member.name) ? member.name.text : member.name.getText(sourceFile);
9048
+ if (overriddenMemberNames.has(memberName)) continue;
9049
+ const { line: zeroIndexedLine, character: zeroIndexedColumn } = sourceFile.getLineAndCharacterOfPosition(member.getStart(sourceFile));
9050
+ const line = zeroIndexedLine + 1;
9051
+ const column = zeroIndexedColumn + 1;
9052
+ const confidence = isExported ? "low" : "high";
9053
+ findings.push({
9054
+ path: modulePath,
9055
+ className: declaration.name.text,
9056
+ memberName,
9057
+ memberKind,
9058
+ isStatic: isStaticMember(member),
9059
+ line,
9060
+ column,
9061
+ confidence,
9062
+ reason: isExported ? `${declaration.name.text}.${memberName} has no internal references; flagged at low confidence because ${declaration.name.text} is part of the public API surface` : `${declaration.name.text}.${memberName} is declared but never referenced`,
9063
+ trace: buildClassMemberTrace(declaration.name.text, memberName, modulePath, line, column, false, isExported)
9064
+ });
6154
9065
  }
6155
9066
  }
6156
- return [...cycle.slice(minPosition), ...cycle.slice(0, minPosition)];
9067
+ return findings;
6157
9068
  };
6158
- const enumerateElementaryCycles = (componentNodes, adjacencyList, graph) => {
6159
- if (componentNodes.length === 2) {
6160
- const [nodeA, nodeB] = componentNodes;
6161
- return [graph.modules[nodeA].fileId.path <= graph.modules[nodeB].fileId.path ? [nodeA, nodeB] : [nodeB, nodeA]];
9069
+
9070
+ //#endregion
9071
+ //#region src/semantic/misclassified-dependencies.ts
9072
+ const TYPES_PACKAGE_PREFIX = "@types/";
9073
+ const recordImportSite = (summary, sitePath) => {
9074
+ if (summary.importSites.length >= 5) return;
9075
+ if (summary.importSites.includes(sitePath)) return;
9076
+ summary.importSites.push(sitePath);
9077
+ };
9078
+ const isImportEffectivelyTypeOnly = (isTypeOnlyDeclaration, importedBindings) => {
9079
+ if (isTypeOnlyDeclaration) return true;
9080
+ if (importedBindings.length === 0) return false;
9081
+ return importedBindings.every((binding) => binding.isTypeOnly);
9082
+ };
9083
+ const collectPackageUsageSummaries = (graph) => {
9084
+ const summaries = /* @__PURE__ */ new Map();
9085
+ const upsertSummary = (packageName) => {
9086
+ const existing = summaries.get(packageName);
9087
+ if (existing) return existing;
9088
+ const fresh = {
9089
+ packageName,
9090
+ hasValueUse: false,
9091
+ hasTypeOnlyUse: false,
9092
+ importSites: []
9093
+ };
9094
+ summaries.set(packageName, fresh);
9095
+ return fresh;
9096
+ };
9097
+ for (const module of graph.modules) {
9098
+ for (const importInfo of module.imports) {
9099
+ const packageName = extractPackageName(importInfo.specifier);
9100
+ if (!packageName) continue;
9101
+ const summary = upsertSummary(packageName);
9102
+ const sitePath = `${module.fileId.path}:${importInfo.line}`;
9103
+ if (importInfo.isSideEffect) {
9104
+ summary.hasValueUse = true;
9105
+ recordImportSite(summary, sitePath);
9106
+ continue;
9107
+ }
9108
+ if (importInfo.isDynamic) {
9109
+ summary.hasValueUse = true;
9110
+ recordImportSite(summary, sitePath);
9111
+ continue;
9112
+ }
9113
+ if (isImportEffectivelyTypeOnly(importInfo.isTypeOnly, importInfo.importedNames)) summary.hasTypeOnlyUse = true;
9114
+ else summary.hasValueUse = true;
9115
+ recordImportSite(summary, sitePath);
9116
+ }
9117
+ for (const exportInfo of module.exports) {
9118
+ if (!exportInfo.isReExport || !exportInfo.reExportSource) continue;
9119
+ const packageName = extractPackageName(exportInfo.reExportSource);
9120
+ if (!packageName) continue;
9121
+ const summary = upsertSummary(packageName);
9122
+ const sitePath = `${module.fileId.path}:${exportInfo.line}`;
9123
+ if (exportInfo.isTypeOnly) summary.hasTypeOnlyUse = true;
9124
+ else summary.hasValueUse = true;
9125
+ recordImportSite(summary, sitePath);
9126
+ }
6162
9127
  }
6163
- const componentSet = new Set(componentNodes);
6164
- const cycles = [];
6165
- const seenKeys = /* @__PURE__ */ new Set();
6166
- for (const startNode of componentNodes) {
6167
- if (cycles.length >= 20) break;
6168
- const visitedInThisSearch = /* @__PURE__ */ new Set();
6169
- visitedInThisSearch.add(startNode);
6170
- const pathStack = [startNode];
6171
- const successorPositionStack = [0];
6172
- while (pathStack.length > 0 && cycles.length < 20) {
6173
- const currentNode = pathStack[pathStack.length - 1];
6174
- const currentSuccessorPosition = successorPositionStack[successorPositionStack.length - 1];
6175
- const successors = adjacencyList[currentNode].filter((successor) => componentSet.has(successor));
6176
- if (currentSuccessorPosition < successors.length) {
6177
- successorPositionStack[successorPositionStack.length - 1]++;
6178
- const successor = successors[currentSuccessorPosition];
6179
- if (successor === startNode) {
6180
- const canonical = canonicalizeCycle([...pathStack], graph);
6181
- const key = canonical.join(",");
6182
- if (!seenKeys.has(key)) {
6183
- seenKeys.add(key);
6184
- cycles.push(canonical);
6185
- }
6186
- } else if (!visitedInThisSearch.has(successor)) {
6187
- visitedInThisSearch.add(successor);
6188
- pathStack.push(successor);
6189
- successorPositionStack.push(0);
6190
- }
6191
- } else {
6192
- visitedInThisSearch.delete(pathStack.pop());
6193
- successorPositionStack.pop();
9128
+ return summaries;
9129
+ };
9130
+ const readDeclaredDependencies = (rootDir) => {
9131
+ const packageJsonPath = (0, node_path.resolve)(rootDir, "package.json");
9132
+ let packageJson;
9133
+ try {
9134
+ const contents = (0, node_fs.readFileSync)(packageJsonPath, "utf-8");
9135
+ packageJson = JSON.parse(contents);
9136
+ } catch {
9137
+ return [];
9138
+ }
9139
+ const entries = [];
9140
+ for (const name of Object.keys(packageJson.dependencies ?? {})) entries.push({
9141
+ name,
9142
+ declaredAs: "dependencies"
9143
+ });
9144
+ return entries;
9145
+ };
9146
+ const detectMisclassifiedDependencies = (graph, config) => {
9147
+ const declaredEntries = readDeclaredDependencies(config.rootDir);
9148
+ if (declaredEntries.length === 0) return [];
9149
+ const packageUsage = collectPackageUsageSummaries(graph);
9150
+ const findings = [];
9151
+ for (const declaredEntry of declaredEntries) {
9152
+ const usage = packageUsage.get(declaredEntry.name);
9153
+ if (!usage) continue;
9154
+ if (usage.hasValueUse) continue;
9155
+ if (!usage.hasTypeOnlyUse) continue;
9156
+ const isTypesPackage = declaredEntry.name.startsWith(TYPES_PACKAGE_PREFIX);
9157
+ findings.push({
9158
+ name: declaredEntry.name,
9159
+ declaredAs: declaredEntry.declaredAs,
9160
+ suggestedAs: "devDependencies",
9161
+ confidence: isTypesPackage ? "high" : "medium",
9162
+ reason: isTypesPackage ? `"${declaredEntry.name}" is a @types/* package in dependencies but is only consumed via type imports — should be in devDependencies` : `"${declaredEntry.name}" is in dependencies but only consumed via \`import type\` / \`export type\` — consider devDependencies (or keep here if downstream consumers need its types)`,
9163
+ trace: usage.importSites
9164
+ });
9165
+ }
9166
+ return findings;
9167
+ };
9168
+
9169
+ //#endregion
9170
+ //#region src/semantic/variable-aliases.ts
9171
+ const isSimpleIdentifierInitializer = (initializer) => Boolean(initializer && typescript.default.isIdentifier(initializer));
9172
+ const isModuleLevelDeclaration = (declaration) => {
9173
+ const variableDeclarationList = declaration.parent;
9174
+ if (!variableDeclarationList || !typescript.default.isVariableDeclarationList(variableDeclarationList)) return false;
9175
+ const statement = variableDeclarationList.parent;
9176
+ return Boolean(statement && typescript.default.isSourceFile(statement.parent));
9177
+ };
9178
+ const isDeclarationExported = (declaration) => {
9179
+ const variableDeclarationList = declaration.parent;
9180
+ if (!variableDeclarationList || !typescript.default.isVariableDeclarationList(variableDeclarationList)) return false;
9181
+ const statement = variableDeclarationList.parent;
9182
+ if (!statement || !typescript.default.isVariableStatement(statement)) return false;
9183
+ const modifiers = typescript.default.canHaveModifiers(statement) ? typescript.default.getModifiers(statement) : void 0;
9184
+ if (!modifiers) return false;
9185
+ return modifiers.some((modifier) => modifier.kind === typescript.default.SyntaxKind.ExportKeyword || modifier.kind === typescript.default.SyntaxKind.DefaultKeyword);
9186
+ };
9187
+ const collectVariableAliasCandidates = (graph, sourceFileLookup) => {
9188
+ const candidates = [];
9189
+ for (const module of graph.modules) {
9190
+ if (!module.isReachable) continue;
9191
+ if (module.isDeclarationFile) continue;
9192
+ const sourceFile = sourceFileLookup.get(normalizeSourcePath(module.fileId.path));
9193
+ if (!sourceFile) continue;
9194
+ for (const statement of sourceFile.statements) {
9195
+ if (!typescript.default.isVariableStatement(statement)) continue;
9196
+ for (const declaration of statement.declarationList.declarations) {
9197
+ if (!typescript.default.isIdentifier(declaration.name)) continue;
9198
+ if (!isSimpleIdentifierInitializer(declaration.initializer)) continue;
9199
+ if (!isModuleLevelDeclaration(declaration)) continue;
9200
+ const aliasName = declaration.name.text;
9201
+ const aliasedFromName = declaration.initializer.text;
9202
+ if (aliasName === aliasedFromName) continue;
9203
+ candidates.push({
9204
+ sourceFile,
9205
+ declaration,
9206
+ aliasName,
9207
+ aliasedFromName,
9208
+ modulePath: module.fileId.path
9209
+ });
6194
9210
  }
6195
9211
  }
6196
9212
  }
6197
- return cycles;
9213
+ return candidates;
6198
9214
  };
6199
- const detectCycles = (graph) => {
6200
- const adjacencyList = buildAdjacencyList(graph);
6201
- const components = findStronglyConnectedComponents(adjacencyList);
6202
- const allCycles = [];
6203
- const seenKeys = /* @__PURE__ */ new Set();
6204
- const sortedComponents = [...components].sort((componentA, componentB) => componentA.length - componentB.length);
6205
- for (const component of sortedComponents) {
6206
- if (allCycles.length >= 200) break;
6207
- if (component.length > 50) continue;
6208
- const elementaryCycles = enumerateElementaryCycles(component, adjacencyList, graph);
6209
- for (const cycle of elementaryCycles) {
6210
- const key = cycle.join(",");
6211
- if (!seenKeys.has(key)) {
6212
- seenKeys.add(key);
6213
- allCycles.push(cycle);
9215
+ const isMeaningfulReference = (site) => {
9216
+ if (site.isDeclarationName) return false;
9217
+ if (site.isImportSpecifier) return false;
9218
+ if (site.isExportSpecifier) return false;
9219
+ return true;
9220
+ };
9221
+ const resolveThroughAliasChain = (symbol, checker) => {
9222
+ if (symbol.flags & typescript.default.SymbolFlags.Alias) try {
9223
+ return checker.getAliasedSymbol(symbol);
9224
+ } catch {
9225
+ return symbol;
9226
+ }
9227
+ return symbol;
9228
+ };
9229
+ const detectRedundantVariableAliases = (graph, context, referenceIndex) => {
9230
+ const findings = [];
9231
+ const candidates = collectVariableAliasCandidates(graph, buildSourceFileLookup(context.program));
9232
+ if (candidates.length === 0) return findings;
9233
+ const { checker } = context;
9234
+ for (const candidate of candidates) {
9235
+ if (isDeclarationExported(candidate.declaration)) continue;
9236
+ const aliasNameIdentifier = candidate.declaration.name;
9237
+ if (!typescript.default.isIdentifier(aliasNameIdentifier)) continue;
9238
+ if (!candidate.declaration.initializer || !typescript.default.isIdentifier(candidate.declaration.initializer)) continue;
9239
+ const rawAliasSymbol = checker.getSymbolAtLocation(aliasNameIdentifier);
9240
+ const rawSourceSymbol = checker.getSymbolAtLocation(candidate.declaration.initializer);
9241
+ if (!rawAliasSymbol || !rawSourceSymbol) continue;
9242
+ const aliasSymbol = resolveThroughAliasChain(rawAliasSymbol, checker);
9243
+ const sourceSymbol = resolveThroughAliasChain(rawSourceSymbol, checker);
9244
+ if (aliasSymbol === sourceSymbol) continue;
9245
+ const sourceMeaningfulRefs = referenceIndex.getReferences(sourceSymbol).filter(isMeaningfulReference);
9246
+ const aliasMeaningfulRefs = referenceIndex.getReferences(aliasSymbol).filter(isMeaningfulReference);
9247
+ if (sourceMeaningfulRefs.filter((site) => site.identifier !== candidate.declaration.initializer).length > 0) continue;
9248
+ if (aliasMeaningfulRefs.length === 0) continue;
9249
+ const { line: zeroIndexedLine, character: zeroIndexedColumn } = candidate.sourceFile.getLineAndCharacterOfPosition(candidate.declaration.getStart(candidate.sourceFile));
9250
+ findings.push({
9251
+ path: candidate.modulePath,
9252
+ kind: "variable-alias",
9253
+ name: candidate.aliasName,
9254
+ aliasedFrom: candidate.aliasedFromName,
9255
+ line: zeroIndexedLine + 1,
9256
+ column: zeroIndexedColumn + 1,
9257
+ confidence: "high",
9258
+ reason: `\`const ${candidate.aliasName} = ${candidate.aliasedFromName}\` is the only consumer of \`${candidate.aliasedFromName}\` — rename or inline`
9259
+ });
9260
+ }
9261
+ return findings;
9262
+ };
9263
+
9264
+ //#endregion
9265
+ //#region src/semantic/redundant-reexports.ts
9266
+ const safeGetAliasedSymbol = (symbol, checker) => {
9267
+ try {
9268
+ return checker.getAliasedSymbol(symbol);
9269
+ } catch {
9270
+ return;
9271
+ }
9272
+ };
9273
+ const collectImportSpecifierRoundTrips = (graph, sourceFileLookup) => {
9274
+ const entries = [];
9275
+ for (const module of graph.modules) {
9276
+ if (!module.isReachable) continue;
9277
+ if (module.isDeclarationFile) continue;
9278
+ const sourceFile = sourceFileLookup.get(normalizeSourcePath(module.fileId.path));
9279
+ if (!sourceFile) continue;
9280
+ for (const statement of sourceFile.statements) {
9281
+ if (!typescript.default.isImportDeclaration(statement)) continue;
9282
+ const importClause = statement.importClause;
9283
+ if (!importClause?.namedBindings) continue;
9284
+ if (!typescript.default.isNamedImports(importClause.namedBindings)) continue;
9285
+ for (const importSpecifier of importClause.namedBindings.elements) {
9286
+ if (!importSpecifier.propertyName) continue;
9287
+ const importedName = importSpecifier.propertyName.text;
9288
+ const localName = importSpecifier.name.text;
9289
+ if (importedName === localName) continue;
9290
+ entries.push({
9291
+ modulePath: module.fileId.path,
9292
+ sourceFile,
9293
+ importSpecifier,
9294
+ importedName,
9295
+ localName
9296
+ });
6214
9297
  }
6215
- if (allCycles.length >= 200) break;
6216
9298
  }
6217
9299
  }
6218
- allCycles.sort((cycleA, cycleB) => {
6219
- const lengthDiff = cycleA.length - cycleB.length;
6220
- if (lengthDiff !== 0) return lengthDiff;
6221
- return graph.modules[cycleA[0]].fileId.path.localeCompare(graph.modules[cycleB[0]].fileId.path);
9300
+ return entries;
9301
+ };
9302
+ const detectRoundTripAliases = (graph, context) => {
9303
+ const findings = [];
9304
+ const importEntries = collectImportSpecifierRoundTrips(graph, buildSourceFileLookup(context.program));
9305
+ if (importEntries.length === 0) return findings;
9306
+ const { checker } = context;
9307
+ for (const entry of importEntries) {
9308
+ const localBindingSymbol = checker.getSymbolAtLocation(entry.importSpecifier.name);
9309
+ if (!localBindingSymbol) continue;
9310
+ if (!(localBindingSymbol.flags & typescript.default.SymbolFlags.Alias)) continue;
9311
+ const resolvedTargetSymbol = safeGetAliasedSymbol(localBindingSymbol, checker);
9312
+ if (!resolvedTargetSymbol) continue;
9313
+ const originalDeclarationName = resolvedTargetSymbol.name;
9314
+ if (!originalDeclarationName) continue;
9315
+ if (originalDeclarationName !== entry.localName) continue;
9316
+ if (originalDeclarationName === entry.importedName) continue;
9317
+ const { line: zeroIndexedLine, character: zeroIndexedColumn } = entry.sourceFile.getLineAndCharacterOfPosition(entry.importSpecifier.getStart(entry.sourceFile));
9318
+ findings.push({
9319
+ path: entry.modulePath,
9320
+ kind: "roundtrip-alias",
9321
+ name: entry.localName,
9322
+ aliasedFrom: entry.importedName,
9323
+ line: zeroIndexedLine + 1,
9324
+ column: zeroIndexedColumn + 1,
9325
+ confidence: "high",
9326
+ reason: `\`import { ${entry.importedName} as ${entry.localName} }\` renames back to the original declaration name — the upstream rename can be removed`
9327
+ });
9328
+ }
9329
+ return findings;
9330
+ };
9331
+
9332
+ //#endregion
9333
+ //#region src/semantic/index.ts
9334
+ const createDisabledSemanticResult = () => ({
9335
+ unusedTypes: [],
9336
+ unusedEnumMembers: [],
9337
+ unusedClassMembers: [],
9338
+ misclassifiedDependencies: [],
9339
+ redundantAliases: [],
9340
+ errors: [],
9341
+ contextStatus: "disabled"
9342
+ });
9343
+ const runSemanticAnalysis = (graph, config) => {
9344
+ const semanticConfig = config.semantic;
9345
+ if (!semanticConfig?.enabled) return createDisabledSemanticResult();
9346
+ const errors = [];
9347
+ const safeDetector = (detectorName, detector, fallback) => runSafeDetector({
9348
+ detectorName,
9349
+ detector,
9350
+ fallback,
9351
+ errorSink: errors,
9352
+ module: "semantic",
9353
+ contextDescription: "during semantic analysis"
6222
9354
  });
6223
- return allCycles.map((cycle) => ({ files: cycle.map((nodeIndex) => graph.modules[nodeIndex].fileId.path) }));
9355
+ const misclassifiedDependencies = semanticConfig.reportMisclassifiedDependencies ? safeDetector("detectMisclassifiedDependencies", () => detectMisclassifiedDependencies(graph, config), []) : [];
9356
+ if (!(semanticConfig.reportUnusedTypes || semanticConfig.reportUnusedEnumMembers || semanticConfig.reportUnusedClassMembers || semanticConfig.reportRedundantVariableAliases || semanticConfig.reportRoundTripAliases)) return {
9357
+ unusedTypes: [],
9358
+ unusedEnumMembers: [],
9359
+ unusedClassMembers: [],
9360
+ misclassifiedDependencies,
9361
+ redundantAliases: [],
9362
+ errors,
9363
+ contextStatus: "no-context-required"
9364
+ };
9365
+ let contextResult;
9366
+ try {
9367
+ contextResult = createSemanticContext(config.rootDir, config.tsConfigPath);
9368
+ } catch (contextError) {
9369
+ return {
9370
+ unusedTypes: [],
9371
+ unusedEnumMembers: [],
9372
+ unusedClassMembers: [],
9373
+ misclassifiedDependencies,
9374
+ redundantAliases: [],
9375
+ errors: [...errors, new TypeScriptError({
9376
+ code: "ts-not-loadable",
9377
+ message: "createSemanticContext threw before returning a result",
9378
+ detail: describeUnknownError(contextError)
9379
+ })],
9380
+ contextStatus: "typescript-load-failed"
9381
+ };
9382
+ }
9383
+ if (!contextResult.ok) return {
9384
+ unusedTypes: [],
9385
+ unusedEnumMembers: [],
9386
+ unusedClassMembers: [],
9387
+ misclassifiedDependencies,
9388
+ redundantAliases: [],
9389
+ errors: [...errors, contextResult.failure.error],
9390
+ contextStatus: contextResult.failure.reason,
9391
+ contextMessage: contextResult.failure.message
9392
+ };
9393
+ const { context } = contextResult;
9394
+ let referenceIndex;
9395
+ const getReferenceIndex = () => {
9396
+ if (!referenceIndex) referenceIndex = buildReferenceIndex(context.program, context.checker);
9397
+ return referenceIndex;
9398
+ };
9399
+ const unusedTypes = semanticConfig.reportUnusedTypes ? safeDetector("detectUnusedTypes", () => detectUnusedTypes(graph, config, context, getReferenceIndex()), []) : [];
9400
+ const unusedEnumMembers = semanticConfig.reportUnusedEnumMembers ? safeDetector("detectUnusedEnumMembers", () => detectUnusedEnumMembers(graph, config, context, getReferenceIndex()), []) : [];
9401
+ const unusedClassMembers = semanticConfig.reportUnusedClassMembers ? safeDetector("detectUnusedClassMembers", () => detectUnusedClassMembers(graph, config, context, getReferenceIndex(), semanticConfig.decoratorAllowlist), []) : [];
9402
+ const variableAliases = semanticConfig.reportRedundantVariableAliases ? safeDetector("detectRedundantVariableAliases", () => detectRedundantVariableAliases(graph, context, getReferenceIndex()), []) : [];
9403
+ const roundTripAliases = semanticConfig.reportRoundTripAliases ? safeDetector("detectRoundTripAliases", () => detectRoundTripAliases(graph, context), []) : [];
9404
+ return {
9405
+ unusedTypes,
9406
+ unusedEnumMembers,
9407
+ unusedClassMembers,
9408
+ misclassifiedDependencies,
9409
+ redundantAliases: [...variableAliases, ...roundTripAliases],
9410
+ errors,
9411
+ contextStatus: "ready"
9412
+ };
6224
9413
  };
6225
9414
 
6226
9415
  //#endregion
6227
9416
  //#region src/report/generate.ts
9417
+ const safeReportDetector = (detectorName, detector, fallback, errorSink) => runSafeDetector({
9418
+ detectorName,
9419
+ detector,
9420
+ fallback,
9421
+ errorSink,
9422
+ module: "report",
9423
+ contextDescription: "while building findings"
9424
+ });
6228
9425
  const generateReport = (graph, config) => {
6229
9426
  const analysisStartTime = performance.now();
6230
- const unusedFiles = detectOrphanFiles(graph);
6231
- const unusedExports = detectDeadExports(graph, config);
6232
- const unusedDependencies = detectStalePackages(graph, config);
6233
- const circularDependencies = detectCycles(graph);
9427
+ const errorSink = [];
9428
+ for (const module of graph.modules) {
9429
+ for (const parseError of module.parseErrors) {
9430
+ if (errorSink.length >= 5e3) break;
9431
+ errorSink.push(parseError);
9432
+ }
9433
+ if (errorSink.length >= 5e3) break;
9434
+ }
9435
+ const unusedFiles = safeReportDetector("detectOrphanFiles", () => detectOrphanFiles(graph), [], errorSink);
9436
+ const unusedExports = safeReportDetector("detectDeadExports", () => detectDeadExports(graph, config), [], errorSink);
9437
+ const unusedDependencies = safeReportDetector("detectStalePackages", () => detectStalePackages(graph, config), [], errorSink);
9438
+ const circularDependencies = safeReportDetector("detectCycles", () => detectCycles(graph), [], errorSink);
9439
+ const syntacticRedundantAliases = config.reportRedundancy ? [...safeReportDetector("detectRedundantAliases", () => detectRedundantAliases(graph), [], errorSink), ...safeReportDetector("detectUselessAliasedReExports", () => detectUselessAliasedReExports(graph), [], errorSink)] : [];
9440
+ const duplicateExports = config.reportRedundancy ? safeReportDetector("detectDuplicateExports", () => detectDuplicateExports(graph), [], errorSink) : [];
9441
+ const duplicateImports = config.reportRedundancy ? safeReportDetector("detectDuplicateImports", () => detectDuplicateImports(graph), [], errorSink) : [];
9442
+ const redundantTypePatterns = config.reportRedundancy ? safeReportDetector("detectRedundantTypePatterns", () => detectRedundantTypePatterns(graph), [], errorSink) : [];
9443
+ const identityWrappers = config.reportRedundancy ? safeReportDetector("detectIdentityWrappers", () => detectIdentityWrappers(graph), [], errorSink) : [];
9444
+ const duplicateTypeDefinitions = config.reportRedundancy ? safeReportDetector("detectDuplicateTypeDefinitions", () => detectDuplicateTypeDefinitions(graph), [], errorSink) : [];
9445
+ const duplicateInlineTypes = config.reportRedundancy ? safeReportDetector("detectDuplicateInlineTypes", () => detectDuplicateInlineTypes(graph), [], errorSink) : [];
9446
+ const simplifiableFunctions = config.reportRedundancy ? safeReportDetector("detectSimplifiableFunctions", () => detectSimplifiableFunctions(graph), [], errorSink) : [];
9447
+ const simplifiableExpressions = config.reportRedundancy ? safeReportDetector("detectSimplifiableExpressions", () => detectSimplifiableExpressions(graph), [], errorSink) : [];
9448
+ const duplicateConstants = config.reportRedundancy ? safeReportDetector("detectDuplicateConstants", () => detectDuplicateConstants(graph), [], errorSink) : [];
9449
+ let semanticResult;
9450
+ try {
9451
+ semanticResult = runSemanticAnalysis(graph, config);
9452
+ } catch (semanticError) {
9453
+ errorSink.push(new DetectorError({
9454
+ module: "semantic",
9455
+ message: "runSemanticAnalysis threw at the top level",
9456
+ detail: describeUnknownError(semanticError)
9457
+ }));
9458
+ semanticResult = {
9459
+ unusedTypes: [],
9460
+ unusedEnumMembers: [],
9461
+ unusedClassMembers: [],
9462
+ misclassifiedDependencies: [],
9463
+ redundantAliases: [],
9464
+ errors: [],
9465
+ contextStatus: "typescript-load-failed"
9466
+ };
9467
+ }
9468
+ for (const semanticError of semanticResult.errors) {
9469
+ if (errorSink.length >= 5e3) break;
9470
+ errorSink.push(semanticError);
9471
+ }
9472
+ const redundantAliases = config.reportRedundancy ? [...syntacticRedundantAliases, ...semanticResult.redundantAliases] : [];
6234
9473
  const totalExports = graph.modules.reduce((exportCount, module) => exportCount + module.exports.filter((exportInfo) => !(exportInfo.name === "*" && exportInfo.isNamespaceReExport)).length, 0);
6235
9474
  return {
6236
9475
  unusedFiles,
6237
9476
  unusedExports,
6238
9477
  unusedDependencies,
6239
9478
  circularDependencies,
9479
+ unusedTypes: semanticResult.unusedTypes,
9480
+ misclassifiedDependencies: semanticResult.misclassifiedDependencies,
9481
+ unusedEnumMembers: semanticResult.unusedEnumMembers,
9482
+ unusedClassMembers: semanticResult.unusedClassMembers,
9483
+ redundantAliases,
9484
+ duplicateExports,
9485
+ duplicateImports,
9486
+ redundantTypePatterns,
9487
+ identityWrappers,
9488
+ duplicateTypeDefinitions,
9489
+ duplicateInlineTypes,
9490
+ simplifiableFunctions,
9491
+ simplifiableExpressions,
9492
+ duplicateConstants,
9493
+ analysisErrors: errorSink,
6240
9494
  totalFiles: graph.modules.length,
6241
9495
  totalExports,
6242
9496
  analysisTimeMs: performance.now() - analysisStartTime
@@ -6247,6 +9501,39 @@ const generateReport = (graph, config) => {
6247
9501
  //#region src/index.ts
6248
9502
  const STYLE_EXTENSIONS = [".css", ".scss"];
6249
9503
  const REACT_NATIVE_ENABLERS = ["react-native", "expo"];
9504
+ const basenameFromPath = (filePath) => {
9505
+ const lastSlashIndex = filePath.lastIndexOf("/");
9506
+ return lastSlashIndex === -1 ? filePath : filePath.slice(lastSlashIndex + 1);
9507
+ };
9508
+ /**
9509
+ * Dynamic registry pattern: many codebases use a central "schema/registry"
9510
+ * module that lists tool/command/page filenames as string literals, then a
9511
+ * runner spawns them via `path.resolve(dir, file)` or `import()`. Static
9512
+ * analysis can't follow the indirection, so those targets get falsely
9513
+ * flagged as unused.
9514
+ *
9515
+ * Heuristic: if a parsed string literal exactly matches the basename of
9516
+ * exactly one file in the project, treat that file as an entry point.
9517
+ * Uniqueness guards against false-positives from common names like
9518
+ * `index.ts` matching dozens of unrelated files.
9519
+ */
9520
+ const markFilenameRegistryEntries = (moduleGraph) => {
9521
+ const basenameToModuleIndex = /* @__PURE__ */ new Map();
9522
+ for (const module of moduleGraph.modules) {
9523
+ const basename = basenameFromPath(module.fileId.path);
9524
+ const existing = basenameToModuleIndex.get(basename);
9525
+ if (existing === void 0) basenameToModuleIndex.set(basename, module.fileId.index);
9526
+ else if (existing !== "ambiguous") basenameToModuleIndex.set(basename, "ambiguous");
9527
+ }
9528
+ for (const module of moduleGraph.modules) for (const referencedFilename of module.referencedFilenames) {
9529
+ const targetIndex = basenameToModuleIndex.get(referencedFilename);
9530
+ if (typeof targetIndex !== "number") continue;
9531
+ const targetModule = moduleGraph.modules[targetIndex];
9532
+ if (!targetModule || targetModule.isEntryPoint) continue;
9533
+ if (targetModule.fileId.index === module.fileId.index) continue;
9534
+ targetModule.isEntryPoint = true;
9535
+ }
9536
+ };
6250
9537
  const detectReactNative = (rootDir, workspacePackages) => {
6251
9538
  const directoriesToCheck = [rootDir, ...workspacePackages.map((workspacePackage) => workspacePackage.directory)];
6252
9539
  for (const directory of directoriesToCheck) {
@@ -6267,32 +9554,144 @@ const detectReactNative = (rootDir, workspacePackages) => {
6267
9554
  }
6268
9555
  return false;
6269
9556
  };
9557
+ /**
9558
+ * Default flags below mark rules off-by-default. Rationale for each:
9559
+ *
9560
+ * - `reportUnusedClassMembers: false` — class-member dead-code detection
9561
+ * requires whole-program semantic analysis to be sound (subclass overrides,
9562
+ * structural typing, framework method-by-name invocation like `@HttpGet`).
9563
+ * When enabled on real React/Effect/NestJS codebases it produces a high
9564
+ * rate of stylistic-FP findings (lifecycle methods, framework hooks). Off
9565
+ * by default until the heuristics are tightened. Opt in via
9566
+ * `semantic.reportUnusedClassMembers = true` when you accept the noise.
9567
+ *
9568
+ * - `reportTypes: false` — type-only exports are over-represented in
9569
+ * barrel re-exports (the canonical `export type * from "./types"` pattern)
9570
+ * and are rarely actionable signal. Off by default; opt in when auditing
9571
+ * a type-heavy package.
9572
+ *
9573
+ * - `includeEntryExports: false` — exports from entry-point files are
9574
+ * "API surface" and intentionally exported for external consumers; flagging
9575
+ * them as "unused" is noise within a single repo scan. Opt in when auditing
9576
+ * a package boundary (e.g. before deleting public APIs).
9577
+ *
9578
+ * - `reportRedundancy: true` — on because redundancy findings are mostly
9579
+ * high-signal and the detectors carry their own confidence tiers.
9580
+ */
9581
+ const fillSemanticConfig = (semanticOverrides) => {
9582
+ if (semanticOverrides === void 0) return void 0;
9583
+ 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
9592
+ };
9593
+ };
6270
9594
  const defineConfig = (options) => ({
6271
9595
  rootDir: (0, node_path.resolve)(options.rootDir),
6272
9596
  entryPatterns: options.entryPatterns ?? DEFAULT_ENTRY_GLOBS,
6273
9597
  ignorePatterns: options.ignorePatterns ?? [],
6274
9598
  includeExtensions: options.includeExtensions ?? DEFAULT_EXTENSIONS,
6275
- tsConfigPath: options.tsConfigPath ?? void 0,
9599
+ tsConfigPath: options.tsConfigPath,
6276
9600
  reportTypes: options.reportTypes ?? false,
6277
- includeEntryExports: options.includeEntryExports ?? false
9601
+ includeEntryExports: options.includeEntryExports ?? false,
9602
+ reportRedundancy: options.reportRedundancy ?? true,
9603
+ semantic: fillSemanticConfig(options.semantic)
9604
+ });
9605
+ const buildEmptyScanResult = (errors, elapsedMs) => ({
9606
+ unusedFiles: [],
9607
+ unusedExports: [],
9608
+ unusedDependencies: [],
9609
+ circularDependencies: [],
9610
+ unusedTypes: [],
9611
+ misclassifiedDependencies: [],
9612
+ unusedEnumMembers: [],
9613
+ unusedClassMembers: [],
9614
+ redundantAliases: [],
9615
+ duplicateExports: [],
9616
+ duplicateImports: [],
9617
+ redundantTypePatterns: [],
9618
+ identityWrappers: [],
9619
+ duplicateTypeDefinitions: [],
9620
+ duplicateInlineTypes: [],
9621
+ simplifiableFunctions: [],
9622
+ simplifiableExpressions: [],
9623
+ duplicateConstants: [],
9624
+ analysisErrors: errors,
9625
+ totalFiles: 0,
9626
+ totalExports: 0,
9627
+ analysisTimeMs: elapsedMs
6278
9628
  });
9629
+ const validateConfig = (config) => {
9630
+ if (!config.rootDir || typeof config.rootDir !== "string") return new ConfigError({ message: "config.rootDir must be a non-empty string" });
9631
+ if (!(0, node_fs.existsSync)(config.rootDir)) return new ConfigError({
9632
+ message: `config.rootDir does not exist: ${config.rootDir}`,
9633
+ path: config.rootDir
9634
+ });
9635
+ };
6279
9636
  const analyze = async (config) => {
6280
9637
  const pipelineStartTime = performance.now();
6281
- const workspaceDiscovery = resolveWorkspaces((0, node_path.resolve)(config.rootDir));
9638
+ const setupErrors = [];
9639
+ const configValidationError = validateConfig(config);
9640
+ if (configValidationError) return buildEmptyScanResult([configValidationError], performance.now() - pipelineStartTime);
9641
+ let workspaceDiscovery;
9642
+ try {
9643
+ workspaceDiscovery = resolveWorkspaces((0, node_path.resolve)(config.rootDir));
9644
+ } catch (workspaceError) {
9645
+ setupErrors.push(new WorkspaceError({
9646
+ code: "workspace-discovery-failed",
9647
+ message: "resolveWorkspaces threw — falling back to single-package mode",
9648
+ path: config.rootDir,
9649
+ detail: describeUnknownError(workspaceError)
9650
+ }));
9651
+ workspaceDiscovery = {
9652
+ packages: [],
9653
+ excludedDirectories: [],
9654
+ hasRootLevelWorkspacePatterns: false
9655
+ };
9656
+ }
6282
9657
  const workspacePackages = [...workspaceDiscovery.packages];
6283
- const monorepoRoot = findMonorepoRoot(config.rootDir);
6284
- if (monorepoRoot) {
9658
+ let monorepoRoot;
9659
+ try {
9660
+ monorepoRoot = findMonorepoRoot(config.rootDir);
9661
+ } catch (monorepoError) {
9662
+ setupErrors.push(new WorkspaceError({
9663
+ code: "monorepo-discovery-failed",
9664
+ message: "findMonorepoRoot threw",
9665
+ path: config.rootDir,
9666
+ detail: describeUnknownError(monorepoError)
9667
+ }));
9668
+ monorepoRoot = void 0;
9669
+ }
9670
+ if (monorepoRoot) try {
6285
9671
  const monorepoWorkspaces = resolveWorkspaces(monorepoRoot);
6286
9672
  const existingDirectories = new Set(workspacePackages.map((workspacePackage) => workspacePackage.directory));
6287
9673
  for (const monorepoPackage of monorepoWorkspaces.packages) if (!existingDirectories.has(monorepoPackage.directory)) workspacePackages.push(monorepoPackage);
9674
+ } catch (monorepoWorkspaceError) {
9675
+ setupErrors.push(new WorkspaceError({
9676
+ code: "workspace-discovery-failed",
9677
+ message: "resolveWorkspaces threw on monorepo root",
9678
+ path: monorepoRoot,
9679
+ detail: describeUnknownError(monorepoWorkspaceError)
9680
+ }));
9681
+ }
9682
+ let frameworkIgnorePatterns = [];
9683
+ try {
9684
+ frameworkIgnorePatterns = getFrameworkExclusions(config.rootDir);
9685
+ } catch (frameworkError) {
9686
+ setupErrors.push(new WorkspaceError({
9687
+ code: "workspace-discovery-failed",
9688
+ message: "getFrameworkExclusions failed — proceeding without framework exclusion patterns",
9689
+ path: config.rootDir,
9690
+ detail: describeUnknownError(frameworkError)
9691
+ }));
6288
9692
  }
6289
- const frameworkIgnorePatterns = getFrameworkExclusions(config.rootDir);
6290
9693
  const absoluteRoot = (0, node_path.resolve)(config.rootDir);
6291
- const outputDirectoryExclusions = OUTPUT_DIRECTORIES.flatMap((outputDirectory) => {
6292
- const exclusions = [`${absoluteRoot}/${outputDirectory}/**`];
6293
- for (const workspacePackage of workspacePackages) exclusions.push(`${workspacePackage.directory}/${outputDirectory}/**`);
6294
- return exclusions;
6295
- });
9694
+ const outputDirectoryExclusions = OUTPUT_DIRECTORIES.flatMap((outputDirectory) => [`${absoluteRoot}/${outputDirectory}/**`, `${absoluteRoot}/**/${outputDirectory}/**`]);
6296
9695
  const allExclusionPatterns = [
6297
9696
  ...workspaceDiscovery.excludedDirectories.map((directory) => `${directory}/**`),
6298
9697
  ...frameworkIgnorePatterns,
@@ -6302,32 +9701,101 @@ const analyze = async (config) => {
6302
9701
  ...config,
6303
9702
  ignorePatterns: [...config.ignorePatterns, ...allExclusionPatterns]
6304
9703
  } : config;
6305
- const files = await collectSourceFiles(configWithExclusions);
6306
- const discoveredEntries = await resolveEntries(configWithExclusions);
9704
+ let files;
9705
+ try {
9706
+ files = await collectSourceFiles(configWithExclusions);
9707
+ } catch (collectError) {
9708
+ setupErrors.push(new WorkspaceError({
9709
+ code: "workspace-discovery-failed",
9710
+ severity: "fatal",
9711
+ message: "collectSourceFiles failed",
9712
+ path: config.rootDir,
9713
+ detail: describeUnknownError(collectError)
9714
+ }));
9715
+ return buildEmptyScanResult(setupErrors, performance.now() - pipelineStartTime);
9716
+ }
9717
+ let discoveredEntries;
9718
+ try {
9719
+ discoveredEntries = await resolveEntries(configWithExclusions);
9720
+ } catch (entriesError) {
9721
+ setupErrors.push(new WorkspaceError({
9722
+ code: "workspace-discovery-failed",
9723
+ message: "resolveEntries failed — defaulting to empty entry set",
9724
+ path: config.rootDir,
9725
+ detail: describeUnknownError(entriesError)
9726
+ }));
9727
+ discoveredEntries = {
9728
+ productionEntries: [],
9729
+ testEntries: [],
9730
+ alwaysUsedFiles: []
9731
+ };
9732
+ }
6307
9733
  const productionEntrySet = new Set(discoveredEntries.productionEntries);
6308
9734
  const testEntrySet = new Set(discoveredEntries.testEntries);
6309
9735
  const alwaysUsedFileSet = new Set(discoveredEntries.alwaysUsedFiles);
6310
- const hasReactNative = detectReactNative(config.rootDir, workspacePackages);
6311
- const moduleResolver = createResolver(config, workspacePackages.map((workspacePackage) => ({
6312
- name: workspacePackage.name,
6313
- directory: workspacePackage.directory
6314
- })), {
6315
- hasReactNative,
6316
- monorepoRoot
6317
- });
9736
+ let hasReactNative = false;
9737
+ try {
9738
+ hasReactNative = detectReactNative(config.rootDir, workspacePackages);
9739
+ } catch {
9740
+ hasReactNative = false;
9741
+ }
9742
+ let moduleResolver;
9743
+ try {
9744
+ moduleResolver = createResolver(config, workspacePackages.map((workspacePackage) => ({
9745
+ name: workspacePackage.name,
9746
+ directory: workspacePackage.directory
9747
+ })), {
9748
+ hasReactNative,
9749
+ monorepoRoot
9750
+ });
9751
+ } catch (resolverError) {
9752
+ setupErrors.push(new ResolverError({
9753
+ message: "createResolver failed",
9754
+ path: config.rootDir,
9755
+ detail: describeUnknownError(resolverError)
9756
+ }));
9757
+ return buildEmptyScanResult(setupErrors, performance.now() - pipelineStartTime);
9758
+ }
6318
9759
  const graphInputs = [];
6319
9760
  for (const file of files) {
6320
9761
  const parsedModule = parseSourceFile(file.path);
6321
9762
  const resolvedImportMap = /* @__PURE__ */ new Map();
9763
+ const safeResolveImport = (specifier) => {
9764
+ try {
9765
+ return moduleResolver.resolveModule(specifier, file.path);
9766
+ } catch (resolveError) {
9767
+ setupErrors.push(new ResolverError({
9768
+ severity: "warning",
9769
+ message: `moduleResolver.resolveModule threw on specifier "${specifier}"`,
9770
+ path: file.path,
9771
+ detail: describeUnknownError(resolveError)
9772
+ }));
9773
+ return {
9774
+ resolvedPath: void 0,
9775
+ isExternal: false,
9776
+ packageName: void 0
9777
+ };
9778
+ }
9779
+ };
6322
9780
  for (const importInfo of parsedModule.imports) {
6323
9781
  if (importInfo.isGlob) {
6324
9782
  const fileDir = (0, node_path.dirname)(file.path);
6325
- const expandedFiles = fast_glob.default.sync(importInfo.specifier, {
6326
- cwd: fileDir,
6327
- absolute: true,
6328
- onlyFiles: true,
6329
- ignore: ["**/node_modules/**"]
6330
- });
9783
+ let expandedFiles = [];
9784
+ try {
9785
+ expandedFiles = fast_glob.default.sync(importInfo.specifier, {
9786
+ cwd: fileDir,
9787
+ absolute: true,
9788
+ onlyFiles: true,
9789
+ ignore: ["**/node_modules/**"]
9790
+ });
9791
+ } catch (globError) {
9792
+ setupErrors.push(new WorkspaceError({
9793
+ code: "workspace-discovery-failed",
9794
+ message: `fast-glob threw on import glob "${importInfo.specifier}"`,
9795
+ path: file.path,
9796
+ detail: describeUnknownError(globError)
9797
+ }));
9798
+ }
6331
9799
  for (const expandedFile of expandedFiles) resolvedImportMap.set(expandedFile, {
6332
9800
  resolvedPath: expandedFile,
6333
9801
  isExternal: false,
@@ -6340,14 +9808,10 @@ const analyze = async (config) => {
6340
9808
  });
6341
9809
  continue;
6342
9810
  }
6343
- const resolvedImport = moduleResolver.resolveModule(importInfo.specifier, file.path);
6344
- resolvedImportMap.set(importInfo.specifier, resolvedImport);
9811
+ resolvedImportMap.set(importInfo.specifier, safeResolveImport(importInfo.specifier));
6345
9812
  }
6346
9813
  for (const exportInfo of parsedModule.exports) if (exportInfo.isReExport && exportInfo.reExportSource) {
6347
- if (!resolvedImportMap.has(exportInfo.reExportSource)) {
6348
- const resolvedImport = moduleResolver.resolveModule(exportInfo.reExportSource, file.path);
6349
- resolvedImportMap.set(exportInfo.reExportSource, resolvedImport);
6350
- }
9814
+ if (!resolvedImportMap.has(exportInfo.reExportSource)) resolvedImportMap.set(exportInfo.reExportSource, safeResolveImport(exportInfo.reExportSource));
6351
9815
  }
6352
9816
  const isAlwaysUsed = alwaysUsedFileSet.has(file.path);
6353
9817
  graphInputs.push({
@@ -6375,7 +9839,22 @@ const analyze = async (config) => {
6375
9839
  const parsedStyleModule = parseSourceFile(styleFilePath);
6376
9840
  const resolvedStyleImportMap = /* @__PURE__ */ new Map();
6377
9841
  for (const importInfo of parsedStyleModule.imports) {
6378
- const resolvedImport = moduleResolver.resolveModule(importInfo.specifier, styleFilePath);
9842
+ let resolvedImport;
9843
+ try {
9844
+ resolvedImport = moduleResolver.resolveModule(importInfo.specifier, styleFilePath);
9845
+ } catch (styleResolveError) {
9846
+ setupErrors.push(new ResolverError({
9847
+ severity: "warning",
9848
+ message: `moduleResolver.resolveModule threw on style import "${importInfo.specifier}"`,
9849
+ path: styleFilePath,
9850
+ detail: describeUnknownError(styleResolveError)
9851
+ }));
9852
+ resolvedImport = {
9853
+ resolvedPath: void 0,
9854
+ isExternal: false,
9855
+ packageName: void 0
9856
+ };
9857
+ }
6379
9858
  resolvedStyleImportMap.set(importInfo.specifier, resolvedImport);
6380
9859
  if (resolvedImport.resolvedPath && !discoveredFilePaths.has(resolvedImport.resolvedPath)) {
6381
9860
  if (STYLE_EXTENSIONS.some((ext) => resolvedImport.resolvedPath.endsWith(ext)) && (0, node_fs.existsSync)(resolvedImport.resolvedPath)) styleFilesToAdd.add(resolvedImport.resolvedPath);
@@ -6391,10 +9870,51 @@ const analyze = async (config) => {
6391
9870
  discoveredFilePaths.add(styleFilePath);
6392
9871
  nextFileIndex++;
6393
9872
  }
6394
- const moduleGraph = buildDependencyGraph(graphInputs);
6395
- resolveReExportChains(moduleGraph);
6396
- traceReachability(moduleGraph);
6397
- const analysisResult = generateReport(moduleGraph, config);
9873
+ let moduleGraph;
9874
+ try {
9875
+ moduleGraph = buildDependencyGraph(graphInputs);
9876
+ } catch (graphError) {
9877
+ setupErrors.push(new DetectorError({
9878
+ module: "linker",
9879
+ severity: "fatal",
9880
+ message: "buildDependencyGraph threw",
9881
+ detail: describeUnknownError(graphError)
9882
+ }));
9883
+ return buildEmptyScanResult(setupErrors, performance.now() - pipelineStartTime);
9884
+ }
9885
+ try {
9886
+ resolveReExportChains(moduleGraph);
9887
+ } catch (reExportError) {
9888
+ setupErrors.push(new DetectorError({
9889
+ module: "linker",
9890
+ message: "resolveReExportChains threw — re-export propagation skipped",
9891
+ detail: describeUnknownError(reExportError)
9892
+ }));
9893
+ }
9894
+ markFilenameRegistryEntries(moduleGraph);
9895
+ try {
9896
+ traceReachability(moduleGraph);
9897
+ } catch (reachabilityError) {
9898
+ setupErrors.push(new DetectorError({
9899
+ module: "linker",
9900
+ message: "traceReachability threw — every module marked reachable to avoid over-reporting",
9901
+ detail: describeUnknownError(reachabilityError)
9902
+ }));
9903
+ for (const module of moduleGraph.modules) module.isReachable = true;
9904
+ }
9905
+ let analysisResult;
9906
+ try {
9907
+ analysisResult = generateReport(moduleGraph, config);
9908
+ } catch (reportError) {
9909
+ setupErrors.push(new DetectorError({
9910
+ module: "report",
9911
+ severity: "fatal",
9912
+ message: "generateReport threw at the top level",
9913
+ detail: describeUnknownError(reportError)
9914
+ }));
9915
+ return buildEmptyScanResult(setupErrors, performance.now() - pipelineStartTime);
9916
+ }
9917
+ if (setupErrors.length > 0) analysisResult.analysisErrors = [...setupErrors, ...analysisResult.analysisErrors];
6398
9918
  analysisResult.analysisTimeMs = performance.now() - pipelineStartTime;
6399
9919
  return analysisResult;
6400
9920
  };