react-doctor 0.7.6-dev.fbd85e3 → 0.7.7-dev.04c7e36

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 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]="63b8a227-543e-5501-a998-58c03a1fb10e")}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]="3de13f8e-2b7e-5141-a62e-0cce9208b352")}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";
@@ -36241,7 +36241,7 @@ const SOCKET_FREE_USER_AGENT = "react-doctor-supply-chain";
36241
36241
  const FILE_LINT_CACHE_FILENAME = "file-lint-cache.json";
36242
36242
  const FILE_LINT_CACHE_MAX_FILE_COUNT = 5e4;
36243
36243
  const SIDECAR_LINT_CACHE_FILENAME = "sidecar-lint-cache.json";
36244
- const CORE_PACKAGE_VERSION = "0.7.6";
36244
+ const CORE_PACKAGE_VERSION = "0.7.7";
36245
36245
  const DEAD_CODE_CACHE_FILENAME = "dead-code-cache.json";
36246
36246
  const DEAD_CODE_SUMMARY_CACHE_FILENAME = "dead-code-summaries.json";
36247
36247
  const SUPPLY_CHAIN_PLUGIN = "socket";
@@ -36341,10 +36341,12 @@ const listSourceFilesViaFilesystem = (rootDirectory) => {
36341
36341
  const listSourceFilesWithSize = (rootDirectory) => collectSizedSourceFiles(rootDirectory, (listSourceFilesViaGit(rootDirectory) ?? listSourceFilesViaFilesystem(rootDirectory)).sort());
36342
36342
  const listSourceFiles = (rootDirectory) => listSourceFilesWithSize(rootDirectory).map((entry) => entry.path);
36343
36343
  const countSourceFiles = (rootDirectory) => listSourceFilesWithSize(rootDirectory).length;
36344
+ const UTF8_BOM_CHARACTER = "";
36345
+ const stripUtf8Bom = (content) => content.startsWith(UTF8_BOM_CHARACTER) ? content.slice(1) : content;
36344
36346
  const cachedPackageJsons = /* @__PURE__ */ new Map();
36345
36347
  const readPackageJsonUncached = (packageJsonPath) => {
36346
36348
  try {
36347
- return JSON.parse(NFS.readFileSync(packageJsonPath, "utf-8"));
36349
+ return JSON.parse(stripUtf8Bom(NFS.readFileSync(packageJsonPath, "utf-8")));
36348
36350
  } catch (error) {
36349
36351
  if (error instanceof SyntaxError) return {};
36350
36352
  if (isErrnoException(error)) {
@@ -39592,6 +39594,49 @@ const assignFixGroups = (diagnostics) => {
39592
39594
  };
39593
39595
  });
39594
39596
  };
39597
+ const DERIVED_STATE_RULE_PRIORITY = new Map([
39598
+ ["no-initialize-state", 0],
39599
+ ["no-adjust-state-on-prop-change", 1],
39600
+ ["no-derived-state", 2],
39601
+ ["no-derived-state-effect", 3]
39602
+ ]);
39603
+ const buildDiagnosticSiteKey = (diagnostic) => `${diagnostic.filePath}\u0000${diagnostic.line}\u0000${diagnostic.column}`;
39604
+ const isReactDoctorRulesOfHooksDiagnostic = (diagnostic) => diagnostic.plugin === "react-doctor" && diagnostic.rule === "rules-of-hooks";
39605
+ const isReactCompilerHooksDiagnostic = (diagnostic) => diagnostic.plugin === "react-hooks-js" && diagnostic.rule === "hooks";
39606
+ const doDiagnosticSpansOverlap = (first, second) => {
39607
+ if (first.filePath !== second.filePath || first.plugin !== second.plugin) return false;
39608
+ if (first.offset !== void 0 && second.offset !== void 0) {
39609
+ const firstEnd = first.offset + Math.max(first.length ?? 1, 1);
39610
+ const secondEnd = second.offset + Math.max(second.length ?? 1, 1);
39611
+ return first.offset < secondEnd && second.offset < firstEnd;
39612
+ }
39613
+ const firstEndLine = first.endLine ?? first.line;
39614
+ const secondEndLine = second.endLine ?? second.line;
39615
+ return first.line <= secondEndLine && second.line <= firstEndLine;
39616
+ };
39617
+ const keepHighestSeverity = (survivingDiagnostic, collapsedDiagnostic) => collapsedDiagnostic.severity === "error" && survivingDiagnostic.severity !== "error" ? {
39618
+ ...survivingDiagnostic,
39619
+ severity: "error"
39620
+ } : survivingDiagnostic;
39621
+ const dedupeRelatedDiagnostics = (diagnostics) => {
39622
+ const reactDoctorHookSites = new Set(diagnostics.filter(isReactDoctorRulesOfHooksDiagnostic).map(buildDiagnosticSiteKey));
39623
+ const uniqueDiagnostics = [];
39624
+ for (const diagnostic of diagnostics) {
39625
+ if (isReactCompilerHooksDiagnostic(diagnostic) && reactDoctorHookSites.has(buildDiagnosticSiteKey(diagnostic))) continue;
39626
+ const priority = DERIVED_STATE_RULE_PRIORITY.get(diagnostic.rule);
39627
+ if (priority !== void 0) {
39628
+ const overlappingDiagnosticIndex = uniqueDiagnostics.findIndex((candidate) => candidate.rule !== diagnostic.rule && DERIVED_STATE_RULE_PRIORITY.has(candidate.rule) && doDiagnosticSpansOverlap(candidate, diagnostic));
39629
+ if (overlappingDiagnosticIndex >= 0) {
39630
+ const existingDiagnostic = uniqueDiagnostics[overlappingDiagnosticIndex];
39631
+ const existingPriority = existingDiagnostic ? DERIVED_STATE_RULE_PRIORITY.get(existingDiagnostic.rule) : void 0;
39632
+ if (existingDiagnostic && existingPriority !== void 0) uniqueDiagnostics[overlappingDiagnosticIndex] = priority < existingPriority ? keepHighestSeverity(diagnostic, existingDiagnostic) : keepHighestSeverity(existingDiagnostic, diagnostic);
39633
+ continue;
39634
+ }
39635
+ }
39636
+ uniqueDiagnostics.push(diagnostic);
39637
+ }
39638
+ return uniqueDiagnostics;
39639
+ };
39595
39640
  const compareStrings = (left, right) => left < right ? -1 : left > right ? 1 : 0;
39596
39641
  const sortDiagnosticsStable = (diagnostics) => [...diagnostics].sort((left, right) => compareStrings(left.filePath, right.filePath) || left.line - right.line || left.column - right.column || compareStrings(left.plugin, right.plugin) || compareStrings(left.rule, right.rule) || compareStrings(left.severity, right.severity) || compareStrings(left.message, right.message));
39597
39642
  const getDirectDependencyNames = (packageJson) => new Set([...Object.keys(packageJson.dependencies ?? {}), ...Object.keys(packageJson.devDependencies ?? {})]);
@@ -40017,7 +40062,6 @@ const PNPM_WORKSPACE_FILE = "pnpm-workspace.yaml";
40017
40062
  const PNPM_LOCKFILE = "pnpm-lock.yaml";
40018
40063
  const PACKAGE_JSON_FILE = "package.json";
40019
40064
  const PNPM_HARDENING_RULE_KEY = "require-pnpm-hardening";
40020
- const UTF8_BOM_CHAR = "";
40021
40065
  const HARDENING_SETTING_KEYS = new Set([
40022
40066
  "minimumReleaseAge",
40023
40067
  "blockExoticSubdeps",
@@ -40042,12 +40086,11 @@ const stripInlineComment = (rawValue) => {
40042
40086
  return rawValue;
40043
40087
  };
40044
40088
  const unquote = (rawValue) => rawValue.replace(/^["']|["']$/g, "");
40045
- const stripBom = (rawContent) => rawContent.startsWith(UTF8_BOM_CHAR) ? rawContent.slice(1) : rawContent;
40046
40089
  const parseHardeningSettings = (content) => {
40047
40090
  let minimumReleaseAge = null;
40048
40091
  let blockExoticSubdeps = null;
40049
40092
  let trustPolicy = null;
40050
- const lines = stripBom(content).split(/\r?\n/);
40093
+ const lines = stripUtf8Bom(content).split(/\r?\n/);
40051
40094
  for (let lineIndex = 0; lineIndex < lines.length; lineIndex += 1) {
40052
40095
  const lineText = lines[lineIndex];
40053
40096
  if (lineText === void 0) continue;
@@ -40081,7 +40124,7 @@ const isPnpmManagedProject = (rootDirectory) => {
40081
40124
  const packageJsonPath = Path.join(rootDirectory, PACKAGE_JSON_FILE);
40082
40125
  if (!isFile(packageJsonPath)) return false;
40083
40126
  try {
40084
- const packageJsonRaw = NFS.readFileSync(packageJsonPath, "utf-8");
40127
+ const packageJsonRaw = stripUtf8Bom(NFS.readFileSync(packageJsonPath, "utf-8"));
40085
40128
  const packageJson = JSON.parse(packageJsonRaw);
40086
40129
  if (packageJson !== null && typeof packageJson === "object" && "packageManager" in packageJson && typeof packageJson.packageManager === "string" && packageJson.packageManager.startsWith("pnpm@")) return true;
40087
40130
  } catch {
@@ -40367,18 +40410,45 @@ const checkReactServerComponentsAdvisory = (scanDirectory, project) => {
40367
40410
  }
40368
40411
  return diagnostics;
40369
40412
  };
40370
- const REDUCED_MOTION_GREP_PATTERN = "prefers-reduced-motion|useReducedMotion|MotionConfig|reducedMotion";
40371
- const REDUCED_MOTION_PATTERN = new RegExp(REDUCED_MOTION_GREP_PATTERN);
40372
- const REDUCED_MOTION_FILE_EXTENSIONS = new Set([
40413
+ const getTypescriptScriptKind = (fileName) => {
40414
+ if (fileName.endsWith(".tsx")) return ts.ScriptKind.TSX;
40415
+ if (fileName.endsWith(".jsx")) return ts.ScriptKind.JSX;
40416
+ if (fileName.endsWith(".ts")) return ts.ScriptKind.TS;
40417
+ return ts.ScriptKind.JS;
40418
+ };
40419
+ const unwrapTypescriptExpression = (expression) => {
40420
+ let currentExpression = expression;
40421
+ while (ts.isParenthesizedExpression(currentExpression) || ts.isAsExpression(currentExpression) || ts.isSatisfiesExpression(currentExpression) || ts.isNonNullExpression(currentExpression) || ts.isTypeAssertionExpression(currentExpression)) currentExpression = currentExpression.expression;
40422
+ return currentExpression;
40423
+ };
40424
+ const EMPTY_MOTION_EXPRESSION_EVIDENCE = {
40425
+ isAnimationFunction: false,
40426
+ isMotionComponent: false,
40427
+ isMotionComponentFactory: false,
40428
+ isMotionComponentNamespace: false,
40429
+ isMotionConfig: false,
40430
+ isMotionNamespace: false,
40431
+ isReducedMotionHook: false
40432
+ };
40433
+ const MOTION_COMPONENT_FACTORY_EXPORT_NAMES = new Set(["m", "motion"]);
40434
+ const MOTION_COMPONENT_NAMESPACE_EXPORT_NAMES = new Set(["Reorder"]);
40435
+ const MOTION_COMPONENT_NAMESPACE_MEMBER_NAMES = new Set(["Item"]);
40436
+ const MOTION_ANIMATION_FUNCTION_EXPORT_NAMES = new Set(["animate"]);
40437
+ const REDUCED_MOTION_HOOK_EXPORT_NAME = "useReducedMotion";
40438
+ const MOTION_CONFIG_EXPORT_NAME = "MotionConfig";
40439
+ const REDUCED_MOTION_PROP_NAME = "reducedMotion";
40440
+ const REDUCED_MOTION_CONFIG_VALUES = new Set(["always", "user"]);
40441
+ const SCRIPT_MODULE_EXTENSIONS = [
40373
40442
  ".ts",
40374
40443
  ".tsx",
40375
40444
  ".js",
40376
- ".jsx",
40377
- ".css",
40378
- ".scss"
40379
- ]);
40380
- const REDUCED_MOTION_FILE_GLOBS = [...REDUCED_MOTION_FILE_EXTENSIONS].map((extension) => `*${extension}`);
40381
- const GIT_GREP_NO_MATCH_STATUS = 1;
40445
+ ".jsx"
40446
+ ];
40447
+ const SCRIPT_FILE_EXTENSIONS = new Set(SCRIPT_MODULE_EXTENSIONS);
40448
+ const STYLE_FILE_EXTENSIONS = new Set([".css", ".scss"]);
40449
+ const MOTION_SOURCE_PREFILTER = /framer-motion|["']motion(?:\/[A-Za-z0-9_./-]+)?["']|MotionConfig|useReducedMotion/;
40450
+ const REDUCED_MOTION_MEDIA_QUERY_PATTERN = /@media\s+([^{}]*\(\s*prefers-reduced-motion\s*:\s*reduce\s*\)[^{}]*)\{/gi;
40451
+ const CSS_DECLARATION_PATTERN = /(?:^|[;{])\s*[-_A-Za-z][-_A-Za-z0-9]*\s*:/;
40382
40452
  const MISSING_REDUCED_MOTION_DIAGNOSTIC = {
40383
40453
  filePath: "package.json",
40384
40454
  plugin: "react-doctor",
@@ -40390,52 +40460,406 @@ const MISSING_REDUCED_MOTION_DIAGNOSTIC = {
40390
40460
  column: 0,
40391
40461
  category: "Accessibility"
40392
40462
  };
40393
- const hasReducedMotionHandlingViaFilesystem = (rootDirectory) => {
40463
+ const isMotionModuleSource = (moduleSource) => {
40464
+ for (const packageName of MOTION_LIBRARY_PACKAGES) if (moduleSource === packageName || moduleSource.startsWith(`${packageName}/`)) return true;
40465
+ return false;
40466
+ };
40467
+ const classifyMotionExport = (exportName) => ({
40468
+ isAnimationFunction: MOTION_ANIMATION_FUNCTION_EXPORT_NAMES.has(exportName),
40469
+ isMotionComponent: false,
40470
+ isMotionComponentFactory: MOTION_COMPONENT_FACTORY_EXPORT_NAMES.has(exportName),
40471
+ isMotionComponentNamespace: MOTION_COMPONENT_NAMESPACE_EXPORT_NAMES.has(exportName),
40472
+ isMotionConfig: exportName === MOTION_CONFIG_EXPORT_NAME,
40473
+ isMotionNamespace: false,
40474
+ isReducedMotionHook: exportName === REDUCED_MOTION_HOOK_EXPORT_NAME
40475
+ });
40476
+ const getImportModuleSource = (node) => {
40477
+ let currentNode = node;
40478
+ while (currentNode) {
40479
+ if (ts.isImportDeclaration(currentNode) && ts.isStringLiteral(currentNode.moduleSpecifier)) return currentNode.moduleSpecifier.text;
40480
+ currentNode = currentNode.parent;
40481
+ }
40482
+ return null;
40483
+ };
40484
+ const getImportedBindingEvidence = (declaration, typeChecker, program, visitedSymbols) => {
40485
+ const moduleSource = getImportModuleSource(declaration);
40486
+ if (!moduleSource) return null;
40487
+ if (isMotionModuleSource(moduleSource) && ts.isNamespaceImport(declaration)) return {
40488
+ ...EMPTY_MOTION_EXPRESSION_EVIDENCE,
40489
+ isMotionNamespace: true
40490
+ };
40491
+ if (isMotionModuleSource(moduleSource) && ts.isImportSpecifier(declaration)) {
40492
+ if (declaration.isTypeOnly) return null;
40493
+ return classifyMotionExport(declaration.propertyName?.text ?? declaration.name.text);
40494
+ }
40495
+ if (isMotionModuleSource(moduleSource) && ts.isImportClause(declaration)) {
40496
+ if (declaration.isTypeOnly) return null;
40497
+ return EMPTY_MOTION_EXPRESSION_EVIDENCE;
40498
+ }
40499
+ if (ts.isImportSpecifier(declaration) && !declaration.isTypeOnly || ts.isImportClause(declaration) && !declaration.isTypeOnly && declaration.name) {
40500
+ const importedName = ts.isImportSpecifier(declaration) ? declaration.propertyName?.text ?? declaration.name.text : "default";
40501
+ const moduleSourceFile = getLocalModuleSourceFile(moduleSource, declaration.getSourceFile().fileName, program);
40502
+ if (!moduleSourceFile) return null;
40503
+ return resolveModuleExportEvidence(moduleSourceFile, importedName, typeChecker, program, visitedSymbols);
40504
+ }
40505
+ return null;
40506
+ };
40507
+ const getLocalModuleSourceFile = (moduleSource, importingFileName, program) => {
40508
+ if (!moduleSource.startsWith(".")) return null;
40509
+ const moduleBasePath = Path.resolve(Path.dirname(importingFileName), moduleSource);
40510
+ const moduleBaseExtension = Path.extname(moduleBasePath);
40511
+ const typescriptModuleBasePath = moduleBaseExtension === ".js" || moduleBaseExtension === ".jsx" ? moduleBasePath.slice(0, -moduleBaseExtension.length) : null;
40512
+ const candidates = [
40513
+ moduleBasePath,
40514
+ ...typescriptModuleBasePath ? [
40515
+ `${typescriptModuleBasePath}.ts`,
40516
+ `${typescriptModuleBasePath}.tsx`,
40517
+ Path.join(typescriptModuleBasePath, "index.ts"),
40518
+ Path.join(typescriptModuleBasePath, "index.tsx")
40519
+ ] : [],
40520
+ ...SCRIPT_MODULE_EXTENSIONS.map((extension) => `${moduleBasePath}${extension}`),
40521
+ ...SCRIPT_MODULE_EXTENSIONS.map((extension) => Path.join(moduleBasePath, `index${extension}`))
40522
+ ];
40523
+ for (const candidate of candidates) {
40524
+ const sourceFile = program.getSourceFile(candidate);
40525
+ if (sourceFile) return sourceFile;
40526
+ }
40527
+ return null;
40528
+ };
40529
+ const resolveModuleExportEvidence = (sourceFile, exportName, typeChecker, program, visitedSymbols) => {
40530
+ const moduleSymbol = typeChecker.getSymbolAtLocation(sourceFile);
40531
+ if (moduleSymbol && visitedSymbols.has(moduleSymbol)) return EMPTY_MOTION_EXPRESSION_EVIDENCE;
40532
+ const nextVisitedSymbols = new Set(visitedSymbols);
40533
+ if (moduleSymbol) nextVisitedSymbols.add(moduleSymbol);
40534
+ for (const statement of sourceFile.statements) {
40535
+ if (exportName === "default" && ts.isExportAssignment(statement) && !statement.isExportEquals) {
40536
+ const evidence = resolveMotionExpressionEvidence(statement.expression, typeChecker, program, nextVisitedSymbols);
40537
+ if (hasMotionExpressionEvidence(evidence)) return evidence;
40538
+ continue;
40539
+ }
40540
+ if (!ts.isExportDeclaration(statement) || !statement.moduleSpecifier) continue;
40541
+ if (statement.isTypeOnly) continue;
40542
+ if (!ts.isStringLiteral(statement.moduleSpecifier)) continue;
40543
+ const moduleSource = statement.moduleSpecifier.text;
40544
+ if (!statement.exportClause) {
40545
+ if (isMotionModuleSource(moduleSource)) {
40546
+ const evidence = classifyMotionExport(exportName);
40547
+ if (hasMotionExpressionEvidence(evidence)) return evidence;
40548
+ continue;
40549
+ }
40550
+ const moduleSourceFile = getLocalModuleSourceFile(moduleSource, sourceFile.fileName, program);
40551
+ if (moduleSourceFile) {
40552
+ const evidence = resolveModuleExportEvidence(moduleSourceFile, exportName, typeChecker, program, nextVisitedSymbols);
40553
+ if (hasMotionExpressionEvidence(evidence)) return evidence;
40554
+ }
40555
+ continue;
40556
+ }
40557
+ if (!ts.isNamedExports(statement.exportClause)) continue;
40558
+ for (const exportSpecifier of statement.exportClause.elements) {
40559
+ if (exportSpecifier.name.text !== exportName || exportSpecifier.isTypeOnly) continue;
40560
+ const sourceExportName = exportSpecifier.propertyName?.text ?? exportSpecifier.name.text;
40561
+ if (isMotionModuleSource(moduleSource)) return classifyMotionExport(sourceExportName);
40562
+ const moduleSourceFile = getLocalModuleSourceFile(moduleSource, sourceFile.fileName, program);
40563
+ if (!moduleSourceFile) continue;
40564
+ return resolveModuleExportEvidence(moduleSourceFile, sourceExportName, typeChecker, program, nextVisitedSymbols);
40565
+ }
40566
+ }
40567
+ if (!moduleSymbol) return EMPTY_MOTION_EXPRESSION_EVIDENCE;
40568
+ const exportedSymbol = typeChecker.getExportsOfModule(moduleSymbol).find((candidateSymbol) => candidateSymbol.name === exportName);
40569
+ if (!exportedSymbol || nextVisitedSymbols.has(exportedSymbol)) return EMPTY_MOTION_EXPRESSION_EVIDENCE;
40570
+ nextVisitedSymbols.add(exportedSymbol);
40571
+ let resolvedEvidence = EMPTY_MOTION_EXPRESSION_EVIDENCE;
40572
+ for (const declaration of exportedSymbol.declarations ?? []) {
40573
+ if (ts.isExportSpecifier(declaration)) {
40574
+ const targetSymbol = typeChecker.getExportSpecifierLocalTargetSymbol(declaration);
40575
+ for (const targetDeclaration of targetSymbol?.declarations ?? []) {
40576
+ const importedEvidence = getImportedBindingEvidence(targetDeclaration, typeChecker, program, nextVisitedSymbols);
40577
+ if (importedEvidence) resolvedEvidence = mergeMotionExpressionEvidence(resolvedEvidence, importedEvidence);
40578
+ if (ts.isVariableDeclaration(targetDeclaration) && targetDeclaration.initializer) resolvedEvidence = mergeMotionExpressionEvidence(resolvedEvidence, resolveMotionExpressionEvidence(targetDeclaration.initializer, typeChecker, program, nextVisitedSymbols));
40579
+ }
40580
+ }
40581
+ if (ts.isVariableDeclaration(declaration) && declaration.initializer) resolvedEvidence = mergeMotionExpressionEvidence(resolvedEvidence, resolveMotionExpressionEvidence(declaration.initializer, typeChecker, program, nextVisitedSymbols));
40582
+ }
40583
+ return resolvedEvidence;
40584
+ };
40585
+ const mergeMotionExpressionEvidence = (leftEvidence, rightEvidence) => ({
40586
+ isAnimationFunction: leftEvidence.isAnimationFunction || rightEvidence.isAnimationFunction,
40587
+ isMotionComponent: leftEvidence.isMotionComponent || rightEvidence.isMotionComponent,
40588
+ isMotionComponentFactory: leftEvidence.isMotionComponentFactory || rightEvidence.isMotionComponentFactory,
40589
+ isMotionComponentNamespace: leftEvidence.isMotionComponentNamespace || rightEvidence.isMotionComponentNamespace,
40590
+ isMotionConfig: leftEvidence.isMotionConfig || rightEvidence.isMotionConfig,
40591
+ isMotionNamespace: leftEvidence.isMotionNamespace || rightEvidence.isMotionNamespace,
40592
+ isReducedMotionHook: leftEvidence.isReducedMotionHook || rightEvidence.isReducedMotionHook
40593
+ });
40594
+ const hasMotionExpressionEvidence = (evidence) => evidence.isAnimationFunction || evidence.isMotionComponent || evidence.isMotionComponentFactory || evidence.isMotionComponentNamespace || evidence.isMotionConfig || evidence.isMotionNamespace || evidence.isReducedMotionHook;
40595
+ const classifyMotionMember = (receiverEvidence, memberName) => {
40596
+ if (receiverEvidence.isMotionNamespace) return classifyMotionExport(memberName);
40597
+ if (receiverEvidence.isMotionComponentFactory) return {
40598
+ ...EMPTY_MOTION_EXPRESSION_EVIDENCE,
40599
+ isMotionComponent: true
40600
+ };
40601
+ if (receiverEvidence.isMotionComponentNamespace && MOTION_COMPONENT_NAMESPACE_MEMBER_NAMES.has(memberName)) return {
40602
+ ...EMPTY_MOTION_EXPRESSION_EVIDENCE,
40603
+ isMotionComponent: true
40604
+ };
40605
+ return EMPTY_MOTION_EXPRESSION_EVIDENCE;
40606
+ };
40607
+ const resolveMotionExpressionEvidence = (expression, typeChecker, program, visitedSymbols = /* @__PURE__ */ new Set()) => {
40608
+ const unwrappedExpression = unwrapTypescriptExpression(expression);
40609
+ if (ts.isIdentifier(unwrappedExpression)) {
40610
+ const symbol = typeChecker.getSymbolAtLocation(unwrappedExpression);
40611
+ if (!symbol || visitedSymbols.has(symbol)) return EMPTY_MOTION_EXPRESSION_EVIDENCE;
40612
+ const nextVisitedSymbols = new Set(visitedSymbols);
40613
+ nextVisitedSymbols.add(symbol);
40614
+ let resolvedEvidence = EMPTY_MOTION_EXPRESSION_EVIDENCE;
40615
+ for (const declaration of symbol.declarations ?? []) {
40616
+ const importedEvidence = getImportedBindingEvidence(declaration, typeChecker, program, nextVisitedSymbols);
40617
+ if (importedEvidence) {
40618
+ resolvedEvidence = mergeMotionExpressionEvidence(resolvedEvidence, importedEvidence);
40619
+ continue;
40620
+ }
40621
+ if (ts.isVariableDeclaration(declaration) && declaration.initializer) resolvedEvidence = mergeMotionExpressionEvidence(resolvedEvidence, resolveMotionExpressionEvidence(declaration.initializer, typeChecker, program, nextVisitedSymbols));
40622
+ if (ts.isBindingElement(declaration) && ts.isObjectBindingPattern(declaration.parent) && ts.isVariableDeclaration(declaration.parent.parent) && declaration.parent.parent.initializer) {
40623
+ const propertyName = declaration.propertyName ?? declaration.name;
40624
+ if (ts.isIdentifier(propertyName) || ts.isStringLiteral(propertyName)) {
40625
+ const receiverEvidence = resolveMotionExpressionEvidence(declaration.parent.parent.initializer, typeChecker, program, nextVisitedSymbols);
40626
+ resolvedEvidence = mergeMotionExpressionEvidence(resolvedEvidence, classifyMotionMember(receiverEvidence, propertyName.text));
40627
+ }
40628
+ }
40629
+ }
40630
+ return resolvedEvidence;
40631
+ }
40632
+ if (ts.isPropertyAccessExpression(unwrappedExpression)) return classifyMotionMember(resolveMotionExpressionEvidence(unwrappedExpression.expression, typeChecker, program, visitedSymbols), unwrappedExpression.name.text);
40633
+ if (ts.isElementAccessExpression(unwrappedExpression) && unwrappedExpression.argumentExpression && (ts.isStringLiteral(unwrappedExpression.argumentExpression) || ts.isNoSubstitutionTemplateLiteral(unwrappedExpression.argumentExpression))) return classifyMotionMember(resolveMotionExpressionEvidence(unwrappedExpression.expression, typeChecker, program, visitedSymbols), unwrappedExpression.argumentExpression.text);
40634
+ if (ts.isCallExpression(unwrappedExpression)) {
40635
+ if (ts.isIdentifier(unwrappedExpression.expression) && unwrappedExpression.expression.text === "require" && !typeChecker.getSymbolAtLocation(unwrappedExpression.expression) && unwrappedExpression.arguments.length === 1 && ts.isStringLiteral(unwrappedExpression.arguments[0]) && isMotionModuleSource(unwrappedExpression.arguments[0].text)) return {
40636
+ ...EMPTY_MOTION_EXPRESSION_EVIDENCE,
40637
+ isMotionNamespace: true
40638
+ };
40639
+ if (ts.isPropertyAccessExpression(unwrappedExpression.expression) && unwrappedExpression.expression.name.text === "bind") {
40640
+ const boundReceiverEvidence = resolveMotionExpressionEvidence(unwrappedExpression.expression.expression, typeChecker, program, visitedSymbols);
40641
+ if (boundReceiverEvidence.isAnimationFunction) return boundReceiverEvidence;
40642
+ }
40643
+ const calleeEvidence = resolveMotionExpressionEvidence(unwrappedExpression.expression, typeChecker, program, visitedSymbols);
40644
+ if (calleeEvidence.isMotionComponentFactory) return {
40645
+ ...EMPTY_MOTION_EXPRESSION_EVIDENCE,
40646
+ isMotionComponent: true
40647
+ };
40648
+ return calleeEvidence;
40649
+ }
40650
+ return EMPTY_MOTION_EXPRESSION_EVIDENCE;
40651
+ };
40652
+ const getStaticStringExpressionValue = (expression, typeChecker, visitedSymbols = /* @__PURE__ */ new Set()) => {
40653
+ const unwrappedExpression = unwrapTypescriptExpression(expression);
40654
+ if (ts.isStringLiteral(unwrappedExpression) || ts.isNoSubstitutionTemplateLiteral(unwrappedExpression)) return unwrappedExpression.text;
40655
+ if (!ts.isIdentifier(unwrappedExpression)) return null;
40656
+ const symbol = typeChecker.getSymbolAtLocation(unwrappedExpression);
40657
+ if (!symbol || visitedSymbols.has(symbol)) return null;
40658
+ const nextVisitedSymbols = new Set(visitedSymbols);
40659
+ nextVisitedSymbols.add(symbol);
40660
+ for (const declaration of symbol.declarations ?? []) {
40661
+ if (!ts.isVariableDeclaration(declaration) || !declaration.initializer) continue;
40662
+ if (!ts.isVariableDeclarationList(declaration.parent)) continue;
40663
+ if (!(declaration.parent.flags & ts.NodeFlags.Const)) continue;
40664
+ const value = getStaticStringExpressionValue(declaration.initializer, typeChecker, nextVisitedSymbols);
40665
+ if (value !== null) return value;
40666
+ }
40667
+ return null;
40668
+ };
40669
+ const getStaticJsxAttributeValue = (attribute, typeChecker) => {
40670
+ if (!attribute.initializer) return null;
40671
+ if (ts.isStringLiteral(attribute.initializer)) return attribute.initializer.text;
40672
+ if (!ts.isJsxExpression(attribute.initializer) || !attribute.initializer.expression) return null;
40673
+ return getStaticStringExpressionValue(attribute.initializer.expression, typeChecker);
40674
+ };
40675
+ const isExpressionValueConsumed = (expression) => {
40676
+ let currentExpression = expression;
40677
+ while (ts.isParenthesizedExpression(currentExpression.parent) || ts.isAsExpression(currentExpression.parent) || ts.isSatisfiesExpression(currentExpression.parent) || ts.isNonNullExpression(currentExpression.parent) || ts.isTypeAssertionExpression(currentExpression.parent)) currentExpression = currentExpression.parent;
40678
+ return !ts.isExpressionStatement(currentExpression.parent) && !ts.isVoidExpression(currentExpression.parent);
40679
+ };
40680
+ const jsxElementHasReducedMotionConfiguration = (attributes, typeChecker) => attributes.properties.some((attribute) => ts.isJsxAttribute(attribute) && ts.isIdentifier(attribute.name) && attribute.name.text === REDUCED_MOTION_PROP_NAME && REDUCED_MOTION_CONFIG_VALUES.has(getStaticJsxAttributeValue(attribute, typeChecker) ?? ""));
40681
+ const collectScriptMotionEvidence = (sourceFile, typeChecker, program) => {
40682
+ const evidence = {
40683
+ hasMotionUse: false,
40684
+ hasReducedMotionHandling: false
40685
+ };
40686
+ const visitNode = (node) => {
40687
+ if (ts.isCallExpression(node)) {
40688
+ const calleeEvidence = resolveMotionExpressionEvidence(node.expression, typeChecker, program);
40689
+ const isAnimationFunctionCallViaCallOrApply = (ts.isPropertyAccessExpression(node.expression) && (node.expression.name.text === "call" || node.expression.name.text === "apply") || ts.isElementAccessExpression(node.expression) && node.expression.argumentExpression && (ts.isStringLiteral(node.expression.argumentExpression) || ts.isNoSubstitutionTemplateLiteral(node.expression.argumentExpression)) && (node.expression.argumentExpression.text === "call" || node.expression.argumentExpression.text === "apply")) && resolveMotionExpressionEvidence(node.expression.expression, typeChecker, program).isAnimationFunction;
40690
+ if (calleeEvidence.isAnimationFunction || isAnimationFunctionCallViaCallOrApply) evidence.hasMotionUse = true;
40691
+ if (calleeEvidence.isReducedMotionHook && isExpressionValueConsumed(node)) evidence.hasReducedMotionHandling = true;
40692
+ }
40693
+ if (ts.isJsxOpeningElement(node) || ts.isJsxSelfClosingElement(node)) {
40694
+ const tagEvidence = ts.isJsxNamespacedName(node.tagName) ? EMPTY_MOTION_EXPRESSION_EVIDENCE : resolveMotionExpressionEvidence(node.tagName, typeChecker, program);
40695
+ if (tagEvidence.isMotionComponent || tagEvidence.isMotionComponentFactory) evidence.hasMotionUse = true;
40696
+ if (tagEvidence.isMotionConfig && jsxElementHasReducedMotionConfiguration(node.attributes, typeChecker)) evidence.hasReducedMotionHandling = true;
40697
+ }
40698
+ ts.forEachChild(node, visitNode);
40699
+ };
40700
+ visitNode(sourceFile);
40701
+ return evidence;
40702
+ };
40703
+ const analyzeReducedMotionSources = (sources) => {
40704
+ if (!sources.some(({ sourceText }) => MOTION_SOURCE_PREFILTER.test(sourceText))) return {
40705
+ hasMotionUse: false,
40706
+ hasReducedMotionHandling: false
40707
+ };
40708
+ const compilerOptions = {
40709
+ allowJs: true,
40710
+ checkJs: false,
40711
+ jsx: ts.JsxEmit.Preserve,
40712
+ module: ts.ModuleKind.ESNext,
40713
+ moduleResolution: ts.ModuleResolutionKind.Bundler,
40714
+ noEmit: true,
40715
+ noLib: true,
40716
+ noResolve: true,
40717
+ skipLibCheck: true,
40718
+ target: ts.ScriptTarget.Latest
40719
+ };
40720
+ const sourceTextByFileName = new Map(sources.map(({ fileName: sourceFileName, sourceText: content }) => [Path.resolve(sourceFileName), content]));
40721
+ const compilerHost = ts.createCompilerHost(compilerOptions);
40722
+ const getDefaultSourceFile = compilerHost.getSourceFile.bind(compilerHost);
40723
+ compilerHost.getSourceFile = (requestedFileName, languageVersionOrOptions, onError, shouldCreateNewSourceFile) => sourceTextByFileName.has(Path.resolve(requestedFileName)) ? ts.createSourceFile(Path.resolve(requestedFileName), sourceTextByFileName.get(Path.resolve(requestedFileName)) ?? "", languageVersionOrOptions, true, getTypescriptScriptKind(requestedFileName)) : getDefaultSourceFile(requestedFileName, languageVersionOrOptions, onError, shouldCreateNewSourceFile);
40724
+ const program = ts.createProgram([...sourceTextByFileName.keys()], compilerOptions, compilerHost);
40725
+ const typeChecker = program.getTypeChecker();
40726
+ const evidence = {
40727
+ hasMotionUse: false,
40728
+ hasReducedMotionHandling: false
40729
+ };
40730
+ for (const sourceFileName of sourceTextByFileName.keys()) {
40731
+ const sourceFile = program.getSourceFile(sourceFileName);
40732
+ if (!sourceFile) continue;
40733
+ const fileEvidence = collectScriptMotionEvidence(sourceFile, typeChecker, program);
40734
+ evidence.hasMotionUse ||= fileEvidence.hasMotionUse;
40735
+ evidence.hasReducedMotionHandling ||= fileEvidence.hasReducedMotionHandling;
40736
+ }
40737
+ return evidence;
40738
+ };
40739
+ const removeCssCommentsAndStrings = (content) => {
40740
+ let sanitizedContent = "";
40741
+ let quote = null;
40742
+ let isInsideComment = false;
40743
+ let isInsideLineComment = false;
40744
+ for (let characterIndex = 0; characterIndex < content.length; characterIndex += 1) {
40745
+ const character = content[characterIndex];
40746
+ const nextCharacter = content[characterIndex + 1];
40747
+ if (isInsideLineComment) {
40748
+ sanitizedContent += character === "\n" ? "\n" : " ";
40749
+ if (character === "\n") isInsideLineComment = false;
40750
+ continue;
40751
+ }
40752
+ if (isInsideComment) {
40753
+ if (character === "*" && nextCharacter === "/") {
40754
+ sanitizedContent += " ";
40755
+ characterIndex += 1;
40756
+ isInsideComment = false;
40757
+ } else sanitizedContent += character === "\n" ? "\n" : " ";
40758
+ continue;
40759
+ }
40760
+ if (quote) {
40761
+ if (character === "\\") {
40762
+ sanitizedContent += " ";
40763
+ characterIndex += 1;
40764
+ continue;
40765
+ }
40766
+ if (character === quote) quote = null;
40767
+ sanitizedContent += character === "\n" ? "\n" : " ";
40768
+ continue;
40769
+ }
40770
+ if (character === "/" && nextCharacter === "*") {
40771
+ sanitizedContent += " ";
40772
+ characterIndex += 1;
40773
+ isInsideComment = true;
40774
+ continue;
40775
+ }
40776
+ if (character === "/" && nextCharacter === "/") {
40777
+ sanitizedContent += " ";
40778
+ characterIndex += 1;
40779
+ isInsideLineComment = true;
40780
+ continue;
40781
+ }
40782
+ if (character === "\"" || character === "'") {
40783
+ sanitizedContent += " ";
40784
+ quote = character;
40785
+ continue;
40786
+ }
40787
+ sanitizedContent += character;
40788
+ }
40789
+ return sanitizedContent;
40790
+ };
40791
+ const findMatchingClosingBrace = (content, openingBraceIndex) => {
40792
+ let depth = 0;
40793
+ for (let characterIndex = openingBraceIndex; characterIndex < content.length; characterIndex += 1) {
40794
+ if (content[characterIndex] === "{") depth += 1;
40795
+ if (content[characterIndex] !== "}") continue;
40796
+ depth -= 1;
40797
+ if (depth === 0) return characterIndex;
40798
+ }
40799
+ return -1;
40800
+ };
40801
+ const hasReducedMotionMediaQuery = (content) => {
40802
+ const sanitizedContent = removeCssCommentsAndStrings(content);
40803
+ REDUCED_MOTION_MEDIA_QUERY_PATTERN.lastIndex = 0;
40804
+ for (const match of sanitizedContent.matchAll(REDUCED_MOTION_MEDIA_QUERY_PATTERN)) {
40805
+ const mediaPrelude = match[1];
40806
+ if (/\bnot\b/i.test(mediaPrelude)) continue;
40807
+ const openingBraceIndex = (match.index ?? 0) + match[0].lastIndexOf("{");
40808
+ const closingBraceIndex = findMatchingClosingBrace(sanitizedContent, openingBraceIndex);
40809
+ if (closingBraceIndex === -1) continue;
40810
+ const body = sanitizedContent.slice(openingBraceIndex + 1, closingBraceIndex);
40811
+ if (CSS_DECLARATION_PATTERN.test(body)) return true;
40812
+ }
40813
+ return false;
40814
+ };
40815
+ const collectProjectMotionEvidence = (rootDirectory) => {
40816
+ const scriptSources = [];
40817
+ let hasReducedMotionHandling = false;
40394
40818
  for (const { absolutePath, name } of walkSourceTreeFiles(rootDirectory)) {
40395
- if (!REDUCED_MOTION_FILE_EXTENSIONS.has(Path.extname(name))) continue;
40819
+ const extension = Path.extname(name);
40820
+ if (!SCRIPT_FILE_EXTENSIONS.has(extension) && !STYLE_FILE_EXTENSIONS.has(extension)) continue;
40396
40821
  let content;
40397
40822
  try {
40398
40823
  content = NFS.readFileSync(absolutePath, "utf-8");
40399
40824
  } catch {
40400
40825
  continue;
40401
40826
  }
40402
- if (REDUCED_MOTION_PATTERN.test(content)) return true;
40827
+ if (STYLE_FILE_EXTENSIONS.has(extension)) {
40828
+ if (hasReducedMotionMediaQuery(content)) hasReducedMotionHandling = true;
40829
+ continue;
40830
+ }
40831
+ scriptSources.push({
40832
+ fileName: absolutePath,
40833
+ sourceText: content
40834
+ });
40403
40835
  }
40404
- return false;
40836
+ if (scriptSources.length === 0) return {
40837
+ hasMotionUse: false,
40838
+ hasReducedMotionHandling
40839
+ };
40840
+ const scriptEvidence = analyzeReducedMotionSources(scriptSources);
40841
+ const hasMotionUse = scriptEvidence.hasMotionUse;
40842
+ hasReducedMotionHandling ||= scriptEvidence.hasReducedMotionHandling;
40843
+ return {
40844
+ hasMotionUse,
40845
+ hasReducedMotionHandling
40846
+ };
40405
40847
  };
40406
40848
  const checkReducedMotion = (rootDirectory) => {
40407
40849
  const packageJsonPath = Path.join(rootDirectory, "package.json");
40408
40850
  if (!isFile(packageJsonPath)) return [];
40409
- let hasMotionLibrary = false;
40410
40851
  try {
40411
40852
  const packageJson = readPackageJson$1(packageJsonPath);
40412
40853
  const allDependencies = {
40413
40854
  ...packageJson.dependencies,
40414
40855
  ...packageJson.devDependencies
40415
40856
  };
40416
- hasMotionLibrary = Object.keys(allDependencies).some((packageName) => MOTION_LIBRARY_PACKAGES.has(packageName));
40857
+ if (!Object.keys(allDependencies).some((packageName) => MOTION_LIBRARY_PACKAGES.has(packageName))) return [];
40417
40858
  } catch {
40418
40859
  return [];
40419
40860
  }
40420
- if (!hasMotionLibrary) return [];
40421
- const result = spawnSync("git", [
40422
- "grep",
40423
- "--untracked",
40424
- "-lE",
40425
- REDUCED_MOTION_GREP_PATTERN,
40426
- "--",
40427
- ...REDUCED_MOTION_FILE_GLOBS
40428
- ], {
40429
- cwd: rootDirectory,
40430
- encoding: "utf-8",
40431
- stdio: [
40432
- "ignore",
40433
- "pipe",
40434
- "pipe"
40435
- ]
40436
- });
40437
- if (!(!result.error && (result.status === 0 || result.status === GIT_GREP_NO_MATCH_STATUS))) return hasReducedMotionHandlingViaFilesystem(rootDirectory) ? [] : [MISSING_REDUCED_MOTION_DIAGNOSTIC];
40438
- return result.status === 0 && result.stdout.split("\n").some((filePath) => filePath.length > 0 && !hasIgnoredPathSegment(filePath)) ? [] : [MISSING_REDUCED_MOTION_DIAGNOSTIC];
40861
+ const evidence = collectProjectMotionEvidence(rootDirectory);
40862
+ return evidence.hasMotionUse && !evidence.hasReducedMotionHandling ? [MISSING_REDUCED_MOTION_DIAGNOSTIC] : [];
40439
40863
  };
40440
40864
  const buildSecurityScanDiagnostic = (finding, entry, relativePath) => ({
40441
40865
  filePath: relativePath,
@@ -40762,9 +41186,7 @@ const setColorEnabled = (enabled) => {
40762
41186
  highlighter.gray = colors.gray;
40763
41187
  highlighter.bold = colors.bold;
40764
41188
  };
40765
- const NEXT_ENTRY_FILE_PATTERN = /^(?:\.\/)?(?:src\/)?(?:middleware|proxy)\.(?:tsx?|jsx?|mts|mjs)$/;
40766
- const shouldIncludeExplicitLintPath = (filePath, project) => JSX_FILE_PATTERN.test(filePath) || project?.framework === "nextjs" && NEXT_ENTRY_FILE_PATTERN.test(filePath);
40767
- const computeExplicitLintIncludePaths = (includePaths, project) => includePaths.length > 0 ? includePaths.filter((filePath) => shouldIncludeExplicitLintPath(filePath, project)) : void 0;
41189
+ const computeExplicitLintIncludePaths = (includePaths) => includePaths.length > 0 ? includePaths.filter(isLintableSourceFile) : void 0;
40768
41190
  const LINGUIST_ATTRIBUTE_PATTERN = /^linguist-(?:vendored|generated)(?:=([a-zA-Z0-9]+))?$/i;
40769
41191
  const FALSY_VALUES = new Set([
40770
41192
  "false",
@@ -41603,11 +42025,11 @@ const resolveDeadCodeTimeout = (input) => {
41603
42025
  phaseTimeoutMs: workerTimeoutMs + DEAD_CODE_PHASE_TIMEOUT_OVER_WORKER_MS
41604
42026
  };
41605
42027
  };
41606
- const resolveLintIncludePaths = (rootDirectory, userConfig, project) => {
42028
+ const resolveLintIncludePaths = (rootDirectory, userConfig) => {
41607
42029
  if (!Array.isArray(userConfig?.ignore?.files) || userConfig.ignore.files.length === 0) return;
41608
42030
  const ignoredPatterns = compileIgnoredFilePatterns(userConfig);
41609
42031
  return listSourceFiles(rootDirectory).filter((filePath) => {
41610
- if (!shouldIncludeExplicitLintPath(filePath, project)) return false;
42032
+ if (!isLintableSourceFile(filePath)) return false;
41611
42033
  return !isFileIgnoredByPatterns(filePath, rootDirectory, ignoredPatterns);
41612
42034
  });
41613
42035
  };
@@ -41825,6 +42247,7 @@ const parseGithubViewerPermission = (stdout) => {
41825
42247
  return /^[A-Z_]+$/.test(value) ? value.toLowerCase() : null;
41826
42248
  };
41827
42249
  const splitNullSeparated = (value) => value.split("\0").filter((entry) => entry.length > 0);
42250
+ const UNTRACKED_FILE_LAST_LINE = Number.MAX_SAFE_INTEGER;
41828
42251
  /**
41829
42252
  * `Git` wraps every `git`-via-subprocess call react-doctor makes
41830
42253
  * behind a `Context.Service`. The production layer (`layerNode`)
@@ -41895,6 +42318,14 @@ var Git = class Git extends Service()("react-doctor/Git") {
41895
42318
  args,
41896
42319
  directory
41897
42320
  });
42321
+ const listUntrackedFilePaths = (directory, includePaths = []) => runGit(directory, [
42322
+ "ls-files",
42323
+ "-z",
42324
+ "--others",
42325
+ "--exclude-standard",
42326
+ ...includePaths.length > 0 ? ["--", ...includePaths] : []
42327
+ ]).pipe(map$3((result) => result.status === 0 ? splitNullSeparated(result.stdout) : null));
42328
+ const mergeUntracked = (directory, trackedFilePaths, includeUntracked) => includeUntracked ? listUntrackedFilePaths(directory).pipe(map$3((untracked) => untracked === null ? trackedFilePaths : [...new Set([...trackedFilePaths, ...untracked])])) : succeed$2(trackedFilePaths);
41898
42329
  const currentBranch = (directory) => runGit(directory, [
41899
42330
  "rev-parse",
41900
42331
  "--abbrev-ref",
@@ -42017,7 +42448,7 @@ var Git = class Git extends Service()("react-doctor/Git") {
42017
42448
  githubViewerPermission,
42018
42449
  branchExists,
42019
42450
  mergeBase,
42020
- diffSelection: ({ directory, explicitBaseBranch }) => gen(function* () {
42451
+ diffSelection: ({ directory, explicitBaseBranch, includeUntracked = false }) => gen(function* () {
42021
42452
  if (explicitBaseBranch !== void 0 && explicitBaseBranch.trim().length === 0) return yield* fail$4(new ReactDoctorError({ reason: new GitBaseBranchInvalid({ detail: "Diff base branch cannot be empty." }) }));
42022
42453
  if (explicitBaseBranch !== void 0) {
42023
42454
  const range = parseGitDiffRange(explicitBaseBranch);
@@ -42046,7 +42477,7 @@ var Git = class Git extends Service()("react-doctor/Git") {
42046
42477
  "HEAD"
42047
42478
  ]);
42048
42479
  if (uncommitted.status !== 0) return null;
42049
- const files = splitNullSeparated(uncommitted.stdout);
42480
+ const files = yield* mergeUntracked(directory, splitNullSeparated(uncommitted.stdout), includeUntracked);
42050
42481
  if (files.length === 0) return null;
42051
42482
  return {
42052
42483
  currentBranch: resolvedCurrentBranch,
@@ -42076,7 +42507,7 @@ var Git = class Git extends Service()("react-doctor/Git") {
42076
42507
  currentBranch: resolvedCurrentBranch,
42077
42508
  baseBranch,
42078
42509
  diffBaseRef: mergeBaseRef,
42079
- changedFiles: splitNullSeparated(diff.stdout),
42510
+ changedFiles: yield* mergeUntracked(directory, splitNullSeparated(diff.stdout), includeUntracked),
42080
42511
  isCurrentChanges: false
42081
42512
  };
42082
42513
  }).pipe(withSpan("Git.diffSelection")),
@@ -42122,7 +42553,7 @@ var Git = class Git extends Service()("react-doctor/Git") {
42122
42553
  stdout: result.stdout
42123
42554
  };
42124
42555
  }).pipe(withSpan("Git.grep")),
42125
- changedLineRanges: ({ directory, baseRef, cached, files }) => gen(function* () {
42556
+ changedLineRanges: ({ directory, baseRef, cached, files, includeUntracked = false }) => gen(function* () {
42126
42557
  if (files.length === 0) return [];
42127
42558
  if (baseRef !== void 0 && !isSafeGitRevision(baseRef)) return null;
42128
42559
  const result = yield* runGit(directory, [
@@ -42136,7 +42567,14 @@ var Git = class Git extends Service()("react-doctor/Git") {
42136
42567
  ...files
42137
42568
  ]);
42138
42569
  if (result.status !== 0) return null;
42139
- return parseChangedLineRanges(result.stdout);
42570
+ const changedLineRanges = parseChangedLineRanges(result.stdout);
42571
+ if (cached || !includeUntracked) return changedLineRanges;
42572
+ const untrackedFilePaths = yield* listUntrackedFilePaths(directory, files);
42573
+ if (untrackedFilePaths === null) return changedLineRanges;
42574
+ return [...changedLineRanges, ...untrackedFilePaths.map((file) => ({
42575
+ file,
42576
+ ranges: [[1, UNTRACKED_FILE_LAST_LINE]]
42577
+ }))];
42140
42578
  }).pipe(catch_$1((error) => error.reason._tag === "GitInvocationFailed" ? succeed$2(null) : fail$4(error)), withSpan("Git.changedLineRanges"))
42141
42579
  });
42142
42580
  })).pipe(provide$2(layer$3.pipe(provide$2(mergeAll$1(layer$2, layer$1)))));
@@ -42721,13 +43159,13 @@ const encodeSidecarFileEntry = (entry) => ({
42721
43159
  diagnostics: entry.diagnostics
42722
43160
  });
42723
43161
  const emptyCache = () => ({
42724
- version: 1,
43162
+ version: 2,
42725
43163
  rulesets: {}
42726
43164
  });
42727
43165
  const loadRulesetEntries = (cacheFilePath, rulesetHash) => {
42728
43166
  const entries = /* @__PURE__ */ new Map();
42729
43167
  const persisted = failOpenReadJson(cacheFilePath, emptyCache());
42730
- if (persisted.version !== 1 || !isRecord$1(persisted.rulesets)) return entries;
43168
+ if (persisted.version !== 2 || !isRecord$1(persisted.rulesets)) return entries;
42731
43169
  const bucket = persisted.rulesets[rulesetHash];
42732
43170
  if (!isRecord$1(bucket) || !isRecord$1(bucket.files)) return entries;
42733
43171
  for (const [fileKey, rawEntry] of Object.entries(bucket.files)) {
@@ -42747,7 +43185,7 @@ const createSidecarLintCache = (cacheDirectory, rulesetHash) => {
42747
43185
  },
42748
43186
  persist: () => {
42749
43187
  const onDisk = failOpenReadJson(cacheFilePath, emptyCache());
42750
- const rulesets = onDisk.version === 1 && isRecord$1(onDisk.rulesets) ? { ...onDisk.rulesets } : {};
43188
+ const rulesets = onDisk.version === 2 && isRecord$1(onDisk.rulesets) ? { ...onDisk.rulesets } : {};
42751
43189
  const existingBucket = rulesets[rulesetHash];
42752
43190
  const existingFiles = isRecord$1(existingBucket) && isRecord$1(existingBucket.files) ? existingBucket.files : {};
42753
43191
  const ourFiles = {};
@@ -42761,7 +43199,7 @@ const createSidecarLintCache = (cacheDirectory, rulesetHash) => {
42761
43199
  files: Object.fromEntries(cappedEntries)
42762
43200
  };
42763
43201
  atomicWriteJson(cacheFilePath, {
42764
- version: 1,
43202
+ version: 2,
42765
43203
  rulesets: Object.fromEntries(Object.entries(rulesets).filter(([, bucket]) => isRecord$1(bucket) && typeof bucket.updatedAtMs === "number").sort(([, first], [, second]) => second.updatedAtMs - first.updatedAtMs).slice(0, 8))
42766
43204
  });
42767
43205
  }
@@ -42812,23 +43250,6 @@ const resolveTsConfigRelativePath = (rootDirectory) => {
42812
43250
  for (const filename of TSCONFIG_FILENAMES) if (NFS.existsSync(Path.join(rootDirectory, filename))) return `./${filename}`;
42813
43251
  return null;
42814
43252
  };
42815
- const DERIVED_STATE_RULE_PRIORITY = new Map([
42816
- ["no-initialize-state", 0],
42817
- ["no-adjust-state-on-prop-change", 1],
42818
- ["no-derived-state", 2],
42819
- ["no-derived-state-effect", 3]
42820
- ]);
42821
- const doDiagnosticSpansOverlap = (first, second) => {
42822
- if (first.filePath !== second.filePath || first.plugin !== second.plugin) return false;
42823
- if (first.offset !== void 0 && second.offset !== void 0) {
42824
- const firstEnd = first.offset + Math.max(first.length ?? 1, 1);
42825
- const secondEnd = second.offset + Math.max(second.length ?? 1, 1);
42826
- return first.offset < secondEnd && second.offset < firstEnd;
42827
- }
42828
- const firstEndLine = first.endLine ?? first.line;
42829
- const secondEndLine = second.endLine ?? second.line;
42830
- return first.line <= secondEndLine && second.line <= firstEndLine;
42831
- };
42832
43253
  const dedupeDiagnostics = (diagnostics) => {
42833
43254
  const seenKeys = /* @__PURE__ */ new Set();
42834
43255
  const uniqueDiagnostics = [];
@@ -42836,16 +43257,6 @@ const dedupeDiagnostics = (diagnostics) => {
42836
43257
  const key = `${diagnostic.filePath}\u0000${diagnostic.line}\u0000${diagnostic.column}\u0000${diagnostic.plugin}\u0000${diagnostic.rule}\u0000${diagnostic.severity}\u0000${diagnostic.message}`;
42837
43258
  if (seenKeys.has(key)) continue;
42838
43259
  seenKeys.add(key);
42839
- const priority = DERIVED_STATE_RULE_PRIORITY.get(diagnostic.rule);
42840
- if (priority !== void 0) {
42841
- const overlappingDiagnosticIndex = uniqueDiagnostics.findIndex((candidate) => candidate.rule !== diagnostic.rule && candidate.severity === diagnostic.severity && DERIVED_STATE_RULE_PRIORITY.has(candidate.rule) && doDiagnosticSpansOverlap(candidate, diagnostic));
42842
- if (overlappingDiagnosticIndex >= 0) {
42843
- const existingDiagnostic = uniqueDiagnostics[overlappingDiagnosticIndex];
42844
- const existingPriority = existingDiagnostic ? DERIVED_STATE_RULE_PRIORITY.get(existingDiagnostic.rule) : void 0;
42845
- if (existingPriority !== void 0 && priority < existingPriority) uniqueDiagnostics[overlappingDiagnosticIndex] = diagnostic;
42846
- continue;
42847
- }
42848
- }
42849
43260
  uniqueDiagnostics.push(diagnostic);
42850
43261
  }
42851
43262
  return uniqueDiagnostics;
@@ -42888,6 +43299,12 @@ const mapWithConcurrency = async (items, concurrency, task) => {
42888
43299
  if (errors.length > 0) throw errors[0];
42889
43300
  return results;
42890
43301
  };
43302
+ const lineOfUtf8Offset = (sourceBuffer, utf8Offset) => {
43303
+ let lineNumber = 1;
43304
+ const scanEnd = Math.min(utf8Offset, sourceBuffer.length);
43305
+ for (let byteIndex = 0; byteIndex < scanEnd; byteIndex++) if (sourceBuffer[byteIndex] === 10) lineNumber++;
43306
+ return lineNumber;
43307
+ };
42891
43308
  const REANIMATED_SHARED_VALUE_HINT = "If this is a Reanimated shared value, prefer its React Compiler-compatible `.get()` / `.set()` accessors over `.value` — https://docs.swmansion.com/react-native-reanimated/docs/core/useSharedValue/#react-compiler-support";
42892
43309
  const ACCESSORS_MIN_VERSION = "3.15.0";
42893
43310
  const supportsAccessors = (reanimatedVersion) => {
@@ -43061,23 +43478,12 @@ const REACT_USE_BINDING_RESOLUTION = {
43061
43478
  isReactUseBinding: true,
43062
43479
  isReactNamespaceBinding: false
43063
43480
  };
43064
- const getScriptKind$2 = (filename) => {
43065
- if (filename.endsWith(".tsx")) return ts.ScriptKind.TSX;
43066
- if (filename.endsWith(".jsx")) return ts.ScriptKind.JSX;
43067
- if (filename.endsWith(".ts")) return ts.ScriptKind.TS;
43068
- return ts.ScriptKind.JS;
43069
- };
43070
43481
  const getUtf16Offset$1 = (sourceText, utf8Offset) => Buffer.from(sourceText).subarray(0, utf8Offset).toString("utf8").length;
43071
- const unwrapExpression = (expression) => {
43072
- let currentExpression = expression;
43073
- while (ts.isParenthesizedExpression(currentExpression) || ts.isAsExpression(currentExpression) || ts.isSatisfiesExpression(currentExpression) || ts.isNonNullExpression(currentExpression) || ts.isTypeAssertionExpression(currentExpression)) currentExpression = currentExpression.expression;
43074
- return currentExpression;
43075
- };
43076
43482
  const getStaticPropertyName = (node) => {
43077
43483
  if (!node) return null;
43078
43484
  if (ts.isIdentifier(node) || ts.isStringLiteral(node) || ts.isNumericLiteral(node)) return node.text;
43079
43485
  if (ts.isComputedPropertyName(node)) {
43080
- const expression = unwrapExpression(node.expression);
43486
+ const expression = unwrapTypescriptExpression(node.expression);
43081
43487
  if (ts.isStringLiteral(expression) || ts.isNoSubstitutionTemplateLiteral(expression)) return expression.text;
43082
43488
  }
43083
43489
  return null;
@@ -43106,7 +43512,7 @@ const isReactUseObjectBindingElement = (bindingElement) => {
43106
43512
  return getStaticPropertyName(bindingElement.propertyName) === USE_IDENTIFIER;
43107
43513
  };
43108
43514
  const isReactRequireCall = (expression) => {
43109
- const unwrappedExpression = unwrapExpression(expression);
43515
+ const unwrappedExpression = unwrapTypescriptExpression(expression);
43110
43516
  return ts.isCallExpression(unwrappedExpression) && ts.isIdentifier(unwrappedExpression.expression) && unwrappedExpression.expression.text === REQUIRE_IDENTIFIER && unwrappedExpression.arguments.length === 1 && ts.isStringLiteral(unwrappedExpression.arguments[0]) && unwrappedExpression.arguments[0].text === REACT_MODULE_SOURCE;
43111
43517
  };
43112
43518
  const getModuleSource = (node) => {
@@ -43208,7 +43614,7 @@ const isIdentifierShadowedByLocalBinding = (identifier, sourceFile) => {
43208
43614
  return false;
43209
43615
  };
43210
43616
  const isReactNamespaceExpression = (expression, reactImportBindings, sourceFile, visitedDeclarations) => {
43211
- const unwrappedExpression = unwrapExpression(expression);
43617
+ const unwrappedExpression = unwrapTypescriptExpression(expression);
43212
43618
  if (isReactRequireCall(unwrappedExpression)) return true;
43213
43619
  if (!ts.isIdentifier(unwrappedExpression)) return false;
43214
43620
  if (reactImportBindings.namespaceNames.has(unwrappedExpression.text) && !isIdentifierShadowedByLocalBinding(unwrappedExpression, sourceFile)) return true;
@@ -43216,7 +43622,7 @@ const isReactNamespaceExpression = (expression, reactImportBindings, sourceFile,
43216
43622
  };
43217
43623
  const isReactUseExpression = (expression, reactImportBindings, sourceFile, visitedDeclarations) => {
43218
43624
  if (!expression) return false;
43219
- const unwrappedExpression = unwrapExpression(expression);
43625
+ const unwrappedExpression = unwrapTypescriptExpression(expression);
43220
43626
  if (ts.isIdentifier(unwrappedExpression)) {
43221
43627
  if (reactImportBindings.useImportNames.has(unwrappedExpression.text) && !isIdentifierShadowedByLocalBinding(unwrappedExpression, sourceFile)) return true;
43222
43628
  if (unwrappedExpression.text === USE_IDENTIFIER) return false;
@@ -43316,7 +43722,7 @@ const findUseCallIdentifier = (sourceFile, useOffset) => {
43316
43722
  return matchedIdentifier;
43317
43723
  };
43318
43724
  const resolveUseCallBinding = (sourceText, filename, utf8Offset) => {
43319
- const sourceFile = ts.createSourceFile(filename, sourceText, ts.ScriptTarget.Latest, true, getScriptKind$2(filename));
43725
+ const sourceFile = ts.createSourceFile(filename, sourceText, ts.ScriptTarget.Latest, true, getTypescriptScriptKind(filename));
43320
43726
  const useIdentifier = findUseCallIdentifier(sourceFile, getUtf16Offset$1(sourceText, utf8Offset));
43321
43727
  if (!useIdentifier) return null;
43322
43728
  return resolveIdentifierBinding(useIdentifier, collectReactImportBindings(sourceFile), sourceFile);
@@ -43352,12 +43758,6 @@ const WORKLET_ACCEPTING_CALLEES = new Set([
43352
43758
  "executeOnUIRuntimeSync"
43353
43759
  ]);
43354
43760
  const GESTURE_ROOT_IDENTIFIER = "Gesture";
43355
- const getScriptKind$1 = (filename) => {
43356
- if (filename.endsWith(".tsx")) return ts.ScriptKind.TSX;
43357
- if (filename.endsWith(".jsx")) return ts.ScriptKind.JSX;
43358
- if (filename.endsWith(".ts")) return ts.ScriptKind.TS;
43359
- return ts.ScriptKind.JS;
43360
- };
43361
43761
  const getUtf16Offset = (sourceText, utf8Offset) => Buffer.from(sourceText).subarray(0, utf8Offset).toString("utf8").length;
43362
43762
  const hasWorkletDirective = (node) => {
43363
43763
  if (!ts.isFunctionDeclaration(node) && !ts.isFunctionExpression(node) && !ts.isArrowFunction(node) && !ts.isMethodDeclaration(node)) return false;
@@ -43429,17 +43829,11 @@ const shouldSuppressCompilerFindingInWorklet = (diagnostic, project, rootDirecto
43429
43829
  } catch {
43430
43830
  return false;
43431
43831
  }
43432
- return isOffsetInsideWorklet(ts.createSourceFile(absolutePath, sourceText, ts.ScriptTarget.Latest, true, getScriptKind$1(absolutePath)), getUtf16Offset(sourceText, primaryLabel.span.offset));
43832
+ return isOffsetInsideWorklet(ts.createSourceFile(absolutePath, sourceText, ts.ScriptTarget.Latest, true, getTypescriptScriptKind(absolutePath)), getUtf16Offset(sourceText, primaryLabel.span.offset));
43433
43833
  };
43434
43834
  const MANUAL_MEMOIZATION_PLUGIN = "react-doctor";
43435
43835
  const MANUAL_MEMOIZATION_RULE = "react-compiler-no-manual-memoization";
43436
43836
  const REACT_COMPILER_PLUGIN = "react-hooks-js";
43437
- const getScriptKind = (filename) => {
43438
- if (filename.endsWith(".tsx")) return ts.ScriptKind.TSX;
43439
- if (filename.endsWith(".jsx")) return ts.ScriptKind.JSX;
43440
- if (filename.endsWith(".ts")) return ts.ScriptKind.TS;
43441
- return ts.ScriptKind.JS;
43442
- };
43443
43837
  const isManualMemoizationDiagnostic = (diagnostic) => diagnostic.plugin === MANUAL_MEMOIZATION_PLUGIN && diagnostic.rule === MANUAL_MEMOIZATION_RULE;
43444
43838
  const nodeLineRange = (sourceFile, node) => ({
43445
43839
  startLine: sourceFile.getLineAndCharacterOfPosition(node.getStart(sourceFile)).line + 1,
@@ -43459,13 +43853,6 @@ const findOutermostEnclosingFunctionRange = (sourceFile, targetLine) => {
43459
43853
  visit(sourceFile);
43460
43854
  return foundRange;
43461
43855
  };
43462
- const NEWLINE_BYTE = 10;
43463
- const lineOfUtf8Offset = (sourceBuffer, utf8Offset) => {
43464
- let lineNumber = 1;
43465
- const scanEnd = Math.min(utf8Offset, sourceBuffer.length);
43466
- for (let byteIndex = 0; byteIndex < scanEnd; byteIndex++) if (sourceBuffer[byteIndex] === NEWLINE_BYTE) lineNumber++;
43467
- return lineNumber;
43468
- };
43469
43856
  /**
43470
43857
  * Drops `react-doctor/react-compiler-no-manual-memoization` diagnostics
43471
43858
  * whose enclosing function also carries a `react-hooks-js` (React
@@ -43508,7 +43895,7 @@ const suppressMemoizationInBailedOutFunctions = (diagnostics, rootDirectory) =>
43508
43895
  try {
43509
43896
  const buffer = NFS.readFileSync(absolutePath);
43510
43897
  parsed = {
43511
- sourceFile: ts.createSourceFile(absolutePath, buffer.toString("utf8"), ts.ScriptTarget.Latest, true, getScriptKind(absolutePath)),
43898
+ sourceFile: ts.createSourceFile(absolutePath, buffer.toString("utf8"), ts.ScriptTarget.Latest, true, getTypescriptScriptKind(absolutePath)),
43512
43899
  buffer
43513
43900
  };
43514
43901
  } catch {
@@ -43646,11 +44033,11 @@ const buildRelatedLocations = (labels, filePath) => {
43646
44033
  }
43647
44034
  return related;
43648
44035
  };
43649
- const isOxlintOutput = (value) => {
43650
- if (typeof value !== "object" || value === null) return false;
43651
- const candidate = value;
43652
- return Array.isArray(candidate.diagnostics);
43653
- };
44036
+ const isOxlintSpan = (value) => isRecord$1(value) && typeof value.offset === "number" && typeof value.length === "number" && typeof value.line === "number" && typeof value.column === "number";
44037
+ const isOxlintLabel = (value) => isRecord$1(value) && isOxlintSpan(value.span);
44038
+ const isMappableOxlintDiagnostic = (value) => isRecord$1(value) && typeof value.code === "string" && value.code.length > 0 && typeof value.filename === "string" && value.filename.length > 0 && (value.severity === "warning" || value.severity === "error") && Array.isArray(value.labels) && value.labels.every(isOxlintLabel);
44039
+ const isEngineFailureDiagnostic = (value) => !isRecord$1(value) || typeof value.filename !== "string" || value.filename.length === 0;
44040
+ const isOxlintOutput = (value) => isRecord$1(value) && Array.isArray(value.diagnostics);
43654
44041
  /**
43655
44042
  * Parses one oxlint subprocess's stdout into a flat `Diagnostic[]`.
43656
44043
  * Tolerates the leading-notice-line shape oxlint sometimes prints
@@ -43669,23 +44056,59 @@ const parseOxlintOutput = (stdout, project, rootDirectory) => {
43669
44056
  throw new ReactDoctorError({ reason: new OxlintOutputUnparseable({ preview: stdout.slice(0, 600) }) });
43670
44057
  }
43671
44058
  if (!isOxlintOutput(parsed)) throw new ReactDoctorError({ reason: new OxlintOutputUnparseable({ preview: stdout.slice(0, 600) }) });
44059
+ const engineFailureDiagnostic = parsed.diagnostics.find(isEngineFailureDiagnostic);
44060
+ if (engineFailureDiagnostic !== void 0) throw new ReactDoctorError({ reason: new OxlintOutputUnparseable({ preview: JSON.stringify(engineFailureDiagnostic).slice(0, 600) }) });
43672
44061
  const minifiedFileCache = /* @__PURE__ */ new Map();
44062
+ const sourceBufferCache = /* @__PURE__ */ new Map();
44063
+ const sourceLinesCache = /* @__PURE__ */ new Map();
44064
+ const resolveAbsolutePath = (filename) => Path.isAbsolute(filename) ? filename : Path.resolve(rootDirectory || ".", filename);
44065
+ const readSourceBuffer = (filename) => {
44066
+ const absolutePath = resolveAbsolutePath(filename);
44067
+ const cached = sourceBufferCache.get(absolutePath);
44068
+ if (cached !== void 0) return cached;
44069
+ let sourceBuffer;
44070
+ try {
44071
+ sourceBuffer = NFS.readFileSync(absolutePath);
44072
+ } catch {
44073
+ sourceBuffer = null;
44074
+ }
44075
+ sourceBufferCache.set(absolutePath, sourceBuffer);
44076
+ return sourceBuffer;
44077
+ };
44078
+ const readSourceLines = (filename) => {
44079
+ const absolutePath = resolveAbsolutePath(filename);
44080
+ const cached = sourceLinesCache.get(absolutePath);
44081
+ if (cached !== void 0) return cached;
44082
+ const sourceBuffer = readSourceBuffer(filename);
44083
+ const sourceLines = sourceBuffer ? sourceBuffer.toString("utf8").split("\n") : null;
44084
+ sourceLinesCache.set(absolutePath, sourceLines);
44085
+ return sourceLines;
44086
+ };
43673
44087
  const isMinifiedDiagnosticFile = (filename) => {
43674
- const absolutePath = Path.isAbsolute(filename) ? filename : Path.resolve(rootDirectory || ".", filename);
44088
+ const absolutePath = resolveAbsolutePath(filename);
43675
44089
  const cached = minifiedFileCache.get(absolutePath);
43676
44090
  if (cached !== void 0) return cached;
43677
44091
  const minified = isMinifiedSource(absolutePath);
43678
44092
  minifiedFileCache.set(absolutePath, minified);
43679
44093
  return minified;
43680
44094
  };
43681
- return suppressMemoizationInBailedOutFunctions(parsed.diagnostics.filter((diagnostic) => diagnostic.code && isLintableSourceFile(diagnostic.filename) && !isMinifiedDiagnosticFile(diagnostic.filename) && !shouldSuppressLocalUseHookDiagnostic(diagnostic, rootDirectory) && !shouldSuppressCompilerFindingInWorklet(diagnostic, project, rootDirectory)).map((diagnostic) => {
44095
+ return suppressMemoizationInBailedOutFunctions(parsed.diagnostics.filter((diagnostic) => isMappableOxlintDiagnostic(diagnostic) && isLintableSourceFile(diagnostic.filename) && !isMinifiedDiagnosticFile(diagnostic.filename) && !shouldSuppressLocalUseHookDiagnostic(diagnostic, rootDirectory) && !shouldSuppressCompilerFindingInWorklet(diagnostic, project, rootDirectory)).map((diagnostic) => {
43682
44096
  const { plugin, rule } = parseRuleCode(diagnostic.code);
43683
44097
  const primaryLabel = diagnostic.labels[0];
43684
44098
  const cleaned = cleanDiagnosticMessage(diagnostic.message, diagnostic.help, plugin, rule, project);
43685
44099
  const normalizedFilePath = diagnostic.filename.replaceAll("\\", "/");
43686
44100
  const primarySpan = primaryLabel?.span;
44101
+ const sourceBuffer = primarySpan ? readSourceBuffer(diagnostic.filename) : null;
43687
44102
  const relatedLocations = buildRelatedLocations(diagnostic.labels, normalizedFilePath);
43688
44103
  const category = resolveDiagnosticCategory(plugin, rule);
44104
+ const matchByOccurrence = resolveMatchByOccurrence(rule, category);
44105
+ const sourceLines = primarySpan ? readSourceLines(diagnostic.filename) : null;
44106
+ const primaryLineIndex = primarySpan ? primarySpan.line - 1 : -1;
44107
+ const primaryLine = sourceLines?.[primaryLineIndex];
44108
+ const primaryColumnIndex = primarySpan ? primarySpan.column - 1 : -1;
44109
+ const isJsxTagLabel = primaryLine !== void 0 && primaryColumnIndex >= 0 && (primaryLine[primaryColumnIndex] === "<" || primaryLine[primaryColumnIndex - 1] === "<");
44110
+ const jsxOpenerEndLineIndex = sourceLines && isJsxTagLabel ? findJsxOpenerSpan(sourceLines, primaryLineIndex) : null;
44111
+ const primarySpanEndLine = jsxOpenerEndLineIndex !== null ? jsxOpenerEndLineIndex + 1 : primarySpan && sourceBuffer ? lineOfUtf8Offset(sourceBuffer, primarySpan.offset + Math.max(primarySpan.length - 1, 0)) : void 0;
43689
44112
  return {
43690
44113
  filePath: normalizedFilePath,
43691
44114
  plugin,
@@ -43699,10 +44122,11 @@ const parseOxlintOutput = (stdout, project, rootDirectory) => {
43699
44122
  column: primarySpan?.column ?? 0,
43700
44123
  ...primarySpan ? {
43701
44124
  offset: primarySpan.offset,
43702
- length: primarySpan.length
44125
+ length: primarySpan.length,
44126
+ ...primarySpanEndLine !== void 0 ? { endLine: primarySpanEndLine } : {}
43703
44127
  } : {},
43704
44128
  category,
43705
- ...resolveMatchByOccurrence(rule, category) ? { matchByOccurrence: true } : {},
44129
+ ...matchByOccurrence ? { matchByOccurrence: true } : {},
43706
44130
  ...relatedLocations.length > 0 ? { relatedLocations } : {}
43707
44131
  };
43708
44132
  }), rootDirectory);
@@ -45489,7 +45913,7 @@ const runInspect = (input, hooks = {}) => gen(function* () {
45489
45913
  directory: scanDirectory,
45490
45914
  repo
45491
45915
  }).pipe(orElseSucceed(() => null)) : succeed$2(null));
45492
- const lintIncludePaths = (input.skipJsxIncludeFilter ? input.includePaths.length > 0 ? [...input.includePaths] : void 0 : computeExplicitLintIncludePaths([...input.includePaths], project)) ?? resolveLintIncludePaths(scanDirectory, resolvedConfig.config, project);
45916
+ const lintIncludePaths = (input.skipExplicitIncludePathFilter ? input.includePaths.length > 0 ? [...input.includePaths] : void 0 : computeExplicitLintIncludePaths([...input.includePaths])) ?? resolveLintIncludePaths(scanDirectory, resolvedConfig.config);
45493
45917
  const fallbackScannedFilePaths = input.suppressScanSummary ? (lintIncludePaths ?? (yield* filesService.listSourceFiles(scanDirectory))).map((relativePath) => Path.resolve(scanDirectory, relativePath)) : [];
45494
45918
  const beforeLint = hooks.beforeLint ?? NO_HOOKS.beforeLint;
45495
45919
  const afterLint = hooks.afterLint ?? NO_HOOKS.afterLint;
@@ -45503,7 +45927,8 @@ const runInspect = (input, hooks = {}) => gen(function* () {
45503
45927
  respectInlineDisables: input.respectInlineDisables,
45504
45928
  showWarnings
45505
45929
  });
45506
- const applyPerElementPipeline = (rawStream) => rawStream.pipe(filterMap(filterMapNullable(transform.apply)), tap((diagnostic) => reporterService.emit(diagnostic)));
45930
+ const filterPerElementPipeline = (rawStream) => rawStream.pipe(filterMap(filterMapNullable(transform.apply)));
45931
+ const applyPerElementPipeline = (rawStream) => filterPerElementPipeline(rawStream).pipe(tap((diagnostic) => reporterService.emit(diagnostic)));
45507
45932
  const envCollected = yield* runCollect(applyPerElementPipeline(fromIterable$1(isDiffMode ? [] : [
45508
45933
  ...checkReducedMotion(scanDirectory),
45509
45934
  ...checkPnpmHardening(scanDirectory),
@@ -45632,7 +46057,7 @@ const runInspect = (input, hooks = {}) => gen(function* () {
45632
46057
  });
45633
46058
  return empty$4;
45634
46059
  }))));
45635
- const lintCollected = yield* runCollect(applyPerElementPipeline(shouldOverlapDeadCode ? baseLintStream.pipe(provideService(OxlintConcurrency, lintConcurrency)) : baseLintStream)).pipe(timeoutOption(lintPhaseTimeoutMs), flatMap$2(match$3({
46060
+ const lintCollected = dedupeRelatedDiagnostics(yield* runCollect(filterPerElementPipeline(shouldOverlapDeadCode ? baseLintStream.pipe(provideService(OxlintConcurrency, lintConcurrency)) : baseLintStream)).pipe(timeoutOption(lintPhaseTimeoutMs), flatMap$2(match$3({
45636
46061
  onNone: () => set(lintFailure, {
45637
46062
  didFail: true,
45638
46063
  reason: `Lint analysis exceeded ${lintPhaseTimeoutMs / MILLISECONDS_PER_SECOND}s and was skipped.`,
@@ -45640,7 +46065,8 @@ const runInspect = (input, hooks = {}) => gen(function* () {
45640
46065
  reasonKind: null
45641
46066
  }).pipe(as([])),
45642
46067
  onSome: succeed$2
45643
- })));
46068
+ }))));
46069
+ yield* forEach$1(lintCollected, reporterService.emit, { discard: true });
45644
46070
  const lintFailureState = yield* get$2(lintFailure);
45645
46071
  yield* afterLint(lintFailureState.didFail);
45646
46072
  if (lintFailureState.didFail) yield* scanProgress.fail(formatLintFailText(lintFailureState.reasonTag, process.version));
@@ -46234,10 +46660,11 @@ const computeDiagnosticDelta = (input) => {
46234
46660
  * render diff-base mistakes (`GitBaseBranchInvalid` /
46235
46661
  * `GitBaseBranchMissing`) as clean user errors instead of crashes.
46236
46662
  */
46237
- const getDiffInfo = (directory, explicitBaseBranch) => runPromise(gen(function* () {
46663
+ const getDiffInfo = (directory, explicitBaseBranch, includeUntracked) => runPromise(gen(function* () {
46238
46664
  const selection = yield* (yield* Git).diffSelection({
46239
46665
  directory,
46240
- explicitBaseBranch
46666
+ explicitBaseBranch,
46667
+ includeUntracked
46241
46668
  });
46242
46669
  if (selection === null) return null;
46243
46670
  return {
@@ -46573,6 +47000,43 @@ const isNonInteractiveEnvironment = () => NON_INTERACTIVE_ENVIRONMENT_VARIABLES.
46573
47000
  //#region src/cli/utils/constants.ts
46574
47001
  const REACT_DOCTOR_CONFIG_PROJECT_NAME = "react-doctor";
46575
47002
  const STAGED_FILES_TEMP_DIR_PREFIX = "react-doctor-staged-";
47003
+ const STAGED_SNAPSHOT_ADDITIONAL_CONFIG_FILENAMES = [
47004
+ ".babelrc",
47005
+ ".babelrc.json",
47006
+ ".eslintignore",
47007
+ ".eslintrc.json",
47008
+ ".oxlintignore",
47009
+ "app.config.cjs",
47010
+ "app.config.js",
47011
+ "app.config.json",
47012
+ "app.config.mjs",
47013
+ "app.config.ts",
47014
+ "app.json",
47015
+ "babel.config.cjs",
47016
+ "babel.config.js",
47017
+ "babel.config.json",
47018
+ "babel.config.mjs",
47019
+ "eslint.config.cjs",
47020
+ "eslint.config.cts",
47021
+ "eslint.config.js",
47022
+ "eslint.config.mjs",
47023
+ "eslint.config.mts",
47024
+ "eslint.config.ts",
47025
+ "next.config.cjs",
47026
+ "next.config.js",
47027
+ "next.config.mjs",
47028
+ "next.config.ts",
47029
+ "oxlint.config.json",
47030
+ "react-doctor.config.json",
47031
+ "vite.config.cjs",
47032
+ "vite.config.cts",
47033
+ "vite.config.js",
47034
+ "vite.config.mjs",
47035
+ "vite.config.mts",
47036
+ "vite.config.ts",
47037
+ "vitest.config.js",
47038
+ "vitest.config.ts"
47039
+ ];
46576
47040
  const BASELINE_FILES_TEMP_DIR_PREFIX = "react-doctor-baseline-";
46577
47041
  const SCAN_RESULT_CACHE_FILENAME = "scan-cache.json";
46578
47042
  const RUN_GIT_MAX_BUFFER_BYTES = 64 * 1024 * 1024;
@@ -46587,6 +47051,7 @@ const METRIC = {
46587
47051
  cliInvoked: "cli.invoked",
46588
47052
  cliError: "cli.error",
46589
47053
  cliEnvironmentError: "cli.env_error",
47054
+ stagedSnapshotDivergence: "staged.snapshot_divergence",
46590
47055
  projectDetected: "project.detected",
46591
47056
  projectPathSelected: "project.path_selected",
46592
47057
  projectConfigSelected: "project.config_selected",
@@ -46667,7 +47132,7 @@ const makeNoopConsole = () => ({
46667
47132
  });
46668
47133
  //#endregion
46669
47134
  //#region src/cli/utils/version.ts
46670
- const VERSION = "0.7.6-dev.fbd85e3";
47135
+ const VERSION = "0.7.7-dev.04c7e36";
46671
47136
  //#endregion
46672
47137
  //#region src/cli/utils/json-mode.ts
46673
47138
  let context = null;
@@ -47036,13 +47501,13 @@ const isDevVersion = (version) => version === "0.0.0" || version.includes("-");
47036
47501
  * uploads source-map artifacts under, so stack frames symbolicate. Honors the
47037
47502
  * standard `SENTRY_RELEASE` override.
47038
47503
  */
47039
- const resolveSentryRelease = () => process.env.SENTRY_RELEASE || `react-doctor@0.7.6-dev.fbd85e3`;
47504
+ const resolveSentryRelease = () => process.env.SENTRY_RELEASE || `react-doctor@0.7.7-dev.04c7e36`;
47040
47505
  /**
47041
47506
  * Deployment environment shown in Sentry's environment filter. Defaults to
47042
47507
  * `production` for tagged releases and `development` for dev/unbuilt versions,
47043
47508
  * overridable via the standard `SENTRY_ENVIRONMENT` env var.
47044
47509
  */
47045
- const resolveSentryEnvironment = () => process.env.SENTRY_ENVIRONMENT || (isDevVersion("0.7.6-dev.fbd85e3") ? "development" : "production");
47510
+ const resolveSentryEnvironment = () => process.env.SENTRY_ENVIRONMENT || (isDevVersion("0.7.7-dev.04c7e36") ? "development" : "production");
47046
47511
  /**
47047
47512
  * Performance-tracing sample rate in `[0, 1]`. Reads `SENTRY_TRACES_SAMPLE_RATE`
47048
47513
  * (set to `0` to disable tracing) and falls back to
@@ -47234,7 +47699,7 @@ const NON_BLOCKING_REACT_DOCTOR_COMMAND = [
47234
47699
  "fi"
47235
47700
  ].join(" ");
47236
47701
  const PACKAGE_JSON_FILE_NAME = "package.json";
47237
- const runGit = (projectRoot, args) => {
47702
+ const runGitRaw = (projectRoot, args) => {
47238
47703
  try {
47239
47704
  return execFileSync("git", [...args], {
47240
47705
  cwd: projectRoot,
@@ -47245,11 +47710,12 @@ const runGit = (projectRoot, args) => {
47245
47710
  "ignore"
47246
47711
  ],
47247
47712
  maxBuffer: RUN_GIT_MAX_BUFFER_BYTES
47248
- }).trim();
47713
+ });
47249
47714
  } catch {
47250
47715
  return null;
47251
47716
  }
47252
47717
  };
47718
+ const runGit = (projectRoot, args) => runGitRaw(projectRoot, args)?.trim() ?? null;
47253
47719
  const resolveGitPath = (baseDirectory, value) => Path.isAbsolute(value) ? value : Path.resolve(baseDirectory, value);
47254
47720
  const isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
47255
47721
  const getPackageJsonPath = (projectRoot) => Path.join(projectRoot, PACKAGE_JSON_FILE_NAME);
@@ -50807,7 +51273,7 @@ const BLOCKING_SUMMARY = {
50807
51273
  const SCOPE_SUMMARY = {
50808
51274
  full: "Scan the whole project on every run",
50809
51275
  files: "Report every issue in changed files",
50810
- lines: "Report issues on changed lines",
51276
+ lines: "Report issues whose source spans touch changed lines",
50811
51277
  changed: "Report only the issues a change introduces"
50812
51278
  };
50813
51279
  const gatesEqual = (left, right) => left.blocking === right.blocking && left.scope === right.scope && left.comment === right.comment && left.reviewComments === right.reviewComments && left.commitStatus === right.commitStatus;
@@ -59120,6 +59586,17 @@ const buildRuleBlastRadii = (diagnostics) => buildSortedRuleGroups(diagnostics).
59120
59586
  })).toSorted((left, right) => right.fileCount - left.fileCount);
59121
59587
  const findMigrationScaleBuckets = (diagnostics) => buildRuleBlastRadii(diagnostics).filter((bucket) => bucket.fileCount >= 40);
59122
59588
  //#endregion
59589
+ //#region src/cli/utils/has-lint-hard-failure.ts
59590
+ const hasLintHardFailure = (result) => result.skippedChecks.includes("lint");
59591
+ //#endregion
59592
+ //#region src/cli/utils/is-inspect-result-complete.ts
59593
+ const isInspectResultComplete = (result) => isScanComplete({
59594
+ analyzedFileCount: result.analyzedFiles?.length,
59595
+ scannedFileCount: result.scannedFileCount,
59596
+ skippedCheckCount: result.skippedChecks.length,
59597
+ skippedCheckReasonCount: Object.keys(result.skippedCheckReasons ?? {}).length
59598
+ });
59599
+ //#endregion
59123
59600
  //#region src/cli/utils/scan-result-cache.ts
59124
59601
  const CACHE_DISABLED_VALUES = new Set(["1", "true"]);
59125
59602
  const TOOLCHAIN_PACKAGE_SPECIFIERS = [
@@ -59202,7 +59679,7 @@ const buildDirtyPathContentFingerprint = (absolutePath, statusCode) => {
59202
59679
  return null;
59203
59680
  }
59204
59681
  if (!stats.isFile()) return null;
59205
- if (stats.size > 10485760) return `stat:${stats.mtimeMs}:${stats.size}`;
59682
+ if (stats.size > 10485760) return null;
59206
59683
  return hashFileContents(absolutePath);
59207
59684
  };
59208
59685
  const buildWorktreeFingerprint = (projectDirectory) => {
@@ -59525,15 +60002,12 @@ const buildOutcomeAttributes = (input) => {
59525
60002
  const summary = summarizeDiagnostics(result.diagnostics);
59526
60003
  const blockingLevel = resolveTelemetryBlocking(input.userConfig);
59527
60004
  const gateDiagnostics = filterDiagnosticsForSurface(result.diagnostics, "ciFailure", input.userConfig);
60005
+ const complete = isInspectResultComplete(result);
60006
+ const didLintHardFail = hasLintHardFailure(result);
60007
+ const failsOnHardFailure = didLintHardFail && blockingLevel !== "none";
59528
60008
  const wouldBlock = !input.scoreOnly && !input.gateExempt && shouldBlockCi(gateDiagnostics, blockingLevel);
59529
- const complete = isScanComplete({
59530
- analyzedFileCount: result.analyzedFiles?.length,
59531
- scannedFileCount: result.scannedFileCount,
59532
- skippedCheckCount: result.skippedChecks.length,
59533
- skippedCheckReasonCount: Object.keys(result.skippedCheckReasons ?? {}).length
59534
- });
59535
60009
  const isClean = result.diagnostics.length === 0 && complete;
59536
- const outcome = wouldBlock ? "blocked" : isClean ? "clean" : "ok";
60010
+ const outcome = didLintHardFail ? "error" : wouldBlock ? "blocked" : isClean ? "clean" : "ok";
59537
60011
  const firings = summarizeRuleFirings(result.diagnostics);
59538
60012
  const countByRule = /* @__PURE__ */ new Map();
59539
60013
  const countByCategory = /* @__PURE__ */ new Map();
@@ -59578,7 +60052,7 @@ const buildOutcomeAttributes = (input) => {
59578
60052
  const attributes = {
59579
60053
  ...withNamespace("outcome", {
59580
60054
  status: outcome,
59581
- exitCode: wouldBlock ? 1 : 0,
60055
+ exitCode: failsOnHardFailure || wouldBlock ? 1 : 0,
59582
60056
  wouldBlock,
59583
60057
  blocking: blockingLevel,
59584
60058
  clean: isClean,
@@ -59676,7 +60150,9 @@ const buildScanAttributes = (input) => {
59676
60150
  hasCustomConfig: input.hasCustomConfig,
59677
60151
  rulesConfigured: ruleKeys.length,
59678
60152
  rulesDisabled: ruleKeys.filter((key) => ruleOverrides[key] === "off").length,
59679
- fileCount: input.result?.scannedFileCount ?? null
60153
+ fileCount: input.result?.scannedFileCount ?? null,
60154
+ nonJsxFileCount: input.result?.analyzedFiles?.filter((filePath) => !JSX_FILE_PATTERN.test(filePath)).length ?? null,
60155
+ multilineDiagnosticCount: input.result?.diagnostics.filter((diagnostic) => (diagnostic.endLine ?? diagnostic.line) > diagnostic.line).length ?? null
59680
60156
  });
59681
60157
  };
59682
60158
  /**
@@ -59745,6 +60221,12 @@ const countDeadlineSkippedFiles = (lintPartialFailures) => sumFileCountsMatching
59745
60221
  const DROPPED_FILES_MESSAGE_PATTERN = /^(\d+) file\(s\) failed to lint and were skipped/;
59746
60222
  const countDroppedLintFiles = (lintPartialFailures) => sumFileCountsMatching(lintPartialFailures, DROPPED_FILES_MESSAGE_PATTERN);
59747
60223
  //#endregion
60224
+ //#region src/cli/utils/diagnostic-intersects-line-ranges.ts
60225
+ const diagnosticIntersectsLineRanges = (diagnostic, lineRanges) => {
60226
+ const diagnosticEndLine = Math.max(diagnostic.line, diagnostic.endLine ?? diagnostic.line);
60227
+ return lineRanges.some(([rangeStartLine, rangeEndLine]) => diagnostic.line <= rangeEndLine && diagnosticEndLine >= rangeStartLine);
60228
+ };
60229
+ //#endregion
59748
60230
  //#region src/cli/utils/materialize-baseline-files.ts
59749
60231
  /**
59750
60232
  * Materializes the `ref` version of `files` into `tempDirectory` (via
@@ -60930,7 +61412,7 @@ const buildChangedLineMatcher = (directory, changedLineRanges) => {
60930
61412
  const relativePath = toForwardSlashes(path.isAbsolute(diagnostic.filePath) ? path.relative(directory, diagnostic.filePath) : diagnostic.filePath);
60931
61413
  const ranges = rangesByFile.get(relativePath);
60932
61414
  if (ranges === void 0) return false;
60933
- return ranges.some(([start, end]) => diagnostic.line >= start && diagnostic.line <= end);
61415
+ return diagnosticIntersectsLineRanges(diagnostic, ranges);
60934
61416
  };
60935
61417
  };
60936
61418
  const buildIgnoredTags = (userConfig) => {
@@ -63752,10 +64234,15 @@ const warnIfAiTrainingEnvironment = () => {
63752
64234
  //#region src/cli/utils/validate-mode-flags.ts
63753
64235
  const usedDiffAlias = (flags) => flags.diff !== void 0 && flags.diff !== false && flags.diff !== "false" && flags.diff !== "";
63754
64236
  const usedScope = (flags) => typeof flags.scope === "string" && flags.scope.length > 0;
64237
+ const validateIncludeUntrackedScope = (includeUntracked, scope) => {
64238
+ if (!includeUntracked || scope !== void 0 && scope !== "full") return;
64239
+ throw new CliInputError("--include-untracked requires the files, changed, or lines scope (via --scope, --diff, or config).");
64240
+ };
63755
64241
  const validateModeFlags = (flags) => {
63756
64242
  if (usedScope(flags) && usedDiffAlias(flags)) throw new CliInputError("Cannot combine --scope and --diff; --diff is the deprecated alias.");
63757
64243
  if (flags.staged && usedDiffAlias(flags)) throw new CliInputError("Cannot combine --staged and --diff; pick one mode.");
63758
64244
  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.`);
64245
+ if (flags.includeUntracked && flags.staged) throw new CliInputError("Cannot combine --include-untracked with --staged; the git index never holds untracked files.");
63759
64246
  if (flags.score && flags.json) throw new CliInputError("Cannot combine --score and --json; pick one output mode.");
63760
64247
  if (flags.score && flags.telemetry === false) throw new CliInputError("Cannot combine --score with --no-telemetry; --score prints the score that --no-telemetry disables.");
63761
64248
  if (flags.debug && (flags.score === false || flags.telemetry === false)) {
@@ -63764,6 +64251,39 @@ const validateModeFlags = (flags) => {
63764
64251
  }
63765
64252
  };
63766
64253
  //#endregion
64254
+ //#region src/cli/utils/parse-staged-snapshot-divergences.ts
64255
+ const SNAPSHOT_CONFIG_FILENAMES = new Set([...STAGED_FILES_PROJECT_CONFIG_FILENAMES, ...STAGED_SNAPSHOT_ADDITIONAL_CONFIG_FILENAMES]);
64256
+ const parseStagedSnapshotDivergences = (statusOutput) => {
64257
+ const divergentConfigFiles = /* @__PURE__ */ new Set();
64258
+ const statusEntries = statusOutput.split("\0").filter((entry) => entry.length > 0);
64259
+ for (let entryIndex = 0; entryIndex < statusEntries.length; entryIndex += 1) {
64260
+ const entry = statusEntries[entryIndex];
64261
+ const [indexStatus, worktreeStatus] = entry;
64262
+ const filePath = entry.slice(3);
64263
+ const hasRenameOrCopySource = indexStatus === "R" || indexStatus === "C" || worktreeStatus === "R" || worktreeStatus === "C";
64264
+ const sourcePath = hasRenameOrCopySource ? statusEntries[entryIndex + 1] : void 0;
64265
+ const worktreePaths = worktreeStatus === "R" || worktreeStatus === "C" ? [filePath, sourcePath] : [filePath];
64266
+ if (worktreeStatus !== " ") {
64267
+ for (const worktreePath of worktreePaths) if (worktreePath && SNAPSHOT_CONFIG_FILENAMES.has(Path.basename(worktreePath))) divergentConfigFiles.add(worktreePath);
64268
+ }
64269
+ if (hasRenameOrCopySource) entryIndex += 1;
64270
+ }
64271
+ return [...divergentConfigFiles].sort();
64272
+ };
64273
+ //#endregion
64274
+ //#region src/cli/utils/find-staged-snapshot-divergences.ts
64275
+ const findStagedSnapshotDivergences = (directory) => {
64276
+ const statusOutput = runGitRaw(directory, [
64277
+ "status",
64278
+ "--porcelain=v1",
64279
+ "-z",
64280
+ "--untracked-files=all",
64281
+ "--ignored=matching"
64282
+ ]);
64283
+ if (statusOutput === null) return null;
64284
+ return parseStagedSnapshotDivergences(statusOutput);
64285
+ };
64286
+ //#endregion
63767
64287
  //#region src/cli/commands/inspect.ts
63768
64288
  const filterCompletedScansByCategories = (completedScans, categoryFilters) => {
63769
64289
  if (categoryFilters.size === 0) return [...completedScans];
@@ -63778,9 +64298,14 @@ const filterCompletedScansByCategories = (completedScans, categoryFilters) => {
63778
64298
  /**
63779
64299
  * Post-scan finalization shared by the staged-arm and project-loop
63780
64300
  * paths of `inspectAction`: emit the JSON report (when in JSON mode)
63781
- * and set `process.exitCode = 1` when a diagnostic at or above the
63782
- * `--blocking` threshold (default `"error"`) reaches the `ciFailure`
63783
- * surface. `--blocking none` keeps the scan advisory (always exits 0).
64301
+ * and set `process.exitCode = 1` when any scan's lint pass hard-failed
64302
+ * (an engine/plugin/binding failure destroys the findings, so success
64303
+ * would be a false clean) or a diagnostic at or above the `--blocking`
64304
+ * threshold (default `"error"`) reaches the `ciFailure` surface.
64305
+ * `--blocking none` keeps the scan advisory (always exits 0), and
64306
+ * fail-open degradations — `--no-lint`, `--max-duration` truncation,
64307
+ * supply-chain/security skips — stay advisory too, surfaced through
64308
+ * `complete: false` in the JSON report.
63784
64309
  */
63785
64310
  const finalizeScans = (input) => {
63786
64311
  const baselineDeltas = input.completedScans.flatMap((scan) => scan.result.baselineDelta ? [scan.result.baselineDelta] : []);
@@ -63810,8 +64335,13 @@ const finalizeScans = (input) => {
63810
64335
  baselineDegraded
63811
64336
  }));
63812
64337
  }
64338
+ const blockingLevel = resolveBlockingLevel(input.flags, input.userConfig);
64339
+ if (input.completedScans.some(({ result }) => hasLintHardFailure(result)) && blockingLevel !== "none") {
64340
+ process.exitCode = 1;
64341
+ return;
64342
+ }
63813
64343
  if (input.isScoreOnly || baselineDegraded) return;
63814
- if (shouldBlockCi(filterScansForSurface(input.completedScans, "ciFailure"), resolveBlockingLevel(input.flags, input.userConfig))) process.exitCode = 1;
64344
+ if (shouldBlockCi(filterScansForSurface(input.completedScans, "ciFailure"), blockingLevel)) process.exitCode = 1;
63815
64345
  };
63816
64346
  const buildChangedFilesDiffInfo = (changedFiles) => ({
63817
64347
  currentBranch: process.env.GITHUB_HEAD_REF?.trim() || null,
@@ -63849,6 +64379,7 @@ const inspectAction = async (directory, flags) => {
63849
64379
  recordCount(METRIC.cliInvoked, 1, { command: "inspect" });
63850
64380
  try {
63851
64381
  validateModeFlags(flags);
64382
+ if (flags.staged) setJsonReportMode("staged");
63852
64383
  await maybeMigrateLegacyConfig(requestedDirectory, {
63853
64384
  isQuiet,
63854
64385
  isStaged: Boolean(flags.staged)
@@ -63864,6 +64395,14 @@ const inspectAction = async (directory, flags) => {
63864
64395
  cliLogger.dim(`Redirected to ${highlighter.info(toRelativePath(resolvedDirectory, requestedDirectory))} via react-doctor config "rootDir".`);
63865
64396
  cliLogger.break();
63866
64397
  }
64398
+ if (flags.staged) {
64399
+ const divergentConfigFiles = findStagedSnapshotDivergences(resolvedDirectory);
64400
+ if (divergentConfigFiles === null) throw new CliInputError("Could not verify that staged configuration matches the worktree. Run the command from a Git worktree with Git available.");
64401
+ if (divergentConfigFiles.length > 0) {
64402
+ recordCount(METRIC.stagedSnapshotDivergence, 1, { divergentInputCount: divergentConfigFiles.length });
64403
+ throw new CliInputError(`Cannot scan staged files while configuration differs between the index and worktree: ${divergentConfigFiles.join(", ")}. Stage or restore those files, then rerun react-doctor --staged.`);
64404
+ }
64405
+ }
63867
64406
  const explainArgument = flags.explain;
63868
64407
  if (explainArgument !== void 0) {
63869
64408
  await runExplain(explainArgument, {
@@ -63884,7 +64423,6 @@ const inspectAction = async (directory, flags) => {
63884
64423
  json: flags.json
63885
64424
  });
63886
64425
  if (flags.staged) {
63887
- setJsonReportMode("staged");
63888
64426
  const stagedFiles = await getStagedSourceFiles(resolvedDirectory);
63889
64427
  if (stagedFiles.length === 0) {
63890
64428
  if (isJsonMode) writeJsonReport(buildJsonReport({
@@ -63965,13 +64503,15 @@ const inspectAction = async (directory, flags) => {
63965
64503
  const projectDirectories = await selectProjects(resolvedDirectory, flags.project, skipPrompts, userConfig?.projects);
63966
64504
  const changedFilesDiffInfo = flags.changedFilesFrom ? buildChangedFilesDiffInfo(readChangedFilesFrom(Path.resolve(flags.changedFilesFrom))) : null;
63967
64505
  const requestedScope = resolveScope(flags, userConfig);
64506
+ const includeUntracked = flags.includeUntracked ?? false;
63968
64507
  const scopeRequest = requestedScope.scope === void 0 && changedFilesDiffInfo !== null ? {
63969
64508
  ...requestedScope,
63970
64509
  scope: "changed"
63971
64510
  } : requestedScope;
64511
+ validateIncludeUntrackedScope(includeUntracked, scopeRequest.scope);
63972
64512
  const wantsDiffMode = scopeRequest.scope !== void 0 && scopeRequest.scope !== "full";
63973
64513
  const shouldDetectDiff = changedFilesDiffInfo === null && (wantsDiffMode || scopeRequest.scope === void 0 && !skipPrompts && !isQuiet);
63974
- const diffInfo = changedFilesDiffInfo ?? (shouldDetectDiff ? await getDiffInfo(resolvedDirectory, scopeRequest.base) : null);
64514
+ const diffInfo = changedFilesDiffInfo ?? (shouldDetectDiff ? await getDiffInfo(resolvedDirectory, scopeRequest.base, includeUntracked) : null);
63975
64515
  const scope = await finalizeScope({
63976
64516
  requested: scopeRequest,
63977
64517
  diffInfo,
@@ -63985,7 +64525,8 @@ const inspectAction = async (directory, flags) => {
63985
64525
  const changedLineRanges = scope === "lines" && diffInfo !== null && (diffInfo.isCurrentChanges || linesBaseRef !== null) && diffInfo !== null ? await getChangedLineRanges({
63986
64526
  directory: resolvedDirectory,
63987
64527
  baseRef: linesBaseRef ?? void 0,
63988
- files: [...diffInfo.changedFiles]
64528
+ files: [...diffInfo.changedFiles],
64529
+ includeUntracked
63989
64530
  }) : null;
63990
64531
  if (scope === "lines" && changedLineRanges === null && !isQuiet) {
63991
64532
  cliLogger.warn("Could not determine changed lines (no base ref or git diff failed); reporting all issues in changed files.");
@@ -64918,6 +65459,7 @@ const ROOT_FLAG_SPEC = {
64918
65459
  "--dead-code",
64919
65460
  "--debug",
64920
65461
  "--help",
65462
+ "--include-untracked",
64921
65463
  "--json",
64922
65464
  "--json-compact",
64923
65465
  "--lint",
@@ -65181,7 +65723,7 @@ ${highlighter.dim("Learn more:")}
65181
65723
  ${highlighter.info(CI_URL)}
65182
65724
  `;
65183
65725
  const collectCategoryOption = (value, previousValues) => [...previousValues ?? [], value];
65184
- 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("--supply-chain", "enable the dependency supply-chain scan (default)").option("--no-supply-chain", "skip the dependency supply-chain scan (Socket.dev dependency health checks)").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("--json-out <path>", "with --json, write the report to a file instead of stdout").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("--max-duration <seconds>", "scan time budget for the whole run, shared across workspace projects: past it, remaining lint batches and dead-code are skipped and partial results are reported (skipped files are listed in the JSON report)").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);
65726
+ 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("--supply-chain", "enable the dependency supply-chain scan (default)").option("--no-supply-chain", "skip the dependency supply-chain scan (Socket.dev dependency health checks)").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("--json-out <path>", "with --json, write the report to a file instead of stdout").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 supported JS/TS source to scan/report: full (default), files, changed (only new issues vs base), or lines (issues whose source spans touch changed lines)").option("--base <ref>", "base git ref for files/changed/lines scope (auto-detected when omitted)").option("--include-untracked", "with --scope files/changed/lines, also scan ordinary untracked files (respects .gitignore)").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("--max-duration <seconds>", "scan time budget for the whole run, shared across workspace projects: past it, remaining lint batches and dead-code are skipped and partial results are reported (skipped files are listed in the JSON report)").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);
65185
65727
  program.action(inspectAction);
65186
65728
  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));
65187
65729
  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);
@@ -65231,4 +65773,4 @@ Promise.resolve().then(() => assertNoRemovedFlags(process.argv)).then(() => prog
65231
65773
  export {};
65232
65774
 
65233
65775
  //# sourceMappingURL=cli.js.map
65234
- //# debugId=63b8a227-543e-5501-a998-58c03a1fb10e
65776
+ //# debugId=3de13f8e-2b7e-5141-a62e-0cce9208b352