|
1 | 1 | import { |
2 | 2 | FETCH_CALLEE_NAMES, |
3 | 3 | FETCH_MEMBER_OBJECTS, |
| 4 | + HEADERS_API_MUTATION_METHOD_NAMES, |
4 | 5 | LOOP_TYPES, |
5 | 6 | MUTATING_HTTP_METHODS, |
6 | 7 | MUTATION_METHOD_NAMES, |
| 8 | + REQUEST_SCOPED_MUTATION_CONSTRUCTOR_NAMES, |
7 | 9 | SETTER_PATTERN, |
8 | 10 | UPPERCASE_PATTERN, |
9 | 11 | } from "./constants.js"; |
@@ -333,6 +335,61 @@ const isCookiesOrHeadersCall = (node: EsTreeNode, methodName: string): boolean = |
333 | 335 | return object.callee.name === methodName; |
334 | 336 | }; |
335 | 337 |
|
| 338 | +const isRequestScopedMutationInitializer = (node: EsTreeNode): boolean => |
| 339 | + node.type === "NewExpression" && |
| 340 | + node.callee?.type === "Identifier" && |
| 341 | + REQUEST_SCOPED_MUTATION_CONSTRUCTOR_NAMES.has(node.callee.name); |
| 342 | + |
| 343 | +const collectRequestScopedMutationBindings = (node: EsTreeNode): Set<string> => { |
| 344 | + const bindings = new Set<string>(); |
| 345 | + walkAst(node, (child: EsTreeNode) => { |
| 346 | + if (child.type !== "VariableDeclarator") return; |
| 347 | + if (child.id?.type !== "Identifier") return; |
| 348 | + if (!child.init || !isRequestScopedMutationInitializer(child.init)) return; |
| 349 | + bindings.add(child.id.name); |
| 350 | + }); |
| 351 | + return bindings; |
| 352 | +}; |
| 353 | + |
| 354 | +const isRequestScopedMutationCall = ( |
| 355 | + node: EsTreeNode, |
| 356 | + requestScopedMutationBindings: Set<string>, |
| 357 | +): boolean => { |
| 358 | + if (node.type !== "CallExpression" || node.callee?.type !== "MemberExpression") return false; |
| 359 | + const { object, property } = node.callee; |
| 360 | + if (object?.type !== "Identifier") return false; |
| 361 | + if (property?.type !== "Identifier") return false; |
| 362 | + return ( |
| 363 | + requestScopedMutationBindings.has(object.name) && MUTATION_METHOD_NAMES.has(property.name) |
| 364 | + ); |
| 365 | +}; |
| 366 | + |
| 367 | +const isHeadersApiMutationCall = (node: EsTreeNode): boolean => { |
| 368 | + if (node.type !== "CallExpression" || node.callee?.type !== "MemberExpression") return false; |
| 369 | + const { object, property } = node.callee; |
| 370 | + if ( |
| 371 | + property?.type !== "Identifier" || |
| 372 | + !HEADERS_API_MUTATION_METHOD_NAMES.has(property.name) |
| 373 | + ) { |
| 374 | + return false; |
| 375 | + } |
| 376 | + if (object?.type !== "MemberExpression") return false; |
| 377 | + return object.property?.type === "Identifier" && object.property.name === "headers"; |
| 378 | +}; |
| 379 | + |
| 380 | +const isHeadersFunctionMutationCall = (node: EsTreeNode): boolean => { |
| 381 | + if (node.type !== "CallExpression" || node.callee?.type !== "MemberExpression") return false; |
| 382 | + const { object, property } = node.callee; |
| 383 | + if ( |
| 384 | + property?.type !== "Identifier" || |
| 385 | + !HEADERS_API_MUTATION_METHOD_NAMES.has(property.name) |
| 386 | + ) { |
| 387 | + return false; |
| 388 | + } |
| 389 | + if (object?.type !== "CallExpression" || object.callee?.type !== "Identifier") return false; |
| 390 | + return object.callee.name === "headers"; |
| 391 | +}; |
| 392 | + |
336 | 393 | const isMutatingDbCall = (node: EsTreeNode): boolean => { |
337 | 394 | if (node.type !== "CallExpression" || node.callee?.type !== "MemberExpression") return false; |
338 | 395 | const { property } = node.callee; |
@@ -363,8 +420,15 @@ const isMutatingFetchCall = (node: EsTreeNode): boolean => { |
363 | 420 |
|
364 | 421 | export const findSideEffect = (node: EsTreeNode): string | null => { |
365 | 422 | let sideEffectDescription: string | null = null; |
| 423 | + const requestScopedMutationBindings = collectRequestScopedMutationBindings(node); |
366 | 424 | walkAst(node, (child: EsTreeNode) => { |
367 | 425 | if (sideEffectDescription) return; |
| 426 | + if (isHeadersApiMutationCall(child) || isHeadersFunctionMutationCall(child)) { |
| 427 | + return; |
| 428 | + } |
| 429 | + if (isRequestScopedMutationCall(child, requestScopedMutationBindings)) { |
| 430 | + return; |
| 431 | + } |
368 | 432 | if (isCookiesOrHeadersCall(child, "cookies")) { |
369 | 433 | const methodName = child.callee.property.name; |
370 | 434 | sideEffectDescription = `cookies().${methodName}()`; |
|
0 commit comments