react-doctor 0.7.6 → 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/index.js CHANGED
@@ -1,5 +1,5 @@
1
1
 
2
- !function(){try{var e="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof globalThis?globalThis:"undefined"!=typeof self?self:{},n=(new e.Error).stack;n&&(e._sentryDebugIds=e._sentryDebugIds||{},e._sentryDebugIds[n]="7a10d23c-04fd-53e1-a623-a3dc5d6d0830")}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]="4096a0e2-9712-5e2f-b602-5ff55d3994a3")}catch(e){}}();
3
3
  import { r as __toESM$1, t as __commonJSMin$1 } from "./chunk-N93fKeF6.js";
4
4
  import { createRequire } from "node:module";
5
5
  import reactDoctorPlugin, { ALL_REACT_DOCTOR_RULE_KEYS, CROSS_FILE_DEPENDENCY_COLLECTORS, CROSS_FILE_RULE_IDS, FRAMEWORK_SPECIFIC_RULE_KEYS, FRAMEWORK_TOKENS, MOTION_LIBRARY_PACKAGES, REACT_COMPILER_RULES, REACT_DOCTOR_RULES, classifySecurityScanFile, collectCrossFileDependencyProbes, resetManifestCaches, shouldReadSecurityScanContent } from "oxlint-plugin-react-doctor";
@@ -32930,7 +32930,6 @@ const IGNORED_DIRECTORIES = new Set([
32930
32930
  "out",
32931
32931
  "storybook-static"
32932
32932
  ]);
32933
- const JSX_FILE_PATTERN = /\.(tsx|jsx)$/;
32934
32933
  const MILLISECONDS_PER_SECOND = 1e3;
32935
32934
  const SCORE_API_URL = "https://www.react.doctor/api/score";
32936
32935
  const FETCH_TIMEOUT_MS = 1e4;
@@ -33040,7 +33039,7 @@ const SOCKET_FREE_USER_AGENT = "react-doctor-supply-chain";
33040
33039
  const FILE_LINT_CACHE_FILENAME = "file-lint-cache.json";
33041
33040
  const FILE_LINT_CACHE_MAX_FILE_COUNT = 5e4;
33042
33041
  const SIDECAR_LINT_CACHE_FILENAME = "sidecar-lint-cache.json";
33043
- const CORE_PACKAGE_VERSION = "0.7.6";
33042
+ const CORE_PACKAGE_VERSION = "0.7.7";
33044
33043
  const DEAD_CODE_CACHE_FILENAME = "dead-code-cache.json";
33045
33044
  const DEAD_CODE_SUMMARY_CACHE_FILENAME = "dead-code-summaries.json";
33046
33045
  const SUPPLY_CHAIN_PLUGIN = "socket";
@@ -33143,13 +33142,15 @@ const listSourceFilesViaFilesystem = (rootDirectory) => {
33143
33142
  const listSourceFilesWithSize = (rootDirectory) => collectSizedSourceFiles(rootDirectory, (listSourceFilesViaGit(rootDirectory) ?? listSourceFilesViaFilesystem(rootDirectory)).sort());
33144
33143
  const listSourceFiles = (rootDirectory) => listSourceFilesWithSize(rootDirectory).map((entry) => entry.path);
33145
33144
  const countSourceFiles = (rootDirectory) => listSourceFilesWithSize(rootDirectory).length;
33145
+ const UTF8_BOM_CHARACTER = "";
33146
+ const stripUtf8Bom = (content) => content.startsWith(UTF8_BOM_CHARACTER) ? content.slice(1) : content;
33146
33147
  const cachedPackageJsons = /* @__PURE__ */ new Map();
33147
33148
  const clearPackageJsonCache = () => {
33148
33149
  cachedPackageJsons.clear();
33149
33150
  };
33150
33151
  const readPackageJsonUncached = (packageJsonPath) => {
33151
33152
  try {
33152
- return JSON.parse(NFS.readFileSync(packageJsonPath, "utf-8"));
33153
+ return JSON.parse(stripUtf8Bom(NFS.readFileSync(packageJsonPath, "utf-8")));
33153
33154
  } catch (error) {
33154
33155
  if (error instanceof SyntaxError) return {};
33155
33156
  if (isErrnoException(error)) {
@@ -36365,6 +36366,49 @@ const assignFixGroups = (diagnostics) => {
36365
36366
  };
36366
36367
  });
36367
36368
  };
36369
+ const DERIVED_STATE_RULE_PRIORITY = new Map([
36370
+ ["no-initialize-state", 0],
36371
+ ["no-adjust-state-on-prop-change", 1],
36372
+ ["no-derived-state", 2],
36373
+ ["no-derived-state-effect", 3]
36374
+ ]);
36375
+ const buildDiagnosticSiteKey = (diagnostic) => `${diagnostic.filePath}\u0000${diagnostic.line}\u0000${diagnostic.column}`;
36376
+ const isReactDoctorRulesOfHooksDiagnostic = (diagnostic) => diagnostic.plugin === "react-doctor" && diagnostic.rule === "rules-of-hooks";
36377
+ const isReactCompilerHooksDiagnostic = (diagnostic) => diagnostic.plugin === "react-hooks-js" && diagnostic.rule === "hooks";
36378
+ const doDiagnosticSpansOverlap = (first, second) => {
36379
+ if (first.filePath !== second.filePath || first.plugin !== second.plugin) return false;
36380
+ if (first.offset !== void 0 && second.offset !== void 0) {
36381
+ const firstEnd = first.offset + Math.max(first.length ?? 1, 1);
36382
+ const secondEnd = second.offset + Math.max(second.length ?? 1, 1);
36383
+ return first.offset < secondEnd && second.offset < firstEnd;
36384
+ }
36385
+ const firstEndLine = first.endLine ?? first.line;
36386
+ const secondEndLine = second.endLine ?? second.line;
36387
+ return first.line <= secondEndLine && second.line <= firstEndLine;
36388
+ };
36389
+ const keepHighestSeverity = (survivingDiagnostic, collapsedDiagnostic) => collapsedDiagnostic.severity === "error" && survivingDiagnostic.severity !== "error" ? {
36390
+ ...survivingDiagnostic,
36391
+ severity: "error"
36392
+ } : survivingDiagnostic;
36393
+ const dedupeRelatedDiagnostics = (diagnostics) => {
36394
+ const reactDoctorHookSites = new Set(diagnostics.filter(isReactDoctorRulesOfHooksDiagnostic).map(buildDiagnosticSiteKey));
36395
+ const uniqueDiagnostics = [];
36396
+ for (const diagnostic of diagnostics) {
36397
+ if (isReactCompilerHooksDiagnostic(diagnostic) && reactDoctorHookSites.has(buildDiagnosticSiteKey(diagnostic))) continue;
36398
+ const priority = DERIVED_STATE_RULE_PRIORITY.get(diagnostic.rule);
36399
+ if (priority !== void 0) {
36400
+ const overlappingDiagnosticIndex = uniqueDiagnostics.findIndex((candidate) => candidate.rule !== diagnostic.rule && DERIVED_STATE_RULE_PRIORITY.has(candidate.rule) && doDiagnosticSpansOverlap(candidate, diagnostic));
36401
+ if (overlappingDiagnosticIndex >= 0) {
36402
+ const existingDiagnostic = uniqueDiagnostics[overlappingDiagnosticIndex];
36403
+ const existingPriority = existingDiagnostic ? DERIVED_STATE_RULE_PRIORITY.get(existingDiagnostic.rule) : void 0;
36404
+ if (existingDiagnostic && existingPriority !== void 0) uniqueDiagnostics[overlappingDiagnosticIndex] = priority < existingPriority ? keepHighestSeverity(diagnostic, existingDiagnostic) : keepHighestSeverity(existingDiagnostic, diagnostic);
36405
+ continue;
36406
+ }
36407
+ }
36408
+ uniqueDiagnostics.push(diagnostic);
36409
+ }
36410
+ return uniqueDiagnostics;
36411
+ };
36368
36412
  const compareStrings = (left, right) => left < right ? -1 : left > right ? 1 : 0;
36369
36413
  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));
36370
36414
  const getDirectDependencyNames = (packageJson) => new Set([...Object.keys(packageJson.dependencies ?? {}), ...Object.keys(packageJson.devDependencies ?? {})]);
@@ -36790,7 +36834,6 @@ const PNPM_WORKSPACE_FILE = "pnpm-workspace.yaml";
36790
36834
  const PNPM_LOCKFILE = "pnpm-lock.yaml";
36791
36835
  const PACKAGE_JSON_FILE = "package.json";
36792
36836
  const PNPM_HARDENING_RULE_KEY = "require-pnpm-hardening";
36793
- const UTF8_BOM_CHAR = "";
36794
36837
  const HARDENING_SETTING_KEYS = new Set([
36795
36838
  "minimumReleaseAge",
36796
36839
  "blockExoticSubdeps",
@@ -36815,12 +36858,11 @@ const stripInlineComment = (rawValue) => {
36815
36858
  return rawValue;
36816
36859
  };
36817
36860
  const unquote = (rawValue) => rawValue.replace(/^["']|["']$/g, "");
36818
- const stripBom = (rawContent) => rawContent.startsWith(UTF8_BOM_CHAR) ? rawContent.slice(1) : rawContent;
36819
36861
  const parseHardeningSettings = (content) => {
36820
36862
  let minimumReleaseAge = null;
36821
36863
  let blockExoticSubdeps = null;
36822
36864
  let trustPolicy = null;
36823
- const lines = stripBom(content).split(/\r?\n/);
36865
+ const lines = stripUtf8Bom(content).split(/\r?\n/);
36824
36866
  for (let lineIndex = 0; lineIndex < lines.length; lineIndex += 1) {
36825
36867
  const lineText = lines[lineIndex];
36826
36868
  if (lineText === void 0) continue;
@@ -36854,7 +36896,7 @@ const isPnpmManagedProject = (rootDirectory) => {
36854
36896
  const packageJsonPath = Path.join(rootDirectory, PACKAGE_JSON_FILE);
36855
36897
  if (!isFile(packageJsonPath)) return false;
36856
36898
  try {
36857
- const packageJsonRaw = NFS.readFileSync(packageJsonPath, "utf-8");
36899
+ const packageJsonRaw = stripUtf8Bom(NFS.readFileSync(packageJsonPath, "utf-8"));
36858
36900
  const packageJson = JSON.parse(packageJsonRaw);
36859
36901
  if (packageJson !== null && typeof packageJson === "object" && "packageManager" in packageJson && typeof packageJson.packageManager === "string" && packageJson.packageManager.startsWith("pnpm@")) return true;
36860
36902
  } catch {
@@ -37140,18 +37182,45 @@ const checkReactServerComponentsAdvisory = (scanDirectory, project) => {
37140
37182
  }
37141
37183
  return diagnostics;
37142
37184
  };
37143
- const REDUCED_MOTION_GREP_PATTERN = "prefers-reduced-motion|useReducedMotion|MotionConfig|reducedMotion";
37144
- const REDUCED_MOTION_PATTERN = new RegExp(REDUCED_MOTION_GREP_PATTERN);
37145
- const REDUCED_MOTION_FILE_EXTENSIONS = new Set([
37185
+ const getTypescriptScriptKind = (fileName) => {
37186
+ if (fileName.endsWith(".tsx")) return ts.ScriptKind.TSX;
37187
+ if (fileName.endsWith(".jsx")) return ts.ScriptKind.JSX;
37188
+ if (fileName.endsWith(".ts")) return ts.ScriptKind.TS;
37189
+ return ts.ScriptKind.JS;
37190
+ };
37191
+ const unwrapTypescriptExpression = (expression) => {
37192
+ let currentExpression = expression;
37193
+ while (ts.isParenthesizedExpression(currentExpression) || ts.isAsExpression(currentExpression) || ts.isSatisfiesExpression(currentExpression) || ts.isNonNullExpression(currentExpression) || ts.isTypeAssertionExpression(currentExpression)) currentExpression = currentExpression.expression;
37194
+ return currentExpression;
37195
+ };
37196
+ const EMPTY_MOTION_EXPRESSION_EVIDENCE = {
37197
+ isAnimationFunction: false,
37198
+ isMotionComponent: false,
37199
+ isMotionComponentFactory: false,
37200
+ isMotionComponentNamespace: false,
37201
+ isMotionConfig: false,
37202
+ isMotionNamespace: false,
37203
+ isReducedMotionHook: false
37204
+ };
37205
+ const MOTION_COMPONENT_FACTORY_EXPORT_NAMES = new Set(["m", "motion"]);
37206
+ const MOTION_COMPONENT_NAMESPACE_EXPORT_NAMES = new Set(["Reorder"]);
37207
+ const MOTION_COMPONENT_NAMESPACE_MEMBER_NAMES = new Set(["Item"]);
37208
+ const MOTION_ANIMATION_FUNCTION_EXPORT_NAMES = new Set(["animate"]);
37209
+ const REDUCED_MOTION_HOOK_EXPORT_NAME = "useReducedMotion";
37210
+ const MOTION_CONFIG_EXPORT_NAME = "MotionConfig";
37211
+ const REDUCED_MOTION_PROP_NAME = "reducedMotion";
37212
+ const REDUCED_MOTION_CONFIG_VALUES = new Set(["always", "user"]);
37213
+ const SCRIPT_MODULE_EXTENSIONS = [
37146
37214
  ".ts",
37147
37215
  ".tsx",
37148
37216
  ".js",
37149
- ".jsx",
37150
- ".css",
37151
- ".scss"
37152
- ]);
37153
- const REDUCED_MOTION_FILE_GLOBS = [...REDUCED_MOTION_FILE_EXTENSIONS].map((extension) => `*${extension}`);
37154
- const GIT_GREP_NO_MATCH_STATUS = 1;
37217
+ ".jsx"
37218
+ ];
37219
+ const SCRIPT_FILE_EXTENSIONS = new Set(SCRIPT_MODULE_EXTENSIONS);
37220
+ const STYLE_FILE_EXTENSIONS = new Set([".css", ".scss"]);
37221
+ const MOTION_SOURCE_PREFILTER = /framer-motion|["']motion(?:\/[A-Za-z0-9_./-]+)?["']|MotionConfig|useReducedMotion/;
37222
+ const REDUCED_MOTION_MEDIA_QUERY_PATTERN = /@media\s+([^{}]*\(\s*prefers-reduced-motion\s*:\s*reduce\s*\)[^{}]*)\{/gi;
37223
+ const CSS_DECLARATION_PATTERN = /(?:^|[;{])\s*[-_A-Za-z][-_A-Za-z0-9]*\s*:/;
37155
37224
  const MISSING_REDUCED_MOTION_DIAGNOSTIC = {
37156
37225
  filePath: "package.json",
37157
37226
  plugin: "react-doctor",
@@ -37163,52 +37232,406 @@ const MISSING_REDUCED_MOTION_DIAGNOSTIC = {
37163
37232
  column: 0,
37164
37233
  category: "Accessibility"
37165
37234
  };
37166
- const hasReducedMotionHandlingViaFilesystem = (rootDirectory) => {
37235
+ const isMotionModuleSource = (moduleSource) => {
37236
+ for (const packageName of MOTION_LIBRARY_PACKAGES) if (moduleSource === packageName || moduleSource.startsWith(`${packageName}/`)) return true;
37237
+ return false;
37238
+ };
37239
+ const classifyMotionExport = (exportName) => ({
37240
+ isAnimationFunction: MOTION_ANIMATION_FUNCTION_EXPORT_NAMES.has(exportName),
37241
+ isMotionComponent: false,
37242
+ isMotionComponentFactory: MOTION_COMPONENT_FACTORY_EXPORT_NAMES.has(exportName),
37243
+ isMotionComponentNamespace: MOTION_COMPONENT_NAMESPACE_EXPORT_NAMES.has(exportName),
37244
+ isMotionConfig: exportName === MOTION_CONFIG_EXPORT_NAME,
37245
+ isMotionNamespace: false,
37246
+ isReducedMotionHook: exportName === REDUCED_MOTION_HOOK_EXPORT_NAME
37247
+ });
37248
+ const getImportModuleSource = (node) => {
37249
+ let currentNode = node;
37250
+ while (currentNode) {
37251
+ if (ts.isImportDeclaration(currentNode) && ts.isStringLiteral(currentNode.moduleSpecifier)) return currentNode.moduleSpecifier.text;
37252
+ currentNode = currentNode.parent;
37253
+ }
37254
+ return null;
37255
+ };
37256
+ const getImportedBindingEvidence = (declaration, typeChecker, program, visitedSymbols) => {
37257
+ const moduleSource = getImportModuleSource(declaration);
37258
+ if (!moduleSource) return null;
37259
+ if (isMotionModuleSource(moduleSource) && ts.isNamespaceImport(declaration)) return {
37260
+ ...EMPTY_MOTION_EXPRESSION_EVIDENCE,
37261
+ isMotionNamespace: true
37262
+ };
37263
+ if (isMotionModuleSource(moduleSource) && ts.isImportSpecifier(declaration)) {
37264
+ if (declaration.isTypeOnly) return null;
37265
+ return classifyMotionExport(declaration.propertyName?.text ?? declaration.name.text);
37266
+ }
37267
+ if (isMotionModuleSource(moduleSource) && ts.isImportClause(declaration)) {
37268
+ if (declaration.isTypeOnly) return null;
37269
+ return EMPTY_MOTION_EXPRESSION_EVIDENCE;
37270
+ }
37271
+ if (ts.isImportSpecifier(declaration) && !declaration.isTypeOnly || ts.isImportClause(declaration) && !declaration.isTypeOnly && declaration.name) {
37272
+ const importedName = ts.isImportSpecifier(declaration) ? declaration.propertyName?.text ?? declaration.name.text : "default";
37273
+ const moduleSourceFile = getLocalModuleSourceFile(moduleSource, declaration.getSourceFile().fileName, program);
37274
+ if (!moduleSourceFile) return null;
37275
+ return resolveModuleExportEvidence(moduleSourceFile, importedName, typeChecker, program, visitedSymbols);
37276
+ }
37277
+ return null;
37278
+ };
37279
+ const getLocalModuleSourceFile = (moduleSource, importingFileName, program) => {
37280
+ if (!moduleSource.startsWith(".")) return null;
37281
+ const moduleBasePath = Path.resolve(Path.dirname(importingFileName), moduleSource);
37282
+ const moduleBaseExtension = Path.extname(moduleBasePath);
37283
+ const typescriptModuleBasePath = moduleBaseExtension === ".js" || moduleBaseExtension === ".jsx" ? moduleBasePath.slice(0, -moduleBaseExtension.length) : null;
37284
+ const candidates = [
37285
+ moduleBasePath,
37286
+ ...typescriptModuleBasePath ? [
37287
+ `${typescriptModuleBasePath}.ts`,
37288
+ `${typescriptModuleBasePath}.tsx`,
37289
+ Path.join(typescriptModuleBasePath, "index.ts"),
37290
+ Path.join(typescriptModuleBasePath, "index.tsx")
37291
+ ] : [],
37292
+ ...SCRIPT_MODULE_EXTENSIONS.map((extension) => `${moduleBasePath}${extension}`),
37293
+ ...SCRIPT_MODULE_EXTENSIONS.map((extension) => Path.join(moduleBasePath, `index${extension}`))
37294
+ ];
37295
+ for (const candidate of candidates) {
37296
+ const sourceFile = program.getSourceFile(candidate);
37297
+ if (sourceFile) return sourceFile;
37298
+ }
37299
+ return null;
37300
+ };
37301
+ const resolveModuleExportEvidence = (sourceFile, exportName, typeChecker, program, visitedSymbols) => {
37302
+ const moduleSymbol = typeChecker.getSymbolAtLocation(sourceFile);
37303
+ if (moduleSymbol && visitedSymbols.has(moduleSymbol)) return EMPTY_MOTION_EXPRESSION_EVIDENCE;
37304
+ const nextVisitedSymbols = new Set(visitedSymbols);
37305
+ if (moduleSymbol) nextVisitedSymbols.add(moduleSymbol);
37306
+ for (const statement of sourceFile.statements) {
37307
+ if (exportName === "default" && ts.isExportAssignment(statement) && !statement.isExportEquals) {
37308
+ const evidence = resolveMotionExpressionEvidence(statement.expression, typeChecker, program, nextVisitedSymbols);
37309
+ if (hasMotionExpressionEvidence(evidence)) return evidence;
37310
+ continue;
37311
+ }
37312
+ if (!ts.isExportDeclaration(statement) || !statement.moduleSpecifier) continue;
37313
+ if (statement.isTypeOnly) continue;
37314
+ if (!ts.isStringLiteral(statement.moduleSpecifier)) continue;
37315
+ const moduleSource = statement.moduleSpecifier.text;
37316
+ if (!statement.exportClause) {
37317
+ if (isMotionModuleSource(moduleSource)) {
37318
+ const evidence = classifyMotionExport(exportName);
37319
+ if (hasMotionExpressionEvidence(evidence)) return evidence;
37320
+ continue;
37321
+ }
37322
+ const moduleSourceFile = getLocalModuleSourceFile(moduleSource, sourceFile.fileName, program);
37323
+ if (moduleSourceFile) {
37324
+ const evidence = resolveModuleExportEvidence(moduleSourceFile, exportName, typeChecker, program, nextVisitedSymbols);
37325
+ if (hasMotionExpressionEvidence(evidence)) return evidence;
37326
+ }
37327
+ continue;
37328
+ }
37329
+ if (!ts.isNamedExports(statement.exportClause)) continue;
37330
+ for (const exportSpecifier of statement.exportClause.elements) {
37331
+ if (exportSpecifier.name.text !== exportName || exportSpecifier.isTypeOnly) continue;
37332
+ const sourceExportName = exportSpecifier.propertyName?.text ?? exportSpecifier.name.text;
37333
+ if (isMotionModuleSource(moduleSource)) return classifyMotionExport(sourceExportName);
37334
+ const moduleSourceFile = getLocalModuleSourceFile(moduleSource, sourceFile.fileName, program);
37335
+ if (!moduleSourceFile) continue;
37336
+ return resolveModuleExportEvidence(moduleSourceFile, sourceExportName, typeChecker, program, nextVisitedSymbols);
37337
+ }
37338
+ }
37339
+ if (!moduleSymbol) return EMPTY_MOTION_EXPRESSION_EVIDENCE;
37340
+ const exportedSymbol = typeChecker.getExportsOfModule(moduleSymbol).find((candidateSymbol) => candidateSymbol.name === exportName);
37341
+ if (!exportedSymbol || nextVisitedSymbols.has(exportedSymbol)) return EMPTY_MOTION_EXPRESSION_EVIDENCE;
37342
+ nextVisitedSymbols.add(exportedSymbol);
37343
+ let resolvedEvidence = EMPTY_MOTION_EXPRESSION_EVIDENCE;
37344
+ for (const declaration of exportedSymbol.declarations ?? []) {
37345
+ if (ts.isExportSpecifier(declaration)) {
37346
+ const targetSymbol = typeChecker.getExportSpecifierLocalTargetSymbol(declaration);
37347
+ for (const targetDeclaration of targetSymbol?.declarations ?? []) {
37348
+ const importedEvidence = getImportedBindingEvidence(targetDeclaration, typeChecker, program, nextVisitedSymbols);
37349
+ if (importedEvidence) resolvedEvidence = mergeMotionExpressionEvidence(resolvedEvidence, importedEvidence);
37350
+ if (ts.isVariableDeclaration(targetDeclaration) && targetDeclaration.initializer) resolvedEvidence = mergeMotionExpressionEvidence(resolvedEvidence, resolveMotionExpressionEvidence(targetDeclaration.initializer, typeChecker, program, nextVisitedSymbols));
37351
+ }
37352
+ }
37353
+ if (ts.isVariableDeclaration(declaration) && declaration.initializer) resolvedEvidence = mergeMotionExpressionEvidence(resolvedEvidence, resolveMotionExpressionEvidence(declaration.initializer, typeChecker, program, nextVisitedSymbols));
37354
+ }
37355
+ return resolvedEvidence;
37356
+ };
37357
+ const mergeMotionExpressionEvidence = (leftEvidence, rightEvidence) => ({
37358
+ isAnimationFunction: leftEvidence.isAnimationFunction || rightEvidence.isAnimationFunction,
37359
+ isMotionComponent: leftEvidence.isMotionComponent || rightEvidence.isMotionComponent,
37360
+ isMotionComponentFactory: leftEvidence.isMotionComponentFactory || rightEvidence.isMotionComponentFactory,
37361
+ isMotionComponentNamespace: leftEvidence.isMotionComponentNamespace || rightEvidence.isMotionComponentNamespace,
37362
+ isMotionConfig: leftEvidence.isMotionConfig || rightEvidence.isMotionConfig,
37363
+ isMotionNamespace: leftEvidence.isMotionNamespace || rightEvidence.isMotionNamespace,
37364
+ isReducedMotionHook: leftEvidence.isReducedMotionHook || rightEvidence.isReducedMotionHook
37365
+ });
37366
+ const hasMotionExpressionEvidence = (evidence) => evidence.isAnimationFunction || evidence.isMotionComponent || evidence.isMotionComponentFactory || evidence.isMotionComponentNamespace || evidence.isMotionConfig || evidence.isMotionNamespace || evidence.isReducedMotionHook;
37367
+ const classifyMotionMember = (receiverEvidence, memberName) => {
37368
+ if (receiverEvidence.isMotionNamespace) return classifyMotionExport(memberName);
37369
+ if (receiverEvidence.isMotionComponentFactory) return {
37370
+ ...EMPTY_MOTION_EXPRESSION_EVIDENCE,
37371
+ isMotionComponent: true
37372
+ };
37373
+ if (receiverEvidence.isMotionComponentNamespace && MOTION_COMPONENT_NAMESPACE_MEMBER_NAMES.has(memberName)) return {
37374
+ ...EMPTY_MOTION_EXPRESSION_EVIDENCE,
37375
+ isMotionComponent: true
37376
+ };
37377
+ return EMPTY_MOTION_EXPRESSION_EVIDENCE;
37378
+ };
37379
+ const resolveMotionExpressionEvidence = (expression, typeChecker, program, visitedSymbols = /* @__PURE__ */ new Set()) => {
37380
+ const unwrappedExpression = unwrapTypescriptExpression(expression);
37381
+ if (ts.isIdentifier(unwrappedExpression)) {
37382
+ const symbol = typeChecker.getSymbolAtLocation(unwrappedExpression);
37383
+ if (!symbol || visitedSymbols.has(symbol)) return EMPTY_MOTION_EXPRESSION_EVIDENCE;
37384
+ const nextVisitedSymbols = new Set(visitedSymbols);
37385
+ nextVisitedSymbols.add(symbol);
37386
+ let resolvedEvidence = EMPTY_MOTION_EXPRESSION_EVIDENCE;
37387
+ for (const declaration of symbol.declarations ?? []) {
37388
+ const importedEvidence = getImportedBindingEvidence(declaration, typeChecker, program, nextVisitedSymbols);
37389
+ if (importedEvidence) {
37390
+ resolvedEvidence = mergeMotionExpressionEvidence(resolvedEvidence, importedEvidence);
37391
+ continue;
37392
+ }
37393
+ if (ts.isVariableDeclaration(declaration) && declaration.initializer) resolvedEvidence = mergeMotionExpressionEvidence(resolvedEvidence, resolveMotionExpressionEvidence(declaration.initializer, typeChecker, program, nextVisitedSymbols));
37394
+ if (ts.isBindingElement(declaration) && ts.isObjectBindingPattern(declaration.parent) && ts.isVariableDeclaration(declaration.parent.parent) && declaration.parent.parent.initializer) {
37395
+ const propertyName = declaration.propertyName ?? declaration.name;
37396
+ if (ts.isIdentifier(propertyName) || ts.isStringLiteral(propertyName)) {
37397
+ const receiverEvidence = resolveMotionExpressionEvidence(declaration.parent.parent.initializer, typeChecker, program, nextVisitedSymbols);
37398
+ resolvedEvidence = mergeMotionExpressionEvidence(resolvedEvidence, classifyMotionMember(receiverEvidence, propertyName.text));
37399
+ }
37400
+ }
37401
+ }
37402
+ return resolvedEvidence;
37403
+ }
37404
+ if (ts.isPropertyAccessExpression(unwrappedExpression)) return classifyMotionMember(resolveMotionExpressionEvidence(unwrappedExpression.expression, typeChecker, program, visitedSymbols), unwrappedExpression.name.text);
37405
+ 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);
37406
+ if (ts.isCallExpression(unwrappedExpression)) {
37407
+ 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 {
37408
+ ...EMPTY_MOTION_EXPRESSION_EVIDENCE,
37409
+ isMotionNamespace: true
37410
+ };
37411
+ if (ts.isPropertyAccessExpression(unwrappedExpression.expression) && unwrappedExpression.expression.name.text === "bind") {
37412
+ const boundReceiverEvidence = resolveMotionExpressionEvidence(unwrappedExpression.expression.expression, typeChecker, program, visitedSymbols);
37413
+ if (boundReceiverEvidence.isAnimationFunction) return boundReceiverEvidence;
37414
+ }
37415
+ const calleeEvidence = resolveMotionExpressionEvidence(unwrappedExpression.expression, typeChecker, program, visitedSymbols);
37416
+ if (calleeEvidence.isMotionComponentFactory) return {
37417
+ ...EMPTY_MOTION_EXPRESSION_EVIDENCE,
37418
+ isMotionComponent: true
37419
+ };
37420
+ return calleeEvidence;
37421
+ }
37422
+ return EMPTY_MOTION_EXPRESSION_EVIDENCE;
37423
+ };
37424
+ const getStaticStringExpressionValue = (expression, typeChecker, visitedSymbols = /* @__PURE__ */ new Set()) => {
37425
+ const unwrappedExpression = unwrapTypescriptExpression(expression);
37426
+ if (ts.isStringLiteral(unwrappedExpression) || ts.isNoSubstitutionTemplateLiteral(unwrappedExpression)) return unwrappedExpression.text;
37427
+ if (!ts.isIdentifier(unwrappedExpression)) return null;
37428
+ const symbol = typeChecker.getSymbolAtLocation(unwrappedExpression);
37429
+ if (!symbol || visitedSymbols.has(symbol)) return null;
37430
+ const nextVisitedSymbols = new Set(visitedSymbols);
37431
+ nextVisitedSymbols.add(symbol);
37432
+ for (const declaration of symbol.declarations ?? []) {
37433
+ if (!ts.isVariableDeclaration(declaration) || !declaration.initializer) continue;
37434
+ if (!ts.isVariableDeclarationList(declaration.parent)) continue;
37435
+ if (!(declaration.parent.flags & ts.NodeFlags.Const)) continue;
37436
+ const value = getStaticStringExpressionValue(declaration.initializer, typeChecker, nextVisitedSymbols);
37437
+ if (value !== null) return value;
37438
+ }
37439
+ return null;
37440
+ };
37441
+ const getStaticJsxAttributeValue = (attribute, typeChecker) => {
37442
+ if (!attribute.initializer) return null;
37443
+ if (ts.isStringLiteral(attribute.initializer)) return attribute.initializer.text;
37444
+ if (!ts.isJsxExpression(attribute.initializer) || !attribute.initializer.expression) return null;
37445
+ return getStaticStringExpressionValue(attribute.initializer.expression, typeChecker);
37446
+ };
37447
+ const isExpressionValueConsumed = (expression) => {
37448
+ let currentExpression = expression;
37449
+ while (ts.isParenthesizedExpression(currentExpression.parent) || ts.isAsExpression(currentExpression.parent) || ts.isSatisfiesExpression(currentExpression.parent) || ts.isNonNullExpression(currentExpression.parent) || ts.isTypeAssertionExpression(currentExpression.parent)) currentExpression = currentExpression.parent;
37450
+ return !ts.isExpressionStatement(currentExpression.parent) && !ts.isVoidExpression(currentExpression.parent);
37451
+ };
37452
+ 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) ?? ""));
37453
+ const collectScriptMotionEvidence = (sourceFile, typeChecker, program) => {
37454
+ const evidence = {
37455
+ hasMotionUse: false,
37456
+ hasReducedMotionHandling: false
37457
+ };
37458
+ const visitNode = (node) => {
37459
+ if (ts.isCallExpression(node)) {
37460
+ const calleeEvidence = resolveMotionExpressionEvidence(node.expression, typeChecker, program);
37461
+ 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;
37462
+ if (calleeEvidence.isAnimationFunction || isAnimationFunctionCallViaCallOrApply) evidence.hasMotionUse = true;
37463
+ if (calleeEvidence.isReducedMotionHook && isExpressionValueConsumed(node)) evidence.hasReducedMotionHandling = true;
37464
+ }
37465
+ if (ts.isJsxOpeningElement(node) || ts.isJsxSelfClosingElement(node)) {
37466
+ const tagEvidence = ts.isJsxNamespacedName(node.tagName) ? EMPTY_MOTION_EXPRESSION_EVIDENCE : resolveMotionExpressionEvidence(node.tagName, typeChecker, program);
37467
+ if (tagEvidence.isMotionComponent || tagEvidence.isMotionComponentFactory) evidence.hasMotionUse = true;
37468
+ if (tagEvidence.isMotionConfig && jsxElementHasReducedMotionConfiguration(node.attributes, typeChecker)) evidence.hasReducedMotionHandling = true;
37469
+ }
37470
+ ts.forEachChild(node, visitNode);
37471
+ };
37472
+ visitNode(sourceFile);
37473
+ return evidence;
37474
+ };
37475
+ const analyzeReducedMotionSources = (sources) => {
37476
+ if (!sources.some(({ sourceText }) => MOTION_SOURCE_PREFILTER.test(sourceText))) return {
37477
+ hasMotionUse: false,
37478
+ hasReducedMotionHandling: false
37479
+ };
37480
+ const compilerOptions = {
37481
+ allowJs: true,
37482
+ checkJs: false,
37483
+ jsx: ts.JsxEmit.Preserve,
37484
+ module: ts.ModuleKind.ESNext,
37485
+ moduleResolution: ts.ModuleResolutionKind.Bundler,
37486
+ noEmit: true,
37487
+ noLib: true,
37488
+ noResolve: true,
37489
+ skipLibCheck: true,
37490
+ target: ts.ScriptTarget.Latest
37491
+ };
37492
+ const sourceTextByFileName = new Map(sources.map(({ fileName: sourceFileName, sourceText: content }) => [Path.resolve(sourceFileName), content]));
37493
+ const compilerHost = ts.createCompilerHost(compilerOptions);
37494
+ const getDefaultSourceFile = compilerHost.getSourceFile.bind(compilerHost);
37495
+ 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);
37496
+ const program = ts.createProgram([...sourceTextByFileName.keys()], compilerOptions, compilerHost);
37497
+ const typeChecker = program.getTypeChecker();
37498
+ const evidence = {
37499
+ hasMotionUse: false,
37500
+ hasReducedMotionHandling: false
37501
+ };
37502
+ for (const sourceFileName of sourceTextByFileName.keys()) {
37503
+ const sourceFile = program.getSourceFile(sourceFileName);
37504
+ if (!sourceFile) continue;
37505
+ const fileEvidence = collectScriptMotionEvidence(sourceFile, typeChecker, program);
37506
+ evidence.hasMotionUse ||= fileEvidence.hasMotionUse;
37507
+ evidence.hasReducedMotionHandling ||= fileEvidence.hasReducedMotionHandling;
37508
+ }
37509
+ return evidence;
37510
+ };
37511
+ const removeCssCommentsAndStrings = (content) => {
37512
+ let sanitizedContent = "";
37513
+ let quote = null;
37514
+ let isInsideComment = false;
37515
+ let isInsideLineComment = false;
37516
+ for (let characterIndex = 0; characterIndex < content.length; characterIndex += 1) {
37517
+ const character = content[characterIndex];
37518
+ const nextCharacter = content[characterIndex + 1];
37519
+ if (isInsideLineComment) {
37520
+ sanitizedContent += character === "\n" ? "\n" : " ";
37521
+ if (character === "\n") isInsideLineComment = false;
37522
+ continue;
37523
+ }
37524
+ if (isInsideComment) {
37525
+ if (character === "*" && nextCharacter === "/") {
37526
+ sanitizedContent += " ";
37527
+ characterIndex += 1;
37528
+ isInsideComment = false;
37529
+ } else sanitizedContent += character === "\n" ? "\n" : " ";
37530
+ continue;
37531
+ }
37532
+ if (quote) {
37533
+ if (character === "\\") {
37534
+ sanitizedContent += " ";
37535
+ characterIndex += 1;
37536
+ continue;
37537
+ }
37538
+ if (character === quote) quote = null;
37539
+ sanitizedContent += character === "\n" ? "\n" : " ";
37540
+ continue;
37541
+ }
37542
+ if (character === "/" && nextCharacter === "*") {
37543
+ sanitizedContent += " ";
37544
+ characterIndex += 1;
37545
+ isInsideComment = true;
37546
+ continue;
37547
+ }
37548
+ if (character === "/" && nextCharacter === "/") {
37549
+ sanitizedContent += " ";
37550
+ characterIndex += 1;
37551
+ isInsideLineComment = true;
37552
+ continue;
37553
+ }
37554
+ if (character === "\"" || character === "'") {
37555
+ sanitizedContent += " ";
37556
+ quote = character;
37557
+ continue;
37558
+ }
37559
+ sanitizedContent += character;
37560
+ }
37561
+ return sanitizedContent;
37562
+ };
37563
+ const findMatchingClosingBrace = (content, openingBraceIndex) => {
37564
+ let depth = 0;
37565
+ for (let characterIndex = openingBraceIndex; characterIndex < content.length; characterIndex += 1) {
37566
+ if (content[characterIndex] === "{") depth += 1;
37567
+ if (content[characterIndex] !== "}") continue;
37568
+ depth -= 1;
37569
+ if (depth === 0) return characterIndex;
37570
+ }
37571
+ return -1;
37572
+ };
37573
+ const hasReducedMotionMediaQuery = (content) => {
37574
+ const sanitizedContent = removeCssCommentsAndStrings(content);
37575
+ REDUCED_MOTION_MEDIA_QUERY_PATTERN.lastIndex = 0;
37576
+ for (const match of sanitizedContent.matchAll(REDUCED_MOTION_MEDIA_QUERY_PATTERN)) {
37577
+ const mediaPrelude = match[1];
37578
+ if (/\bnot\b/i.test(mediaPrelude)) continue;
37579
+ const openingBraceIndex = (match.index ?? 0) + match[0].lastIndexOf("{");
37580
+ const closingBraceIndex = findMatchingClosingBrace(sanitizedContent, openingBraceIndex);
37581
+ if (closingBraceIndex === -1) continue;
37582
+ const body = sanitizedContent.slice(openingBraceIndex + 1, closingBraceIndex);
37583
+ if (CSS_DECLARATION_PATTERN.test(body)) return true;
37584
+ }
37585
+ return false;
37586
+ };
37587
+ const collectProjectMotionEvidence = (rootDirectory) => {
37588
+ const scriptSources = [];
37589
+ let hasReducedMotionHandling = false;
37167
37590
  for (const { absolutePath, name } of walkSourceTreeFiles(rootDirectory)) {
37168
- if (!REDUCED_MOTION_FILE_EXTENSIONS.has(Path.extname(name))) continue;
37591
+ const extension = Path.extname(name);
37592
+ if (!SCRIPT_FILE_EXTENSIONS.has(extension) && !STYLE_FILE_EXTENSIONS.has(extension)) continue;
37169
37593
  let content;
37170
37594
  try {
37171
37595
  content = NFS.readFileSync(absolutePath, "utf-8");
37172
37596
  } catch {
37173
37597
  continue;
37174
37598
  }
37175
- if (REDUCED_MOTION_PATTERN.test(content)) return true;
37599
+ if (STYLE_FILE_EXTENSIONS.has(extension)) {
37600
+ if (hasReducedMotionMediaQuery(content)) hasReducedMotionHandling = true;
37601
+ continue;
37602
+ }
37603
+ scriptSources.push({
37604
+ fileName: absolutePath,
37605
+ sourceText: content
37606
+ });
37176
37607
  }
37177
- return false;
37608
+ if (scriptSources.length === 0) return {
37609
+ hasMotionUse: false,
37610
+ hasReducedMotionHandling
37611
+ };
37612
+ const scriptEvidence = analyzeReducedMotionSources(scriptSources);
37613
+ const hasMotionUse = scriptEvidence.hasMotionUse;
37614
+ hasReducedMotionHandling ||= scriptEvidence.hasReducedMotionHandling;
37615
+ return {
37616
+ hasMotionUse,
37617
+ hasReducedMotionHandling
37618
+ };
37178
37619
  };
37179
37620
  const checkReducedMotion = (rootDirectory) => {
37180
37621
  const packageJsonPath = Path.join(rootDirectory, "package.json");
37181
37622
  if (!isFile(packageJsonPath)) return [];
37182
- let hasMotionLibrary = false;
37183
37623
  try {
37184
37624
  const packageJson = readPackageJson(packageJsonPath);
37185
37625
  const allDependencies = {
37186
37626
  ...packageJson.dependencies,
37187
37627
  ...packageJson.devDependencies
37188
37628
  };
37189
- hasMotionLibrary = Object.keys(allDependencies).some((packageName) => MOTION_LIBRARY_PACKAGES.has(packageName));
37629
+ if (!Object.keys(allDependencies).some((packageName) => MOTION_LIBRARY_PACKAGES.has(packageName))) return [];
37190
37630
  } catch {
37191
37631
  return [];
37192
37632
  }
37193
- if (!hasMotionLibrary) return [];
37194
- const result = spawnSync("git", [
37195
- "grep",
37196
- "--untracked",
37197
- "-lE",
37198
- REDUCED_MOTION_GREP_PATTERN,
37199
- "--",
37200
- ...REDUCED_MOTION_FILE_GLOBS
37201
- ], {
37202
- cwd: rootDirectory,
37203
- encoding: "utf-8",
37204
- stdio: [
37205
- "ignore",
37206
- "pipe",
37207
- "pipe"
37208
- ]
37209
- });
37210
- if (!(!result.error && (result.status === 0 || result.status === GIT_GREP_NO_MATCH_STATUS))) return hasReducedMotionHandlingViaFilesystem(rootDirectory) ? [] : [MISSING_REDUCED_MOTION_DIAGNOSTIC];
37211
- return result.status === 0 && result.stdout.split("\n").some((filePath) => filePath.length > 0 && !hasIgnoredPathSegment(filePath)) ? [] : [MISSING_REDUCED_MOTION_DIAGNOSTIC];
37633
+ const evidence = collectProjectMotionEvidence(rootDirectory);
37634
+ return evidence.hasMotionUse && !evidence.hasReducedMotionHandling ? [MISSING_REDUCED_MOTION_DIAGNOSTIC] : [];
37212
37635
  };
37213
37636
  const buildSecurityScanDiagnostic = (finding, entry, relativePath) => ({
37214
37637
  filePath: relativePath,
@@ -37516,9 +37939,7 @@ const highlighter = {
37516
37939
  gray: import_picocolors.default.gray,
37517
37940
  bold: import_picocolors.default.bold
37518
37941
  };
37519
- const NEXT_ENTRY_FILE_PATTERN = /^(?:\.\/)?(?:src\/)?(?:middleware|proxy)\.(?:tsx?|jsx?|mts|mjs)$/;
37520
- const shouldIncludeExplicitLintPath = (filePath, project) => JSX_FILE_PATTERN.test(filePath) || project?.framework === "nextjs" && NEXT_ENTRY_FILE_PATTERN.test(filePath);
37521
- const computeExplicitLintIncludePaths = (includePaths, project) => includePaths.length > 0 ? includePaths.filter((filePath) => shouldIncludeExplicitLintPath(filePath, project)) : void 0;
37942
+ const computeExplicitLintIncludePaths = (includePaths) => includePaths.length > 0 ? includePaths.filter(isLintableSourceFile) : void 0;
37522
37943
  const LINGUIST_ATTRIBUTE_PATTERN = /^linguist-(?:vendored|generated)(?:=([a-zA-Z0-9]+))?$/i;
37523
37944
  const FALSY_VALUES = new Set([
37524
37945
  "false",
@@ -38360,11 +38781,11 @@ const resolveDeadCodeTimeout = (input) => {
38360
38781
  phaseTimeoutMs: workerTimeoutMs + DEAD_CODE_PHASE_TIMEOUT_OVER_WORKER_MS
38361
38782
  };
38362
38783
  };
38363
- const resolveLintIncludePaths = (rootDirectory, userConfig, project) => {
38784
+ const resolveLintIncludePaths = (rootDirectory, userConfig) => {
38364
38785
  if (!Array.isArray(userConfig?.ignore?.files) || userConfig.ignore.files.length === 0) return;
38365
38786
  const ignoredPatterns = compileIgnoredFilePatterns(userConfig);
38366
38787
  return listSourceFiles(rootDirectory).filter((filePath) => {
38367
- if (!shouldIncludeExplicitLintPath(filePath, project)) return false;
38788
+ if (!isLintableSourceFile(filePath)) return false;
38368
38789
  return !isFileIgnoredByPatterns(filePath, rootDirectory, ignoredPatterns);
38369
38790
  });
38370
38791
  };
@@ -38582,6 +39003,7 @@ const parseGithubViewerPermission = (stdout) => {
38582
39003
  return /^[A-Z_]+$/.test(value) ? value.toLowerCase() : null;
38583
39004
  };
38584
39005
  const splitNullSeparated = (value) => value.split("\0").filter((entry) => entry.length > 0);
39006
+ const UNTRACKED_FILE_LAST_LINE = Number.MAX_SAFE_INTEGER;
38585
39007
  /**
38586
39008
  * `Git` wraps every `git`-via-subprocess call react-doctor makes
38587
39009
  * behind a `Context.Service`. The production layer (`layerNode`)
@@ -38652,6 +39074,14 @@ var Git = class Git extends Service()("react-doctor/Git") {
38652
39074
  args,
38653
39075
  directory
38654
39076
  });
39077
+ const listUntrackedFilePaths = (directory, includePaths = []) => runGit(directory, [
39078
+ "ls-files",
39079
+ "-z",
39080
+ "--others",
39081
+ "--exclude-standard",
39082
+ ...includePaths.length > 0 ? ["--", ...includePaths] : []
39083
+ ]).pipe(map$3((result) => result.status === 0 ? splitNullSeparated(result.stdout) : null));
39084
+ const mergeUntracked = (directory, trackedFilePaths, includeUntracked) => includeUntracked ? listUntrackedFilePaths(directory).pipe(map$3((untracked) => untracked === null ? trackedFilePaths : [...new Set([...trackedFilePaths, ...untracked])])) : succeed$2(trackedFilePaths);
38655
39085
  const currentBranch = (directory) => runGit(directory, [
38656
39086
  "rev-parse",
38657
39087
  "--abbrev-ref",
@@ -38774,7 +39204,7 @@ var Git = class Git extends Service()("react-doctor/Git") {
38774
39204
  githubViewerPermission,
38775
39205
  branchExists,
38776
39206
  mergeBase,
38777
- diffSelection: ({ directory, explicitBaseBranch }) => gen(function* () {
39207
+ diffSelection: ({ directory, explicitBaseBranch, includeUntracked = false }) => gen(function* () {
38778
39208
  if (explicitBaseBranch !== void 0 && explicitBaseBranch.trim().length === 0) return yield* fail$4(new ReactDoctorError({ reason: new GitBaseBranchInvalid({ detail: "Diff base branch cannot be empty." }) }));
38779
39209
  if (explicitBaseBranch !== void 0) {
38780
39210
  const range = parseGitDiffRange(explicitBaseBranch);
@@ -38803,7 +39233,7 @@ var Git = class Git extends Service()("react-doctor/Git") {
38803
39233
  "HEAD"
38804
39234
  ]);
38805
39235
  if (uncommitted.status !== 0) return null;
38806
- const files = splitNullSeparated(uncommitted.stdout);
39236
+ const files = yield* mergeUntracked(directory, splitNullSeparated(uncommitted.stdout), includeUntracked);
38807
39237
  if (files.length === 0) return null;
38808
39238
  return {
38809
39239
  currentBranch: resolvedCurrentBranch,
@@ -38833,7 +39263,7 @@ var Git = class Git extends Service()("react-doctor/Git") {
38833
39263
  currentBranch: resolvedCurrentBranch,
38834
39264
  baseBranch,
38835
39265
  diffBaseRef: mergeBaseRef,
38836
- changedFiles: splitNullSeparated(diff.stdout),
39266
+ changedFiles: yield* mergeUntracked(directory, splitNullSeparated(diff.stdout), includeUntracked),
38837
39267
  isCurrentChanges: false
38838
39268
  };
38839
39269
  }).pipe(withSpan("Git.diffSelection")),
@@ -38879,7 +39309,7 @@ var Git = class Git extends Service()("react-doctor/Git") {
38879
39309
  stdout: result.stdout
38880
39310
  };
38881
39311
  }).pipe(withSpan("Git.grep")),
38882
- changedLineRanges: ({ directory, baseRef, cached, files }) => gen(function* () {
39312
+ changedLineRanges: ({ directory, baseRef, cached, files, includeUntracked = false }) => gen(function* () {
38883
39313
  if (files.length === 0) return [];
38884
39314
  if (baseRef !== void 0 && !isSafeGitRevision(baseRef)) return null;
38885
39315
  const result = yield* runGit(directory, [
@@ -38893,7 +39323,14 @@ var Git = class Git extends Service()("react-doctor/Git") {
38893
39323
  ...files
38894
39324
  ]);
38895
39325
  if (result.status !== 0) return null;
38896
- return parseChangedLineRanges(result.stdout);
39326
+ const changedLineRanges = parseChangedLineRanges(result.stdout);
39327
+ if (cached || !includeUntracked) return changedLineRanges;
39328
+ const untrackedFilePaths = yield* listUntrackedFilePaths(directory, files);
39329
+ if (untrackedFilePaths === null) return changedLineRanges;
39330
+ return [...changedLineRanges, ...untrackedFilePaths.map((file) => ({
39331
+ file,
39332
+ ranges: [[1, UNTRACKED_FILE_LAST_LINE]]
39333
+ }))];
38897
39334
  }).pipe(catch_$1((error) => error.reason._tag === "GitInvocationFailed" ? succeed$2(null) : fail$4(error)), withSpan("Git.changedLineRanges"))
38898
39335
  });
38899
39336
  })).pipe(provide$2(layer$2.pipe(provide$2(mergeAll$1(layer$1, layer)))));
@@ -39478,13 +39915,13 @@ const encodeSidecarFileEntry = (entry) => ({
39478
39915
  diagnostics: entry.diagnostics
39479
39916
  });
39480
39917
  const emptyCache = () => ({
39481
- version: 1,
39918
+ version: 2,
39482
39919
  rulesets: {}
39483
39920
  });
39484
39921
  const loadRulesetEntries = (cacheFilePath, rulesetHash) => {
39485
39922
  const entries = /* @__PURE__ */ new Map();
39486
39923
  const persisted = failOpenReadJson(cacheFilePath, emptyCache());
39487
- if (persisted.version !== 1 || !isRecord(persisted.rulesets)) return entries;
39924
+ if (persisted.version !== 2 || !isRecord(persisted.rulesets)) return entries;
39488
39925
  const bucket = persisted.rulesets[rulesetHash];
39489
39926
  if (!isRecord(bucket) || !isRecord(bucket.files)) return entries;
39490
39927
  for (const [fileKey, rawEntry] of Object.entries(bucket.files)) {
@@ -39504,7 +39941,7 @@ const createSidecarLintCache = (cacheDirectory, rulesetHash) => {
39504
39941
  },
39505
39942
  persist: () => {
39506
39943
  const onDisk = failOpenReadJson(cacheFilePath, emptyCache());
39507
- const rulesets = onDisk.version === 1 && isRecord(onDisk.rulesets) ? { ...onDisk.rulesets } : {};
39944
+ const rulesets = onDisk.version === 2 && isRecord(onDisk.rulesets) ? { ...onDisk.rulesets } : {};
39508
39945
  const existingBucket = rulesets[rulesetHash];
39509
39946
  const existingFiles = isRecord(existingBucket) && isRecord(existingBucket.files) ? existingBucket.files : {};
39510
39947
  const ourFiles = {};
@@ -39518,7 +39955,7 @@ const createSidecarLintCache = (cacheDirectory, rulesetHash) => {
39518
39955
  files: Object.fromEntries(cappedEntries)
39519
39956
  };
39520
39957
  atomicWriteJson(cacheFilePath, {
39521
- version: 1,
39958
+ version: 2,
39522
39959
  rulesets: Object.fromEntries(Object.entries(rulesets).filter(([, bucket]) => isRecord(bucket) && typeof bucket.updatedAtMs === "number").sort(([, first], [, second]) => second.updatedAtMs - first.updatedAtMs).slice(0, 8))
39523
39960
  });
39524
39961
  }
@@ -39569,23 +40006,6 @@ const resolveTsConfigRelativePath = (rootDirectory) => {
39569
40006
  for (const filename of TSCONFIG_FILENAMES) if (NFS.existsSync(Path.join(rootDirectory, filename))) return `./${filename}`;
39570
40007
  return null;
39571
40008
  };
39572
- const DERIVED_STATE_RULE_PRIORITY = new Map([
39573
- ["no-initialize-state", 0],
39574
- ["no-adjust-state-on-prop-change", 1],
39575
- ["no-derived-state", 2],
39576
- ["no-derived-state-effect", 3]
39577
- ]);
39578
- const doDiagnosticSpansOverlap = (first, second) => {
39579
- if (first.filePath !== second.filePath || first.plugin !== second.plugin) return false;
39580
- if (first.offset !== void 0 && second.offset !== void 0) {
39581
- const firstEnd = first.offset + Math.max(first.length ?? 1, 1);
39582
- const secondEnd = second.offset + Math.max(second.length ?? 1, 1);
39583
- return first.offset < secondEnd && second.offset < firstEnd;
39584
- }
39585
- const firstEndLine = first.endLine ?? first.line;
39586
- const secondEndLine = second.endLine ?? second.line;
39587
- return first.line <= secondEndLine && second.line <= firstEndLine;
39588
- };
39589
40009
  const dedupeDiagnostics = (diagnostics) => {
39590
40010
  const seenKeys = /* @__PURE__ */ new Set();
39591
40011
  const uniqueDiagnostics = [];
@@ -39593,16 +40013,6 @@ const dedupeDiagnostics = (diagnostics) => {
39593
40013
  const key = `${diagnostic.filePath}\u0000${diagnostic.line}\u0000${diagnostic.column}\u0000${diagnostic.plugin}\u0000${diagnostic.rule}\u0000${diagnostic.severity}\u0000${diagnostic.message}`;
39594
40014
  if (seenKeys.has(key)) continue;
39595
40015
  seenKeys.add(key);
39596
- const priority = DERIVED_STATE_RULE_PRIORITY.get(diagnostic.rule);
39597
- if (priority !== void 0) {
39598
- const overlappingDiagnosticIndex = uniqueDiagnostics.findIndex((candidate) => candidate.rule !== diagnostic.rule && candidate.severity === diagnostic.severity && DERIVED_STATE_RULE_PRIORITY.has(candidate.rule) && doDiagnosticSpansOverlap(candidate, diagnostic));
39599
- if (overlappingDiagnosticIndex >= 0) {
39600
- const existingDiagnostic = uniqueDiagnostics[overlappingDiagnosticIndex];
39601
- const existingPriority = existingDiagnostic ? DERIVED_STATE_RULE_PRIORITY.get(existingDiagnostic.rule) : void 0;
39602
- if (existingPriority !== void 0 && priority < existingPriority) uniqueDiagnostics[overlappingDiagnosticIndex] = diagnostic;
39603
- continue;
39604
- }
39605
- }
39606
40016
  uniqueDiagnostics.push(diagnostic);
39607
40017
  }
39608
40018
  return uniqueDiagnostics;
@@ -39645,6 +40055,12 @@ const mapWithConcurrency = async (items, concurrency, task) => {
39645
40055
  if (errors.length > 0) throw errors[0];
39646
40056
  return results;
39647
40057
  };
40058
+ const lineOfUtf8Offset = (sourceBuffer, utf8Offset) => {
40059
+ let lineNumber = 1;
40060
+ const scanEnd = Math.min(utf8Offset, sourceBuffer.length);
40061
+ for (let byteIndex = 0; byteIndex < scanEnd; byteIndex++) if (sourceBuffer[byteIndex] === 10) lineNumber++;
40062
+ return lineNumber;
40063
+ };
39648
40064
  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";
39649
40065
  const ACCESSORS_MIN_VERSION = "3.15.0";
39650
40066
  const supportsAccessors = (reanimatedVersion) => {
@@ -39818,23 +40234,12 @@ const REACT_USE_BINDING_RESOLUTION = {
39818
40234
  isReactUseBinding: true,
39819
40235
  isReactNamespaceBinding: false
39820
40236
  };
39821
- const getScriptKind$2 = (filename) => {
39822
- if (filename.endsWith(".tsx")) return ts.ScriptKind.TSX;
39823
- if (filename.endsWith(".jsx")) return ts.ScriptKind.JSX;
39824
- if (filename.endsWith(".ts")) return ts.ScriptKind.TS;
39825
- return ts.ScriptKind.JS;
39826
- };
39827
40237
  const getUtf16Offset$1 = (sourceText, utf8Offset) => Buffer.from(sourceText).subarray(0, utf8Offset).toString("utf8").length;
39828
- const unwrapExpression = (expression) => {
39829
- let currentExpression = expression;
39830
- while (ts.isParenthesizedExpression(currentExpression) || ts.isAsExpression(currentExpression) || ts.isSatisfiesExpression(currentExpression) || ts.isNonNullExpression(currentExpression) || ts.isTypeAssertionExpression(currentExpression)) currentExpression = currentExpression.expression;
39831
- return currentExpression;
39832
- };
39833
40238
  const getStaticPropertyName = (node) => {
39834
40239
  if (!node) return null;
39835
40240
  if (ts.isIdentifier(node) || ts.isStringLiteral(node) || ts.isNumericLiteral(node)) return node.text;
39836
40241
  if (ts.isComputedPropertyName(node)) {
39837
- const expression = unwrapExpression(node.expression);
40242
+ const expression = unwrapTypescriptExpression(node.expression);
39838
40243
  if (ts.isStringLiteral(expression) || ts.isNoSubstitutionTemplateLiteral(expression)) return expression.text;
39839
40244
  }
39840
40245
  return null;
@@ -39863,7 +40268,7 @@ const isReactUseObjectBindingElement = (bindingElement) => {
39863
40268
  return getStaticPropertyName(bindingElement.propertyName) === USE_IDENTIFIER;
39864
40269
  };
39865
40270
  const isReactRequireCall = (expression) => {
39866
- const unwrappedExpression = unwrapExpression(expression);
40271
+ const unwrappedExpression = unwrapTypescriptExpression(expression);
39867
40272
  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;
39868
40273
  };
39869
40274
  const getModuleSource = (node) => {
@@ -39965,7 +40370,7 @@ const isIdentifierShadowedByLocalBinding = (identifier, sourceFile) => {
39965
40370
  return false;
39966
40371
  };
39967
40372
  const isReactNamespaceExpression = (expression, reactImportBindings, sourceFile, visitedDeclarations) => {
39968
- const unwrappedExpression = unwrapExpression(expression);
40373
+ const unwrappedExpression = unwrapTypescriptExpression(expression);
39969
40374
  if (isReactRequireCall(unwrappedExpression)) return true;
39970
40375
  if (!ts.isIdentifier(unwrappedExpression)) return false;
39971
40376
  if (reactImportBindings.namespaceNames.has(unwrappedExpression.text) && !isIdentifierShadowedByLocalBinding(unwrappedExpression, sourceFile)) return true;
@@ -39973,7 +40378,7 @@ const isReactNamespaceExpression = (expression, reactImportBindings, sourceFile,
39973
40378
  };
39974
40379
  const isReactUseExpression = (expression, reactImportBindings, sourceFile, visitedDeclarations) => {
39975
40380
  if (!expression) return false;
39976
- const unwrappedExpression = unwrapExpression(expression);
40381
+ const unwrappedExpression = unwrapTypescriptExpression(expression);
39977
40382
  if (ts.isIdentifier(unwrappedExpression)) {
39978
40383
  if (reactImportBindings.useImportNames.has(unwrappedExpression.text) && !isIdentifierShadowedByLocalBinding(unwrappedExpression, sourceFile)) return true;
39979
40384
  if (unwrappedExpression.text === USE_IDENTIFIER) return false;
@@ -40073,7 +40478,7 @@ const findUseCallIdentifier = (sourceFile, useOffset) => {
40073
40478
  return matchedIdentifier;
40074
40479
  };
40075
40480
  const resolveUseCallBinding = (sourceText, filename, utf8Offset) => {
40076
- const sourceFile = ts.createSourceFile(filename, sourceText, ts.ScriptTarget.Latest, true, getScriptKind$2(filename));
40481
+ const sourceFile = ts.createSourceFile(filename, sourceText, ts.ScriptTarget.Latest, true, getTypescriptScriptKind(filename));
40077
40482
  const useIdentifier = findUseCallIdentifier(sourceFile, getUtf16Offset$1(sourceText, utf8Offset));
40078
40483
  if (!useIdentifier) return null;
40079
40484
  return resolveIdentifierBinding(useIdentifier, collectReactImportBindings(sourceFile), sourceFile);
@@ -40109,12 +40514,6 @@ const WORKLET_ACCEPTING_CALLEES = new Set([
40109
40514
  "executeOnUIRuntimeSync"
40110
40515
  ]);
40111
40516
  const GESTURE_ROOT_IDENTIFIER = "Gesture";
40112
- const getScriptKind$1 = (filename) => {
40113
- if (filename.endsWith(".tsx")) return ts.ScriptKind.TSX;
40114
- if (filename.endsWith(".jsx")) return ts.ScriptKind.JSX;
40115
- if (filename.endsWith(".ts")) return ts.ScriptKind.TS;
40116
- return ts.ScriptKind.JS;
40117
- };
40118
40517
  const getUtf16Offset = (sourceText, utf8Offset) => Buffer.from(sourceText).subarray(0, utf8Offset).toString("utf8").length;
40119
40518
  const hasWorkletDirective = (node) => {
40120
40519
  if (!ts.isFunctionDeclaration(node) && !ts.isFunctionExpression(node) && !ts.isArrowFunction(node) && !ts.isMethodDeclaration(node)) return false;
@@ -40186,17 +40585,11 @@ const shouldSuppressCompilerFindingInWorklet = (diagnostic, project, rootDirecto
40186
40585
  } catch {
40187
40586
  return false;
40188
40587
  }
40189
- return isOffsetInsideWorklet(ts.createSourceFile(absolutePath, sourceText, ts.ScriptTarget.Latest, true, getScriptKind$1(absolutePath)), getUtf16Offset(sourceText, primaryLabel.span.offset));
40588
+ return isOffsetInsideWorklet(ts.createSourceFile(absolutePath, sourceText, ts.ScriptTarget.Latest, true, getTypescriptScriptKind(absolutePath)), getUtf16Offset(sourceText, primaryLabel.span.offset));
40190
40589
  };
40191
40590
  const MANUAL_MEMOIZATION_PLUGIN = "react-doctor";
40192
40591
  const MANUAL_MEMOIZATION_RULE = "react-compiler-no-manual-memoization";
40193
40592
  const REACT_COMPILER_PLUGIN = "react-hooks-js";
40194
- const getScriptKind = (filename) => {
40195
- if (filename.endsWith(".tsx")) return ts.ScriptKind.TSX;
40196
- if (filename.endsWith(".jsx")) return ts.ScriptKind.JSX;
40197
- if (filename.endsWith(".ts")) return ts.ScriptKind.TS;
40198
- return ts.ScriptKind.JS;
40199
- };
40200
40593
  const isManualMemoizationDiagnostic = (diagnostic) => diagnostic.plugin === MANUAL_MEMOIZATION_PLUGIN && diagnostic.rule === MANUAL_MEMOIZATION_RULE;
40201
40594
  const nodeLineRange = (sourceFile, node) => ({
40202
40595
  startLine: sourceFile.getLineAndCharacterOfPosition(node.getStart(sourceFile)).line + 1,
@@ -40216,13 +40609,6 @@ const findOutermostEnclosingFunctionRange = (sourceFile, targetLine) => {
40216
40609
  visit(sourceFile);
40217
40610
  return foundRange;
40218
40611
  };
40219
- const NEWLINE_BYTE = 10;
40220
- const lineOfUtf8Offset = (sourceBuffer, utf8Offset) => {
40221
- let lineNumber = 1;
40222
- const scanEnd = Math.min(utf8Offset, sourceBuffer.length);
40223
- for (let byteIndex = 0; byteIndex < scanEnd; byteIndex++) if (sourceBuffer[byteIndex] === NEWLINE_BYTE) lineNumber++;
40224
- return lineNumber;
40225
- };
40226
40612
  /**
40227
40613
  * Drops `react-doctor/react-compiler-no-manual-memoization` diagnostics
40228
40614
  * whose enclosing function also carries a `react-hooks-js` (React
@@ -40265,7 +40651,7 @@ const suppressMemoizationInBailedOutFunctions = (diagnostics, rootDirectory) =>
40265
40651
  try {
40266
40652
  const buffer = NFS.readFileSync(absolutePath);
40267
40653
  parsed = {
40268
- sourceFile: ts.createSourceFile(absolutePath, buffer.toString("utf8"), ts.ScriptTarget.Latest, true, getScriptKind(absolutePath)),
40654
+ sourceFile: ts.createSourceFile(absolutePath, buffer.toString("utf8"), ts.ScriptTarget.Latest, true, getTypescriptScriptKind(absolutePath)),
40269
40655
  buffer
40270
40656
  };
40271
40657
  } catch {
@@ -40403,11 +40789,11 @@ const buildRelatedLocations = (labels, filePath) => {
40403
40789
  }
40404
40790
  return related;
40405
40791
  };
40406
- const isOxlintOutput = (value) => {
40407
- if (typeof value !== "object" || value === null) return false;
40408
- const candidate = value;
40409
- return Array.isArray(candidate.diagnostics);
40410
- };
40792
+ const isOxlintSpan = (value) => isRecord(value) && typeof value.offset === "number" && typeof value.length === "number" && typeof value.line === "number" && typeof value.column === "number";
40793
+ const isOxlintLabel = (value) => isRecord(value) && isOxlintSpan(value.span);
40794
+ 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);
40795
+ const isEngineFailureDiagnostic = (value) => !isRecord(value) || typeof value.filename !== "string" || value.filename.length === 0;
40796
+ const isOxlintOutput = (value) => isRecord(value) && Array.isArray(value.diagnostics);
40411
40797
  /**
40412
40798
  * Parses one oxlint subprocess's stdout into a flat `Diagnostic[]`.
40413
40799
  * Tolerates the leading-notice-line shape oxlint sometimes prints
@@ -40426,23 +40812,59 @@ const parseOxlintOutput = (stdout, project, rootDirectory) => {
40426
40812
  throw new ReactDoctorError({ reason: new OxlintOutputUnparseable({ preview: stdout.slice(0, 600) }) });
40427
40813
  }
40428
40814
  if (!isOxlintOutput(parsed)) throw new ReactDoctorError({ reason: new OxlintOutputUnparseable({ preview: stdout.slice(0, 600) }) });
40815
+ const engineFailureDiagnostic = parsed.diagnostics.find(isEngineFailureDiagnostic);
40816
+ if (engineFailureDiagnostic !== void 0) throw new ReactDoctorError({ reason: new OxlintOutputUnparseable({ preview: JSON.stringify(engineFailureDiagnostic).slice(0, 600) }) });
40429
40817
  const minifiedFileCache = /* @__PURE__ */ new Map();
40818
+ const sourceBufferCache = /* @__PURE__ */ new Map();
40819
+ const sourceLinesCache = /* @__PURE__ */ new Map();
40820
+ const resolveAbsolutePath = (filename) => Path.isAbsolute(filename) ? filename : Path.resolve(rootDirectory || ".", filename);
40821
+ const readSourceBuffer = (filename) => {
40822
+ const absolutePath = resolveAbsolutePath(filename);
40823
+ const cached = sourceBufferCache.get(absolutePath);
40824
+ if (cached !== void 0) return cached;
40825
+ let sourceBuffer;
40826
+ try {
40827
+ sourceBuffer = NFS.readFileSync(absolutePath);
40828
+ } catch {
40829
+ sourceBuffer = null;
40830
+ }
40831
+ sourceBufferCache.set(absolutePath, sourceBuffer);
40832
+ return sourceBuffer;
40833
+ };
40834
+ const readSourceLines = (filename) => {
40835
+ const absolutePath = resolveAbsolutePath(filename);
40836
+ const cached = sourceLinesCache.get(absolutePath);
40837
+ if (cached !== void 0) return cached;
40838
+ const sourceBuffer = readSourceBuffer(filename);
40839
+ const sourceLines = sourceBuffer ? sourceBuffer.toString("utf8").split("\n") : null;
40840
+ sourceLinesCache.set(absolutePath, sourceLines);
40841
+ return sourceLines;
40842
+ };
40430
40843
  const isMinifiedDiagnosticFile = (filename) => {
40431
- const absolutePath = Path.isAbsolute(filename) ? filename : Path.resolve(rootDirectory || ".", filename);
40844
+ const absolutePath = resolveAbsolutePath(filename);
40432
40845
  const cached = minifiedFileCache.get(absolutePath);
40433
40846
  if (cached !== void 0) return cached;
40434
40847
  const minified = isMinifiedSource(absolutePath);
40435
40848
  minifiedFileCache.set(absolutePath, minified);
40436
40849
  return minified;
40437
40850
  };
40438
- return suppressMemoizationInBailedOutFunctions(parsed.diagnostics.filter((diagnostic) => diagnostic.code && isLintableSourceFile(diagnostic.filename) && !isMinifiedDiagnosticFile(diagnostic.filename) && !shouldSuppressLocalUseHookDiagnostic(diagnostic, rootDirectory) && !shouldSuppressCompilerFindingInWorklet(diagnostic, project, rootDirectory)).map((diagnostic) => {
40851
+ return suppressMemoizationInBailedOutFunctions(parsed.diagnostics.filter((diagnostic) => isMappableOxlintDiagnostic(diagnostic) && isLintableSourceFile(diagnostic.filename) && !isMinifiedDiagnosticFile(diagnostic.filename) && !shouldSuppressLocalUseHookDiagnostic(diagnostic, rootDirectory) && !shouldSuppressCompilerFindingInWorklet(diagnostic, project, rootDirectory)).map((diagnostic) => {
40439
40852
  const { plugin, rule } = parseRuleCode(diagnostic.code);
40440
40853
  const primaryLabel = diagnostic.labels[0];
40441
40854
  const cleaned = cleanDiagnosticMessage(diagnostic.message, diagnostic.help, plugin, rule, project);
40442
40855
  const normalizedFilePath = diagnostic.filename.replaceAll("\\", "/");
40443
40856
  const primarySpan = primaryLabel?.span;
40857
+ const sourceBuffer = primarySpan ? readSourceBuffer(diagnostic.filename) : null;
40444
40858
  const relatedLocations = buildRelatedLocations(diagnostic.labels, normalizedFilePath);
40445
40859
  const category = resolveDiagnosticCategory(plugin, rule);
40860
+ const matchByOccurrence = resolveMatchByOccurrence(rule, category);
40861
+ const sourceLines = primarySpan ? readSourceLines(diagnostic.filename) : null;
40862
+ const primaryLineIndex = primarySpan ? primarySpan.line - 1 : -1;
40863
+ const primaryLine = sourceLines?.[primaryLineIndex];
40864
+ const primaryColumnIndex = primarySpan ? primarySpan.column - 1 : -1;
40865
+ const isJsxTagLabel = primaryLine !== void 0 && primaryColumnIndex >= 0 && (primaryLine[primaryColumnIndex] === "<" || primaryLine[primaryColumnIndex - 1] === "<");
40866
+ const jsxOpenerEndLineIndex = sourceLines && isJsxTagLabel ? findJsxOpenerSpan(sourceLines, primaryLineIndex) : null;
40867
+ const primarySpanEndLine = jsxOpenerEndLineIndex !== null ? jsxOpenerEndLineIndex + 1 : primarySpan && sourceBuffer ? lineOfUtf8Offset(sourceBuffer, primarySpan.offset + Math.max(primarySpan.length - 1, 0)) : void 0;
40446
40868
  return {
40447
40869
  filePath: normalizedFilePath,
40448
40870
  plugin,
@@ -40456,10 +40878,11 @@ const parseOxlintOutput = (stdout, project, rootDirectory) => {
40456
40878
  column: primarySpan?.column ?? 0,
40457
40879
  ...primarySpan ? {
40458
40880
  offset: primarySpan.offset,
40459
- length: primarySpan.length
40881
+ length: primarySpan.length,
40882
+ ...primarySpanEndLine !== void 0 ? { endLine: primarySpanEndLine } : {}
40460
40883
  } : {},
40461
40884
  category,
40462
- ...resolveMatchByOccurrence(rule, category) ? { matchByOccurrence: true } : {},
40885
+ ...matchByOccurrence ? { matchByOccurrence: true } : {},
40463
40886
  ...relatedLocations.length > 0 ? { relatedLocations } : {}
40464
40887
  };
40465
40888
  }), rootDirectory);
@@ -42246,7 +42669,7 @@ const runInspect = (input, hooks = {}) => gen(function* () {
42246
42669
  directory: scanDirectory,
42247
42670
  repo
42248
42671
  }).pipe(orElseSucceed(() => null)) : succeed$2(null));
42249
- const lintIncludePaths = (input.skipJsxIncludeFilter ? input.includePaths.length > 0 ? [...input.includePaths] : void 0 : computeExplicitLintIncludePaths([...input.includePaths], project)) ?? resolveLintIncludePaths(scanDirectory, resolvedConfig.config, project);
42672
+ const lintIncludePaths = (input.skipExplicitIncludePathFilter ? input.includePaths.length > 0 ? [...input.includePaths] : void 0 : computeExplicitLintIncludePaths([...input.includePaths])) ?? resolveLintIncludePaths(scanDirectory, resolvedConfig.config);
42250
42673
  const fallbackScannedFilePaths = input.suppressScanSummary ? (lintIncludePaths ?? (yield* filesService.listSourceFiles(scanDirectory))).map((relativePath) => Path.resolve(scanDirectory, relativePath)) : [];
42251
42674
  const beforeLint = hooks.beforeLint ?? NO_HOOKS.beforeLint;
42252
42675
  const afterLint = hooks.afterLint ?? NO_HOOKS.afterLint;
@@ -42260,7 +42683,8 @@ const runInspect = (input, hooks = {}) => gen(function* () {
42260
42683
  respectInlineDisables: input.respectInlineDisables,
42261
42684
  showWarnings
42262
42685
  });
42263
- const applyPerElementPipeline = (rawStream) => rawStream.pipe(filterMap(filterMapNullable(transform.apply)), tap((diagnostic) => reporterService.emit(diagnostic)));
42686
+ const filterPerElementPipeline = (rawStream) => rawStream.pipe(filterMap(filterMapNullable(transform.apply)));
42687
+ const applyPerElementPipeline = (rawStream) => filterPerElementPipeline(rawStream).pipe(tap((diagnostic) => reporterService.emit(diagnostic)));
42264
42688
  const envCollected = yield* runCollect(applyPerElementPipeline(fromIterable$1(isDiffMode ? [] : [
42265
42689
  ...checkReducedMotion(scanDirectory),
42266
42690
  ...checkPnpmHardening(scanDirectory),
@@ -42389,7 +42813,7 @@ const runInspect = (input, hooks = {}) => gen(function* () {
42389
42813
  });
42390
42814
  return empty$4;
42391
42815
  }))));
42392
- const lintCollected = yield* runCollect(applyPerElementPipeline(shouldOverlapDeadCode ? baseLintStream.pipe(provideService(OxlintConcurrency, lintConcurrency)) : baseLintStream)).pipe(timeoutOption(lintPhaseTimeoutMs), flatMap$2(match$3({
42816
+ const lintCollected = dedupeRelatedDiagnostics(yield* runCollect(filterPerElementPipeline(shouldOverlapDeadCode ? baseLintStream.pipe(provideService(OxlintConcurrency, lintConcurrency)) : baseLintStream)).pipe(timeoutOption(lintPhaseTimeoutMs), flatMap$2(match$3({
42393
42817
  onNone: () => set(lintFailure, {
42394
42818
  didFail: true,
42395
42819
  reason: `Lint analysis exceeded ${lintPhaseTimeoutMs / MILLISECONDS_PER_SECOND}s and was skipped.`,
@@ -42397,7 +42821,8 @@ const runInspect = (input, hooks = {}) => gen(function* () {
42397
42821
  reasonKind: null
42398
42822
  }).pipe(as([])),
42399
42823
  onSome: succeed$2
42400
- })));
42824
+ }))));
42825
+ yield* forEach$1(lintCollected, reporterService.emit, { discard: true });
42401
42826
  const lintFailureState = yield* get$2(lintFailure);
42402
42827
  yield* afterLint(lintFailureState.didFail);
42403
42828
  if (lintFailureState.didFail) yield* scanProgress.fail(formatLintFailText(lintFailureState.reasonTag, process.version));
@@ -42917,10 +43342,11 @@ const buildSkippedChecks = (input) => {
42917
43342
  * render diff-base mistakes (`GitBaseBranchInvalid` /
42918
43343
  * `GitBaseBranchMissing`) as clean user errors instead of crashes.
42919
43344
  */
42920
- const getDiffInfo = (directory, explicitBaseBranch) => runPromise(gen(function* () {
43345
+ const getDiffInfo = (directory, explicitBaseBranch, includeUntracked) => runPromise(gen(function* () {
42921
43346
  const selection = yield* (yield* Git).diffSelection({
42922
43347
  directory,
42923
- explicitBaseBranch
43348
+ explicitBaseBranch,
43349
+ includeUntracked
42924
43350
  });
42925
43351
  if (selection === null) return null;
42926
43352
  return {
@@ -43210,4 +43636,4 @@ const toJsonReport = (result, options) => buildJsonReport({
43210
43636
  export { AmbiguousProjectError, NoReactDependencyError, NotADirectoryError, PackageJsonNotFoundError, ProjectNotFoundError, ReactDoctorError, buildJsonReport, buildJsonReportError, clearCaches, defineConfig, diagnose, filterSourceFiles, getDiffInfo, hasReactRuntime, isProjectDiscoveryError, isReactDoctorError, summarizeDiagnostics, toJsonReport };
43211
43637
 
43212
43638
  //# sourceMappingURL=index.js.map
43213
- //# debugId=7a10d23c-04fd-53e1-a623-a3dc5d6d0830
43639
+ //# debugId=4096a0e2-9712-5e2f-b602-5ff55d3994a3