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/index.d.ts CHANGED
@@ -9489,6 +9489,15 @@ interface Diagnostic {
9489
9489
  suppressionHint?: string;
9490
9490
  /** Secondary source locations (oxlint's non-primary labels). */
9491
9491
  relatedLocations?: DiagnosticRelatedLocation[];
9492
+ /**
9493
+ * Stable id shared by every finding that a single fix resolves together —
9494
+ * e.g. four `useEffect`s that reset state on one prop change all clear with
9495
+ * one `key` prop. Set only when ≥2 findings share a root cause; absent for
9496
+ * standalone findings. A consumer that turns findings into work items should
9497
+ * group by it so one fix reads as one task, not N. Presentation-only and
9498
+ * score-neutral — the score never reads it.
9499
+ */
9500
+ fixGroupId?: string;
9492
9501
  }
9493
9502
  //#endregion
9494
9503
  //#region src/types/project-info.d.ts
@@ -9794,6 +9803,16 @@ interface JsonReportProjectEntry {
9794
9803
  skippedChecks: string[];
9795
9804
  /** Human-readable explanation per skipped check. See `InspectResult.skippedCheckReasons`. */
9796
9805
  skippedCheckReasons?: Record<string, string>;
9806
+ /**
9807
+ * Number of source files this scan's linter examined. In diff / changed
9808
+ * mode it's the count of changed React-eligible files (`.tsx`/`.jsx` plus
9809
+ * framework entry files); in a full scan it's the whole source tree. `0`
9810
+ * in a diff scan means the changed files held nothing React Doctor lints —
9811
+ * the GitHub Action reads that as "nothing to report" (skips the PR comment;
9812
+ * the commit status says "skipped"). Optional: absent on reports from
9813
+ * constructors that don't track it (e.g. `toJsonReport`).
9814
+ */
9815
+ scannedFileCount?: number;
9797
9816
  elapsedMilliseconds: number;
9798
9817
  }
9799
9818
  interface JsonReportSummary {
package/dist/index.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]="ff319d21-ecce-5f79-a822-8cb00c30fea1")}catch(e){}}();
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]="03972752-ed17-5a0b-a633-71b652d2f457")}catch(e){}}();
3
3
  import { r as __toESM$1, t as __commonJSMin$1 } from "./chunk-N93fKeF6.js";
4
4
  import { createRequire } from "node:module";
5
5
  import * as NFS from "node:fs";
@@ -17,6 +17,7 @@ import * as NodeUrl from "node:url";
17
17
  import { fileURLToPath } from "node:url";
18
18
  import { createJiti } from "jiti";
19
19
  import * as Crypto from "node:crypto";
20
+ import { createHash } from "node:crypto";
20
21
  import { gzipSync } from "node:zlib";
21
22
  //#region ../../node_modules/.pnpm/effect@4.0.0-beta.70/node_modules/effect/dist/Pipeable.js
22
23
  /**
@@ -19256,7 +19257,8 @@ var Diagnostic = class extends Class("Diagnostic")({
19256
19257
  category: String$1,
19257
19258
  fileContext: optional(Literals(["test", "story"])),
19258
19259
  suppressionHint: optional(String$1),
19259
- relatedLocations: optional(ArraySchema(DiagnosticRelatedLocation))
19260
+ relatedLocations: optional(ArraySchema(DiagnosticRelatedLocation)),
19261
+ fixGroupId: optional(String$1)
19260
19262
  }) {};
19261
19263
  const JsonReportMode = Literals([
19262
19264
  "full",
@@ -19298,6 +19300,7 @@ var JsonReportProjectEntry = class extends Class("JsonReportProjectEntry")({
19298
19300
  score: Unknown,
19299
19301
  skippedChecks: ArraySchema(String$1),
19300
19302
  skippedCheckReasons: optional(Record$1(String$1, String$1)),
19303
+ scannedFileCount: optional(Number$1),
19301
19304
  elapsedMilliseconds: Number$1
19302
19305
  }) {};
19303
19306
  /**
@@ -33660,6 +33663,13 @@ const APP_ONLY_RULE_KEYS = new Set([
33660
33663
  ]);
33661
33664
  const COMPILER_CLEANUP_BUCKET = "compiler-cleanup";
33662
33665
  const COMPILER_CLEANUP_RULE_KEYS = new Set(["react-doctor/react-compiler-no-manual-memoization"]);
33666
+ const ROOT_CAUSE_GROUPABLE_RULE_KEYS = new Set([
33667
+ "react-doctor/no-derived-state",
33668
+ "react-doctor/no-derived-state-effect",
33669
+ "react-doctor/no-derived-useState",
33670
+ "react-doctor/no-adjust-state-on-prop-change",
33671
+ "react-doctor/no-reset-all-state-on-prop-change"
33672
+ ]);
33663
33673
  const MAX_GLOB_PATTERN_LENGTH_CHARS = 1024;
33664
33674
  const CONFIG_CACHE_TTL_MS = 300 * 1e3;
33665
33675
  const SOCKET_FREE_PURL_API_BASE = "https://firewall-api.socket.dev/purl";
@@ -34237,115 +34247,6 @@ const buildRuleSeverityControls = (config) => {
34237
34247
  ...config.buckets !== void 0 ? { buckets: config.buckets } : {}
34238
34248
  };
34239
34249
  };
34240
- const JSX_OPENER_TAG_PATTERN = /<[A-Za-z][\w.]*/g;
34241
- const JSX_TAG_NAME_FOLLOW = /[A-Za-z]/;
34242
- const isOpenerMatchInsideLineComment = (line, openerCharIndex) => {
34243
- let stringDelimiter = null;
34244
- for (let charIndex = 0; charIndex < openerCharIndex; charIndex++) {
34245
- const character = line[charIndex];
34246
- if (stringDelimiter !== null) {
34247
- if (character === "\\") {
34248
- charIndex++;
34249
- continue;
34250
- }
34251
- if (character === stringDelimiter) stringDelimiter = null;
34252
- continue;
34253
- }
34254
- if (character === "\"" || character === "'" || character === "`") {
34255
- stringDelimiter = character;
34256
- continue;
34257
- }
34258
- if (character === "/" && line[charIndex + 1] === "/") return true;
34259
- }
34260
- return false;
34261
- };
34262
- const findOpenerTagOnLine = (line) => {
34263
- for (const match of line.matchAll(JSX_OPENER_TAG_PATTERN)) {
34264
- if (match.index === void 0) continue;
34265
- if (!isOpenerMatchInsideLineComment(line, match.index)) return { startCharIndex: match.index + match[0].length };
34266
- }
34267
- return null;
34268
- };
34269
- const findJsxOpenerSpan = (lines, openerLineIndex) => {
34270
- const openerLine = lines[openerLineIndex];
34271
- if (openerLine === void 0) return null;
34272
- const opener = findOpenerTagOnLine(openerLine);
34273
- if (!opener) return null;
34274
- const lookaheadLimit = Math.min(lines.length, openerLineIndex + 32);
34275
- let braceDepth = 0;
34276
- let innerAngleDepth = 0;
34277
- let stringDelimiter = null;
34278
- for (let lineIndex = openerLineIndex; lineIndex < lookaheadLimit; lineIndex++) {
34279
- const currentLine = lines[lineIndex];
34280
- const startCharForLine = lineIndex === openerLineIndex ? opener.startCharIndex : 0;
34281
- for (let charIndex = startCharForLine; charIndex < currentLine.length; charIndex++) {
34282
- const character = currentLine[charIndex];
34283
- if (stringDelimiter !== null) {
34284
- if (character === "\\") {
34285
- charIndex++;
34286
- continue;
34287
- }
34288
- if (character === stringDelimiter) stringDelimiter = null;
34289
- continue;
34290
- }
34291
- if (character === "\"" || character === "'" || character === "`") {
34292
- stringDelimiter = character;
34293
- continue;
34294
- }
34295
- if (character === "{") {
34296
- braceDepth++;
34297
- continue;
34298
- }
34299
- if (character === "}") {
34300
- braceDepth--;
34301
- continue;
34302
- }
34303
- if (braceDepth !== 0) continue;
34304
- if (character === "<") {
34305
- const followCharacter = currentLine[charIndex + 1];
34306
- if (followCharacter !== void 0 && JSX_TAG_NAME_FOLLOW.test(followCharacter)) innerAngleDepth++;
34307
- continue;
34308
- }
34309
- if (character !== ">") continue;
34310
- const previousCharacter = currentLine[charIndex - 1];
34311
- const nextCharacter = currentLine[charIndex + 1];
34312
- if (previousCharacter === "=" || nextCharacter === "=") continue;
34313
- if (innerAngleDepth > 0) {
34314
- innerAngleDepth--;
34315
- continue;
34316
- }
34317
- return lineIndex;
34318
- }
34319
- }
34320
- return null;
34321
- };
34322
- const findEnclosingMultilineJsxOpenerStart = (lines, diagnosticLineIndex) => {
34323
- for (let candidateIndex = diagnosticLineIndex - 1; candidateIndex >= 0 && diagnosticLineIndex - candidateIndex <= 32; candidateIndex--) {
34324
- const openerCloseIndex = findJsxOpenerSpan(lines, candidateIndex);
34325
- if (openerCloseIndex !== null && openerCloseIndex >= diagnosticLineIndex) return candidateIndex;
34326
- }
34327
- return null;
34328
- };
34329
- const DISABLE_NEXT_LINE_PATTERN = /(?:\/\/|\/\*)\s*react-doctor-disable-next-line\b(?:\s+([^\r\n]*?))?\s*(?:\*\/)?\s*\}?\s*$/;
34330
- const findStackedDisableCommentsAbove = (lines, anchorIndex) => {
34331
- const collected = [];
34332
- let isStillInChain = true;
34333
- for (let candidateIndex = anchorIndex - 1; candidateIndex >= 0 && anchorIndex - candidateIndex <= 10; candidateIndex--) {
34334
- const candidateLine = lines[candidateIndex];
34335
- if (candidateLine === void 0) break;
34336
- const match = candidateLine.match(DISABLE_NEXT_LINE_PATTERN);
34337
- if (match) {
34338
- collected.push({
34339
- commentLineIndex: candidateIndex,
34340
- ruleList: match[1],
34341
- isInChain: isStillInChain
34342
- });
34343
- continue;
34344
- }
34345
- isStillInChain = false;
34346
- }
34347
- return collected;
34348
- };
34349
34250
  const LEGACY_RULE_KEY_TO_NATIVE_RULE_KEY = {
34350
34251
  "effect/no-adjust-state-on-prop-change": "react-doctor/no-adjust-state-on-prop-change",
34351
34252
  "effect/no-chain-state-updates": "react-doctor/no-chain-state-updates",
@@ -34470,7 +34371,13 @@ for (const [legacyRuleKey, nativeRuleKey] of Object.entries(LEGACY_RULE_KEY_TO_N
34470
34371
  }
34471
34372
  const getLegacyRuleKeysForNative = (ruleKey) => NATIVE_RULE_KEY_TO_LEGACY_RULE_KEYS.get(ruleKey) ?? [];
34472
34373
  const canonicalizeRuleKey = (ruleKey) => LEGACY_RULE_KEY_TO_NATIVE_RULE_KEY[ruleKey] ?? ruleKey;
34473
- const isSameRuleKey = (candidateRuleKey, targetRuleKey) => canonicalizeRuleKey(candidateRuleKey) === canonicalizeRuleKey(targetRuleKey);
34374
+ const isReactDoctorShortIdOf = (bareRuleKey, qualifiedRuleKey) => !bareRuleKey.includes("/") && qualifiedRuleKey === `react-doctor/${bareRuleKey}`;
34375
+ const isSameRuleKey = (candidateRuleKey, targetRuleKey) => {
34376
+ const canonicalCandidate = canonicalizeRuleKey(candidateRuleKey);
34377
+ const canonicalTarget = canonicalizeRuleKey(targetRuleKey);
34378
+ if (canonicalCandidate === canonicalTarget) return true;
34379
+ return isReactDoctorShortIdOf(canonicalCandidate, canonicalTarget) || isReactDoctorShortIdOf(canonicalTarget, canonicalCandidate);
34380
+ };
34474
34381
  const getEquivalentRuleKeys = (ruleKey) => {
34475
34382
  const nativeRuleKey = canonicalizeRuleKey(ruleKey);
34476
34383
  return [nativeRuleKey, ...getLegacyRuleKeysForNative(nativeRuleKey)];
@@ -34480,12 +34387,182 @@ const stripDescriptionTail = (ruleList) => {
34480
34387
  if (!descriptionMatch || descriptionMatch.index === void 0) return ruleList;
34481
34388
  return ruleList.slice(0, descriptionMatch.index);
34482
34389
  };
34483
- const isRuleListedInComment = (ruleList, ruleId) => {
34390
+ const tokenizeRuleList = (ruleList) => {
34484
34391
  const trimmed = ruleList?.trim();
34485
- if (!trimmed) return true;
34392
+ if (!trimmed) return [];
34486
34393
  const ruleSection = stripDescriptionTail(trimmed).trim();
34487
- if (!ruleSection) return true;
34488
- return ruleSection.split(/[,\s]+/).some((token) => isSameRuleKey(token.trim(), ruleId));
34394
+ if (!ruleSection) return [];
34395
+ return ruleSection.split(/[,\s]+/).map((token) => token.trim()).filter(Boolean);
34396
+ };
34397
+ const FOREIGN_INLINE_DISABLE_PATTERN = /(?:\/\/|\/\*)[ \t]*(eslint|oxlint)-disable-(next-line|line)(?![\w-])([^\r\n]*)/;
34398
+ const FOREIGN_BLOCK_DISABLE_PATTERN = /\/\*[ \t]*(eslint|oxlint)-disable(?![\w-])([^*\r\n]*)/;
34399
+ const FOREIGN_BLOCK_ENABLE_PATTERN = /\/\*[ \t]*(?:eslint|oxlint)-enable(?![\w-])([^*\r\n]*)/;
34400
+ 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}\`.`;
34401
+ const tokenMisnamesRule = (token, ruleId) => token !== ruleId && isSameRuleKey(token, ruleId);
34402
+ const detectInlineNearMiss = (lines, diagnosticLineIndex, ruleId) => {
34403
+ const candidates = [{
34404
+ line: lines[diagnosticLineIndex],
34405
+ requiredScope: "line"
34406
+ }, {
34407
+ line: lines[diagnosticLineIndex - 1],
34408
+ requiredScope: "next-line"
34409
+ }];
34410
+ for (const { line, requiredScope } of candidates) {
34411
+ const match = line?.match(FOREIGN_INLINE_DISABLE_PATTERN);
34412
+ if (!match) continue;
34413
+ const [, tool, scope, ruleList] = match;
34414
+ if (scope !== requiredScope) continue;
34415
+ const tokens = tokenizeRuleList(ruleList);
34416
+ if (tokens.includes(ruleId)) continue;
34417
+ for (const token of tokens) if (tokenMisnamesRule(token, ruleId)) return buildHint(tool, token, ruleId);
34418
+ }
34419
+ return null;
34420
+ };
34421
+ const detectBlockNearMiss = (lines, diagnosticLineIndex, ruleId) => {
34422
+ let openMisname = null;
34423
+ const lastLineIndex = Math.min(diagnosticLineIndex, lines.length - 1);
34424
+ for (let lineIndex = 0; lineIndex <= lastLineIndex; lineIndex++) {
34425
+ const line = lines[lineIndex];
34426
+ if (line === void 0 || !line.includes("-disable") && !line.includes("-enable")) continue;
34427
+ const disableMatch = line.match(FOREIGN_BLOCK_DISABLE_PATTERN);
34428
+ if (disableMatch) {
34429
+ const [, tool, ruleList] = disableMatch;
34430
+ const tokens = tokenizeRuleList(ruleList);
34431
+ if (tokens.includes(ruleId)) openMisname = null;
34432
+ else {
34433
+ const misnamed = tokens.find((token) => tokenMisnamesRule(token, ruleId));
34434
+ if (misnamed) openMisname = {
34435
+ tool,
34436
+ token: misnamed
34437
+ };
34438
+ }
34439
+ continue;
34440
+ }
34441
+ const enableMatch = line.match(FOREIGN_BLOCK_ENABLE_PATTERN);
34442
+ if (enableMatch) {
34443
+ const enabledRules = tokenizeRuleList(enableMatch[1]);
34444
+ if (enabledRules.length === 0 || enabledRules.some((rule) => isSameRuleKey(rule, ruleId))) openMisname = null;
34445
+ }
34446
+ }
34447
+ return openMisname ? buildHint(openMisname.tool, openMisname.token, ruleId) : null;
34448
+ };
34449
+ const detectForeignDisableNearMiss = (lines, diagnosticLineIndex, ruleId) => {
34450
+ if (!ruleId.startsWith("react-doctor/")) return null;
34451
+ return detectInlineNearMiss(lines, diagnosticLineIndex, ruleId) ?? detectBlockNearMiss(lines, diagnosticLineIndex, ruleId);
34452
+ };
34453
+ const JSX_OPENER_TAG_PATTERN = /<[A-Za-z][\w.]*/g;
34454
+ const JSX_TAG_NAME_FOLLOW = /[A-Za-z]/;
34455
+ const isOpenerMatchInsideLineComment = (line, openerCharIndex) => {
34456
+ let stringDelimiter = null;
34457
+ for (let charIndex = 0; charIndex < openerCharIndex; charIndex++) {
34458
+ const character = line[charIndex];
34459
+ if (stringDelimiter !== null) {
34460
+ if (character === "\\") {
34461
+ charIndex++;
34462
+ continue;
34463
+ }
34464
+ if (character === stringDelimiter) stringDelimiter = null;
34465
+ continue;
34466
+ }
34467
+ if (character === "\"" || character === "'" || character === "`") {
34468
+ stringDelimiter = character;
34469
+ continue;
34470
+ }
34471
+ if (character === "/" && line[charIndex + 1] === "/") return true;
34472
+ }
34473
+ return false;
34474
+ };
34475
+ const findOpenerTagOnLine = (line) => {
34476
+ for (const match of line.matchAll(JSX_OPENER_TAG_PATTERN)) {
34477
+ if (match.index === void 0) continue;
34478
+ if (!isOpenerMatchInsideLineComment(line, match.index)) return { startCharIndex: match.index + match[0].length };
34479
+ }
34480
+ return null;
34481
+ };
34482
+ const findJsxOpenerSpan = (lines, openerLineIndex) => {
34483
+ const openerLine = lines[openerLineIndex];
34484
+ if (openerLine === void 0) return null;
34485
+ const opener = findOpenerTagOnLine(openerLine);
34486
+ if (!opener) return null;
34487
+ const lookaheadLimit = Math.min(lines.length, openerLineIndex + 32);
34488
+ let braceDepth = 0;
34489
+ let innerAngleDepth = 0;
34490
+ let stringDelimiter = null;
34491
+ for (let lineIndex = openerLineIndex; lineIndex < lookaheadLimit; lineIndex++) {
34492
+ const currentLine = lines[lineIndex];
34493
+ const startCharForLine = lineIndex === openerLineIndex ? opener.startCharIndex : 0;
34494
+ for (let charIndex = startCharForLine; charIndex < currentLine.length; charIndex++) {
34495
+ const character = currentLine[charIndex];
34496
+ if (stringDelimiter !== null) {
34497
+ if (character === "\\") {
34498
+ charIndex++;
34499
+ continue;
34500
+ }
34501
+ if (character === stringDelimiter) stringDelimiter = null;
34502
+ continue;
34503
+ }
34504
+ if (character === "\"" || character === "'" || character === "`") {
34505
+ stringDelimiter = character;
34506
+ continue;
34507
+ }
34508
+ if (character === "{") {
34509
+ braceDepth++;
34510
+ continue;
34511
+ }
34512
+ if (character === "}") {
34513
+ braceDepth--;
34514
+ continue;
34515
+ }
34516
+ if (braceDepth !== 0) continue;
34517
+ if (character === "<") {
34518
+ const followCharacter = currentLine[charIndex + 1];
34519
+ if (followCharacter !== void 0 && JSX_TAG_NAME_FOLLOW.test(followCharacter)) innerAngleDepth++;
34520
+ continue;
34521
+ }
34522
+ if (character !== ">") continue;
34523
+ const previousCharacter = currentLine[charIndex - 1];
34524
+ const nextCharacter = currentLine[charIndex + 1];
34525
+ if (previousCharacter === "=" || nextCharacter === "=") continue;
34526
+ if (innerAngleDepth > 0) {
34527
+ innerAngleDepth--;
34528
+ continue;
34529
+ }
34530
+ return lineIndex;
34531
+ }
34532
+ }
34533
+ return null;
34534
+ };
34535
+ const findEnclosingMultilineJsxOpenerStart = (lines, diagnosticLineIndex) => {
34536
+ for (let candidateIndex = diagnosticLineIndex - 1; candidateIndex >= 0 && diagnosticLineIndex - candidateIndex <= 32; candidateIndex--) {
34537
+ const openerCloseIndex = findJsxOpenerSpan(lines, candidateIndex);
34538
+ if (openerCloseIndex !== null && openerCloseIndex >= diagnosticLineIndex) return candidateIndex;
34539
+ }
34540
+ return null;
34541
+ };
34542
+ const DISABLE_NEXT_LINE_PATTERN = /(?:\/\/|\/\*)\s*react-doctor-disable-next-line\b(?:\s+([^\r\n]*?))?\s*(?:\*\/)?\s*\}?\s*$/;
34543
+ const findStackedDisableCommentsAbove = (lines, anchorIndex) => {
34544
+ const collected = [];
34545
+ let isStillInChain = true;
34546
+ for (let candidateIndex = anchorIndex - 1; candidateIndex >= 0 && anchorIndex - candidateIndex <= 10; candidateIndex--) {
34547
+ const candidateLine = lines[candidateIndex];
34548
+ if (candidateLine === void 0) break;
34549
+ const match = candidateLine.match(DISABLE_NEXT_LINE_PATTERN);
34550
+ if (match) {
34551
+ collected.push({
34552
+ commentLineIndex: candidateIndex,
34553
+ ruleList: match[1],
34554
+ isInChain: isStillInChain
34555
+ });
34556
+ continue;
34557
+ }
34558
+ isStillInChain = false;
34559
+ }
34560
+ return collected;
34561
+ };
34562
+ const isRuleListedInComment = (ruleList, ruleId) => {
34563
+ const tokens = tokenizeRuleList(ruleList);
34564
+ if (tokens.length === 0) return true;
34565
+ return tokens.some((token) => isSameRuleKey(token, ruleId));
34489
34566
  };
34490
34567
  const DISABLE_LINE_PATTERN = /(?:\/\/|\/\*)\s*react-doctor-disable-line\b(?:\s+([^\r\n]*?))?\s*(?:\*\/)?\s*\}?\s*$/;
34491
34568
  const formatLineGap = (gapLineCount) => `${gapLineCount} line${gapLineCount === 1 ? "" : "s"}`;
@@ -34529,7 +34606,7 @@ const evaluateSuppression = (lines, diagnosticLineIndex, ruleId) => {
34529
34606
  };
34530
34607
  return {
34531
34608
  isSuppressed: false,
34532
- nearMissHint: classifyFromComments([directComments, openerComments], diagnosticLineIndex, ruleId)
34609
+ nearMissHint: classifyFromComments([directComments, openerComments], diagnosticLineIndex, ruleId) ?? detectForeignDisableNearMiss(lines, diagnosticLineIndex, ruleId)
34533
34610
  };
34534
34611
  };
34535
34612
  /**
@@ -35532,6 +35609,29 @@ const resolveScanTarget = async (requestedDirectory, options = {}) => {
35532
35609
  didRedirectViaRootDir: redirectedDirectory !== null
35533
35610
  };
35534
35611
  };
35612
+ const buildFixGroupId = (diagnostic) => createHash("sha1").update(JSON.stringify([
35613
+ diagnostic.filePath,
35614
+ `${diagnostic.plugin}/${diagnostic.rule}`,
35615
+ diagnostic.message
35616
+ ])).digest("hex").slice(0, 16);
35617
+ const isGroupableRule = (diagnostic) => ROOT_CAUSE_GROUPABLE_RULE_KEYS.has(`${diagnostic.plugin}/${diagnostic.rule}`);
35618
+ const assignFixGroups = (diagnostics) => {
35619
+ const siteCountByGroupId = /* @__PURE__ */ new Map();
35620
+ for (const diagnostic of diagnostics) {
35621
+ if (!isGroupableRule(diagnostic)) continue;
35622
+ const groupId = buildFixGroupId(diagnostic);
35623
+ siteCountByGroupId.set(groupId, (siteCountByGroupId.get(groupId) ?? 0) + 1);
35624
+ }
35625
+ return diagnostics.map((diagnostic) => {
35626
+ if (!isGroupableRule(diagnostic)) return diagnostic;
35627
+ const groupId = buildFixGroupId(diagnostic);
35628
+ if ((siteCountByGroupId.get(groupId) ?? 0) < 2) return diagnostic;
35629
+ return {
35630
+ ...diagnostic,
35631
+ fixGroupId: groupId
35632
+ };
35633
+ });
35634
+ };
35535
35635
  const getDirectDependencyNames = (packageJson) => new Set([...Object.keys(packageJson.dependencies ?? {}), ...Object.keys(packageJson.devDependencies ?? {})]);
35536
35636
  const buildExpoCheckContext = (rootDirectory, expoVersion) => {
35537
35637
  const packageJson = readPackageJson(Path.join(rootDirectory, "package.json"));
@@ -39978,12 +40078,12 @@ const runInspect = (input, hooks = {}) => gen(function* () {
39978
40078
  else if (input.suppressScanSummary) yield* scanProgress.stop();
39979
40079
  else yield* scanProgress.succeed(`Scanned ${scannedFilesLabel} in ${scanElapsedSeconds}s${workerCountSuffix}`);
39980
40080
  yield* reporterService.finalize;
39981
- const finalDiagnostics = [
40081
+ const finalDiagnostics = assignFixGroups([
39982
40082
  ...envCollected,
39983
40083
  ...supplyChainCollected,
39984
40084
  ...lintCollected,
39985
40085
  ...deadCodeCollected
39986
- ];
40086
+ ]);
39987
40087
  const githubViewerPermission = yield* join(githubViewerPermissionFiber);
39988
40088
  const scoreMetadata = {
39989
40089
  ...repo !== null ? { repo } : {},
@@ -40329,6 +40429,7 @@ const buildJsonReport = (input) => {
40329
40429
  score: result.score,
40330
40430
  skippedChecks: result.skippedChecks,
40331
40431
  ...result.skippedCheckReasons ? { skippedCheckReasons: result.skippedCheckReasons } : {},
40432
+ ...typeof result.scannedFileCount === "number" ? { scannedFileCount: result.scannedFileCount } : {},
40332
40433
  elapsedMilliseconds: result.elapsedMilliseconds
40333
40434
  }));
40334
40435
  const flattenedDiagnostics = projects.flatMap((entry) => entry.diagnostics);
@@ -40595,4 +40696,4 @@ const toJsonReport = (result, options) => buildJsonReport({
40595
40696
  export { AmbiguousProjectError, NoReactDependencyError, NotADirectoryError, PackageJsonNotFoundError, ProjectNotFoundError, ReactDoctorError, buildJsonReport, buildJsonReportError, clearCaches, defineConfig, diagnose, filterSourceFiles, getDiffInfo, isProjectDiscoveryError, isReactDoctorError, summarizeDiagnostics, toJsonReport };
40596
40697
 
40597
40698
  //# sourceMappingURL=index.js.map
40598
- //# debugId=ff319d21-ecce-5f79-a822-8cb00c30fea1
40699
+ //# debugId=03972752-ed17-5a0b-a633-71b652d2f457