react-doctor 0.5.6-dev.5d1347e → 0.5.6-dev.66133f8
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/cli.js +364 -159
- package/dist/index.d.ts +19 -0
- package/dist/index.js +221 -120
- package/dist/lsp.js +220 -121
- package/package.json +4 -4
package/dist/cli.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
|
|
2
|
-
!function(){try{var e="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof globalThis?globalThis:"undefined"!=typeof self?self:{},n=(new e.Error).stack;n&&(e._sentryDebugIds=e._sentryDebugIds||{},e._sentryDebugIds[n]="
|
|
2
|
+
!function(){try{var e="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof globalThis?globalThis:"undefined"!=typeof self?self:{},n=(new e.Error).stack;n&&(e._sentryDebugIds=e._sentryDebugIds||{},e._sentryDebugIds[n]="4aa7cab0-c8a8-5513-bde6-3f7a0a1bdaa7")}catch(e){}}();
|
|
3
3
|
import { createRequire } from "node:module";
|
|
4
4
|
import * as NodeChildProcess from "node:child_process";
|
|
5
5
|
import { execFile, execFileSync, spawn, spawnSync } from "node:child_process";
|
|
@@ -22358,7 +22358,8 @@ var Diagnostic = class extends Class("Diagnostic")({
|
|
|
22358
22358
|
category: String$1,
|
|
22359
22359
|
fileContext: optional(Literals(["test", "story"])),
|
|
22360
22360
|
suppressionHint: optional(String$1),
|
|
22361
|
-
relatedLocations: optional(ArraySchema(DiagnosticRelatedLocation))
|
|
22361
|
+
relatedLocations: optional(ArraySchema(DiagnosticRelatedLocation)),
|
|
22362
|
+
fixGroupId: optional(String$1)
|
|
22362
22363
|
}) {};
|
|
22363
22364
|
const JsonReportMode = Literals([
|
|
22364
22365
|
"full",
|
|
@@ -22400,6 +22401,7 @@ var JsonReportProjectEntry = class extends Class("JsonReportProjectEntry")({
|
|
|
22400
22401
|
score: Unknown,
|
|
22401
22402
|
skippedChecks: ArraySchema(String$1),
|
|
22402
22403
|
skippedCheckReasons: optional(Record$1(String$1, String$1)),
|
|
22404
|
+
scannedFileCount: optional(Number$1),
|
|
22403
22405
|
elapsedMilliseconds: Number$1
|
|
22404
22406
|
}) {};
|
|
22405
22407
|
/**
|
|
@@ -36859,6 +36861,13 @@ const APP_ONLY_RULE_KEYS = new Set([
|
|
|
36859
36861
|
]);
|
|
36860
36862
|
const COMPILER_CLEANUP_BUCKET = "compiler-cleanup";
|
|
36861
36863
|
const COMPILER_CLEANUP_RULE_KEYS = new Set(["react-doctor/react-compiler-no-manual-memoization"]);
|
|
36864
|
+
const ROOT_CAUSE_GROUPABLE_RULE_KEYS = new Set([
|
|
36865
|
+
"react-doctor/no-derived-state",
|
|
36866
|
+
"react-doctor/no-derived-state-effect",
|
|
36867
|
+
"react-doctor/no-derived-useState",
|
|
36868
|
+
"react-doctor/no-adjust-state-on-prop-change",
|
|
36869
|
+
"react-doctor/no-reset-all-state-on-prop-change"
|
|
36870
|
+
]);
|
|
36862
36871
|
const MAX_GLOB_PATTERN_LENGTH_CHARS = 1024;
|
|
36863
36872
|
const CONFIG_CACHE_TTL_MS = 300 * 1e3;
|
|
36864
36873
|
const SOCKET_FREE_PURL_API_BASE = "https://firewall-api.socket.dev/purl";
|
|
@@ -37433,115 +37442,6 @@ const buildRuleSeverityControls = (config) => {
|
|
|
37433
37442
|
...config.buckets !== void 0 ? { buckets: config.buckets } : {}
|
|
37434
37443
|
};
|
|
37435
37444
|
};
|
|
37436
|
-
const JSX_OPENER_TAG_PATTERN = /<[A-Za-z][\w.]*/g;
|
|
37437
|
-
const JSX_TAG_NAME_FOLLOW = /[A-Za-z]/;
|
|
37438
|
-
const isOpenerMatchInsideLineComment = (line, openerCharIndex) => {
|
|
37439
|
-
let stringDelimiter = null;
|
|
37440
|
-
for (let charIndex = 0; charIndex < openerCharIndex; charIndex++) {
|
|
37441
|
-
const character = line[charIndex];
|
|
37442
|
-
if (stringDelimiter !== null) {
|
|
37443
|
-
if (character === "\\") {
|
|
37444
|
-
charIndex++;
|
|
37445
|
-
continue;
|
|
37446
|
-
}
|
|
37447
|
-
if (character === stringDelimiter) stringDelimiter = null;
|
|
37448
|
-
continue;
|
|
37449
|
-
}
|
|
37450
|
-
if (character === "\"" || character === "'" || character === "`") {
|
|
37451
|
-
stringDelimiter = character;
|
|
37452
|
-
continue;
|
|
37453
|
-
}
|
|
37454
|
-
if (character === "/" && line[charIndex + 1] === "/") return true;
|
|
37455
|
-
}
|
|
37456
|
-
return false;
|
|
37457
|
-
};
|
|
37458
|
-
const findOpenerTagOnLine = (line) => {
|
|
37459
|
-
for (const match of line.matchAll(JSX_OPENER_TAG_PATTERN)) {
|
|
37460
|
-
if (match.index === void 0) continue;
|
|
37461
|
-
if (!isOpenerMatchInsideLineComment(line, match.index)) return { startCharIndex: match.index + match[0].length };
|
|
37462
|
-
}
|
|
37463
|
-
return null;
|
|
37464
|
-
};
|
|
37465
|
-
const findJsxOpenerSpan = (lines, openerLineIndex) => {
|
|
37466
|
-
const openerLine = lines[openerLineIndex];
|
|
37467
|
-
if (openerLine === void 0) return null;
|
|
37468
|
-
const opener = findOpenerTagOnLine(openerLine);
|
|
37469
|
-
if (!opener) return null;
|
|
37470
|
-
const lookaheadLimit = Math.min(lines.length, openerLineIndex + 32);
|
|
37471
|
-
let braceDepth = 0;
|
|
37472
|
-
let innerAngleDepth = 0;
|
|
37473
|
-
let stringDelimiter = null;
|
|
37474
|
-
for (let lineIndex = openerLineIndex; lineIndex < lookaheadLimit; lineIndex++) {
|
|
37475
|
-
const currentLine = lines[lineIndex];
|
|
37476
|
-
const startCharForLine = lineIndex === openerLineIndex ? opener.startCharIndex : 0;
|
|
37477
|
-
for (let charIndex = startCharForLine; charIndex < currentLine.length; charIndex++) {
|
|
37478
|
-
const character = currentLine[charIndex];
|
|
37479
|
-
if (stringDelimiter !== null) {
|
|
37480
|
-
if (character === "\\") {
|
|
37481
|
-
charIndex++;
|
|
37482
|
-
continue;
|
|
37483
|
-
}
|
|
37484
|
-
if (character === stringDelimiter) stringDelimiter = null;
|
|
37485
|
-
continue;
|
|
37486
|
-
}
|
|
37487
|
-
if (character === "\"" || character === "'" || character === "`") {
|
|
37488
|
-
stringDelimiter = character;
|
|
37489
|
-
continue;
|
|
37490
|
-
}
|
|
37491
|
-
if (character === "{") {
|
|
37492
|
-
braceDepth++;
|
|
37493
|
-
continue;
|
|
37494
|
-
}
|
|
37495
|
-
if (character === "}") {
|
|
37496
|
-
braceDepth--;
|
|
37497
|
-
continue;
|
|
37498
|
-
}
|
|
37499
|
-
if (braceDepth !== 0) continue;
|
|
37500
|
-
if (character === "<") {
|
|
37501
|
-
const followCharacter = currentLine[charIndex + 1];
|
|
37502
|
-
if (followCharacter !== void 0 && JSX_TAG_NAME_FOLLOW.test(followCharacter)) innerAngleDepth++;
|
|
37503
|
-
continue;
|
|
37504
|
-
}
|
|
37505
|
-
if (character !== ">") continue;
|
|
37506
|
-
const previousCharacter = currentLine[charIndex - 1];
|
|
37507
|
-
const nextCharacter = currentLine[charIndex + 1];
|
|
37508
|
-
if (previousCharacter === "=" || nextCharacter === "=") continue;
|
|
37509
|
-
if (innerAngleDepth > 0) {
|
|
37510
|
-
innerAngleDepth--;
|
|
37511
|
-
continue;
|
|
37512
|
-
}
|
|
37513
|
-
return lineIndex;
|
|
37514
|
-
}
|
|
37515
|
-
}
|
|
37516
|
-
return null;
|
|
37517
|
-
};
|
|
37518
|
-
const findEnclosingMultilineJsxOpenerStart = (lines, diagnosticLineIndex) => {
|
|
37519
|
-
for (let candidateIndex = diagnosticLineIndex - 1; candidateIndex >= 0 && diagnosticLineIndex - candidateIndex <= 32; candidateIndex--) {
|
|
37520
|
-
const openerCloseIndex = findJsxOpenerSpan(lines, candidateIndex);
|
|
37521
|
-
if (openerCloseIndex !== null && openerCloseIndex >= diagnosticLineIndex) return candidateIndex;
|
|
37522
|
-
}
|
|
37523
|
-
return null;
|
|
37524
|
-
};
|
|
37525
|
-
const DISABLE_NEXT_LINE_PATTERN = /(?:\/\/|\/\*)\s*react-doctor-disable-next-line\b(?:\s+([^\r\n]*?))?\s*(?:\*\/)?\s*\}?\s*$/;
|
|
37526
|
-
const findStackedDisableCommentsAbove = (lines, anchorIndex) => {
|
|
37527
|
-
const collected = [];
|
|
37528
|
-
let isStillInChain = true;
|
|
37529
|
-
for (let candidateIndex = anchorIndex - 1; candidateIndex >= 0 && anchorIndex - candidateIndex <= 10; candidateIndex--) {
|
|
37530
|
-
const candidateLine = lines[candidateIndex];
|
|
37531
|
-
if (candidateLine === void 0) break;
|
|
37532
|
-
const match = candidateLine.match(DISABLE_NEXT_LINE_PATTERN);
|
|
37533
|
-
if (match) {
|
|
37534
|
-
collected.push({
|
|
37535
|
-
commentLineIndex: candidateIndex,
|
|
37536
|
-
ruleList: match[1],
|
|
37537
|
-
isInChain: isStillInChain
|
|
37538
|
-
});
|
|
37539
|
-
continue;
|
|
37540
|
-
}
|
|
37541
|
-
isStillInChain = false;
|
|
37542
|
-
}
|
|
37543
|
-
return collected;
|
|
37544
|
-
};
|
|
37545
37445
|
const LEGACY_RULE_KEY_TO_NATIVE_RULE_KEY = {
|
|
37546
37446
|
"effect/no-adjust-state-on-prop-change": "react-doctor/no-adjust-state-on-prop-change",
|
|
37547
37447
|
"effect/no-chain-state-updates": "react-doctor/no-chain-state-updates",
|
|
@@ -37666,7 +37566,13 @@ for (const [legacyRuleKey, nativeRuleKey] of Object.entries(LEGACY_RULE_KEY_TO_N
|
|
|
37666
37566
|
}
|
|
37667
37567
|
const getLegacyRuleKeysForNative = (ruleKey) => NATIVE_RULE_KEY_TO_LEGACY_RULE_KEYS.get(ruleKey) ?? [];
|
|
37668
37568
|
const canonicalizeRuleKey = (ruleKey) => LEGACY_RULE_KEY_TO_NATIVE_RULE_KEY[ruleKey] ?? ruleKey;
|
|
37669
|
-
const
|
|
37569
|
+
const isReactDoctorShortIdOf = (bareRuleKey, qualifiedRuleKey) => !bareRuleKey.includes("/") && qualifiedRuleKey === `react-doctor/${bareRuleKey}`;
|
|
37570
|
+
const isSameRuleKey = (candidateRuleKey, targetRuleKey) => {
|
|
37571
|
+
const canonicalCandidate = canonicalizeRuleKey(candidateRuleKey);
|
|
37572
|
+
const canonicalTarget = canonicalizeRuleKey(targetRuleKey);
|
|
37573
|
+
if (canonicalCandidate === canonicalTarget) return true;
|
|
37574
|
+
return isReactDoctorShortIdOf(canonicalCandidate, canonicalTarget) || isReactDoctorShortIdOf(canonicalTarget, canonicalCandidate);
|
|
37575
|
+
};
|
|
37670
37576
|
const getEquivalentRuleKeys = (ruleKey) => {
|
|
37671
37577
|
const nativeRuleKey = canonicalizeRuleKey(ruleKey);
|
|
37672
37578
|
return [nativeRuleKey, ...getLegacyRuleKeysForNative(nativeRuleKey)];
|
|
@@ -37676,12 +37582,182 @@ const stripDescriptionTail = (ruleList) => {
|
|
|
37676
37582
|
if (!descriptionMatch || descriptionMatch.index === void 0) return ruleList;
|
|
37677
37583
|
return ruleList.slice(0, descriptionMatch.index);
|
|
37678
37584
|
};
|
|
37679
|
-
const
|
|
37585
|
+
const tokenizeRuleList = (ruleList) => {
|
|
37680
37586
|
const trimmed = ruleList?.trim();
|
|
37681
|
-
if (!trimmed) return
|
|
37587
|
+
if (!trimmed) return [];
|
|
37682
37588
|
const ruleSection = stripDescriptionTail(trimmed).trim();
|
|
37683
|
-
if (!ruleSection) return
|
|
37684
|
-
return ruleSection.split(/[,\s]+/).
|
|
37589
|
+
if (!ruleSection) return [];
|
|
37590
|
+
return ruleSection.split(/[,\s]+/).map((token) => token.trim()).filter(Boolean);
|
|
37591
|
+
};
|
|
37592
|
+
const FOREIGN_INLINE_DISABLE_PATTERN = /(?:\/\/|\/\*)[ \t]*(eslint|oxlint)-disable-(next-line|line)(?![\w-])([^\r\n]*)/;
|
|
37593
|
+
const FOREIGN_BLOCK_DISABLE_PATTERN = /\/\*[ \t]*(eslint|oxlint)-disable(?![\w-])([^*\r\n]*)/;
|
|
37594
|
+
const FOREIGN_BLOCK_ENABLE_PATTERN = /\/\*[ \t]*(?:eslint|oxlint)-enable(?![\w-])([^*\r\n]*)/;
|
|
37595
|
+
const buildHint = (tool, token, ruleId) => `oxlint matches plugin rules only by their full name, so \`${token}\` in your ${tool}-disable comment does not silence \`${ruleId}\` — change it to \`${ruleId}\`.`;
|
|
37596
|
+
const tokenMisnamesRule = (token, ruleId) => token !== ruleId && isSameRuleKey(token, ruleId);
|
|
37597
|
+
const detectInlineNearMiss = (lines, diagnosticLineIndex, ruleId) => {
|
|
37598
|
+
const candidates = [{
|
|
37599
|
+
line: lines[diagnosticLineIndex],
|
|
37600
|
+
requiredScope: "line"
|
|
37601
|
+
}, {
|
|
37602
|
+
line: lines[diagnosticLineIndex - 1],
|
|
37603
|
+
requiredScope: "next-line"
|
|
37604
|
+
}];
|
|
37605
|
+
for (const { line, requiredScope } of candidates) {
|
|
37606
|
+
const match = line?.match(FOREIGN_INLINE_DISABLE_PATTERN);
|
|
37607
|
+
if (!match) continue;
|
|
37608
|
+
const [, tool, scope, ruleList] = match;
|
|
37609
|
+
if (scope !== requiredScope) continue;
|
|
37610
|
+
const tokens = tokenizeRuleList(ruleList);
|
|
37611
|
+
if (tokens.includes(ruleId)) continue;
|
|
37612
|
+
for (const token of tokens) if (tokenMisnamesRule(token, ruleId)) return buildHint(tool, token, ruleId);
|
|
37613
|
+
}
|
|
37614
|
+
return null;
|
|
37615
|
+
};
|
|
37616
|
+
const detectBlockNearMiss = (lines, diagnosticLineIndex, ruleId) => {
|
|
37617
|
+
let openMisname = null;
|
|
37618
|
+
const lastLineIndex = Math.min(diagnosticLineIndex, lines.length - 1);
|
|
37619
|
+
for (let lineIndex = 0; lineIndex <= lastLineIndex; lineIndex++) {
|
|
37620
|
+
const line = lines[lineIndex];
|
|
37621
|
+
if (line === void 0 || !line.includes("-disable") && !line.includes("-enable")) continue;
|
|
37622
|
+
const disableMatch = line.match(FOREIGN_BLOCK_DISABLE_PATTERN);
|
|
37623
|
+
if (disableMatch) {
|
|
37624
|
+
const [, tool, ruleList] = disableMatch;
|
|
37625
|
+
const tokens = tokenizeRuleList(ruleList);
|
|
37626
|
+
if (tokens.includes(ruleId)) openMisname = null;
|
|
37627
|
+
else {
|
|
37628
|
+
const misnamed = tokens.find((token) => tokenMisnamesRule(token, ruleId));
|
|
37629
|
+
if (misnamed) openMisname = {
|
|
37630
|
+
tool,
|
|
37631
|
+
token: misnamed
|
|
37632
|
+
};
|
|
37633
|
+
}
|
|
37634
|
+
continue;
|
|
37635
|
+
}
|
|
37636
|
+
const enableMatch = line.match(FOREIGN_BLOCK_ENABLE_PATTERN);
|
|
37637
|
+
if (enableMatch) {
|
|
37638
|
+
const enabledRules = tokenizeRuleList(enableMatch[1]);
|
|
37639
|
+
if (enabledRules.length === 0 || enabledRules.some((rule) => isSameRuleKey(rule, ruleId))) openMisname = null;
|
|
37640
|
+
}
|
|
37641
|
+
}
|
|
37642
|
+
return openMisname ? buildHint(openMisname.tool, openMisname.token, ruleId) : null;
|
|
37643
|
+
};
|
|
37644
|
+
const detectForeignDisableNearMiss = (lines, diagnosticLineIndex, ruleId) => {
|
|
37645
|
+
if (!ruleId.startsWith("react-doctor/")) return null;
|
|
37646
|
+
return detectInlineNearMiss(lines, diagnosticLineIndex, ruleId) ?? detectBlockNearMiss(lines, diagnosticLineIndex, ruleId);
|
|
37647
|
+
};
|
|
37648
|
+
const JSX_OPENER_TAG_PATTERN = /<[A-Za-z][\w.]*/g;
|
|
37649
|
+
const JSX_TAG_NAME_FOLLOW = /[A-Za-z]/;
|
|
37650
|
+
const isOpenerMatchInsideLineComment = (line, openerCharIndex) => {
|
|
37651
|
+
let stringDelimiter = null;
|
|
37652
|
+
for (let charIndex = 0; charIndex < openerCharIndex; charIndex++) {
|
|
37653
|
+
const character = line[charIndex];
|
|
37654
|
+
if (stringDelimiter !== null) {
|
|
37655
|
+
if (character === "\\") {
|
|
37656
|
+
charIndex++;
|
|
37657
|
+
continue;
|
|
37658
|
+
}
|
|
37659
|
+
if (character === stringDelimiter) stringDelimiter = null;
|
|
37660
|
+
continue;
|
|
37661
|
+
}
|
|
37662
|
+
if (character === "\"" || character === "'" || character === "`") {
|
|
37663
|
+
stringDelimiter = character;
|
|
37664
|
+
continue;
|
|
37665
|
+
}
|
|
37666
|
+
if (character === "/" && line[charIndex + 1] === "/") return true;
|
|
37667
|
+
}
|
|
37668
|
+
return false;
|
|
37669
|
+
};
|
|
37670
|
+
const findOpenerTagOnLine = (line) => {
|
|
37671
|
+
for (const match of line.matchAll(JSX_OPENER_TAG_PATTERN)) {
|
|
37672
|
+
if (match.index === void 0) continue;
|
|
37673
|
+
if (!isOpenerMatchInsideLineComment(line, match.index)) return { startCharIndex: match.index + match[0].length };
|
|
37674
|
+
}
|
|
37675
|
+
return null;
|
|
37676
|
+
};
|
|
37677
|
+
const findJsxOpenerSpan = (lines, openerLineIndex) => {
|
|
37678
|
+
const openerLine = lines[openerLineIndex];
|
|
37679
|
+
if (openerLine === void 0) return null;
|
|
37680
|
+
const opener = findOpenerTagOnLine(openerLine);
|
|
37681
|
+
if (!opener) return null;
|
|
37682
|
+
const lookaheadLimit = Math.min(lines.length, openerLineIndex + 32);
|
|
37683
|
+
let braceDepth = 0;
|
|
37684
|
+
let innerAngleDepth = 0;
|
|
37685
|
+
let stringDelimiter = null;
|
|
37686
|
+
for (let lineIndex = openerLineIndex; lineIndex < lookaheadLimit; lineIndex++) {
|
|
37687
|
+
const currentLine = lines[lineIndex];
|
|
37688
|
+
const startCharForLine = lineIndex === openerLineIndex ? opener.startCharIndex : 0;
|
|
37689
|
+
for (let charIndex = startCharForLine; charIndex < currentLine.length; charIndex++) {
|
|
37690
|
+
const character = currentLine[charIndex];
|
|
37691
|
+
if (stringDelimiter !== null) {
|
|
37692
|
+
if (character === "\\") {
|
|
37693
|
+
charIndex++;
|
|
37694
|
+
continue;
|
|
37695
|
+
}
|
|
37696
|
+
if (character === stringDelimiter) stringDelimiter = null;
|
|
37697
|
+
continue;
|
|
37698
|
+
}
|
|
37699
|
+
if (character === "\"" || character === "'" || character === "`") {
|
|
37700
|
+
stringDelimiter = character;
|
|
37701
|
+
continue;
|
|
37702
|
+
}
|
|
37703
|
+
if (character === "{") {
|
|
37704
|
+
braceDepth++;
|
|
37705
|
+
continue;
|
|
37706
|
+
}
|
|
37707
|
+
if (character === "}") {
|
|
37708
|
+
braceDepth--;
|
|
37709
|
+
continue;
|
|
37710
|
+
}
|
|
37711
|
+
if (braceDepth !== 0) continue;
|
|
37712
|
+
if (character === "<") {
|
|
37713
|
+
const followCharacter = currentLine[charIndex + 1];
|
|
37714
|
+
if (followCharacter !== void 0 && JSX_TAG_NAME_FOLLOW.test(followCharacter)) innerAngleDepth++;
|
|
37715
|
+
continue;
|
|
37716
|
+
}
|
|
37717
|
+
if (character !== ">") continue;
|
|
37718
|
+
const previousCharacter = currentLine[charIndex - 1];
|
|
37719
|
+
const nextCharacter = currentLine[charIndex + 1];
|
|
37720
|
+
if (previousCharacter === "=" || nextCharacter === "=") continue;
|
|
37721
|
+
if (innerAngleDepth > 0) {
|
|
37722
|
+
innerAngleDepth--;
|
|
37723
|
+
continue;
|
|
37724
|
+
}
|
|
37725
|
+
return lineIndex;
|
|
37726
|
+
}
|
|
37727
|
+
}
|
|
37728
|
+
return null;
|
|
37729
|
+
};
|
|
37730
|
+
const findEnclosingMultilineJsxOpenerStart = (lines, diagnosticLineIndex) => {
|
|
37731
|
+
for (let candidateIndex = diagnosticLineIndex - 1; candidateIndex >= 0 && diagnosticLineIndex - candidateIndex <= 32; candidateIndex--) {
|
|
37732
|
+
const openerCloseIndex = findJsxOpenerSpan(lines, candidateIndex);
|
|
37733
|
+
if (openerCloseIndex !== null && openerCloseIndex >= diagnosticLineIndex) return candidateIndex;
|
|
37734
|
+
}
|
|
37735
|
+
return null;
|
|
37736
|
+
};
|
|
37737
|
+
const DISABLE_NEXT_LINE_PATTERN = /(?:\/\/|\/\*)\s*react-doctor-disable-next-line\b(?:\s+([^\r\n]*?))?\s*(?:\*\/)?\s*\}?\s*$/;
|
|
37738
|
+
const findStackedDisableCommentsAbove = (lines, anchorIndex) => {
|
|
37739
|
+
const collected = [];
|
|
37740
|
+
let isStillInChain = true;
|
|
37741
|
+
for (let candidateIndex = anchorIndex - 1; candidateIndex >= 0 && anchorIndex - candidateIndex <= 10; candidateIndex--) {
|
|
37742
|
+
const candidateLine = lines[candidateIndex];
|
|
37743
|
+
if (candidateLine === void 0) break;
|
|
37744
|
+
const match = candidateLine.match(DISABLE_NEXT_LINE_PATTERN);
|
|
37745
|
+
if (match) {
|
|
37746
|
+
collected.push({
|
|
37747
|
+
commentLineIndex: candidateIndex,
|
|
37748
|
+
ruleList: match[1],
|
|
37749
|
+
isInChain: isStillInChain
|
|
37750
|
+
});
|
|
37751
|
+
continue;
|
|
37752
|
+
}
|
|
37753
|
+
isStillInChain = false;
|
|
37754
|
+
}
|
|
37755
|
+
return collected;
|
|
37756
|
+
};
|
|
37757
|
+
const isRuleListedInComment = (ruleList, ruleId) => {
|
|
37758
|
+
const tokens = tokenizeRuleList(ruleList);
|
|
37759
|
+
if (tokens.length === 0) return true;
|
|
37760
|
+
return tokens.some((token) => isSameRuleKey(token, ruleId));
|
|
37685
37761
|
};
|
|
37686
37762
|
const DISABLE_LINE_PATTERN = /(?:\/\/|\/\*)\s*react-doctor-disable-line\b(?:\s+([^\r\n]*?))?\s*(?:\*\/)?\s*\}?\s*$/;
|
|
37687
37763
|
const formatLineGap = (gapLineCount) => `${gapLineCount} line${gapLineCount === 1 ? "" : "s"}`;
|
|
@@ -37725,7 +37801,7 @@ const evaluateSuppression = (lines, diagnosticLineIndex, ruleId) => {
|
|
|
37725
37801
|
};
|
|
37726
37802
|
return {
|
|
37727
37803
|
isSuppressed: false,
|
|
37728
|
-
nearMissHint: classifyFromComments([directComments, openerComments], diagnosticLineIndex, ruleId)
|
|
37804
|
+
nearMissHint: classifyFromComments([directComments, openerComments], diagnosticLineIndex, ruleId) ?? detectForeignDisableNearMiss(lines, diagnosticLineIndex, ruleId)
|
|
37729
37805
|
};
|
|
37730
37806
|
};
|
|
37731
37807
|
/**
|
|
@@ -38752,6 +38828,29 @@ const resolveScanTarget = async (requestedDirectory, options = {}) => {
|
|
|
38752
38828
|
didRedirectViaRootDir: redirectedDirectory !== null
|
|
38753
38829
|
};
|
|
38754
38830
|
};
|
|
38831
|
+
const buildFixGroupId = (diagnostic) => createHash("sha1").update(JSON.stringify([
|
|
38832
|
+
diagnostic.filePath,
|
|
38833
|
+
`${diagnostic.plugin}/${diagnostic.rule}`,
|
|
38834
|
+
diagnostic.message
|
|
38835
|
+
])).digest("hex").slice(0, 16);
|
|
38836
|
+
const isGroupableRule = (diagnostic) => ROOT_CAUSE_GROUPABLE_RULE_KEYS.has(`${diagnostic.plugin}/${diagnostic.rule}`);
|
|
38837
|
+
const assignFixGroups = (diagnostics) => {
|
|
38838
|
+
const siteCountByGroupId = /* @__PURE__ */ new Map();
|
|
38839
|
+
for (const diagnostic of diagnostics) {
|
|
38840
|
+
if (!isGroupableRule(diagnostic)) continue;
|
|
38841
|
+
const groupId = buildFixGroupId(diagnostic);
|
|
38842
|
+
siteCountByGroupId.set(groupId, (siteCountByGroupId.get(groupId) ?? 0) + 1);
|
|
38843
|
+
}
|
|
38844
|
+
return diagnostics.map((diagnostic) => {
|
|
38845
|
+
if (!isGroupableRule(diagnostic)) return diagnostic;
|
|
38846
|
+
const groupId = buildFixGroupId(diagnostic);
|
|
38847
|
+
if ((siteCountByGroupId.get(groupId) ?? 0) < 2) return diagnostic;
|
|
38848
|
+
return {
|
|
38849
|
+
...diagnostic,
|
|
38850
|
+
fixGroupId: groupId
|
|
38851
|
+
};
|
|
38852
|
+
});
|
|
38853
|
+
};
|
|
38755
38854
|
const getDirectDependencyNames = (packageJson) => new Set([...Object.keys(packageJson.dependencies ?? {}), ...Object.keys(packageJson.devDependencies ?? {})]);
|
|
38756
38855
|
const buildExpoCheckContext = (rootDirectory, expoVersion) => {
|
|
38757
38856
|
const packageJson = readPackageJson$1(Path.join(rootDirectory, "package.json"));
|
|
@@ -43214,12 +43313,12 @@ const runInspect = (input, hooks = {}) => gen(function* () {
|
|
|
43214
43313
|
else if (input.suppressScanSummary) yield* scanProgress.stop();
|
|
43215
43314
|
else yield* scanProgress.succeed(`Scanned ${scannedFilesLabel} in ${scanElapsedSeconds}s${workerCountSuffix}`);
|
|
43216
43315
|
yield* reporterService.finalize;
|
|
43217
|
-
const finalDiagnostics = [
|
|
43316
|
+
const finalDiagnostics = assignFixGroups([
|
|
43218
43317
|
...envCollected,
|
|
43219
43318
|
...supplyChainCollected,
|
|
43220
43319
|
...lintCollected,
|
|
43221
43320
|
...deadCodeCollected
|
|
43222
|
-
];
|
|
43321
|
+
]);
|
|
43223
43322
|
const githubViewerPermission = yield* join(githubViewerPermissionFiber);
|
|
43224
43323
|
const scoreMetadata = {
|
|
43225
43324
|
...repo !== null ? { repo } : {},
|
|
@@ -43589,6 +43688,7 @@ const buildJsonReport = (input) => {
|
|
|
43589
43688
|
score: result.score,
|
|
43590
43689
|
skippedChecks: result.skippedChecks,
|
|
43591
43690
|
...result.skippedCheckReasons ? { skippedCheckReasons: result.skippedCheckReasons } : {},
|
|
43691
|
+
...typeof result.scannedFileCount === "number" ? { scannedFileCount: result.scannedFileCount } : {},
|
|
43592
43692
|
elapsedMilliseconds: result.elapsedMilliseconds
|
|
43593
43693
|
}));
|
|
43594
43694
|
const flattenedDiagnostics = projects.flatMap((entry) => entry.diagnostics);
|
|
@@ -43924,6 +44024,17 @@ const detectTerminalKind = (env = process.env) => {
|
|
|
43924
44024
|
return "unknown";
|
|
43925
44025
|
};
|
|
43926
44026
|
//#endregion
|
|
44027
|
+
//#region src/cli/utils/is-debug-flag.ts
|
|
44028
|
+
/**
|
|
44029
|
+
* Whether the user passed `--debug` (surface the run's Sentry trace id, and
|
|
44030
|
+
* force performance tracing on so there's a trace to surface). Read straight
|
|
44031
|
+
* from argv rather than Commander's parsed flags because `initializeSentry()`
|
|
44032
|
+
* runs before Commander parses — the same reason `shouldEnableSentry()` reads
|
|
44033
|
+
* `--no-score` from argv. Sharing this one reader keeps the init-time sampling
|
|
44034
|
+
* override and the end-of-run print in agreement.
|
|
44035
|
+
*/
|
|
44036
|
+
const isDebugFlagEnabled = (argv = process.argv) => argv.includes("--debug");
|
|
44037
|
+
//#endregion
|
|
43927
44038
|
//#region src/cli/utils/is-git-hook-environment.ts
|
|
43928
44039
|
const isGitHookEnvironment = () => Boolean(process.env.GIT_DIR);
|
|
43929
44040
|
//#endregion
|
|
@@ -44031,7 +44142,7 @@ const makeNoopConsole = () => ({
|
|
|
44031
44142
|
});
|
|
44032
44143
|
//#endregion
|
|
44033
44144
|
//#region src/cli/utils/version.ts
|
|
44034
|
-
const VERSION = "0.5.6-dev.
|
|
44145
|
+
const VERSION = "0.5.6-dev.66133f8";
|
|
44035
44146
|
//#endregion
|
|
44036
44147
|
//#region src/cli/utils/json-mode.ts
|
|
44037
44148
|
let context = null;
|
|
@@ -44183,6 +44294,7 @@ const buildRunContext = () => {
|
|
|
44183
44294
|
interactive: !isNonInteractiveEnvironment(),
|
|
44184
44295
|
terminalKind: detectTerminalKind(),
|
|
44185
44296
|
jsonMode: isJsonModeActive(),
|
|
44297
|
+
debug: isDebugFlagEnabled(),
|
|
44186
44298
|
invokedVia: detectInvokedVia()
|
|
44187
44299
|
};
|
|
44188
44300
|
};
|
|
@@ -44254,6 +44366,7 @@ const buildSentryScope = (runContext = buildRunContext()) => {
|
|
|
44254
44366
|
interactive: runContext.interactive,
|
|
44255
44367
|
terminalKind: runContext.terminalKind,
|
|
44256
44368
|
jsonMode: runContext.jsonMode,
|
|
44369
|
+
debug: runContext.debug,
|
|
44257
44370
|
invokedVia: runContext.invokedVia,
|
|
44258
44371
|
nodeMajor: runContext.nodeMajor
|
|
44259
44372
|
};
|
|
@@ -44391,13 +44504,13 @@ const isDevVersion = (version) => version === "0.0.0" || version.includes("-");
|
|
|
44391
44504
|
* uploads source-map artifacts under, so stack frames symbolicate. Honors the
|
|
44392
44505
|
* standard `SENTRY_RELEASE` override.
|
|
44393
44506
|
*/
|
|
44394
|
-
const resolveSentryRelease = () => process.env.SENTRY_RELEASE || `react-doctor@0.5.6-dev.
|
|
44507
|
+
const resolveSentryRelease = () => process.env.SENTRY_RELEASE || `react-doctor@0.5.6-dev.66133f8`;
|
|
44395
44508
|
/**
|
|
44396
44509
|
* Deployment environment shown in Sentry's environment filter. Defaults to
|
|
44397
44510
|
* `production` for tagged releases and `development` for dev/unbuilt versions,
|
|
44398
44511
|
* overridable via the standard `SENTRY_ENVIRONMENT` env var.
|
|
44399
44512
|
*/
|
|
44400
|
-
const resolveSentryEnvironment = () => process.env.SENTRY_ENVIRONMENT || (isDevVersion("0.5.6-dev.
|
|
44513
|
+
const resolveSentryEnvironment = () => process.env.SENTRY_ENVIRONMENT || (isDevVersion("0.5.6-dev.66133f8") ? "development" : "production");
|
|
44401
44514
|
/**
|
|
44402
44515
|
* Performance-tracing sample rate in `[0, 1]`. Reads `SENTRY_TRACES_SAMPLE_RATE`
|
|
44403
44516
|
* (set to `0` to disable tracing) and falls back to
|
|
@@ -44461,7 +44574,7 @@ const flushSentry = async () => {
|
|
|
44461
44574
|
const initializeSentry = () => {
|
|
44462
44575
|
if (isInitialized || !shouldEnableSentry()) return;
|
|
44463
44576
|
isInitialized = true;
|
|
44464
|
-
resolvedTracesSampleRate = resolveTracesSampleRate();
|
|
44577
|
+
resolvedTracesSampleRate = isDebugFlagEnabled() ? 1 : resolveTracesSampleRate();
|
|
44465
44578
|
const { tags, contexts } = buildSentryScope();
|
|
44466
44579
|
Sentry.init({
|
|
44467
44580
|
dsn: process.env.SENTRY_DSN || "https://f253d570240a59b8dbd77b7a548ef133@o4510226365743104.ingest.us.sentry.io/4511487817809920",
|
|
@@ -47677,6 +47790,11 @@ const setActiveRunTrace = (trace) => {
|
|
|
47677
47790
|
activeRunTrace = trace;
|
|
47678
47791
|
};
|
|
47679
47792
|
const getActiveRunTrace = () => activeRunTrace;
|
|
47793
|
+
let lastRunTraceId = null;
|
|
47794
|
+
const recordRunTraceId = (traceId) => {
|
|
47795
|
+
lastRunTraceId = traceId;
|
|
47796
|
+
};
|
|
47797
|
+
const getLastRunTraceId = () => lastRunTraceId;
|
|
47680
47798
|
//#endregion
|
|
47681
47799
|
//#region src/cli/utils/to-span-attributes.ts
|
|
47682
47800
|
/**
|
|
@@ -47739,14 +47857,13 @@ const withSentryRunSpan = (run, options = {}) => {
|
|
|
47739
47857
|
op: "cli.inspect",
|
|
47740
47858
|
attributes: toSpanAttributes(tags)
|
|
47741
47859
|
}, (rootSpan) => {
|
|
47742
|
-
|
|
47743
|
-
|
|
47744
|
-
|
|
47745
|
-
|
|
47746
|
-
|
|
47747
|
-
|
|
47748
|
-
|
|
47749
|
-
}
|
|
47860
|
+
const spanContext = rootSpan.spanContext();
|
|
47861
|
+
recordRunTraceId(spanContext.traceId);
|
|
47862
|
+
if (options.concurrentScan !== true) setActiveRunTrace({
|
|
47863
|
+
traceId: spanContext.traceId,
|
|
47864
|
+
spanId: spanContext.spanId,
|
|
47865
|
+
sampled: (spanContext.traceFlags & 1) === 1
|
|
47866
|
+
});
|
|
47750
47867
|
return run(rootSpan);
|
|
47751
47868
|
});
|
|
47752
47869
|
};
|
|
@@ -47886,6 +48003,42 @@ const recordScanMetrics = (input) => {
|
|
|
47886
48003
|
});
|
|
47887
48004
|
};
|
|
47888
48005
|
//#endregion
|
|
48006
|
+
//#region src/cli/utils/diagnostic-grouping.ts
|
|
48007
|
+
const buildRulePriorityMap = (scores) => {
|
|
48008
|
+
const rulePriority = /* @__PURE__ */ new Map();
|
|
48009
|
+
for (const score of scores) {
|
|
48010
|
+
if (!score?.rules) continue;
|
|
48011
|
+
for (const [ruleKey, info] of Object.entries(score.rules)) if (typeof info.priority === "number") rulePriority.set(ruleKey, info.priority);
|
|
48012
|
+
}
|
|
48013
|
+
return rulePriority;
|
|
48014
|
+
};
|
|
48015
|
+
const compareByRulePriority = (ruleKeyA, ruleKeyB, rulePriority) => {
|
|
48016
|
+
const priorityA = rulePriority?.get(ruleKeyA);
|
|
48017
|
+
const priorityB = rulePriority?.get(ruleKeyB);
|
|
48018
|
+
if (priorityA === void 0 && priorityB === void 0) return 0;
|
|
48019
|
+
if (priorityA === void 0) return 1;
|
|
48020
|
+
if (priorityB === void 0) return -1;
|
|
48021
|
+
return priorityB - priorityA;
|
|
48022
|
+
};
|
|
48023
|
+
const sortRuleGroupsByImportance = (diagnosticGroups, rulePriority) => diagnosticGroups.toSorted(([ruleKeyA], [ruleKeyB]) => compareByRulePriority(ruleKeyA, ruleKeyB, rulePriority));
|
|
48024
|
+
const buildSortedRuleGroups = (diagnostics, rulePriority) => sortRuleGroupsByImportance([...groupBy([...diagnostics], (diagnostic) => `${diagnostic.plugin}/${diagnostic.rule}`)], rulePriority);
|
|
48025
|
+
const getSharedFixSiteCount = (diagnostics) => {
|
|
48026
|
+
if (diagnostics.length < 2) return 0;
|
|
48027
|
+
const firstFixGroupId = diagnostics[0]?.fixGroupId;
|
|
48028
|
+
if (!firstFixGroupId) return 0;
|
|
48029
|
+
return diagnostics.every((diagnostic) => diagnostic.fixGroupId === firstFixGroupId) ? diagnostics.length : 0;
|
|
48030
|
+
};
|
|
48031
|
+
const FIX_RECIPE_DIRECTIVE_LABEL = "Curl with no cache & follow the canonical fix and false positive check recipe before fixing";
|
|
48032
|
+
const formatFixRecipeLine = (diagnostic) => hasPublishedFixRecipe(diagnostic) ? `${FIX_RECIPE_DIRECTIVE_LABEL}: ${buildRuleDocsUrl(diagnostic.plugin, diagnostic.rule)}` : null;
|
|
48033
|
+
const formatLearnMoreLine = (diagnostic) => hasPublishedFixRecipe(diagnostic) ? `Learn more: ${buildRuleDocsUrl(diagnostic.plugin, diagnostic.rule)}` : null;
|
|
48034
|
+
const buildRuleBlastRadii = (diagnostics) => buildSortedRuleGroups(diagnostics).map(([ruleKey, ruleDiagnostics]) => ({
|
|
48035
|
+
ruleKey,
|
|
48036
|
+
title: ruleDiagnostics[0].title ?? ruleKey,
|
|
48037
|
+
siteCount: ruleDiagnostics.length,
|
|
48038
|
+
fileCount: new Set(ruleDiagnostics.map((diagnostic) => diagnostic.filePath)).size
|
|
48039
|
+
})).toSorted((left, right) => right.fileCount - left.fileCount);
|
|
48040
|
+
const findMigrationScaleBuckets = (diagnostics) => buildRuleBlastRadii(diagnostics).filter((bucket) => bucket.fileCount >= 40);
|
|
48041
|
+
//#endregion
|
|
47889
48042
|
//#region src/cli/utils/cli-logger.ts
|
|
47890
48043
|
/**
|
|
47891
48044
|
* Thin synchronous façade over Effect's `Console` module. Used by
|
|
@@ -48002,12 +48155,17 @@ const buildOutcomeAttributes = (input) => {
|
|
|
48002
48155
|
topRule = rule;
|
|
48003
48156
|
topRuleCount = count;
|
|
48004
48157
|
}
|
|
48158
|
+
const largestRuleBucket = buildRuleBlastRadii(result.diagnostics)[0] ?? null;
|
|
48005
48159
|
let diagnosticsInTestFiles = 0;
|
|
48006
48160
|
let diagnosticsInStoryFiles = 0;
|
|
48161
|
+
const findingsPerFixGroup = /* @__PURE__ */ new Map();
|
|
48007
48162
|
for (const diagnostic of result.diagnostics) {
|
|
48008
48163
|
if (diagnostic.fileContext === "test") diagnosticsInTestFiles += 1;
|
|
48009
48164
|
if (diagnostic.fileContext === "story") diagnosticsInStoryFiles += 1;
|
|
48165
|
+
if (diagnostic.fixGroupId) findingsPerFixGroup.set(diagnostic.fixGroupId, (findingsPerFixGroup.get(diagnostic.fixGroupId) ?? 0) + 1);
|
|
48010
48166
|
}
|
|
48167
|
+
let fixGroupedFindings = 0;
|
|
48168
|
+
for (const count of findingsPerFixGroup.values()) fixGroupedFindings += count;
|
|
48011
48169
|
const attributes = {
|
|
48012
48170
|
outcome,
|
|
48013
48171
|
exitCode: wouldBlock ? 1 : 0,
|
|
@@ -48021,7 +48179,12 @@ const buildOutcomeAttributes = (input) => {
|
|
|
48021
48179
|
diagnosticsInTestFiles,
|
|
48022
48180
|
diagnosticsInStoryFiles,
|
|
48023
48181
|
distinctRulesFired: countByRule.size,
|
|
48182
|
+
"diag.fixGroups": findingsPerFixGroup.size,
|
|
48183
|
+
"diag.fixGroupedFindings": fixGroupedFindings,
|
|
48024
48184
|
topRule,
|
|
48185
|
+
"migration.largestRuleBucketFiles": largestRuleBucket ? largestRuleBucket.fileCount : null,
|
|
48186
|
+
"migration.largestRuleBucketSites": largestRuleBucket ? largestRuleBucket.siteCount : null,
|
|
48187
|
+
"migration.largestRuleBucketRule": largestRuleBucket ? largestRuleBucket.ruleKey : null,
|
|
48025
48188
|
scannedFileCount: result.scannedFileCount ?? null,
|
|
48026
48189
|
elapsedMs: result.elapsedMilliseconds,
|
|
48027
48190
|
scanPhaseMs: result.scanElapsedMilliseconds ?? null,
|
|
@@ -48182,6 +48345,7 @@ const AGENT_GUIDANCE_LINES = [
|
|
|
48182
48345
|
"Run `npx react-doctor@latest --verbose --scope changed` before and after changes, plus relevant tests after each focused batch.",
|
|
48183
48346
|
"When available, spawn subagents or isolated worktrees for independent rule families, then review and merge only the best safe fixes.",
|
|
48184
48347
|
"Split unrelated, broad, or behavior-changing work into separate PRs/branches instead of one large cleanup.",
|
|
48348
|
+
"When one rule spans dozens of files (a migration-scale change), fix a representative sample first, confirm the recipe holds, and get the code owner's sign-off before changing the rest. Don't mass-fix a broad pattern in one unreviewed pass.",
|
|
48185
48349
|
"For confirmed issues that cannot be fixed now, create GitHub issues with the rule, file/line, confidence, impact, and proposed fix.",
|
|
48186
48350
|
"If a fix needs an API, UX, or architecture decision, stop and ask before editing."
|
|
48187
48351
|
];
|
|
@@ -48191,29 +48355,6 @@ const printAgentGuidance = () => gen(function* () {
|
|
|
48191
48355
|
yield* log("");
|
|
48192
48356
|
});
|
|
48193
48357
|
//#endregion
|
|
48194
|
-
//#region src/cli/utils/diagnostic-grouping.ts
|
|
48195
|
-
const buildRulePriorityMap = (scores) => {
|
|
48196
|
-
const rulePriority = /* @__PURE__ */ new Map();
|
|
48197
|
-
for (const score of scores) {
|
|
48198
|
-
if (!score?.rules) continue;
|
|
48199
|
-
for (const [ruleKey, info] of Object.entries(score.rules)) if (typeof info.priority === "number") rulePriority.set(ruleKey, info.priority);
|
|
48200
|
-
}
|
|
48201
|
-
return rulePriority;
|
|
48202
|
-
};
|
|
48203
|
-
const compareByRulePriority = (ruleKeyA, ruleKeyB, rulePriority) => {
|
|
48204
|
-
const priorityA = rulePriority?.get(ruleKeyA);
|
|
48205
|
-
const priorityB = rulePriority?.get(ruleKeyB);
|
|
48206
|
-
if (priorityA === void 0 && priorityB === void 0) return 0;
|
|
48207
|
-
if (priorityA === void 0) return 1;
|
|
48208
|
-
if (priorityB === void 0) return -1;
|
|
48209
|
-
return priorityB - priorityA;
|
|
48210
|
-
};
|
|
48211
|
-
const sortRuleGroupsByImportance = (diagnosticGroups, rulePriority) => diagnosticGroups.toSorted(([ruleKeyA], [ruleKeyB]) => compareByRulePriority(ruleKeyA, ruleKeyB, rulePriority));
|
|
48212
|
-
const buildSortedRuleGroups = (diagnostics, rulePriority) => sortRuleGroupsByImportance([...groupBy([...diagnostics], (diagnostic) => `${diagnostic.plugin}/${diagnostic.rule}`)], rulePriority);
|
|
48213
|
-
const FIX_RECIPE_DIRECTIVE_LABEL = "Curl with no cache & follow the canonical fix and false positive check recipe before fixing";
|
|
48214
|
-
const formatFixRecipeLine = (diagnostic) => hasPublishedFixRecipe(diagnostic) ? `${FIX_RECIPE_DIRECTIVE_LABEL}: ${buildRuleDocsUrl(diagnostic.plugin, diagnostic.rule)}` : null;
|
|
48215
|
-
const formatLearnMoreLine = (diagnostic) => hasPublishedFixRecipe(diagnostic) ? `Learn more: ${buildRuleDocsUrl(diagnostic.plugin, diagnostic.rule)}` : null;
|
|
48216
|
-
//#endregion
|
|
48217
48358
|
//#region src/cli/utils/box-text.ts
|
|
48218
48359
|
const ESCAPE = String.fromCharCode(27);
|
|
48219
48360
|
const ANSI_ESCAPE_PATTERN = new RegExp(`${ESCAPE}\\[[0-9;]*m`, "g");
|
|
@@ -48525,6 +48666,8 @@ const buildRuleDetailBlock = (ruleKey, ruleDiagnostics, resolveSourceRoot, rende
|
|
|
48525
48666
|
const impactMessages = isCollapsedWarningGroup ? [...new Set(ruleDiagnostics.map((diagnostic) => diagnostic.message))] : [representative.message];
|
|
48526
48667
|
for (const impactMessage of impactMessages) for (const explanationLine of wrapTextToWidth(impactMessage, resolveMeasureWidth(4), { breakLongWords: false })) lines.push(`${TOP_ERROR_DETAIL_INDENT}${explanationLine}`);
|
|
48527
48668
|
if (representative.help) for (const fixLine of wrapTextToWidth(`→ ${representative.help}`, resolveMeasureWidth(4), { breakLongWords: false })) lines.push(highlighter.dim(`${TOP_ERROR_DETAIL_INDENT}${fixLine}`));
|
|
48669
|
+
const sharedFixSiteCount = getSharedFixSiteCount(ruleDiagnostics);
|
|
48670
|
+
if (sharedFixSiteCount > 0) lines.push(highlighter.dim(`${TOP_ERROR_DETAIL_INDENT}↳ One fix clears all ${sharedFixSiteCount} findings.`));
|
|
48528
48671
|
if (renderEverySite && isAgentEnvironment) {
|
|
48529
48672
|
const fixRecipeLine = formatFixRecipeLine(representative);
|
|
48530
48673
|
if (fixRecipeLine) lines.push(highlighter.gray(`${TOP_ERROR_DETAIL_INDENT}${fixRecipeLine}`));
|
|
@@ -48543,6 +48686,19 @@ const buildOverflowSummaryLine = (diagnostics, rulePriority) => {
|
|
|
48543
48686
|
const command = highlighter.bold(highlighter.info("npx react-doctor@latest --verbose"));
|
|
48544
48687
|
return ` ${highlighter.dim("Run")} ${command} ${highlighter.dim("to list every error and warning")}`;
|
|
48545
48688
|
};
|
|
48689
|
+
const formatMigrationBucketLine = (bucket) => `${TOP_ERROR_DETAIL_INDENT}${bucket.title} ${highlighter.gray(`×${bucket.siteCount} across ${bucket.fileCount} files`)}`;
|
|
48690
|
+
const buildMigrationScaleAdvisoryLines = (diagnostics) => {
|
|
48691
|
+
const buckets = findMigrationScaleBuckets(diagnostics);
|
|
48692
|
+
if (buckets.length === 0) return [];
|
|
48693
|
+
const shownBuckets = buckets.slice(0, 3);
|
|
48694
|
+
const lines = [` ${highlighter.warn("⚠")} ${highlighter.bold("Migration-scale change")}${highlighter.dim(": sample before you sweep")}`, ...shownBuckets.map(formatMigrationBucketLine)];
|
|
48695
|
+
const remainingBuckets = buckets.length - shownBuckets.length;
|
|
48696
|
+
if (remainingBuckets > 0) lines.push(highlighter.gray(`${TOP_ERROR_DETAIL_INDENT}+${remainingBuckets} more ${remainingBuckets === 1 ? "rule" : "rules"} at this scale`));
|
|
48697
|
+
for (const guidanceLine of wrapTextToWidth("Fixing all of them at once is hard to review and prone to subtle mistakes across the whole repo. Fix a representative few first and confirm the recipe holds. Then get the code owner's sign-off before changing the rest.", resolveMeasureWidth(4), { breakLongWords: false })) lines.push(highlighter.dim(`${TOP_ERROR_DETAIL_INDENT}${guidanceLine}`));
|
|
48698
|
+
const command = highlighter.info("npx react-doctor@latest <path>");
|
|
48699
|
+
lines.push(`${TOP_ERROR_DETAIL_INDENT}${highlighter.dim("Scope it down one area at a time:")} ${command}`);
|
|
48700
|
+
return lines;
|
|
48701
|
+
};
|
|
48546
48702
|
const getTopErrorRuleKeys = (diagnostics, limit, rulePriority) => new Set(selectTopErrorRuleGroups(diagnostics, limit, rulePriority).map(([ruleKey]) => ruleKey));
|
|
48547
48703
|
const buildTopErrorsSection = (diagnostics, resolveSourceRoot, hyperlinks, rulePriority) => {
|
|
48548
48704
|
const topRuleGroups = selectErrorRuleGroups(diagnostics, rulePriority).slice(0, 3);
|
|
@@ -48609,7 +48765,7 @@ const printDiagnostics = (diagnostics, isVerbose, sourceRoot, rulePriority, isAg
|
|
|
48609
48765
|
const categoryTallies = buildCategoryDiagnosticGroups(diagnostics, rulePriority).map(buildCategoryTally);
|
|
48610
48766
|
const categoryLines = buildCategoryTallyLines(categoryTallies);
|
|
48611
48767
|
const overviewDividerLines = detailLines.length > 0 && categoryLines.length > 0 ? [buildSectionDivider()] : [];
|
|
48612
|
-
const { lines, sectionStarts } = joinSections(detailLines, overviewDividerLines, buildOverviewHeaderLines(diagnostics), categoryLines, overflowLine ? [overflowLine] : []);
|
|
48768
|
+
const { lines, sectionStarts } = joinSections(detailLines, overviewDividerLines, buildOverviewHeaderLines(diagnostics), categoryLines, overflowLine ? [overflowLine] : [], buildMigrationScaleAdvisoryLines(diagnostics));
|
|
48613
48769
|
const [detailStart, , , categoryStart] = sectionStarts;
|
|
48614
48770
|
const pauseBeforeLineIndices = detailStart == null ? /* @__PURE__ */ new Set() : new Set(topErrorBlockOffsets.map((offset) => detailStart + offset));
|
|
48615
48771
|
let lineIndex = 0;
|
|
@@ -50075,6 +50231,7 @@ const isExpectedUserError = (error) => error instanceof CliInputError || isProje
|
|
|
50075
50231
|
//#region src/cli/utils/build-handoff-payload.ts
|
|
50076
50232
|
const buildHandoffPayload = (input) => {
|
|
50077
50233
|
const topGroups = buildSortedRuleGroups(input.diagnostics).slice(0, 3);
|
|
50234
|
+
const migrationScaleBuckets = new Map(findMigrationScaleBuckets(input.diagnostics).map((bucket) => [bucket.ruleKey, bucket]));
|
|
50078
50235
|
let outputDirectory = null;
|
|
50079
50236
|
try {
|
|
50080
50237
|
outputDirectory = writeDiagnosticsDirectory([...input.diagnostics], input.outputDirectory);
|
|
@@ -50083,7 +50240,9 @@ const buildHandoffPayload = (input) => {
|
|
|
50083
50240
|
topGroups.forEach(([ruleKey, ruleDiagnostics], index) => {
|
|
50084
50241
|
const representative = ruleDiagnostics[0];
|
|
50085
50242
|
const severityLabel = representative.severity === "error" ? "ERROR" : "WARN";
|
|
50086
|
-
|
|
50243
|
+
const sharedFixSiteCount = getSharedFixSiteCount(ruleDiagnostics);
|
|
50244
|
+
const countBadge = sharedFixSiteCount > 0 ? `one fix · ${sharedFixSiteCount} sites` : `×${ruleDiagnostics.length}`;
|
|
50245
|
+
lines.push(`${index + 1}. ${severityLabel} ${representative.category}: ${representative.title ?? ruleKey} (${countBadge})`, ` ${representative.message}`);
|
|
50087
50246
|
const fixRecipeLine = formatFixRecipeLine(representative);
|
|
50088
50247
|
if (fixRecipeLine) lines.push(` ${fixRecipeLine}`);
|
|
50089
50248
|
const uniqueFiles = [...new Set(ruleDiagnostics.map((diagnostic) => diagnostic.filePath))];
|
|
@@ -50093,10 +50252,19 @@ const buildHandoffPayload = (input) => {
|
|
|
50093
50252
|
}
|
|
50094
50253
|
const remainingFiles = uniqueFiles.length - 3;
|
|
50095
50254
|
if (remainingFiles > 0) lines.push(` - +${remainingFiles} more files`);
|
|
50255
|
+
const migrationBucket = migrationScaleBuckets.get(ruleKey);
|
|
50256
|
+
if (migrationBucket) lines.push(` Migration-scale (${migrationBucket.fileCount} files): fix a representative sample, confirm the recipe holds, and get the code owner's sign-off before changing the rest in one pass.`);
|
|
50096
50257
|
});
|
|
50097
50258
|
lines.push("");
|
|
50098
50259
|
if (outputDirectory) lines.push(`Full results for all ${input.diagnostics.length} issues (diagnostics.json + a .txt per rule): ${outputDirectory}`, "");
|
|
50099
|
-
lines.push("Read each file and fix the root cause — don't suppress or silence the rule.", "", "Verify against the real thing, don't assume: confirm each change matches the canonical fix recipe you fetched for that rule, then re-run `npx react-doctor@latest --verbose` and check the issue is actually gone against the real tool before moving on.", "", "Teach me as you go: for every issue you touch, explain it in plain language (no jargon) — what the problem is, why it's a problem, and how serious it is in human terms. Describe the real-world impact and severity concretely (e.g. \"this crashes the page for users on Safari\" vs. \"this is a minor cleanup with no user impact\") so I understand why it matters, not just what changed.", ""
|
|
50260
|
+
lines.push("Read each file and fix the root cause — don't suppress or silence the rule.", "", "Findings that share a `fixGroupId` (in diagnostics.json) are one root cause — a single fix clears all of them, so treat each `fixGroupId` as ONE task, not one per site.", "", "Verify against the real thing, don't assume: confirm each change matches the canonical fix recipe you fetched for that rule, then re-run `npx react-doctor@latest --verbose` and check the issue is actually gone against the real tool before moving on.", "", "Teach me as you go: for every issue you touch, explain it in plain language (no jargon) — what the problem is, why it's a problem, and how serious it is in human terms. Describe the real-world impact and severity concretely (e.g. \"this crashes the page for users on Safari\" vs. \"this is a minor cleanup with no user impact\") so I understand why it matters, not just what changed.", "");
|
|
50261
|
+
const shownRuleKeys = new Set(topGroups.map(([ruleKey]) => ruleKey));
|
|
50262
|
+
const deferredMigrationBuckets = [...migrationScaleBuckets.values()].filter((bucket) => !shownRuleKeys.has(bucket.ruleKey));
|
|
50263
|
+
if (deferredMigrationBuckets.length > 0) {
|
|
50264
|
+
const ruleSummaries = deferredMigrationBuckets.map((bucket) => `${bucket.title} (${bucket.fileCount} files)`).join(", ");
|
|
50265
|
+
lines.push(`Some of the rest are migration-scale (span dozens of files): ${ruleSummaries}. For each, fix a representative sample, confirm the recipe holds, and get the code owner's sign-off before changing the rest in one pass.`, "");
|
|
50266
|
+
}
|
|
50267
|
+
lines.push("Then work through the rest from the full results above.");
|
|
50100
50268
|
return lines.join("\n");
|
|
50101
50269
|
};
|
|
50102
50270
|
//#endregion
|
|
@@ -52131,6 +52299,7 @@ const reportErrorToSentry = async (error) => {
|
|
|
52131
52299
|
sampled: runTrace.sampled,
|
|
52132
52300
|
sampleRand: Math.random()
|
|
52133
52301
|
});
|
|
52302
|
+
recordRunTraceId(scope.getPropagationContext().traceId);
|
|
52134
52303
|
return Sentry.captureException(error);
|
|
52135
52304
|
});
|
|
52136
52305
|
await Sentry.flush(SENTRY_FLUSH_TIMEOUT_MS);
|
|
@@ -52745,6 +52914,10 @@ const validateModeFlags = (flags) => {
|
|
|
52745
52914
|
if (flags.staged && (flags.scope === "full" || flags.scope === "changed")) throw new CliInputError(`Cannot combine --staged with --scope ${flags.scope}; use --scope files or --scope lines, or drop --scope.`);
|
|
52746
52915
|
if (flags.score && flags.json) throw new CliInputError("Cannot combine --score and --json; pick one output mode.");
|
|
52747
52916
|
if (flags.score && flags.telemetry === false) throw new CliInputError("Cannot combine --score with --no-telemetry; --score prints the score that --no-telemetry disables.");
|
|
52917
|
+
if (flags.debug && (flags.score === false || flags.telemetry === false)) {
|
|
52918
|
+
const disablingFlag = flags.score === false ? "--no-score" : "--no-telemetry";
|
|
52919
|
+
throw new CliInputError(`Cannot combine --debug with ${disablingFlag}; ${disablingFlag} disables the Sentry reporting --debug needs to capture a trace.`);
|
|
52920
|
+
}
|
|
52748
52921
|
};
|
|
52749
52922
|
//#endregion
|
|
52750
52923
|
//#region src/cli/commands/inspect.ts
|
|
@@ -53098,6 +53271,7 @@ const inspectAction = async (directory, flags) => {
|
|
|
53098
53271
|
} catch (error) {
|
|
53099
53272
|
const isUserError = isExpectedUserError(error);
|
|
53100
53273
|
const sentryEventId = isUserError ? void 0 : await reportErrorToSentry(error);
|
|
53274
|
+
if (isDebugFlagEnabled()) await flushSentry();
|
|
53101
53275
|
if (isJsonMode) {
|
|
53102
53276
|
writeJsonErrorReport(error, sentryEventId);
|
|
53103
53277
|
process.exitCode = 1;
|
|
@@ -53820,6 +53994,33 @@ const normalizeHelpInvocation = (argv, knownCommands) => {
|
|
|
53820
53994
|
return [...nodeArguments, "--help"];
|
|
53821
53995
|
};
|
|
53822
53996
|
//#endregion
|
|
53997
|
+
//#region src/cli/utils/print-debug-trace.ts
|
|
53998
|
+
/**
|
|
53999
|
+
* The `--debug` end-of-run line, pure so it's testable without the Sentry SDK.
|
|
54000
|
+
* Mirrors the crash-reference phrasing in `handle-error.ts` ("mention this when
|
|
54001
|
+
* reporting") so users learn one habit for both paths. A `null` trace says why,
|
|
54002
|
+
* so `--debug` never silently does nothing.
|
|
54003
|
+
*/
|
|
54004
|
+
const buildDebugTraceMessage = (traceId) => traceId === null ? "Sentry trace unavailable for this run (no trace was recorded)." : `Sentry trace (mention this when reporting): ${traceId}`;
|
|
54005
|
+
/**
|
|
54006
|
+
* Prints the run's Sentry trace id to stderr at the end of a `--debug` run, so
|
|
54007
|
+
* maintainers can pull the full trace from a pasted id. Runs from the process
|
|
54008
|
+
* `exit` handler, so it's the last line on both the success path and the error
|
|
54009
|
+
* funnels (which `process.exit()` before the promise chain could resume).
|
|
54010
|
+
*
|
|
54011
|
+
* Writes straight to `process.stderr` (not `Console`) for three reasons: the
|
|
54012
|
+
* exit handler is synchronous, JSON mode patches the global console to no-ops —
|
|
54013
|
+
* a diagnostic the user explicitly asked for must survive that — and stderr
|
|
54014
|
+
* keeps `--json` / `--score` stdout machine-clean. The write is wrapped because
|
|
54015
|
+
* a diagnostic must never throw out of an exit handler.
|
|
54016
|
+
*/
|
|
54017
|
+
const printDebugTrace = () => {
|
|
54018
|
+
if (!Sentry.isInitialized()) return;
|
|
54019
|
+
try {
|
|
54020
|
+
process.stderr.write(`${highlighter.dim(buildDebugTraceMessage(getLastRunTraceId()))}\n`);
|
|
54021
|
+
} catch {}
|
|
54022
|
+
};
|
|
54023
|
+
//#endregion
|
|
53823
54024
|
//#region src/cli/utils/removed-cli-flags.ts
|
|
53824
54025
|
const REMOVED_FLAGS = new Map([
|
|
53825
54026
|
["--full", "use `--diff false` to force a full scan"],
|
|
@@ -53846,6 +54047,7 @@ const ROOT_FLAG_SPEC = {
|
|
|
53846
54047
|
longOptionsWithoutValues: new Set([
|
|
53847
54048
|
"--color",
|
|
53848
54049
|
"--dead-code",
|
|
54050
|
+
"--debug",
|
|
53849
54051
|
"--help",
|
|
53850
54052
|
"--json",
|
|
53851
54053
|
"--json-compact",
|
|
@@ -54013,6 +54215,9 @@ const stripUnknownCliFlags = (argv) => {
|
|
|
54013
54215
|
initializeSentry();
|
|
54014
54216
|
process.on("SIGINT", exitGracefully);
|
|
54015
54217
|
process.on("SIGTERM", exitGracefully);
|
|
54218
|
+
process.on("exit", () => {
|
|
54219
|
+
if (isDebugFlagEnabled()) printDebugTrace();
|
|
54220
|
+
});
|
|
54016
54221
|
unrefStdin();
|
|
54017
54222
|
guardStdin();
|
|
54018
54223
|
const formatExampleLines = (examples) => {
|
|
@@ -54057,7 +54262,7 @@ ${highlighter.dim("Learn more:")}
|
|
|
54057
54262
|
${highlighter.info(CANONICAL_GITHUB_URL)}
|
|
54058
54263
|
`;
|
|
54059
54264
|
const collectCategoryOption = (value, previousValues) => [...previousValues ?? [], value];
|
|
54060
|
-
const program = new Command().name("react-doctor").description("Diagnose React codebase health").version(VERSION, "-v, --version", "display the version number").argument("[directory]", "project directory to scan", ".").option("--lint", "enable linting").option("--no-lint", "skip linting").option("--dead-code", "enable dead-code analysis (default)").option("--no-dead-code", "skip dead-code analysis (unused files / exports / dependencies, circular imports)").option("--verbose", "show every rule and per-file details (default shows top 3 rules)").option("--output-dir <dir>", "directory for the full diagnostics dump (default: a temp folder)").option("--score", "output only the score").option("--json", "output a single structured JSON report (suppresses other output)").option("--json-compact", "with --json, emit compact JSON (no indentation)").option("-y, --yes", "skip prompts, scan all workspace projects").option("--no-parallel", "lint serially with one worker (default: parallel across CPU cores; set the worker count with REACT_DOCTOR_PARALLEL)").option("--project <name>", "select projects: workspace names or directory paths (comma-separated for multiple); overrides the `projects` config field").option("--scope <value>", "how much to scan/report: full (default), files, changed (only new issues vs base), or lines (only changed lines)").option("--base <ref>", "base git ref for files/changed/lines scope (auto-detected when omitted)").addOption(new Option("--diff [base]", "[deprecated] alias for --scope changed (pass `false` to force a full scan)").hideHelp()).addOption(new Option("--changed-files-from <file>", "scan source files listed in a newline-delimited changed-files file").hideHelp()).option("--no-score", "skip the score API, the share URL, and crash reporting").addOption(new Option("--category <category>", "only show diagnostics in a category (repeatable; e.g. Security)").argParser(collectCategoryOption)).option("--no-telemetry", "alias for --no-score (skip the score API, share URL, and crash reporting)").option("--staged", "scan only staged (git index) files for pre-commit hooks").option("--blocking <level>", "severity that fails CI: error (default), warning, or none (advisory)").addOption(new Option("--fail-on <level>", "[deprecated] alias for --blocking <level>").hideHelp()).option("--no-respect-inline-disables", "audit mode: neutralize inline lint suppressions before scanning").option("--warnings", "show warning-severity diagnostics (default)").option("--no-warnings", "hide warning-severity diagnostics (errors only)").option("--color", "force colored output").option("--no-color", "disable colored output (also honors NO_COLOR)").addHelpText("after", renderRootHelpEpilog);
|
|
54265
|
+
const program = new Command().name("react-doctor").description("Diagnose React codebase health").version(VERSION, "-v, --version", "display the version number").argument("[directory]", "project directory to scan", ".").option("--lint", "enable linting").option("--no-lint", "skip linting").option("--dead-code", "enable dead-code analysis (default)").option("--no-dead-code", "skip dead-code analysis (unused files / exports / dependencies, circular imports)").option("--verbose", "show every rule and per-file details (default shows top 3 rules)").option("--debug", "force a Sentry trace and print its id at the end (paste it into a bug report)").option("--output-dir <dir>", "directory for the full diagnostics dump (default: a temp folder)").option("--score", "output only the score").option("--json", "output a single structured JSON report (suppresses other output)").option("--json-compact", "with --json, emit compact JSON (no indentation)").option("-y, --yes", "skip prompts, scan all workspace projects").option("--no-parallel", "lint serially with one worker (default: parallel across CPU cores; set the worker count with REACT_DOCTOR_PARALLEL)").option("--project <name>", "select projects: workspace names or directory paths (comma-separated for multiple); overrides the `projects` config field").option("--scope <value>", "how much to scan/report: full (default), files, changed (only new issues vs base), or lines (only changed lines)").option("--base <ref>", "base git ref for files/changed/lines scope (auto-detected when omitted)").addOption(new Option("--diff [base]", "[deprecated] alias for --scope changed (pass `false` to force a full scan)").hideHelp()).addOption(new Option("--changed-files-from <file>", "scan source files listed in a newline-delimited changed-files file").hideHelp()).option("--no-score", "skip the score API, the share URL, and crash reporting").addOption(new Option("--category <category>", "only show diagnostics in a category (repeatable; e.g. Security)").argParser(collectCategoryOption)).option("--no-telemetry", "alias for --no-score (skip the score API, share URL, and crash reporting)").option("--staged", "scan only staged (git index) files for pre-commit hooks").option("--blocking <level>", "severity that fails CI: error (default), warning, or none (advisory)").addOption(new Option("--fail-on <level>", "[deprecated] alias for --blocking <level>").hideHelp()).option("--no-respect-inline-disables", "audit mode: neutralize inline lint suppressions before scanning").option("--warnings", "show warning-severity diagnostics (default)").option("--no-warnings", "hide warning-severity diagnostics (errors only)").option("--color", "force colored output").option("--no-color", "disable colored output (also honors NO_COLOR)").addHelpText("after", renderRootHelpEpilog);
|
|
54061
54266
|
program.action(inspectAction);
|
|
54062
54267
|
program.command("why <location>").description("Explain why a rule fired (or why a suppression didn't apply) at a file:line").option("--project <name>", "select projects: workspace names or directory paths (comma-separated for multiple)").option("-c, --cwd <cwd>", "working directory", process.cwd()).option("--color", "force colored output").option("--no-color", "disable colored output (also honors NO_COLOR)").action((location, options) => whyAction(location, options));
|
|
54063
54268
|
program.command("install").alias("setup").description("Install the react-doctor skill into your coding agents and optional git hook").option("-y, --yes", "skip prompts, install for all detected agents").option("--dry-run", "show what would be installed without writing files").option("--agent-hooks", "install native non-blocking agent hooks for Claude Code and Cursor").option("-c, --cwd <cwd>", "working directory", process.cwd()).option("--color", "force colored output").option("--no-color", "disable colored output (also honors NO_COLOR)").addHelpText("after", renderInstallHelpEpilog).action(installAction);
|
|
@@ -54100,4 +54305,4 @@ Promise.resolve().then(() => assertNoRemovedFlags(process.argv)).then(() => prog
|
|
|
54100
54305
|
export {};
|
|
54101
54306
|
|
|
54102
54307
|
//# sourceMappingURL=cli.js.map
|
|
54103
|
-
//# debugId=
|
|
54308
|
+
//# debugId=4aa7cab0-c8a8-5513-bde6-3f7a0a1bdaa7
|