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