import type {SourceLocation as BabelSourceLocation} from '@babel/types';
import {
type CompilerDiagnosticOptions,
type CompilerErrorDetailOptions,
CompilerSuggestionOperation,
LintRules,
type LintRule,
ErrorSeverity,
LintRulePreset,
} from 'babel-plugin-react-compiler';
import {type Linter, type Rule} from 'eslint';
import runReactCompiler, {RunCacheEntry} from './RunReactCompiler';
function assertExhaustive(_: never, errorMsg: string): never {
throw new Error(errorMsg);
}
function makeSuggestions(
detail: CompilerErrorDetailOptions | CompilerDiagnosticOptions,
): Array<Rule.SuggestionReportDescriptor> {
const suggest: Array<Rule.SuggestionReportDescriptor> = [];
if (Array.isArray(detail.suggestions)) {
for (const suggestion of detail.suggestions) {
switch (suggestion.op) {
case CompilerSuggestionOperation.InsertBefore:
suggest.push({
desc: suggestion.description,
fix(fixer) {
return fixer.insertTextBeforeRange(
suggestion.range,
suggestion.text,
);
},
});
break;
case CompilerSuggestionOperation.InsertAfter:
suggest.push({
desc: suggestion.description,
fix(fixer) {
return fixer.insertTextAfterRange(
suggestion.range,
suggestion.text,
);
},
});
break;
case CompilerSuggestionOperation.Replace:
suggest.push({
desc: suggestion.description,
fix(fixer) {
return fixer.replaceTextRange(suggestion.range, suggestion.text);
},
});
break;
case CompilerSuggestionOperation.Remove:
suggest.push({
desc: suggestion.description,
fix(fixer) {
return fixer.removeRange(suggestion.range);
},
});
break;
default:
assertExhaustive(suggestion, 'Unhandled suggestion operation');
}
}
}
return suggest;
}
function getReactCompilerResult(context: Rule.RuleContext): RunCacheEntry {
const sourceCode = context.sourceCode ?? context.getSourceCode();
const filename = context.filename ?? context.getFilename();
const userOpts = context.options[0] ?? {};
const results = runReactCompiler({
sourceCode,
filename,
userOpts,
});
return results;
}
function hasFlowSuppression(
program: RunCacheEntry,
nodeLoc: BabelSourceLocation,
suppressions: Array<string>,
): boolean {
for (const commentNode of program.flowSuppressions) {
if (
suppressions.includes(commentNode.code) &&
commentNode.line === nodeLoc.start.line - 1
) {
return true;
}
}
return false;
}
function makeRule(rule: LintRule): Rule.RuleModule {
const create = (context: Rule.RuleContext): Rule.RuleListener => {
const result = getReactCompilerResult(context);
for (const event of result.events) {
if (event.kind === 'CompileError') {
const detail = event.detail;
if (detail.category === rule.category) {
const loc = detail.primaryLocation();
if (loc == null || typeof loc === 'symbol') {
continue;
}
if (
hasFlowSuppression(result, loc, [
'react-rule-hook',
'react-rule-unsafe-ref',
])
) {
continue;
}
context.report({
message: detail.printErrorMessage(result.sourceCode, {
eslint: true,
}),
loc,
suggest: makeSuggestions(detail.options),
});
}
}
}
return {};
};
return {
meta: {
type: 'problem',
docs: {
description: rule.description,
recommended: rule.preset === LintRulePreset.Recommended,
url: `https://react.dev/reference/eslint-plugin-react-hooks/lints/${rule.name}`,
},
fixable: 'code',
hasSuggestions: true,
schema: [{type: 'object', additionalProperties: true}],
},
create,
};
}
type RulesConfig = {
[name: string]: {rule: Rule.RuleModule; severity: ErrorSeverity};
};
export const allRules: RulesConfig = LintRules.reduce((acc, rule) => {
acc[rule.name] = {rule: makeRule(rule), severity: rule.severity};
return acc;
}, {} as RulesConfig);
export const recommendedRules: RulesConfig = LintRules.filter(
rule => rule.preset === LintRulePreset.Recommended,
).reduce((acc, rule) => {
acc[rule.name] = {rule: makeRule(rule), severity: rule.severity};
return acc;
}, {} as RulesConfig);
export const recommendedLatestRules: RulesConfig = LintRules.filter(
rule =>
rule.preset === LintRulePreset.Recommended ||
rule.preset === LintRulePreset.RecommendedLatest,
).reduce((acc, rule) => {
acc[rule.name] = {rule: makeRule(rule), severity: rule.severity};
return acc;
}, {} as RulesConfig);
export function mapErrorSeverityToESlint(
severity: ErrorSeverity,
): Linter.StringSeverity {
switch (severity) {
case ErrorSeverity.Error: {
return 'error';
}
case ErrorSeverity.Warning: {
return 'warn';
}
case ErrorSeverity.Hint:
case ErrorSeverity.Off: {
return 'off';
}
default: {
assertExhaustive(severity, `Unhandled severity: ${severity}`);
}
}
}