deslop-js 0.0.10 → 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 +3532 -190
- package/dist/index.d.cts +228 -1
- package/dist/index.d.mts +228 -1
- package/dist/index.mjs +3531 -190
- package/package.json +5 -4
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)))
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
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
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
return
|
|
427
|
-
|
|
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
|
-
|
|
432
|
-
|
|
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
|
-
|
|
439
|
-
|
|
1758
|
+
errors: [...earlyErrors, new ParseError({
|
|
1759
|
+
code: "parse-failed",
|
|
1760
|
+
message: "oxc-parser returned no program body",
|
|
1761
|
+
path: filePath
|
|
1762
|
+
})]
|
|
440
1763
|
};
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
}
|
|
455
|
-
|
|
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
|
|
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
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
"
|
|
473
|
-
"
|
|
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
|
|
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
|
|
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)
|
|
1996
|
-
|
|
1997
|
-
|
|
1998
|
-
|
|
1999
|
-
|
|
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))
|
|
2002
|
-
|
|
2003
|
-
|
|
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)(
|
|
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
|
|
2945
|
-
if (
|
|
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
|
|
2952
|
-
if (
|
|
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;
|
|
@@ -5437,6 +7152,7 @@ const matchesPackageImportReference = (content, packageName) => {
|
|
|
5437
7152
|
new RegExp(`\\bfrom\\s+['"]${escapedPackageName}${subpathPattern}['"]`),
|
|
5438
7153
|
new RegExp(`\\bimport\\s+(?:[^'";\\n]*?\\sfrom\\s+)?['"]${escapedPackageName}${subpathPattern}['"]`),
|
|
5439
7154
|
new RegExp(`\\brequire\\s*\\(\\s*['"]${escapedPackageName}${subpathPattern}['"]\\s*\\)`),
|
|
7155
|
+
new RegExp(`\\brequire\\s*\\(\\s*\`${escapedPackageName}${subpathPattern}`),
|
|
5440
7156
|
new RegExp(`\\bimport\\s*\\(\\s*['"]${escapedPackageName}${subpathPattern}['"]`)
|
|
5441
7157
|
].some((pattern) => pattern.test(content));
|
|
5442
7158
|
};
|
|
@@ -5686,7 +7402,9 @@ const CLI_BINARY_TO_PACKAGE = {
|
|
|
5686
7402
|
};
|
|
5687
7403
|
const CLI_BINARY_FALLBACK_PACKAGES = {
|
|
5688
7404
|
babel: ["babel-cli"],
|
|
5689
|
-
jest: ["jest-cli"]
|
|
7405
|
+
jest: ["jest-cli"],
|
|
7406
|
+
remark: ["remark-cli"],
|
|
7407
|
+
dumi: ["dumi"]
|
|
5690
7408
|
};
|
|
5691
7409
|
const ENV_WRAPPER_BINARY_SET = new Set([
|
|
5692
7410
|
"cross-env",
|
|
@@ -5791,7 +7509,12 @@ const CONFIG_FILE_GLOBS = [
|
|
|
5791
7509
|
".lintstagedrc.{js,cjs,mjs,json}",
|
|
5792
7510
|
"commitlint.config.{js,cjs,mjs,ts}",
|
|
5793
7511
|
".commitlintrc.{js,cjs,mjs,json,yaml,yml}",
|
|
5794
|
-
"tslint.json"
|
|
7512
|
+
"tslint.json",
|
|
7513
|
+
".remarkrc",
|
|
7514
|
+
".remarkrc.{js,cjs,mjs,json}",
|
|
7515
|
+
".dumirc.ts",
|
|
7516
|
+
".dumirc.js",
|
|
7517
|
+
"dumi.config.{ts,js}"
|
|
5795
7518
|
];
|
|
5796
7519
|
const collectConfigReferencedPackages = (rootDir, graph, declaredNames) => {
|
|
5797
7520
|
const referenced = /* @__PURE__ */ new Set();
|
|
@@ -5818,6 +7541,24 @@ const collectConfigReferencedPackages = (rootDir, graph, declaredNames) => {
|
|
|
5818
7541
|
} catch {
|
|
5819
7542
|
continue;
|
|
5820
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
|
+
}
|
|
5821
7562
|
return referenced;
|
|
5822
7563
|
};
|
|
5823
7564
|
const PACKAGE_JSON_CONFIG_SECTIONS = [
|
|
@@ -6140,103 +7881,1468 @@ const findStronglyConnectedComponents = (adjacencyList) => {
|
|
|
6140
7881
|
}
|
|
6141
7882
|
}
|
|
6142
7883
|
}
|
|
6143
|
-
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() };
|
|
6144
8827
|
};
|
|
6145
|
-
const
|
|
6146
|
-
|
|
6147
|
-
|
|
6148
|
-
|
|
6149
|
-
|
|
6150
|
-
|
|
6151
|
-
|
|
6152
|
-
|
|
6153
|
-
|
|
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
|
+
});
|
|
6154
8917
|
}
|
|
6155
8918
|
}
|
|
6156
|
-
return
|
|
8919
|
+
return findings;
|
|
6157
8920
|
};
|
|
6158
|
-
|
|
6159
|
-
|
|
6160
|
-
|
|
6161
|
-
|
|
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
|
+
}
|
|
6162
8979
|
}
|
|
6163
|
-
|
|
6164
|
-
|
|
6165
|
-
|
|
6166
|
-
|
|
6167
|
-
|
|
6168
|
-
|
|
6169
|
-
|
|
6170
|
-
|
|
6171
|
-
|
|
6172
|
-
|
|
6173
|
-
|
|
6174
|
-
|
|
6175
|
-
|
|
6176
|
-
|
|
6177
|
-
|
|
6178
|
-
|
|
6179
|
-
|
|
6180
|
-
|
|
6181
|
-
|
|
6182
|
-
|
|
6183
|
-
|
|
6184
|
-
|
|
6185
|
-
|
|
6186
|
-
|
|
6187
|
-
|
|
6188
|
-
|
|
6189
|
-
|
|
6190
|
-
|
|
6191
|
-
|
|
6192
|
-
|
|
6193
|
-
|
|
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
|
+
});
|
|
6194
9062
|
}
|
|
6195
9063
|
}
|
|
6196
9064
|
}
|
|
6197
|
-
return
|
|
9065
|
+
return candidates;
|
|
6198
9066
|
};
|
|
6199
|
-
const
|
|
6200
|
-
|
|
6201
|
-
|
|
6202
|
-
|
|
6203
|
-
|
|
6204
|
-
|
|
6205
|
-
|
|
6206
|
-
|
|
6207
|
-
|
|
6208
|
-
|
|
6209
|
-
|
|
6210
|
-
|
|
6211
|
-
|
|
6212
|
-
|
|
6213
|
-
|
|
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
|
+
});
|
|
6214
9149
|
}
|
|
6215
|
-
if (allCycles.length >= 200) break;
|
|
6216
9150
|
}
|
|
6217
9151
|
}
|
|
6218
|
-
|
|
6219
|
-
|
|
6220
|
-
|
|
6221
|
-
|
|
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"
|
|
6222
9206
|
});
|
|
6223
|
-
|
|
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
|
+
};
|
|
6224
9265
|
};
|
|
6225
9266
|
|
|
6226
9267
|
//#endregion
|
|
6227
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
|
+
});
|
|
6228
9277
|
const generateReport = (graph, config) => {
|
|
6229
9278
|
const analysisStartTime = performance.now();
|
|
6230
|
-
const
|
|
6231
|
-
const
|
|
6232
|
-
|
|
6233
|
-
|
|
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] : [];
|
|
6234
9325
|
const totalExports = graph.modules.reduce((exportCount, module) => exportCount + module.exports.filter((exportInfo) => !(exportInfo.name === "*" && exportInfo.isNamespaceReExport)).length, 0);
|
|
6235
9326
|
return {
|
|
6236
9327
|
unusedFiles,
|
|
6237
9328
|
unusedExports,
|
|
6238
9329
|
unusedDependencies,
|
|
6239
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,
|
|
6240
9346
|
totalFiles: graph.modules.length,
|
|
6241
9347
|
totalExports,
|
|
6242
9348
|
analysisTimeMs: performance.now() - analysisStartTime
|
|
@@ -6267,26 +9373,142 @@ const detectReactNative = (rootDir, workspacePackages) => {
|
|
|
6267
9373
|
}
|
|
6268
9374
|
return false;
|
|
6269
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
|
+
};
|
|
6270
9413
|
const defineConfig = (options) => ({
|
|
6271
9414
|
rootDir: (0, node_path.resolve)(options.rootDir),
|
|
6272
9415
|
entryPatterns: options.entryPatterns ?? DEFAULT_ENTRY_GLOBS,
|
|
6273
9416
|
ignorePatterns: options.ignorePatterns ?? [],
|
|
6274
9417
|
includeExtensions: options.includeExtensions ?? DEFAULT_EXTENSIONS,
|
|
6275
|
-
tsConfigPath: options.tsConfigPath
|
|
9418
|
+
tsConfigPath: options.tsConfigPath,
|
|
6276
9419
|
reportTypes: options.reportTypes ?? false,
|
|
6277
|
-
includeEntryExports: options.includeEntryExports ?? false
|
|
9420
|
+
includeEntryExports: options.includeEntryExports ?? false,
|
|
9421
|
+
reportRedundancy: options.reportRedundancy ?? true,
|
|
9422
|
+
semantic: fillSemanticConfig(options.semantic)
|
|
6278
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
|
+
};
|
|
6279
9455
|
const analyze = async (config) => {
|
|
6280
9456
|
const pipelineStartTime = performance.now();
|
|
6281
|
-
const
|
|
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
|
+
}
|
|
6282
9476
|
const workspacePackages = [...workspaceDiscovery.packages];
|
|
6283
|
-
|
|
6284
|
-
|
|
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 {
|
|
6285
9490
|
const monorepoWorkspaces = resolveWorkspaces(monorepoRoot);
|
|
6286
9491
|
const existingDirectories = new Set(workspacePackages.map((workspacePackage) => workspacePackage.directory));
|
|
6287
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
|
+
}));
|
|
6288
9511
|
}
|
|
6289
|
-
const frameworkIgnorePatterns = getFrameworkExclusions(config.rootDir);
|
|
6290
9512
|
const absoluteRoot = (0, node_path.resolve)(config.rootDir);
|
|
6291
9513
|
const outputDirectoryExclusions = OUTPUT_DIRECTORIES.flatMap((outputDirectory) => {
|
|
6292
9514
|
const exclusions = [`${absoluteRoot}/${outputDirectory}/**`];
|
|
@@ -6302,32 +9524,101 @@ const analyze = async (config) => {
|
|
|
6302
9524
|
...config,
|
|
6303
9525
|
ignorePatterns: [...config.ignorePatterns, ...allExclusionPatterns]
|
|
6304
9526
|
} : config;
|
|
6305
|
-
|
|
6306
|
-
|
|
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
|
+
}
|
|
6307
9556
|
const productionEntrySet = new Set(discoveredEntries.productionEntries);
|
|
6308
9557
|
const testEntrySet = new Set(discoveredEntries.testEntries);
|
|
6309
9558
|
const alwaysUsedFileSet = new Set(discoveredEntries.alwaysUsedFiles);
|
|
6310
|
-
|
|
6311
|
-
|
|
6312
|
-
|
|
6313
|
-
|
|
6314
|
-
|
|
6315
|
-
|
|
6316
|
-
|
|
6317
|
-
|
|
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
|
+
}
|
|
6318
9582
|
const graphInputs = [];
|
|
6319
9583
|
for (const file of files) {
|
|
6320
9584
|
const parsedModule = parseSourceFile(file.path);
|
|
6321
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
|
+
};
|
|
6322
9603
|
for (const importInfo of parsedModule.imports) {
|
|
6323
9604
|
if (importInfo.isGlob) {
|
|
6324
9605
|
const fileDir = (0, node_path.dirname)(file.path);
|
|
6325
|
-
|
|
6326
|
-
|
|
6327
|
-
|
|
6328
|
-
|
|
6329
|
-
|
|
6330
|
-
|
|
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
|
+
}
|
|
6331
9622
|
for (const expandedFile of expandedFiles) resolvedImportMap.set(expandedFile, {
|
|
6332
9623
|
resolvedPath: expandedFile,
|
|
6333
9624
|
isExternal: false,
|
|
@@ -6340,14 +9631,10 @@ const analyze = async (config) => {
|
|
|
6340
9631
|
});
|
|
6341
9632
|
continue;
|
|
6342
9633
|
}
|
|
6343
|
-
|
|
6344
|
-
resolvedImportMap.set(importInfo.specifier, resolvedImport);
|
|
9634
|
+
resolvedImportMap.set(importInfo.specifier, safeResolveImport(importInfo.specifier));
|
|
6345
9635
|
}
|
|
6346
9636
|
for (const exportInfo of parsedModule.exports) if (exportInfo.isReExport && exportInfo.reExportSource) {
|
|
6347
|
-
if (!resolvedImportMap.has(exportInfo.reExportSource))
|
|
6348
|
-
const resolvedImport = moduleResolver.resolveModule(exportInfo.reExportSource, file.path);
|
|
6349
|
-
resolvedImportMap.set(exportInfo.reExportSource, resolvedImport);
|
|
6350
|
-
}
|
|
9637
|
+
if (!resolvedImportMap.has(exportInfo.reExportSource)) resolvedImportMap.set(exportInfo.reExportSource, safeResolveImport(exportInfo.reExportSource));
|
|
6351
9638
|
}
|
|
6352
9639
|
const isAlwaysUsed = alwaysUsedFileSet.has(file.path);
|
|
6353
9640
|
graphInputs.push({
|
|
@@ -6375,7 +9662,22 @@ const analyze = async (config) => {
|
|
|
6375
9662
|
const parsedStyleModule = parseSourceFile(styleFilePath);
|
|
6376
9663
|
const resolvedStyleImportMap = /* @__PURE__ */ new Map();
|
|
6377
9664
|
for (const importInfo of parsedStyleModule.imports) {
|
|
6378
|
-
|
|
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
|
+
}
|
|
6379
9681
|
resolvedStyleImportMap.set(importInfo.specifier, resolvedImport);
|
|
6380
9682
|
if (resolvedImport.resolvedPath && !discoveredFilePaths.has(resolvedImport.resolvedPath)) {
|
|
6381
9683
|
if (STYLE_EXTENSIONS.some((ext) => resolvedImport.resolvedPath.endsWith(ext)) && (0, node_fs.existsSync)(resolvedImport.resolvedPath)) styleFilesToAdd.add(resolvedImport.resolvedPath);
|
|
@@ -6391,10 +9693,50 @@ const analyze = async (config) => {
|
|
|
6391
9693
|
discoveredFilePaths.add(styleFilePath);
|
|
6392
9694
|
nextFileIndex++;
|
|
6393
9695
|
}
|
|
6394
|
-
|
|
6395
|
-
|
|
6396
|
-
|
|
6397
|
-
|
|
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];
|
|
6398
9740
|
analysisResult.analysisTimeMs = performance.now() - pipelineStartTime;
|
|
6399
9741
|
return analysisResult;
|
|
6400
9742
|
};
|