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