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/lsp.js CHANGED
@@ -32959,7 +32959,6 @@ const IGNORED_DIRECTORIES = new Set([
32959
32959
  "out",
32960
32960
  "storybook-static"
32961
32961
  ]);
32962
- const JSX_FILE_PATTERN = /\.(tsx|jsx)$/;
32963
32962
  const MILLISECONDS_PER_SECOND = 1e3;
32964
32963
  const SCORE_API_URL = "https://www.react.doctor/api/score";
32965
32964
  const FETCH_TIMEOUT_MS = 1e4;
@@ -33092,7 +33091,7 @@ const SOCKET_FREE_USER_AGENT = "react-doctor-supply-chain";
33092
33091
  const FILE_LINT_CACHE_FILENAME = "file-lint-cache.json";
33093
33092
  const FILE_LINT_CACHE_MAX_FILE_COUNT = 5e4;
33094
33093
  const SIDECAR_LINT_CACHE_FILENAME = "sidecar-lint-cache.json";
33095
- const CORE_PACKAGE_VERSION = "0.7.6";
33094
+ const CORE_PACKAGE_VERSION = "0.7.7";
33096
33095
  const DEAD_CODE_CACHE_FILENAME = "dead-code-cache.json";
33097
33096
  const DEAD_CODE_SUMMARY_CACHE_FILENAME = "dead-code-summaries.json";
33098
33097
  const SUPPLY_CHAIN_PLUGIN = "socket";
@@ -33195,13 +33194,15 @@ const listSourceFilesViaFilesystem = (rootDirectory) => {
33195
33194
  const listSourceFilesWithSize = (rootDirectory) => collectSizedSourceFiles(rootDirectory, (listSourceFilesViaGit(rootDirectory) ?? listSourceFilesViaFilesystem(rootDirectory)).sort());
33196
33195
  const listSourceFiles = (rootDirectory) => listSourceFilesWithSize(rootDirectory).map((entry) => entry.path);
33197
33196
  const countSourceFiles = (rootDirectory) => listSourceFilesWithSize(rootDirectory).length;
33197
+ const UTF8_BOM_CHARACTER = "";
33198
+ const stripUtf8Bom = (content) => content.startsWith(UTF8_BOM_CHARACTER) ? content.slice(1) : content;
33198
33199
  const cachedPackageJsons = /* @__PURE__ */ new Map();
33199
33200
  const clearPackageJsonCache = () => {
33200
33201
  cachedPackageJsons.clear();
33201
33202
  };
33202
33203
  const readPackageJsonUncached = (packageJsonPath) => {
33203
33204
  try {
33204
- return JSON.parse(NFS.readFileSync(packageJsonPath, "utf-8"));
33205
+ return JSON.parse(stripUtf8Bom(NFS.readFileSync(packageJsonPath, "utf-8")));
33205
33206
  } catch (error) {
33206
33207
  if (error instanceof SyntaxError) return {};
33207
33208
  if (isErrnoException(error)) {
@@ -36343,6 +36344,49 @@ const assignFixGroups = (diagnostics) => {
36343
36344
  };
36344
36345
  });
36345
36346
  };
36347
+ const DERIVED_STATE_RULE_PRIORITY = new Map([
36348
+ ["no-initialize-state", 0],
36349
+ ["no-adjust-state-on-prop-change", 1],
36350
+ ["no-derived-state", 2],
36351
+ ["no-derived-state-effect", 3]
36352
+ ]);
36353
+ const buildDiagnosticSiteKey = (diagnostic) => `${diagnostic.filePath}\u0000${diagnostic.line}\u0000${diagnostic.column}`;
36354
+ const isReactDoctorRulesOfHooksDiagnostic = (diagnostic) => diagnostic.plugin === "react-doctor" && diagnostic.rule === "rules-of-hooks";
36355
+ const isReactCompilerHooksDiagnostic = (diagnostic) => diagnostic.plugin === "react-hooks-js" && diagnostic.rule === "hooks";
36356
+ const doDiagnosticSpansOverlap = (first, second) => {
36357
+ if (first.filePath !== second.filePath || first.plugin !== second.plugin) return false;
36358
+ if (first.offset !== void 0 && second.offset !== void 0) {
36359
+ const firstEnd = first.offset + Math.max(first.length ?? 1, 1);
36360
+ const secondEnd = second.offset + Math.max(second.length ?? 1, 1);
36361
+ return first.offset < secondEnd && second.offset < firstEnd;
36362
+ }
36363
+ const firstEndLine = first.endLine ?? first.line;
36364
+ const secondEndLine = second.endLine ?? second.line;
36365
+ return first.line <= secondEndLine && second.line <= firstEndLine;
36366
+ };
36367
+ const keepHighestSeverity = (survivingDiagnostic, collapsedDiagnostic) => collapsedDiagnostic.severity === "error" && survivingDiagnostic.severity !== "error" ? {
36368
+ ...survivingDiagnostic,
36369
+ severity: "error"
36370
+ } : survivingDiagnostic;
36371
+ const dedupeRelatedDiagnostics = (diagnostics) => {
36372
+ const reactDoctorHookSites = new Set(diagnostics.filter(isReactDoctorRulesOfHooksDiagnostic).map(buildDiagnosticSiteKey));
36373
+ const uniqueDiagnostics = [];
36374
+ for (const diagnostic of diagnostics) {
36375
+ if (isReactCompilerHooksDiagnostic(diagnostic) && reactDoctorHookSites.has(buildDiagnosticSiteKey(diagnostic))) continue;
36376
+ const priority = DERIVED_STATE_RULE_PRIORITY.get(diagnostic.rule);
36377
+ if (priority !== void 0) {
36378
+ const overlappingDiagnosticIndex = uniqueDiagnostics.findIndex((candidate) => candidate.rule !== diagnostic.rule && DERIVED_STATE_RULE_PRIORITY.has(candidate.rule) && doDiagnosticSpansOverlap(candidate, diagnostic));
36379
+ if (overlappingDiagnosticIndex >= 0) {
36380
+ const existingDiagnostic = uniqueDiagnostics[overlappingDiagnosticIndex];
36381
+ const existingPriority = existingDiagnostic ? DERIVED_STATE_RULE_PRIORITY.get(existingDiagnostic.rule) : void 0;
36382
+ if (existingDiagnostic && existingPriority !== void 0) uniqueDiagnostics[overlappingDiagnosticIndex] = priority < existingPriority ? keepHighestSeverity(diagnostic, existingDiagnostic) : keepHighestSeverity(existingDiagnostic, diagnostic);
36383
+ continue;
36384
+ }
36385
+ }
36386
+ uniqueDiagnostics.push(diagnostic);
36387
+ }
36388
+ return uniqueDiagnostics;
36389
+ };
36346
36390
  const compareStrings = (left, right) => left < right ? -1 : left > right ? 1 : 0;
36347
36391
  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));
36348
36392
  const getDirectDependencyNames = (packageJson) => new Set([...Object.keys(packageJson.dependencies ?? {}), ...Object.keys(packageJson.devDependencies ?? {})]);
@@ -36768,7 +36812,6 @@ const PNPM_WORKSPACE_FILE = "pnpm-workspace.yaml";
36768
36812
  const PNPM_LOCKFILE = "pnpm-lock.yaml";
36769
36813
  const PACKAGE_JSON_FILE = "package.json";
36770
36814
  const PNPM_HARDENING_RULE_KEY = "require-pnpm-hardening";
36771
- const UTF8_BOM_CHAR = "";
36772
36815
  const HARDENING_SETTING_KEYS = new Set([
36773
36816
  "minimumReleaseAge",
36774
36817
  "blockExoticSubdeps",
@@ -36793,12 +36836,11 @@ const stripInlineComment = (rawValue) => {
36793
36836
  return rawValue;
36794
36837
  };
36795
36838
  const unquote = (rawValue) => rawValue.replace(/^["']|["']$/g, "");
36796
- const stripBom = (rawContent) => rawContent.startsWith(UTF8_BOM_CHAR) ? rawContent.slice(1) : rawContent;
36797
36839
  const parseHardeningSettings = (content) => {
36798
36840
  let minimumReleaseAge = null;
36799
36841
  let blockExoticSubdeps = null;
36800
36842
  let trustPolicy = null;
36801
- const lines = stripBom(content).split(/\r?\n/);
36843
+ const lines = stripUtf8Bom(content).split(/\r?\n/);
36802
36844
  for (let lineIndex = 0; lineIndex < lines.length; lineIndex += 1) {
36803
36845
  const lineText = lines[lineIndex];
36804
36846
  if (lineText === void 0) continue;
@@ -36832,7 +36874,7 @@ const isPnpmManagedProject = (rootDirectory) => {
36832
36874
  const packageJsonPath = Path.join(rootDirectory, PACKAGE_JSON_FILE);
36833
36875
  if (!isFile(packageJsonPath)) return false;
36834
36876
  try {
36835
- const packageJsonRaw = NFS.readFileSync(packageJsonPath, "utf-8");
36877
+ const packageJsonRaw = stripUtf8Bom(NFS.readFileSync(packageJsonPath, "utf-8"));
36836
36878
  const packageJson = JSON.parse(packageJsonRaw);
36837
36879
  if (packageJson !== null && typeof packageJson === "object" && "packageManager" in packageJson && typeof packageJson.packageManager === "string" && packageJson.packageManager.startsWith("pnpm@")) return true;
36838
36880
  } catch {
@@ -37118,18 +37160,45 @@ const checkReactServerComponentsAdvisory = (scanDirectory, project) => {
37118
37160
  }
37119
37161
  return diagnostics;
37120
37162
  };
37121
- const REDUCED_MOTION_GREP_PATTERN = "prefers-reduced-motion|useReducedMotion|MotionConfig|reducedMotion";
37122
- const REDUCED_MOTION_PATTERN = new RegExp(REDUCED_MOTION_GREP_PATTERN);
37123
- const REDUCED_MOTION_FILE_EXTENSIONS = new Set([
37163
+ const getTypescriptScriptKind = (fileName) => {
37164
+ if (fileName.endsWith(".tsx")) return ts.ScriptKind.TSX;
37165
+ if (fileName.endsWith(".jsx")) return ts.ScriptKind.JSX;
37166
+ if (fileName.endsWith(".ts")) return ts.ScriptKind.TS;
37167
+ return ts.ScriptKind.JS;
37168
+ };
37169
+ const unwrapTypescriptExpression = (expression) => {
37170
+ let currentExpression = expression;
37171
+ while (ts.isParenthesizedExpression(currentExpression) || ts.isAsExpression(currentExpression) || ts.isSatisfiesExpression(currentExpression) || ts.isNonNullExpression(currentExpression) || ts.isTypeAssertionExpression(currentExpression)) currentExpression = currentExpression.expression;
37172
+ return currentExpression;
37173
+ };
37174
+ const EMPTY_MOTION_EXPRESSION_EVIDENCE = {
37175
+ isAnimationFunction: false,
37176
+ isMotionComponent: false,
37177
+ isMotionComponentFactory: false,
37178
+ isMotionComponentNamespace: false,
37179
+ isMotionConfig: false,
37180
+ isMotionNamespace: false,
37181
+ isReducedMotionHook: false
37182
+ };
37183
+ const MOTION_COMPONENT_FACTORY_EXPORT_NAMES = new Set(["m", "motion"]);
37184
+ const MOTION_COMPONENT_NAMESPACE_EXPORT_NAMES = new Set(["Reorder"]);
37185
+ const MOTION_COMPONENT_NAMESPACE_MEMBER_NAMES = new Set(["Item"]);
37186
+ const MOTION_ANIMATION_FUNCTION_EXPORT_NAMES = new Set(["animate"]);
37187
+ const REDUCED_MOTION_HOOK_EXPORT_NAME = "useReducedMotion";
37188
+ const MOTION_CONFIG_EXPORT_NAME = "MotionConfig";
37189
+ const REDUCED_MOTION_PROP_NAME = "reducedMotion";
37190
+ const REDUCED_MOTION_CONFIG_VALUES = new Set(["always", "user"]);
37191
+ const SCRIPT_MODULE_EXTENSIONS = [
37124
37192
  ".ts",
37125
37193
  ".tsx",
37126
37194
  ".js",
37127
- ".jsx",
37128
- ".css",
37129
- ".scss"
37130
- ]);
37131
- const REDUCED_MOTION_FILE_GLOBS = [...REDUCED_MOTION_FILE_EXTENSIONS].map((extension) => `*${extension}`);
37132
- const GIT_GREP_NO_MATCH_STATUS = 1;
37195
+ ".jsx"
37196
+ ];
37197
+ const SCRIPT_FILE_EXTENSIONS = new Set(SCRIPT_MODULE_EXTENSIONS);
37198
+ const STYLE_FILE_EXTENSIONS = new Set([".css", ".scss"]);
37199
+ const MOTION_SOURCE_PREFILTER = /framer-motion|["']motion(?:\/[A-Za-z0-9_./-]+)?["']|MotionConfig|useReducedMotion/;
37200
+ const REDUCED_MOTION_MEDIA_QUERY_PATTERN = /@media\s+([^{}]*\(\s*prefers-reduced-motion\s*:\s*reduce\s*\)[^{}]*)\{/gi;
37201
+ const CSS_DECLARATION_PATTERN = /(?:^|[;{])\s*[-_A-Za-z][-_A-Za-z0-9]*\s*:/;
37133
37202
  const MISSING_REDUCED_MOTION_DIAGNOSTIC = {
37134
37203
  filePath: "package.json",
37135
37204
  plugin: "react-doctor",
@@ -37141,52 +37210,406 @@ const MISSING_REDUCED_MOTION_DIAGNOSTIC = {
37141
37210
  column: 0,
37142
37211
  category: "Accessibility"
37143
37212
  };
37144
- const hasReducedMotionHandlingViaFilesystem = (rootDirectory) => {
37213
+ const isMotionModuleSource = (moduleSource) => {
37214
+ for (const packageName of MOTION_LIBRARY_PACKAGES) if (moduleSource === packageName || moduleSource.startsWith(`${packageName}/`)) return true;
37215
+ return false;
37216
+ };
37217
+ const classifyMotionExport = (exportName) => ({
37218
+ isAnimationFunction: MOTION_ANIMATION_FUNCTION_EXPORT_NAMES.has(exportName),
37219
+ isMotionComponent: false,
37220
+ isMotionComponentFactory: MOTION_COMPONENT_FACTORY_EXPORT_NAMES.has(exportName),
37221
+ isMotionComponentNamespace: MOTION_COMPONENT_NAMESPACE_EXPORT_NAMES.has(exportName),
37222
+ isMotionConfig: exportName === MOTION_CONFIG_EXPORT_NAME,
37223
+ isMotionNamespace: false,
37224
+ isReducedMotionHook: exportName === REDUCED_MOTION_HOOK_EXPORT_NAME
37225
+ });
37226
+ const getImportModuleSource = (node) => {
37227
+ let currentNode = node;
37228
+ while (currentNode) {
37229
+ if (ts.isImportDeclaration(currentNode) && ts.isStringLiteral(currentNode.moduleSpecifier)) return currentNode.moduleSpecifier.text;
37230
+ currentNode = currentNode.parent;
37231
+ }
37232
+ return null;
37233
+ };
37234
+ const getImportedBindingEvidence = (declaration, typeChecker, program, visitedSymbols) => {
37235
+ const moduleSource = getImportModuleSource(declaration);
37236
+ if (!moduleSource) return null;
37237
+ if (isMotionModuleSource(moduleSource) && ts.isNamespaceImport(declaration)) return {
37238
+ ...EMPTY_MOTION_EXPRESSION_EVIDENCE,
37239
+ isMotionNamespace: true
37240
+ };
37241
+ if (isMotionModuleSource(moduleSource) && ts.isImportSpecifier(declaration)) {
37242
+ if (declaration.isTypeOnly) return null;
37243
+ return classifyMotionExport(declaration.propertyName?.text ?? declaration.name.text);
37244
+ }
37245
+ if (isMotionModuleSource(moduleSource) && ts.isImportClause(declaration)) {
37246
+ if (declaration.isTypeOnly) return null;
37247
+ return EMPTY_MOTION_EXPRESSION_EVIDENCE;
37248
+ }
37249
+ if (ts.isImportSpecifier(declaration) && !declaration.isTypeOnly || ts.isImportClause(declaration) && !declaration.isTypeOnly && declaration.name) {
37250
+ const importedName = ts.isImportSpecifier(declaration) ? declaration.propertyName?.text ?? declaration.name.text : "default";
37251
+ const moduleSourceFile = getLocalModuleSourceFile(moduleSource, declaration.getSourceFile().fileName, program);
37252
+ if (!moduleSourceFile) return null;
37253
+ return resolveModuleExportEvidence(moduleSourceFile, importedName, typeChecker, program, visitedSymbols);
37254
+ }
37255
+ return null;
37256
+ };
37257
+ const getLocalModuleSourceFile = (moduleSource, importingFileName, program) => {
37258
+ if (!moduleSource.startsWith(".")) return null;
37259
+ const moduleBasePath = Path.resolve(Path.dirname(importingFileName), moduleSource);
37260
+ const moduleBaseExtension = Path.extname(moduleBasePath);
37261
+ const typescriptModuleBasePath = moduleBaseExtension === ".js" || moduleBaseExtension === ".jsx" ? moduleBasePath.slice(0, -moduleBaseExtension.length) : null;
37262
+ const candidates = [
37263
+ moduleBasePath,
37264
+ ...typescriptModuleBasePath ? [
37265
+ `${typescriptModuleBasePath}.ts`,
37266
+ `${typescriptModuleBasePath}.tsx`,
37267
+ Path.join(typescriptModuleBasePath, "index.ts"),
37268
+ Path.join(typescriptModuleBasePath, "index.tsx")
37269
+ ] : [],
37270
+ ...SCRIPT_MODULE_EXTENSIONS.map((extension) => `${moduleBasePath}${extension}`),
37271
+ ...SCRIPT_MODULE_EXTENSIONS.map((extension) => Path.join(moduleBasePath, `index${extension}`))
37272
+ ];
37273
+ for (const candidate of candidates) {
37274
+ const sourceFile = program.getSourceFile(candidate);
37275
+ if (sourceFile) return sourceFile;
37276
+ }
37277
+ return null;
37278
+ };
37279
+ const resolveModuleExportEvidence = (sourceFile, exportName, typeChecker, program, visitedSymbols) => {
37280
+ const moduleSymbol = typeChecker.getSymbolAtLocation(sourceFile);
37281
+ if (moduleSymbol && visitedSymbols.has(moduleSymbol)) return EMPTY_MOTION_EXPRESSION_EVIDENCE;
37282
+ const nextVisitedSymbols = new Set(visitedSymbols);
37283
+ if (moduleSymbol) nextVisitedSymbols.add(moduleSymbol);
37284
+ for (const statement of sourceFile.statements) {
37285
+ if (exportName === "default" && ts.isExportAssignment(statement) && !statement.isExportEquals) {
37286
+ const evidence = resolveMotionExpressionEvidence(statement.expression, typeChecker, program, nextVisitedSymbols);
37287
+ if (hasMotionExpressionEvidence(evidence)) return evidence;
37288
+ continue;
37289
+ }
37290
+ if (!ts.isExportDeclaration(statement) || !statement.moduleSpecifier) continue;
37291
+ if (statement.isTypeOnly) continue;
37292
+ if (!ts.isStringLiteral(statement.moduleSpecifier)) continue;
37293
+ const moduleSource = statement.moduleSpecifier.text;
37294
+ if (!statement.exportClause) {
37295
+ if (isMotionModuleSource(moduleSource)) {
37296
+ const evidence = classifyMotionExport(exportName);
37297
+ if (hasMotionExpressionEvidence(evidence)) return evidence;
37298
+ continue;
37299
+ }
37300
+ const moduleSourceFile = getLocalModuleSourceFile(moduleSource, sourceFile.fileName, program);
37301
+ if (moduleSourceFile) {
37302
+ const evidence = resolveModuleExportEvidence(moduleSourceFile, exportName, typeChecker, program, nextVisitedSymbols);
37303
+ if (hasMotionExpressionEvidence(evidence)) return evidence;
37304
+ }
37305
+ continue;
37306
+ }
37307
+ if (!ts.isNamedExports(statement.exportClause)) continue;
37308
+ for (const exportSpecifier of statement.exportClause.elements) {
37309
+ if (exportSpecifier.name.text !== exportName || exportSpecifier.isTypeOnly) continue;
37310
+ const sourceExportName = exportSpecifier.propertyName?.text ?? exportSpecifier.name.text;
37311
+ if (isMotionModuleSource(moduleSource)) return classifyMotionExport(sourceExportName);
37312
+ const moduleSourceFile = getLocalModuleSourceFile(moduleSource, sourceFile.fileName, program);
37313
+ if (!moduleSourceFile) continue;
37314
+ return resolveModuleExportEvidence(moduleSourceFile, sourceExportName, typeChecker, program, nextVisitedSymbols);
37315
+ }
37316
+ }
37317
+ if (!moduleSymbol) return EMPTY_MOTION_EXPRESSION_EVIDENCE;
37318
+ const exportedSymbol = typeChecker.getExportsOfModule(moduleSymbol).find((candidateSymbol) => candidateSymbol.name === exportName);
37319
+ if (!exportedSymbol || nextVisitedSymbols.has(exportedSymbol)) return EMPTY_MOTION_EXPRESSION_EVIDENCE;
37320
+ nextVisitedSymbols.add(exportedSymbol);
37321
+ let resolvedEvidence = EMPTY_MOTION_EXPRESSION_EVIDENCE;
37322
+ for (const declaration of exportedSymbol.declarations ?? []) {
37323
+ if (ts.isExportSpecifier(declaration)) {
37324
+ const targetSymbol = typeChecker.getExportSpecifierLocalTargetSymbol(declaration);
37325
+ for (const targetDeclaration of targetSymbol?.declarations ?? []) {
37326
+ const importedEvidence = getImportedBindingEvidence(targetDeclaration, typeChecker, program, nextVisitedSymbols);
37327
+ if (importedEvidence) resolvedEvidence = mergeMotionExpressionEvidence(resolvedEvidence, importedEvidence);
37328
+ if (ts.isVariableDeclaration(targetDeclaration) && targetDeclaration.initializer) resolvedEvidence = mergeMotionExpressionEvidence(resolvedEvidence, resolveMotionExpressionEvidence(targetDeclaration.initializer, typeChecker, program, nextVisitedSymbols));
37329
+ }
37330
+ }
37331
+ if (ts.isVariableDeclaration(declaration) && declaration.initializer) resolvedEvidence = mergeMotionExpressionEvidence(resolvedEvidence, resolveMotionExpressionEvidence(declaration.initializer, typeChecker, program, nextVisitedSymbols));
37332
+ }
37333
+ return resolvedEvidence;
37334
+ };
37335
+ const mergeMotionExpressionEvidence = (leftEvidence, rightEvidence) => ({
37336
+ isAnimationFunction: leftEvidence.isAnimationFunction || rightEvidence.isAnimationFunction,
37337
+ isMotionComponent: leftEvidence.isMotionComponent || rightEvidence.isMotionComponent,
37338
+ isMotionComponentFactory: leftEvidence.isMotionComponentFactory || rightEvidence.isMotionComponentFactory,
37339
+ isMotionComponentNamespace: leftEvidence.isMotionComponentNamespace || rightEvidence.isMotionComponentNamespace,
37340
+ isMotionConfig: leftEvidence.isMotionConfig || rightEvidence.isMotionConfig,
37341
+ isMotionNamespace: leftEvidence.isMotionNamespace || rightEvidence.isMotionNamespace,
37342
+ isReducedMotionHook: leftEvidence.isReducedMotionHook || rightEvidence.isReducedMotionHook
37343
+ });
37344
+ const hasMotionExpressionEvidence = (evidence) => evidence.isAnimationFunction || evidence.isMotionComponent || evidence.isMotionComponentFactory || evidence.isMotionComponentNamespace || evidence.isMotionConfig || evidence.isMotionNamespace || evidence.isReducedMotionHook;
37345
+ const classifyMotionMember = (receiverEvidence, memberName) => {
37346
+ if (receiverEvidence.isMotionNamespace) return classifyMotionExport(memberName);
37347
+ if (receiverEvidence.isMotionComponentFactory) return {
37348
+ ...EMPTY_MOTION_EXPRESSION_EVIDENCE,
37349
+ isMotionComponent: true
37350
+ };
37351
+ if (receiverEvidence.isMotionComponentNamespace && MOTION_COMPONENT_NAMESPACE_MEMBER_NAMES.has(memberName)) return {
37352
+ ...EMPTY_MOTION_EXPRESSION_EVIDENCE,
37353
+ isMotionComponent: true
37354
+ };
37355
+ return EMPTY_MOTION_EXPRESSION_EVIDENCE;
37356
+ };
37357
+ const resolveMotionExpressionEvidence = (expression, typeChecker, program, visitedSymbols = /* @__PURE__ */ new Set()) => {
37358
+ const unwrappedExpression = unwrapTypescriptExpression(expression);
37359
+ if (ts.isIdentifier(unwrappedExpression)) {
37360
+ const symbol = typeChecker.getSymbolAtLocation(unwrappedExpression);
37361
+ if (!symbol || visitedSymbols.has(symbol)) return EMPTY_MOTION_EXPRESSION_EVIDENCE;
37362
+ const nextVisitedSymbols = new Set(visitedSymbols);
37363
+ nextVisitedSymbols.add(symbol);
37364
+ let resolvedEvidence = EMPTY_MOTION_EXPRESSION_EVIDENCE;
37365
+ for (const declaration of symbol.declarations ?? []) {
37366
+ const importedEvidence = getImportedBindingEvidence(declaration, typeChecker, program, nextVisitedSymbols);
37367
+ if (importedEvidence) {
37368
+ resolvedEvidence = mergeMotionExpressionEvidence(resolvedEvidence, importedEvidence);
37369
+ continue;
37370
+ }
37371
+ if (ts.isVariableDeclaration(declaration) && declaration.initializer) resolvedEvidence = mergeMotionExpressionEvidence(resolvedEvidence, resolveMotionExpressionEvidence(declaration.initializer, typeChecker, program, nextVisitedSymbols));
37372
+ if (ts.isBindingElement(declaration) && ts.isObjectBindingPattern(declaration.parent) && ts.isVariableDeclaration(declaration.parent.parent) && declaration.parent.parent.initializer) {
37373
+ const propertyName = declaration.propertyName ?? declaration.name;
37374
+ if (ts.isIdentifier(propertyName) || ts.isStringLiteral(propertyName)) {
37375
+ const receiverEvidence = resolveMotionExpressionEvidence(declaration.parent.parent.initializer, typeChecker, program, nextVisitedSymbols);
37376
+ resolvedEvidence = mergeMotionExpressionEvidence(resolvedEvidence, classifyMotionMember(receiverEvidence, propertyName.text));
37377
+ }
37378
+ }
37379
+ }
37380
+ return resolvedEvidence;
37381
+ }
37382
+ if (ts.isPropertyAccessExpression(unwrappedExpression)) return classifyMotionMember(resolveMotionExpressionEvidence(unwrappedExpression.expression, typeChecker, program, visitedSymbols), unwrappedExpression.name.text);
37383
+ 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);
37384
+ if (ts.isCallExpression(unwrappedExpression)) {
37385
+ 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 {
37386
+ ...EMPTY_MOTION_EXPRESSION_EVIDENCE,
37387
+ isMotionNamespace: true
37388
+ };
37389
+ if (ts.isPropertyAccessExpression(unwrappedExpression.expression) && unwrappedExpression.expression.name.text === "bind") {
37390
+ const boundReceiverEvidence = resolveMotionExpressionEvidence(unwrappedExpression.expression.expression, typeChecker, program, visitedSymbols);
37391
+ if (boundReceiverEvidence.isAnimationFunction) return boundReceiverEvidence;
37392
+ }
37393
+ const calleeEvidence = resolveMotionExpressionEvidence(unwrappedExpression.expression, typeChecker, program, visitedSymbols);
37394
+ if (calleeEvidence.isMotionComponentFactory) return {
37395
+ ...EMPTY_MOTION_EXPRESSION_EVIDENCE,
37396
+ isMotionComponent: true
37397
+ };
37398
+ return calleeEvidence;
37399
+ }
37400
+ return EMPTY_MOTION_EXPRESSION_EVIDENCE;
37401
+ };
37402
+ const getStaticStringExpressionValue = (expression, typeChecker, visitedSymbols = /* @__PURE__ */ new Set()) => {
37403
+ const unwrappedExpression = unwrapTypescriptExpression(expression);
37404
+ if (ts.isStringLiteral(unwrappedExpression) || ts.isNoSubstitutionTemplateLiteral(unwrappedExpression)) return unwrappedExpression.text;
37405
+ if (!ts.isIdentifier(unwrappedExpression)) return null;
37406
+ const symbol = typeChecker.getSymbolAtLocation(unwrappedExpression);
37407
+ if (!symbol || visitedSymbols.has(symbol)) return null;
37408
+ const nextVisitedSymbols = new Set(visitedSymbols);
37409
+ nextVisitedSymbols.add(symbol);
37410
+ for (const declaration of symbol.declarations ?? []) {
37411
+ if (!ts.isVariableDeclaration(declaration) || !declaration.initializer) continue;
37412
+ if (!ts.isVariableDeclarationList(declaration.parent)) continue;
37413
+ if (!(declaration.parent.flags & ts.NodeFlags.Const)) continue;
37414
+ const value = getStaticStringExpressionValue(declaration.initializer, typeChecker, nextVisitedSymbols);
37415
+ if (value !== null) return value;
37416
+ }
37417
+ return null;
37418
+ };
37419
+ const getStaticJsxAttributeValue = (attribute, typeChecker) => {
37420
+ if (!attribute.initializer) return null;
37421
+ if (ts.isStringLiteral(attribute.initializer)) return attribute.initializer.text;
37422
+ if (!ts.isJsxExpression(attribute.initializer) || !attribute.initializer.expression) return null;
37423
+ return getStaticStringExpressionValue(attribute.initializer.expression, typeChecker);
37424
+ };
37425
+ const isExpressionValueConsumed = (expression) => {
37426
+ let currentExpression = expression;
37427
+ while (ts.isParenthesizedExpression(currentExpression.parent) || ts.isAsExpression(currentExpression.parent) || ts.isSatisfiesExpression(currentExpression.parent) || ts.isNonNullExpression(currentExpression.parent) || ts.isTypeAssertionExpression(currentExpression.parent)) currentExpression = currentExpression.parent;
37428
+ return !ts.isExpressionStatement(currentExpression.parent) && !ts.isVoidExpression(currentExpression.parent);
37429
+ };
37430
+ 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) ?? ""));
37431
+ const collectScriptMotionEvidence = (sourceFile, typeChecker, program) => {
37432
+ const evidence = {
37433
+ hasMotionUse: false,
37434
+ hasReducedMotionHandling: false
37435
+ };
37436
+ const visitNode = (node) => {
37437
+ if (ts.isCallExpression(node)) {
37438
+ const calleeEvidence = resolveMotionExpressionEvidence(node.expression, typeChecker, program);
37439
+ 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;
37440
+ if (calleeEvidence.isAnimationFunction || isAnimationFunctionCallViaCallOrApply) evidence.hasMotionUse = true;
37441
+ if (calleeEvidence.isReducedMotionHook && isExpressionValueConsumed(node)) evidence.hasReducedMotionHandling = true;
37442
+ }
37443
+ if (ts.isJsxOpeningElement(node) || ts.isJsxSelfClosingElement(node)) {
37444
+ const tagEvidence = ts.isJsxNamespacedName(node.tagName) ? EMPTY_MOTION_EXPRESSION_EVIDENCE : resolveMotionExpressionEvidence(node.tagName, typeChecker, program);
37445
+ if (tagEvidence.isMotionComponent || tagEvidence.isMotionComponentFactory) evidence.hasMotionUse = true;
37446
+ if (tagEvidence.isMotionConfig && jsxElementHasReducedMotionConfiguration(node.attributes, typeChecker)) evidence.hasReducedMotionHandling = true;
37447
+ }
37448
+ ts.forEachChild(node, visitNode);
37449
+ };
37450
+ visitNode(sourceFile);
37451
+ return evidence;
37452
+ };
37453
+ const analyzeReducedMotionSources = (sources) => {
37454
+ if (!sources.some(({ sourceText }) => MOTION_SOURCE_PREFILTER.test(sourceText))) return {
37455
+ hasMotionUse: false,
37456
+ hasReducedMotionHandling: false
37457
+ };
37458
+ const compilerOptions = {
37459
+ allowJs: true,
37460
+ checkJs: false,
37461
+ jsx: ts.JsxEmit.Preserve,
37462
+ module: ts.ModuleKind.ESNext,
37463
+ moduleResolution: ts.ModuleResolutionKind.Bundler,
37464
+ noEmit: true,
37465
+ noLib: true,
37466
+ noResolve: true,
37467
+ skipLibCheck: true,
37468
+ target: ts.ScriptTarget.Latest
37469
+ };
37470
+ const sourceTextByFileName = new Map(sources.map(({ fileName: sourceFileName, sourceText: content }) => [Path.resolve(sourceFileName), content]));
37471
+ const compilerHost = ts.createCompilerHost(compilerOptions);
37472
+ const getDefaultSourceFile = compilerHost.getSourceFile.bind(compilerHost);
37473
+ 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);
37474
+ const program = ts.createProgram([...sourceTextByFileName.keys()], compilerOptions, compilerHost);
37475
+ const typeChecker = program.getTypeChecker();
37476
+ const evidence = {
37477
+ hasMotionUse: false,
37478
+ hasReducedMotionHandling: false
37479
+ };
37480
+ for (const sourceFileName of sourceTextByFileName.keys()) {
37481
+ const sourceFile = program.getSourceFile(sourceFileName);
37482
+ if (!sourceFile) continue;
37483
+ const fileEvidence = collectScriptMotionEvidence(sourceFile, typeChecker, program);
37484
+ evidence.hasMotionUse ||= fileEvidence.hasMotionUse;
37485
+ evidence.hasReducedMotionHandling ||= fileEvidence.hasReducedMotionHandling;
37486
+ }
37487
+ return evidence;
37488
+ };
37489
+ const removeCssCommentsAndStrings = (content) => {
37490
+ let sanitizedContent = "";
37491
+ let quote = null;
37492
+ let isInsideComment = false;
37493
+ let isInsideLineComment = false;
37494
+ for (let characterIndex = 0; characterIndex < content.length; characterIndex += 1) {
37495
+ const character = content[characterIndex];
37496
+ const nextCharacter = content[characterIndex + 1];
37497
+ if (isInsideLineComment) {
37498
+ sanitizedContent += character === "\n" ? "\n" : " ";
37499
+ if (character === "\n") isInsideLineComment = false;
37500
+ continue;
37501
+ }
37502
+ if (isInsideComment) {
37503
+ if (character === "*" && nextCharacter === "/") {
37504
+ sanitizedContent += " ";
37505
+ characterIndex += 1;
37506
+ isInsideComment = false;
37507
+ } else sanitizedContent += character === "\n" ? "\n" : " ";
37508
+ continue;
37509
+ }
37510
+ if (quote) {
37511
+ if (character === "\\") {
37512
+ sanitizedContent += " ";
37513
+ characterIndex += 1;
37514
+ continue;
37515
+ }
37516
+ if (character === quote) quote = null;
37517
+ sanitizedContent += character === "\n" ? "\n" : " ";
37518
+ continue;
37519
+ }
37520
+ if (character === "/" && nextCharacter === "*") {
37521
+ sanitizedContent += " ";
37522
+ characterIndex += 1;
37523
+ isInsideComment = true;
37524
+ continue;
37525
+ }
37526
+ if (character === "/" && nextCharacter === "/") {
37527
+ sanitizedContent += " ";
37528
+ characterIndex += 1;
37529
+ isInsideLineComment = true;
37530
+ continue;
37531
+ }
37532
+ if (character === "\"" || character === "'") {
37533
+ sanitizedContent += " ";
37534
+ quote = character;
37535
+ continue;
37536
+ }
37537
+ sanitizedContent += character;
37538
+ }
37539
+ return sanitizedContent;
37540
+ };
37541
+ const findMatchingClosingBrace = (content, openingBraceIndex) => {
37542
+ let depth = 0;
37543
+ for (let characterIndex = openingBraceIndex; characterIndex < content.length; characterIndex += 1) {
37544
+ if (content[characterIndex] === "{") depth += 1;
37545
+ if (content[characterIndex] !== "}") continue;
37546
+ depth -= 1;
37547
+ if (depth === 0) return characterIndex;
37548
+ }
37549
+ return -1;
37550
+ };
37551
+ const hasReducedMotionMediaQuery = (content) => {
37552
+ const sanitizedContent = removeCssCommentsAndStrings(content);
37553
+ REDUCED_MOTION_MEDIA_QUERY_PATTERN.lastIndex = 0;
37554
+ for (const match of sanitizedContent.matchAll(REDUCED_MOTION_MEDIA_QUERY_PATTERN)) {
37555
+ const mediaPrelude = match[1];
37556
+ if (/\bnot\b/i.test(mediaPrelude)) continue;
37557
+ const openingBraceIndex = (match.index ?? 0) + match[0].lastIndexOf("{");
37558
+ const closingBraceIndex = findMatchingClosingBrace(sanitizedContent, openingBraceIndex);
37559
+ if (closingBraceIndex === -1) continue;
37560
+ const body = sanitizedContent.slice(openingBraceIndex + 1, closingBraceIndex);
37561
+ if (CSS_DECLARATION_PATTERN.test(body)) return true;
37562
+ }
37563
+ return false;
37564
+ };
37565
+ const collectProjectMotionEvidence = (rootDirectory) => {
37566
+ const scriptSources = [];
37567
+ let hasReducedMotionHandling = false;
37145
37568
  for (const { absolutePath, name } of walkSourceTreeFiles(rootDirectory)) {
37146
- if (!REDUCED_MOTION_FILE_EXTENSIONS.has(Path.extname(name))) continue;
37569
+ const extension = Path.extname(name);
37570
+ if (!SCRIPT_FILE_EXTENSIONS.has(extension) && !STYLE_FILE_EXTENSIONS.has(extension)) continue;
37147
37571
  let content;
37148
37572
  try {
37149
37573
  content = NFS.readFileSync(absolutePath, "utf-8");
37150
37574
  } catch {
37151
37575
  continue;
37152
37576
  }
37153
- if (REDUCED_MOTION_PATTERN.test(content)) return true;
37577
+ if (STYLE_FILE_EXTENSIONS.has(extension)) {
37578
+ if (hasReducedMotionMediaQuery(content)) hasReducedMotionHandling = true;
37579
+ continue;
37580
+ }
37581
+ scriptSources.push({
37582
+ fileName: absolutePath,
37583
+ sourceText: content
37584
+ });
37154
37585
  }
37155
- return false;
37586
+ if (scriptSources.length === 0) return {
37587
+ hasMotionUse: false,
37588
+ hasReducedMotionHandling
37589
+ };
37590
+ const scriptEvidence = analyzeReducedMotionSources(scriptSources);
37591
+ const hasMotionUse = scriptEvidence.hasMotionUse;
37592
+ hasReducedMotionHandling ||= scriptEvidence.hasReducedMotionHandling;
37593
+ return {
37594
+ hasMotionUse,
37595
+ hasReducedMotionHandling
37596
+ };
37156
37597
  };
37157
37598
  const checkReducedMotion = (rootDirectory) => {
37158
37599
  const packageJsonPath = Path.join(rootDirectory, "package.json");
37159
37600
  if (!isFile(packageJsonPath)) return [];
37160
- let hasMotionLibrary = false;
37161
37601
  try {
37162
37602
  const packageJson = readPackageJson(packageJsonPath);
37163
37603
  const allDependencies = {
37164
37604
  ...packageJson.dependencies,
37165
37605
  ...packageJson.devDependencies
37166
37606
  };
37167
- hasMotionLibrary = Object.keys(allDependencies).some((packageName) => MOTION_LIBRARY_PACKAGES.has(packageName));
37607
+ if (!Object.keys(allDependencies).some((packageName) => MOTION_LIBRARY_PACKAGES.has(packageName))) return [];
37168
37608
  } catch {
37169
37609
  return [];
37170
37610
  }
37171
- if (!hasMotionLibrary) return [];
37172
- const result = spawnSync("git", [
37173
- "grep",
37174
- "--untracked",
37175
- "-lE",
37176
- REDUCED_MOTION_GREP_PATTERN,
37177
- "--",
37178
- ...REDUCED_MOTION_FILE_GLOBS
37179
- ], {
37180
- cwd: rootDirectory,
37181
- encoding: "utf-8",
37182
- stdio: [
37183
- "ignore",
37184
- "pipe",
37185
- "pipe"
37186
- ]
37187
- });
37188
- if (!(!result.error && (result.status === 0 || result.status === GIT_GREP_NO_MATCH_STATUS))) return hasReducedMotionHandlingViaFilesystem(rootDirectory) ? [] : [MISSING_REDUCED_MOTION_DIAGNOSTIC];
37189
- return result.status === 0 && result.stdout.split("\n").some((filePath) => filePath.length > 0 && !hasIgnoredPathSegment(filePath)) ? [] : [MISSING_REDUCED_MOTION_DIAGNOSTIC];
37611
+ const evidence = collectProjectMotionEvidence(rootDirectory);
37612
+ return evidence.hasMotionUse && !evidence.hasReducedMotionHandling ? [MISSING_REDUCED_MOTION_DIAGNOSTIC] : [];
37190
37613
  };
37191
37614
  const buildSecurityScanDiagnostic = (finding, entry, relativePath) => ({
37192
37615
  filePath: relativePath,
@@ -37494,9 +37917,7 @@ const highlighter = {
37494
37917
  gray: import_picocolors.default.gray,
37495
37918
  bold: import_picocolors.default.bold
37496
37919
  };
37497
- const NEXT_ENTRY_FILE_PATTERN = /^(?:\.\/)?(?:src\/)?(?:middleware|proxy)\.(?:tsx?|jsx?|mts|mjs)$/;
37498
- const shouldIncludeExplicitLintPath = (filePath, project) => JSX_FILE_PATTERN.test(filePath) || project?.framework === "nextjs" && NEXT_ENTRY_FILE_PATTERN.test(filePath);
37499
- const computeExplicitLintIncludePaths = (includePaths, project) => includePaths.length > 0 ? includePaths.filter((filePath) => shouldIncludeExplicitLintPath(filePath, project)) : void 0;
37920
+ const computeExplicitLintIncludePaths = (includePaths) => includePaths.length > 0 ? includePaths.filter(isLintableSourceFile) : void 0;
37500
37921
  const LINGUIST_ATTRIBUTE_PATTERN = /^linguist-(?:vendored|generated)(?:=([a-zA-Z0-9]+))?$/i;
37501
37922
  const FALSY_VALUES = new Set([
37502
37923
  "false",
@@ -38338,11 +38759,11 @@ const resolveDeadCodeTimeout = (input) => {
38338
38759
  phaseTimeoutMs: workerTimeoutMs + DEAD_CODE_PHASE_TIMEOUT_OVER_WORKER_MS
38339
38760
  };
38340
38761
  };
38341
- const resolveLintIncludePaths = (rootDirectory, userConfig, project) => {
38762
+ const resolveLintIncludePaths = (rootDirectory, userConfig) => {
38342
38763
  if (!Array.isArray(userConfig?.ignore?.files) || userConfig.ignore.files.length === 0) return;
38343
38764
  const ignoredPatterns = compileIgnoredFilePatterns(userConfig);
38344
38765
  return listSourceFiles(rootDirectory).filter((filePath) => {
38345
- if (!shouldIncludeExplicitLintPath(filePath, project)) return false;
38766
+ if (!isLintableSourceFile(filePath)) return false;
38346
38767
  return !isFileIgnoredByPatterns(filePath, rootDirectory, ignoredPatterns);
38347
38768
  });
38348
38769
  };
@@ -38560,6 +38981,7 @@ const parseGithubViewerPermission = (stdout) => {
38560
38981
  return /^[A-Z_]+$/.test(value) ? value.toLowerCase() : null;
38561
38982
  };
38562
38983
  const splitNullSeparated = (value) => value.split("\0").filter((entry) => entry.length > 0);
38984
+ const UNTRACKED_FILE_LAST_LINE = Number.MAX_SAFE_INTEGER;
38563
38985
  /**
38564
38986
  * `Git` wraps every `git`-via-subprocess call react-doctor makes
38565
38987
  * behind a `Context.Service`. The production layer (`layerNode`)
@@ -38630,6 +39052,14 @@ var Git = class Git extends Service()("react-doctor/Git") {
38630
39052
  args,
38631
39053
  directory
38632
39054
  });
39055
+ const listUntrackedFilePaths = (directory, includePaths = []) => runGit(directory, [
39056
+ "ls-files",
39057
+ "-z",
39058
+ "--others",
39059
+ "--exclude-standard",
39060
+ ...includePaths.length > 0 ? ["--", ...includePaths] : []
39061
+ ]).pipe(map$3((result) => result.status === 0 ? splitNullSeparated(result.stdout) : null));
39062
+ const mergeUntracked = (directory, trackedFilePaths, includeUntracked) => includeUntracked ? listUntrackedFilePaths(directory).pipe(map$3((untracked) => untracked === null ? trackedFilePaths : [...new Set([...trackedFilePaths, ...untracked])])) : succeed$2(trackedFilePaths);
38633
39063
  const currentBranch = (directory) => runGit(directory, [
38634
39064
  "rev-parse",
38635
39065
  "--abbrev-ref",
@@ -38752,7 +39182,7 @@ var Git = class Git extends Service()("react-doctor/Git") {
38752
39182
  githubViewerPermission,
38753
39183
  branchExists,
38754
39184
  mergeBase,
38755
- diffSelection: ({ directory, explicitBaseBranch }) => gen(function* () {
39185
+ diffSelection: ({ directory, explicitBaseBranch, includeUntracked = false }) => gen(function* () {
38756
39186
  if (explicitBaseBranch !== void 0 && explicitBaseBranch.trim().length === 0) return yield* fail$4(new ReactDoctorError({ reason: new GitBaseBranchInvalid({ detail: "Diff base branch cannot be empty." }) }));
38757
39187
  if (explicitBaseBranch !== void 0) {
38758
39188
  const range = parseGitDiffRange(explicitBaseBranch);
@@ -38781,7 +39211,7 @@ var Git = class Git extends Service()("react-doctor/Git") {
38781
39211
  "HEAD"
38782
39212
  ]);
38783
39213
  if (uncommitted.status !== 0) return null;
38784
- const files = splitNullSeparated(uncommitted.stdout);
39214
+ const files = yield* mergeUntracked(directory, splitNullSeparated(uncommitted.stdout), includeUntracked);
38785
39215
  if (files.length === 0) return null;
38786
39216
  return {
38787
39217
  currentBranch: resolvedCurrentBranch,
@@ -38811,7 +39241,7 @@ var Git = class Git extends Service()("react-doctor/Git") {
38811
39241
  currentBranch: resolvedCurrentBranch,
38812
39242
  baseBranch,
38813
39243
  diffBaseRef: mergeBaseRef,
38814
- changedFiles: splitNullSeparated(diff.stdout),
39244
+ changedFiles: yield* mergeUntracked(directory, splitNullSeparated(diff.stdout), includeUntracked),
38815
39245
  isCurrentChanges: false
38816
39246
  };
38817
39247
  }).pipe(withSpan("Git.diffSelection")),
@@ -38857,7 +39287,7 @@ var Git = class Git extends Service()("react-doctor/Git") {
38857
39287
  stdout: result.stdout
38858
39288
  };
38859
39289
  }).pipe(withSpan("Git.grep")),
38860
- changedLineRanges: ({ directory, baseRef, cached, files }) => gen(function* () {
39290
+ changedLineRanges: ({ directory, baseRef, cached, files, includeUntracked = false }) => gen(function* () {
38861
39291
  if (files.length === 0) return [];
38862
39292
  if (baseRef !== void 0 && !isSafeGitRevision(baseRef)) return null;
38863
39293
  const result = yield* runGit(directory, [
@@ -38871,7 +39301,14 @@ var Git = class Git extends Service()("react-doctor/Git") {
38871
39301
  ...files
38872
39302
  ]);
38873
39303
  if (result.status !== 0) return null;
38874
- return parseChangedLineRanges(result.stdout);
39304
+ const changedLineRanges = parseChangedLineRanges(result.stdout);
39305
+ if (cached || !includeUntracked) return changedLineRanges;
39306
+ const untrackedFilePaths = yield* listUntrackedFilePaths(directory, files);
39307
+ if (untrackedFilePaths === null) return changedLineRanges;
39308
+ return [...changedLineRanges, ...untrackedFilePaths.map((file) => ({
39309
+ file,
39310
+ ranges: [[1, UNTRACKED_FILE_LAST_LINE]]
39311
+ }))];
38875
39312
  }).pipe(catch_$1((error) => error.reason._tag === "GitInvocationFailed" ? succeed$2(null) : fail$4(error)), withSpan("Git.changedLineRanges"))
38876
39313
  });
38877
39314
  })).pipe(provide$2(layer$2.pipe(provide$2(mergeAll$1(layer$1, layer)))));
@@ -39456,13 +39893,13 @@ const encodeSidecarFileEntry = (entry) => ({
39456
39893
  diagnostics: entry.diagnostics
39457
39894
  });
39458
39895
  const emptyCache = () => ({
39459
- version: 1,
39896
+ version: 2,
39460
39897
  rulesets: {}
39461
39898
  });
39462
39899
  const loadRulesetEntries = (cacheFilePath, rulesetHash) => {
39463
39900
  const entries = /* @__PURE__ */ new Map();
39464
39901
  const persisted = failOpenReadJson(cacheFilePath, emptyCache());
39465
- if (persisted.version !== 1 || !isRecord(persisted.rulesets)) return entries;
39902
+ if (persisted.version !== 2 || !isRecord(persisted.rulesets)) return entries;
39466
39903
  const bucket = persisted.rulesets[rulesetHash];
39467
39904
  if (!isRecord(bucket) || !isRecord(bucket.files)) return entries;
39468
39905
  for (const [fileKey, rawEntry] of Object.entries(bucket.files)) {
@@ -39482,7 +39919,7 @@ const createSidecarLintCache = (cacheDirectory, rulesetHash) => {
39482
39919
  },
39483
39920
  persist: () => {
39484
39921
  const onDisk = failOpenReadJson(cacheFilePath, emptyCache());
39485
- const rulesets = onDisk.version === 1 && isRecord(onDisk.rulesets) ? { ...onDisk.rulesets } : {};
39922
+ const rulesets = onDisk.version === 2 && isRecord(onDisk.rulesets) ? { ...onDisk.rulesets } : {};
39486
39923
  const existingBucket = rulesets[rulesetHash];
39487
39924
  const existingFiles = isRecord(existingBucket) && isRecord(existingBucket.files) ? existingBucket.files : {};
39488
39925
  const ourFiles = {};
@@ -39496,7 +39933,7 @@ const createSidecarLintCache = (cacheDirectory, rulesetHash) => {
39496
39933
  files: Object.fromEntries(cappedEntries)
39497
39934
  };
39498
39935
  atomicWriteJson(cacheFilePath, {
39499
- version: 1,
39936
+ version: 2,
39500
39937
  rulesets: Object.fromEntries(Object.entries(rulesets).filter(([, bucket]) => isRecord(bucket) && typeof bucket.updatedAtMs === "number").sort(([, first], [, second]) => second.updatedAtMs - first.updatedAtMs).slice(0, 8))
39501
39938
  });
39502
39939
  }
@@ -39547,23 +39984,6 @@ const resolveTsConfigRelativePath = (rootDirectory) => {
39547
39984
  for (const filename of TSCONFIG_FILENAMES) if (NFS.existsSync(Path.join(rootDirectory, filename))) return `./${filename}`;
39548
39985
  return null;
39549
39986
  };
39550
- const DERIVED_STATE_RULE_PRIORITY = new Map([
39551
- ["no-initialize-state", 0],
39552
- ["no-adjust-state-on-prop-change", 1],
39553
- ["no-derived-state", 2],
39554
- ["no-derived-state-effect", 3]
39555
- ]);
39556
- const doDiagnosticSpansOverlap = (first, second) => {
39557
- if (first.filePath !== second.filePath || first.plugin !== second.plugin) return false;
39558
- if (first.offset !== void 0 && second.offset !== void 0) {
39559
- const firstEnd = first.offset + Math.max(first.length ?? 1, 1);
39560
- const secondEnd = second.offset + Math.max(second.length ?? 1, 1);
39561
- return first.offset < secondEnd && second.offset < firstEnd;
39562
- }
39563
- const firstEndLine = first.endLine ?? first.line;
39564
- const secondEndLine = second.endLine ?? second.line;
39565
- return first.line <= secondEndLine && second.line <= firstEndLine;
39566
- };
39567
39987
  const dedupeDiagnostics = (diagnostics) => {
39568
39988
  const seenKeys = /* @__PURE__ */ new Set();
39569
39989
  const uniqueDiagnostics = [];
@@ -39571,16 +39991,6 @@ const dedupeDiagnostics = (diagnostics) => {
39571
39991
  const key = `${diagnostic.filePath}\u0000${diagnostic.line}\u0000${diagnostic.column}\u0000${diagnostic.plugin}\u0000${diagnostic.rule}\u0000${diagnostic.severity}\u0000${diagnostic.message}`;
39572
39992
  if (seenKeys.has(key)) continue;
39573
39993
  seenKeys.add(key);
39574
- const priority = DERIVED_STATE_RULE_PRIORITY.get(diagnostic.rule);
39575
- if (priority !== void 0) {
39576
- const overlappingDiagnosticIndex = uniqueDiagnostics.findIndex((candidate) => candidate.rule !== diagnostic.rule && candidate.severity === diagnostic.severity && DERIVED_STATE_RULE_PRIORITY.has(candidate.rule) && doDiagnosticSpansOverlap(candidate, diagnostic));
39577
- if (overlappingDiagnosticIndex >= 0) {
39578
- const existingDiagnostic = uniqueDiagnostics[overlappingDiagnosticIndex];
39579
- const existingPriority = existingDiagnostic ? DERIVED_STATE_RULE_PRIORITY.get(existingDiagnostic.rule) : void 0;
39580
- if (existingPriority !== void 0 && priority < existingPriority) uniqueDiagnostics[overlappingDiagnosticIndex] = diagnostic;
39581
- continue;
39582
- }
39583
- }
39584
39994
  uniqueDiagnostics.push(diagnostic);
39585
39995
  }
39586
39996
  return uniqueDiagnostics;
@@ -39623,6 +40033,12 @@ const mapWithConcurrency = async (items, concurrency, task) => {
39623
40033
  if (errors.length > 0) throw errors[0];
39624
40034
  return results;
39625
40035
  };
40036
+ const lineOfUtf8Offset = (sourceBuffer, utf8Offset) => {
40037
+ let lineNumber = 1;
40038
+ const scanEnd = Math.min(utf8Offset, sourceBuffer.length);
40039
+ for (let byteIndex = 0; byteIndex < scanEnd; byteIndex++) if (sourceBuffer[byteIndex] === 10) lineNumber++;
40040
+ return lineNumber;
40041
+ };
39626
40042
  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";
39627
40043
  const ACCESSORS_MIN_VERSION = "3.15.0";
39628
40044
  const supportsAccessors = (reanimatedVersion) => {
@@ -39796,23 +40212,12 @@ const REACT_USE_BINDING_RESOLUTION = {
39796
40212
  isReactUseBinding: true,
39797
40213
  isReactNamespaceBinding: false
39798
40214
  };
39799
- const getScriptKind$2 = (filename) => {
39800
- if (filename.endsWith(".tsx")) return ts.ScriptKind.TSX;
39801
- if (filename.endsWith(".jsx")) return ts.ScriptKind.JSX;
39802
- if (filename.endsWith(".ts")) return ts.ScriptKind.TS;
39803
- return ts.ScriptKind.JS;
39804
- };
39805
40215
  const getUtf16Offset$1 = (sourceText, utf8Offset) => Buffer.from(sourceText).subarray(0, utf8Offset).toString("utf8").length;
39806
- const unwrapExpression = (expression) => {
39807
- let currentExpression = expression;
39808
- while (ts.isParenthesizedExpression(currentExpression) || ts.isAsExpression(currentExpression) || ts.isSatisfiesExpression(currentExpression) || ts.isNonNullExpression(currentExpression) || ts.isTypeAssertionExpression(currentExpression)) currentExpression = currentExpression.expression;
39809
- return currentExpression;
39810
- };
39811
40216
  const getStaticPropertyName = (node) => {
39812
40217
  if (!node) return null;
39813
40218
  if (ts.isIdentifier(node) || ts.isStringLiteral(node) || ts.isNumericLiteral(node)) return node.text;
39814
40219
  if (ts.isComputedPropertyName(node)) {
39815
- const expression = unwrapExpression(node.expression);
40220
+ const expression = unwrapTypescriptExpression(node.expression);
39816
40221
  if (ts.isStringLiteral(expression) || ts.isNoSubstitutionTemplateLiteral(expression)) return expression.text;
39817
40222
  }
39818
40223
  return null;
@@ -39841,7 +40246,7 @@ const isReactUseObjectBindingElement = (bindingElement) => {
39841
40246
  return getStaticPropertyName(bindingElement.propertyName) === USE_IDENTIFIER;
39842
40247
  };
39843
40248
  const isReactRequireCall = (expression) => {
39844
- const unwrappedExpression = unwrapExpression(expression);
40249
+ const unwrappedExpression = unwrapTypescriptExpression(expression);
39845
40250
  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;
39846
40251
  };
39847
40252
  const getModuleSource = (node) => {
@@ -39943,7 +40348,7 @@ const isIdentifierShadowedByLocalBinding = (identifier, sourceFile) => {
39943
40348
  return false;
39944
40349
  };
39945
40350
  const isReactNamespaceExpression = (expression, reactImportBindings, sourceFile, visitedDeclarations) => {
39946
- const unwrappedExpression = unwrapExpression(expression);
40351
+ const unwrappedExpression = unwrapTypescriptExpression(expression);
39947
40352
  if (isReactRequireCall(unwrappedExpression)) return true;
39948
40353
  if (!ts.isIdentifier(unwrappedExpression)) return false;
39949
40354
  if (reactImportBindings.namespaceNames.has(unwrappedExpression.text) && !isIdentifierShadowedByLocalBinding(unwrappedExpression, sourceFile)) return true;
@@ -39951,7 +40356,7 @@ const isReactNamespaceExpression = (expression, reactImportBindings, sourceFile,
39951
40356
  };
39952
40357
  const isReactUseExpression = (expression, reactImportBindings, sourceFile, visitedDeclarations) => {
39953
40358
  if (!expression) return false;
39954
- const unwrappedExpression = unwrapExpression(expression);
40359
+ const unwrappedExpression = unwrapTypescriptExpression(expression);
39955
40360
  if (ts.isIdentifier(unwrappedExpression)) {
39956
40361
  if (reactImportBindings.useImportNames.has(unwrappedExpression.text) && !isIdentifierShadowedByLocalBinding(unwrappedExpression, sourceFile)) return true;
39957
40362
  if (unwrappedExpression.text === USE_IDENTIFIER) return false;
@@ -40051,7 +40456,7 @@ const findUseCallIdentifier = (sourceFile, useOffset) => {
40051
40456
  return matchedIdentifier;
40052
40457
  };
40053
40458
  const resolveUseCallBinding = (sourceText, filename, utf8Offset) => {
40054
- const sourceFile = ts.createSourceFile(filename, sourceText, ts.ScriptTarget.Latest, true, getScriptKind$2(filename));
40459
+ const sourceFile = ts.createSourceFile(filename, sourceText, ts.ScriptTarget.Latest, true, getTypescriptScriptKind(filename));
40055
40460
  const useIdentifier = findUseCallIdentifier(sourceFile, getUtf16Offset$1(sourceText, utf8Offset));
40056
40461
  if (!useIdentifier) return null;
40057
40462
  return resolveIdentifierBinding(useIdentifier, collectReactImportBindings(sourceFile), sourceFile);
@@ -40087,12 +40492,6 @@ const WORKLET_ACCEPTING_CALLEES = new Set([
40087
40492
  "executeOnUIRuntimeSync"
40088
40493
  ]);
40089
40494
  const GESTURE_ROOT_IDENTIFIER = "Gesture";
40090
- const getScriptKind$1 = (filename) => {
40091
- if (filename.endsWith(".tsx")) return ts.ScriptKind.TSX;
40092
- if (filename.endsWith(".jsx")) return ts.ScriptKind.JSX;
40093
- if (filename.endsWith(".ts")) return ts.ScriptKind.TS;
40094
- return ts.ScriptKind.JS;
40095
- };
40096
40495
  const getUtf16Offset = (sourceText, utf8Offset) => Buffer.from(sourceText).subarray(0, utf8Offset).toString("utf8").length;
40097
40496
  const hasWorkletDirective = (node) => {
40098
40497
  if (!ts.isFunctionDeclaration(node) && !ts.isFunctionExpression(node) && !ts.isArrowFunction(node) && !ts.isMethodDeclaration(node)) return false;
@@ -40164,17 +40563,11 @@ const shouldSuppressCompilerFindingInWorklet = (diagnostic, project, rootDirecto
40164
40563
  } catch {
40165
40564
  return false;
40166
40565
  }
40167
- return isOffsetInsideWorklet(ts.createSourceFile(absolutePath, sourceText, ts.ScriptTarget.Latest, true, getScriptKind$1(absolutePath)), getUtf16Offset(sourceText, primaryLabel.span.offset));
40566
+ return isOffsetInsideWorklet(ts.createSourceFile(absolutePath, sourceText, ts.ScriptTarget.Latest, true, getTypescriptScriptKind(absolutePath)), getUtf16Offset(sourceText, primaryLabel.span.offset));
40168
40567
  };
40169
40568
  const MANUAL_MEMOIZATION_PLUGIN = "react-doctor";
40170
40569
  const MANUAL_MEMOIZATION_RULE = "react-compiler-no-manual-memoization";
40171
40570
  const REACT_COMPILER_PLUGIN = "react-hooks-js";
40172
- const getScriptKind = (filename) => {
40173
- if (filename.endsWith(".tsx")) return ts.ScriptKind.TSX;
40174
- if (filename.endsWith(".jsx")) return ts.ScriptKind.JSX;
40175
- if (filename.endsWith(".ts")) return ts.ScriptKind.TS;
40176
- return ts.ScriptKind.JS;
40177
- };
40178
40571
  const isManualMemoizationDiagnostic = (diagnostic) => diagnostic.plugin === MANUAL_MEMOIZATION_PLUGIN && diagnostic.rule === MANUAL_MEMOIZATION_RULE;
40179
40572
  const nodeLineRange = (sourceFile, node) => ({
40180
40573
  startLine: sourceFile.getLineAndCharacterOfPosition(node.getStart(sourceFile)).line + 1,
@@ -40194,13 +40587,6 @@ const findOutermostEnclosingFunctionRange = (sourceFile, targetLine) => {
40194
40587
  visit(sourceFile);
40195
40588
  return foundRange;
40196
40589
  };
40197
- const NEWLINE_BYTE = 10;
40198
- const lineOfUtf8Offset = (sourceBuffer, utf8Offset) => {
40199
- let lineNumber = 1;
40200
- const scanEnd = Math.min(utf8Offset, sourceBuffer.length);
40201
- for (let byteIndex = 0; byteIndex < scanEnd; byteIndex++) if (sourceBuffer[byteIndex] === NEWLINE_BYTE) lineNumber++;
40202
- return lineNumber;
40203
- };
40204
40590
  /**
40205
40591
  * Drops `react-doctor/react-compiler-no-manual-memoization` diagnostics
40206
40592
  * whose enclosing function also carries a `react-hooks-js` (React
@@ -40243,7 +40629,7 @@ const suppressMemoizationInBailedOutFunctions = (diagnostics, rootDirectory) =>
40243
40629
  try {
40244
40630
  const buffer = NFS.readFileSync(absolutePath);
40245
40631
  parsed = {
40246
- sourceFile: ts.createSourceFile(absolutePath, buffer.toString("utf8"), ts.ScriptTarget.Latest, true, getScriptKind(absolutePath)),
40632
+ sourceFile: ts.createSourceFile(absolutePath, buffer.toString("utf8"), ts.ScriptTarget.Latest, true, getTypescriptScriptKind(absolutePath)),
40247
40633
  buffer
40248
40634
  };
40249
40635
  } catch {
@@ -40381,11 +40767,11 @@ const buildRelatedLocations = (labels, filePath) => {
40381
40767
  }
40382
40768
  return related;
40383
40769
  };
40384
- const isOxlintOutput = (value) => {
40385
- if (typeof value !== "object" || value === null) return false;
40386
- const candidate = value;
40387
- return Array.isArray(candidate.diagnostics);
40388
- };
40770
+ const isOxlintSpan = (value) => isRecord(value) && typeof value.offset === "number" && typeof value.length === "number" && typeof value.line === "number" && typeof value.column === "number";
40771
+ const isOxlintLabel = (value) => isRecord(value) && isOxlintSpan(value.span);
40772
+ const isMappableOxlintDiagnostic = (value) => isRecord(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);
40773
+ const isEngineFailureDiagnostic = (value) => !isRecord(value) || typeof value.filename !== "string" || value.filename.length === 0;
40774
+ const isOxlintOutput = (value) => isRecord(value) && Array.isArray(value.diagnostics);
40389
40775
  /**
40390
40776
  * Parses one oxlint subprocess's stdout into a flat `Diagnostic[]`.
40391
40777
  * Tolerates the leading-notice-line shape oxlint sometimes prints
@@ -40404,23 +40790,59 @@ const parseOxlintOutput = (stdout, project, rootDirectory) => {
40404
40790
  throw new ReactDoctorError({ reason: new OxlintOutputUnparseable({ preview: stdout.slice(0, 600) }) });
40405
40791
  }
40406
40792
  if (!isOxlintOutput(parsed)) throw new ReactDoctorError({ reason: new OxlintOutputUnparseable({ preview: stdout.slice(0, 600) }) });
40793
+ const engineFailureDiagnostic = parsed.diagnostics.find(isEngineFailureDiagnostic);
40794
+ if (engineFailureDiagnostic !== void 0) throw new ReactDoctorError({ reason: new OxlintOutputUnparseable({ preview: JSON.stringify(engineFailureDiagnostic).slice(0, 600) }) });
40407
40795
  const minifiedFileCache = /* @__PURE__ */ new Map();
40796
+ const sourceBufferCache = /* @__PURE__ */ new Map();
40797
+ const sourceLinesCache = /* @__PURE__ */ new Map();
40798
+ const resolveAbsolutePath = (filename) => Path.isAbsolute(filename) ? filename : Path.resolve(rootDirectory || ".", filename);
40799
+ const readSourceBuffer = (filename) => {
40800
+ const absolutePath = resolveAbsolutePath(filename);
40801
+ const cached = sourceBufferCache.get(absolutePath);
40802
+ if (cached !== void 0) return cached;
40803
+ let sourceBuffer;
40804
+ try {
40805
+ sourceBuffer = NFS.readFileSync(absolutePath);
40806
+ } catch {
40807
+ sourceBuffer = null;
40808
+ }
40809
+ sourceBufferCache.set(absolutePath, sourceBuffer);
40810
+ return sourceBuffer;
40811
+ };
40812
+ const readSourceLines = (filename) => {
40813
+ const absolutePath = resolveAbsolutePath(filename);
40814
+ const cached = sourceLinesCache.get(absolutePath);
40815
+ if (cached !== void 0) return cached;
40816
+ const sourceBuffer = readSourceBuffer(filename);
40817
+ const sourceLines = sourceBuffer ? sourceBuffer.toString("utf8").split("\n") : null;
40818
+ sourceLinesCache.set(absolutePath, sourceLines);
40819
+ return sourceLines;
40820
+ };
40408
40821
  const isMinifiedDiagnosticFile = (filename) => {
40409
- const absolutePath = Path.isAbsolute(filename) ? filename : Path.resolve(rootDirectory || ".", filename);
40822
+ const absolutePath = resolveAbsolutePath(filename);
40410
40823
  const cached = minifiedFileCache.get(absolutePath);
40411
40824
  if (cached !== void 0) return cached;
40412
40825
  const minified = isMinifiedSource(absolutePath);
40413
40826
  minifiedFileCache.set(absolutePath, minified);
40414
40827
  return minified;
40415
40828
  };
40416
- return suppressMemoizationInBailedOutFunctions(parsed.diagnostics.filter((diagnostic) => diagnostic.code && isLintableSourceFile(diagnostic.filename) && !isMinifiedDiagnosticFile(diagnostic.filename) && !shouldSuppressLocalUseHookDiagnostic(diagnostic, rootDirectory) && !shouldSuppressCompilerFindingInWorklet(diagnostic, project, rootDirectory)).map((diagnostic) => {
40829
+ return suppressMemoizationInBailedOutFunctions(parsed.diagnostics.filter((diagnostic) => isMappableOxlintDiagnostic(diagnostic) && isLintableSourceFile(diagnostic.filename) && !isMinifiedDiagnosticFile(diagnostic.filename) && !shouldSuppressLocalUseHookDiagnostic(diagnostic, rootDirectory) && !shouldSuppressCompilerFindingInWorklet(diagnostic, project, rootDirectory)).map((diagnostic) => {
40417
40830
  const { plugin, rule } = parseRuleCode(diagnostic.code);
40418
40831
  const primaryLabel = diagnostic.labels[0];
40419
40832
  const cleaned = cleanDiagnosticMessage(diagnostic.message, diagnostic.help, plugin, rule, project);
40420
40833
  const normalizedFilePath = diagnostic.filename.replaceAll("\\", "/");
40421
40834
  const primarySpan = primaryLabel?.span;
40835
+ const sourceBuffer = primarySpan ? readSourceBuffer(diagnostic.filename) : null;
40422
40836
  const relatedLocations = buildRelatedLocations(diagnostic.labels, normalizedFilePath);
40423
40837
  const category = resolveDiagnosticCategory(plugin, rule);
40838
+ const matchByOccurrence = resolveMatchByOccurrence(rule, category);
40839
+ const sourceLines = primarySpan ? readSourceLines(diagnostic.filename) : null;
40840
+ const primaryLineIndex = primarySpan ? primarySpan.line - 1 : -1;
40841
+ const primaryLine = sourceLines?.[primaryLineIndex];
40842
+ const primaryColumnIndex = primarySpan ? primarySpan.column - 1 : -1;
40843
+ const isJsxTagLabel = primaryLine !== void 0 && primaryColumnIndex >= 0 && (primaryLine[primaryColumnIndex] === "<" || primaryLine[primaryColumnIndex - 1] === "<");
40844
+ const jsxOpenerEndLineIndex = sourceLines && isJsxTagLabel ? findJsxOpenerSpan(sourceLines, primaryLineIndex) : null;
40845
+ const primarySpanEndLine = jsxOpenerEndLineIndex !== null ? jsxOpenerEndLineIndex + 1 : primarySpan && sourceBuffer ? lineOfUtf8Offset(sourceBuffer, primarySpan.offset + Math.max(primarySpan.length - 1, 0)) : void 0;
40424
40846
  return {
40425
40847
  filePath: normalizedFilePath,
40426
40848
  plugin,
@@ -40434,10 +40856,11 @@ const parseOxlintOutput = (stdout, project, rootDirectory) => {
40434
40856
  column: primarySpan?.column ?? 0,
40435
40857
  ...primarySpan ? {
40436
40858
  offset: primarySpan.offset,
40437
- length: primarySpan.length
40859
+ length: primarySpan.length,
40860
+ ...primarySpanEndLine !== void 0 ? { endLine: primarySpanEndLine } : {}
40438
40861
  } : {},
40439
40862
  category,
40440
- ...resolveMatchByOccurrence(rule, category) ? { matchByOccurrence: true } : {},
40863
+ ...matchByOccurrence ? { matchByOccurrence: true } : {},
40441
40864
  ...relatedLocations.length > 0 ? { relatedLocations } : {}
40442
40865
  };
40443
40866
  }), rootDirectory);
@@ -42224,7 +42647,7 @@ const runInspect = (input, hooks = {}) => gen(function* () {
42224
42647
  directory: scanDirectory,
42225
42648
  repo
42226
42649
  }).pipe(orElseSucceed(() => null)) : succeed$2(null));
42227
- const lintIncludePaths = (input.skipJsxIncludeFilter ? input.includePaths.length > 0 ? [...input.includePaths] : void 0 : computeExplicitLintIncludePaths([...input.includePaths], project)) ?? resolveLintIncludePaths(scanDirectory, resolvedConfig.config, project);
42650
+ const lintIncludePaths = (input.skipExplicitIncludePathFilter ? input.includePaths.length > 0 ? [...input.includePaths] : void 0 : computeExplicitLintIncludePaths([...input.includePaths])) ?? resolveLintIncludePaths(scanDirectory, resolvedConfig.config);
42228
42651
  const fallbackScannedFilePaths = input.suppressScanSummary ? (lintIncludePaths ?? (yield* filesService.listSourceFiles(scanDirectory))).map((relativePath) => Path.resolve(scanDirectory, relativePath)) : [];
42229
42652
  const beforeLint = hooks.beforeLint ?? NO_HOOKS.beforeLint;
42230
42653
  const afterLint = hooks.afterLint ?? NO_HOOKS.afterLint;
@@ -42238,7 +42661,8 @@ const runInspect = (input, hooks = {}) => gen(function* () {
42238
42661
  respectInlineDisables: input.respectInlineDisables,
42239
42662
  showWarnings
42240
42663
  });
42241
- const applyPerElementPipeline = (rawStream) => rawStream.pipe(filterMap(filterMapNullable(transform.apply)), tap((diagnostic) => reporterService.emit(diagnostic)));
42664
+ const filterPerElementPipeline = (rawStream) => rawStream.pipe(filterMap(filterMapNullable(transform.apply)));
42665
+ const applyPerElementPipeline = (rawStream) => filterPerElementPipeline(rawStream).pipe(tap((diagnostic) => reporterService.emit(diagnostic)));
42242
42666
  const envCollected = yield* runCollect(applyPerElementPipeline(fromIterable$1(isDiffMode ? [] : [
42243
42667
  ...checkReducedMotion(scanDirectory),
42244
42668
  ...checkPnpmHardening(scanDirectory),
@@ -42367,7 +42791,7 @@ const runInspect = (input, hooks = {}) => gen(function* () {
42367
42791
  });
42368
42792
  return empty$4;
42369
42793
  }))));
42370
- const lintCollected = yield* runCollect(applyPerElementPipeline(shouldOverlapDeadCode ? baseLintStream.pipe(provideService(OxlintConcurrency, lintConcurrency)) : baseLintStream)).pipe(timeoutOption(lintPhaseTimeoutMs), flatMap$2(match$3({
42794
+ const lintCollected = dedupeRelatedDiagnostics(yield* runCollect(filterPerElementPipeline(shouldOverlapDeadCode ? baseLintStream.pipe(provideService(OxlintConcurrency, lintConcurrency)) : baseLintStream)).pipe(timeoutOption(lintPhaseTimeoutMs), flatMap$2(match$3({
42371
42795
  onNone: () => set(lintFailure, {
42372
42796
  didFail: true,
42373
42797
  reason: `Lint analysis exceeded ${lintPhaseTimeoutMs / MILLISECONDS_PER_SECOND}s and was skipped.`,
@@ -42375,7 +42799,8 @@ const runInspect = (input, hooks = {}) => gen(function* () {
42375
42799
  reasonKind: null
42376
42800
  }).pipe(as([])),
42377
42801
  onSome: succeed$2
42378
- })));
42802
+ }))));
42803
+ yield* forEach$1(lintCollected, reporterService.emit, { discard: true });
42379
42804
  const lintFailureState = yield* get$2(lintFailure);
42380
42805
  yield* afterLint(lintFailureState.didFail);
42381
42806
  if (lintFailureState.didFail) yield* scanProgress.fail(formatLintFailText(lintFailureState.reasonTag, process.version));
@@ -42719,7 +43144,7 @@ const runEditorScan = async (input) => {
42719
43144
  warnings,
42720
43145
  isCi: false,
42721
43146
  resolveLocalGithubViewerPermission: false,
42722
- skipJsxIncludeFilter: true
43147
+ skipExplicitIncludePathFilter: true
42723
43148
  }).pipe(withSpan("runEditorScan", { attributes: {
42724
43149
  "editor.lint": lint,
42725
43150
  "editor.runDeadCode": runDeadCode
@@ -42803,7 +43228,7 @@ const computeConfigFingerprint = (projectDirectory, version) => {
42803
43228
  /** Display name used in client-facing messages and progress titles. */
42804
43229
  const SERVER_DISPLAY_NAME = "React Doctor";
42805
43230
  /** Server version reported in `serverInfo`; injected at build, `dev` from source. */
42806
- const SERVER_VERSION = "0.7.6";
43231
+ const SERVER_VERSION = "0.7.7";
42807
43232
  /** `Diagnostic.source` shown next to every published diagnostic. */
42808
43233
  const DIAGNOSTIC_SOURCE = "react-doctor";
42809
43234
  /**
@@ -43482,7 +43907,7 @@ const createLintCache = (input) => {
43482
43907
  let persistTimer = null;
43483
43908
  try {
43484
43909
  const parsed = JSON.parse(fs.readFileSync(cacheFilePath, "utf8"));
43485
- if (parsed.version === 1 && parsed.fingerprint === input.fingerprint) for (const [fsPath, entry] of Object.entries(parsed.entries)) entries.set(fsPath, entry);
43910
+ if (parsed.version === 2 && parsed.fingerprint === input.fingerprint) for (const [fsPath, entry] of Object.entries(parsed.entries)) entries.set(fsPath, entry);
43486
43911
  } catch {}
43487
43912
  const persist = () => {
43488
43913
  if (persistTimer) {
@@ -43493,7 +43918,7 @@ const createLintCache = (input) => {
43493
43918
  dirty = false;
43494
43919
  try {
43495
43920
  const payload = {
43496
- version: 1,
43921
+ version: 2,
43497
43922
  fingerprint: input.fingerprint,
43498
43923
  entries: Object.fromEntries(entries)
43499
43924
  };
@@ -43506,14 +43931,14 @@ const createLintCache = (input) => {
43506
43931
  }
43507
43932
  };
43508
43933
  return {
43509
- lookup: (fsPath, stat) => {
43934
+ lookup: (fsPath, identity) => {
43510
43935
  const entry = entries.get(fsPath);
43511
- if (entry !== void 0 && entry.mtimeMs === stat.mtimeMs && entry.size === stat.size) return entry.diagnostics;
43936
+ if (entry !== void 0 && entry.contentHash === identity.contentHash) return entry.diagnostics;
43512
43937
  return null;
43513
43938
  },
43514
- store: (fsPath, stat, diagnostics) => {
43939
+ store: (fsPath, identity, diagnostics) => {
43515
43940
  entries.set(fsPath, {
43516
- ...stat,
43941
+ ...identity,
43517
43942
  diagnostics
43518
43943
  });
43519
43944
  dirty = true;
@@ -43602,16 +44027,9 @@ const resolveDiagnosticFsPath = (rawFilePath, scanDirectory, projectDirectory, o
43602
44027
  }
43603
44028
  return normalizeFsPath(absolute);
43604
44029
  };
43605
- const statSafe = (fsPath) => {
43606
- try {
43607
- const stat = fs.statSync(fsPath);
43608
- return {
43609
- mtimeMs: stat.mtimeMs,
43610
- size: stat.size
43611
- };
43612
- } catch {
43613
- return null;
43614
- }
44030
+ const readFileIdentity = (fsPath) => {
44031
+ const contentHash = hashFileContents(fsPath);
44032
+ return contentHash === null ? null : { contentHash };
43615
44033
  };
43616
44034
  /**
43617
44035
  * Outcome for a per-file request served without running oxlint (every
@@ -43637,7 +44055,7 @@ const outcomeWithoutScan = (request, byFile, requestedPaths) => ({
43637
44055
  * overlay tree (unsaved buffers) or disk, groups diagnostics by canonical
43638
44056
  * absolute path, and reports stale-detection metadata.
43639
44057
  *
43640
- * A persistent per-file lint cache (keyed by mtime + size, namespaced by a
44058
+ * A persistent per-file lint cache (keyed by content, namespaced by a
43641
44059
  * config fingerprint) short-circuits unchanged files so a re-opened editor
43642
44060
  * or repeated workspace scan skips the oxlint subprocess for everything it
43643
44061
  * hasn't edited. The cache applies only to disk-based, lint-only, per-file
@@ -43667,15 +44085,15 @@ const createScanRunner = (options) => {
43667
44085
  const requestedPaths = request.priority === "background" && !request.useOverlay && options.isOpen ? allRequested.filter((fsPath) => !options.isOpen?.(fsPath)) : allRequested;
43668
44086
  const cache = cacheEnabled && !isWholeProject && !request.useOverlay && !request.runDeadCode ? getCache(projectDirectory) : null;
43669
44087
  const byFile = /* @__PURE__ */ new Map();
43670
- const statByPath = /* @__PURE__ */ new Map();
44088
+ const identityByPath = /* @__PURE__ */ new Map();
43671
44089
  let filesToScan = requestedPaths;
43672
44090
  if (cache) {
43673
44091
  const uncached = [];
43674
44092
  for (const fsPath of requestedPaths) {
43675
- const stat = statSafe(fsPath);
43676
- if (stat) {
43677
- statByPath.set(fsPath, stat);
43678
- const hit = cache.lookup(fsPath, stat);
44093
+ const identity = readFileIdentity(fsPath);
44094
+ if (identity) {
44095
+ identityByPath.set(fsPath, identity);
44096
+ const hit = cache.lookup(fsPath, identity);
43679
44097
  if (hit !== null) {
43680
44098
  if (hit.length > 0) byFile.set(fsPath, hit);
43681
44099
  continue;
@@ -43718,9 +44136,9 @@ const createScanRunner = (options) => {
43718
44136
  }
43719
44137
  if (cache && result.ok && !result.skipped && !result.didLintFail && result.lintPartialFailures.length === 0) {
43720
44138
  for (const fsPath of filesToScan) {
43721
- const stat = statByPath.get(fsPath);
43722
- if (!stat) continue;
43723
- cache.store(fsPath, stat, byFile.get(fsPath) ?? []);
44139
+ const identity = identityByPath.get(fsPath);
44140
+ if (!identity) continue;
44141
+ cache.store(fsPath, identity, byFile.get(fsPath) ?? []);
43724
44142
  }
43725
44143
  cache.schedulePersist();
43726
44144
  }
@@ -44500,6 +44918,7 @@ const METRIC = {
44500
44918
  cliInvoked: "cli.invoked",
44501
44919
  cliError: "cli.error",
44502
44920
  cliEnvironmentError: "cli.env_error",
44921
+ stagedSnapshotDivergence: "staged.snapshot_divergence",
44503
44922
  projectDetected: "project.detected",
44504
44923
  projectPathSelected: "project.path_selected",
44505
44924
  projectConfigSelected: "project.config_selected",
@@ -44854,5 +45273,5 @@ const startLanguageServer = () => {
44854
45273
  };
44855
45274
  //#endregion
44856
45275
  export { startLanguageServer };
44857
- !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]="b77898db-d8e8-531a-a32b-9de2d1a63635")}catch(e){}}();
44858
- //# debugId=b77898db-d8e8-531a-a32b-9de2d1a63635
45276
+ !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]="9428cea1-cc36-547a-941f-20487b0140fc")}catch(e){}}();
45277
+ //# debugId=9428cea1-cc36-547a-941f-20487b0140fc