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.