compiler/packages/eslint-plugin-react-compiler/src/rules/ReactCompilerRule.ts TYPESCRIPT 271 lines View on github.com → Search inside
1/**2 * Copyright (c) Meta Platforms, Inc. and affiliates.3 *4 * This source code is licensed under the MIT license found in the5 * LICENSE file in the root directory of this source tree.6 */78import type {SourceLocation as BabelSourceLocation} from '@babel/types';9import {10  CompilerSuggestionOperation,11  type CompileErrorDetail,12} from 'babel-plugin-react-compiler/src';13import type {Linter, Rule} from 'eslint';14import runReactCompiler, {RunCacheEntry} from '../shared/RunReactCompiler';15import {16  ErrorSeverity,17  LintRulePreset,18  LintRules,19  printCodeFrame,20  type LintRule,21} from 'babel-plugin-react-compiler/src/CompilerError';2223function assertExhaustive(_: never, errorMsg: string): never {24  throw new Error(errorMsg);25}2627/**28 * Get the primary source location from a CompileErrorDetail.29 * Handles both the new format (details array) and legacy format (flat loc).30 */31function primaryLocation(32  detail: CompileErrorDetail,33): BabelSourceLocation | null {34  if (detail.details != null) {35    const firstError = detail.details.find(d => d.kind === 'error');36    if (firstError != null) {37      return firstError.loc ?? null;38    }39  }40  return detail.loc ?? null;41}4243/**44 * Format an error message from a CompileErrorDetail, matching the old45 * CompilerErrorDetail.printErrorMessage() / CompilerDiagnostic.printErrorMessage() behavior.46 */47function printErrorMessage(source: string, error: CompileErrorDetail): string {48  const buffer = [`[ReactCompilerError] ${error.reason}`];49  if (error.description != null) {50    buffer.push(`\n\n${error.description}.`);51  }52  /*53   * A CompileError's source location(s) may be provided either as a `details`54   * array (CompilerDiagnostic and the Rust compiler) or as a single flat `loc`55   * (legacy CompilerErrorDetail). Normalize to a list so both shapes render56   * code frames identically.57   */58  const details =59    error.details ??60    (error.loc != null61      ? [{kind: 'error' as const, loc: error.loc, message: error.reason}]62      : []);63  for (const detail of details) {64    if (detail.kind === 'error') {65      const loc = detail.loc;66      if (loc == null || typeof loc === 'symbol') {67        continue;68      }69      let codeFrame: string;70      try {71        codeFrame = printCodeFrame(source, loc, detail.message ?? '');72      } catch {73        codeFrame = detail.message ?? '';74      }75      buffer.push('\n\n');76      if (loc.filename != null) {77        // ESLint uses 1-indexed columns78        buffer.push(79          `${loc.filename}:${loc.start.line}:${loc.start.column + 1}\n`,80        );81      }82      buffer.push(codeFrame);83    } else if (detail.kind === 'hint') {84      buffer.push('\n\n');85      buffer.push(detail.message ?? '');86    }87  }88  return buffer.join('');89}9091function makeSuggestions(92  detail: CompileErrorDetail,93): Array<Rule.SuggestionReportDescriptor> {94  const suggest: Array<Rule.SuggestionReportDescriptor> = [];95  if (Array.isArray(detail.suggestions)) {96    for (const suggestion of detail.suggestions as Array<any>) {97      switch (suggestion.op) {98        case CompilerSuggestionOperation.InsertBefore:99          suggest.push({100            desc: suggestion.description,101            fix(fixer) {102              return fixer.insertTextBeforeRange(103                suggestion.range,104                suggestion.text,105              );106            },107          });108          break;109        case CompilerSuggestionOperation.InsertAfter:110          suggest.push({111            desc: suggestion.description,112            fix(fixer) {113              return fixer.insertTextAfterRange(114                suggestion.range,115                suggestion.text,116              );117            },118          });119          break;120        case CompilerSuggestionOperation.Replace:121          suggest.push({122            desc: suggestion.description,123            fix(fixer) {124              return fixer.replaceTextRange(suggestion.range, suggestion.text);125            },126          });127          break;128        case CompilerSuggestionOperation.Remove:129          suggest.push({130            desc: suggestion.description,131            fix(fixer) {132              return fixer.removeRange(suggestion.range);133            },134          });135          break;136        default:137          assertExhaustive(suggestion, 'Unhandled suggestion operation');138      }139    }140  }141  return suggest;142}143144function getReactCompilerResult(context: Rule.RuleContext): RunCacheEntry {145  // Compat with older versions of eslint146  const sourceCode = context.sourceCode ?? context.getSourceCode();147  const filename = context.filename ?? context.getFilename();148  const userOpts = context.options[0] ?? {};149150  const results = runReactCompiler({151    sourceCode,152    filename,153    userOpts,154  });155156  return results;157}158159function hasFlowSuppression(160  program: RunCacheEntry,161  nodeLoc: BabelSourceLocation,162  suppressions: Array<string>,163): boolean {164  for (const commentNode of program.flowSuppressions) {165    if (166      suppressions.includes(commentNode.code) &&167      commentNode.line === nodeLoc.start.line - 1168    ) {169      return true;170    }171  }172  return false;173}174175function makeRule(rule: LintRule): Rule.RuleModule {176  const create = (context: Rule.RuleContext): Rule.RuleListener => {177    const result = getReactCompilerResult(context);178179    for (const event of result.events) {180      if (event.kind === 'CompileError') {181        const detail = event.detail;182        if (detail.category === rule.category) {183          const loc = primaryLocation(detail);184          if (loc == null) {185            continue;186          }187          if (188            hasFlowSuppression(result, loc, [189              'react-rule-hook',190              'react-rule-unsafe-ref',191            ])192          ) {193            // If Flow already caught this error, we don't need to report it again.194            continue;195          }196          /*197           * TODO: if multiple rules report the same linter category,198           * we should deduplicate them with a "reported" set199           */200          context.report({201            message: printErrorMessage(result.sourceCode, detail),202            loc,203            suggest: makeSuggestions(detail),204          });205        }206      }207    }208    return {};209  };210211  return {212    meta: {213      type: 'problem',214      docs: {215        description: rule.description,216        recommended: rule.preset === LintRulePreset.Recommended,217      },218      fixable: 'code',219      hasSuggestions: true,220      // validation is done at runtime with zod221      schema: [{type: 'object', additionalProperties: true}],222    },223    create,224  };225}226227type RulesConfig = {228  [name: string]: {rule: Rule.RuleModule; severity: ErrorSeverity};229};230231export const allRules: RulesConfig = LintRules.reduce((acc, rule) => {232  acc[rule.name] = {rule: makeRule(rule), severity: rule.severity};233  return acc;234}, {} as RulesConfig);235236export const recommendedRules: RulesConfig = LintRules.filter(237  rule => rule.preset === LintRulePreset.Recommended,238).reduce((acc, rule) => {239  acc[rule.name] = {rule: makeRule(rule), severity: rule.severity};240  return acc;241}, {} as RulesConfig);242243export const recommendedLatestRules: RulesConfig = LintRules.filter(244  rule =>245    rule.preset === LintRulePreset.Recommended ||246    rule.preset === LintRulePreset.RecommendedLatest,247).reduce((acc, rule) => {248  acc[rule.name] = {rule: makeRule(rule), severity: rule.severity};249  return acc;250}, {} as RulesConfig);251252export function mapErrorSeverityToESlint(253  severity: ErrorSeverity,254): Linter.StringSeverity {255  switch (severity) {256    case ErrorSeverity.Error: {257      return 'error';258    }259    case ErrorSeverity.Warning: {260      return 'warn';261    }262    case ErrorSeverity.Hint:263    case ErrorSeverity.Off: {264      return 'off';265    }266    default: {267      assertExhaustive(severity, `Unhandled severity: ${severity}`);268    }269  }270}

Findings

✓ No findings reported for this file.

Get this view in your editor

Same data, no extra tab — call code_get_file + code_get_findings over MCP from Claude/Cursor/Copilot.