1
+/*
2
+ * Copyright (c) Meta Platforms, Inc. and affiliates.
3
+ *
4
+ * This source code is licensed under the MIT license found in the
5
+ * LICENSE file in the root directory of this source tree.
6
+ */
7
+
8
+import { NodePath } from "@babel/core";
9
+import * as t from "@babel/types";
10
+import {
11
+ CompilerError,
12
+ CompilerErrorDetail,
13
+ CompilerSuggestionOperation,
14
+ ErrorSeverity,
15
+} from "../CompilerError";
16
+
17
+/**
18
+ * Captures the start and end range of a pair of eslint-disable ... eslint-enable comments. In the
19
+ * case of a CommentLine, both the disable and enable point to the same comment.
20
+ *
21
+ * The enable comment can be missing in the case where only a disable block is present, ie the rest
22
+ * of the file has potential React violations.
23
+ */
24
+export type EslintSuppressionRange = {
25
+ disableComment: t.Comment;
26
+ enableComment: t.Comment | null;
27
+};
28
+
29
+/**
30
+ * An eslint suppression affects a function if:
31
+ * 1. The suppression is within the function's body; or
32
+ * 2. The suppression wraps the function
33
+ */
34
+export function filterEslintSuppressionsThatAffectFunction(
35
+ suppressionRanges: Array<EslintSuppressionRange>,
36
+ fn: NodePath<t.Function>
37
+): Array<EslintSuppressionRange> {
38
+ const suppressionsInScope: Array<EslintSuppressionRange> = [];
39
+ const fnNode = fn.node;
40
+ for (const suppressionRange of suppressionRanges) {
41
+ if (
42
+ suppressionRange.disableComment.start == null ||
43
+ fnNode.start == null ||
44
+ fnNode.end == null
45
+ ) {
46
+ continue;
47
+ }
48
+ // The suppression is within the function
49
+ if (
50
+ suppressionRange.disableComment.start > fnNode.start &&
51
+ // If there is no matching enable, the rest of the file has potential violations
52
+ (suppressionRange.enableComment === null ||
53
+ (suppressionRange.enableComment.end != null &&
54
+ suppressionRange.enableComment.end < fnNode.end))
55
+ ) {
56
+ suppressionsInScope.push(suppressionRange);
57
+ }
58
+
59
+ // The suppression wraps the function
60
+ if (
61
+ suppressionRange.disableComment.start < fnNode.start &&
62
+ // If there is no matching enable, the rest of the file has potential violations
63
+ (suppressionRange.enableComment === null ||
64
+ (suppressionRange.enableComment.end != null &&
65
+ suppressionRange.enableComment.end > fnNode.end))
66
+ ) {
67
+ suppressionsInScope.push(suppressionRange);
68
+ }
69
+ }
70
+ return suppressionsInScope;
71
+}
72
+
73
+export function findProgramEslintSuppressions(
74
+ programComments: Array<t.Comment>
75
+): Array<EslintSuppressionRange> {
76
+ const suppressionRanges: Array<EslintSuppressionRange> = [];
77
+ let disableComment: t.Comment | null = null;
78
+ let enableComment: t.Comment | null = null;
79
+ for (const comment of programComments) {
80
+ if (comment.start == null || comment.end == null) {
81
+ continue;
82
+ }
83
+
84
+ if (
85
+ /*
86
+ * If we're already within a CommentBlock, we should not restart the range prematurely for a
87
+ * CommentLine within the block.
88
+ */
89
+ disableComment == null &&
90
+ /eslint-disable-next-line react-hooks\/(exhaustive-deps|rules-of-hooks)/.test(
91
+ comment.value
92
+ )
93
+ ) {
94
+ disableComment = comment;
95
+ enableComment = comment;
96
+ }
97
+
98
+ if (
99
+ /eslint-disable react-hooks\/(exhaustive-deps|rules-of-hooks)/.test(
100
+ comment.value
101
+ )
102
+ ) {
103
+ disableComment = comment;
104
+ }
105
+
106
+ if (
107
+ /eslint-enable react-hooks\/(exhaustive-deps|rules-of-hooks)/.test(
108
+ comment.value
109
+ )
110
+ ) {
111
+ enableComment = comment;
112
+ }
113
+
114
+ if (disableComment != null) {
115
+ suppressionRanges.push({
116
+ disableComment: disableComment,
117
+ enableComment: enableComment,
118
+ });
119
+ disableComment = null;
120
+ enableComment = null;
121
+ }
122
+ }
123
+ return suppressionRanges;
124
+}
125
+
126
+export function suppressionsToCompilerError(
127
+ suppressionRanges: Array<EslintSuppressionRange>
128
+): CompilerError | null {
129
+ if (suppressionRanges.length === 0) {
130
+ return null;
131
+ }
132
+ const reason =
133
+ "React Forget has bailed out of optimizing this component as one or more React eslint rules were disabled. React Forget only works when your components follow all the rules of React, disabling them may result in undefined behavior";
134
+ const error = new CompilerError();
135
+ for (const suppressionRange of suppressionRanges) {
136
+ if (
137
+ suppressionRange.disableComment.start == null ||
138
+ suppressionRange.disableComment.end == null
139
+ ) {
140
+ continue;
141
+ }
142
+ error.pushErrorDetail(
143
+ new CompilerErrorDetail({
144
+ reason,
145
+ description: suppressionRange.disableComment.value.trim(),
146
+ severity: ErrorSeverity.InvalidReact,
147
+ loc: suppressionRange.disableComment.loc ?? null,
148
+ suggestions: [
149
+ {
150
+ description: "Remove the eslint disable",
151
+ range: [
152
+ suppressionRange.disableComment.start,
153
+ suppressionRange.disableComment.end,
154
+ ],
155
+ op: CompilerSuggestionOperation.Remove,
156
+ },
157
+ ],
158
+ })
159
+ );
160
+ }
161
+ return error;
162
+}