deslop-js 0.0.9 → 0.0.11

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,998 @@ 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 isSimpleReturnArgument = (argumentNode) => {
1082
+ if (!isOxcAstNode(argumentNode)) return false;
1083
+ if (argumentNode.type === "BlockStatement") return false;
1084
+ if (argumentNode.type === "ObjectExpression") return false;
1085
+ return true;
1086
+ };
1087
+ const detectBlockArrowSingleReturn = (functionNode) => {
1088
+ if (functionNode.type !== "ArrowFunctionExpression") return void 0;
1089
+ if (functionNode.async) return void 0;
1090
+ const bodyNode = functionNode.body;
1091
+ if (!bodyNode || bodyNode.type !== "BlockStatement") return void 0;
1092
+ const statements = bodyNode.body ?? [];
1093
+ if (statements.length !== 1) return void 0;
1094
+ const onlyStatement = statements[0];
1095
+ if (!isOxcAstNode(onlyStatement)) return void 0;
1096
+ if (onlyStatement.type !== "ReturnStatement") return void 0;
1097
+ const returnArgument = onlyStatement.argument;
1098
+ if (!returnArgument) return void 0;
1099
+ if (!isSimpleReturnArgument(returnArgument)) return void 0;
1100
+ return {
1101
+ kind: "block-arrow-single-return",
1102
+ startOffset: functionNode.start ?? 0,
1103
+ reason: "arrow body is a single `return` statement; the block can be replaced by the expression directly",
1104
+ suggestion: "rewrite as `() => expression` without `{}`"
1105
+ };
1106
+ };
1107
+ const detectRedundantAwaitReturn = (functionNode) => {
1108
+ const bodyNode = functionNode.body;
1109
+ if (!bodyNode || bodyNode.type !== "BlockStatement") return void 0;
1110
+ const statements = bodyNode.body ?? [];
1111
+ if (statements.length < 2) return void 0;
1112
+ const penultimate = statements[statements.length - 2];
1113
+ const last = statements[statements.length - 1];
1114
+ if (!isOxcAstNode(penultimate) || !isOxcAstNode(last)) return void 0;
1115
+ if (penultimate.type !== "VariableDeclaration") return void 0;
1116
+ if (last.type !== "ReturnStatement") return void 0;
1117
+ const declarators = penultimate.declarations ?? [];
1118
+ if (declarators.length !== 1) return void 0;
1119
+ const declarator = declarators[0];
1120
+ if (!isOxcAstNode(declarator)) return void 0;
1121
+ const declaredIdentifier = declarator.id;
1122
+ const initializer = declarator.init;
1123
+ if (!declaredIdentifier?.name) return void 0;
1124
+ if (!isOxcAstNode(initializer)) return void 0;
1125
+ if (initializer.type !== "AwaitExpression") return void 0;
1126
+ const returnedArgument = last.argument;
1127
+ if (!isOxcAstNode(returnedArgument)) return void 0;
1128
+ if (returnedArgument.type !== "Identifier") return void 0;
1129
+ if (returnedArgument.name !== declaredIdentifier.name) return void 0;
1130
+ return {
1131
+ kind: "redundant-await-return",
1132
+ startOffset: penultimate.start ?? 0,
1133
+ reason: `\`const ${declaredIdentifier.name} = await …; return ${declaredIdentifier.name};\` can be \`return …;\` (the await is preserved by the implicit promise chain)`,
1134
+ suggestion: `replace the await/assign/return sequence with a single \`return await …\` or \`return …\` if no try/catch wraps it`
1135
+ };
1136
+ };
1137
+ const isAsyncFunction = (functionNode) => Boolean(functionNode.async);
1138
+ const detectUselessAsync = (functionNode) => {
1139
+ if (!isAsyncFunction(functionNode)) return void 0;
1140
+ if (functionNode.type === "ClassDeclaration" || functionNode.type === "MethodDefinition") return;
1141
+ const bodyNode = functionNode.body;
1142
+ if (!isOxcAstNode(bodyNode)) return void 0;
1143
+ if (containsAwaitExpression(bodyNode)) return void 0;
1144
+ if (containsCallOrPromiseSurface(bodyNode)) return void 0;
1145
+ return {
1146
+ kind: "useless-async-no-await",
1147
+ startOffset: functionNode.start ?? 0,
1148
+ reason: "async function body contains no `await`, no function calls, and no Promise surface — the implicit Promise wrap is purely decorative",
1149
+ suggestion: "drop `async` (caller's existing `await` keeps the type identical) or add an explicit return type"
1150
+ };
1151
+ };
1152
+ const detectSimplifiableFunctionPatterns = (functionNode) => {
1153
+ if (!isOxcAstNode(functionNode)) return [];
1154
+ const findings = [];
1155
+ const blockArrow = detectBlockArrowSingleReturn(functionNode);
1156
+ if (blockArrow) findings.push(blockArrow);
1157
+ const awaitReturn = detectRedundantAwaitReturn(functionNode);
1158
+ if (awaitReturn) findings.push(awaitReturn);
1159
+ const uselessAsync = detectUselessAsync(functionNode);
1160
+ if (uselessAsync) findings.push(uselessAsync);
1161
+ return findings;
1162
+ };
1163
+
1164
+ //#endregion
1165
+ //#region src/utils/collect-simplifiable-functions.ts
1166
+ const looksLikeFunction = (node) => node.type === "FunctionDeclaration" || node.type === "FunctionExpression" || node.type === "ArrowFunctionExpression";
1167
+ const inferFunctionName = (functionNode, parentContext) => {
1168
+ const declaredId = functionNode.id;
1169
+ if (declaredId?.name) return declaredId.name;
1170
+ return parentContext;
1171
+ };
1172
+ const visitFunctionAndDescend = (functionNode, captures, contextName, recursionDepth) => {
1173
+ const functionName = inferFunctionName(functionNode, contextName);
1174
+ const detections = detectSimplifiableFunctionPatterns(functionNode);
1175
+ for (const detection of detections) captures.push({
1176
+ kind: detection.kind,
1177
+ functionName,
1178
+ startOffset: detection.startOffset,
1179
+ reason: detection.reason,
1180
+ suggestion: detection.suggestion
1181
+ });
1182
+ const bodyNode = functionNode.body;
1183
+ if (isOxcAstNode(bodyNode)) walkForFunctions(bodyNode, captures, functionName, recursionDepth + 1);
1184
+ const parameters = functionNode.params ?? [];
1185
+ for (const parameter of parameters) if (isOxcAstNode(parameter)) walkForFunctions(parameter, captures, functionName, recursionDepth + 1);
1186
+ };
1187
+ const walkForFunctions = (node, captures, contextName, recursionDepth = 0) => {
1188
+ if (recursionDepth > 200) return;
1189
+ if (looksLikeFunction(node)) {
1190
+ visitFunctionAndDescend(node, captures, contextName, recursionDepth);
1191
+ return;
1192
+ }
1193
+ let nextContext = contextName;
1194
+ if (node.type === "VariableDeclarator") {
1195
+ const declaredName = getIdentifierName(node.id);
1196
+ if (declaredName) nextContext = declaredName;
1197
+ }
1198
+ if (node.type === "MethodDefinition" || node.type === "PropertyDefinition") {
1199
+ const propertyKeyName = getIdentifierName(node.key);
1200
+ if (propertyKeyName) nextContext = propertyKeyName;
1201
+ }
1202
+ if (node.type === "ClassDeclaration") {
1203
+ const className = getIdentifierName(node.id);
1204
+ if (className) nextContext = className;
1205
+ }
1206
+ for (const value of Object.values(node)) if (Array.isArray(value)) {
1207
+ for (const element of value) if (isOxcAstNode(element)) walkForFunctions(element, captures, nextContext, recursionDepth + 1);
1208
+ } else if (isOxcAstNode(value)) walkForFunctions(value, captures, nextContext, recursionDepth + 1);
1209
+ };
1210
+ const collectSimplifiableFunctions = (programBody) => {
1211
+ const captures = [];
1212
+ for (const statement of programBody) if (isOxcAstNode(statement)) walkForFunctions(statement, captures, void 0, 0);
1213
+ return captures;
1214
+ };
1215
+
1216
+ //#endregion
1217
+ //#region src/utils/collect-simplifiable-expressions.ts
1218
+ const memberAccessText = (node, depth = 0) => {
1219
+ if (depth > 6) return void 0;
1220
+ if (node.type === "Identifier") return node.name;
1221
+ if (node.type === "ThisExpression") return "this";
1222
+ if (node.type === "MemberExpression") {
1223
+ if (node.computed) return void 0;
1224
+ const objectNode = node.object;
1225
+ const propertyNode = node.property;
1226
+ if (!objectNode || !propertyNode) return void 0;
1227
+ const objectText = memberAccessText(objectNode, depth + 1);
1228
+ const propertyText = propertyNode.type === "Identifier" ? propertyNode.name : void 0;
1229
+ if (!objectText || !propertyText) return void 0;
1230
+ return `${objectText}.${propertyText}`;
1231
+ }
1232
+ };
1233
+ const isBooleanLiteral = (node, expected) => {
1234
+ if (node.type !== "Literal") return false;
1235
+ return node.value === expected;
1236
+ };
1237
+ const detectSelfFallbackTernary = (conditionalNode) => {
1238
+ if (conditionalNode.type !== "ConditionalExpression") return void 0;
1239
+ const testNode = conditionalNode.test;
1240
+ const consequentNode = conditionalNode.consequent;
1241
+ if (!testNode || !consequentNode) return void 0;
1242
+ const testText = memberAccessText(testNode);
1243
+ const consequentText = memberAccessText(consequentNode);
1244
+ if (!testText || !consequentText) return void 0;
1245
+ if (testText !== consequentText) return void 0;
1246
+ return {
1247
+ kind: "self-fallback-ternary",
1248
+ snippet: `${testText} ? ${consequentText} : ...`,
1249
+ startOffset: conditionalNode.start ?? 0,
1250
+ reason: `\`${testText} ? ${testText} : x\` is a self-fallback ternary`,
1251
+ suggestion: `use \`${testText} ?? x\` (nullish-only) or \`${testText} || x\` (falsy fallback) depending on intent`
1252
+ };
1253
+ };
1254
+ const detectTernaryReturnsBoolean = (conditionalNode) => {
1255
+ if (conditionalNode.type !== "ConditionalExpression") return void 0;
1256
+ const consequentNode = conditionalNode.consequent;
1257
+ const alternateNode = conditionalNode.alternate;
1258
+ if (!consequentNode || !alternateNode) return void 0;
1259
+ const isTrueFalse = isBooleanLiteral(consequentNode, true) && isBooleanLiteral(alternateNode, false);
1260
+ const isFalseTrue = isBooleanLiteral(consequentNode, false) && isBooleanLiteral(alternateNode, true);
1261
+ if (!isTrueFalse && !isFalseTrue) return void 0;
1262
+ return {
1263
+ kind: "ternary-returns-boolean",
1264
+ snippet: isTrueFalse ? "cond ? true : false" : "cond ? false : true",
1265
+ startOffset: conditionalNode.start ?? 0,
1266
+ reason: isTrueFalse ? "`cond ? true : false` collapses to `Boolean(cond)`" : "`cond ? false : true` collapses to `!cond`",
1267
+ suggestion: isTrueFalse ? "replace with `Boolean(cond)` or just `cond` when types match" : "replace with `!cond`"
1268
+ };
1269
+ };
1270
+ const isNullLiteral = (node) => node.type === "Literal" && node.value === null;
1271
+ const isUndefinedIdentifier = (node) => node.type === "Identifier" && node.name === "undefined";
1272
+ const detectNullishCoalescingWithNullish = (logicalNode) => {
1273
+ if (logicalNode.type !== "LogicalExpression") return void 0;
1274
+ if (logicalNode.operator !== "??") return void 0;
1275
+ const rightNode = logicalNode.right;
1276
+ if (!rightNode) return void 0;
1277
+ if (!(isNullLiteral(rightNode) || isUndefinedIdentifier(rightNode))) return void 0;
1278
+ const leftNode = logicalNode.left;
1279
+ const leftText = leftNode ? memberAccessText(leftNode) ?? "expr" : "expr";
1280
+ const rightLabel = isNullLiteral(rightNode) ? "null" : "undefined";
1281
+ return {
1282
+ kind: "nullish-coalescing-with-nullish",
1283
+ snippet: `${leftText} ?? ${rightLabel}`,
1284
+ startOffset: logicalNode.start ?? 0,
1285
+ reason: `\`x ?? ${rightLabel}\` looks like a no-op — but may be intentional when a caller's signature requires \`${rightLabel}\` (PropTypes, form-control onChange, etc.)`,
1286
+ suggestion: `if \`x\` is already \`T | ${rightLabel}\`, drop the \`?? ${rightLabel}\`; otherwise keep — the coercion changes the resolved type`
1287
+ };
1288
+ };
1289
+ const detectRedundantNullAndUndefinedCheck = (logicalNode) => {
1290
+ if (logicalNode.type !== "LogicalExpression") return void 0;
1291
+ if (logicalNode.operator !== "&&") return void 0;
1292
+ const leftNode = logicalNode.left;
1293
+ const rightNode = logicalNode.right;
1294
+ if (!leftNode || !rightNode) return void 0;
1295
+ if (leftNode.type !== "BinaryExpression" || rightNode.type !== "BinaryExpression") return void 0;
1296
+ const leftOp = leftNode.operator;
1297
+ const rightOp = rightNode.operator;
1298
+ if (leftOp !== "!==" || rightOp !== "!==") return void 0;
1299
+ const leftLeft = leftNode.left;
1300
+ const leftRight = leftNode.right;
1301
+ const rightLeft = rightNode.left;
1302
+ const rightRight = rightNode.right;
1303
+ if (!leftLeft || !leftRight || !rightLeft || !rightRight) return void 0;
1304
+ const leftLeftText = memberAccessText(leftLeft);
1305
+ const rightLeftText = memberAccessText(rightLeft);
1306
+ if (!leftLeftText || leftLeftText !== rightLeftText) return void 0;
1307
+ const leftRhsIsNull = isNullLiteral(leftRight);
1308
+ const leftRhsIsUndefined = isUndefinedIdentifier(leftRight);
1309
+ const rightRhsIsNull = isNullLiteral(rightRight);
1310
+ const rightRhsIsUndefined = isUndefinedIdentifier(rightRight);
1311
+ if (!(leftRhsIsNull && rightRhsIsUndefined || leftRhsIsUndefined && rightRhsIsNull)) return void 0;
1312
+ return {
1313
+ kind: "redundant-null-and-undefined-check",
1314
+ snippet: `${leftLeftText} !== null && ${leftLeftText} !== undefined`,
1315
+ startOffset: logicalNode.start ?? 0,
1316
+ reason: `\`x !== null && x !== undefined\` is equivalent to \`x != null\` (loose comparison checks both)`,
1317
+ suggestion: `replace with \`${leftLeftText} != null\``
1318
+ };
1319
+ };
1320
+ const detectDoubleBangBoolean = (unaryNode) => {
1321
+ if (unaryNode.type !== "UnaryExpression") return void 0;
1322
+ if (unaryNode.operator !== "!") return void 0;
1323
+ const inner = unaryNode.argument;
1324
+ if (!inner || inner.type !== "UnaryExpression") return void 0;
1325
+ if (inner.operator !== "!") return void 0;
1326
+ const coerced = inner.argument;
1327
+ if (!coerced) return void 0;
1328
+ const coercedText = memberAccessText(coerced) ?? "expr";
1329
+ return {
1330
+ kind: "double-bang-boolean",
1331
+ snippet: `!!${coercedText}`,
1332
+ startOffset: unaryNode.start ?? 0,
1333
+ reason: "`!!x` is a double-negation boolean coercion",
1334
+ suggestion: `replace with \`Boolean(${coercedText})\``
1335
+ };
1336
+ };
1337
+ const visit = (node, captures, depth) => {
1338
+ if (depth > 100) return;
1339
+ const conditionalCapture = detectSelfFallbackTernary(node) ?? detectTernaryReturnsBoolean(node);
1340
+ if (conditionalCapture) captures.push(conditionalCapture);
1341
+ const doubleBangCapture = detectDoubleBangBoolean(node);
1342
+ if (doubleBangCapture) captures.push(doubleBangCapture);
1343
+ const logicalCapture = detectNullishCoalescingWithNullish(node) ?? detectRedundantNullAndUndefinedCheck(node);
1344
+ if (logicalCapture) captures.push(logicalCapture);
1345
+ for (const value of Object.values(node)) if (Array.isArray(value)) {
1346
+ for (const element of value) if (isOxcAstNode(element)) visit(element, captures, depth + 1);
1347
+ } else if (isOxcAstNode(value)) visit(value, captures, depth + 1);
1348
+ };
1349
+ const collectSimplifiableExpressions = (programBody) => {
1350
+ const captures = [];
1351
+ for (const statement of programBody) if (isOxcAstNode(statement)) visit(statement, captures, 0);
1352
+ return captures;
1353
+ };
1354
+
1355
+ //#endregion
1356
+ //#region src/utils/collect-duplicate-constants.ts
1357
+ const FRAMEWORK_RESERVED_CONSTANT_NAMES = new Set([
1358
+ "dynamic",
1359
+ "dynamicParams",
1360
+ "revalidate",
1361
+ "runtime",
1362
+ "fetchCache",
1363
+ "preferredRegion",
1364
+ "maxDuration",
1365
+ "metadata",
1366
+ "viewport",
1367
+ "generateStaticParams",
1368
+ "generateMetadata",
1369
+ "config",
1370
+ "loader",
1371
+ "action",
1372
+ "links",
1373
+ "meta",
1374
+ "headers",
1375
+ "handle",
1376
+ "shouldRevalidate",
1377
+ "ErrorBoundary",
1378
+ "HydrateFallback",
1379
+ "Layout"
1380
+ ]);
1381
+ const isLiteralCandidate = (node) => {
1382
+ if (node.type === "Literal") {
1383
+ const value = node.value;
1384
+ if (typeof value === "string") {
1385
+ if (value.length < 8) return false;
1386
+ return true;
1387
+ }
1388
+ if (typeof value === "number") {
1389
+ if (!Number.isFinite(value)) return false;
1390
+ if (Math.abs(value) < 1e3) return false;
1391
+ return true;
1392
+ }
1393
+ return false;
1394
+ }
1395
+ if (node.type === "TemplateLiteral") {
1396
+ const expressions = node.expressions;
1397
+ if (Array.isArray(expressions) && expressions.length > 0) return false;
1398
+ const quasis = node.quasis;
1399
+ if (!Array.isArray(quasis) || quasis.length === 0) return false;
1400
+ return (quasis[0].value?.cooked ?? "").length >= 8;
1401
+ }
1402
+ if (node.type === "ArrayExpression") {
1403
+ const elements = node.elements ?? [];
1404
+ if (elements.length === 0) return false;
1405
+ for (const element of elements) {
1406
+ if (!isOxcAstNode(element)) return false;
1407
+ if (element.type !== "Literal") return false;
1408
+ }
1409
+ return true;
1410
+ }
1411
+ return false;
1412
+ };
1413
+ const hashLiteralNode = (node) => {
1414
+ if (node.type === "Literal") return `lit:${typeof node.value}:${JSON.stringify(node.value)}`;
1415
+ if (node.type === "TemplateLiteral") {
1416
+ const quasis = node.quasis ?? [];
1417
+ return `tpl:${JSON.stringify(quasis[0]?.value?.cooked ?? "")}`;
1418
+ }
1419
+ if (node.type === "ArrayExpression") return `arr:[${(node.elements ?? []).map((element) => {
1420
+ if (!isOxcAstNode(element)) return "?";
1421
+ if (element.type !== "Literal") return "?";
1422
+ return JSON.stringify(element.value);
1423
+ }).join(",")}]`;
1424
+ return "?";
1425
+ };
1426
+ const previewLiteralNode = (node) => {
1427
+ if (node.type === "Literal") {
1428
+ const value = node.value;
1429
+ if (typeof value === "string") return `"${value.length > 60 ? value.slice(0, 57) + "..." : value}"`;
1430
+ return String(value);
1431
+ }
1432
+ if (node.type === "TemplateLiteral") {
1433
+ const cooked = (node.quasis ?? [])[0]?.value?.cooked ?? "";
1434
+ return `\`${cooked.length > 60 ? cooked.slice(0, 57) + "..." : cooked}\``;
1435
+ }
1436
+ if (node.type === "ArrayExpression") {
1437
+ const elements = node.elements ?? [];
1438
+ return `[${elements.slice(0, 3).map((element) => isOxcAstNode(element) && element.type === "Literal" ? JSON.stringify(element.value) : "?").join(", ")}${elements.length > 3 ? `, +${elements.length - 3} more` : ""}]`;
1439
+ }
1440
+ return "<literal>";
1441
+ };
1442
+ const visitForConstants = (statementNode, candidates) => {
1443
+ if (!isOxcAstNode(statementNode)) return;
1444
+ const inner = (statementNode.type === "ExportNamedDeclaration" || statementNode.type === "ExportDefaultDeclaration") && statementNode.declaration ? statementNode.declaration : statementNode;
1445
+ if (!isOxcAstNode(inner)) return;
1446
+ if (inner.type !== "VariableDeclaration") return;
1447
+ if (inner.kind !== "const") return;
1448
+ const declarators = inner.declarations ?? [];
1449
+ for (const declarator of declarators) {
1450
+ if (!isOxcAstNode(declarator)) continue;
1451
+ const idNode = declarator.id;
1452
+ const initializerNode = declarator.init;
1453
+ if (!idNode || !initializerNode) continue;
1454
+ if (idNode.type !== "Identifier") continue;
1455
+ const constantName = idNode.name;
1456
+ if (!constantName) continue;
1457
+ if (FRAMEWORK_RESERVED_CONSTANT_NAMES.has(constantName)) continue;
1458
+ if (!isLiteralCandidate(initializerNode)) continue;
1459
+ candidates.push({
1460
+ constantName,
1461
+ literalHash: hashLiteralNode(initializerNode),
1462
+ literalPreview: previewLiteralNode(initializerNode),
1463
+ startOffset: declarator.start ?? inner.start ?? 0
1464
+ });
1465
+ }
1466
+ };
1467
+ const collectDuplicateConstantCandidates = (programBody) => {
1468
+ const candidates = [];
1469
+ for (const statement of programBody) visitForConstants(statement, candidates);
1470
+ return candidates;
1471
+ };
1472
+
312
1473
  //#endregion
313
1474
  //#region src/collect/parse.ts
314
1475
  const extractMdxImportsExports = (sourceText) => {
@@ -398,19 +1559,156 @@ const parseCssImports = (filePath) => {
398
1559
  imports,
399
1560
  exports: [],
400
1561
  memberAccesses: [],
401
- wholeObjectUses: []
1562
+ wholeObjectUses: [],
1563
+ localIdentifierReferences: [],
1564
+ redundantTypePatterns: [],
1565
+ identityWrappers: [],
1566
+ typeDefinitionHashes: [],
1567
+ inlineTypeLiterals: [],
1568
+ simplifiableFunctions: [],
1569
+ simplifiableExpressions: [],
1570
+ duplicateConstantCandidates: [],
1571
+ errors: []
402
1572
  };
403
1573
  };
404
1574
  const NON_JS_EXTENSIONS = [".graphql", ".gql"];
1575
+ const collectLocalIdentifierReferences = (statements) => {
1576
+ const references = [];
1577
+ const seenNames = /* @__PURE__ */ new Set();
1578
+ const visitNode = (node) => {
1579
+ if (!node || typeof node !== "object") return;
1580
+ const record = node;
1581
+ if (record.type === "Identifier" && typeof record.name === "string") {
1582
+ if (!seenNames.has(record.name)) {
1583
+ seenNames.add(record.name);
1584
+ references.push(record.name);
1585
+ }
1586
+ return;
1587
+ }
1588
+ for (const value of Object.values(record)) if (Array.isArray(value)) for (const innerValue of value) visitNode(innerValue);
1589
+ else if (value && typeof value === "object") visitNode(value);
1590
+ };
1591
+ for (const statement of statements) {
1592
+ if (statement.type === "ImportDeclaration" || statement.type === "ExportNamedDeclaration" || statement.type === "ExportDefaultDeclaration" || statement.type === "ExportAllDeclaration") continue;
1593
+ visitNode(statement);
1594
+ }
1595
+ return references;
1596
+ };
1597
+ const createEmptyParsedSource = () => ({
1598
+ imports: [],
1599
+ exports: [],
1600
+ memberAccesses: [],
1601
+ wholeObjectUses: [],
1602
+ localIdentifierReferences: [],
1603
+ redundantTypePatterns: [],
1604
+ identityWrappers: [],
1605
+ typeDefinitionHashes: [],
1606
+ inlineTypeLiterals: [],
1607
+ simplifiableFunctions: [],
1608
+ simplifiableExpressions: [],
1609
+ duplicateConstantCandidates: [],
1610
+ errors: []
1611
+ });
1612
+ const stripByteOrderMark = (sourceText) => {
1613
+ if (sourceText.charCodeAt(0) === 65279) return sourceText.slice(1);
1614
+ return sourceText;
1615
+ };
1616
+ const looksLikeBinaryContent = (sourceText) => {
1617
+ const sampleLength = Math.min(sourceText.length, BINARY_DETECTION_SAMPLE_BYTES);
1618
+ let nullByteCount = 0;
1619
+ for (let scanIndex = 0; scanIndex < sampleLength; scanIndex++) {
1620
+ if (sourceText.charCodeAt(scanIndex) === 0) nullByteCount++;
1621
+ if (nullByteCount > 4) return true;
1622
+ }
1623
+ return false;
1624
+ };
1625
+ const looksLikeMinifiedSource = (sourceText) => {
1626
+ if (sourceText.length < 5e3) return false;
1627
+ let newlineCount = 0;
1628
+ for (let scanIndex = 0; scanIndex < sourceText.length; scanIndex++) if (sourceText.charCodeAt(scanIndex) === 10) newlineCount++;
1629
+ return sourceText.length / (newlineCount + 1) > 500;
1630
+ };
1631
+ const safeReadSourceFile = (filePath, errors) => {
1632
+ try {
1633
+ const stats = (0, node_fs.statSync)(filePath);
1634
+ if (stats.size === 0) {
1635
+ errors.push(new FileReadError({
1636
+ code: "file-empty",
1637
+ severity: "info",
1638
+ message: "file is empty — nothing to analyze",
1639
+ path: filePath
1640
+ }));
1641
+ return;
1642
+ }
1643
+ if (stats.size > 2e6) {
1644
+ errors.push(new FileReadError({
1645
+ code: "file-too-large",
1646
+ message: `file size ${stats.size}B exceeds MAX_PARSE_FILE_SIZE_BYTES (${MAX_PARSE_FILE_SIZE_BYTES})`,
1647
+ path: filePath
1648
+ }));
1649
+ return;
1650
+ }
1651
+ } catch (statError) {
1652
+ errors.push(new FileReadError({
1653
+ code: "file-read-failed",
1654
+ message: "could not stat source file",
1655
+ path: filePath,
1656
+ detail: describeUnknownError(statError)
1657
+ }));
1658
+ return;
1659
+ }
1660
+ try {
1661
+ const sourceText = stripByteOrderMark((0, node_fs.readFileSync)(filePath, "utf-8"));
1662
+ if (looksLikeBinaryContent(sourceText)) {
1663
+ errors.push(new FileReadError({
1664
+ code: "file-binary",
1665
+ severity: "info",
1666
+ message: "file appears to be binary — skipping",
1667
+ path: filePath
1668
+ }));
1669
+ return;
1670
+ }
1671
+ if (looksLikeMinifiedSource(sourceText)) {
1672
+ errors.push(new FileReadError({
1673
+ code: "file-minified",
1674
+ severity: "info",
1675
+ message: "file appears to be a minified/bundled artifact — skipping redundancy analysis",
1676
+ path: filePath
1677
+ }));
1678
+ return;
1679
+ }
1680
+ return sourceText;
1681
+ } catch (readError) {
1682
+ errors.push(new FileReadError({
1683
+ code: "file-read-failed",
1684
+ message: "could not read source file",
1685
+ path: filePath,
1686
+ detail: describeUnknownError(readError)
1687
+ }));
1688
+ return;
1689
+ }
1690
+ };
405
1691
  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: []
1692
+ if (CSS_EXTENSIONS.some((ext) => filePath.endsWith(ext))) try {
1693
+ return parseCssImports(filePath);
1694
+ } catch (cssError) {
1695
+ return {
1696
+ ...createEmptyParsedSource(),
1697
+ errors: [new ParseError({
1698
+ code: "parse-failed",
1699
+ message: "CSS import parsing crashed",
1700
+ path: filePath,
1701
+ detail: describeUnknownError(cssError)
1702
+ })]
1703
+ };
1704
+ }
1705
+ if (NON_JS_EXTENSIONS.some((ext) => filePath.endsWith(ext))) return createEmptyParsedSource();
1706
+ const earlyErrors = [];
1707
+ const sourceText = safeReadSourceFile(filePath, earlyErrors);
1708
+ if (sourceText === void 0) return {
1709
+ ...createEmptyParsedSource(),
1710
+ errors: earlyErrors
412
1711
  };
413
- const sourceText = (0, node_fs.readFileSync)(filePath, "utf-8");
414
1712
  const imports = [];
415
1713
  const exports = [];
416
1714
  const isMdx = filePath.endsWith(".mdx");
@@ -419,58 +1717,215 @@ const parseSourceFile = (filePath) => {
419
1717
  const isSvelte = filePath.endsWith(".svelte");
420
1718
  const textToParse = isMdx ? extractMdxImportsExports(sourceText) : isAstro ? extractAstroFrontmatter(sourceText) : isVue ? extractVueScriptContent(sourceText) : isSvelte ? extractSvelteScriptContent(sourceText) : sourceText;
421
1719
  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);
1720
+ let result;
1721
+ try {
1722
+ result = (0, oxc_parser.parseSync)(parseFileName, textToParse);
1723
+ } catch (parseError) {
1724
+ return {
1725
+ ...createEmptyParsedSource(),
1726
+ errors: [...earlyErrors, new ParseError({
1727
+ code: "parse-failed",
1728
+ message: "oxc-parser threw during initial parse",
1729
+ path: filePath,
1730
+ detail: describeUnknownError(parseError)
1731
+ })]
1732
+ };
1733
+ }
1734
+ if ((parseFileName.endsWith(".js") || parseFileName.endsWith(".mjs") || parseFileName.endsWith(".cjs")) && result.errors.length > 0) try {
1735
+ const jsxResult = (0, oxc_parser.parseSync)(parseFileName.replace(/\.(m?js|cjs)$/, ".jsx"), textToParse);
1736
+ if (jsxResult.errors.length === 0) result = jsxResult;
1737
+ else {
1738
+ const tsxResult = (0, oxc_parser.parseSync)(parseFileName.replace(/\.(m?js|cjs)$/, ".tsx"), textToParse);
1739
+ if (tsxResult.errors.length === 0) result = tsxResult;
1740
+ }
1741
+ } catch {}
428
1742
  if (result.errors.length > 0) return {
1743
+ ...createEmptyParsedSource(),
429
1744
  imports,
430
1745
  exports,
431
- memberAccesses: [],
432
- wholeObjectUses: []
1746
+ errors: [...earlyErrors, new ParseError({
1747
+ code: "parse-recovered",
1748
+ severity: "info",
1749
+ message: `oxc-parser reported ${result.errors.length} syntax issue(s); skipping deep analysis for this file`,
1750
+ path: filePath
1751
+ })]
433
1752
  };
434
1753
  const program = result.program;
435
1754
  if (!program?.body) return {
1755
+ ...createEmptyParsedSource(),
436
1756
  imports,
437
1757
  exports,
438
- memberAccesses: [],
439
- wholeObjectUses: []
1758
+ errors: [...earlyErrors, new ParseError({
1759
+ code: "parse-failed",
1760
+ message: "oxc-parser returned no program body",
1761
+ path: filePath
1762
+ })]
440
1763
  };
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);
1764
+ const detectorErrors = [];
1765
+ const safeWalk = (walkerName, walker, fallback) => {
1766
+ try {
1767
+ return walker();
1768
+ } catch (walkError) {
1769
+ detectorErrors.push(new ParseError({
1770
+ code: "ast-walk-failed",
1771
+ message: `${walkerName} threw during AST traversal`,
1772
+ path: filePath,
1773
+ detail: describeUnknownError(walkError)
1774
+ }));
1775
+ return fallback;
1776
+ }
1777
+ };
1778
+ safeWalk("extractImportsAndExports", () => {
1779
+ for (const node of program.body) switch (node.type) {
1780
+ case "ImportDeclaration":
1781
+ extractImportDeclaration(node, sourceText, imports);
1782
+ break;
1783
+ case "ExportNamedDeclaration":
1784
+ extractNamedExportDeclaration(node, sourceText, exports);
1785
+ break;
1786
+ case "ExportDefaultDeclaration":
1787
+ extractDefaultExportDeclaration(node, sourceText, exports);
1788
+ break;
1789
+ case "ExportAllDeclaration":
1790
+ extractExportAllDeclaration(node, sourceText, exports);
1791
+ break;
1792
+ }
1793
+ }, void 0);
1794
+ safeWalk("collectDynamicImports", () => {
1795
+ collectDynamicImports(program.body, sourceText, imports);
1796
+ }, void 0);
456
1797
  const namespaceLocalNames = collectNamespaceLocalNames(imports);
457
1798
  const memberAccesses = [];
458
1799
  const wholeObjectUses = [];
459
- if (namespaceLocalNames.size > 0) collectMemberAccesses(program.body, namespaceLocalNames, memberAccesses, wholeObjectUses);
1800
+ if (namespaceLocalNames.size > 0) safeWalk("collectMemberAccesses", () => {
1801
+ collectMemberAccesses(program.body, namespaceLocalNames, memberAccesses, wholeObjectUses);
1802
+ }, void 0);
1803
+ const localIdentifierReferences = safeWalk("collectLocalIdentifierReferences", () => collectLocalIdentifierReferences(program.body), []);
1804
+ const redundantTypePatterns = [];
1805
+ const identityWrappers = [];
1806
+ const typeDefinitionHashes = [];
1807
+ safeWalk("collectDryPatterns", () => {
1808
+ collectDryPatterns(program.body, sourceText, redundantTypePatterns, identityWrappers, typeDefinitionHashes);
1809
+ }, void 0);
460
1810
  return {
461
1811
  imports,
462
1812
  exports,
463
1813
  memberAccesses,
464
- wholeObjectUses
1814
+ wholeObjectUses,
1815
+ localIdentifierReferences,
1816
+ redundantTypePatterns,
1817
+ identityWrappers,
1818
+ typeDefinitionHashes,
1819
+ inlineTypeLiterals: safeWalk("collectInlineTypeLiterals", () => collectInlineTypeLiterals(program.body), []).map((capture) => ({
1820
+ structuralHash: capture.structuralHash,
1821
+ memberCount: capture.memberCount,
1822
+ preview: capture.preview,
1823
+ context: capture.context,
1824
+ nearestName: capture.nearestName,
1825
+ line: getLineFromOffset(sourceText, capture.startOffset),
1826
+ column: getColumnFromOffset(sourceText, capture.startOffset)
1827
+ })),
1828
+ simplifiableFunctions: safeWalk("collectSimplifiableFunctions", () => collectSimplifiableFunctions(program.body), []).map((capture) => ({
1829
+ kind: capture.kind,
1830
+ functionName: capture.functionName,
1831
+ line: getLineFromOffset(sourceText, capture.startOffset),
1832
+ column: getColumnFromOffset(sourceText, capture.startOffset),
1833
+ reason: capture.reason,
1834
+ suggestion: capture.suggestion
1835
+ })),
1836
+ simplifiableExpressions: safeWalk("collectSimplifiableExpressions", () => collectSimplifiableExpressions(program.body), []).map((capture) => ({
1837
+ kind: capture.kind,
1838
+ snippet: capture.snippet,
1839
+ line: getLineFromOffset(sourceText, capture.startOffset),
1840
+ column: getColumnFromOffset(sourceText, capture.startOffset),
1841
+ reason: capture.reason,
1842
+ suggestion: capture.suggestion
1843
+ })),
1844
+ duplicateConstantCandidates: safeWalk("collectDuplicateConstantCandidates", () => collectDuplicateConstantCandidates(program.body), []).map((capture) => ({
1845
+ constantName: capture.constantName,
1846
+ literalHash: capture.literalHash,
1847
+ literalPreview: capture.literalPreview,
1848
+ line: getLineFromOffset(sourceText, capture.startOffset),
1849
+ column: getColumnFromOffset(sourceText, capture.startOffset)
1850
+ })),
1851
+ errors: [...earlyErrors, ...detectorErrors]
465
1852
  };
466
1853
  };
467
- const WHOLE_OBJECT_FUNCTION_NAMES = new Set([
468
- "keys",
469
- "values",
470
- "entries",
471
- "assign",
472
- "freeze",
473
- "getOwnPropertyNames",
1854
+ const collectDryPatterns = (bodyNodes, sourceText, redundantTypePatterns, identityWrappers, typeDefinitionHashes) => {
1855
+ for (const statement of bodyNodes) inspectStatement(statement, sourceText, redundantTypePatterns, identityWrappers, typeDefinitionHashes);
1856
+ };
1857
+ const inspectStatement = (statementNode, sourceText, redundantTypePatterns, identityWrappers, typeDefinitionHashes) => {
1858
+ let declarationOfInterest = statementNode;
1859
+ if (statementNode.type === "ExportNamedDeclaration" && statementNode.declaration) declarationOfInterest = statementNode.declaration;
1860
+ if (declarationOfInterest && typeof declarationOfInterest === "object") {
1861
+ const declarationNode = declarationOfInterest;
1862
+ if (declarationNode.type === "TSTypeAliasDeclaration") {
1863
+ const typeAliasName = declarationNode.id?.name;
1864
+ const typeAnnotation = declarationNode.typeAnnotation;
1865
+ const startOffset = declarationNode.start ?? 0;
1866
+ if (typeAliasName && typeAnnotation) {
1867
+ const redundantPattern = detectRedundantTypePatternForTypeAnnotation(typeAnnotation);
1868
+ if (redundantPattern) redundantTypePatterns.push({
1869
+ typeName: typeAliasName,
1870
+ kind: redundantPattern.kind,
1871
+ line: getLineFromOffset(sourceText, startOffset),
1872
+ column: getColumnFromOffset(sourceText, startOffset),
1873
+ reason: redundantPattern.reason,
1874
+ suggestion: redundantPattern.suggestion
1875
+ });
1876
+ typeDefinitionHashes.push({
1877
+ typeName: typeAliasName,
1878
+ structuralHash: `alias:${normalizeTypeAstHash(typeAnnotation)}`,
1879
+ line: getLineFromOffset(sourceText, startOffset),
1880
+ column: getColumnFromOffset(sourceText, startOffset)
1881
+ });
1882
+ }
1883
+ } else if (declarationNode.type === "TSInterfaceDeclaration") {
1884
+ const interfaceName = declarationNode.id?.name;
1885
+ const startOffset = declarationNode.start ?? 0;
1886
+ if (interfaceName) {
1887
+ const redundantPattern = detectRedundantInterfaceDeclaration(declarationNode);
1888
+ if (redundantPattern) redundantTypePatterns.push({
1889
+ typeName: interfaceName,
1890
+ kind: redundantPattern.kind,
1891
+ line: getLineFromOffset(sourceText, startOffset),
1892
+ column: getColumnFromOffset(sourceText, startOffset),
1893
+ reason: redundantPattern.reason,
1894
+ suggestion: redundantPattern.suggestion
1895
+ });
1896
+ const declarationCopy = {
1897
+ ...declarationNode,
1898
+ id: void 0
1899
+ };
1900
+ typeDefinitionHashes.push({
1901
+ typeName: interfaceName,
1902
+ structuralHash: `interface:${normalizeTypeAstHash(declarationCopy)}`,
1903
+ line: getLineFromOffset(sourceText, startOffset),
1904
+ column: getColumnFromOffset(sourceText, startOffset)
1905
+ });
1906
+ }
1907
+ } else if (declarationNode.type === "VariableDeclaration") for (const declarator of declarationNode.declarations ?? []) {
1908
+ const wrapperName = declarator.id?.name;
1909
+ const initializerNode = declarator.init;
1910
+ const startOffset = declarator.start ?? declarationNode.start ?? 0;
1911
+ if (!wrapperName || !initializerNode) continue;
1912
+ const wrapperDetection = detectIdentityWrapperFromInitializer(initializerNode, wrapperName);
1913
+ if (wrapperDetection) identityWrappers.push({
1914
+ wrapperName,
1915
+ wrappedExpression: wrapperDetection.wrappedExpression,
1916
+ line: getLineFromOffset(sourceText, startOffset),
1917
+ column: getColumnFromOffset(sourceText, startOffset)
1918
+ });
1919
+ }
1920
+ }
1921
+ };
1922
+ const WHOLE_OBJECT_FUNCTION_NAMES = new Set([
1923
+ "keys",
1924
+ "values",
1925
+ "entries",
1926
+ "assign",
1927
+ "freeze",
1928
+ "getOwnPropertyNames",
474
1929
  "getOwnPropertyDescriptors"
475
1930
  ]);
476
1931
  const collectNamespaceLocalNames = (imports) => {
@@ -557,12 +2012,14 @@ const extractImportDeclaration = (node, sourceText, imports) => {
557
2012
  case "ImportSpecifier": {
558
2013
  const importedName = getModuleExportNameValue(specifierNode.imported);
559
2014
  const localName = specifierNode.local.name;
2015
+ const isSelfAlias = localName === importedName && specifierNode.imported.type === "Identifier" && specifierNode.imported.start !== specifierNode.local.start;
560
2016
  importedNames.push({
561
2017
  name: importedName,
562
2018
  alias: localName !== importedName ? localName : void 0,
563
2019
  isNamespace: false,
564
2020
  isDefault: importedName === "default",
565
- isTypeOnly: isTypeOnly || specifierNode.importKind === "type"
2021
+ isTypeOnly: isTypeOnly || specifierNode.importKind === "type",
2022
+ isRedundantAlias: isSelfAlias || void 0
566
2023
  });
567
2024
  break;
568
2025
  }
@@ -587,11 +2044,12 @@ const extractImportDeclaration = (node, sourceText, imports) => {
587
2044
  };
588
2045
  const extractNamedExportDeclaration = (node, sourceText, exports) => {
589
2046
  const isTypeOnly = node.exportKind === "type";
590
- const reExportSource = node.source?.value ?? void 0;
2047
+ const reExportSource = node.source?.value;
591
2048
  if (node.declaration) extractDeclarationNames(node.declaration, isTypeOnly, sourceText, exports, node.start);
592
2049
  for (const specifierNode of node.specifiers) {
593
2050
  const exportedName = getModuleExportNameValue(specifierNode.exported);
594
2051
  const localName = getModuleExportNameValue(specifierNode.local);
2052
+ const isSelfAlias = exportedName === localName && specifierNode.exported.type === "Identifier" && specifierNode.local.type === "Identifier" && specifierNode.exported.start !== specifierNode.local.start;
595
2053
  exports.push({
596
2054
  name: exportedName,
597
2055
  isDefault: exportedName === "default",
@@ -602,15 +2060,13 @@ const extractNamedExportDeclaration = (node, sourceText, exports) => {
602
2060
  reExportOriginalName: reExportSource !== void 0 ? localName : void 0,
603
2061
  isNamespaceReExport: false,
604
2062
  line: getLineFromOffset(sourceText, specifierNode.start ?? node.start),
605
- column: getColumnFromOffset(sourceText, specifierNode.start ?? node.start)
2063
+ column: getColumnFromOffset(sourceText, specifierNode.start ?? node.start),
2064
+ isRedundantAlias: isSelfAlias || void 0
606
2065
  });
607
2066
  }
608
2067
  };
609
2068
  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;
2069
+ const defaultExportLocalName = extractDefaultExportLocalName(node.declaration);
614
2070
  exports.push({
615
2071
  name: "default",
616
2072
  isDefault: true,
@@ -1718,6 +3174,198 @@ const findMonorepoRoot = (rootDir) => {
1718
3174
  }
1719
3175
  };
1720
3176
 
3177
+ //#endregion
3178
+ //#region src/utils/resolve-entry-with-extensions.ts
3179
+ const RESOLVABLE_EXTENSIONS = [
3180
+ ".ts",
3181
+ ".tsx",
3182
+ ".js",
3183
+ ".jsx",
3184
+ ".mjs",
3185
+ ".mts",
3186
+ ".cjs",
3187
+ ".cts"
3188
+ ];
3189
+ const resolveEntryWithExtensions = (basePath) => {
3190
+ if ((0, node_fs.existsSync)(basePath)) return basePath;
3191
+ for (const extension of RESOLVABLE_EXTENSIONS) {
3192
+ const withExtension = basePath + extension;
3193
+ if ((0, node_fs.existsSync)(withExtension)) return withExtension;
3194
+ }
3195
+ for (const extension of RESOLVABLE_EXTENSIONS) {
3196
+ const indexCandidate = (0, node_path.join)(basePath, `index${extension}`);
3197
+ if ((0, node_fs.existsSync)(indexCandidate)) return indexCandidate;
3198
+ }
3199
+ };
3200
+ const resolveEntryPathWithExtensions = (entryPath, rootDirectory) => {
3201
+ return resolveEntryWithExtensions((0, node_path.resolve)(rootDirectory, entryPath));
3202
+ };
3203
+
3204
+ //#endregion
3205
+ //#region src/collect/config-string-entries.ts
3206
+ const CONFIG_STRING_ENTRY_GLOBS = [
3207
+ "webpack.config.{js,ts,mjs,cjs}",
3208
+ "**/webpack*.config.{js,ts,mjs,cjs,babel.js}",
3209
+ "**/configs/webpack.config.{js,ts,mjs,cjs,babel.js}",
3210
+ "**/configs/webpack*.config.{js,ts,mjs,cjs,babel.js}",
3211
+ "jest.config.{js,ts,mjs,cjs,cts}",
3212
+ "**/jest.config.{js,ts,mjs,cjs,cts}",
3213
+ "vitest.config.{js,ts,mjs,mts}",
3214
+ "**/vitest.config.{js,ts,mjs,mts}",
3215
+ "**/vitest.*.config.{js,ts,mjs,mts}",
3216
+ "vite.config.{js,ts,mjs,mts}",
3217
+ "tailwind.config.{js,ts,cjs,mjs}",
3218
+ "**/tailwind.config.{js,ts,cjs,mjs}",
3219
+ "electron.vite.config.{js,ts,mjs}",
3220
+ "electron-builder.config.{js,ts,cjs}",
3221
+ "esbuild*.ts",
3222
+ "**/esbuild.entrypoints.ts",
3223
+ "metro.config.{js,ts}",
3224
+ "playwright.config.{js,ts}",
3225
+ "cypress.config.{js,ts}",
3226
+ "rollup.config.{js,ts,mjs,cjs}",
3227
+ "rollup.*.config.js",
3228
+ "**/.erb/configs/webpack*.config.{js,ts}",
3229
+ "**/.erb/configs/webpack.config.*.{js,ts}",
3230
+ "**/astro-tina-directive/register.js",
3231
+ "rspack.config.{js,ts,mjs,cjs}",
3232
+ "rsbuild.config.{js,ts,mjs,cjs}",
3233
+ "**/scripts/build.ts",
3234
+ "**/scripts/utils/createJestConfig.js"
3235
+ ];
3236
+ const CONFIG_RELATIVE_PATH_PATTERN = /['"`]((\.{1,2}\/|\.\.\/)[^'"`\n]+?|\.\/[^'"`\n]+?)['"`]/g;
3237
+ const JEST_ROOT_DIR_PATH_PATTERN = /<rootDir>\/([^'"`\n]+?)(?:['"`]|$)/g;
3238
+ const RESOLVE_CALL_PATH_PATTERN = /resolve\s*\(\s*['"`]([^'"`\n]+?)['"`]\s*\)/g;
3239
+ const PATH_JOIN_STRING_PATTERN = /path\.(?:join|resolve)\(\s*[^,]+,\s*['"`]([^'"`\n]+?)['"`]/g;
3240
+ const ENTRY_POINTS_STRING_PATTERN = /entryPoints:\s*\[\s*['"`]([^'"`\n]+?)['"`]/g;
3241
+ const ADD_PREAMBLE_PATTERN = /addPreamble\s*\(\s*['"`]([^'"`\n]+?)['"`]\s*\)/g;
3242
+ const ROLLUP_INPUT_PATTERN = /\binput\s*:\s*['"`]([^'"`\n]+?)['"`]/g;
3243
+ const VITEST_ENVIRONMENT_PATTERN = /environment\s*:\s*['"`](\.\/[^'"`\n]+?)['"`]/g;
3244
+ const ASTRO_ENTRYPOINT_PATTERN = /entrypoint\s*:\s*['"`](\.\/[^'"`\n]+?)['"`]/g;
3245
+ const WEBPACK_PATH_JOIN_ENTRY_PATTERN = /path\.join\(\s*[^,]+,\s*['"`]([^'"`\n]+?)['"`]\s*\)/g;
3246
+ const WEBPACK_RENDERER_PATH_JOIN_PATTERN = /path\.join\(\s*webpackPaths\.srcRendererPath\s*,\s*['"`]([^'"`\n]+?)['"`]\s*\)/g;
3247
+ const WEBPACK_MAIN_PATH_JOIN_PATTERN = /path\.join\(\s*webpackPaths\.srcMainPath\s*,\s*['"`]([^'"`\n]+?)['"`]\s*\)/g;
3248
+ const BARE_CONFIG_PATH_PATTERN = /['"`](config\/[^'"`\n]+?)['"`]/g;
3249
+ 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, "");
3250
+ const shouldSkipConfigPath = (rawPath) => {
3251
+ if (rawPath.includes("*") || rawPath.includes("?")) return true;
3252
+ if (rawPath.endsWith(".json") && !rawPath.includes("/src/")) return true;
3253
+ if (rawPath.startsWith("node:")) return true;
3254
+ if (rawPath.startsWith("@")) return true;
3255
+ return false;
3256
+ };
3257
+ const addResolvedConfigPath = (rawPath, configDirectory, projectRootDirectory, entries) => {
3258
+ if (shouldSkipConfigPath(rawPath)) return;
3259
+ const resolvedEntry = resolveEntryWithExtensions((0, node_path.resolve)(rawPath.startsWith(".") ? configDirectory : projectRootDirectory, rawPath.startsWith(".") ? rawPath : `./${rawPath}`));
3260
+ if (resolvedEntry) {
3261
+ entries.add(resolvedEntry);
3262
+ return;
3263
+ }
3264
+ if (rawPath.startsWith(".")) {
3265
+ const projectRootResolvedEntry = resolveEntryWithExtensions((0, node_path.resolve)(projectRootDirectory, rawPath));
3266
+ if (projectRootResolvedEntry) entries.add(projectRootResolvedEntry);
3267
+ }
3268
+ };
3269
+ const collectResolvedPathsFromStrings = (content, configDirectory, projectRootDirectory, entries) => {
3270
+ const contentWithoutImports = stripModuleImportStatements(content);
3271
+ const patterns = [
3272
+ CONFIG_RELATIVE_PATH_PATTERN,
3273
+ RESOLVE_CALL_PATH_PATTERN,
3274
+ PATH_JOIN_STRING_PATTERN,
3275
+ ENTRY_POINTS_STRING_PATTERN,
3276
+ ADD_PREAMBLE_PATTERN,
3277
+ ROLLUP_INPUT_PATTERN,
3278
+ VITEST_ENVIRONMENT_PATTERN,
3279
+ ASTRO_ENTRYPOINT_PATTERN,
3280
+ WEBPACK_PATH_JOIN_ENTRY_PATTERN,
3281
+ BARE_CONFIG_PATH_PATTERN
3282
+ ];
3283
+ for (const pattern of patterns) {
3284
+ let pathMatch;
3285
+ pattern.lastIndex = 0;
3286
+ while ((pathMatch = pattern.exec(contentWithoutImports)) !== null) addResolvedConfigPath(pathMatch[1], configDirectory, projectRootDirectory, entries);
3287
+ }
3288
+ let rendererEntryMatch;
3289
+ WEBPACK_RENDERER_PATH_JOIN_PATTERN.lastIndex = 0;
3290
+ while ((rendererEntryMatch = WEBPACK_RENDERER_PATH_JOIN_PATTERN.exec(contentWithoutImports)) !== null) addResolvedConfigPath(`src/renderer/${rendererEntryMatch[1]}`, configDirectory, projectRootDirectory, entries);
3291
+ let mainEntryMatch;
3292
+ WEBPACK_MAIN_PATH_JOIN_PATTERN.lastIndex = 0;
3293
+ while ((mainEntryMatch = WEBPACK_MAIN_PATH_JOIN_PATTERN.exec(contentWithoutImports)) !== null) addResolvedConfigPath(`src/main/${mainEntryMatch[1]}`, configDirectory, projectRootDirectory, entries);
3294
+ let rootDirMatch;
3295
+ JEST_ROOT_DIR_PATH_PATTERN.lastIndex = 0;
3296
+ while ((rootDirMatch = JEST_ROOT_DIR_PATH_PATTERN.exec(content)) !== null) addResolvedConfigPath(rootDirMatch[1], configDirectory, projectRootDirectory, entries);
3297
+ };
3298
+ const extractConfigStringReferencedEntries = (directory) => {
3299
+ const entries = /* @__PURE__ */ new Set();
3300
+ const configPaths = fast_glob.default.sync(CONFIG_STRING_ENTRY_GLOBS, {
3301
+ cwd: directory,
3302
+ absolute: true,
3303
+ onlyFiles: true,
3304
+ ignore: [
3305
+ "**/node_modules/**",
3306
+ "**/dist/**",
3307
+ "**/build/**"
3308
+ ],
3309
+ deep: 6
3310
+ });
3311
+ for (const configPath of configPaths) try {
3312
+ collectResolvedPathsFromStrings((0, node_fs.readFileSync)(configPath, "utf-8"), (0, node_path.dirname)(configPath), directory, entries);
3313
+ } catch {
3314
+ continue;
3315
+ }
3316
+ return [...entries];
3317
+ };
3318
+
3319
+ //#endregion
3320
+ //#region src/collect/sections-module-entries.ts
3321
+ const SECTIONS_FILE_GLOBS = ["sections.js", "**/sections.js"];
3322
+ const CALYPSO_MODULE_PATTERN = /module:\s*['"]calypso\/([^'"]+)['"]/g;
3323
+ const SECTION_BOOTSTRAP_SUFFIXES = [
3324
+ "",
3325
+ "/index",
3326
+ "/index.js",
3327
+ "/index.jsx",
3328
+ "/index.ts",
3329
+ "/index.tsx",
3330
+ "/main",
3331
+ "/controller",
3332
+ "/controller.js",
3333
+ "/controller.jsx"
3334
+ ];
3335
+ const addSectionModuleEntry = (modulePath, projectRootDirectory, entries) => {
3336
+ const moduleBasePath = (0, node_path.resolve)(projectRootDirectory, modulePath.replace(/^calypso\//, ""));
3337
+ for (const suffix of SECTION_BOOTSTRAP_SUFFIXES) {
3338
+ const resolvedEntry = resolveEntryWithExtensions(suffix ? `${moduleBasePath}${suffix}` : moduleBasePath);
3339
+ if (resolvedEntry) entries.add(resolvedEntry);
3340
+ }
3341
+ };
3342
+ const extractSectionsModuleEntries = (directory) => {
3343
+ const entries = /* @__PURE__ */ new Set();
3344
+ const sectionsFilePaths = fast_glob.default.sync(SECTIONS_FILE_GLOBS, {
3345
+ cwd: directory,
3346
+ absolute: true,
3347
+ onlyFiles: true,
3348
+ ignore: [
3349
+ "**/node_modules/**",
3350
+ "**/dist/**",
3351
+ "**/build/**"
3352
+ ],
3353
+ deep: 4
3354
+ });
3355
+ for (const sectionsFilePath of sectionsFilePaths) {
3356
+ if (!sectionsFilePath.endsWith("/client/sections.js")) continue;
3357
+ try {
3358
+ const content = (0, node_fs.readFileSync)(sectionsFilePath, "utf-8");
3359
+ let moduleMatch;
3360
+ CALYPSO_MODULE_PATTERN.lastIndex = 0;
3361
+ while ((moduleMatch = CALYPSO_MODULE_PATTERN.exec(content)) !== null) addSectionModuleEntry(moduleMatch[1], directory, entries);
3362
+ } catch {
3363
+ continue;
3364
+ }
3365
+ }
3366
+ return [...entries];
3367
+ };
3368
+
1721
3369
  //#endregion
1722
3370
  //#region src/collect/entries.ts
1723
3371
  const collectSourceFiles = async (config) => {
@@ -1829,6 +3477,9 @@ const resolveEntries = async (config) => {
1829
3477
  for (const workspacePackage of entryEligiblePackages) webWorkerEntries.push(...extractWebWorkerEntries(workspacePackage.directory));
1830
3478
  const tsConfigIncludeEntries = extractTsConfigIncludeFilesEntries(absoluteRoot);
1831
3479
  for (const workspacePackage of entryEligiblePackages) tsConfigIncludeEntries.push(...extractTsConfigIncludeFilesEntries(workspacePackage.directory));
3480
+ const configStringEntries = extractConfigStringReferencedEntries(absoluteRoot);
3481
+ for (const workspacePackage of entryEligiblePackages) configStringEntries.push(...extractConfigStringReferencedEntries(workspacePackage.directory));
3482
+ const sectionsModuleEntries = extractSectionsModuleEntries(absoluteRoot);
1832
3483
  const wranglerEntries = extractWranglerEntries(absoluteRoot);
1833
3484
  for (const workspacePackage of entryEligiblePackages) wranglerEntries.push(...extractWranglerEntries(workspacePackage.directory));
1834
3485
  const testSetupEntries = extractTestSetupFiles(absoluteRoot);
@@ -1855,6 +3506,8 @@ const resolveEntries = async (config) => {
1855
3506
  ...browserExtensionEntries,
1856
3507
  ...webWorkerEntries,
1857
3508
  ...tsConfigIncludeEntries,
3509
+ ...configStringEntries,
3510
+ ...sectionsModuleEntries,
1858
3511
  ...wranglerEntries,
1859
3512
  ...pluginFileEntries,
1860
3513
  ...toolingDiscovery.entryFiles,
@@ -1992,15 +3645,21 @@ const extractPackageJsonEntries = async (packageJsonPath) => {
1992
3645
  if (packageJson.exports) {
1993
3646
  const exportEntries = [];
1994
3647
  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")) {
3648
+ for (const exportEntry of exportEntries) {
3649
+ const resolvedExportEntry = resolveEntryWithExtensions(exportEntry) ?? resolveEntryPathWithExtensions(exportEntry, rootDir) ?? resolveSourcePath(exportEntry, rootDir);
3650
+ if (resolvedExportEntry && (0, node_fs.existsSync)(resolvedExportEntry)) {
3651
+ entries.push(resolvedExportEntry);
3652
+ continue;
3653
+ }
3654
+ if (exportEntry.endsWith(".ts")) {
2000
3655
  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));
3656
+ if ((0, node_fs.existsSync)(tsxFallback)) {
3657
+ entries.push(tsxFallback);
3658
+ continue;
3659
+ }
3660
+ }
3661
+ if ((0, node_fs.existsSync)(exportEntry)) entries.push(exportEntry);
3662
+ else entries.push(resolveEntryPath(exportEntry, rootDir));
2004
3663
  }
2005
3664
  }
2006
3665
  if (packageJson.bin) {
@@ -2009,9 +3668,52 @@ const extractPackageJsonEntries = async (packageJsonPath) => {
2009
3668
  for (const binPath of Object.values(packageJson.bin)) if (typeof binPath === "string") entries.push(resolveEntryPath(binPath, rootDir));
2010
3669
  }
2011
3670
  }
3671
+ if (Array.isArray(packageJson.sideEffects)) for (const sideEffectPattern of packageJson.sideEffects) {
3672
+ if (typeof sideEffectPattern !== "string") continue;
3673
+ const sourcePatterns = expandSideEffectGlobToSourcePatterns(sideEffectPattern);
3674
+ for (const sourcePattern of sourcePatterns) {
3675
+ const matchedSideEffectFiles = fast_glob.default.sync(sourcePattern, {
3676
+ cwd: rootDir,
3677
+ absolute: true,
3678
+ onlyFiles: true,
3679
+ ignore: [
3680
+ "**/node_modules/**",
3681
+ "**/dist/**",
3682
+ "**/build/**"
3683
+ ]
3684
+ });
3685
+ for (const matchedSideEffectFile of matchedSideEffectFiles) if (isImportableSourceFile(matchedSideEffectFile)) entries.push(matchedSideEffectFile);
3686
+ }
3687
+ }
3688
+ if (packageJson.build && typeof packageJson.build === "object") {
3689
+ const buildConfig = packageJson.build;
3690
+ if (Array.isArray(buildConfig.files)) for (const buildFileEntry of buildConfig.files) {
3691
+ if (typeof buildFileEntry !== "string") continue;
3692
+ if (buildFileEntry.includes("*")) continue;
3693
+ const resolvedBuildFile = resolveEntryWithExtensions((0, node_path.resolve)(rootDir, buildFileEntry)) ?? resolveEntryPathWithExtensions(buildFileEntry, rootDir);
3694
+ if (resolvedBuildFile && (0, node_fs.existsSync)(resolvedBuildFile)) entries.push(resolvedBuildFile);
3695
+ }
3696
+ }
3697
+ if (packageJson.jest && typeof packageJson.jest === "object") {
3698
+ const jestRootDirMatches = JSON.stringify(packageJson.jest).matchAll(/<rootDir>\/([^"\\]+)/g);
3699
+ for (const jestRootDirMatch of jestRootDirMatches) {
3700
+ const resolvedJestFile = resolveEntryPathWithExtensions(jestRootDirMatch[1], rootDir);
3701
+ if (resolvedJestFile && (0, node_fs.existsSync)(resolvedJestFile)) entries.push(resolvedJestFile);
3702
+ }
3703
+ }
2012
3704
  } catch {}
2013
3705
  return entries;
2014
3706
  };
3707
+ const expandSideEffectGlobToSourcePatterns = (pattern) => {
3708
+ const patterns = new Set([pattern]);
3709
+ if (pattern.endsWith(".js")) {
3710
+ patterns.add(pattern.replace(/\.js$/, ".ts"));
3711
+ patterns.add(pattern.replace(/\.js$/, ".tsx"));
3712
+ }
3713
+ if (pattern.includes("/lib/") || pattern.startsWith("lib/")) patterns.add(pattern.replace(/\blib\b/g, "src"));
3714
+ if (pattern.includes("/esm/") || pattern.startsWith("esm/")) patterns.add(pattern.replace(/\besm\b/g, "src"));
3715
+ return [...patterns];
3716
+ };
2015
3717
  const SHELL_OPERATORS_PATTERN = /\s*(?:&&|\|\||[;&|])\s*/;
2016
3718
  const SCRIPT_MULTIPLEXERS = new Set([
2017
3719
  "concurrently",
@@ -2389,23 +4091,6 @@ const WEBPACK_ENTRY_BLOCK_PATTERN = /entry\s*:\s*(?:\{[^}]*\}|\[[^\]]*\]|['"][^'
2389
4091
  const WEBPACK_ENTRY_FILE_PATTERN = /['"]([^'"]+)['"]/g;
2390
4092
  const WEBPACK_PATH_JOIN_PATTERN = /path\.(?:join|resolve)\(\s*__dirname\s*,\s*((?:['"][^'"]*['"]\s*,?\s*)+)\)/g;
2391
4093
  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
4094
  const extractWebpackEntryPoints = (directory) => {
2410
4095
  const entries = [];
2411
4096
  const webpackConfigPaths = fast_glob.default.sync([
@@ -2451,7 +4136,7 @@ const extractWebpackEntryPoints = (directory) => {
2451
4136
  while ((valueMatch = WEBPACK_ENTRY_FILE_PATTERN.exec(entryBlock)) !== null) {
2452
4137
  const entryPath = valueMatch[1];
2453
4138
  if (entryPath.startsWith("./") || entryPath.startsWith("../") || !entryPath.startsWith("/")) {
2454
- const resolvedEntry = resolveEntryWithExtensions((0, node_path.resolve)(directory, entryPath));
4139
+ const resolvedEntry = resolveEntryWithExtensions((0, node_path.resolve)(configDirectory, entryPath));
2455
4140
  if (resolvedEntry) entries.push(resolvedEntry);
2456
4141
  }
2457
4142
  }
@@ -2941,15 +4626,15 @@ const extractTestSetupFiles = (directory) => {
2941
4626
  const arrayContent = setupMatch[1];
2942
4627
  const singleValue = setupMatch[2];
2943
4628
  if (singleValue) {
2944
- const absolutePath = (0, node_path.resolve)(configDirectory, singleValue);
2945
- if ((0, node_fs.existsSync)(absolutePath)) entries.push(absolutePath);
4629
+ const resolvedPath = resolveEntryWithExtensions((0, node_path.resolve)(configDirectory, singleValue));
4630
+ if (resolvedPath) entries.push(resolvedPath);
2946
4631
  }
2947
4632
  if (arrayContent) {
2948
4633
  let pathMatch;
2949
4634
  SETUP_FILE_PATH_PATTERN.lastIndex = 0;
2950
4635
  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);
4636
+ const resolvedPath = resolveEntryWithExtensions((0, node_path.resolve)(configDirectory, pathMatch[1]));
4637
+ if (resolvedPath) entries.push(resolvedPath);
2953
4638
  }
2954
4639
  }
2955
4640
  }
@@ -3686,13 +5371,16 @@ const FRAMEWORK_PATTERNS = [
3686
5371
  "electron-builder",
3687
5372
  "@electron-forge/cli",
3688
5373
  "electron-vite",
3689
- "electron-webpack"
5374
+ "electron-webpack",
5375
+ "electron-next"
3690
5376
  ],
3691
5377
  enablerPrefixes: ["@electron-forge/", "@electron/"],
3692
5378
  entryPatterns: [
3693
5379
  "src/main/**/*.{ts,tsx,js,jsx}",
3694
5380
  "src/preload/**/*.{ts,tsx,js,jsx}",
3695
- "electron/main.{ts,js}"
5381
+ "electron/main.{ts,js}",
5382
+ "main/index.{ts,tsx,js,jsx}",
5383
+ "renderer/pages/**/*.{ts,tsx,js,jsx}"
3696
5384
  ],
3697
5385
  alwaysUsed: [
3698
5386
  "electron-builder.{yml,yaml,json,json5,toml}",
@@ -4105,7 +5793,7 @@ const TSCONFIG_FILENAMES = [
4105
5793
  "tsconfig.base.json",
4106
5794
  "jsconfig.json"
4107
5795
  ];
4108
- const findNearestTsconfig = (fromDir, rootDir, monorepoRootDir) => {
5796
+ const findNearestTsconfig$1 = (fromDir, rootDir, monorepoRootDir) => {
4109
5797
  let currentDirectory = fromDir;
4110
5798
  const stopAt = monorepoRootDir ? (0, node_path.resolve)(monorepoRootDir) : (0, node_path.resolve)(rootDir);
4111
5799
  while (currentDirectory.length >= stopAt.length) {
@@ -4307,7 +5995,7 @@ const createResolver = (config, workspacePackages = [], options = {}) => {
4307
5995
  const fileDir = (0, node_path.dirname)(filePath);
4308
5996
  const cached = tsconfigPathCache.get(fileDir);
4309
5997
  if (cached !== void 0) return cached;
4310
- const tsconfigResult = findNearestTsconfig(fileDir, config.rootDir, options.monorepoRoot) ?? rootTsconfigPath;
5998
+ const tsconfigResult = findNearestTsconfig$1(fileDir, config.rootDir, options.monorepoRoot) ?? rootTsconfigPath;
4311
5999
  tsconfigPathCache.set(fileDir, tsconfigResult);
4312
6000
  return tsconfigResult;
4313
6001
  };
@@ -4731,6 +6419,18 @@ const createResolver = (config, workspacePackages = [], options = {}) => {
4731
6419
  resolveResultCache.set(cacheKey, resolvedResult);
4732
6420
  return resolvedResult;
4733
6421
  }
6422
+ if (cleanedSpecifier.startsWith(".")) {
6423
+ const relativeResolved = tryResolveFromDirectory(fromDir, cleanedSpecifier);
6424
+ if (relativeResolved && existsAsFile(relativeResolved)) {
6425
+ const resolvedResult = {
6426
+ resolvedPath: relativeResolved,
6427
+ isExternal: false,
6428
+ packageName: void 0
6429
+ };
6430
+ resolveResultCache.set(cacheKey, resolvedResult);
6431
+ return resolvedResult;
6432
+ }
6433
+ }
4734
6434
  const unresolvedResult = {
4735
6435
  resolvedPath: void 0,
4736
6436
  isExternal: false,
@@ -4826,6 +6526,15 @@ const buildDependencyGraph = (inputs) => {
4826
6526
  exports: input.parsed.exports,
4827
6527
  memberAccesses: input.parsed.memberAccesses,
4828
6528
  wholeObjectUses: input.parsed.wholeObjectUses,
6529
+ localIdentifierReferences: input.parsed.localIdentifierReferences,
6530
+ redundantTypePatterns: input.parsed.redundantTypePatterns,
6531
+ identityWrappers: input.parsed.identityWrappers,
6532
+ typeDefinitionHashes: input.parsed.typeDefinitionHashes,
6533
+ inlineTypeLiterals: input.parsed.inlineTypeLiterals,
6534
+ simplifiableFunctions: input.parsed.simplifiableFunctions,
6535
+ simplifiableExpressions: input.parsed.simplifiableExpressions,
6536
+ duplicateConstantCandidates: input.parsed.duplicateConstantCandidates,
6537
+ parseErrors: input.parsed.errors,
4829
6538
  isEntryPoint: input.isEntryPoint,
4830
6539
  isTestEntry: input.isTestEntry,
4831
6540
  isReachable: false,
@@ -5194,6 +6903,7 @@ const detectDeadExports = (graph, config) => {
5194
6903
  if (!config.reportTypes && exportInfo.isTypeOnly) continue;
5195
6904
  const usageKey = `${module.fileId.path}::${exportInfo.name}`;
5196
6905
  if (usageMap.has(usageKey)) continue;
6906
+ if (module.localIdentifierReferences.includes(exportInfo.name)) continue;
5197
6907
  if (!exportInfo.isDefault && defaultExportLinkedNames.has(exportInfo.name)) continue;
5198
6908
  unusedExports.push({
5199
6909
  path: module.fileId.path,
@@ -5227,6 +6937,11 @@ const buildUsageMap = (graph) => {
5227
6937
  else {
5228
6938
  const importName = symbol.isDefault ? "default" : symbol.importedName;
5229
6939
  markExportUsedRecursive(targetModule.fileId.path, importName, graph, sourceToTargetMap, usedExportKeys, /* @__PURE__ */ new Set());
6940
+ if (symbol.isDefault) {
6941
+ if (!targetModule.exports.some((exportInfo) => exportInfo.isDefault) && symbol.localName !== "default") {
6942
+ if (targetModule.exports.find((exportInfo) => exportInfo.name === symbol.localName)) markExportUsedRecursive(targetModule.fileId.path, symbol.localName, graph, sourceToTargetMap, usedExportKeys, /* @__PURE__ */ new Set());
6943
+ }
6944
+ }
5230
6945
  }
5231
6946
  }
5232
6947
  return usedExportKeys;
@@ -5321,6 +7036,127 @@ const extractPackageName = (specifier) => {
5321
7036
  return normalizedSpecifier.split("/")[0];
5322
7037
  };
5323
7038
 
7039
+ //#endregion
7040
+ //#region src/utils/extract-override-target.ts
7041
+ const extractOverrideTargetPackage = (overrideValue) => {
7042
+ let normalizedValue = overrideValue.trim().replace(/^["']|["']$/g, "");
7043
+ if (normalizedValue.startsWith("npm:")) normalizedValue = normalizedValue.slice(4);
7044
+ if (normalizedValue.startsWith("@")) {
7045
+ const slashIndex = normalizedValue.indexOf("/");
7046
+ if (slashIndex === -1) return void 0;
7047
+ const scope = normalizedValue.slice(0, slashIndex);
7048
+ const remainder = normalizedValue.slice(slashIndex + 1);
7049
+ const versionSeparatorIndex = remainder.indexOf("@");
7050
+ const packageName = versionSeparatorIndex === -1 ? remainder : remainder.slice(0, versionSeparatorIndex);
7051
+ if (!packageName) return void 0;
7052
+ return `${scope}/${packageName}`;
7053
+ }
7054
+ const versionSeparatorIndex = normalizedValue.indexOf("@");
7055
+ return (versionSeparatorIndex === -1 ? normalizedValue : normalizedValue.slice(0, versionSeparatorIndex)) || void 0;
7056
+ };
7057
+
7058
+ //#endregion
7059
+ //#region src/utils/collect-override-mappings-from-record.ts
7060
+ const collectOverrideMappingsFromUnknown = (fromPackage, overrideValue, mappings) => {
7061
+ if (typeof overrideValue === "string") {
7062
+ const toPackage = extractOverrideTargetPackage(overrideValue);
7063
+ if (!toPackage) return;
7064
+ mappings.push({
7065
+ fromPackage,
7066
+ toPackage
7067
+ });
7068
+ return;
7069
+ }
7070
+ if (!overrideValue || typeof overrideValue !== "object" || Array.isArray(overrideValue)) return;
7071
+ for (const [nestedFromPackage, nestedValue] of Object.entries(overrideValue)) collectOverrideMappingsFromUnknown(nestedFromPackage, nestedValue, mappings);
7072
+ };
7073
+ const collectOverrideMappingsFromRecord = (overrideRecord) => {
7074
+ const mappings = [];
7075
+ for (const [fromPackage, overrideValue] of Object.entries(overrideRecord)) collectOverrideMappingsFromUnknown(fromPackage, overrideValue, mappings);
7076
+ return mappings;
7077
+ };
7078
+
7079
+ //#endregion
7080
+ //#region src/utils/parse-pnpm-workspace-overrides.ts
7081
+ const PNPM_WORKSPACE_FILENAMES = ["pnpm-workspace.yaml", "pnpm-workspace.yml"];
7082
+ const parseIndentedYamlMapping = (lines, startLineIndex, sectionIndent) => {
7083
+ const entries = {};
7084
+ let lineIndex = startLineIndex;
7085
+ while (lineIndex < lines.length) {
7086
+ const line = lines[lineIndex];
7087
+ const trimmedLine = line.trim();
7088
+ if (trimmedLine.length === 0 || trimmedLine.startsWith("#")) {
7089
+ lineIndex++;
7090
+ continue;
7091
+ }
7092
+ const indent = line.length - line.trimStart().length;
7093
+ if (indent <= sectionIndent) break;
7094
+ const colonIndex = trimmedLine.indexOf(":");
7095
+ if (colonIndex === -1) {
7096
+ lineIndex++;
7097
+ continue;
7098
+ }
7099
+ const key = trimmedLine.slice(0, colonIndex).trim().replace(/^["']|["']$/g, "");
7100
+ const rawValue = trimmedLine.slice(colonIndex + 1).trim();
7101
+ if (!key) {
7102
+ lineIndex++;
7103
+ continue;
7104
+ }
7105
+ if (rawValue.length === 0) {
7106
+ const nestedMapping = parseIndentedYamlMapping(lines, lineIndex + 1, indent);
7107
+ entries[key] = nestedMapping.entries;
7108
+ lineIndex = nestedMapping.endLineIndex;
7109
+ continue;
7110
+ }
7111
+ entries[key] = rawValue.replace(/^["']|["']$/g, "");
7112
+ lineIndex++;
7113
+ }
7114
+ return {
7115
+ entries,
7116
+ endLineIndex: lineIndex
7117
+ };
7118
+ };
7119
+ const parsePnpmWorkspaceOverrideRecords = (yamlContent) => {
7120
+ const lines = yamlContent.split("\n");
7121
+ const overrideRecords = [];
7122
+ for (let lineIndex = 0; lineIndex < lines.length; lineIndex++) {
7123
+ if (lines[lineIndex].trim() !== "overrides:") continue;
7124
+ const sectionIndent = lines[lineIndex].length - lines[lineIndex].trimStart().length;
7125
+ const parsedMapping = parseIndentedYamlMapping(lines, lineIndex + 1, sectionIndent);
7126
+ if (Object.keys(parsedMapping.entries).length > 0) overrideRecords.push(parsedMapping.entries);
7127
+ }
7128
+ return overrideRecords;
7129
+ };
7130
+ const collectPnpmWorkspaceOverrideMappings = (rootDir) => {
7131
+ const mappings = [];
7132
+ for (const workspaceFilename of PNPM_WORKSPACE_FILENAMES) {
7133
+ const workspacePath = (0, node_path.join)(rootDir, workspaceFilename);
7134
+ if (!(0, node_fs.existsSync)(workspacePath)) continue;
7135
+ try {
7136
+ const overrideRecords = parsePnpmWorkspaceOverrideRecords((0, node_fs.readFileSync)(workspacePath, "utf-8"));
7137
+ for (const overrideRecord of overrideRecords) mappings.push(...collectOverrideMappingsFromRecord(overrideRecord));
7138
+ } catch {
7139
+ continue;
7140
+ }
7141
+ }
7142
+ return mappings;
7143
+ };
7144
+
7145
+ //#endregion
7146
+ //#region src/utils/matches-package-import-reference.ts
7147
+ const escapeRegExp = (value) => value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
7148
+ const matchesPackageImportReference = (content, packageName) => {
7149
+ const escapedPackageName = escapeRegExp(packageName);
7150
+ const subpathPattern = `(?:/[^'"]*)?`;
7151
+ return [
7152
+ new RegExp(`\\bfrom\\s+['"]${escapedPackageName}${subpathPattern}['"]`),
7153
+ new RegExp(`\\bimport\\s+(?:[^'";\\n]*?\\sfrom\\s+)?['"]${escapedPackageName}${subpathPattern}['"]`),
7154
+ new RegExp(`\\brequire\\s*\\(\\s*['"]${escapedPackageName}${subpathPattern}['"]\\s*\\)`),
7155
+ new RegExp(`\\brequire\\s*\\(\\s*\`${escapedPackageName}${subpathPattern}`),
7156
+ new RegExp(`\\bimport\\s*\\(\\s*['"]${escapedPackageName}${subpathPattern}['"]`)
7157
+ ].some((pattern) => pattern.test(content));
7158
+ };
7159
+
5324
7160
  //#endregion
5325
7161
  //#region src/report/packages.ts
5326
7162
  const discoverAllPackageJsonPaths = (rootDir) => {
@@ -5415,6 +7251,11 @@ const detectStalePackages = (graph, config) => {
5415
7251
  for (const packageName of staticPeerSatisfied) usedPackageNames.add(packageName);
5416
7252
  const implicitCompanionPackages = collectImplicitCompanionPackages(declaredNames, usedPackageNames);
5417
7253
  for (const packageName of implicitCompanionPackages) usedPackageNames.add(packageName);
7254
+ const overrideMappings = collectOverrideMappings(configSearchRoots, allPackageJsonPaths, monorepoRoot);
7255
+ for (const { fromPackage, toPackage } of overrideMappings) {
7256
+ if (declaredNames.has(toPackage)) usedPackageNames.add(toPackage);
7257
+ if (usedPackageNames.has(fromPackage) && declaredNames.has(toPackage)) usedPackageNames.add(toPackage);
7258
+ }
5418
7259
  const candidateUnused = /* @__PURE__ */ new Set();
5419
7260
  for (const [dependencyName] of declaredDependencies) {
5420
7261
  if (isAlwaysConsideredUsed(dependencyName)) continue;
@@ -5511,7 +7352,8 @@ const collectStaticPeerSatisfiedPackages = (declaredNames, confirmedUsedNames) =
5511
7352
  };
5512
7353
  const IMPLICIT_COMPANION_DEPENDENCY_MAP = {
5513
7354
  jest: ["jest-config"],
5514
- "jest-cli": ["jest-config"]
7355
+ "jest-cli": ["jest-config"],
7356
+ "vite-plus": ["@voidzero-dev/vite-plus-core"]
5515
7357
  };
5516
7358
  const collectImplicitCompanionPackages = (declaredNames, confirmedUsedNames) => {
5517
7359
  const companions = /* @__PURE__ */ new Set();
@@ -5552,11 +7394,17 @@ const CLI_BINARY_TO_PACKAGE = {
5552
7394
  "rndebugger-open": "react-native-debugger-open",
5553
7395
  "simple-git-hooks": "simple-git-hooks",
5554
7396
  "generate-arg-types": "@webstudio-is/generate-arg-types",
5555
- email: "@react-email/preview-server"
7397
+ email: "@react-email/preview-server",
7398
+ vp: "vite-plus",
7399
+ turbo: "turbo",
7400
+ changeset: "@changesets/cli",
7401
+ tsx: "tsx"
5556
7402
  };
5557
7403
  const CLI_BINARY_FALLBACK_PACKAGES = {
5558
7404
  babel: ["babel-cli"],
5559
- jest: ["jest-cli"]
7405
+ jest: ["jest-cli"],
7406
+ remark: ["remark-cli"],
7407
+ dumi: ["dumi"]
5560
7408
  };
5561
7409
  const ENV_WRAPPER_BINARY_SET = new Set([
5562
7410
  "cross-env",
@@ -5661,7 +7509,12 @@ const CONFIG_FILE_GLOBS = [
5661
7509
  ".lintstagedrc.{js,cjs,mjs,json}",
5662
7510
  "commitlint.config.{js,cjs,mjs,ts}",
5663
7511
  ".commitlintrc.{js,cjs,mjs,json,yaml,yml}",
5664
- "tslint.json"
7512
+ "tslint.json",
7513
+ ".remarkrc",
7514
+ ".remarkrc.{js,cjs,mjs,json}",
7515
+ ".dumirc.ts",
7516
+ ".dumirc.js",
7517
+ "dumi.config.{ts,js}"
5665
7518
  ];
5666
7519
  const collectConfigReferencedPackages = (rootDir, graph, declaredNames) => {
5667
7520
  const referenced = /* @__PURE__ */ new Set();
@@ -5688,6 +7541,24 @@ const collectConfigReferencedPackages = (rootDir, graph, declaredNames) => {
5688
7541
  } catch {
5689
7542
  continue;
5690
7543
  }
7544
+ const documentationFiles = fast_glob.default.sync(["**/*.{mdx,md}"], {
7545
+ cwd: rootDir,
7546
+ absolute: true,
7547
+ onlyFiles: true,
7548
+ ignore: [
7549
+ "**/node_modules/**",
7550
+ "**/dist/**",
7551
+ "**/build/**",
7552
+ "**/CHANGELOG.md"
7553
+ ],
7554
+ deep: 6
7555
+ });
7556
+ for (const documentationPath of documentationFiles) try {
7557
+ const content = (0, node_fs.readFileSync)(documentationPath, "utf-8");
7558
+ for (const packageName of declaredNames) if (matchesPackageImportReference(content, packageName)) referenced.add(packageName);
7559
+ } catch {
7560
+ continue;
7561
+ }
5691
7562
  return referenced;
5692
7563
  };
5693
7564
  const PACKAGE_JSON_CONFIG_SECTIONS = [
@@ -5706,6 +7577,42 @@ const PACKAGE_JSON_CONFIG_SECTIONS = [
5706
7577
  "resolutions",
5707
7578
  "overrides"
5708
7579
  ];
7580
+ const collectOverrideMappingsFromPackageJson = (packageJsonPath) => {
7581
+ const mappings = [];
7582
+ try {
7583
+ const content = (0, node_fs.readFileSync)(packageJsonPath, "utf-8");
7584
+ const packageJson = JSON.parse(content);
7585
+ const overrideSections = [
7586
+ packageJson.overrides,
7587
+ packageJson.resolutions,
7588
+ packageJson.pnpm?.overrides
7589
+ ];
7590
+ for (const overrideSection of overrideSections) {
7591
+ if (!overrideSection || typeof overrideSection !== "object") continue;
7592
+ mappings.push(...collectOverrideMappingsFromRecord(overrideSection));
7593
+ }
7594
+ } catch {
7595
+ return mappings;
7596
+ }
7597
+ return mappings;
7598
+ };
7599
+ const collectOverrideMappings = (configSearchRoots, packageJsonPaths, monorepoRoot) => {
7600
+ const mappings = [];
7601
+ const seenMappings = /* @__PURE__ */ new Set();
7602
+ const addMappings = (nextMappings) => {
7603
+ for (const mapping of nextMappings) {
7604
+ const mappingKey = `${mapping.fromPackage}->${mapping.toPackage}`;
7605
+ if (seenMappings.has(mappingKey)) continue;
7606
+ seenMappings.add(mappingKey);
7607
+ mappings.push(mapping);
7608
+ }
7609
+ };
7610
+ for (const packageJsonPath of packageJsonPaths) addMappings(collectOverrideMappingsFromPackageJson(packageJsonPath));
7611
+ const workspaceRoots = new Set(configSearchRoots);
7612
+ if (monorepoRoot) workspaceRoots.add(monorepoRoot);
7613
+ for (const workspaceRoot of workspaceRoots) addMappings(collectPnpmWorkspaceOverrideMappings(workspaceRoot));
7614
+ return mappings;
7615
+ };
5709
7616
  const collectPackageJsonConfigReferences = (packageJsonPath, declaredNames) => {
5710
7617
  const referenced = /* @__PURE__ */ new Set();
5711
7618
  try {
@@ -5831,7 +7738,7 @@ const scanSourceFilesForPackageImports = (rootDir, candidatePackages) => {
5831
7738
  if (candidatePackages.size === 0) break;
5832
7739
  try {
5833
7740
  const content = (0, node_fs.readFileSync)(filePath, "utf-8");
5834
- for (const packageName of candidatePackages) if (content.includes(`'${packageName}'`) || content.includes(`"${packageName}"`) || content.includes(`'${packageName}/`) || content.includes(`"${packageName}/`)) {
7741
+ for (const packageName of candidatePackages) if (matchesPackageImportReference(content, packageName)) {
5835
7742
  found.add(packageName);
5836
7743
  candidatePackages.delete(packageName);
5837
7744
  }
@@ -5974,103 +7881,1468 @@ const findStronglyConnectedComponents = (adjacencyList) => {
5974
7881
  }
5975
7882
  }
5976
7883
  }
5977
- return components;
7884
+ return components;
7885
+ };
7886
+ const canonicalizeCycle = (cycle, graph) => {
7887
+ if (cycle.length === 0) return [];
7888
+ let minPosition = 0;
7889
+ let minPath = graph.modules[cycle[0]].fileId.path;
7890
+ for (let position = 1; position < cycle.length; position++) {
7891
+ const currentPath = graph.modules[cycle[position]].fileId.path;
7892
+ if (currentPath < minPath) {
7893
+ minPath = currentPath;
7894
+ minPosition = position;
7895
+ }
7896
+ }
7897
+ return [...cycle.slice(minPosition), ...cycle.slice(0, minPosition)];
7898
+ };
7899
+ const enumerateElementaryCycles = (componentNodes, adjacencyList, graph) => {
7900
+ if (componentNodes.length === 2) {
7901
+ const [nodeA, nodeB] = componentNodes;
7902
+ return [graph.modules[nodeA].fileId.path <= graph.modules[nodeB].fileId.path ? [nodeA, nodeB] : [nodeB, nodeA]];
7903
+ }
7904
+ const componentSet = new Set(componentNodes);
7905
+ const cycles = [];
7906
+ const seenKeys = /* @__PURE__ */ new Set();
7907
+ for (const startNode of componentNodes) {
7908
+ if (cycles.length >= 20) break;
7909
+ const visitedInThisSearch = /* @__PURE__ */ new Set();
7910
+ visitedInThisSearch.add(startNode);
7911
+ const pathStack = [startNode];
7912
+ const successorPositionStack = [0];
7913
+ while (pathStack.length > 0 && cycles.length < 20) {
7914
+ const currentNode = pathStack[pathStack.length - 1];
7915
+ const currentSuccessorPosition = successorPositionStack[successorPositionStack.length - 1];
7916
+ const successors = adjacencyList[currentNode].filter((successor) => componentSet.has(successor));
7917
+ if (currentSuccessorPosition < successors.length) {
7918
+ successorPositionStack[successorPositionStack.length - 1]++;
7919
+ const successor = successors[currentSuccessorPosition];
7920
+ if (successor === startNode) {
7921
+ const canonical = canonicalizeCycle([...pathStack], graph);
7922
+ const key = canonical.join(",");
7923
+ if (!seenKeys.has(key)) {
7924
+ seenKeys.add(key);
7925
+ cycles.push(canonical);
7926
+ }
7927
+ } else if (!visitedInThisSearch.has(successor)) {
7928
+ visitedInThisSearch.add(successor);
7929
+ pathStack.push(successor);
7930
+ successorPositionStack.push(0);
7931
+ }
7932
+ } else {
7933
+ visitedInThisSearch.delete(pathStack.pop());
7934
+ successorPositionStack.pop();
7935
+ }
7936
+ }
7937
+ }
7938
+ return cycles;
7939
+ };
7940
+ const detectCycles = (graph) => {
7941
+ const adjacencyList = buildAdjacencyList(graph);
7942
+ const components = findStronglyConnectedComponents(adjacencyList);
7943
+ const allCycles = [];
7944
+ const seenKeys = /* @__PURE__ */ new Set();
7945
+ const sortedComponents = [...components].sort((componentA, componentB) => componentA.length - componentB.length);
7946
+ for (const component of sortedComponents) {
7947
+ if (allCycles.length >= 200) break;
7948
+ if (component.length > 50) continue;
7949
+ const elementaryCycles = enumerateElementaryCycles(component, adjacencyList, graph);
7950
+ for (const cycle of elementaryCycles) {
7951
+ const key = cycle.join(",");
7952
+ if (!seenKeys.has(key)) {
7953
+ seenKeys.add(key);
7954
+ allCycles.push(cycle);
7955
+ }
7956
+ if (allCycles.length >= 200) break;
7957
+ }
7958
+ }
7959
+ allCycles.sort((cycleA, cycleB) => {
7960
+ const lengthDiff = cycleA.length - cycleB.length;
7961
+ if (lengthDiff !== 0) return lengthDiff;
7962
+ return graph.modules[cycleA[0]].fileId.path.localeCompare(graph.modules[cycleB[0]].fileId.path);
7963
+ });
7964
+ return allCycles.map((cycle) => ({ files: cycle.map((nodeIndex) => graph.modules[nodeIndex].fileId.path) }));
7965
+ };
7966
+
7967
+ //#endregion
7968
+ //#region src/report/redundancy.ts
7969
+ const isPlatformSpecificModulePath = (modulePath) => {
7970
+ const extensionIndex = modulePath.lastIndexOf(".");
7971
+ if (extensionIndex === -1) return false;
7972
+ const withoutExtension = modulePath.slice(0, extensionIndex);
7973
+ return PLATFORM_SUFFIXES.some((suffix) => withoutExtension.endsWith(suffix));
7974
+ };
7975
+ const platformStrippedBasePath = (modulePath) => {
7976
+ const extensionIndex = modulePath.lastIndexOf(".");
7977
+ if (extensionIndex === -1) return modulePath;
7978
+ const withoutExtension = modulePath.slice(0, extensionIndex);
7979
+ for (const suffix of PLATFORM_SUFFIXES) if (withoutExtension.endsWith(suffix)) return withoutExtension.slice(0, -suffix.length) + modulePath.slice(extensionIndex);
7980
+ return modulePath;
7981
+ };
7982
+ const buildPlatformSiblingGroupSizes = (graph) => {
7983
+ const baseToCount = /* @__PURE__ */ new Map();
7984
+ for (const module of graph.modules) {
7985
+ const base = platformStrippedBasePath(module.fileId.path);
7986
+ baseToCount.set(base, (baseToCount.get(base) ?? 0) + 1);
7987
+ }
7988
+ return baseToCount;
7989
+ };
7990
+ const detectUselessAliasedReExports = (graph) => {
7991
+ const findings = [];
7992
+ const moduleConsumerImportedNames = /* @__PURE__ */ new Map();
7993
+ const moduleConsumedWholesale = /* @__PURE__ */ new Set();
7994
+ const platformSiblingGroupSizes = buildPlatformSiblingGroupSizes(graph);
7995
+ for (const edge of graph.edges) {
7996
+ if (edge.isReExportEdge) {
7997
+ const reExportedSet = moduleConsumerImportedNames.get(edge.target);
7998
+ if (edge.reExportedNames.includes("*")) moduleConsumedWholesale.add(edge.target);
7999
+ if (reExportedSet) for (const reExportedName of edge.reExportedNames) reExportedSet.add(reExportedName);
8000
+ else moduleConsumerImportedNames.set(edge.target, new Set(edge.reExportedNames));
8001
+ continue;
8002
+ }
8003
+ if (edge.importedSymbols.length === 0) {
8004
+ moduleConsumedWholesale.add(edge.target);
8005
+ continue;
8006
+ }
8007
+ const importedSet = moduleConsumerImportedNames.get(edge.target);
8008
+ for (const symbol of edge.importedSymbols) {
8009
+ if (symbol.isNamespace || symbol.importedName === "*") {
8010
+ moduleConsumedWholesale.add(edge.target);
8011
+ continue;
8012
+ }
8013
+ const importedName = symbol.isDefault ? "default" : symbol.importedName;
8014
+ if (importedSet) importedSet.add(importedName);
8015
+ else moduleConsumerImportedNames.set(edge.target, new Set([importedName]));
8016
+ }
8017
+ }
8018
+ for (const module of graph.modules) {
8019
+ if (!module.isReachable) continue;
8020
+ if (module.isDeclarationFile) continue;
8021
+ if (moduleConsumedWholesale.has(module.fileId.index)) continue;
8022
+ if (isPlatformSpecificModulePath(module.fileId.path)) continue;
8023
+ const platformBase = platformStrippedBasePath(module.fileId.path);
8024
+ if ((platformSiblingGroupSizes.get(platformBase) ?? 0) > 1) continue;
8025
+ const consumerImportedNames = moduleConsumerImportedNames.get(module.fileId.index) ?? /* @__PURE__ */ new Set();
8026
+ for (const exportInfo of module.exports) {
8027
+ if (exportInfo.isSynthetic) continue;
8028
+ if (!exportInfo.isReExport) continue;
8029
+ if (!exportInfo.reExportOriginalName) continue;
8030
+ const exportedName = exportInfo.name;
8031
+ const originalName = exportInfo.reExportOriginalName;
8032
+ if (exportedName === originalName) continue;
8033
+ if (exportedName === "*") continue;
8034
+ if (originalName === "*") continue;
8035
+ if (originalName === "default") continue;
8036
+ if (exportInfo.isNamespaceReExport) continue;
8037
+ if (consumerImportedNames.has(exportedName)) continue;
8038
+ findings.push({
8039
+ path: module.fileId.path,
8040
+ kind: "reexport-aliased-not-used",
8041
+ name: exportedName,
8042
+ aliasedFrom: originalName,
8043
+ line: exportInfo.line,
8044
+ column: exportInfo.column,
8045
+ confidence: "medium",
8046
+ 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`
8047
+ });
8048
+ }
8049
+ }
8050
+ return findings;
8051
+ };
8052
+ const detectRedundantAliases = (graph) => {
8053
+ const findings = [];
8054
+ for (const module of graph.modules) {
8055
+ if (module.isDeclarationFile) continue;
8056
+ if (!module.isReachable) continue;
8057
+ for (const importInfo of module.imports) for (const binding of importInfo.importedNames) {
8058
+ if (!binding.isRedundantAlias) continue;
8059
+ findings.push({
8060
+ path: module.fileId.path,
8061
+ kind: "import-self-alias",
8062
+ name: binding.name,
8063
+ aliasedFrom: binding.name,
8064
+ line: importInfo.line,
8065
+ column: importInfo.column,
8066
+ confidence: "high",
8067
+ reason: `\`import { ${binding.name} as ${binding.name} }\` aliases an identifier to its own name`
8068
+ });
8069
+ }
8070
+ for (const exportInfo of module.exports) {
8071
+ if (exportInfo.isSynthetic) continue;
8072
+ if (!exportInfo.isRedundantAlias) continue;
8073
+ const kind = exportInfo.isReExport ? "reexport-self-alias" : "export-self-alias";
8074
+ const sourceSuffix = exportInfo.reExportSource ? ` from "${exportInfo.reExportSource}"` : "";
8075
+ findings.push({
8076
+ path: module.fileId.path,
8077
+ kind,
8078
+ name: exportInfo.name,
8079
+ aliasedFrom: exportInfo.name,
8080
+ line: exportInfo.line,
8081
+ column: exportInfo.column,
8082
+ confidence: "high",
8083
+ reason: `\`export { ${exportInfo.name} as ${exportInfo.name} }${sourceSuffix}\` aliases an identifier to its own name`
8084
+ });
8085
+ }
8086
+ }
8087
+ return findings;
8088
+ };
8089
+ const detectDuplicateExports = (graph) => {
8090
+ const findings = [];
8091
+ for (const module of graph.modules) {
8092
+ if (module.isDeclarationFile) continue;
8093
+ const nameToOccurrences = /* @__PURE__ */ new Map();
8094
+ const nameHasReExport = /* @__PURE__ */ new Map();
8095
+ for (const exportInfo of module.exports) {
8096
+ if (exportInfo.isSynthetic) continue;
8097
+ if (exportInfo.name === "*" && exportInfo.isNamespaceReExport) continue;
8098
+ const occurrence = {
8099
+ line: exportInfo.line,
8100
+ column: exportInfo.column,
8101
+ reExportSource: exportInfo.reExportSource,
8102
+ isReExport: exportInfo.isReExport
8103
+ };
8104
+ const existing = nameToOccurrences.get(exportInfo.name);
8105
+ if (existing) existing.push(occurrence);
8106
+ else nameToOccurrences.set(exportInfo.name, [occurrence]);
8107
+ if (exportInfo.isReExport) nameHasReExport.set(exportInfo.name, true);
8108
+ }
8109
+ for (const [name, occurrences] of nameToOccurrences) {
8110
+ if (occurrences.length < 2) continue;
8111
+ if (!nameHasReExport.get(name)) continue;
8112
+ findings.push({
8113
+ path: module.fileId.path,
8114
+ name,
8115
+ occurrences,
8116
+ confidence: "high",
8117
+ reason: `"${name}" is exported ${occurrences.length} times from the same module`
8118
+ });
8119
+ }
8120
+ }
8121
+ return findings;
8122
+ };
8123
+
8124
+ //#endregion
8125
+ //#region src/report/dry-patterns.ts
8126
+ const detectDuplicateImports = (graph) => {
8127
+ const findings = [];
8128
+ for (const module of graph.modules) {
8129
+ if (module.isDeclarationFile) continue;
8130
+ const specifierToOccurrences = /* @__PURE__ */ new Map();
8131
+ for (const importInfo of module.imports) {
8132
+ if (importInfo.isSideEffect) continue;
8133
+ if (importInfo.isDynamic) continue;
8134
+ if (importInfo.isGlob) continue;
8135
+ const occurrence = {
8136
+ line: importInfo.line,
8137
+ column: importInfo.column,
8138
+ importedNames: importInfo.importedNames.map((binding) => binding.isNamespace ? `* as ${binding.alias ?? ""}` : binding.alias ?? binding.name),
8139
+ isTypeOnly: importInfo.isTypeOnly
8140
+ };
8141
+ const existing = specifierToOccurrences.get(importInfo.specifier);
8142
+ if (existing) existing.push(occurrence);
8143
+ else specifierToOccurrences.set(importInfo.specifier, [occurrence]);
8144
+ }
8145
+ for (const [specifier, occurrences] of specifierToOccurrences) {
8146
+ if (occurrences.length < 2) continue;
8147
+ findings.push({
8148
+ path: module.fileId.path,
8149
+ specifier,
8150
+ occurrences,
8151
+ confidence: "high",
8152
+ reason: `"${specifier}" is imported ${occurrences.length} times in this file — merge into a single statement`
8153
+ });
8154
+ }
8155
+ }
8156
+ return findings;
8157
+ };
8158
+ const detectRedundantTypePatterns = (graph) => {
8159
+ const findings = [];
8160
+ for (const module of graph.modules) {
8161
+ if (module.isDeclarationFile) continue;
8162
+ for (const parsedPattern of module.redundantTypePatterns) findings.push({
8163
+ path: module.fileId.path,
8164
+ typeName: parsedPattern.typeName,
8165
+ kind: parsedPattern.kind,
8166
+ line: parsedPattern.line,
8167
+ column: parsedPattern.column,
8168
+ confidence: "high",
8169
+ reason: parsedPattern.reason,
8170
+ suggestion: parsedPattern.suggestion
8171
+ });
8172
+ }
8173
+ return findings;
8174
+ };
8175
+ const detectIdentityWrappers = (graph) => {
8176
+ const findings = [];
8177
+ for (const module of graph.modules) {
8178
+ if (module.isDeclarationFile) continue;
8179
+ for (const parsedWrapper of module.identityWrappers) findings.push({
8180
+ path: module.fileId.path,
8181
+ wrapperName: parsedWrapper.wrapperName,
8182
+ wrappedExpression: parsedWrapper.wrappedExpression,
8183
+ line: parsedWrapper.line,
8184
+ column: parsedWrapper.column,
8185
+ confidence: "high",
8186
+ reason: `\`${parsedWrapper.wrapperName}\` is a thin wrapper that forwards every argument to \`${parsedWrapper.wrappedExpression}\` unchanged`
8187
+ });
8188
+ }
8189
+ return findings;
8190
+ };
8191
+ const detectDuplicateTypeDefinitions = (graph) => {
8192
+ const hashToInstances = /* @__PURE__ */ new Map();
8193
+ for (const module of graph.modules) {
8194
+ if (module.isDeclarationFile) continue;
8195
+ for (const typeHash of module.typeDefinitionHashes) {
8196
+ const instance = {
8197
+ path: module.fileId.path,
8198
+ typeName: typeHash.typeName,
8199
+ line: typeHash.line,
8200
+ column: typeHash.column
8201
+ };
8202
+ const existing = hashToInstances.get(typeHash.structuralHash);
8203
+ if (existing) existing.push(instance);
8204
+ else hashToInstances.set(typeHash.structuralHash, [instance]);
8205
+ }
8206
+ }
8207
+ const findings = [];
8208
+ for (const [structuralHash, instances] of hashToInstances) {
8209
+ if (instances.length < 2) continue;
8210
+ const uniquePaths = new Set(instances.map((instance) => instance.path));
8211
+ if (uniquePaths.size < 2) continue;
8212
+ const uniqueNames = new Set(instances.map((instance) => instance.typeName));
8213
+ const isAllSameName = uniqueNames.size === 1;
8214
+ findings.push({
8215
+ structuralHash,
8216
+ instances,
8217
+ confidence: isAllSameName ? "high" : "medium",
8218
+ 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`
8219
+ });
8220
+ }
8221
+ return findings;
8222
+ };
8223
+ const detectDuplicateConstants = (graph) => {
8224
+ const hashToBuckets = /* @__PURE__ */ new Map();
8225
+ for (const module of graph.modules) {
8226
+ if (module.isDeclarationFile) continue;
8227
+ for (const candidate of module.duplicateConstantCandidates) {
8228
+ const occurrence = {
8229
+ path: module.fileId.path,
8230
+ constantName: candidate.constantName,
8231
+ line: candidate.line,
8232
+ column: candidate.column
8233
+ };
8234
+ const existing = hashToBuckets.get(candidate.literalHash);
8235
+ if (existing) existing.occurrences.push(occurrence);
8236
+ else hashToBuckets.set(candidate.literalHash, {
8237
+ literalPreview: candidate.literalPreview,
8238
+ occurrences: [occurrence]
8239
+ });
8240
+ }
8241
+ }
8242
+ const findings = [];
8243
+ for (const [literalHash, bucket] of hashToBuckets) {
8244
+ const uniqueFilePaths = new Set(bucket.occurrences.map((occurrence) => occurrence.path));
8245
+ if (uniqueFilePaths.size < 3) continue;
8246
+ const uniqueNames = new Set(bucket.occurrences.map((occurrence) => occurrence.constantName));
8247
+ findings.push({
8248
+ literalHash,
8249
+ literalPreview: bucket.literalPreview,
8250
+ occurrences: bucket.occurrences,
8251
+ confidence: uniqueNames.size === 1 ? "high" : "medium",
8252
+ 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`
8253
+ });
8254
+ }
8255
+ return findings;
8256
+ };
8257
+ const detectSimplifiableExpressions = (graph) => {
8258
+ const findings = [];
8259
+ for (const module of graph.modules) {
8260
+ if (module.isDeclarationFile) continue;
8261
+ for (const parsedExpression of module.simplifiableExpressions) findings.push({
8262
+ path: module.fileId.path,
8263
+ kind: parsedExpression.kind,
8264
+ snippet: parsedExpression.snippet,
8265
+ line: parsedExpression.line,
8266
+ column: parsedExpression.column,
8267
+ confidence: parsedExpression.kind === "double-bang-boolean" || parsedExpression.kind === "ternary-returns-boolean" || parsedExpression.kind === "redundant-null-and-undefined-check" ? "high" : "medium",
8268
+ reason: parsedExpression.reason,
8269
+ suggestion: parsedExpression.suggestion
8270
+ });
8271
+ }
8272
+ return findings;
8273
+ };
8274
+ const detectSimplifiableFunctions = (graph) => {
8275
+ const findings = [];
8276
+ for (const module of graph.modules) {
8277
+ if (module.isDeclarationFile) continue;
8278
+ for (const parsedFunction of module.simplifiableFunctions) findings.push({
8279
+ path: module.fileId.path,
8280
+ kind: parsedFunction.kind,
8281
+ functionName: parsedFunction.functionName,
8282
+ line: parsedFunction.line,
8283
+ column: parsedFunction.column,
8284
+ confidence: parsedFunction.kind === "useless-async-no-await" ? "low" : "high",
8285
+ reason: parsedFunction.reason,
8286
+ suggestion: parsedFunction.suggestion
8287
+ });
8288
+ }
8289
+ return findings;
8290
+ };
8291
+ const detectDuplicateInlineTypes = (graph) => {
8292
+ const hashToOccurrences = /* @__PURE__ */ new Map();
8293
+ for (const module of graph.modules) {
8294
+ if (module.isDeclarationFile) continue;
8295
+ for (const inlineLiteral of module.inlineTypeLiterals) {
8296
+ const occurrence = {
8297
+ path: module.fileId.path,
8298
+ line: inlineLiteral.line,
8299
+ column: inlineLiteral.column,
8300
+ context: inlineLiteral.context,
8301
+ nearestName: inlineLiteral.nearestName
8302
+ };
8303
+ const existing = hashToOccurrences.get(inlineLiteral.structuralHash);
8304
+ if (existing) existing.occurrences.push(occurrence);
8305
+ else hashToOccurrences.set(inlineLiteral.structuralHash, {
8306
+ memberCount: inlineLiteral.memberCount,
8307
+ preview: inlineLiteral.preview,
8308
+ occurrences: [occurrence]
8309
+ });
8310
+ }
8311
+ }
8312
+ const findings = [];
8313
+ for (const [structuralHash, group] of hashToOccurrences) {
8314
+ if (group.occurrences.length < 2) continue;
8315
+ if (new Set(group.occurrences.map((occurrence) => `${occurrence.path}:${occurrence.line}`)).size < 2) continue;
8316
+ const uniquePaths = new Set(group.occurrences.map((occurrence) => occurrence.path));
8317
+ const confidence = uniquePaths.size >= 2 || group.memberCount >= 5 ? "medium" : "low";
8318
+ findings.push({
8319
+ structuralHash,
8320
+ memberCount: group.memberCount,
8321
+ preview: group.preview,
8322
+ occurrences: group.occurrences,
8323
+ confidence,
8324
+ reason: `inline object shape ${group.preview} appears at ${group.occurrences.length} sites across ${uniquePaths.size} file(s) — extract a named type`
8325
+ });
8326
+ }
8327
+ return findings;
8328
+ };
8329
+
8330
+ //#endregion
8331
+ //#region src/utils/run-safe-detector.ts
8332
+ const runSafeDetector = (input) => {
8333
+ try {
8334
+ return input.detector();
8335
+ } catch (caughtError) {
8336
+ input.errorSink.push(new DetectorError({
8337
+ module: input.module,
8338
+ message: `${input.detectorName} threw ${input.contextDescription}`,
8339
+ detail: describeUnknownError(caughtError)
8340
+ }));
8341
+ return input.fallback;
8342
+ }
8343
+ };
8344
+
8345
+ //#endregion
8346
+ //#region src/semantic/program.ts
8347
+ const failureFor = (reason, message, options = { rootDir: "" }) => {
8348
+ return {
8349
+ reason,
8350
+ message,
8351
+ error: new TypeScriptError({
8352
+ code: {
8353
+ "no-tsconfig": "tsconfig-not-found",
8354
+ "tsconfig-parse-error": "tsconfig-parse-failed",
8355
+ "program-creation-failed": "ts-program-creation-failed",
8356
+ "too-many-files": "ts-program-too-large",
8357
+ "typescript-load-failed": "ts-not-loadable"
8358
+ }[reason],
8359
+ severity: reason === "no-tsconfig" ? "info" : "warning",
8360
+ message,
8361
+ path: options.rootDir || void 0,
8362
+ detail: options.detail
8363
+ })
8364
+ };
8365
+ };
8366
+ const findNearestTsconfig = (rootDir, explicitPath) => {
8367
+ if (explicitPath) {
8368
+ const absoluteExplicit = (0, node_path.resolve)(rootDir, explicitPath);
8369
+ if ((0, node_fs.existsSync)(absoluteExplicit)) return absoluteExplicit;
8370
+ return;
8371
+ }
8372
+ for (const candidateName of DEFAULT_SEMANTIC_TSCONFIG_NAMES) {
8373
+ const candidatePath = (0, node_path.resolve)(rootDir, candidateName);
8374
+ if ((0, node_fs.existsSync)(candidatePath)) return candidatePath;
8375
+ }
8376
+ };
8377
+ const createSemanticContext = (rootDir, tsconfigPath) => {
8378
+ const resolvedTsconfigPath = findNearestTsconfig(rootDir, tsconfigPath);
8379
+ if (!resolvedTsconfigPath) return {
8380
+ ok: false,
8381
+ failure: failureFor("no-tsconfig", `No tsconfig found under ${rootDir}`, { rootDir })
8382
+ };
8383
+ let configFileContent;
8384
+ try {
8385
+ configFileContent = typescript.default.readConfigFile(resolvedTsconfigPath, typescript.default.sys.readFile);
8386
+ } catch (readError) {
8387
+ return {
8388
+ ok: false,
8389
+ failure: failureFor("tsconfig-parse-error", "ts.readConfigFile threw", {
8390
+ rootDir: resolvedTsconfigPath,
8391
+ detail: describeUnknownError(readError)
8392
+ })
8393
+ };
8394
+ }
8395
+ if (configFileContent.error) return {
8396
+ ok: false,
8397
+ failure: failureFor("tsconfig-parse-error", typescript.default.flattenDiagnosticMessageText(configFileContent.error.messageText, "\n"), { rootDir: resolvedTsconfigPath })
8398
+ };
8399
+ let parsedCommandLine;
8400
+ try {
8401
+ parsedCommandLine = typescript.default.parseJsonConfigFileContent(configFileContent.config, typescript.default.sys, (0, node_path.dirname)(resolvedTsconfigPath), {
8402
+ noEmit: true,
8403
+ skipLibCheck: true,
8404
+ allowJs: true,
8405
+ isolatedModules: false
8406
+ }, resolvedTsconfigPath);
8407
+ } catch (parseError) {
8408
+ return {
8409
+ ok: false,
8410
+ failure: failureFor("tsconfig-parse-error", "ts.parseJsonConfigFileContent threw", {
8411
+ rootDir: resolvedTsconfigPath,
8412
+ detail: describeUnknownError(parseError)
8413
+ })
8414
+ };
8415
+ }
8416
+ if (parsedCommandLine.errors.length > 0) {
8417
+ const fatalErrors = parsedCommandLine.errors.filter((diagnostic) => diagnostic.category === typescript.default.DiagnosticCategory.Error);
8418
+ if (fatalErrors.length > 0 && parsedCommandLine.fileNames.length === 0) return {
8419
+ ok: false,
8420
+ failure: failureFor("tsconfig-parse-error", typescript.default.flattenDiagnosticMessageText(fatalErrors[0].messageText, "\n"), { rootDir: resolvedTsconfigPath })
8421
+ };
8422
+ }
8423
+ if (parsedCommandLine.fileNames.length > 5e3) return {
8424
+ ok: false,
8425
+ failure: failureFor("too-many-files", `Project has ${parsedCommandLine.fileNames.length} files, exceeds SEMANTIC_MAX_PROGRAM_FILES=${SEMANTIC_MAX_PROGRAM_FILES}`, { rootDir: resolvedTsconfigPath })
8426
+ };
8427
+ try {
8428
+ const program = typescript.default.createProgram({
8429
+ rootNames: parsedCommandLine.fileNames,
8430
+ options: parsedCommandLine.options,
8431
+ projectReferences: parsedCommandLine.projectReferences
8432
+ });
8433
+ return {
8434
+ ok: true,
8435
+ context: {
8436
+ program,
8437
+ checker: program.getTypeChecker(),
8438
+ rootSourceFiles: program.getSourceFiles().filter((sourceFile) => !sourceFile.isDeclarationFile || sourceFile.fileName.endsWith(".d.ts")),
8439
+ tsconfigPath: resolvedTsconfigPath
8440
+ }
8441
+ };
8442
+ } catch (programError) {
8443
+ return {
8444
+ ok: false,
8445
+ failure: failureFor("program-creation-failed", "ts.createProgram threw", {
8446
+ rootDir: resolvedTsconfigPath,
8447
+ detail: describeUnknownError(programError)
8448
+ })
8449
+ };
8450
+ }
8451
+ };
8452
+
8453
+ //#endregion
8454
+ //#region src/semantic/references.ts
8455
+ const canonicalKeyForSymbol = (symbol) => {
8456
+ return symbol.declarations?.[0] ?? symbol;
8457
+ };
8458
+ const isDeclarationNameIdentifier = (identifier) => {
8459
+ const parent = identifier.parent;
8460
+ if (!parent) return false;
8461
+ 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;
8462
+ if (typescript.default.isEnumMember(parent) && parent.name === identifier) return true;
8463
+ if (typescript.default.isPropertyDeclaration(parent) && parent.name === identifier) return true;
8464
+ if (typescript.default.isMethodDeclaration(parent) && parent.name === identifier) return true;
8465
+ if (typescript.default.isParameter(parent) && parent.name === identifier) return true;
8466
+ if (typescript.default.isBindingElement(parent) && parent.name === identifier) return true;
8467
+ return false;
8468
+ };
8469
+ const isExportSpecifierIdentifier = (identifier) => {
8470
+ const parent = identifier.parent;
8471
+ return Boolean(parent && typescript.default.isExportSpecifier(parent));
8472
+ };
8473
+ const isImportSpecifierIdentifier = (identifier) => {
8474
+ const parent = identifier.parent;
8475
+ if (!parent) return false;
8476
+ return typescript.default.isImportSpecifier(parent) || typescript.default.isImportClause(parent) || typescript.default.isNamespaceImport(parent);
8477
+ };
8478
+ const isInTypeContext = (identifier) => {
8479
+ let current = identifier.parent;
8480
+ let depth = 0;
8481
+ while (current && depth < 12) {
8482
+ 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;
8483
+ if (typescript.default.isExpressionStatement(current) || typescript.default.isBlock(current)) return false;
8484
+ current = current.parent;
8485
+ depth++;
8486
+ }
8487
+ return false;
8488
+ };
8489
+ const resolveSymbolForIdentifier = (identifier, checker) => {
8490
+ let symbol;
8491
+ try {
8492
+ symbol = checker.getSymbolAtLocation(identifier);
8493
+ } catch {
8494
+ return;
8495
+ }
8496
+ if (!symbol) return void 0;
8497
+ if (symbol.flags & typescript.default.SymbolFlags.Alias) try {
8498
+ return checker.getAliasedSymbol(symbol);
8499
+ } catch {
8500
+ return symbol;
8501
+ }
8502
+ return symbol;
8503
+ };
8504
+ const visitJsDocNodes = (node, visit) => {
8505
+ const jsDocContainer = node;
8506
+ if (!jsDocContainer.jsDoc) return;
8507
+ for (const jsDocNode of jsDocContainer.jsDoc) visit(jsDocNode);
8508
+ };
8509
+ const buildReferenceIndex = (program, checker) => {
8510
+ const keyedToReferences = /* @__PURE__ */ new Map();
8511
+ const recordIdentifier = (identifier, sourceFile) => {
8512
+ const resolvedSymbol = resolveSymbolForIdentifier(identifier, checker);
8513
+ if (!resolvedSymbol) return;
8514
+ const key = canonicalKeyForSymbol(resolvedSymbol);
8515
+ const site = {
8516
+ sourceFile,
8517
+ identifier,
8518
+ isDeclarationName: isDeclarationNameIdentifier(identifier),
8519
+ isExportSpecifier: isExportSpecifierIdentifier(identifier),
8520
+ isImportSpecifier: isImportSpecifierIdentifier(identifier),
8521
+ isTypeContext: isInTypeContext(identifier)
8522
+ };
8523
+ const existing = keyedToReferences.get(key);
8524
+ if (existing) existing.push(site);
8525
+ else keyedToReferences.set(key, [site]);
8526
+ };
8527
+ const visitNode = (node, sourceFile, recursionDepth) => {
8528
+ if (recursionDepth > 200) return;
8529
+ if (typescript.default.isIdentifier(node)) recordIdentifier(node, sourceFile);
8530
+ visitJsDocNodes(node, (jsDocNode) => visitNode(jsDocNode, sourceFile, recursionDepth + 1));
8531
+ node.forEachChild((child) => visitNode(child, sourceFile, recursionDepth + 1));
8532
+ };
8533
+ for (const sourceFile of program.getSourceFiles()) {
8534
+ if (sourceFile.isDeclarationFile) continue;
8535
+ visitNode(sourceFile, sourceFile, 0);
8536
+ }
8537
+ return {
8538
+ getReferences: (symbol) => keyedToReferences.get(canonicalKeyForSymbol(symbol)) ?? [],
8539
+ size: keyedToReferences.size
8540
+ };
8541
+ };
8542
+
8543
+ //#endregion
8544
+ //#region src/semantic/utils/source-file-lookup.ts
8545
+ const normalizeSourcePath = node_path.resolve;
8546
+ const buildSourceFileLookup = (program) => {
8547
+ const lookup = /* @__PURE__ */ new Map();
8548
+ for (const sourceFile of program.getSourceFiles()) {
8549
+ if (sourceFile.isDeclarationFile) continue;
8550
+ lookup.set(normalizeSourcePath(sourceFile.fileName), sourceFile);
8551
+ }
8552
+ return lookup;
8553
+ };
8554
+
8555
+ //#endregion
8556
+ //#region src/semantic/unused-types.ts
8557
+ const TYPE_DECLARATION_FLAGS = typescript.default.SymbolFlags.Interface | typescript.default.SymbolFlags.TypeAlias | typescript.default.SymbolFlags.Enum | typescript.default.SymbolFlags.ConstEnum | typescript.default.SymbolFlags.RegularEnum;
8558
+ const VALUE_DECLARATION_FLAGS = typescript.default.SymbolFlags.Variable | typescript.default.SymbolFlags.Function | typescript.default.SymbolFlags.Class | typescript.default.SymbolFlags.BlockScopedVariable | typescript.default.SymbolFlags.FunctionScopedVariable;
8559
+ const collectTypeExportCandidates = (graph, config) => {
8560
+ const candidates = [];
8561
+ for (const module of graph.modules) {
8562
+ if (!module.isReachable) continue;
8563
+ if (module.isDeclarationFile) continue;
8564
+ if (module.isEntryPoint && !config.includeEntryExports) continue;
8565
+ for (const exportInfo of module.exports) {
8566
+ if (exportInfo.isSynthetic) continue;
8567
+ if (!exportInfo.isTypeOnly) continue;
8568
+ if (exportInfo.isReExport) continue;
8569
+ if (exportInfo.name === "*") continue;
8570
+ candidates.push({
8571
+ module,
8572
+ exportName: exportInfo.name,
8573
+ line: exportInfo.line,
8574
+ column: exportInfo.column
8575
+ });
8576
+ }
8577
+ }
8578
+ return candidates;
8579
+ };
8580
+ const resolveExportSymbol = (sourceFile, exportName, checker) => {
8581
+ const moduleSymbol = checker.getSymbolAtLocation(sourceFile);
8582
+ if (!moduleSymbol) return void 0;
8583
+ const matchingExport = checker.getExportsOfModule(moduleSymbol).find((exportSymbol) => exportSymbol.name === exportName);
8584
+ if (!matchingExport) return void 0;
8585
+ if (matchingExport.flags & typescript.default.SymbolFlags.Alias) try {
8586
+ return checker.getAliasedSymbol(matchingExport);
8587
+ } catch {
8588
+ return matchingExport;
8589
+ }
8590
+ return matchingExport;
8591
+ };
8592
+ const isPureTypeSymbol = (symbol) => {
8593
+ const hasTypeFlags = (symbol.flags & TYPE_DECLARATION_FLAGS) !== 0;
8594
+ const hasValueFlags = (symbol.flags & VALUE_DECLARATION_FLAGS) !== 0;
8595
+ return hasTypeFlags && !hasValueFlags;
8596
+ };
8597
+ const classifyTypeKind = (symbol) => {
8598
+ if (symbol.flags & typescript.default.SymbolFlags.Interface) return "interface";
8599
+ if (symbol.flags & typescript.default.SymbolFlags.TypeAlias) return "type-alias";
8600
+ if (symbol.flags & (typescript.default.SymbolFlags.Enum | typescript.default.SymbolFlags.ConstEnum | typescript.default.SymbolFlags.RegularEnum)) return "enum-type";
8601
+ };
8602
+ const isReferenceMeaningful = (site) => {
8603
+ if (site.isDeclarationName) return false;
8604
+ return true;
8605
+ };
8606
+ const buildTrace = (candidate, meaningfulReferenceCount, totalReferenceCount, reExportSiteCount) => {
8607
+ return [
8608
+ `${candidate.module.fileId.path}:${candidate.line}:${candidate.column} declares "${candidate.exportName}"`,
8609
+ `total identifier references resolved to symbol: ${totalReferenceCount}`,
8610
+ `references excluding declaration site: ${meaningfulReferenceCount}`,
8611
+ `re-export specifier sites: ${reExportSiteCount}`
8612
+ ].slice(0, 5);
8613
+ };
8614
+ const detectUnusedTypes = (graph, config, context, referenceIndex) => {
8615
+ const findings = [];
8616
+ const candidates = collectTypeExportCandidates(graph, config);
8617
+ if (candidates.length === 0) return findings;
8618
+ const sourceFileLookup = buildSourceFileLookup(context.program);
8619
+ for (const candidate of candidates) {
8620
+ const sourceFile = sourceFileLookup.get(normalizeSourcePath(candidate.module.fileId.path));
8621
+ if (!sourceFile) continue;
8622
+ const exportSymbol = resolveExportSymbol(sourceFile, candidate.exportName, context.checker);
8623
+ if (!exportSymbol) continue;
8624
+ if (!isPureTypeSymbol(exportSymbol)) continue;
8625
+ const kind = classifyTypeKind(exportSymbol);
8626
+ if (!kind) continue;
8627
+ const allReferences = referenceIndex.getReferences(exportSymbol);
8628
+ const reExportSites = allReferences.filter((site) => site.isExportSpecifier);
8629
+ const meaningfulReferences = allReferences.filter(isReferenceMeaningful);
8630
+ if (meaningfulReferences.filter((site) => !site.isExportSpecifier).length > 0) continue;
8631
+ const declarations = exportSymbol.declarations ?? [];
8632
+ if (declarations.length > 1) {
8633
+ const declarationFiles = new Set(declarations.map((decl) => normalizeSourcePath(decl.getSourceFile().fileName)));
8634
+ if (declarationFiles.size > 1) {
8635
+ if (meaningfulReferences.some((site) => {
8636
+ const referenceFileName = normalizeSourcePath(site.sourceFile.fileName);
8637
+ return !declarationFiles.has(referenceFileName);
8638
+ })) continue;
8639
+ }
8640
+ }
8641
+ findings.push({
8642
+ path: candidate.module.fileId.path,
8643
+ name: candidate.exportName,
8644
+ line: candidate.line,
8645
+ column: candidate.column,
8646
+ kind,
8647
+ confidence: reExportSites.length > 0 ? "medium" : "high",
8648
+ 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`,
8649
+ trace: buildTrace(candidate, meaningfulReferences.length, allReferences.length, reExportSites.length)
8650
+ });
8651
+ }
8652
+ return findings;
8653
+ };
8654
+
8655
+ //#endregion
8656
+ //#region src/semantic/unused-enum-members.ts
8657
+ const collectEnumDeclarations = (graph, config, sourceFileLookup) => {
8658
+ const declarations = [];
8659
+ const visitTopLevel = (sourceFile, modulePath) => {
8660
+ for (const statement of sourceFile.statements) if (typescript.default.isEnumDeclaration(statement)) declarations.push({
8661
+ sourceFile,
8662
+ declaration: statement,
8663
+ modulePath
8664
+ });
8665
+ };
8666
+ for (const module of graph.modules) {
8667
+ if (!module.isReachable) continue;
8668
+ if (module.isDeclarationFile) continue;
8669
+ if (module.isEntryPoint && !config.includeEntryExports) continue;
8670
+ const sourceFile = sourceFileLookup.get(normalizeSourcePath(module.fileId.path));
8671
+ if (!sourceFile) continue;
8672
+ visitTopLevel(sourceFile, module.fileId.path);
8673
+ }
8674
+ return declarations;
8675
+ };
8676
+ const isStringLiteralEnum = (declaration) => {
8677
+ if (declaration.members.length === 0) return false;
8678
+ for (const member of declaration.members) {
8679
+ if (!member.initializer) return false;
8680
+ if (!typescript.default.isStringLiteral(member.initializer)) return false;
8681
+ }
8682
+ return true;
8683
+ };
8684
+ const isConstEnum = (declaration) => {
8685
+ const modifiers = typescript.default.canHaveModifiers(declaration) ? typescript.default.getModifiers(declaration) : void 0;
8686
+ if (!modifiers) return false;
8687
+ return modifiers.some((modifier) => modifier.kind === typescript.default.SyntaxKind.ConstKeyword);
8688
+ };
8689
+ const enumHasComputedAccess = (enumSymbol, referenceIndex) => {
8690
+ const references = referenceIndex.getReferences(enumSymbol);
8691
+ for (const referenceSite of references) {
8692
+ const parent = referenceSite.identifier.parent;
8693
+ if (!parent) continue;
8694
+ if (typescript.default.isElementAccessExpression(parent) && parent.expression === referenceSite.identifier) return true;
8695
+ }
8696
+ return false;
8697
+ };
8698
+ const enumHasWholeObjectUse = (enumSymbol, referenceIndex) => {
8699
+ const references = referenceIndex.getReferences(enumSymbol);
8700
+ for (const referenceSite of references) {
8701
+ if (referenceSite.isDeclarationName) continue;
8702
+ if (referenceSite.isExportSpecifier) continue;
8703
+ if (referenceSite.isImportSpecifier) continue;
8704
+ const parent = referenceSite.identifier.parent;
8705
+ if (!parent) continue;
8706
+ if (typescript.default.isPropertyAccessExpression(parent) && parent.expression === referenceSite.identifier) continue;
8707
+ if (typescript.default.isQualifiedName(parent) && parent.left === referenceSite.identifier) continue;
8708
+ if (typescript.default.isElementAccessExpression(parent) && parent.expression === referenceSite.identifier) continue;
8709
+ if (typescript.default.isTypeReferenceNode(parent)) continue;
8710
+ if (typescript.default.isTypeQueryNode(parent)) continue;
8711
+ return true;
8712
+ }
8713
+ return false;
8714
+ };
8715
+ const memberHasExternalReference$1 = (memberSymbol, referenceIndex) => {
8716
+ const references = referenceIndex.getReferences(memberSymbol);
8717
+ for (const referenceSite of references) {
8718
+ if (referenceSite.isDeclarationName) continue;
8719
+ return true;
8720
+ }
8721
+ return false;
8722
+ };
8723
+ const buildEnumMemberTrace = (enumName, memberName, declarationPath, line, column, hasComputedAccess, hasWholeObjectUse) => {
8724
+ const trace = [`${declarationPath}:${line}:${column} declares ${enumName}.${memberName}`, `no static \`${enumName}.${memberName}\` reference found in the project`];
8725
+ if (hasComputedAccess) trace.push(`${enumName}[...] computed access observed — confidence downgraded`);
8726
+ if (hasWholeObjectUse) trace.push(`${enumName} used as a whole value — confidence downgraded`);
8727
+ return trace.slice(0, 5);
8728
+ };
8729
+ const detectUnusedEnumMembers = (graph, config, context, referenceIndex) => {
8730
+ const findings = [];
8731
+ const enumDeclarations = collectEnumDeclarations(graph, config, buildSourceFileLookup(context.program));
8732
+ if (enumDeclarations.length === 0) return findings;
8733
+ const { checker } = context;
8734
+ for (const { sourceFile, declaration, modulePath } of enumDeclarations) {
8735
+ const enumSymbol = checker.getSymbolAtLocation(declaration.name);
8736
+ if (!enumSymbol) continue;
8737
+ const hasComputedAccess = enumHasComputedAccess(enumSymbol, referenceIndex);
8738
+ const hasWholeObjectUse = enumHasWholeObjectUse(enumSymbol, referenceIndex);
8739
+ const isPureStringEnum = isStringLiteralEnum(declaration);
8740
+ const isConst = isConstEnum(declaration);
8741
+ if (hasWholeObjectUse) continue;
8742
+ if (hasComputedAccess) continue;
8743
+ let confidence;
8744
+ if (isConst) confidence = "low";
8745
+ else if (isPureStringEnum) confidence = "high";
8746
+ else confidence = "medium";
8747
+ for (const member of declaration.members) {
8748
+ const memberSymbol = checker.getSymbolAtLocation(member.name);
8749
+ if (!memberSymbol) continue;
8750
+ if (memberHasExternalReference$1(memberSymbol, referenceIndex)) continue;
8751
+ const memberName = member.name.getText(sourceFile);
8752
+ const { line: zeroIndexedLine, character: zeroIndexedColumn } = sourceFile.getLineAndCharacterOfPosition(member.getStart(sourceFile));
8753
+ const line = zeroIndexedLine + 1;
8754
+ const column = zeroIndexedColumn + 1;
8755
+ findings.push({
8756
+ path: modulePath,
8757
+ enumName: declaration.name.text,
8758
+ memberName,
8759
+ line,
8760
+ column,
8761
+ confidence,
8762
+ reason: `${declaration.name.text}.${memberName} is declared but never referenced`,
8763
+ trace: buildEnumMemberTrace(declaration.name.text, memberName, modulePath, line, column, false, false)
8764
+ });
8765
+ }
8766
+ }
8767
+ return findings;
8768
+ };
8769
+
8770
+ //#endregion
8771
+ //#region src/semantic/unused-class-members.ts
8772
+ const isClassExported = (declaration) => {
8773
+ const modifiers = typescript.default.canHaveModifiers(declaration) ? typescript.default.getModifiers(declaration) : void 0;
8774
+ if (!modifiers) return false;
8775
+ return modifiers.some((modifier) => modifier.kind === typescript.default.SyntaxKind.ExportKeyword || modifier.kind === typescript.default.SyntaxKind.DefaultKeyword);
8776
+ };
8777
+ const collectClassDeclarations = (graph, config, sourceFileLookup) => {
8778
+ const contexts = [];
8779
+ for (const module of graph.modules) {
8780
+ if (!module.isReachable) continue;
8781
+ if (module.isDeclarationFile) continue;
8782
+ if (module.isEntryPoint && !config.includeEntryExports) continue;
8783
+ const sourceFile = sourceFileLookup.get(normalizeSourcePath(module.fileId.path));
8784
+ if (!sourceFile) continue;
8785
+ for (const statement of sourceFile.statements) {
8786
+ if (!typescript.default.isClassDeclaration(statement)) continue;
8787
+ if (!statement.name) continue;
8788
+ contexts.push({
8789
+ sourceFile,
8790
+ declaration: statement,
8791
+ modulePath: module.fileId.path,
8792
+ isExported: isClassExported(statement)
8793
+ });
8794
+ }
8795
+ }
8796
+ return contexts;
8797
+ };
8798
+ const buildSubclassMemberIndex = (classContexts, checker) => {
8799
+ const parentToOverriddenMemberNames = /* @__PURE__ */ new Map();
8800
+ const addOverrideNames = (parentSymbol, memberNames) => {
8801
+ const existing = parentToOverriddenMemberNames.get(parentSymbol);
8802
+ if (existing) for (const memberName of memberNames) existing.add(memberName);
8803
+ else parentToOverriddenMemberNames.set(parentSymbol, new Set(memberNames));
8804
+ };
8805
+ const collectMemberNames = (declaration) => {
8806
+ const names = [];
8807
+ for (const member of declaration.members) {
8808
+ if (!member.name || !typescript.default.isIdentifier(member.name)) continue;
8809
+ names.push(member.name.text);
8810
+ }
8811
+ return names;
8812
+ };
8813
+ for (const { declaration } of classContexts) {
8814
+ if (!declaration.heritageClauses) continue;
8815
+ for (const heritageClause of declaration.heritageClauses) {
8816
+ if (heritageClause.token !== typescript.default.SyntaxKind.ExtendsKeyword) continue;
8817
+ for (const heritageType of heritageClause.types) {
8818
+ const baseSymbol = checker.getSymbolAtLocation(heritageType.expression);
8819
+ if (!baseSymbol) continue;
8820
+ const resolvedBaseSymbol = baseSymbol.flags & typescript.default.SymbolFlags.Alias ? safeGetAliasedSymbol$1(baseSymbol, checker) : baseSymbol;
8821
+ if (!resolvedBaseSymbol) continue;
8822
+ addOverrideNames(resolvedBaseSymbol, collectMemberNames(declaration));
8823
+ }
8824
+ }
8825
+ }
8826
+ return { getOverridingMemberNames: (parentClassSymbol) => parentToOverriddenMemberNames.get(parentClassSymbol) ?? /* @__PURE__ */ new Set() };
5978
8827
  };
5979
- const canonicalizeCycle = (cycle, graph) => {
5980
- if (cycle.length === 0) return [];
5981
- let minPosition = 0;
5982
- let minPath = graph.modules[cycle[0]].fileId.path;
5983
- for (let position = 1; position < cycle.length; position++) {
5984
- const currentPath = graph.modules[cycle[position]].fileId.path;
5985
- if (currentPath < minPath) {
5986
- minPath = currentPath;
5987
- minPosition = position;
8828
+ const safeGetAliasedSymbol$1 = (symbol, checker) => {
8829
+ try {
8830
+ return checker.getAliasedSymbol(symbol);
8831
+ } catch {
8832
+ return;
8833
+ }
8834
+ };
8835
+ const isPrivateMember = (member) => {
8836
+ if (typescript.default.isPrivateIdentifier(member.name)) return true;
8837
+ const modifiers = typescript.default.canHaveModifiers(member) ? typescript.default.getModifiers(member) : void 0;
8838
+ if (!modifiers) return false;
8839
+ return modifiers.some((modifier) => modifier.kind === typescript.default.SyntaxKind.PrivateKeyword);
8840
+ };
8841
+ const isStaticMember = (member) => {
8842
+ const modifiers = typescript.default.canHaveModifiers(member) ? typescript.default.getModifiers(member) : void 0;
8843
+ if (!modifiers) return false;
8844
+ return modifiers.some((modifier) => modifier.kind === typescript.default.SyntaxKind.StaticKeyword);
8845
+ };
8846
+ const hasAllowedDecorator = (member, decoratorAllowlist) => {
8847
+ const decorators = typescript.default.canHaveDecorators(member) ? typescript.default.getDecorators(member) : void 0;
8848
+ if (!decorators || decorators.length === 0) return false;
8849
+ for (const decorator of decorators) {
8850
+ const expression = decorator.expression;
8851
+ let decoratorName;
8852
+ if (typescript.default.isIdentifier(expression)) decoratorName = expression.text;
8853
+ else if (typescript.default.isCallExpression(expression) && typescript.default.isIdentifier(expression.expression)) decoratorName = expression.expression.text;
8854
+ else if (typescript.default.isPropertyAccessExpression(expression) && typescript.default.isIdentifier(expression.name)) decoratorName = expression.name.text;
8855
+ if (decoratorName && decoratorAllowlist.has(decoratorName)) return true;
8856
+ }
8857
+ return false;
8858
+ };
8859
+ const classifyMemberKind = (member) => {
8860
+ if (typescript.default.isMethodDeclaration(member)) return "method";
8861
+ if (typescript.default.isPropertyDeclaration(member)) return "property";
8862
+ if (typescript.default.isGetAccessorDeclaration(member) || typescript.default.isSetAccessorDeclaration(member)) return "accessor";
8863
+ };
8864
+ const memberHasExternalReference = (memberSymbol, referenceIndex) => {
8865
+ const references = referenceIndex.getReferences(memberSymbol);
8866
+ for (const referenceSite of references) {
8867
+ if (referenceSite.isDeclarationName) continue;
8868
+ return true;
8869
+ }
8870
+ return false;
8871
+ };
8872
+ const buildClassMemberTrace = (className, memberName, modulePath, line, column, isOverriddenInSubclass, isExportedClass) => {
8873
+ const trace = [`${modulePath}:${line}:${column} declares ${className}.${memberName}`, `no \`${className}.${memberName}\` reference found outside the declaration`];
8874
+ if (isExportedClass) trace.push(`${className} is exported — confidence reduced for public-API safety`);
8875
+ if (isOverriddenInSubclass) trace.push(`subclass override observed — polymorphic call path possible`);
8876
+ return trace.slice(0, 5);
8877
+ };
8878
+ const detectUnusedClassMembers = (graph, config, context, referenceIndex, decoratorAllowlist) => {
8879
+ const findings = [];
8880
+ const classContexts = collectClassDeclarations(graph, config, buildSourceFileLookup(context.program));
8881
+ if (classContexts.length === 0) return findings;
8882
+ const { checker } = context;
8883
+ const decoratorAllowSet = new Set(decoratorAllowlist);
8884
+ const subclassMemberIndex = buildSubclassMemberIndex(classContexts, checker);
8885
+ for (const { sourceFile, declaration, modulePath, isExported } of classContexts) {
8886
+ const classSymbol = checker.getSymbolAtLocation(declaration.name);
8887
+ if (!classSymbol) continue;
8888
+ const overriddenMemberNames = subclassMemberIndex.getOverridingMemberNames(classSymbol);
8889
+ for (const member of declaration.members) {
8890
+ if (typescript.default.isConstructorDeclaration(member)) continue;
8891
+ if (!member.name) continue;
8892
+ const memberKind = classifyMemberKind(member);
8893
+ if (!memberKind) continue;
8894
+ if (isPrivateMember(member)) continue;
8895
+ if (hasAllowedDecorator(member, decoratorAllowSet)) continue;
8896
+ const memberSymbol = checker.getSymbolAtLocation(member.name);
8897
+ if (!memberSymbol) continue;
8898
+ if (memberHasExternalReference(memberSymbol, referenceIndex)) continue;
8899
+ const memberName = typescript.default.isIdentifier(member.name) ? member.name.text : member.name.getText(sourceFile);
8900
+ if (overriddenMemberNames.has(memberName)) continue;
8901
+ const { line: zeroIndexedLine, character: zeroIndexedColumn } = sourceFile.getLineAndCharacterOfPosition(member.getStart(sourceFile));
8902
+ const line = zeroIndexedLine + 1;
8903
+ const column = zeroIndexedColumn + 1;
8904
+ const confidence = isExported ? "low" : "high";
8905
+ findings.push({
8906
+ path: modulePath,
8907
+ className: declaration.name.text,
8908
+ memberName,
8909
+ memberKind,
8910
+ isStatic: isStaticMember(member),
8911
+ line,
8912
+ column,
8913
+ confidence,
8914
+ 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`,
8915
+ trace: buildClassMemberTrace(declaration.name.text, memberName, modulePath, line, column, false, isExported)
8916
+ });
5988
8917
  }
5989
8918
  }
5990
- return [...cycle.slice(minPosition), ...cycle.slice(0, minPosition)];
8919
+ return findings;
5991
8920
  };
5992
- const enumerateElementaryCycles = (componentNodes, adjacencyList, graph) => {
5993
- if (componentNodes.length === 2) {
5994
- const [nodeA, nodeB] = componentNodes;
5995
- return [graph.modules[nodeA].fileId.path <= graph.modules[nodeB].fileId.path ? [nodeA, nodeB] : [nodeB, nodeA]];
8921
+
8922
+ //#endregion
8923
+ //#region src/semantic/misclassified-dependencies.ts
8924
+ const TYPES_PACKAGE_PREFIX = "@types/";
8925
+ const recordImportSite = (summary, sitePath) => {
8926
+ if (summary.importSites.length >= 5) return;
8927
+ if (summary.importSites.includes(sitePath)) return;
8928
+ summary.importSites.push(sitePath);
8929
+ };
8930
+ const isImportEffectivelyTypeOnly = (isTypeOnlyDeclaration, importedBindings) => {
8931
+ if (isTypeOnlyDeclaration) return true;
8932
+ if (importedBindings.length === 0) return false;
8933
+ return importedBindings.every((binding) => binding.isTypeOnly);
8934
+ };
8935
+ const collectPackageUsageSummaries = (graph) => {
8936
+ const summaries = /* @__PURE__ */ new Map();
8937
+ const upsertSummary = (packageName) => {
8938
+ const existing = summaries.get(packageName);
8939
+ if (existing) return existing;
8940
+ const fresh = {
8941
+ packageName,
8942
+ hasValueUse: false,
8943
+ hasTypeOnlyUse: false,
8944
+ importSites: []
8945
+ };
8946
+ summaries.set(packageName, fresh);
8947
+ return fresh;
8948
+ };
8949
+ for (const module of graph.modules) {
8950
+ for (const importInfo of module.imports) {
8951
+ const packageName = extractPackageName(importInfo.specifier);
8952
+ if (!packageName) continue;
8953
+ const summary = upsertSummary(packageName);
8954
+ const sitePath = `${module.fileId.path}:${importInfo.line}`;
8955
+ if (importInfo.isSideEffect) {
8956
+ summary.hasValueUse = true;
8957
+ recordImportSite(summary, sitePath);
8958
+ continue;
8959
+ }
8960
+ if (importInfo.isDynamic) {
8961
+ summary.hasValueUse = true;
8962
+ recordImportSite(summary, sitePath);
8963
+ continue;
8964
+ }
8965
+ if (isImportEffectivelyTypeOnly(importInfo.isTypeOnly, importInfo.importedNames)) summary.hasTypeOnlyUse = true;
8966
+ else summary.hasValueUse = true;
8967
+ recordImportSite(summary, sitePath);
8968
+ }
8969
+ for (const exportInfo of module.exports) {
8970
+ if (!exportInfo.isReExport || !exportInfo.reExportSource) continue;
8971
+ const packageName = extractPackageName(exportInfo.reExportSource);
8972
+ if (!packageName) continue;
8973
+ const summary = upsertSummary(packageName);
8974
+ const sitePath = `${module.fileId.path}:${exportInfo.line}`;
8975
+ if (exportInfo.isTypeOnly) summary.hasTypeOnlyUse = true;
8976
+ else summary.hasValueUse = true;
8977
+ recordImportSite(summary, sitePath);
8978
+ }
5996
8979
  }
5997
- const componentSet = new Set(componentNodes);
5998
- const cycles = [];
5999
- const seenKeys = /* @__PURE__ */ new Set();
6000
- for (const startNode of componentNodes) {
6001
- if (cycles.length >= 20) break;
6002
- const visitedInThisSearch = /* @__PURE__ */ new Set();
6003
- visitedInThisSearch.add(startNode);
6004
- const pathStack = [startNode];
6005
- const successorPositionStack = [0];
6006
- while (pathStack.length > 0 && cycles.length < 20) {
6007
- const currentNode = pathStack[pathStack.length - 1];
6008
- const currentSuccessorPosition = successorPositionStack[successorPositionStack.length - 1];
6009
- const successors = adjacencyList[currentNode].filter((successor) => componentSet.has(successor));
6010
- if (currentSuccessorPosition < successors.length) {
6011
- successorPositionStack[successorPositionStack.length - 1]++;
6012
- const successor = successors[currentSuccessorPosition];
6013
- if (successor === startNode) {
6014
- const canonical = canonicalizeCycle([...pathStack], graph);
6015
- const key = canonical.join(",");
6016
- if (!seenKeys.has(key)) {
6017
- seenKeys.add(key);
6018
- cycles.push(canonical);
6019
- }
6020
- } else if (!visitedInThisSearch.has(successor)) {
6021
- visitedInThisSearch.add(successor);
6022
- pathStack.push(successor);
6023
- successorPositionStack.push(0);
6024
- }
6025
- } else {
6026
- visitedInThisSearch.delete(pathStack.pop());
6027
- successorPositionStack.pop();
8980
+ return summaries;
8981
+ };
8982
+ const readDeclaredDependencies = (rootDir) => {
8983
+ const packageJsonPath = (0, node_path.resolve)(rootDir, "package.json");
8984
+ let packageJson;
8985
+ try {
8986
+ const contents = (0, node_fs.readFileSync)(packageJsonPath, "utf-8");
8987
+ packageJson = JSON.parse(contents);
8988
+ } catch {
8989
+ return [];
8990
+ }
8991
+ const entries = [];
8992
+ for (const name of Object.keys(packageJson.dependencies ?? {})) entries.push({
8993
+ name,
8994
+ declaredAs: "dependencies"
8995
+ });
8996
+ return entries;
8997
+ };
8998
+ const detectMisclassifiedDependencies = (graph, config) => {
8999
+ const declaredEntries = readDeclaredDependencies(config.rootDir);
9000
+ if (declaredEntries.length === 0) return [];
9001
+ const packageUsage = collectPackageUsageSummaries(graph);
9002
+ const findings = [];
9003
+ for (const declaredEntry of declaredEntries) {
9004
+ const usage = packageUsage.get(declaredEntry.name);
9005
+ if (!usage) continue;
9006
+ if (usage.hasValueUse) continue;
9007
+ if (!usage.hasTypeOnlyUse) continue;
9008
+ const isTypesPackage = declaredEntry.name.startsWith(TYPES_PACKAGE_PREFIX);
9009
+ findings.push({
9010
+ name: declaredEntry.name,
9011
+ declaredAs: declaredEntry.declaredAs,
9012
+ suggestedAs: "devDependencies",
9013
+ confidence: isTypesPackage ? "high" : "medium",
9014
+ 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)`,
9015
+ trace: usage.importSites
9016
+ });
9017
+ }
9018
+ return findings;
9019
+ };
9020
+
9021
+ //#endregion
9022
+ //#region src/semantic/variable-aliases.ts
9023
+ const isSimpleIdentifierInitializer = (initializer) => Boolean(initializer && typescript.default.isIdentifier(initializer));
9024
+ const isModuleLevelDeclaration = (declaration) => {
9025
+ const variableDeclarationList = declaration.parent;
9026
+ if (!variableDeclarationList || !typescript.default.isVariableDeclarationList(variableDeclarationList)) return false;
9027
+ const statement = variableDeclarationList.parent;
9028
+ return Boolean(statement && typescript.default.isSourceFile(statement.parent));
9029
+ };
9030
+ const isDeclarationExported = (declaration) => {
9031
+ const variableDeclarationList = declaration.parent;
9032
+ if (!variableDeclarationList || !typescript.default.isVariableDeclarationList(variableDeclarationList)) return false;
9033
+ const statement = variableDeclarationList.parent;
9034
+ if (!statement || !typescript.default.isVariableStatement(statement)) return false;
9035
+ const modifiers = typescript.default.canHaveModifiers(statement) ? typescript.default.getModifiers(statement) : void 0;
9036
+ if (!modifiers) return false;
9037
+ return modifiers.some((modifier) => modifier.kind === typescript.default.SyntaxKind.ExportKeyword || modifier.kind === typescript.default.SyntaxKind.DefaultKeyword);
9038
+ };
9039
+ const collectVariableAliasCandidates = (graph, sourceFileLookup) => {
9040
+ const candidates = [];
9041
+ for (const module of graph.modules) {
9042
+ if (!module.isReachable) continue;
9043
+ if (module.isDeclarationFile) continue;
9044
+ const sourceFile = sourceFileLookup.get(normalizeSourcePath(module.fileId.path));
9045
+ if (!sourceFile) continue;
9046
+ for (const statement of sourceFile.statements) {
9047
+ if (!typescript.default.isVariableStatement(statement)) continue;
9048
+ for (const declaration of statement.declarationList.declarations) {
9049
+ if (!typescript.default.isIdentifier(declaration.name)) continue;
9050
+ if (!isSimpleIdentifierInitializer(declaration.initializer)) continue;
9051
+ if (!isModuleLevelDeclaration(declaration)) continue;
9052
+ const aliasName = declaration.name.text;
9053
+ const aliasedFromName = declaration.initializer.text;
9054
+ if (aliasName === aliasedFromName) continue;
9055
+ candidates.push({
9056
+ sourceFile,
9057
+ declaration,
9058
+ aliasName,
9059
+ aliasedFromName,
9060
+ modulePath: module.fileId.path
9061
+ });
6028
9062
  }
6029
9063
  }
6030
9064
  }
6031
- return cycles;
9065
+ return candidates;
6032
9066
  };
6033
- const detectCycles = (graph) => {
6034
- const adjacencyList = buildAdjacencyList(graph);
6035
- const components = findStronglyConnectedComponents(adjacencyList);
6036
- const allCycles = [];
6037
- const seenKeys = /* @__PURE__ */ new Set();
6038
- const sortedComponents = [...components].sort((componentA, componentB) => componentA.length - componentB.length);
6039
- for (const component of sortedComponents) {
6040
- if (allCycles.length >= 200) break;
6041
- if (component.length > 50) continue;
6042
- const elementaryCycles = enumerateElementaryCycles(component, adjacencyList, graph);
6043
- for (const cycle of elementaryCycles) {
6044
- const key = cycle.join(",");
6045
- if (!seenKeys.has(key)) {
6046
- seenKeys.add(key);
6047
- allCycles.push(cycle);
9067
+ const isMeaningfulReference = (site) => {
9068
+ if (site.isDeclarationName) return false;
9069
+ if (site.isImportSpecifier) return false;
9070
+ if (site.isExportSpecifier) return false;
9071
+ return true;
9072
+ };
9073
+ const resolveThroughAliasChain = (symbol, checker) => {
9074
+ if (symbol.flags & typescript.default.SymbolFlags.Alias) try {
9075
+ return checker.getAliasedSymbol(symbol);
9076
+ } catch {
9077
+ return symbol;
9078
+ }
9079
+ return symbol;
9080
+ };
9081
+ const detectRedundantVariableAliases = (graph, context, referenceIndex) => {
9082
+ const findings = [];
9083
+ const candidates = collectVariableAliasCandidates(graph, buildSourceFileLookup(context.program));
9084
+ if (candidates.length === 0) return findings;
9085
+ const { checker } = context;
9086
+ for (const candidate of candidates) {
9087
+ if (isDeclarationExported(candidate.declaration)) continue;
9088
+ const aliasNameIdentifier = candidate.declaration.name;
9089
+ if (!typescript.default.isIdentifier(aliasNameIdentifier)) continue;
9090
+ if (!candidate.declaration.initializer || !typescript.default.isIdentifier(candidate.declaration.initializer)) continue;
9091
+ const rawAliasSymbol = checker.getSymbolAtLocation(aliasNameIdentifier);
9092
+ const rawSourceSymbol = checker.getSymbolAtLocation(candidate.declaration.initializer);
9093
+ if (!rawAliasSymbol || !rawSourceSymbol) continue;
9094
+ const aliasSymbol = resolveThroughAliasChain(rawAliasSymbol, checker);
9095
+ const sourceSymbol = resolveThroughAliasChain(rawSourceSymbol, checker);
9096
+ if (aliasSymbol === sourceSymbol) continue;
9097
+ const sourceMeaningfulRefs = referenceIndex.getReferences(sourceSymbol).filter(isMeaningfulReference);
9098
+ const aliasMeaningfulRefs = referenceIndex.getReferences(aliasSymbol).filter(isMeaningfulReference);
9099
+ if (sourceMeaningfulRefs.filter((site) => site.identifier !== candidate.declaration.initializer).length > 0) continue;
9100
+ if (aliasMeaningfulRefs.length === 0) continue;
9101
+ const { line: zeroIndexedLine, character: zeroIndexedColumn } = candidate.sourceFile.getLineAndCharacterOfPosition(candidate.declaration.getStart(candidate.sourceFile));
9102
+ findings.push({
9103
+ path: candidate.modulePath,
9104
+ kind: "variable-alias",
9105
+ name: candidate.aliasName,
9106
+ aliasedFrom: candidate.aliasedFromName,
9107
+ line: zeroIndexedLine + 1,
9108
+ column: zeroIndexedColumn + 1,
9109
+ confidence: "high",
9110
+ reason: `\`const ${candidate.aliasName} = ${candidate.aliasedFromName}\` is the only consumer of \`${candidate.aliasedFromName}\` — rename or inline`
9111
+ });
9112
+ }
9113
+ return findings;
9114
+ };
9115
+
9116
+ //#endregion
9117
+ //#region src/semantic/redundant-reexports.ts
9118
+ const safeGetAliasedSymbol = (symbol, checker) => {
9119
+ try {
9120
+ return checker.getAliasedSymbol(symbol);
9121
+ } catch {
9122
+ return;
9123
+ }
9124
+ };
9125
+ const collectImportSpecifierRoundTrips = (graph, sourceFileLookup) => {
9126
+ const entries = [];
9127
+ for (const module of graph.modules) {
9128
+ if (!module.isReachable) continue;
9129
+ if (module.isDeclarationFile) continue;
9130
+ const sourceFile = sourceFileLookup.get(normalizeSourcePath(module.fileId.path));
9131
+ if (!sourceFile) continue;
9132
+ for (const statement of sourceFile.statements) {
9133
+ if (!typescript.default.isImportDeclaration(statement)) continue;
9134
+ const importClause = statement.importClause;
9135
+ if (!importClause?.namedBindings) continue;
9136
+ if (!typescript.default.isNamedImports(importClause.namedBindings)) continue;
9137
+ for (const importSpecifier of importClause.namedBindings.elements) {
9138
+ if (!importSpecifier.propertyName) continue;
9139
+ const importedName = importSpecifier.propertyName.text;
9140
+ const localName = importSpecifier.name.text;
9141
+ if (importedName === localName) continue;
9142
+ entries.push({
9143
+ modulePath: module.fileId.path,
9144
+ sourceFile,
9145
+ importSpecifier,
9146
+ importedName,
9147
+ localName
9148
+ });
6048
9149
  }
6049
- if (allCycles.length >= 200) break;
6050
9150
  }
6051
9151
  }
6052
- allCycles.sort((cycleA, cycleB) => {
6053
- const lengthDiff = cycleA.length - cycleB.length;
6054
- if (lengthDiff !== 0) return lengthDiff;
6055
- return graph.modules[cycleA[0]].fileId.path.localeCompare(graph.modules[cycleB[0]].fileId.path);
9152
+ return entries;
9153
+ };
9154
+ const detectRoundTripAliases = (graph, context) => {
9155
+ const findings = [];
9156
+ const importEntries = collectImportSpecifierRoundTrips(graph, buildSourceFileLookup(context.program));
9157
+ if (importEntries.length === 0) return findings;
9158
+ const { checker } = context;
9159
+ for (const entry of importEntries) {
9160
+ const localBindingSymbol = checker.getSymbolAtLocation(entry.importSpecifier.name);
9161
+ if (!localBindingSymbol) continue;
9162
+ if (!(localBindingSymbol.flags & typescript.default.SymbolFlags.Alias)) continue;
9163
+ const resolvedTargetSymbol = safeGetAliasedSymbol(localBindingSymbol, checker);
9164
+ if (!resolvedTargetSymbol) continue;
9165
+ const originalDeclarationName = resolvedTargetSymbol.name;
9166
+ if (!originalDeclarationName) continue;
9167
+ if (originalDeclarationName !== entry.localName) continue;
9168
+ if (originalDeclarationName === entry.importedName) continue;
9169
+ const { line: zeroIndexedLine, character: zeroIndexedColumn } = entry.sourceFile.getLineAndCharacterOfPosition(entry.importSpecifier.getStart(entry.sourceFile));
9170
+ findings.push({
9171
+ path: entry.modulePath,
9172
+ kind: "roundtrip-alias",
9173
+ name: entry.localName,
9174
+ aliasedFrom: entry.importedName,
9175
+ line: zeroIndexedLine + 1,
9176
+ column: zeroIndexedColumn + 1,
9177
+ confidence: "high",
9178
+ reason: `\`import { ${entry.importedName} as ${entry.localName} }\` renames back to the original declaration name — the upstream rename can be removed`
9179
+ });
9180
+ }
9181
+ return findings;
9182
+ };
9183
+
9184
+ //#endregion
9185
+ //#region src/semantic/index.ts
9186
+ const createDisabledSemanticResult = () => ({
9187
+ unusedTypes: [],
9188
+ unusedEnumMembers: [],
9189
+ unusedClassMembers: [],
9190
+ misclassifiedDependencies: [],
9191
+ redundantAliases: [],
9192
+ errors: [],
9193
+ contextStatus: "disabled"
9194
+ });
9195
+ const runSemanticAnalysis = (graph, config) => {
9196
+ const semanticConfig = config.semantic;
9197
+ if (!semanticConfig?.enabled) return createDisabledSemanticResult();
9198
+ const errors = [];
9199
+ const safeDetector = (detectorName, detector, fallback) => runSafeDetector({
9200
+ detectorName,
9201
+ detector,
9202
+ fallback,
9203
+ errorSink: errors,
9204
+ module: "semantic",
9205
+ contextDescription: "during semantic analysis"
6056
9206
  });
6057
- return allCycles.map((cycle) => ({ files: cycle.map((nodeIndex) => graph.modules[nodeIndex].fileId.path) }));
9207
+ const misclassifiedDependencies = semanticConfig.reportMisclassifiedDependencies ? safeDetector("detectMisclassifiedDependencies", () => detectMisclassifiedDependencies(graph, config), []) : [];
9208
+ if (!(semanticConfig.reportUnusedTypes || semanticConfig.reportUnusedEnumMembers || semanticConfig.reportUnusedClassMembers || semanticConfig.reportRedundantVariableAliases || semanticConfig.reportRoundTripAliases)) return {
9209
+ unusedTypes: [],
9210
+ unusedEnumMembers: [],
9211
+ unusedClassMembers: [],
9212
+ misclassifiedDependencies,
9213
+ redundantAliases: [],
9214
+ errors,
9215
+ contextStatus: "no-context-required"
9216
+ };
9217
+ let contextResult;
9218
+ try {
9219
+ contextResult = createSemanticContext(config.rootDir, config.tsConfigPath);
9220
+ } catch (contextError) {
9221
+ return {
9222
+ unusedTypes: [],
9223
+ unusedEnumMembers: [],
9224
+ unusedClassMembers: [],
9225
+ misclassifiedDependencies,
9226
+ redundantAliases: [],
9227
+ errors: [...errors, new TypeScriptError({
9228
+ code: "ts-not-loadable",
9229
+ message: "createSemanticContext threw before returning a result",
9230
+ detail: describeUnknownError(contextError)
9231
+ })],
9232
+ contextStatus: "typescript-load-failed"
9233
+ };
9234
+ }
9235
+ if (!contextResult.ok) return {
9236
+ unusedTypes: [],
9237
+ unusedEnumMembers: [],
9238
+ unusedClassMembers: [],
9239
+ misclassifiedDependencies,
9240
+ redundantAliases: [],
9241
+ errors: [...errors, contextResult.failure.error],
9242
+ contextStatus: contextResult.failure.reason,
9243
+ contextMessage: contextResult.failure.message
9244
+ };
9245
+ const { context } = contextResult;
9246
+ let referenceIndex;
9247
+ const getReferenceIndex = () => {
9248
+ if (!referenceIndex) referenceIndex = buildReferenceIndex(context.program, context.checker);
9249
+ return referenceIndex;
9250
+ };
9251
+ const unusedTypes = semanticConfig.reportUnusedTypes ? safeDetector("detectUnusedTypes", () => detectUnusedTypes(graph, config, context, getReferenceIndex()), []) : [];
9252
+ const unusedEnumMembers = semanticConfig.reportUnusedEnumMembers ? safeDetector("detectUnusedEnumMembers", () => detectUnusedEnumMembers(graph, config, context, getReferenceIndex()), []) : [];
9253
+ const unusedClassMembers = semanticConfig.reportUnusedClassMembers ? safeDetector("detectUnusedClassMembers", () => detectUnusedClassMembers(graph, config, context, getReferenceIndex(), semanticConfig.decoratorAllowlist), []) : [];
9254
+ const variableAliases = semanticConfig.reportRedundantVariableAliases ? safeDetector("detectRedundantVariableAliases", () => detectRedundantVariableAliases(graph, context, getReferenceIndex()), []) : [];
9255
+ const roundTripAliases = semanticConfig.reportRoundTripAliases ? safeDetector("detectRoundTripAliases", () => detectRoundTripAliases(graph, context), []) : [];
9256
+ return {
9257
+ unusedTypes,
9258
+ unusedEnumMembers,
9259
+ unusedClassMembers,
9260
+ misclassifiedDependencies,
9261
+ redundantAliases: [...variableAliases, ...roundTripAliases],
9262
+ errors,
9263
+ contextStatus: "ready"
9264
+ };
6058
9265
  };
6059
9266
 
6060
9267
  //#endregion
6061
9268
  //#region src/report/generate.ts
9269
+ const safeReportDetector = (detectorName, detector, fallback, errorSink) => runSafeDetector({
9270
+ detectorName,
9271
+ detector,
9272
+ fallback,
9273
+ errorSink,
9274
+ module: "report",
9275
+ contextDescription: "while building findings"
9276
+ });
6062
9277
  const generateReport = (graph, config) => {
6063
9278
  const analysisStartTime = performance.now();
6064
- const unusedFiles = detectOrphanFiles(graph);
6065
- const unusedExports = detectDeadExports(graph, config);
6066
- const unusedDependencies = detectStalePackages(graph, config);
6067
- const circularDependencies = detectCycles(graph);
9279
+ const errorSink = [];
9280
+ for (const module of graph.modules) {
9281
+ for (const parseError of module.parseErrors) {
9282
+ if (errorSink.length >= 5e3) break;
9283
+ errorSink.push(parseError);
9284
+ }
9285
+ if (errorSink.length >= 5e3) break;
9286
+ }
9287
+ const unusedFiles = safeReportDetector("detectOrphanFiles", () => detectOrphanFiles(graph), [], errorSink);
9288
+ const unusedExports = safeReportDetector("detectDeadExports", () => detectDeadExports(graph, config), [], errorSink);
9289
+ const unusedDependencies = safeReportDetector("detectStalePackages", () => detectStalePackages(graph, config), [], errorSink);
9290
+ const circularDependencies = safeReportDetector("detectCycles", () => detectCycles(graph), [], errorSink);
9291
+ const syntacticRedundantAliases = config.reportRedundancy ? [...safeReportDetector("detectRedundantAliases", () => detectRedundantAliases(graph), [], errorSink), ...safeReportDetector("detectUselessAliasedReExports", () => detectUselessAliasedReExports(graph), [], errorSink)] : [];
9292
+ const duplicateExports = config.reportRedundancy ? safeReportDetector("detectDuplicateExports", () => detectDuplicateExports(graph), [], errorSink) : [];
9293
+ const duplicateImports = config.reportRedundancy ? safeReportDetector("detectDuplicateImports", () => detectDuplicateImports(graph), [], errorSink) : [];
9294
+ const redundantTypePatterns = config.reportRedundancy ? safeReportDetector("detectRedundantTypePatterns", () => detectRedundantTypePatterns(graph), [], errorSink) : [];
9295
+ const identityWrappers = config.reportRedundancy ? safeReportDetector("detectIdentityWrappers", () => detectIdentityWrappers(graph), [], errorSink) : [];
9296
+ const duplicateTypeDefinitions = config.reportRedundancy ? safeReportDetector("detectDuplicateTypeDefinitions", () => detectDuplicateTypeDefinitions(graph), [], errorSink) : [];
9297
+ const duplicateInlineTypes = config.reportRedundancy ? safeReportDetector("detectDuplicateInlineTypes", () => detectDuplicateInlineTypes(graph), [], errorSink) : [];
9298
+ const simplifiableFunctions = config.reportRedundancy ? safeReportDetector("detectSimplifiableFunctions", () => detectSimplifiableFunctions(graph), [], errorSink) : [];
9299
+ const simplifiableExpressions = config.reportRedundancy ? safeReportDetector("detectSimplifiableExpressions", () => detectSimplifiableExpressions(graph), [], errorSink) : [];
9300
+ const duplicateConstants = config.reportRedundancy ? safeReportDetector("detectDuplicateConstants", () => detectDuplicateConstants(graph), [], errorSink) : [];
9301
+ let semanticResult;
9302
+ try {
9303
+ semanticResult = runSemanticAnalysis(graph, config);
9304
+ } catch (semanticError) {
9305
+ errorSink.push(new DetectorError({
9306
+ module: "semantic",
9307
+ message: "runSemanticAnalysis threw at the top level",
9308
+ detail: describeUnknownError(semanticError)
9309
+ }));
9310
+ semanticResult = {
9311
+ unusedTypes: [],
9312
+ unusedEnumMembers: [],
9313
+ unusedClassMembers: [],
9314
+ misclassifiedDependencies: [],
9315
+ redundantAliases: [],
9316
+ errors: [],
9317
+ contextStatus: "typescript-load-failed"
9318
+ };
9319
+ }
9320
+ for (const semanticError of semanticResult.errors) {
9321
+ if (errorSink.length >= 5e3) break;
9322
+ errorSink.push(semanticError);
9323
+ }
9324
+ const redundantAliases = config.reportRedundancy ? [...syntacticRedundantAliases, ...semanticResult.redundantAliases] : [];
6068
9325
  const totalExports = graph.modules.reduce((exportCount, module) => exportCount + module.exports.filter((exportInfo) => !(exportInfo.name === "*" && exportInfo.isNamespaceReExport)).length, 0);
6069
9326
  return {
6070
9327
  unusedFiles,
6071
9328
  unusedExports,
6072
9329
  unusedDependencies,
6073
9330
  circularDependencies,
9331
+ unusedTypes: semanticResult.unusedTypes,
9332
+ misclassifiedDependencies: semanticResult.misclassifiedDependencies,
9333
+ unusedEnumMembers: semanticResult.unusedEnumMembers,
9334
+ unusedClassMembers: semanticResult.unusedClassMembers,
9335
+ redundantAliases,
9336
+ duplicateExports,
9337
+ duplicateImports,
9338
+ redundantTypePatterns,
9339
+ identityWrappers,
9340
+ duplicateTypeDefinitions,
9341
+ duplicateInlineTypes,
9342
+ simplifiableFunctions,
9343
+ simplifiableExpressions,
9344
+ duplicateConstants,
9345
+ analysisErrors: errorSink,
6074
9346
  totalFiles: graph.modules.length,
6075
9347
  totalExports,
6076
9348
  analysisTimeMs: performance.now() - analysisStartTime
@@ -6101,26 +9373,142 @@ const detectReactNative = (rootDir, workspacePackages) => {
6101
9373
  }
6102
9374
  return false;
6103
9375
  };
9376
+ /**
9377
+ * Default flags below mark rules off-by-default. Rationale for each:
9378
+ *
9379
+ * - `reportUnusedClassMembers: false` — class-member dead-code detection
9380
+ * requires whole-program semantic analysis to be sound (subclass overrides,
9381
+ * structural typing, framework method-by-name invocation like `@HttpGet`).
9382
+ * When enabled on real React/Effect/NestJS codebases it produces a high
9383
+ * rate of stylistic-FP findings (lifecycle methods, framework hooks). Off
9384
+ * by default until the heuristics are tightened. Opt in via
9385
+ * `semantic.reportUnusedClassMembers = true` when you accept the noise.
9386
+ *
9387
+ * - `reportTypes: false` — type-only exports are over-represented in
9388
+ * barrel re-exports (the canonical `export type * from "./types"` pattern)
9389
+ * and are rarely actionable signal. Off by default; opt in when auditing
9390
+ * a type-heavy package.
9391
+ *
9392
+ * - `includeEntryExports: false` — exports from entry-point files are
9393
+ * "API surface" and intentionally exported for external consumers; flagging
9394
+ * them as "unused" is noise within a single repo scan. Opt in when auditing
9395
+ * a package boundary (e.g. before deleting public APIs).
9396
+ *
9397
+ * - `reportRedundancy: true` — on because redundancy findings are mostly
9398
+ * high-signal and the detectors carry their own confidence tiers.
9399
+ */
9400
+ const fillSemanticConfig = (semanticOverrides) => {
9401
+ if (semanticOverrides === void 0) return void 0;
9402
+ return {
9403
+ enabled: semanticOverrides.enabled ?? false,
9404
+ reportUnusedTypes: semanticOverrides.reportUnusedTypes ?? true,
9405
+ reportUnusedEnumMembers: semanticOverrides.reportUnusedEnumMembers ?? true,
9406
+ reportUnusedClassMembers: semanticOverrides.reportUnusedClassMembers ?? false,
9407
+ reportRedundantVariableAliases: semanticOverrides.reportRedundantVariableAliases ?? true,
9408
+ reportMisclassifiedDependencies: semanticOverrides.reportMisclassifiedDependencies ?? true,
9409
+ reportRoundTripAliases: semanticOverrides.reportRoundTripAliases ?? true,
9410
+ decoratorAllowlist: semanticOverrides.decoratorAllowlist ?? DEFAULT_SEMANTIC_DECORATOR_ALLOWLIST
9411
+ };
9412
+ };
6104
9413
  const defineConfig = (options) => ({
6105
9414
  rootDir: (0, node_path.resolve)(options.rootDir),
6106
9415
  entryPatterns: options.entryPatterns ?? DEFAULT_ENTRY_GLOBS,
6107
9416
  ignorePatterns: options.ignorePatterns ?? [],
6108
9417
  includeExtensions: options.includeExtensions ?? DEFAULT_EXTENSIONS,
6109
- tsConfigPath: options.tsConfigPath ?? void 0,
9418
+ tsConfigPath: options.tsConfigPath,
6110
9419
  reportTypes: options.reportTypes ?? false,
6111
- includeEntryExports: options.includeEntryExports ?? false
9420
+ includeEntryExports: options.includeEntryExports ?? false,
9421
+ reportRedundancy: options.reportRedundancy ?? true,
9422
+ semantic: fillSemanticConfig(options.semantic)
6112
9423
  });
9424
+ const buildEmptyScanResult = (errors, elapsedMs) => ({
9425
+ unusedFiles: [],
9426
+ unusedExports: [],
9427
+ unusedDependencies: [],
9428
+ circularDependencies: [],
9429
+ unusedTypes: [],
9430
+ misclassifiedDependencies: [],
9431
+ unusedEnumMembers: [],
9432
+ unusedClassMembers: [],
9433
+ redundantAliases: [],
9434
+ duplicateExports: [],
9435
+ duplicateImports: [],
9436
+ redundantTypePatterns: [],
9437
+ identityWrappers: [],
9438
+ duplicateTypeDefinitions: [],
9439
+ duplicateInlineTypes: [],
9440
+ simplifiableFunctions: [],
9441
+ simplifiableExpressions: [],
9442
+ duplicateConstants: [],
9443
+ analysisErrors: errors,
9444
+ totalFiles: 0,
9445
+ totalExports: 0,
9446
+ analysisTimeMs: elapsedMs
9447
+ });
9448
+ const validateConfig = (config) => {
9449
+ if (!config.rootDir || typeof config.rootDir !== "string") return new ConfigError({ message: "config.rootDir must be a non-empty string" });
9450
+ if (!(0, node_fs.existsSync)(config.rootDir)) return new ConfigError({
9451
+ message: `config.rootDir does not exist: ${config.rootDir}`,
9452
+ path: config.rootDir
9453
+ });
9454
+ };
6113
9455
  const analyze = async (config) => {
6114
9456
  const pipelineStartTime = performance.now();
6115
- const workspaceDiscovery = resolveWorkspaces((0, node_path.resolve)(config.rootDir));
9457
+ const setupErrors = [];
9458
+ const configValidationError = validateConfig(config);
9459
+ if (configValidationError) return buildEmptyScanResult([configValidationError], performance.now() - pipelineStartTime);
9460
+ let workspaceDiscovery;
9461
+ try {
9462
+ workspaceDiscovery = resolveWorkspaces((0, node_path.resolve)(config.rootDir));
9463
+ } catch (workspaceError) {
9464
+ setupErrors.push(new WorkspaceError({
9465
+ code: "workspace-discovery-failed",
9466
+ message: "resolveWorkspaces threw — falling back to single-package mode",
9467
+ path: config.rootDir,
9468
+ detail: describeUnknownError(workspaceError)
9469
+ }));
9470
+ workspaceDiscovery = {
9471
+ packages: [],
9472
+ excludedDirectories: [],
9473
+ hasRootLevelWorkspacePatterns: false
9474
+ };
9475
+ }
6116
9476
  const workspacePackages = [...workspaceDiscovery.packages];
6117
- const monorepoRoot = findMonorepoRoot(config.rootDir);
6118
- if (monorepoRoot) {
9477
+ let monorepoRoot;
9478
+ try {
9479
+ monorepoRoot = findMonorepoRoot(config.rootDir);
9480
+ } catch (monorepoError) {
9481
+ setupErrors.push(new WorkspaceError({
9482
+ code: "monorepo-discovery-failed",
9483
+ message: "findMonorepoRoot threw",
9484
+ path: config.rootDir,
9485
+ detail: describeUnknownError(monorepoError)
9486
+ }));
9487
+ monorepoRoot = void 0;
9488
+ }
9489
+ if (monorepoRoot) try {
6119
9490
  const monorepoWorkspaces = resolveWorkspaces(monorepoRoot);
6120
9491
  const existingDirectories = new Set(workspacePackages.map((workspacePackage) => workspacePackage.directory));
6121
9492
  for (const monorepoPackage of monorepoWorkspaces.packages) if (!existingDirectories.has(monorepoPackage.directory)) workspacePackages.push(monorepoPackage);
9493
+ } catch (monorepoWorkspaceError) {
9494
+ setupErrors.push(new WorkspaceError({
9495
+ code: "workspace-discovery-failed",
9496
+ message: "resolveWorkspaces threw on monorepo root",
9497
+ path: monorepoRoot,
9498
+ detail: describeUnknownError(monorepoWorkspaceError)
9499
+ }));
9500
+ }
9501
+ let frameworkIgnorePatterns = [];
9502
+ try {
9503
+ frameworkIgnorePatterns = getFrameworkExclusions(config.rootDir);
9504
+ } catch (frameworkError) {
9505
+ setupErrors.push(new WorkspaceError({
9506
+ code: "workspace-discovery-failed",
9507
+ message: "getFrameworkExclusions failed — proceeding without framework exclusion patterns",
9508
+ path: config.rootDir,
9509
+ detail: describeUnknownError(frameworkError)
9510
+ }));
6122
9511
  }
6123
- const frameworkIgnorePatterns = getFrameworkExclusions(config.rootDir);
6124
9512
  const absoluteRoot = (0, node_path.resolve)(config.rootDir);
6125
9513
  const outputDirectoryExclusions = OUTPUT_DIRECTORIES.flatMap((outputDirectory) => {
6126
9514
  const exclusions = [`${absoluteRoot}/${outputDirectory}/**`];
@@ -6136,32 +9524,101 @@ const analyze = async (config) => {
6136
9524
  ...config,
6137
9525
  ignorePatterns: [...config.ignorePatterns, ...allExclusionPatterns]
6138
9526
  } : config;
6139
- const files = await collectSourceFiles(configWithExclusions);
6140
- const discoveredEntries = await resolveEntries(configWithExclusions);
9527
+ let files;
9528
+ try {
9529
+ files = await collectSourceFiles(configWithExclusions);
9530
+ } catch (collectError) {
9531
+ setupErrors.push(new WorkspaceError({
9532
+ code: "workspace-discovery-failed",
9533
+ severity: "fatal",
9534
+ message: "collectSourceFiles failed",
9535
+ path: config.rootDir,
9536
+ detail: describeUnknownError(collectError)
9537
+ }));
9538
+ return buildEmptyScanResult(setupErrors, performance.now() - pipelineStartTime);
9539
+ }
9540
+ let discoveredEntries;
9541
+ try {
9542
+ discoveredEntries = await resolveEntries(configWithExclusions);
9543
+ } catch (entriesError) {
9544
+ setupErrors.push(new WorkspaceError({
9545
+ code: "workspace-discovery-failed",
9546
+ message: "resolveEntries failed — defaulting to empty entry set",
9547
+ path: config.rootDir,
9548
+ detail: describeUnknownError(entriesError)
9549
+ }));
9550
+ discoveredEntries = {
9551
+ productionEntries: [],
9552
+ testEntries: [],
9553
+ alwaysUsedFiles: []
9554
+ };
9555
+ }
6141
9556
  const productionEntrySet = new Set(discoveredEntries.productionEntries);
6142
9557
  const testEntrySet = new Set(discoveredEntries.testEntries);
6143
9558
  const alwaysUsedFileSet = new Set(discoveredEntries.alwaysUsedFiles);
6144
- const hasReactNative = detectReactNative(config.rootDir, workspacePackages);
6145
- const moduleResolver = createResolver(config, workspacePackages.map((workspacePackage) => ({
6146
- name: workspacePackage.name,
6147
- directory: workspacePackage.directory
6148
- })), {
6149
- hasReactNative,
6150
- monorepoRoot
6151
- });
9559
+ let hasReactNative = false;
9560
+ try {
9561
+ hasReactNative = detectReactNative(config.rootDir, workspacePackages);
9562
+ } catch {
9563
+ hasReactNative = false;
9564
+ }
9565
+ let moduleResolver;
9566
+ try {
9567
+ moduleResolver = createResolver(config, workspacePackages.map((workspacePackage) => ({
9568
+ name: workspacePackage.name,
9569
+ directory: workspacePackage.directory
9570
+ })), {
9571
+ hasReactNative,
9572
+ monorepoRoot
9573
+ });
9574
+ } catch (resolverError) {
9575
+ setupErrors.push(new ResolverError({
9576
+ message: "createResolver failed",
9577
+ path: config.rootDir,
9578
+ detail: describeUnknownError(resolverError)
9579
+ }));
9580
+ return buildEmptyScanResult(setupErrors, performance.now() - pipelineStartTime);
9581
+ }
6152
9582
  const graphInputs = [];
6153
9583
  for (const file of files) {
6154
9584
  const parsedModule = parseSourceFile(file.path);
6155
9585
  const resolvedImportMap = /* @__PURE__ */ new Map();
9586
+ const safeResolveImport = (specifier) => {
9587
+ try {
9588
+ return moduleResolver.resolveModule(specifier, file.path);
9589
+ } catch (resolveError) {
9590
+ setupErrors.push(new ResolverError({
9591
+ severity: "warning",
9592
+ message: `moduleResolver.resolveModule threw on specifier "${specifier}"`,
9593
+ path: file.path,
9594
+ detail: describeUnknownError(resolveError)
9595
+ }));
9596
+ return {
9597
+ resolvedPath: void 0,
9598
+ isExternal: false,
9599
+ packageName: void 0
9600
+ };
9601
+ }
9602
+ };
6156
9603
  for (const importInfo of parsedModule.imports) {
6157
9604
  if (importInfo.isGlob) {
6158
9605
  const fileDir = (0, node_path.dirname)(file.path);
6159
- const expandedFiles = fast_glob.default.sync(importInfo.specifier, {
6160
- cwd: fileDir,
6161
- absolute: true,
6162
- onlyFiles: true,
6163
- ignore: ["**/node_modules/**"]
6164
- });
9606
+ let expandedFiles = [];
9607
+ try {
9608
+ expandedFiles = fast_glob.default.sync(importInfo.specifier, {
9609
+ cwd: fileDir,
9610
+ absolute: true,
9611
+ onlyFiles: true,
9612
+ ignore: ["**/node_modules/**"]
9613
+ });
9614
+ } catch (globError) {
9615
+ setupErrors.push(new WorkspaceError({
9616
+ code: "workspace-discovery-failed",
9617
+ message: `fast-glob threw on import glob "${importInfo.specifier}"`,
9618
+ path: file.path,
9619
+ detail: describeUnknownError(globError)
9620
+ }));
9621
+ }
6165
9622
  for (const expandedFile of expandedFiles) resolvedImportMap.set(expandedFile, {
6166
9623
  resolvedPath: expandedFile,
6167
9624
  isExternal: false,
@@ -6174,14 +9631,10 @@ const analyze = async (config) => {
6174
9631
  });
6175
9632
  continue;
6176
9633
  }
6177
- const resolvedImport = moduleResolver.resolveModule(importInfo.specifier, file.path);
6178
- resolvedImportMap.set(importInfo.specifier, resolvedImport);
9634
+ resolvedImportMap.set(importInfo.specifier, safeResolveImport(importInfo.specifier));
6179
9635
  }
6180
9636
  for (const exportInfo of parsedModule.exports) if (exportInfo.isReExport && exportInfo.reExportSource) {
6181
- if (!resolvedImportMap.has(exportInfo.reExportSource)) {
6182
- const resolvedImport = moduleResolver.resolveModule(exportInfo.reExportSource, file.path);
6183
- resolvedImportMap.set(exportInfo.reExportSource, resolvedImport);
6184
- }
9637
+ if (!resolvedImportMap.has(exportInfo.reExportSource)) resolvedImportMap.set(exportInfo.reExportSource, safeResolveImport(exportInfo.reExportSource));
6185
9638
  }
6186
9639
  const isAlwaysUsed = alwaysUsedFileSet.has(file.path);
6187
9640
  graphInputs.push({
@@ -6209,7 +9662,22 @@ const analyze = async (config) => {
6209
9662
  const parsedStyleModule = parseSourceFile(styleFilePath);
6210
9663
  const resolvedStyleImportMap = /* @__PURE__ */ new Map();
6211
9664
  for (const importInfo of parsedStyleModule.imports) {
6212
- const resolvedImport = moduleResolver.resolveModule(importInfo.specifier, styleFilePath);
9665
+ let resolvedImport;
9666
+ try {
9667
+ resolvedImport = moduleResolver.resolveModule(importInfo.specifier, styleFilePath);
9668
+ } catch (styleResolveError) {
9669
+ setupErrors.push(new ResolverError({
9670
+ severity: "warning",
9671
+ message: `moduleResolver.resolveModule threw on style import "${importInfo.specifier}"`,
9672
+ path: styleFilePath,
9673
+ detail: describeUnknownError(styleResolveError)
9674
+ }));
9675
+ resolvedImport = {
9676
+ resolvedPath: void 0,
9677
+ isExternal: false,
9678
+ packageName: void 0
9679
+ };
9680
+ }
6213
9681
  resolvedStyleImportMap.set(importInfo.specifier, resolvedImport);
6214
9682
  if (resolvedImport.resolvedPath && !discoveredFilePaths.has(resolvedImport.resolvedPath)) {
6215
9683
  if (STYLE_EXTENSIONS.some((ext) => resolvedImport.resolvedPath.endsWith(ext)) && (0, node_fs.existsSync)(resolvedImport.resolvedPath)) styleFilesToAdd.add(resolvedImport.resolvedPath);
@@ -6225,10 +9693,50 @@ const analyze = async (config) => {
6225
9693
  discoveredFilePaths.add(styleFilePath);
6226
9694
  nextFileIndex++;
6227
9695
  }
6228
- const moduleGraph = buildDependencyGraph(graphInputs);
6229
- resolveReExportChains(moduleGraph);
6230
- traceReachability(moduleGraph);
6231
- const analysisResult = generateReport(moduleGraph, config);
9696
+ let moduleGraph;
9697
+ try {
9698
+ moduleGraph = buildDependencyGraph(graphInputs);
9699
+ } catch (graphError) {
9700
+ setupErrors.push(new DetectorError({
9701
+ module: "linker",
9702
+ severity: "fatal",
9703
+ message: "buildDependencyGraph threw",
9704
+ detail: describeUnknownError(graphError)
9705
+ }));
9706
+ return buildEmptyScanResult(setupErrors, performance.now() - pipelineStartTime);
9707
+ }
9708
+ try {
9709
+ resolveReExportChains(moduleGraph);
9710
+ } catch (reExportError) {
9711
+ setupErrors.push(new DetectorError({
9712
+ module: "linker",
9713
+ message: "resolveReExportChains threw — re-export propagation skipped",
9714
+ detail: describeUnknownError(reExportError)
9715
+ }));
9716
+ }
9717
+ try {
9718
+ traceReachability(moduleGraph);
9719
+ } catch (reachabilityError) {
9720
+ setupErrors.push(new DetectorError({
9721
+ module: "linker",
9722
+ message: "traceReachability threw — every module marked reachable to avoid over-reporting",
9723
+ detail: describeUnknownError(reachabilityError)
9724
+ }));
9725
+ for (const module of moduleGraph.modules) module.isReachable = true;
9726
+ }
9727
+ let analysisResult;
9728
+ try {
9729
+ analysisResult = generateReport(moduleGraph, config);
9730
+ } catch (reportError) {
9731
+ setupErrors.push(new DetectorError({
9732
+ module: "report",
9733
+ severity: "fatal",
9734
+ message: "generateReport threw at the top level",
9735
+ detail: describeUnknownError(reportError)
9736
+ }));
9737
+ return buildEmptyScanResult(setupErrors, performance.now() - pipelineStartTime);
9738
+ }
9739
+ if (setupErrors.length > 0) analysisResult.analysisErrors = [...setupErrors, ...analysisResult.analysisErrors];
6232
9740
  analysisResult.analysisTimeMs = performance.now() - pipelineStartTime;
6233
9741
  return analysisResult;
6234
9742
  };