import { NodePath } from "@babel/core";
import * as t from "@babel/types";
import {
CompilerError,
CompilerErrorDetail,
CompilerSuggestionOperation,
ErrorSeverity,
} from "../CompilerError";
import { assertExhaustive } from "../Utils/utils";
export type SuppressionRange = {
disableComment: t.Comment;
enableComment: t.Comment | null;
source: SuppressionSource;
};
type SuppressionSource = "Eslint" | "Flow";
export function filterSuppressionsThatAffectFunction(
suppressionRanges: Array<SuppressionRange>,
fn: NodePath<t.Function>
): Array<SuppressionRange> {
const suppressionsInScope: Array<SuppressionRange> = [];
const fnNode = fn.node;
for (const suppressionRange of suppressionRanges) {
if (
suppressionRange.disableComment.start == null ||
fnNode.start == null ||
fnNode.end == null
) {
continue;
}
if (
suppressionRange.disableComment.start > fnNode.start &&
(suppressionRange.enableComment === null ||
(suppressionRange.enableComment.end != null &&
suppressionRange.enableComment.end < fnNode.end))
) {
suppressionsInScope.push(suppressionRange);
}
if (
suppressionRange.disableComment.start < fnNode.start &&
(suppressionRange.enableComment === null ||
(suppressionRange.enableComment.end != null &&
suppressionRange.enableComment.end > fnNode.end))
) {
suppressionsInScope.push(suppressionRange);
}
}
return suppressionsInScope;
}
export function findProgramSuppressions(
programComments: Array<t.Comment>,
ruleNames: Array<string>,
flowSuppressions: boolean
): Array<SuppressionRange> {
const suppressionRanges: Array<SuppressionRange> = [];
let disableComment: t.Comment | null = null;
let enableComment: t.Comment | null = null;
let source: SuppressionSource | null = null;
const rulePattern = `(${ruleNames.join("|")})`;
const disableNextLinePattern = new RegExp(
`eslint-disable-next-line ${rulePattern}`
);
const disablePattern = new RegExp(`eslint-disable ${rulePattern}`);
const enablePattern = new RegExp(`eslint-enable ${rulePattern}`);
const flowSuppressionPattern = new RegExp(
"\\$(FlowFixMe\\w*|FlowExpectedError|FlowIssue)\\[react\\-rule"
);
for (const comment of programComments) {
if (comment.start == null || comment.end == null) {
continue;
}
if (
disableComment == null &&
disableNextLinePattern.test(comment.value)
) {
disableComment = comment;
enableComment = comment;
source = "Eslint";
}
if (
flowSuppressions &&
disableComment == null &&
flowSuppressionPattern.test(comment.value)
) {
disableComment = comment;
enableComment = comment;
source = "Flow";
}
if (disablePattern.test(comment.value)) {
disableComment = comment;
source = "Eslint";
}
if (enablePattern.test(comment.value) && source === "Eslint") {
enableComment = comment;
}
if (disableComment != null && source != null) {
suppressionRanges.push({
disableComment: disableComment,
enableComment: enableComment,
source,
});
disableComment = null;
enableComment = null;
source = null;
}
}
return suppressionRanges;
}
export function suppressionsToCompilerError(
suppressionRanges: Array<SuppressionRange>
): CompilerError | null {
if (suppressionRanges.length === 0) {
return null;
}
const error = new CompilerError();
for (const suppressionRange of suppressionRanges) {
if (
suppressionRange.disableComment.start == null ||
suppressionRange.disableComment.end == null
) {
continue;
}
let reason, suggestion;
switch (suppressionRange.source) {
case "Eslint":
reason =
"React Compiler has skipped optimizing this component because one or more React ESLint rules were disabled";
suggestion =
"Remove the ESLint suppression and address the React error";
break;
case "Flow":
reason =
"React Compiler has skipped optimizing this component because one or more React rule violations were reported by Flow";
suggestion = "Remove the Flow suppression and address the React error";
break;
default:
assertExhaustive(
suppressionRange.source,
"Unhandled suppression source"
);
}
error.pushErrorDetail(
new CompilerErrorDetail({
reason: `${reason}. React Compiler only works when your components follow all the rules of React, disabling them may result in unexpected or incorrect behavior`,
description: suppressionRange.disableComment.value.trim(),
severity: ErrorSeverity.InvalidReact,
loc: suppressionRange.disableComment.loc ?? null,
suggestions: [
{
description: suggestion,
range: [
suppressionRange.disableComment.start,
suppressionRange.disableComment.end,
],
op: CompilerSuggestionOperation.Remove,
},
],
})
);
}
return error;
}