1
1
import type { Rule } from "eslint" ;
2
2
import type {
3
+ BinaryExpression ,
3
4
Expression ,
4
5
ExpressionStatement ,
5
6
Identifier ,
6
7
ImportSpecifier ,
7
- MemberExpression ,
8
8
Node ,
9
9
Program ,
10
10
SpreadElement ,
@@ -27,6 +27,8 @@ const browserOnlyGlobals = Object.keys(globals.browser).reduce<
27
27
return acc ;
28
28
} , new Set ( ) ) ;
29
29
30
+ const validGlobalsForServerChecks = new Set ( [ "document" , "window" ] ) ;
31
+
30
32
type Options = [
31
33
{
32
34
allowedServerHooks ?: string [ ] ;
@@ -114,42 +116,97 @@ const create = Components.detect(
114
116
} ) ;
115
117
}
116
118
117
- function getIsSafeWindowCheck ( node : Rule . NodeParentExtension ) {
118
- // check if the window usage is behind a typeof window === 'undefined' check
119
- const conditionalExpressionNode = node . parent ?. parent ;
119
+ function findFirstParentOfType (
120
+ node : Rule . Node ,
121
+ type : string
122
+ ) : Rule . Node | null {
123
+ let currentNode : Rule . Node | null = node ;
124
+
125
+ while ( currentNode ) {
126
+ if ( currentNode . type === type ) {
127
+ return currentNode ;
128
+ }
129
+ currentNode = currentNode ?. parent ;
130
+ }
131
+
132
+ return null ;
133
+ }
134
+
135
+ function isNodeInTree ( node : Rule . Node , target : Rule . Node ) : boolean {
136
+ let currentNode : Rule . Node | null = node ;
137
+
138
+ while ( currentNode ) {
139
+ if ( currentNode === target ) {
140
+ return true ;
141
+ }
142
+ currentNode = currentNode . parent ;
143
+ }
144
+
145
+ return false ;
146
+ }
147
+
148
+ function getBinaryBranchExecutedOnServer ( node : BinaryExpression ) : {
149
+ isWindowCheck : boolean ;
150
+ serverBranch : Rule . Node | null ;
151
+ } {
120
152
const isWindowCheck =
121
- conditionalExpressionNode ?. type === "ConditionalExpression" &&
122
- conditionalExpressionNode . test ?. type === "BinaryExpression" &&
123
- conditionalExpressionNode . test . left ?. type === "UnaryExpression" &&
124
- conditionalExpressionNode . test . left . operator === "typeof" &&
125
- conditionalExpressionNode . test . left . argument ?. type === "Identifier" &&
126
- conditionalExpressionNode . test . left . argument ?. name === "window" &&
127
- conditionalExpressionNode . test . right ?. type === "Literal" &&
128
- conditionalExpressionNode . test . right . value === "undefined" ;
129
-
130
- // checks to see if it's `typeof window !== 'undefined'` or `typeof window === 'undefined'`
131
- const isNegatedWindowCheck =
132
- isWindowCheck &&
133
- conditionalExpressionNode . test ?. type === "BinaryExpression" &&
134
- conditionalExpressionNode . test . operator === "!==" ;
135
-
136
- // checks to see if window is being accessed safely behind a window check
137
- const isSafelyBehindWindowCheck =
138
- ( isWindowCheck &&
139
- ! isNegatedWindowCheck &&
140
- conditionalExpressionNode . alternate === node ?. parent ) ||
141
- ( isNegatedWindowCheck &&
142
- conditionalExpressionNode . consequent === node ?. parent ) ;
143
-
144
- return isSafelyBehindWindowCheck ;
153
+ node . left ?. type === "UnaryExpression" &&
154
+ node . left . operator === "typeof" &&
155
+ node . left . argument ?. type === "Identifier" &&
156
+ validGlobalsForServerChecks . has ( node . left . argument ?. name ) &&
157
+ node . right ?. type === "Literal" &&
158
+ node . right . value === "undefined" &&
159
+ ( node . operator === "===" || node . operator === "!==" ) ;
160
+
161
+ let serverBranch = null ;
162
+
163
+ if ( ! isWindowCheck ) {
164
+ return { isWindowCheck, serverBranch } ;
165
+ }
166
+
167
+ //@ts -expect-error
168
+ const { parent } = node ;
169
+ if ( ! parent ) {
170
+ return { isWindowCheck, serverBranch } ;
171
+ }
172
+
173
+ if ( node . operator === "===" ) {
174
+ serverBranch =
175
+ parent . type === "IfStatement" ||
176
+ parent . type === "ConditionalExpression"
177
+ ? parent . alternate
178
+ : null ;
179
+ } else {
180
+ serverBranch =
181
+ parent . type === "IfStatement" ||
182
+ parent . type === "ConditionalExpression"
183
+ ? parent . consequent
184
+ : null ;
185
+ }
186
+
187
+ return { isWindowCheck, serverBranch } ;
145
188
}
146
189
190
+ const isNodePartOfSafelyExecutedServerBranch = (
191
+ node : Rule . Node
192
+ ) : boolean => {
193
+ let isUsedInServerBranch = false ;
194
+ serverBranches . forEach ( ( serverBranch ) => {
195
+ if ( isNodeInTree ( node , serverBranch ) ) {
196
+ isUsedInServerBranch = true ;
197
+ }
198
+ } ) ;
199
+ return isUsedInServerBranch ;
200
+ } ;
201
+
147
202
const reactImports : Record < string | "namespace" , string | string [ ] > = {
148
203
namespace : [ ] ,
149
204
} ;
150
205
151
206
const undeclaredReferences = new Set ( ) ;
152
207
208
+ const serverBranches = new Set < Rule . Node > ( ) ;
209
+
153
210
return {
154
211
Program ( node ) {
155
212
for ( const block of node . body ) {
@@ -226,30 +283,33 @@ const create = Components.detect(
226
283
} ) ;
227
284
}
228
285
} ,
229
- MemberExpression ( node ) {
230
- // Catch uses of browser APIs in module scope
231
- // or React component scope.
232
- // eg:
233
- // const foo = window.foo
234
- // window.addEventListener(() => {})
235
- // const Foo() {
236
- // const foo = window.foo
237
- // return <div />;
238
- // }
286
+ Identifier ( node ) {
287
+ const name = node . name ;
239
288
// @ts -expect-error
240
- const name = node . object . name ;
241
- const scopeType = context . getScope ( ) . type ;
242
-
243
- const isSafelyBehindWindowCheck = getIsSafeWindowCheck ( node ) ;
244
-
245
- if (
246
- undeclaredReferences . has ( name ) &&
247
- browserOnlyGlobals . has ( name ) &&
248
- ( scopeType === "module" || ! ! util . getParentComponent ( node ) ) &&
249
- ! isSafelyBehindWindowCheck
250
- ) {
251
- instances . push ( name ) ;
252
- reportMissingDirective ( "addUseClientBrowserAPI" , node . object ) ;
289
+ if ( undeclaredReferences . has ( name ) && browserOnlyGlobals . has ( name ) ) {
290
+ // find the nearest binary expression so we can see if this instance of window is being used in a `typeof window === undefined`-like check
291
+ const binaryExpressionNode = findFirstParentOfType (
292
+ node ,
293
+ "BinaryExpression"
294
+ ) as BinaryExpression | null ;
295
+ if ( binaryExpressionNode ) {
296
+ const { isWindowCheck, serverBranch } =
297
+ getBinaryBranchExecutedOnServer ( binaryExpressionNode ) ;
298
+ // if this instance isn't part of a window check we report it
299
+ if ( ! isWindowCheck ) {
300
+ instances . push ( name ) ;
301
+ reportMissingDirective ( "addUseClientBrowserAPI" , node ) ;
302
+ } else if ( isWindowCheck && serverBranch ) {
303
+ // if it is part of a window check, we don't report it and we save the server branch so we can check if future window instances are a part of the branch of code safely executed on the server
304
+ serverBranches . add ( serverBranch ) ;
305
+ }
306
+ } else {
307
+ // if the window usage isn't part of the binary expression, we check to see if it's part of a safely checked server branch and report if not
308
+ if ( ! isNodePartOfSafelyExecutedServerBranch ( node ) ) {
309
+ instances . push ( name ) ;
310
+ reportMissingDirective ( "addUseClientBrowserAPI" , node ) ;
311
+ }
312
+ }
253
313
}
254
314
} ,
255
315
ExpressionStatement ( node ) {
0 commit comments