oxlint-plugin-react-doctor 0.7.9-dev.e5d5753 → 0.7.9-dev.f441f59

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/dist/index.js +2922 -543
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -1,8 +1,8 @@
1
1
  import { KEYS } from "eslint-visitor-keys";
2
2
  import * as path from "node:path";
3
+ import { parseSync, visitorKeys } from "oxc-parser";
3
4
  import * as fs from "node:fs";
4
5
  import { readFileSync } from "node:fs";
5
- import { parseSync, visitorKeys } from "oxc-parser";
6
6
  import { analyze } from "eslint-scope";
7
7
  //#region src/plugin/utils/is-node-of-type.ts
8
8
  const isNodeOfType = (node, type) => node !== null && typeof node === "object" && "type" in node && node.type === type;
@@ -327,7 +327,15 @@ const defineRule = (rule) => {
327
327
  let lastContentLineIndex;
328
328
  const buildLineStartOffsets = (content) => {
329
329
  const lineStartOffsets = [0];
330
- for (let newlineIndex = content.indexOf("\n"); newlineIndex !== -1; newlineIndex = content.indexOf("\n", newlineIndex + 1)) lineStartOffsets.push(newlineIndex + 1);
330
+ for (let characterIndex = 0; characterIndex < content.length; characterIndex += 1) {
331
+ const character = content[characterIndex];
332
+ if (character === "\r" && content[characterIndex + 1] === "\n") {
333
+ characterIndex += 1;
334
+ lineStartOffsets.push(characterIndex + 1);
335
+ continue;
336
+ }
337
+ if (character === "\r" || character === "\n" || character === "\u2028" || character === "\u2029") lineStartOffsets.push(characterIndex + 1);
338
+ }
331
339
  return lineStartOffsets;
332
340
  };
333
341
  const getLineStartOffsets = (content) => {
@@ -579,6 +587,22 @@ const EXTERNAL_SYNC_OBSERVER_CONSTRUCTORS = new Set([
579
587
  "ResizeObserver",
580
588
  "PerformanceObserver"
581
589
  ]);
590
+ const EXTERNAL_SYNC_DOM_MEMBER_METHOD_NAMES = new Set([
591
+ "blur",
592
+ "focus",
593
+ "getBoundingClientRect",
594
+ "getClientRects",
595
+ "measure",
596
+ "measureInWindow",
597
+ "measureLayout",
598
+ "scroll",
599
+ "scrollBy",
600
+ "scrollIntoView",
601
+ "scrollTo",
602
+ "select",
603
+ "setRangeText",
604
+ "setSelectionRange"
605
+ ]);
582
606
  const STORAGE_OBJECTS = new Set(["localStorage", "sessionStorage"]);
583
607
  //#endregion
584
608
  //#region src/plugin/constants/react.ts
@@ -1749,7 +1773,7 @@ const isImportedFromModule = (contextNode, localIdentifierName, moduleSource) =>
1749
1773
  if (!info) return false;
1750
1774
  return info.source === moduleSource;
1751
1775
  };
1752
- const isNamespaceImportFromModule = (contextNode, localIdentifierName, moduleSource) => {
1776
+ const isNamespaceImportFromModule$1 = (contextNode, localIdentifierName, moduleSource) => {
1753
1777
  const lookup = getImportLookup(contextNode);
1754
1778
  if (!lookup) return false;
1755
1779
  const info = lookup.get(localIdentifierName);
@@ -1835,7 +1859,7 @@ const GENERATED_IMAGE_RENDERER_MODULES = [
1835
1859
  "satori"
1836
1860
  ];
1837
1861
  const IMAGE_RESPONSE_MODULES = new Set(["next/og", "@vercel/og"]);
1838
- const getImportDeclaration$1 = (node) => {
1862
+ const getImportDeclaration = (node) => {
1839
1863
  let current = node.parent;
1840
1864
  while (current) {
1841
1865
  if (isNodeOfType(current, "ImportDeclaration")) return current;
@@ -1849,7 +1873,7 @@ const isNamedImport = (symbol, importedName, moduleSources) => {
1849
1873
  if (symbol.kind !== "import") return false;
1850
1874
  const declaration = symbol.declarationNode;
1851
1875
  if (!isNodeOfType(declaration, "ImportSpecifier")) return false;
1852
- const importDeclaration = getImportDeclaration$1(declaration);
1876
+ const importDeclaration = getImportDeclaration(declaration);
1853
1877
  const source = importDeclaration ? getImportSource(importDeclaration) : null;
1854
1878
  if (!source || !moduleSources.has(source)) return false;
1855
1879
  const imported = declaration.imported;
@@ -1858,7 +1882,7 @@ const isNamedImport = (symbol, importedName, moduleSources) => {
1858
1882
  const isSatoriImport = (symbol) => {
1859
1883
  if (symbol.kind !== "import") return false;
1860
1884
  const declaration = symbol.declarationNode;
1861
- const importDeclaration = getImportDeclaration$1(declaration);
1885
+ const importDeclaration = getImportDeclaration(declaration);
1862
1886
  if (!importDeclaration || getImportSource(importDeclaration) !== "satori") return false;
1863
1887
  if (isNodeOfType(declaration, "ImportDefaultSpecifier")) return true;
1864
1888
  if (!isNodeOfType(declaration, "ImportSpecifier")) return false;
@@ -1879,7 +1903,7 @@ const isGeneratedImageRendererCall = (node, scopes) => {
1879
1903
  if (!symbol || symbol.kind !== "import") return false;
1880
1904
  const declaration = symbol.declarationNode;
1881
1905
  if (!isNodeOfType(declaration, "ImportNamespaceSpecifier")) return false;
1882
- const importDeclaration = getImportDeclaration$1(declaration);
1906
+ const importDeclaration = getImportDeclaration(declaration);
1883
1907
  const source = importDeclaration ? getImportSource(importDeclaration) : null;
1884
1908
  return Boolean(source && IMAGE_RESPONSE_MODULES.has(source));
1885
1909
  };
@@ -2099,6 +2123,386 @@ const isGeneratedImageRenderContext = (context, node) => {
2099
2123
  return false;
2100
2124
  };
2101
2125
  //#endregion
2126
+ //#region src/plugin/utils/find-enclosing-function.ts
2127
+ const findEnclosingFunction$1 = (node) => {
2128
+ let cursor = node.parent;
2129
+ while (cursor) {
2130
+ if (isFunctionLike$1(cursor)) return cursor;
2131
+ cursor = cursor.parent ?? null;
2132
+ }
2133
+ return null;
2134
+ };
2135
+ //#endregion
2136
+ //#region src/plugin/utils/find-transparent-expression-root.ts
2137
+ const findTransparentExpressionRoot = (node) => {
2138
+ let current = node;
2139
+ while (current.parent && TRANSPARENT_EXPRESSION_WRAPPER_TYPES.has(current.parent.type)) current = current.parent;
2140
+ return current;
2141
+ };
2142
+ //#endregion
2143
+ //#region src/plugin/constants/js.ts
2144
+ const LOOP_TYPES = [
2145
+ "ForStatement",
2146
+ "ForInStatement",
2147
+ "ForOfStatement",
2148
+ "WhileStatement",
2149
+ "DoWhileStatement"
2150
+ ];
2151
+ const FUNCTION_LIKE_TYPES = new Set([
2152
+ "FunctionDeclaration",
2153
+ "FunctionExpression",
2154
+ "ArrowFunctionExpression"
2155
+ ]);
2156
+ const BUILTIN_GLOBAL_NAMESPACE_NAMES = new Set([
2157
+ "Math",
2158
+ "Date",
2159
+ "JSON",
2160
+ "Object",
2161
+ "Array",
2162
+ "Number",
2163
+ "String",
2164
+ "Boolean",
2165
+ "RegExp",
2166
+ "Symbol",
2167
+ "BigInt",
2168
+ "Reflect"
2169
+ ]);
2170
+ const MUTATING_ARRAY_METHODS = new Set([
2171
+ "push",
2172
+ "pop",
2173
+ "shift",
2174
+ "unshift",
2175
+ "splice",
2176
+ "sort",
2177
+ "reverse",
2178
+ "fill",
2179
+ "copyWithin"
2180
+ ]);
2181
+ const MUTATING_COLLECTION_METHODS = new Set([
2182
+ "add",
2183
+ "clear",
2184
+ "delete",
2185
+ "set"
2186
+ ]);
2187
+ const CHAINABLE_ITERATION_METHODS = new Set([
2188
+ "map",
2189
+ "filter",
2190
+ "forEach",
2191
+ "flatMap"
2192
+ ]);
2193
+ const ITERATOR_PRODUCING_METHOD_NAMES = new Set([
2194
+ "values",
2195
+ "keys",
2196
+ "entries"
2197
+ ]);
2198
+ const BROWSER_TEST_FILE_PATTERN = /\.browser\.[cm]?[jt]sx?$/;
2199
+ const TEST_LIBRARY_IMPORT_SOURCES = new Set([
2200
+ "vitest",
2201
+ "jest",
2202
+ "mocha",
2203
+ "chai",
2204
+ "sinon",
2205
+ "expect",
2206
+ "ava",
2207
+ "uvu",
2208
+ "node:test",
2209
+ "bun:test",
2210
+ "@testing-library/react",
2211
+ "@testing-library/react-native",
2212
+ "@testing-library/react-hooks",
2213
+ "@testing-library/dom",
2214
+ "@testing-library/user-event",
2215
+ "@testing-library/jest-dom",
2216
+ "@testing-library/vue",
2217
+ "@testing-library/svelte",
2218
+ "@testing-library/preact",
2219
+ "@testing-library/cypress",
2220
+ "playwright",
2221
+ "playwright-core",
2222
+ "@playwright/test",
2223
+ "@playwright/experimental-ct-react",
2224
+ "@playwright/experimental-ct-react17",
2225
+ "cypress",
2226
+ "@cypress/react",
2227
+ "@cypress/react18",
2228
+ "@storybook/test",
2229
+ "@storybook/test-runner",
2230
+ "@storybook/testing-library",
2231
+ "@storybook/jest",
2232
+ "puppeteer",
2233
+ "puppeteer-core",
2234
+ "webdriverio",
2235
+ "@wdio/globals",
2236
+ "@nuxt/test-utils"
2237
+ ]);
2238
+ const TEST_LIBRARY_IMPORT_SOURCE_PREFIXES = [
2239
+ "vitest/",
2240
+ "@vitest/",
2241
+ "@jest/",
2242
+ "@testing-library/",
2243
+ "@playwright/",
2244
+ "@storybook/test/",
2245
+ "@storybook/test-runner/",
2246
+ "@storybook/testing-library/",
2247
+ "@cypress/",
2248
+ "@nuxt/test-utils/"
2249
+ ];
2250
+ const ORDERED_UI_FLOW_CALLEE_NAMES = new Set([
2251
+ "render",
2252
+ "rerender",
2253
+ "renderHook",
2254
+ "renderToString",
2255
+ "renderToStaticMarkup",
2256
+ "act",
2257
+ "click",
2258
+ "dblClick",
2259
+ "dblclick",
2260
+ "tripleClick",
2261
+ "tap",
2262
+ "press",
2263
+ "longPress",
2264
+ "type",
2265
+ "clear",
2266
+ "fill",
2267
+ "focus",
2268
+ "blur",
2269
+ "hover",
2270
+ "unhover",
2271
+ "check",
2272
+ "uncheck",
2273
+ "selectOption",
2274
+ "selectOptions",
2275
+ "setChecked",
2276
+ "setInputFiles",
2277
+ "scrollIntoViewIfNeeded",
2278
+ "dragTo",
2279
+ "dragAndDrop",
2280
+ "drop",
2281
+ "evaluate",
2282
+ "evaluateHandle",
2283
+ "waitFor",
2284
+ "waitForLoadState",
2285
+ "waitForSelector",
2286
+ "waitForURL",
2287
+ "waitForResponse",
2288
+ "waitForRequest",
2289
+ "waitForEvent",
2290
+ "waitForFunction",
2291
+ "waitForElementToBeRemoved",
2292
+ "goto",
2293
+ "goBack",
2294
+ "goForward",
2295
+ "reload",
2296
+ "screenshot",
2297
+ "snapshot",
2298
+ "toMatchSnapshot",
2299
+ "toMatchInlineSnapshot",
2300
+ "expect",
2301
+ "expectTypeOf",
2302
+ "step",
2303
+ "describe",
2304
+ "test",
2305
+ "it",
2306
+ "beforeAll",
2307
+ "beforeEach",
2308
+ "afterAll",
2309
+ "afterEach",
2310
+ "play",
2311
+ "userEvent",
2312
+ "screen",
2313
+ "within"
2314
+ ]);
2315
+ const ORDERED_UI_FLOW_CALLEE_PREFIXES = ["findBy", "findAllBy"];
2316
+ const INTENTIONAL_SEQUENCING_CALLEE_NAMES = new Set([
2317
+ "sleep",
2318
+ "delay",
2319
+ "wait",
2320
+ "pause",
2321
+ "throttle",
2322
+ "debounce",
2323
+ "tick",
2324
+ "nextTick",
2325
+ "advanceTimersByTime",
2326
+ "advanceTimersByTimeAsync",
2327
+ "runAllTimers",
2328
+ "runAllTimersAsync",
2329
+ "runOnlyPendingTimers",
2330
+ "runOnlyPendingTimersAsync",
2331
+ "setTimeout",
2332
+ "setInterval",
2333
+ "setImmediate",
2334
+ "queueMicrotask",
2335
+ "requestAnimationFrame",
2336
+ "requestIdleCallback",
2337
+ "animate",
2338
+ "transition",
2339
+ "spring",
2340
+ "tween",
2341
+ "stagger",
2342
+ "sequence",
2343
+ "timeline",
2344
+ "scrub",
2345
+ "query",
2346
+ "execute",
2347
+ "exec",
2348
+ "raw",
2349
+ "transaction",
2350
+ "$transaction",
2351
+ "$executeRaw",
2352
+ "$queryRaw",
2353
+ "$executeRawUnsafe",
2354
+ "$queryRawUnsafe",
2355
+ "begin",
2356
+ "commit",
2357
+ "rollback",
2358
+ "savepoint",
2359
+ "lock",
2360
+ "unlock",
2361
+ "spawn",
2362
+ "spawnSync",
2363
+ "execSync",
2364
+ "execFile",
2365
+ "execFileSync",
2366
+ "fork",
2367
+ "$",
2368
+ "sh",
2369
+ "mkdir",
2370
+ "rmdir",
2371
+ "rename",
2372
+ "rm",
2373
+ "unlink",
2374
+ "writeFile",
2375
+ "appendFile",
2376
+ "copyFile",
2377
+ "navigate",
2378
+ "goto",
2379
+ "waitForNavigation",
2380
+ "waitForURL",
2381
+ "waitForLoadState",
2382
+ "waitForResponse",
2383
+ "waitForRequest",
2384
+ "waitForSelector",
2385
+ "waitForFunction",
2386
+ "waitForEvent"
2387
+ ]);
2388
+ //#endregion
2389
+ //#region src/plugin/utils/is-test-library-import-source.ts
2390
+ const isTestLibraryImportSource = (source) => {
2391
+ if (typeof source !== "string" || source.length === 0) return false;
2392
+ if (TEST_LIBRARY_IMPORT_SOURCES.has(source)) return true;
2393
+ return TEST_LIBRARY_IMPORT_SOURCE_PREFIXES.some((prefix) => source.startsWith(prefix));
2394
+ };
2395
+ //#endregion
2396
+ //#region src/plugin/utils/is-local-test-scaffold-jsx.ts
2397
+ const TEST_CALLBACK_EXPORT_NAMES = new Set(["it", "test"]);
2398
+ const TEST_CALLBACK_MEMBER_NAMES = new Set([
2399
+ "concurrent",
2400
+ "only",
2401
+ "skip"
2402
+ ]);
2403
+ const TEST_CALLBACK_TABLE_MEMBER_NAME = "each";
2404
+ const TEST_MOCK_METHOD_NAMES = new Set([
2405
+ "doMock",
2406
+ "mock",
2407
+ "unstable_mockModule"
2408
+ ]);
2409
+ const TEST_RUNTIME_EXPORT_NAMES = new Set(["jest", "vi"]);
2410
+ const TEST_RUNTIME_MODULE_SOURCES = new Set([
2411
+ "@jest/globals",
2412
+ "bun:test",
2413
+ "node:test",
2414
+ "vitest"
2415
+ ]);
2416
+ const REACT_MODULE_SOURCES = new Set([
2417
+ "react",
2418
+ "react/jsx-dev-runtime",
2419
+ "react/jsx-runtime"
2420
+ ]);
2421
+ const hasUnitTestFilename = (rawFilename) => {
2422
+ if (!rawFilename) return false;
2423
+ const filename = `/${rawFilename.replaceAll("\\", "/")}`;
2424
+ const basename = filename.slice(filename.lastIndexOf("/") + 1);
2425
+ return basename.includes(".test.") || basename.includes(".spec.") || filename.includes("/__tests__/") || filename.includes("/__test__/") || filename.includes("/__mocks__/");
2426
+ };
2427
+ const isExactImportedBinding = (identifier, expectedExportNames, context) => {
2428
+ if (context.scopes.referenceFor(identifier)?.resolvedSymbol?.kind !== "import") return false;
2429
+ const importBinding = getImportBindingForName(identifier, identifier.name);
2430
+ return Boolean(importBinding && TEST_RUNTIME_MODULE_SOURCES.has(importBinding.source) && importBinding.exportedName && expectedExportNames.has(importBinding.exportedName));
2431
+ };
2432
+ const isRecognizedTestGlobal = (identifier, expectedNames, context) => hasUnitTestFilename(context.filename) && expectedNames.has(identifier.name) && context.scopes.isGlobalReference(identifier);
2433
+ const isRecognizedTestBinding = (identifier, expectedNames, context) => isExactImportedBinding(identifier, expectedNames, context) || isRecognizedTestGlobal(identifier, expectedNames, context);
2434
+ const getTestCallbackBaseIdentifier = (callee) => {
2435
+ const unwrappedCallee = stripParenExpression(callee);
2436
+ if (isNodeOfType(unwrappedCallee, "Identifier")) return unwrappedCallee;
2437
+ if (isNodeOfType(unwrappedCallee, "MemberExpression")) {
2438
+ const memberName = getStaticPropertyName(unwrappedCallee);
2439
+ if (!memberName || !TEST_CALLBACK_MEMBER_NAMES.has(memberName)) return null;
2440
+ return getTestCallbackBaseIdentifier(unwrappedCallee.object);
2441
+ }
2442
+ const tableBuilderCallee = isNodeOfType(unwrappedCallee, "CallExpression") ? stripParenExpression(unwrappedCallee.callee) : isNodeOfType(unwrappedCallee, "TaggedTemplateExpression") ? stripParenExpression(unwrappedCallee.tag) : null;
2443
+ if (!isNodeOfType(tableBuilderCallee, "MemberExpression")) return null;
2444
+ if (getStaticPropertyName(tableBuilderCallee) !== TEST_CALLBACK_TABLE_MEMBER_NAME) return null;
2445
+ return getTestCallbackBaseIdentifier(tableBuilderCallee.object);
2446
+ };
2447
+ const isDirectTestCallback = (functionNode, context) => {
2448
+ const callbackRoot = findTransparentExpressionRoot(functionNode);
2449
+ const callExpression = callbackRoot.parent;
2450
+ if (!callExpression || !isNodeOfType(callExpression, "CallExpression")) return false;
2451
+ if (!callExpression.arguments.some((argument) => argument === callbackRoot)) return false;
2452
+ const baseIdentifier = getTestCallbackBaseIdentifier(callExpression.callee);
2453
+ return Boolean(baseIdentifier && isRecognizedTestBinding(baseIdentifier, TEST_CALLBACK_EXPORT_NAMES, context));
2454
+ };
2455
+ const isRecognizedMockFactoryCall = (callExpression, factoryRoot, context) => {
2456
+ if (callExpression.arguments[1] !== factoryRoot) return false;
2457
+ const moduleSpecifier = callExpression.arguments[0];
2458
+ if (!moduleSpecifier || !isNodeOfType(moduleSpecifier, "Literal")) return false;
2459
+ if (typeof moduleSpecifier.value !== "string") return false;
2460
+ const callee = stripParenExpression(callExpression.callee);
2461
+ if (!isNodeOfType(callee, "MemberExpression")) return false;
2462
+ const methodName = getStaticPropertyName(callee);
2463
+ if (!methodName || !TEST_MOCK_METHOD_NAMES.has(methodName)) return false;
2464
+ const receiver = stripParenExpression(callee.object);
2465
+ return isNodeOfType(receiver, "Identifier") && isRecognizedTestBinding(receiver, TEST_RUNTIME_EXPORT_NAMES, context);
2466
+ };
2467
+ const isInsideRecognizedMockFactory = (node, context) => {
2468
+ let current = node.parent;
2469
+ while (current) {
2470
+ if (isFunctionLike$1(current)) {
2471
+ const factoryRoot = findTransparentExpressionRoot(current);
2472
+ const callExpression = factoryRoot.parent;
2473
+ if (callExpression && isNodeOfType(callExpression, "CallExpression") && isRecognizedMockFactoryCall(callExpression, factoryRoot, context)) return true;
2474
+ }
2475
+ current = current.parent;
2476
+ }
2477
+ return false;
2478
+ };
2479
+ const hasImportedProductComponentAttributeAncestor = (node, enclosingFunction, context) => {
2480
+ let current = node.parent;
2481
+ let attributeAncestor = null;
2482
+ if (current && isNodeOfType(current, "JSXElement") && current.openingElement === node) current = current.parent;
2483
+ while (current && current !== enclosingFunction) {
2484
+ if (isFunctionLike$1(current)) return false;
2485
+ if (isNodeOfType(current, "JSXAttribute")) attributeAncestor = current;
2486
+ if (isNodeOfType(current, "JSXElement")) {
2487
+ const componentName = current.openingElement.name;
2488
+ if (isNodeOfType(componentName, "JSXIdentifier")) {
2489
+ const reference = context.scopes.referenceFor(componentName);
2490
+ const importBinding = getImportBindingForName(componentName, componentName.name);
2491
+ if (reference?.resolvedSymbol?.kind === "import" && importBinding && !REACT_MODULE_SOURCES.has(importBinding.source) && !isTestLibraryImportSource(importBinding.source) && attributeAncestor?.parent === current.openingElement && isNodeOfType(attributeAncestor.name, "JSXIdentifier") && attributeAncestor.name.name !== "children") return true;
2492
+ }
2493
+ attributeAncestor = null;
2494
+ }
2495
+ current = current.parent;
2496
+ }
2497
+ return false;
2498
+ };
2499
+ const isLocalTestScaffoldJsx = (node, context) => {
2500
+ if (isInsideRecognizedMockFactory(node, context)) return true;
2501
+ const enclosingFunction = findEnclosingFunction$1(node);
2502
+ if (!enclosingFunction || !isDirectTestCallback(enclosingFunction, context)) return false;
2503
+ return hasImportedProductComponentAttributeAncestor(node, enclosingFunction, context);
2504
+ };
2505
+ //#endregion
2102
2506
  //#region src/plugin/utils/object-has-accessible-child.ts
2103
2507
  const objectHasAccessibleChild = (jsxElement, settings) => {
2104
2508
  for (const child of jsxElement.children) {
@@ -2267,6 +2671,7 @@ const altText = defineRule({
2267
2671
  const inputImageAliases = new Set(settings["input[type=\"image\"]"] ?? []);
2268
2672
  const fileHasJsxA11ySettings = hasJsxA11ySettings(context.settings);
2269
2673
  return { JSXOpeningElement(node) {
2674
+ if (isLocalTestScaffoldJsx(node, context)) return;
2270
2675
  if (!fileHasJsxA11ySettings && isNodeOfType(node.name, "JSXIdentifier")) {
2271
2676
  const rawName = node.name.name;
2272
2677
  if (rawName !== "img" && rawName !== "object" && rawName !== "area" && rawName.toLowerCase() !== "input" && !imgAliases.has(rawName) && !objectAliases.has(rawName) && !areaAliases.has(rawName) && !inputImageAliases.has(rawName)) return;
@@ -3945,7 +4350,8 @@ const SECRET_VALUE_PATTERNS = [
3945
4350
  ];
3946
4351
  const JWT_LITERAL_VALUE_PATTERN = /\beyJ[A-Za-z0-9_-]{8,}\.eyJ[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{16,}\b/;
3947
4352
  const PUBLIC_ENV_SECRET_NAME_PATTERN = /\b(?:NEXT_PUBLIC|VITE|REACT_APP|EXPO_PUBLIC)_[A-Z0-9_]*(?:SECRET|TOKEN|PASSWORD|PRIVATE|DATABASE_URL|SERVICE_ROLE|AWS_ACCESS_KEY|AWS_SECRET)[A-Z0-9_]*\b/i;
3948
- const FULL_ENV_LEAK_CONTEXT_PATTERN = /\b(?:process\.env|import\.meta\.env|window\.__[A-Z0-9_]*ENV[A-Z0-9_]*__|__[A-Z0-9_]*ENV[A-Z0-9_]*__)\b/;
4353
+ const FULL_ENV_LEAK_CONTEXT_PATTERN = /\b(?:process\s*\.\s*env|import\s*\.\s*meta\s*\.\s*env|window\.__[A-Z0-9_]*ENV[A-Z0-9_]*__|__[A-Z0-9_]*ENV[A-Z0-9_]*__)\b/;
4354
+ const FULL_ENV_LEAK_COMMENT_TRIVIA_PATTERN = /\b(?:(?:process|window)\s*(?:\/[*/]|<!--|-->|\.\s*(?:\/[*/]|<!--|-->))|import\s*(?:\/[*/]|<!--|-->|\.\s*(?:\/[*/]|<!--|-->|meta\s*(?:\/[*/]|<!--|-->|\.\s*(?:\/[*/]|<!--|-->)))))/;
3949
4355
  const FULL_ENV_LEAK_SECRET_NAME_PATTERN = /\b(?:DATABASE_URL|AWS_SECRET_ACCESS_KEY|AWS_ACCESS_KEY_ID|MAILGUN_API_KEY|SALESFORCE_CLIENT_SECRET|OKTA_CLIENT_SECRET|SESSION_SECRET|COOKIE_SECRET|PRIVATE_KEY|SERVICE_ROLE)\b/;
3950
4356
  const TRUSTED_PUBLIC_SECRET_NAME_PATTERN = /(?:SENTRY_DSN|PUBLIC_KEY|PUBLISHABLE|ANON_KEY|POSTHOG_(?:PROJECT_)?TOKEN|POSTHOG_KEY|TLDRAW_LICENSE_KEY|CLERK_PUBLISHABLE_KEY|ALGOLIA_SEARCH_KEY|GC_API_KEY|GOOGLE_MAPS_API_KEY|MAPBOX_TOKEN|MIXPANEL_TOKEN|FACEBOOK_CLIENT_TOKEN|(?:NEXT_PUBLIC|VITE|REACT_APP|EXPO_PUBLIC)_(?:DISABLE|ENABLE|ALLOW|REQUIRE)_)|(?:TOKEN|SECRET|PASSWORD|PRIVATE)_(?:KIND|TYPE|URL|URI|ENDPOINT|HEADER|NAME)$/i;
3951
4357
  const PUBLIC_CLIENT_KEY_PATTERNS = [
@@ -4103,6 +4509,172 @@ const findSuspiciousPublicEnvSecretNamePattern = (content) => {
4103
4509
  //#region src/plugin/rules/security-scan/utils/has-full-env-leak-shape.ts
4104
4510
  const hasFullEnvLeakShape = (content) => FULL_ENV_LEAK_CONTEXT_PATTERN.test(content) && FULL_ENV_LEAK_SECRET_NAME_PATTERN.test(content);
4105
4511
  //#endregion
4512
+ //#region src/plugin/utils/attach-parent-references.ts
4513
+ const attachParentReferences = (root) => {
4514
+ const visit = (node, parent) => {
4515
+ const writableNode = node;
4516
+ writableNode.parent = parent;
4517
+ const nodeRecord = node;
4518
+ for (const key of Object.keys(nodeRecord)) {
4519
+ if (key === "parent") continue;
4520
+ const child = nodeRecord[key];
4521
+ if (Array.isArray(child)) {
4522
+ for (const item of child) if (isAstNode(item)) visit(item, node);
4523
+ } else if (isAstNode(child)) visit(child, node);
4524
+ }
4525
+ };
4526
+ visit(root, null);
4527
+ };
4528
+ //#endregion
4529
+ //#region src/plugin/utils/cross-file-probe-recorder.ts
4530
+ let activeProbeTrace = null;
4531
+ const recordExistenceProbe = (absolutePath) => {
4532
+ activeProbeTrace?.existencePaths.add(absolutePath);
4533
+ };
4534
+ const recordContentProbe = (absolutePath) => {
4535
+ activeProbeTrace?.contentPaths.add(absolutePath);
4536
+ };
4537
+ const isProbeRecorderActive = () => activeProbeTrace !== null;
4538
+ const collectCrossFileProbes = (collect) => {
4539
+ const previousTrace = activeProbeTrace;
4540
+ const trace = {
4541
+ existencePaths: /* @__PURE__ */ new Set(),
4542
+ contentPaths: /* @__PURE__ */ new Set()
4543
+ };
4544
+ activeProbeTrace = trace;
4545
+ try {
4546
+ collect();
4547
+ } finally {
4548
+ activeProbeTrace = previousTrace;
4549
+ }
4550
+ return trace;
4551
+ };
4552
+ //#endregion
4553
+ //#region src/plugin/utils/parse-source-file.ts
4554
+ const FILENAME_TO_LANG = {
4555
+ ".ts": "ts",
4556
+ ".tsx": "tsx",
4557
+ ".js": "js",
4558
+ ".jsx": "jsx",
4559
+ ".mjs": "js",
4560
+ ".cjs": "js",
4561
+ ".mts": "ts",
4562
+ ".cts": "ts"
4563
+ };
4564
+ const resolveLang = (filename) => {
4565
+ return FILENAME_TO_LANG[path.extname(filename).toLowerCase()] ?? "tsx";
4566
+ };
4567
+ const parseSourceText = ({ filename, sourceText, shouldAttachParentReferences = true }) => {
4568
+ try {
4569
+ const result = parseSync(filename, sourceText, {
4570
+ astType: "ts",
4571
+ lang: resolveLang(filename)
4572
+ });
4573
+ if (result.errors.some((parseError) => parseError.severity === "Error")) return null;
4574
+ const parsedProgram = result.program;
4575
+ if (shouldAttachParentReferences) attachParentReferences(parsedProgram);
4576
+ return parsedProgram;
4577
+ } catch {
4578
+ return null;
4579
+ }
4580
+ };
4581
+ const parseCache = /* @__PURE__ */ new Map();
4582
+ const parseSourceFile = (absoluteFilePath) => {
4583
+ if (!(absoluteFilePath.endsWith(".d.ts") || absoluteFilePath.endsWith(".d.mts") || absoluteFilePath.endsWith(".d.cts"))) recordContentProbe(absoluteFilePath);
4584
+ let fileStat;
4585
+ try {
4586
+ fileStat = fs.statSync(absoluteFilePath);
4587
+ } catch {
4588
+ return null;
4589
+ }
4590
+ if (!fileStat.isFile()) return null;
4591
+ if (fileStat.size > 2e6) return null;
4592
+ const cached = parseCache.get(absoluteFilePath);
4593
+ if (cached && cached.mtimeMs === fileStat.mtimeMs && cached.size === fileStat.size) return cached.program;
4594
+ if (absoluteFilePath.endsWith(".d.ts") || absoluteFilePath.endsWith(".d.mts") || absoluteFilePath.endsWith(".d.cts")) {
4595
+ parseCache.set(absoluteFilePath, {
4596
+ mtimeMs: fileStat.mtimeMs,
4597
+ size: fileStat.size,
4598
+ program: null
4599
+ });
4600
+ return null;
4601
+ }
4602
+ let sourceText;
4603
+ try {
4604
+ sourceText = fs.readFileSync(absoluteFilePath, "utf8");
4605
+ } catch {
4606
+ parseCache.set(absoluteFilePath, {
4607
+ mtimeMs: fileStat.mtimeMs,
4608
+ size: fileStat.size,
4609
+ program: null
4610
+ });
4611
+ return null;
4612
+ }
4613
+ const parsedProgram = parseSourceText({
4614
+ filename: absoluteFilePath,
4615
+ sourceText
4616
+ });
4617
+ parseCache.set(absoluteFilePath, {
4618
+ mtimeMs: fileStat.mtimeMs,
4619
+ size: fileStat.size,
4620
+ program: parsedProgram
4621
+ });
4622
+ return parsedProgram;
4623
+ };
4624
+ //#endregion
4625
+ //#region src/plugin/rules/security-scan/utils/mask-source-comments.ts
4626
+ const SOURCE_FILE_EXTENSION_PATTERN$1 = /\.(?:[cm]?[jt]sx?)$/i;
4627
+ const POSSIBLE_SOURCE_COMMENT_PATTERN = /\/\/|\/\*|<!--/;
4628
+ const LINE_TERMINATORS = new Set([
4629
+ "\r",
4630
+ "\n",
4631
+ "\u2028",
4632
+ "\u2029"
4633
+ ]);
4634
+ const hasPossibleAnnexBClosingComment = (content) => {
4635
+ let searchIndex = 0;
4636
+ while (searchIndex < content.length) {
4637
+ const closingCommentIndex = content.indexOf("-->", searchIndex);
4638
+ if (closingCommentIndex === -1) return false;
4639
+ let prefixIndex = closingCommentIndex - 1;
4640
+ while (prefixIndex >= 0 && !LINE_TERMINATORS.has(content[prefixIndex] ?? "")) {
4641
+ if (content[prefixIndex]?.trim() !== "") break;
4642
+ prefixIndex -= 1;
4643
+ }
4644
+ if (prefixIndex < 0 || LINE_TERMINATORS.has(content[prefixIndex] ?? "")) return true;
4645
+ searchIndex = closingCommentIndex + 3;
4646
+ }
4647
+ return false;
4648
+ };
4649
+ const maskSourceComments = (relativePath, content) => {
4650
+ if (!SOURCE_FILE_EXTENSION_PATTERN$1.test(relativePath)) return content;
4651
+ if (!content.startsWith("#!") && !POSSIBLE_SOURCE_COMMENT_PATTERN.test(content) && !hasPossibleAnnexBClosingComment(content)) return content;
4652
+ try {
4653
+ const result = parseSync(relativePath, content, {
4654
+ astType: "ts",
4655
+ lang: resolveLang(relativePath)
4656
+ });
4657
+ if (result.errors.some((parseError) => parseError.severity === "Error")) return void 0;
4658
+ const firstLineTerminatorIndex = content.search(/[\r\n\u2028\u2029]/);
4659
+ const ignoredRanges = [...content.startsWith("#!") ? [{
4660
+ start: 0,
4661
+ end: firstLineTerminatorIndex === -1 ? content.length : firstLineTerminatorIndex
4662
+ }] : [], ...result.comments];
4663
+ if (ignoredRanges.length === 0) return content;
4664
+ const contentParts = [];
4665
+ let previousEnd = 0;
4666
+ for (const ignoredRange of ignoredRanges) {
4667
+ contentParts.push(content.slice(previousEnd, ignoredRange.start));
4668
+ contentParts.push(content.slice(ignoredRange.start, ignoredRange.end).replace(/[^\r\n\u2028\u2029]/g, " "));
4669
+ previousEnd = ignoredRange.end;
4670
+ }
4671
+ contentParts.push(content.slice(previousEnd));
4672
+ return contentParts.join("");
4673
+ } catch {
4674
+ return;
4675
+ }
4676
+ };
4677
+ //#endregion
4106
4678
  //#region src/plugin/rules/security-scan/utils/scan-artifact-leak.ts
4107
4679
  const scanArtifactLeak = (file, findLeakPattern, message) => {
4108
4680
  if (DOCUMENTATION_CONTEXT_PATTERN.test(file.relativePath)) return [];
@@ -4118,12 +4690,39 @@ const scanArtifactLeak = (file, findLeakPattern, message) => {
4118
4690
  };
4119
4691
  //#endregion
4120
4692
  //#region src/plugin/rules/security-scan/artifact-env-leak.ts
4693
+ const ARTIFACT_ENV_LEAK_MESSAGE = "A browser artifact contains server-secret environment names or a full environment dump shape.";
4694
+ const findArtifactEnvLeakPattern = (content) => findSuspiciousPublicEnvSecretNamePattern(content) ?? (hasFullEnvLeakShape(content) ? FULL_ENV_LEAK_SECRET_NAME_PATTERN : void 0);
4121
4695
  const artifactEnvLeak = defineRule({
4122
4696
  id: "artifact-env-leak",
4123
4697
  title: "Server env leaked to browser artifact",
4124
4698
  severity: "error",
4125
4699
  recommendation: "Treat public env prefixes as publication, not secrecy; keep secret env vars server-only and rebuild after rotating leaked keys.",
4126
- scan: (file) => scanArtifactLeak(file, (content) => findSuspiciousPublicEnvSecretNamePattern(content) ?? (hasFullEnvLeakShape(content) ? FULL_ENV_LEAK_SECRET_NAME_PATTERN : void 0), "A browser artifact contains server-secret environment names or a full environment dump shape.")
4700
+ scan: (file) => {
4701
+ let isRawCandidateExact = false;
4702
+ const findRawCandidatePattern = (content) => {
4703
+ const suspiciousPublicNamePattern = findSuspiciousPublicEnvSecretNamePattern(content);
4704
+ if (suspiciousPublicNamePattern) {
4705
+ isRawCandidateExact = true;
4706
+ return suspiciousPublicNamePattern;
4707
+ }
4708
+ if (!FULL_ENV_LEAK_SECRET_NAME_PATTERN.test(content)) return void 0;
4709
+ if (FULL_ENV_LEAK_CONTEXT_PATTERN.test(content)) {
4710
+ isRawCandidateExact = true;
4711
+ return FULL_ENV_LEAK_SECRET_NAME_PATTERN;
4712
+ }
4713
+ return FULL_ENV_LEAK_COMMENT_TRIVIA_PATTERN.test(content) ? FULL_ENV_LEAK_SECRET_NAME_PATTERN : void 0;
4714
+ };
4715
+ const rawCandidateFindings = scanArtifactLeak(file, findRawCandidatePattern, ARTIFACT_ENV_LEAK_MESSAGE);
4716
+ if (rawCandidateFindings.length === 0) return rawCandidateFindings;
4717
+ const rawFindings = isRawCandidateExact ? rawCandidateFindings : scanArtifactLeak(file, findArtifactEnvLeakPattern, ARTIFACT_ENV_LEAK_MESSAGE);
4718
+ const executableContent = maskSourceComments(file.relativePath, file.content);
4719
+ if (executableContent === void 0) return rawCandidateFindings;
4720
+ if (executableContent === file.content) return rawFindings;
4721
+ return scanArtifactLeak({
4722
+ ...file,
4723
+ content: executableContent
4724
+ }, findArtifactEnvLeakPattern, ARTIFACT_ENV_LEAK_MESSAGE);
4725
+ }
4127
4726
  });
4128
4727
  //#endregion
4129
4728
  //#region src/plugin/rules/security-scan/artifact-secret-leak.ts
@@ -4135,252 +4734,6 @@ const artifactSecretLeak = defineRule({
4135
4734
  scan: (file) => scanArtifactLeak(file, (content) => SECRET_VALUE_PATTERNS.find((pattern) => pattern.test(content)), "A browser-delivered artifact contains a secret-looking credential value.")
4136
4735
  });
4137
4736
  //#endregion
4138
- //#region src/plugin/constants/js.ts
4139
- const LOOP_TYPES = [
4140
- "ForStatement",
4141
- "ForInStatement",
4142
- "ForOfStatement",
4143
- "WhileStatement",
4144
- "DoWhileStatement"
4145
- ];
4146
- const FUNCTION_LIKE_TYPES = new Set([
4147
- "FunctionDeclaration",
4148
- "FunctionExpression",
4149
- "ArrowFunctionExpression"
4150
- ]);
4151
- const BUILTIN_GLOBAL_NAMESPACE_NAMES = new Set([
4152
- "Math",
4153
- "Date",
4154
- "JSON",
4155
- "Object",
4156
- "Array",
4157
- "Number",
4158
- "String",
4159
- "Boolean",
4160
- "RegExp",
4161
- "Symbol",
4162
- "BigInt",
4163
- "Reflect"
4164
- ]);
4165
- const MUTATING_ARRAY_METHODS = new Set([
4166
- "push",
4167
- "pop",
4168
- "shift",
4169
- "unshift",
4170
- "splice",
4171
- "sort",
4172
- "reverse",
4173
- "fill",
4174
- "copyWithin"
4175
- ]);
4176
- const MUTATING_COLLECTION_METHODS = new Set([
4177
- "add",
4178
- "clear",
4179
- "delete",
4180
- "set"
4181
- ]);
4182
- const CHAINABLE_ITERATION_METHODS = new Set([
4183
- "map",
4184
- "filter",
4185
- "forEach",
4186
- "flatMap"
4187
- ]);
4188
- const ITERATOR_PRODUCING_METHOD_NAMES = new Set([
4189
- "values",
4190
- "keys",
4191
- "entries"
4192
- ]);
4193
- const BROWSER_TEST_FILE_PATTERN = /\.browser\.[cm]?[jt]sx?$/;
4194
- const TEST_LIBRARY_IMPORT_SOURCES = new Set([
4195
- "vitest",
4196
- "jest",
4197
- "mocha",
4198
- "chai",
4199
- "sinon",
4200
- "expect",
4201
- "ava",
4202
- "uvu",
4203
- "node:test",
4204
- "bun:test",
4205
- "@testing-library/react",
4206
- "@testing-library/react-native",
4207
- "@testing-library/react-hooks",
4208
- "@testing-library/dom",
4209
- "@testing-library/user-event",
4210
- "@testing-library/jest-dom",
4211
- "@testing-library/vue",
4212
- "@testing-library/svelte",
4213
- "@testing-library/preact",
4214
- "@testing-library/cypress",
4215
- "playwright",
4216
- "playwright-core",
4217
- "@playwright/test",
4218
- "@playwright/experimental-ct-react",
4219
- "@playwright/experimental-ct-react17",
4220
- "cypress",
4221
- "@cypress/react",
4222
- "@cypress/react18",
4223
- "@storybook/test",
4224
- "@storybook/test-runner",
4225
- "@storybook/testing-library",
4226
- "@storybook/jest",
4227
- "puppeteer",
4228
- "puppeteer-core",
4229
- "webdriverio",
4230
- "@wdio/globals",
4231
- "@nuxt/test-utils"
4232
- ]);
4233
- const TEST_LIBRARY_IMPORT_SOURCE_PREFIXES = [
4234
- "vitest/",
4235
- "@vitest/",
4236
- "@jest/",
4237
- "@testing-library/",
4238
- "@playwright/",
4239
- "@storybook/test/",
4240
- "@storybook/test-runner/",
4241
- "@storybook/testing-library/",
4242
- "@cypress/",
4243
- "@nuxt/test-utils/"
4244
- ];
4245
- const ORDERED_UI_FLOW_CALLEE_NAMES = new Set([
4246
- "render",
4247
- "rerender",
4248
- "renderHook",
4249
- "renderToString",
4250
- "renderToStaticMarkup",
4251
- "act",
4252
- "click",
4253
- "dblClick",
4254
- "dblclick",
4255
- "tripleClick",
4256
- "tap",
4257
- "press",
4258
- "longPress",
4259
- "type",
4260
- "clear",
4261
- "fill",
4262
- "focus",
4263
- "blur",
4264
- "hover",
4265
- "unhover",
4266
- "check",
4267
- "uncheck",
4268
- "selectOption",
4269
- "selectOptions",
4270
- "setChecked",
4271
- "setInputFiles",
4272
- "scrollIntoViewIfNeeded",
4273
- "dragTo",
4274
- "dragAndDrop",
4275
- "drop",
4276
- "evaluate",
4277
- "evaluateHandle",
4278
- "waitFor",
4279
- "waitForLoadState",
4280
- "waitForSelector",
4281
- "waitForURL",
4282
- "waitForResponse",
4283
- "waitForRequest",
4284
- "waitForEvent",
4285
- "waitForFunction",
4286
- "waitForElementToBeRemoved",
4287
- "goto",
4288
- "goBack",
4289
- "goForward",
4290
- "reload",
4291
- "screenshot",
4292
- "snapshot",
4293
- "toMatchSnapshot",
4294
- "toMatchInlineSnapshot",
4295
- "expect",
4296
- "expectTypeOf",
4297
- "step",
4298
- "describe",
4299
- "test",
4300
- "it",
4301
- "beforeAll",
4302
- "beforeEach",
4303
- "afterAll",
4304
- "afterEach",
4305
- "play",
4306
- "userEvent",
4307
- "screen",
4308
- "within"
4309
- ]);
4310
- const ORDERED_UI_FLOW_CALLEE_PREFIXES = ["findBy", "findAllBy"];
4311
- const INTENTIONAL_SEQUENCING_CALLEE_NAMES = new Set([
4312
- "sleep",
4313
- "delay",
4314
- "wait",
4315
- "pause",
4316
- "throttle",
4317
- "debounce",
4318
- "tick",
4319
- "nextTick",
4320
- "advanceTimersByTime",
4321
- "advanceTimersByTimeAsync",
4322
- "runAllTimers",
4323
- "runAllTimersAsync",
4324
- "runOnlyPendingTimers",
4325
- "runOnlyPendingTimersAsync",
4326
- "setTimeout",
4327
- "setInterval",
4328
- "setImmediate",
4329
- "queueMicrotask",
4330
- "requestAnimationFrame",
4331
- "requestIdleCallback",
4332
- "animate",
4333
- "transition",
4334
- "spring",
4335
- "tween",
4336
- "stagger",
4337
- "sequence",
4338
- "timeline",
4339
- "scrub",
4340
- "query",
4341
- "execute",
4342
- "exec",
4343
- "raw",
4344
- "transaction",
4345
- "$transaction",
4346
- "$executeRaw",
4347
- "$queryRaw",
4348
- "$executeRawUnsafe",
4349
- "$queryRawUnsafe",
4350
- "begin",
4351
- "commit",
4352
- "rollback",
4353
- "savepoint",
4354
- "lock",
4355
- "unlock",
4356
- "spawn",
4357
- "spawnSync",
4358
- "execSync",
4359
- "execFile",
4360
- "execFileSync",
4361
- "fork",
4362
- "$",
4363
- "sh",
4364
- "mkdir",
4365
- "rmdir",
4366
- "rename",
4367
- "rm",
4368
- "unlink",
4369
- "writeFile",
4370
- "appendFile",
4371
- "copyFile",
4372
- "navigate",
4373
- "goto",
4374
- "waitForNavigation",
4375
- "waitForURL",
4376
- "waitForLoadState",
4377
- "waitForResponse",
4378
- "waitForRequest",
4379
- "waitForSelector",
4380
- "waitForFunction",
4381
- "waitForEvent"
4382
- ]);
4383
- //#endregion
4384
4737
  //#region src/plugin/constants/ts-type-position-keys.ts
4385
4738
  const TYPE_POSITION_CHILD_KEYS = new Set([
4386
4739
  "implements",
@@ -4506,13 +4859,6 @@ const containsDirectAwait = (node) => {
4506
4859
  return foundAwait;
4507
4860
  };
4508
4861
  //#endregion
4509
- //#region src/plugin/utils/find-transparent-expression-root.ts
4510
- const findTransparentExpressionRoot = (node) => {
4511
- let current = node;
4512
- while (current.parent && TRANSPARENT_EXPRESSION_WRAPPER_TYPES.has(current.parent.type)) current = current.parent;
4513
- return current;
4514
- };
4515
- //#endregion
4516
4862
  //#region src/plugin/utils/get-static-property-key-name.ts
4517
4863
  const getStaticPropertyKeyName = (node, options = {}) => {
4518
4864
  if (!isNodeOfType(node, "Property") && !isNodeOfType(node, "MethodDefinition") && !isNodeOfType(node, "MemberExpression")) return null;
@@ -4772,16 +5118,6 @@ const hasStaticPropertyWriteBefore = (identifier, propertyName, referenceNode, s
4772
5118
  return getEquivalentSymbols(identifier, scopes).some((symbol) => symbolHasStaticPropertyWriteBefore(symbol, propertyName, referenceNode, scopes));
4773
5119
  };
4774
5120
  //#endregion
4775
- //#region src/plugin/utils/find-enclosing-function.ts
4776
- const findEnclosingFunction$1 = (node) => {
4777
- let cursor = node.parent;
4778
- while (cursor) {
4779
- if (isFunctionLike$1(cursor)) return cursor;
4780
- cursor = cursor.parent ?? null;
4781
- }
4782
- return null;
4783
- };
4784
- //#endregion
4785
5121
  //#region src/plugin/utils/has-symbol-write-before.ts
4786
5122
  const hasSymbolWriteBefore = (symbol, referenceNode, scopes) => symbol.references.some((reference) => {
4787
5123
  if (reference.flag === "read") return false;
@@ -6283,13 +6619,6 @@ const getCalleeIdentifierTrail = (call) => {
6283
6619
  return trail;
6284
6620
  };
6285
6621
  //#endregion
6286
- //#region src/plugin/utils/is-test-library-import-source.ts
6287
- const isTestLibraryImportSource = (source) => {
6288
- if (typeof source !== "string" || source.length === 0) return false;
6289
- if (TEST_LIBRARY_IMPORT_SOURCES.has(source)) return true;
6290
- return TEST_LIBRARY_IMPORT_SOURCE_PREFIXES.some((prefix) => source.startsWith(prefix));
6291
- };
6292
- //#endregion
6293
6622
  //#region src/plugin/rules/js-performance/async-parallel.ts
6294
6623
  const getAwaitedCall = (statement) => {
6295
6624
  if (isNodeOfType(statement, "VariableDeclaration")) {
@@ -8251,7 +8580,27 @@ const collectFunctionReturnStatements = (functionNode) => {
8251
8580
  //#region src/plugin/utils/statement-always-exits.ts
8252
8581
  const statementAlwaysExits = (statement) => {
8253
8582
  if (isNodeOfType(statement, "ReturnStatement") || isNodeOfType(statement, "ThrowStatement")) return true;
8254
- if (isNodeOfType(statement, "IfStatement")) return Boolean(statement.alternate && statementAlwaysExits(statement.consequent) && statementAlwaysExits(statement.alternate));
8583
+ if (isNodeOfType(statement, "IfStatement")) {
8584
+ if (isNodeOfType(statement.test, "Literal")) {
8585
+ const reachableBranch = statement.test.value ? statement.consequent : statement.alternate;
8586
+ return reachableBranch ? statementAlwaysExits(reachableBranch) : false;
8587
+ }
8588
+ return Boolean(statement.alternate && statementAlwaysExits(statement.consequent) && statementAlwaysExits(statement.alternate));
8589
+ }
8590
+ if (isNodeOfType(statement, "TryStatement")) {
8591
+ if (statement.finalizer && statementAlwaysExits(statement.finalizer)) return true;
8592
+ if (!statementAlwaysExits(statement.block)) return false;
8593
+ return statement.handler ? statementAlwaysExits(statement.handler.body) : true;
8594
+ }
8595
+ if (isNodeOfType(statement, "DoWhileStatement")) return statementAlwaysExits(statement.body);
8596
+ if (isNodeOfType(statement, "WhileStatement")) {
8597
+ const whileStatementTest = statement.test;
8598
+ return Boolean(isNodeOfType(whileStatementTest, "Literal") && whileStatementTest.value && statementAlwaysExits(statement.body));
8599
+ }
8600
+ if (isNodeOfType(statement, "ForStatement")) {
8601
+ const forStatementTest = statement.test;
8602
+ return Boolean((!forStatementTest || isNodeOfType(forStatementTest, "Literal") && forStatementTest.value) && statementAlwaysExits(statement.body));
8603
+ }
8255
8604
  if (!isNodeOfType(statement, "BlockStatement")) return false;
8256
8605
  return statement.body.some((childStatement) => statementAlwaysExits(childStatement));
8257
8606
  };
@@ -8936,119 +9285,6 @@ const findReExportTargetsForName = (programRoot, exportedName) => {
8936
9285
  return exportAllTargets;
8937
9286
  };
8938
9287
  //#endregion
8939
- //#region src/plugin/utils/attach-parent-references.ts
8940
- const attachParentReferences = (root) => {
8941
- const visit = (node, parent) => {
8942
- const writableNode = node;
8943
- writableNode.parent = parent;
8944
- const nodeRecord = node;
8945
- for (const key of Object.keys(nodeRecord)) {
8946
- if (key === "parent") continue;
8947
- const child = nodeRecord[key];
8948
- if (Array.isArray(child)) {
8949
- for (const item of child) if (isAstNode(item)) visit(item, node);
8950
- } else if (isAstNode(child)) visit(child, node);
8951
- }
8952
- };
8953
- visit(root, null);
8954
- };
8955
- //#endregion
8956
- //#region src/plugin/utils/cross-file-probe-recorder.ts
8957
- let activeProbeTrace = null;
8958
- const recordExistenceProbe = (absolutePath) => {
8959
- activeProbeTrace?.existencePaths.add(absolutePath);
8960
- };
8961
- const recordContentProbe = (absolutePath) => {
8962
- activeProbeTrace?.contentPaths.add(absolutePath);
8963
- };
8964
- const isProbeRecorderActive = () => activeProbeTrace !== null;
8965
- const collectCrossFileProbes = (collect) => {
8966
- const previousTrace = activeProbeTrace;
8967
- const trace = {
8968
- existencePaths: /* @__PURE__ */ new Set(),
8969
- contentPaths: /* @__PURE__ */ new Set()
8970
- };
8971
- activeProbeTrace = trace;
8972
- try {
8973
- collect();
8974
- } finally {
8975
- activeProbeTrace = previousTrace;
8976
- }
8977
- return trace;
8978
- };
8979
- //#endregion
8980
- //#region src/plugin/utils/parse-source-file.ts
8981
- const FILENAME_TO_LANG = {
8982
- ".ts": "ts",
8983
- ".tsx": "tsx",
8984
- ".js": "js",
8985
- ".jsx": "jsx",
8986
- ".mjs": "js",
8987
- ".cjs": "js",
8988
- ".mts": "ts",
8989
- ".cts": "ts"
8990
- };
8991
- const resolveLang = (filename) => {
8992
- return FILENAME_TO_LANG[path.extname(filename).toLowerCase()] ?? "tsx";
8993
- };
8994
- const parseSourceText = ({ filename, sourceText, shouldAttachParentReferences = true }) => {
8995
- try {
8996
- const result = parseSync(filename, sourceText, {
8997
- astType: "ts",
8998
- lang: resolveLang(filename)
8999
- });
9000
- if (result.errors.some((parseError) => parseError.severity === "Error")) return null;
9001
- const parsedProgram = result.program;
9002
- if (shouldAttachParentReferences) attachParentReferences(parsedProgram);
9003
- return parsedProgram;
9004
- } catch {
9005
- return null;
9006
- }
9007
- };
9008
- const parseCache = /* @__PURE__ */ new Map();
9009
- const parseSourceFile = (absoluteFilePath) => {
9010
- if (!(absoluteFilePath.endsWith(".d.ts") || absoluteFilePath.endsWith(".d.mts") || absoluteFilePath.endsWith(".d.cts"))) recordContentProbe(absoluteFilePath);
9011
- let fileStat;
9012
- try {
9013
- fileStat = fs.statSync(absoluteFilePath);
9014
- } catch {
9015
- return null;
9016
- }
9017
- if (!fileStat.isFile()) return null;
9018
- if (fileStat.size > 2e6) return null;
9019
- const cached = parseCache.get(absoluteFilePath);
9020
- if (cached && cached.mtimeMs === fileStat.mtimeMs && cached.size === fileStat.size) return cached.program;
9021
- if (absoluteFilePath.endsWith(".d.ts") || absoluteFilePath.endsWith(".d.mts") || absoluteFilePath.endsWith(".d.cts")) {
9022
- parseCache.set(absoluteFilePath, {
9023
- mtimeMs: fileStat.mtimeMs,
9024
- size: fileStat.size,
9025
- program: null
9026
- });
9027
- return null;
9028
- }
9029
- let sourceText;
9030
- try {
9031
- sourceText = fs.readFileSync(absoluteFilePath, "utf8");
9032
- } catch {
9033
- parseCache.set(absoluteFilePath, {
9034
- mtimeMs: fileStat.mtimeMs,
9035
- size: fileStat.size,
9036
- program: null
9037
- });
9038
- return null;
9039
- }
9040
- const parsedProgram = parseSourceText({
9041
- filename: absoluteFilePath,
9042
- sourceText
9043
- });
9044
- parseCache.set(absoluteFilePath, {
9045
- mtimeMs: fileStat.mtimeMs,
9046
- size: fileStat.size,
9047
- program: parsedProgram
9048
- });
9049
- return parsedProgram;
9050
- };
9051
- //#endregion
9052
9288
  //#region src/plugin/utils/resolve-relative-import-path.ts
9053
9289
  const MODULE_FILE_EXTENSIONS = [
9054
9290
  ".ts",
@@ -10370,10 +10606,10 @@ const controlHasAssociatedLabel = defineRule({
10370
10606
  if (isTestlikeFile) return;
10371
10607
  const opening = node.openingElement;
10372
10608
  const tagName = getElementType(opening, context.settings);
10373
- if (tagName === LABEL_ELEMENT && hasAccessibleLabelText(node, checkContext) && !isInsideJsxAttribute(node)) {
10609
+ if (rendersLabelElement(tagName, opening) && hasAccessibleLabelText(node, checkContext) && !isInsideJsxAttribute(node)) {
10374
10610
  const htmlForAttribute = hasJsxPropIgnoreCase(opening.attributes, HTML_FOR_ATTRIBUTE);
10375
10611
  for (const htmlForKey of getAttributeMatchKeys(htmlForAttribute)) labelHtmlForKeys.add(htmlForKey);
10376
- collectLabelEmbeddedNames(node, 1, checkContext, labelEmbeddedNames);
10612
+ if (tagName === LABEL_ELEMENT) collectLabelEmbeddedNames(node, 1, checkContext, labelEmbeddedNames);
10377
10613
  }
10378
10614
  if (DEFAULT_IGNORE_ELEMENTS.includes(tagName)) return;
10379
10615
  if (settings.ignoreElements.includes(tagName)) return;
@@ -10459,6 +10695,1048 @@ const findMatchingBracket = (content, openIndex) => {
10459
10695
  return -1;
10460
10696
  };
10461
10697
  //#endregion
10698
+ //#region src/plugin/utils/get-node-end-index.ts
10699
+ const getNodeEndIndex = (node) => "end" in node && typeof node.end === "number" ? node.end : -1;
10700
+ //#endregion
10701
+ //#region src/plugin/utils/get-node-start-index.ts
10702
+ const getNodeStartIndex = (node) => "start" in node && typeof node.start === "number" ? node.start : -1;
10703
+ //#endregion
10704
+ //#region src/plugin/utils/get-import-declaration-for-symbol.ts
10705
+ const getImportDeclarationForSymbol = (symbol) => {
10706
+ if (symbol.kind !== "import") return null;
10707
+ const importDeclaration = symbol.declarationNode.parent;
10708
+ return isNodeOfType(importDeclaration, "ImportDeclaration") ? importDeclaration : null;
10709
+ };
10710
+ //#endregion
10711
+ //#region src/plugin/constants/mutation-methods.ts
10712
+ const OBJECT_PROPERTY_MUTATION_METHOD_NAMES = new Set([
10713
+ "assign",
10714
+ "defineProperties",
10715
+ "defineProperty"
10716
+ ]);
10717
+ const REFLECT_PROPERTY_MUTATION_METHOD_NAMES = new Set(["defineProperty", "set"]);
10718
+ //#endregion
10719
+ //#region src/plugin/rules/security-scan/utils/get-symbol-mutation-inspector.ts
10720
+ const inspectorCache = /* @__PURE__ */ new WeakMap();
10721
+ const getOutermostTarget = (node) => {
10722
+ let current = findTransparentExpressionRoot(node);
10723
+ while (current.parent) {
10724
+ const parent = current.parent;
10725
+ if (!isNodeOfType(parent, "MemberExpression") || parent.object !== current) break;
10726
+ current = findTransparentExpressionRoot(parent);
10727
+ }
10728
+ return current;
10729
+ };
10730
+ const getExecutionOwner = (node) => {
10731
+ let current = node;
10732
+ while (current) {
10733
+ if (isFunctionLike$1(current) || isNodeOfType(current, "Program")) return current;
10734
+ current = current.parent;
10735
+ }
10736
+ return node;
10737
+ };
10738
+ const isAbruptCompletionStatement = (node, includesContinue) => {
10739
+ if (isNodeOfType(node, "ReturnStatement") || isNodeOfType(node, "ThrowStatement") || isNodeOfType(node, "BreakStatement") || includesContinue && isNodeOfType(node, "ContinueStatement")) return true;
10740
+ if (isNodeOfType(node, "BlockStatement")) return node.body.some((statement) => isAbruptCompletionStatement(statement, includesContinue));
10741
+ if (!isNodeOfType(node, "IfStatement")) return false;
10742
+ if (isNodeOfType(node.test, "Literal")) {
10743
+ const reachableBranch = node.test.value ? node.consequent : node.alternate;
10744
+ return reachableBranch ? isAbruptCompletionStatement(reachableBranch, includesContinue) : false;
10745
+ }
10746
+ return Boolean(node.alternate && isAbruptCompletionStatement(node.consequent, includesContinue) && isAbruptCompletionStatement(node.alternate, includesContinue));
10747
+ };
10748
+ const isTerminalStatement = (node) => isAbruptCompletionStatement(node, true);
10749
+ const isAfterTerminalStatement = (node, statements) => {
10750
+ const statementIndex = statements.indexOf(node);
10751
+ return statementIndex > 0 && statements.slice(0, statementIndex).some(isTerminalStatement);
10752
+ };
10753
+ const isStaticallyUnreachable = (node, owner) => {
10754
+ let current = node;
10755
+ while (current.parent && current !== owner) {
10756
+ const parent = current.parent;
10757
+ if ((isNodeOfType(parent, "BlockStatement") || isNodeOfType(parent, "Program")) && isAfterTerminalStatement(current, parent.body)) return true;
10758
+ if ((isNodeOfType(parent, "WhileStatement") && isNodeOfType(parent.test, "Literal") && !parent.test.value || isNodeOfType(parent, "ForStatement") && parent.test && isNodeOfType(parent.test, "Literal") && !parent.test.value) && parent.body === current) return true;
10759
+ if (isNodeOfType(parent, "SwitchCase") && isAfterTerminalStatement(current, parent.consequent)) return true;
10760
+ if (isNodeOfType(parent, "IfStatement") && isNodeOfType(parent.test, "Literal")) {
10761
+ if (parent.test.value === false && parent.consequent === current) return true;
10762
+ if (parent.test.value === true && parent.alternate === current) return true;
10763
+ }
10764
+ if (isNodeOfType(parent, "ConditionalExpression") && isNodeOfType(parent.test, "Literal")) {
10765
+ if (parent.test.value === false && parent.consequent === current) return true;
10766
+ if (parent.test.value === true && parent.alternate === current) return true;
10767
+ }
10768
+ if (isNodeOfType(parent, "LogicalExpression") && parent.right === current && isNodeOfType(parent.left, "Literal")) {
10769
+ if (parent.operator === "&&" && !parent.left.value) return true;
10770
+ if (parent.operator === "||" && Boolean(parent.left.value)) return true;
10771
+ }
10772
+ current = parent;
10773
+ }
10774
+ return false;
10775
+ };
10776
+ const isConditionallyExecuted = (node, owner) => {
10777
+ let current = node;
10778
+ while (current.parent && current !== owner) {
10779
+ const parent = current.parent;
10780
+ if (isNodeOfType(parent, "IfStatement")) {
10781
+ if (!isNodeOfType(parent.test, "Literal")) return true;
10782
+ if (parent.test.value === true && parent.alternate === current) return true;
10783
+ if (parent.test.value === false && parent.consequent === current) return true;
10784
+ }
10785
+ if (isNodeOfType(parent, "ConditionalExpression")) {
10786
+ if (!isNodeOfType(parent.test, "Literal")) return true;
10787
+ if (parent.test.value === true && parent.alternate === current) return true;
10788
+ if (parent.test.value === false && parent.consequent === current) return true;
10789
+ }
10790
+ if (isNodeOfType(parent, "LogicalExpression") && parent.right === current) {
10791
+ if (!isNodeOfType(parent.left, "Literal")) return true;
10792
+ if (parent.operator === "&&" && !parent.left.value) return true;
10793
+ if (parent.operator === "||" && Boolean(parent.left.value)) return true;
10794
+ }
10795
+ if (isNodeOfType(parent, "DoWhileStatement")) {
10796
+ if (!(parent.body === current && isNodeOfType(parent.test, "Literal") && !parent.test.value)) return true;
10797
+ }
10798
+ if (isNodeOfType(parent, "TryStatement") && parent.block === current) return true;
10799
+ if (isNodeOfType(parent, "ForStatement") || isNodeOfType(parent, "ForInStatement") || isNodeOfType(parent, "ForOfStatement") || isNodeOfType(parent, "WhileStatement") || isNodeOfType(parent, "SwitchCase") || isNodeOfType(parent, "CatchClause")) return true;
10800
+ if ((isNodeOfType(parent, "CallExpression") || isNodeOfType(parent, "MemberExpression")) && parent.optional) return true;
10801
+ current = parent;
10802
+ }
10803
+ return false;
10804
+ };
10805
+ const getSymbolMutationInspector = (scopes) => {
10806
+ const cached = inspectorCache.get(scopes);
10807
+ if (cached) return cached;
10808
+ const isGlobalNamespaceMethod = (node, namespaceName, methodNames) => {
10809
+ const callee = stripParenExpression(node);
10810
+ if (!isNodeOfType(callee, "MemberExpression")) return false;
10811
+ const receiver = stripParenExpression(callee.object);
10812
+ return Boolean(isNodeOfType(receiver, "Identifier") && receiver.name === namespaceName && scopes.isGlobalReference(receiver) && methodNames.has(getStaticPropertyName(callee) ?? ""));
10813
+ };
10814
+ const getObjectExpressionPropertyNames = (node) => {
10815
+ const expression = stripParenExpression(node);
10816
+ if (!isNodeOfType(expression, "ObjectExpression")) return null;
10817
+ const propertyNames = /* @__PURE__ */ new Set();
10818
+ for (const property of expression.properties) {
10819
+ if (!isNodeOfType(property, "Property")) return null;
10820
+ const propertyName = getStaticPropertyKeyName(property, { allowComputedString: true });
10821
+ if (propertyName === null) return null;
10822
+ propertyNames.add(propertyName);
10823
+ }
10824
+ return propertyNames;
10825
+ };
10826
+ const getMutationPropertyNames = (node) => {
10827
+ const target = getOutermostTarget(node);
10828
+ const parent = target.parent;
10829
+ if (!parent) return void 0;
10830
+ if (isNodeOfType(parent, "AssignmentExpression") && parent.left === target || isNodeOfType(parent, "UpdateExpression") && parent.argument === target || isNodeOfType(parent, "UnaryExpression") && parent.operator === "delete") {
10831
+ if (!isNodeOfType(target, "MemberExpression")) return null;
10832
+ const propertyName = getStaticPropertyName(target);
10833
+ return propertyName === null ? null : new Set([propertyName]);
10834
+ }
10835
+ if (!isNodeOfType(parent, "CallExpression") || parent.arguments[0] !== target) return void 0;
10836
+ if (isGlobalNamespaceMethod(parent.callee, "Object", OBJECT_PROPERTY_MUTATION_METHOD_NAMES)) {
10837
+ const callee = stripParenExpression(parent.callee);
10838
+ if (!isNodeOfType(callee, "MemberExpression")) return void 0;
10839
+ const methodName = getStaticPropertyName(callee);
10840
+ if (methodName === "assign") {
10841
+ const assignedProperties = parent.arguments.slice(1).map(getObjectExpressionPropertyNames);
10842
+ if (assignedProperties.some((properties) => properties === null)) return null;
10843
+ return new Set(assignedProperties.flatMap((properties) => [...properties ?? []]));
10844
+ }
10845
+ if (methodName === "defineProperties") {
10846
+ const propertyDescriptors = parent.arguments[1];
10847
+ return propertyDescriptors ? getObjectExpressionPropertyNames(propertyDescriptors) : null;
10848
+ }
10849
+ const propertyKey = parent.arguments[1];
10850
+ return propertyKey && isNodeOfType(propertyKey, "Literal") && typeof propertyKey.value === "string" ? new Set([propertyKey.value]) : null;
10851
+ }
10852
+ if (isGlobalNamespaceMethod(parent.callee, "Reflect", REFLECT_PROPERTY_MUTATION_METHOD_NAMES)) {
10853
+ const propertyKey = parent.arguments[1];
10854
+ return propertyKey && isNodeOfType(propertyKey, "Literal") && typeof propertyKey.value === "string" ? new Set([propertyKey.value]) : null;
10855
+ }
10856
+ };
10857
+ const getLocalCallTarget = (call) => {
10858
+ const callee = stripParenExpression(call.callee);
10859
+ if (isFunctionLike$1(callee)) return callee;
10860
+ if (!isNodeOfType(callee, "Identifier")) return null;
10861
+ const symbol = resolveConstIdentifierAlias(callee, scopes);
10862
+ if (!symbol) return null;
10863
+ if (symbol.kind === "function" && isFunctionLike$1(symbol.declarationNode)) return symbol.declarationNode;
10864
+ if (symbol.kind !== "const" || !symbol.initializer) return null;
10865
+ const initializer = stripParenExpression(symbol.initializer);
10866
+ return isFunctionLike$1(initializer) ? initializer : null;
10867
+ };
10868
+ const calls = [];
10869
+ const eventsBySymbolId = /* @__PURE__ */ new Map();
10870
+ walkAst(scopes.rootScope.node, (node) => {
10871
+ if (isNodeOfType(node, "CallExpression")) {
10872
+ const owner = getExecutionOwner(node);
10873
+ const targetOwner = getLocalCallTarget(node);
10874
+ if (targetOwner && !isStaticallyUnreachable(node, owner)) calls.push({
10875
+ call: node,
10876
+ owner,
10877
+ targetOwner
10878
+ });
10879
+ }
10880
+ if (!isNodeOfType(node, "Identifier")) return;
10881
+ const propertyNames = getMutationPropertyNames(node);
10882
+ if (propertyNames === void 0) return;
10883
+ const symbol = resolveConstIdentifierAlias(node, scopes);
10884
+ if (!symbol) return;
10885
+ const owner = getExecutionOwner(node);
10886
+ if (isStaticallyUnreachable(node, owner)) return;
10887
+ const events = eventsBySymbolId.get(symbol.id) ?? [];
10888
+ events.push({
10889
+ node,
10890
+ owner,
10891
+ propertyNames
10892
+ });
10893
+ eventsBySymbolId.set(symbol.id, events);
10894
+ });
10895
+ const getInvokedOwnersBefore = (checkpoint) => {
10896
+ const checkpointOwner = getExecutionOwner(checkpoint);
10897
+ const checkpointStartIndex = getNodeStartIndex(checkpoint);
10898
+ const invokedOwners = /* @__PURE__ */ new Set();
10899
+ const visitOwner = (owner, cutoffIndex) => {
10900
+ for (const call of calls) {
10901
+ if (call.owner !== owner || getNodeStartIndex(call.call) >= cutoffIndex) continue;
10902
+ if (invokedOwners.has(call.targetOwner)) continue;
10903
+ invokedOwners.add(call.targetOwner);
10904
+ visitOwner(call.targetOwner, Number.POSITIVE_INFINITY);
10905
+ }
10906
+ };
10907
+ visitOwner(checkpointOwner, checkpointStartIndex);
10908
+ if (!isNodeOfType(checkpointOwner, "Program")) visitOwner(scopes.rootScope.node, Number.POSITIVE_INFINITY);
10909
+ return invokedOwners;
10910
+ };
10911
+ const getProgramCutoffIndex = (usageOwner) => {
10912
+ if (isNodeOfType(usageOwner, "Program")) return Number.POSITIVE_INFINITY;
10913
+ const directProgramCall = calls.find((call) => isNodeOfType(call.owner, "Program") && call.targetOwner === usageOwner);
10914
+ return directProgramCall ? getNodeStartIndex(directProgramCall.call) : Number.POSITIVE_INFINITY;
10915
+ };
10916
+ const callsByOwner = /* @__PURE__ */ new Map();
10917
+ for (const call of calls) {
10918
+ const ownerCalls = callsByOwner.get(call.owner) ?? [];
10919
+ ownerCalls.push(call);
10920
+ callsByOwner.set(call.owner, ownerCalls);
10921
+ }
10922
+ const ownerReachabilityCache = /* @__PURE__ */ new WeakMap();
10923
+ const canOwnerReach = (owner, targetOwner) => {
10924
+ const cachedResult = ownerReachabilityCache.get(owner)?.get(targetOwner);
10925
+ if (cachedResult !== void 0) return cachedResult;
10926
+ const pendingOwners = [owner];
10927
+ const visitedOwners = /* @__PURE__ */ new Set();
10928
+ let canReach = false;
10929
+ while (pendingOwners.length > 0) {
10930
+ const currentOwner = pendingOwners.pop();
10931
+ if (!currentOwner || visitedOwners.has(currentOwner)) continue;
10932
+ if (currentOwner === targetOwner) {
10933
+ canReach = true;
10934
+ break;
10935
+ }
10936
+ visitedOwners.add(currentOwner);
10937
+ for (const call of callsByOwner.get(currentOwner) ?? []) pendingOwners.push(call.targetOwner);
10938
+ }
10939
+ const cachedTargets = ownerReachabilityCache.get(owner) ?? /* @__PURE__ */ new WeakMap();
10940
+ cachedTargets.set(targetOwner, canReach);
10941
+ ownerReachabilityCache.set(owner, cachedTargets);
10942
+ return canReach;
10943
+ };
10944
+ const getRepeatedControlFlowAncestors = (node, owner) => {
10945
+ const ancestors = /* @__PURE__ */ new Set();
10946
+ let current = node;
10947
+ while (current?.parent && current !== owner) {
10948
+ const parent = current.parent;
10949
+ const isSingleIterationDoWhile = isNodeOfType(parent, "DoWhileStatement") && isNodeOfType(parent.test, "Literal") && !parent.test.value;
10950
+ const loopBody = isNodeOfType(parent, "ForStatement") || isNodeOfType(parent, "ForInStatement") || isNodeOfType(parent, "ForOfStatement") || isNodeOfType(parent, "WhileStatement") || isNodeOfType(parent, "DoWhileStatement") ? parent.body : null;
10951
+ let bodyStatement = node;
10952
+ while (loopBody && isNodeOfType(loopBody, "BlockStatement") && bodyStatement && bodyStatement.parent !== loopBody) bodyStatement = bodyStatement.parent ?? null;
10953
+ const bodyStatementIndex = loopBody && isNodeOfType(loopBody, "BlockStatement") && bodyStatement ? loopBody.body.findIndex((statement) => statement === bodyStatement) : -1;
10954
+ const hasFollowingLoopExit = Boolean(loopBody && isNodeOfType(loopBody, "BlockStatement") && bodyStatementIndex >= 0 && loopBody.body.slice(bodyStatementIndex + 1).some((statement) => isAbruptCompletionStatement(statement, false)));
10955
+ if (loopBody && !isSingleIterationDoWhile && !hasFollowingLoopExit) ancestors.add(parent);
10956
+ current = parent;
10957
+ }
10958
+ return ancestors;
10959
+ };
10960
+ const nodesShareRepeatedControlFlow = (leftNode, rightNode, owner) => {
10961
+ const leftAncestors = getRepeatedControlFlowAncestors(leftNode, owner);
10962
+ if (leftAncestors.size === 0) return false;
10963
+ return [...getRepeatedControlFlowAncestors(rightNode, owner)].some((ancestor) => leftAncestors.has(ancestor));
10964
+ };
10965
+ const callsReachingOwnerCache = /* @__PURE__ */ new WeakMap();
10966
+ const getCallsReachingOwnerByCaller = (targetOwner) => {
10967
+ const cachedCalls = callsReachingOwnerCache.get(targetOwner);
10968
+ if (cachedCalls) return cachedCalls;
10969
+ const reachingCalls = /* @__PURE__ */ new Map();
10970
+ for (const call of calls) {
10971
+ if (!canOwnerReach(call.targetOwner, targetOwner)) continue;
10972
+ const ownerCalls = reachingCalls.get(call.owner) ?? [];
10973
+ ownerCalls.push(call);
10974
+ reachingCalls.set(call.owner, ownerCalls);
10975
+ }
10976
+ callsReachingOwnerCache.set(targetOwner, reachingCalls);
10977
+ return reachingCalls;
10978
+ };
10979
+ const canMutationReachUsageAcrossCalls = (mutationOwner, usageOwner) => {
10980
+ const mutationCallsByOwner = getCallsReachingOwnerByCaller(mutationOwner);
10981
+ const usageCallsByOwner = getCallsReachingOwnerByCaller(usageOwner);
10982
+ for (const [owner, mutationCalls] of mutationCallsByOwner) {
10983
+ const usageCalls = usageCallsByOwner.get(owner);
10984
+ if (!usageCalls) continue;
10985
+ for (const mutationCall of mutationCalls) for (const usageCall of usageCalls) {
10986
+ if (mutationCall === usageCall) continue;
10987
+ if (getNodeStartIndex(mutationCall.call) < getNodeStartIndex(usageCall.call) || isFunctionLike$1(owner) || nodesShareRepeatedControlFlow(mutationCall.call, usageCall.call, owner)) return true;
10988
+ }
10989
+ }
10990
+ return false;
10991
+ };
10992
+ const isExecutionOrderAmbiguous = (usageNode) => {
10993
+ const usageOwner = getExecutionOwner(usageNode);
10994
+ if (isNodeOfType(usageOwner, "Program")) return false;
10995
+ const reachingProgramCalls = calls.filter((call) => isNodeOfType(call.owner, "Program") && canOwnerReach(call.targetOwner, usageOwner));
10996
+ if (reachingProgramCalls.length === 0) return false;
10997
+ return reachingProgramCalls.length !== 1 || reachingProgramCalls[0]?.targetOwner !== usageOwner;
10998
+ };
10999
+ const isMutationOrderAmbiguous = (symbol, usageNode, relevantPropertyName) => {
11000
+ const usageOwner = getExecutionOwner(usageNode);
11001
+ const usageStartIndex = getNodeStartIndex(usageNode);
11002
+ return (eventsBySymbolId.get(symbol.id) ?? []).some((event) => {
11003
+ if (relevantPropertyName !== null && event.propertyNames !== null && !event.propertyNames.has(relevantPropertyName)) return false;
11004
+ if (event.owner === usageOwner) return getNodeStartIndex(event.node) >= usageStartIndex && (isFunctionLike$1(usageOwner) || nodesShareRepeatedControlFlow(event.node, usageNode, usageOwner));
11005
+ if (isNodeOfType(event.owner, "Program")) return (getCallsReachingOwnerByCaller(usageOwner).get(event.owner) ?? []).some((usageCall) => nodesShareRepeatedControlFlow(event.node, usageCall.call, event.owner));
11006
+ return canOwnerReach(event.owner, usageOwner) || canMutationReachUsageAcrossCalls(event.owner, usageOwner);
11007
+ });
11008
+ };
11009
+ const getEventsBefore = (symbol, usageNode) => {
11010
+ const symbolEvents = eventsBySymbolId.get(symbol.id) ?? [];
11011
+ const mutationEvents = [];
11012
+ const visitOwner = (owner, cutoffIndex, activeOwners, isConditionalPath) => {
11013
+ if (activeOwners.has(owner)) return;
11014
+ const nextActiveOwners = new Set(activeOwners);
11015
+ nextActiveOwners.add(owner);
11016
+ const operations = [...symbolEvents.filter((event) => event.owner === owner).map((event) => ({
11017
+ event,
11018
+ index: getNodeStartIndex(event.node)
11019
+ })), ...calls.filter((call) => call.owner === owner).map((call) => ({
11020
+ call,
11021
+ index: getNodeStartIndex(call.call)
11022
+ }))].sort((left, right) => left.index - right.index);
11023
+ for (const operation of operations) {
11024
+ if (operation.index >= cutoffIndex) break;
11025
+ if ("event" in operation) {
11026
+ mutationEvents.push({
11027
+ isConditional: isConditionalPath || isConditionallyExecuted(operation.event.node, operation.event.owner),
11028
+ node: operation.event.node
11029
+ });
11030
+ continue;
11031
+ }
11032
+ visitOwner(operation.call.targetOwner, Number.POSITIVE_INFINITY, nextActiveOwners, isConditionalPath || isConditionallyExecuted(operation.call.call, operation.call.owner));
11033
+ }
11034
+ };
11035
+ const usageOwner = getExecutionOwner(usageNode);
11036
+ if (!isNodeOfType(usageOwner, "Program")) visitOwner(scopes.rootScope.node, getProgramCutoffIndex(usageOwner), /* @__PURE__ */ new Set(), false);
11037
+ visitOwner(usageOwner, getNodeStartIndex(usageNode), /* @__PURE__ */ new Set(), false);
11038
+ return mutationEvents;
11039
+ };
11040
+ const isMutatedBefore = (symbol, usageNode, relevantPropertyName) => {
11041
+ const events = eventsBySymbolId.get(symbol.id);
11042
+ if (!events) return false;
11043
+ const usageStartIndex = getNodeStartIndex(usageNode);
11044
+ const usageOwner = getExecutionOwner(usageNode);
11045
+ const invokedOwners = getInvokedOwnersBefore(usageNode);
11046
+ return events.some((event) => {
11047
+ if (relevantPropertyName !== null && event.propertyNames !== null && !event.propertyNames.has(relevantPropertyName)) return false;
11048
+ if (event.owner === usageOwner) return getNodeStartIndex(event.node) < usageStartIndex;
11049
+ if (isNodeOfType(event.owner, "Program") && !isNodeOfType(usageOwner, "Program")) return getNodeStartIndex(event.node) < getProgramCutoffIndex(usageOwner);
11050
+ return invokedOwners.has(event.owner);
11051
+ });
11052
+ };
11053
+ const inspector = {
11054
+ getEventsBefore,
11055
+ getOutermostTarget,
11056
+ isGlobalNamespaceMethod,
11057
+ isExecutionOrderAmbiguous,
11058
+ isMutationOrderAmbiguous,
11059
+ isMutatedBefore
11060
+ };
11061
+ inspectorCache.set(scopes, inspector);
11062
+ return inspector;
11063
+ };
11064
+ //#endregion
11065
+ //#region src/plugin/rules/security-scan/utils/get-katex-renderer-provenance.ts
11066
+ const isExpectedModuleName = (actualModuleName, expectedModuleName) => expectedModuleName === "katex" ? actualModuleName === "katex" || actualModuleName.startsWith("katex/") : actualModuleName === expectedModuleName;
11067
+ const isGlobalRequireCall = (node, moduleName, scopes) => {
11068
+ const expression = stripParenExpression(node);
11069
+ if (!isNodeOfType(expression, "CallExpression")) return false;
11070
+ const callee = stripParenExpression(expression.callee);
11071
+ const firstArgument = expression.arguments[0];
11072
+ return Boolean(isNodeOfType(callee, "Identifier") && callee.name === "require" && scopes.isGlobalReference(callee) && firstArgument && isNodeOfType(firstArgument, "Literal") && typeof firstArgument.value === "string" && isExpectedModuleName(firstArgument.value, moduleName));
11073
+ };
11074
+ const isTypeScriptImportEqualsFromModule = (symbol, moduleName) => {
11075
+ if (symbol.kind !== "ts-import-equals") return false;
11076
+ const declaration = symbol.declarationNode;
11077
+ if (!isNodeOfType(declaration, "TSImportEqualsDeclaration")) return false;
11078
+ const moduleReference = declaration.moduleReference;
11079
+ return Boolean(isNodeOfType(moduleReference, "TSExternalModuleReference") && isNodeOfType(moduleReference.expression, "Literal") && typeof moduleReference.expression.value === "string" && isExpectedModuleName(moduleReference.expression.value, moduleName));
11080
+ };
11081
+ const isAwaitedImportFromModule = (node, moduleName) => {
11082
+ const expression = stripParenExpression(node);
11083
+ return Boolean(isNodeOfType(expression, "AwaitExpression") && isNodeOfType(expression.argument, "ImportExpression") && isNodeOfType(expression.argument.source, "Literal") && typeof expression.argument.source.value === "string" && isExpectedModuleName(expression.argument.source.value, moduleName));
11084
+ };
11085
+ const getModuleNamespaceSymbol = (node, moduleName, namespacePropertyName, usageNode, scopes) => {
11086
+ const symbol = resolveConstIdentifierAlias(stripParenExpression(node), scopes);
11087
+ const mutationInspector = getSymbolMutationInspector(scopes);
11088
+ if (!symbol || mutationInspector.isExecutionOrderAmbiguous(usageNode) || mutationInspector.isMutationOrderAmbiguous(symbol, usageNode, namespacePropertyName) || mutationInspector.isMutatedBefore(symbol, usageNode, namespacePropertyName)) return null;
11089
+ const importDeclaration = getImportDeclarationForSymbol(symbol);
11090
+ if (typeof importDeclaration?.source.value === "string" && isExpectedModuleName(importDeclaration.source.value, moduleName)) return isNodeOfType(symbol.declarationNode, "ImportDefaultSpecifier") || isNodeOfType(symbol.declarationNode, "ImportNamespaceSpecifier") || getImportedName(symbol.declarationNode) === "default" ? symbol : null;
11091
+ if (isTypeScriptImportEqualsFromModule(symbol, moduleName)) return symbol;
11092
+ if (symbol.kind !== "const" || !symbol.initializer) return null;
11093
+ const initializer = stripParenExpression(symbol.initializer);
11094
+ if (isGlobalRequireCall(initializer, moduleName, scopes)) return symbol;
11095
+ if (isNodeOfType(initializer, "MemberExpression") && getStaticPropertyName(initializer) === "default" && (isGlobalRequireCall(initializer.object, moduleName, scopes) || isAwaitedImportFromModule(initializer.object, moduleName))) return symbol;
11096
+ if (isAwaitedImportFromModule(initializer, moduleName)) return symbol;
11097
+ return null;
11098
+ };
11099
+ const getNamedImportSymbol = (node, moduleName, importedName, usageNode, scopes) => {
11100
+ const symbol = resolveConstIdentifierAlias(stripParenExpression(node), scopes);
11101
+ if (!symbol) return null;
11102
+ const importDeclaration = getImportDeclarationForSymbol(symbol);
11103
+ const mutationInspector = getSymbolMutationInspector(scopes);
11104
+ if (typeof importDeclaration?.source.value !== "string" || !isExpectedModuleName(importDeclaration.source.value, moduleName) || getImportedName(symbol.declarationNode) !== importedName || mutationInspector.isExecutionOrderAmbiguous(usageNode) || mutationInspector.isMutationOrderAmbiguous(symbol, usageNode, null) || mutationInspector.isMutatedBefore(symbol, usageNode, null)) return null;
11105
+ return symbol;
11106
+ };
11107
+ const isKatexNamespace = (node, usageNode, scopes) => getModuleNamespaceSymbol(node, "katex", "renderToString", usageNode, scopes) !== null || isGlobalRequireCall(node, "katex", scopes);
11108
+ const isKatexNamedRenderer = (node, usageNode, scopes) => {
11109
+ if (getNamedImportSymbol(node, "katex", "renderToString", usageNode, scopes)) return true;
11110
+ const expression = stripParenExpression(node);
11111
+ if (!isNodeOfType(expression, "Identifier")) return false;
11112
+ const symbol = scopes.referenceFor(expression)?.resolvedSymbol;
11113
+ if (!symbol || symbol.kind !== "const" || !symbol.initializer || getSymbolMutationInspector(scopes).isMutatedBefore(symbol, usageNode, null)) return false;
11114
+ const initializer = stripParenExpression(symbol.initializer);
11115
+ const bindingProperty = symbol.bindingIdentifier.parent;
11116
+ if (isNodeOfType(bindingProperty, "Property") && getStaticPropertyKeyName(bindingProperty, { allowComputedString: true }) === "renderToString") return isKatexNamespace(initializer, symbol.declarationNode, scopes);
11117
+ if (isNodeOfType(initializer, "MemberExpression") && getStaticPropertyName(initializer) === "renderToString" && isKatexNamespace(initializer.object, symbol.declarationNode, scopes)) return true;
11118
+ if (isNodeOfType(initializer, "Identifier")) return isKatexNamedRenderer(initializer, symbol.declarationNode, scopes);
11119
+ return false;
11120
+ };
11121
+ const isUnprovenKatexShapedRenderer = (node, scopes) => {
11122
+ const expression = stripParenExpression(node);
11123
+ if (isNodeOfType(expression, "Identifier")) {
11124
+ if (!/katex/i.test(expression.name)) return false;
11125
+ const symbol = scopes.referenceFor(expression)?.resolvedSymbol;
11126
+ return Boolean(symbol && symbol.kind !== "parameter" && symbol.kind !== "let");
11127
+ }
11128
+ if (!isNodeOfType(expression, "MemberExpression")) return false;
11129
+ if (getStaticPropertyName(expression) !== "renderToString") return false;
11130
+ const receiver = stripParenExpression(expression.object);
11131
+ if (!isNodeOfType(receiver, "Identifier") || !/katex/i.test(receiver.name)) return false;
11132
+ const symbol = scopes.referenceFor(receiver)?.resolvedSymbol;
11133
+ if (!symbol) return false;
11134
+ if (symbol.kind === "import") {
11135
+ if (!isExpectedModuleName(String(getImportDeclarationForSymbol(symbol)?.source.value ?? ""), "katex")) return true;
11136
+ const mutationInspector = getSymbolMutationInspector(scopes);
11137
+ if (mutationInspector.isExecutionOrderAmbiguous(expression) || mutationInspector.isMutationOrderAmbiguous(symbol, expression, "renderToString")) return false;
11138
+ return mutationInspector.isMutatedBefore(symbol, expression, "renderToString");
11139
+ }
11140
+ if (symbol.kind === "parameter" || symbol.kind === "let" || symbol.kind === "var") return true;
11141
+ if (symbol.kind !== "const" || !symbol.initializer) return false;
11142
+ const initializer = stripParenExpression(symbol.initializer);
11143
+ if (isNodeOfType(initializer, "ObjectExpression")) return true;
11144
+ if (isNodeOfType(initializer, "CallExpression")) {
11145
+ const callee = stripParenExpression(initializer.callee);
11146
+ return isNodeOfType(callee, "Identifier") && callee.name === "require";
11147
+ }
11148
+ return isNodeOfType(initializer, "AwaitExpression") && isNodeOfType(initializer.argument, "ImportExpression");
11149
+ };
11150
+ //#endregion
11151
+ //#region src/plugin/rules/security-scan/utils/get-katex-options-proof.ts
11152
+ const parameterOptionsProofsByScopes = /* @__PURE__ */ new WeakMap();
11153
+ const isStaticallyDisabledTrustValue = (node, scopes) => {
11154
+ const expression = stripParenExpression(node);
11155
+ if (isNodeOfType(expression, "Identifier")) return expression.name === "undefined" && scopes.isGlobalReference(expression);
11156
+ return isNodeOfType(expression, "Literal") && !expression.value;
11157
+ };
11158
+ const getStaticObjectPropertyValue = (node, expectedPropertyName) => {
11159
+ const expression = stripParenExpression(node);
11160
+ if (!isNodeOfType(expression, "ObjectExpression")) return null;
11161
+ let propertyValue;
11162
+ for (const property of expression.properties) {
11163
+ if (!isNodeOfType(property, "Property")) return null;
11164
+ const propertyName = getStaticPropertyKeyName(property, { allowComputedString: true });
11165
+ if (propertyName === null) return null;
11166
+ if (propertyName !== expectedPropertyName) continue;
11167
+ if (property.kind !== "init") return null;
11168
+ propertyValue = property.value;
11169
+ }
11170
+ return propertyValue;
11171
+ };
11172
+ const getPropertyDescriptorValue = (node) => {
11173
+ const expression = stripParenExpression(node);
11174
+ if (!isNodeOfType(expression, "ObjectExpression")) return null;
11175
+ for (const property of expression.properties) {
11176
+ if (!isNodeOfType(property, "Property")) return null;
11177
+ const propertyName = getStaticPropertyKeyName(property, { allowComputedString: true });
11178
+ if (propertyName === null || propertyName === "get" || propertyName === "set") return null;
11179
+ }
11180
+ return getStaticObjectPropertyValue(expression, "value");
11181
+ };
11182
+ const getTrustStateAfterPropertyDescriptor = (currentState, propertyDescriptor, scopes) => {
11183
+ const propertyValue = getPropertyDescriptorValue(propertyDescriptor);
11184
+ if (propertyValue === null) return "trusted";
11185
+ if (propertyValue === void 0) return currentState;
11186
+ return isStaticallyDisabledTrustValue(propertyValue, scopes) ? "untrusted" : "trusted";
11187
+ };
11188
+ const mergeConditionalTrustStates = (currentState, conditionalState) => {
11189
+ if (currentState === conditionalState) return currentState;
11190
+ if (currentState === "trusted" || conditionalState === "trusted") return "trusted";
11191
+ if (currentState === "unsupported" || conditionalState === "unsupported") return "unsupported";
11192
+ return "untrusted";
11193
+ };
11194
+ const applyTrustMutation = (currentState, eventNode, scopes, visitedSymbolIds) => {
11195
+ const mutationInspector = getSymbolMutationInspector(scopes);
11196
+ const target = mutationInspector.getOutermostTarget(eventNode);
11197
+ const parent = target.parent;
11198
+ if (!parent) return "unsupported";
11199
+ if (isNodeOfType(parent, "AssignmentExpression") && parent.left === target) {
11200
+ if (!isNodeOfType(target, "MemberExpression")) return "unsupported";
11201
+ const propertyName = getStaticPropertyName(target);
11202
+ if (propertyName === null) return "trusted";
11203
+ if (propertyName !== "trust") return currentState;
11204
+ return isStaticallyDisabledTrustValue(parent.right, scopes) ? "untrusted" : "trusted";
11205
+ }
11206
+ if (isNodeOfType(parent, "UnaryExpression") && parent.operator === "delete") {
11207
+ if (!isNodeOfType(target, "MemberExpression")) return "unsupported";
11208
+ const propertyName = getStaticPropertyName(target);
11209
+ if (propertyName === null) return "trusted";
11210
+ return propertyName === "trust" ? "absent" : currentState;
11211
+ }
11212
+ if (isNodeOfType(parent, "UpdateExpression")) {
11213
+ if (!isNodeOfType(target, "MemberExpression")) return "unsupported";
11214
+ const propertyName = getStaticPropertyName(target);
11215
+ return propertyName === "trust" || propertyName === null ? "trusted" : currentState;
11216
+ }
11217
+ if (!isNodeOfType(parent, "CallExpression") || parent.arguments[0] !== target) return "unsupported";
11218
+ if (mutationInspector.isGlobalNamespaceMethod(parent.callee, "Object", OBJECT_PROPERTY_MUTATION_METHOD_NAMES)) {
11219
+ const callee = stripParenExpression(parent.callee);
11220
+ if (!isNodeOfType(callee, "MemberExpression")) return "unsupported";
11221
+ const methodName = getStaticPropertyName(callee);
11222
+ if (methodName === "assign") {
11223
+ let nextState = currentState;
11224
+ for (const source of parent.arguments.slice(1)) {
11225
+ const sourceState = getKatexOptionsTrustState(source, source, scopes, new Set(visitedSymbolIds));
11226
+ if (sourceState !== "absent") nextState = sourceState;
11227
+ }
11228
+ return nextState;
11229
+ }
11230
+ if (methodName === "defineProperties") {
11231
+ const propertyDescriptors = parent.arguments[1];
11232
+ if (!propertyDescriptors) return "unsupported";
11233
+ const trustDescriptor = getStaticObjectPropertyValue(propertyDescriptors, "trust");
11234
+ if (trustDescriptor === null) return "trusted";
11235
+ if (trustDescriptor === void 0) return currentState;
11236
+ return getTrustStateAfterPropertyDescriptor(currentState, trustDescriptor, scopes);
11237
+ }
11238
+ const propertyKey = parent.arguments[1];
11239
+ if (!propertyKey || !isNodeOfType(propertyKey, "Literal") || typeof propertyKey.value !== "string") return "trusted";
11240
+ if (propertyKey.value !== "trust") return currentState;
11241
+ const propertyDescriptor = parent.arguments[2];
11242
+ if (!propertyDescriptor) return "unsupported";
11243
+ return getTrustStateAfterPropertyDescriptor(currentState, propertyDescriptor, scopes);
11244
+ }
11245
+ if (mutationInspector.isGlobalNamespaceMethod(parent.callee, "Reflect", REFLECT_PROPERTY_MUTATION_METHOD_NAMES)) {
11246
+ const callee = stripParenExpression(parent.callee);
11247
+ if (!isNodeOfType(callee, "MemberExpression")) return "unsupported";
11248
+ const methodName = getStaticPropertyName(callee);
11249
+ const propertyKey = parent.arguments[1];
11250
+ if (!propertyKey || !isNodeOfType(propertyKey, "Literal") || typeof propertyKey.value !== "string") return "trusted";
11251
+ if (propertyKey.value !== "trust") return currentState;
11252
+ const propertyValue = parent.arguments[2];
11253
+ if (!propertyValue) return "unsupported";
11254
+ if (methodName === "defineProperty") return getTrustStateAfterPropertyDescriptor(currentState, propertyValue, scopes);
11255
+ return isStaticallyDisabledTrustValue(propertyValue, scopes) ? "untrusted" : "trusted";
11256
+ }
11257
+ return "unsupported";
11258
+ };
11259
+ const getKatexOptionsTrustState = (rawNode, usageNode, scopes, visitedSymbolIds) => {
11260
+ if (rawNode === void 0) return "absent";
11261
+ const node = stripParenExpression(rawNode);
11262
+ if (isNodeOfType(node, "Identifier")) {
11263
+ if (node.name === "undefined" && scopes.isGlobalReference(node)) return "absent";
11264
+ const symbol = resolveConstIdentifierAlias(node, scopes);
11265
+ if (!symbol || symbol.kind !== "const" || !symbol.initializer || visitedSymbolIds.has(symbol.id)) return "unsupported";
11266
+ const nextVisitedSymbolIds = new Set(visitedSymbolIds);
11267
+ nextVisitedSymbolIds.add(symbol.id);
11268
+ const mutationInspector = getSymbolMutationInspector(scopes);
11269
+ if (mutationInspector.isMutationOrderAmbiguous(symbol, usageNode, "trust")) return "unsupported";
11270
+ let trustState = getKatexOptionsTrustState(symbol.initializer, usageNode, scopes, nextVisitedSymbolIds);
11271
+ for (const replayedEvent of mutationInspector.getEventsBefore(symbol, usageNode)) {
11272
+ const nextTrustState = applyTrustMutation(trustState, replayedEvent.node, scopes, nextVisitedSymbolIds);
11273
+ if (replayedEvent.isConditional) trustState = mergeConditionalTrustStates(trustState, nextTrustState);
11274
+ else trustState = nextTrustState;
11275
+ }
11276
+ return trustState;
11277
+ }
11278
+ if (!isNodeOfType(node, "ObjectExpression")) return "unsupported";
11279
+ let trustState = "absent";
11280
+ for (const property of node.properties) {
11281
+ if (isNodeOfType(property, "SpreadElement")) {
11282
+ const spreadState = getKatexOptionsTrustState(property.argument, property.argument, scopes, new Set(visitedSymbolIds));
11283
+ if (spreadState !== "absent") trustState = spreadState === "unsupported" ? "trusted" : spreadState;
11284
+ continue;
11285
+ }
11286
+ if (!isNodeOfType(property, "Property")) {
11287
+ trustState = "trusted";
11288
+ continue;
11289
+ }
11290
+ const propertyName = getStaticPropertyKeyName(property, { allowComputedString: true });
11291
+ if (propertyName === null) {
11292
+ trustState = "trusted";
11293
+ continue;
11294
+ }
11295
+ if (propertyName === "trust") trustState = isStaticallyDisabledTrustValue(property.value, scopes) ? "untrusted" : "trusted";
11296
+ }
11297
+ return trustState;
11298
+ };
11299
+ const setKatexParameterOptionsProofs = (scopes, proofs) => {
11300
+ parameterOptionsProofsByScopes.set(scopes, proofs);
11301
+ };
11302
+ const getKatexOptionsProof = (rawNode, usageNode, scopes, visitedSymbolIds) => {
11303
+ const node = rawNode ? stripParenExpression(rawNode) : void 0;
11304
+ if (node && isNodeOfType(node, "Identifier")) {
11305
+ const parameterSymbol = scopes.referenceFor(node)?.resolvedSymbol;
11306
+ const parameterProof = parameterSymbol ? parameterOptionsProofsByScopes.get(scopes)?.get(parameterSymbol.id) : void 0;
11307
+ if (parameterProof) return parameterProof;
11308
+ }
11309
+ const trustState = getKatexOptionsTrustState(rawNode, usageNode, scopes, visitedSymbolIds);
11310
+ return {
11311
+ isConclusive: trustState !== "unsupported",
11312
+ isSafe: trustState === "absent" || trustState === "untrusted"
11313
+ };
11314
+ };
11315
+ //#endregion
11316
+ //#region src/plugin/rules/security-scan/utils/get-katex-html-proof.ts
11317
+ const SAFE_STATIC_HTML_PROOF = {
11318
+ containsKatex: false,
11319
+ isConclusive: true,
11320
+ isSafe: true,
11321
+ isSafeInAttributeContext: true
11322
+ };
11323
+ const SAFE_HTML_FRAGMENT_PROOF = {
11324
+ containsKatex: false,
11325
+ isConclusive: true,
11326
+ isSafe: true,
11327
+ isSafeInAttributeContext: false
11328
+ };
11329
+ const UNKNOWN_HTML_PROOF = {
11330
+ containsKatex: false,
11331
+ isConclusive: false,
11332
+ isSafe: false,
11333
+ isSafeInAttributeContext: false
11334
+ };
11335
+ const UNSUPPORTED_KATEX_PROOF = {
11336
+ containsKatex: true,
11337
+ isConclusive: false,
11338
+ isSafe: false,
11339
+ isSafeInAttributeContext: false
11340
+ };
11341
+ const UNSAFE_KATEX_PROOF = {
11342
+ containsKatex: true,
11343
+ isConclusive: true,
11344
+ isSafe: false,
11345
+ isSafeInAttributeContext: false
11346
+ };
11347
+ const sourceFilenameByScopes = /* @__PURE__ */ new WeakMap();
11348
+ const crossFileDepthByScopes = /* @__PURE__ */ new WeakMap();
11349
+ const registerKatexProofSource = (scopes, filename, depth) => {
11350
+ sourceFilenameByScopes.set(scopes, filename);
11351
+ crossFileDepthByScopes.set(scopes, depth);
11352
+ };
11353
+ const combineHtmlProofs = (proofs) => ({
11354
+ containsKatex: proofs.some((proof) => proof.containsKatex),
11355
+ isConclusive: proofs.every((proof) => proof.isSafe) ? proofs.filter((proof) => proof.containsKatex).every((proof) => proof.isConclusive) : proofs.some((proof) => proof.containsKatex && proof.isConclusive && !proof.isSafe) || proofs.some((proof) => proof.containsKatex && proof.isConclusive) && proofs.some((proof) => !proof.containsKatex && !proof.isSafe),
11356
+ isSafe: proofs.every((proof) => proof.isSafe),
11357
+ isSafeInAttributeContext: proofs.every((proof) => proof.isSafeInAttributeContext)
11358
+ });
11359
+ const getOrderedObjectPropertyValue = (node, propertyName) => {
11360
+ const expression = stripParenExpression(node);
11361
+ if (!isNodeOfType(expression, "ObjectExpression")) return {
11362
+ isKnown: false,
11363
+ value: null
11364
+ };
11365
+ let isKnown = true;
11366
+ let propertyValue = null;
11367
+ for (const property of expression.properties) {
11368
+ if (!isNodeOfType(property, "Property")) {
11369
+ isKnown = false;
11370
+ propertyValue = null;
11371
+ continue;
11372
+ }
11373
+ const currentPropertyName = getStaticPropertyKeyName(property, { allowComputedString: true });
11374
+ if (currentPropertyName === null) {
11375
+ isKnown = false;
11376
+ propertyValue = null;
11377
+ } else if (currentPropertyName === propertyName) {
11378
+ isKnown = true;
11379
+ propertyValue = property.value;
11380
+ }
11381
+ }
11382
+ return {
11383
+ isKnown,
11384
+ value: propertyValue
11385
+ };
11386
+ };
11387
+ const isReactUseMemo = (node, scopes) => {
11388
+ const expression = stripParenExpression(node);
11389
+ if (isNodeOfType(expression, "Identifier")) {
11390
+ const symbol = resolveConstIdentifierAlias(expression, scopes);
11391
+ return Boolean(symbol && symbol.kind === "import" && getImportDeclarationForSymbol(symbol)?.source.value === "react" && getImportedName(symbol.declarationNode) === "useMemo");
11392
+ }
11393
+ if (!isNodeOfType(expression, "MemberExpression") || getStaticPropertyName(expression) !== "useMemo") return false;
11394
+ const symbol = resolveConstIdentifierAlias(stripParenExpression(expression.object), scopes);
11395
+ return Boolean(symbol && symbol.kind === "import" && getImportDeclarationForSymbol(symbol)?.source.value === "react" && (isNodeOfType(symbol.declarationNode, "ImportDefaultSpecifier") || isNodeOfType(symbol.declarationNode, "ImportNamespaceSpecifier") || getImportedName(symbol.declarationNode) === "default"));
11396
+ };
11397
+ const isAllOpeningAngleBracketsEscaped = (node, scopes) => {
11398
+ let current = stripParenExpression(node);
11399
+ let didEscapeEveryOpeningAngleBracket = false;
11400
+ while (isNodeOfType(current, "CallExpression")) {
11401
+ const callee = stripParenExpression(current.callee);
11402
+ if (!isNodeOfType(callee, "MemberExpression")) return false;
11403
+ const methodName = getStaticPropertyName(callee);
11404
+ if (methodName !== "replace" && methodName !== "replaceAll") return false;
11405
+ const searchValue = current.arguments[0];
11406
+ const replacementValue = current.arguments[1];
11407
+ if (!searchValue || !replacementValue || !isNodeOfType(replacementValue, "Literal") || typeof replacementValue.value !== "string" || replacementValue.value.includes("<") || replacementValue.value.includes("$")) return false;
11408
+ if (isNodeOfType(searchValue, "Literal")) {
11409
+ const regularExpression = "regex" in searchValue ? searchValue.regex : void 0;
11410
+ const replacesLiteralOpeningAngleBracket = methodName === "replaceAll" && searchValue.value === "<";
11411
+ const replacesGlobalOpeningAngleBracketPattern = regularExpression?.pattern === "<" && regularExpression.flags.includes("g");
11412
+ if (replacesLiteralOpeningAngleBracket || replacesGlobalOpeningAngleBracketPattern) didEscapeEveryOpeningAngleBracket = true;
11413
+ }
11414
+ current = stripParenExpression(callee.object);
11415
+ }
11416
+ if (!didEscapeEveryOpeningAngleBracket || !isNodeOfType(current, "Identifier")) return false;
11417
+ return scopes.referenceFor(current)?.resolvedSymbol?.kind === "parameter";
11418
+ };
11419
+ const getSanitizerProof = (node, scopes) => {
11420
+ const callee = stripParenExpression(node.callee);
11421
+ if (isNodeOfType(callee, "MemberExpression") && getStaticPropertyName(callee) === "sanitize") {
11422
+ if (getModuleNamespaceSymbol(callee.object, "dompurify", "sanitize", node, scopes) || getModuleNamespaceSymbol(callee.object, "isomorphic-dompurify", "sanitize", node, scopes)) return SAFE_HTML_FRAGMENT_PROOF;
11423
+ }
11424
+ if (isNodeOfType(callee, "MemberExpression") && getStaticPropertyName(callee) === "escape") {
11425
+ if (getModuleNamespaceSymbol(callee.object, "html-escaper", "escape", node, scopes)) return SAFE_STATIC_HTML_PROOF;
11426
+ }
11427
+ if (getNamedImportSymbol(callee, "html-escaper", "escape", node, scopes)) return SAFE_STATIC_HTML_PROOF;
11428
+ if (getNamedImportSymbol(callee, "dompurify", "sanitize", node, scopes) || getNamedImportSymbol(callee, "isomorphic-dompurify", "sanitize", node, scopes)) return SAFE_HTML_FRAGMENT_PROOF;
11429
+ return null;
11430
+ };
11431
+ const getSafePostTransformProof = (node, receiverProof) => {
11432
+ if (!receiverProof.isSafe) return null;
11433
+ const callee = stripParenExpression(node.callee);
11434
+ if (!isNodeOfType(callee, "MemberExpression")) return null;
11435
+ const methodName = getStaticPropertyName(callee);
11436
+ if ((methodName === "trim" || methodName === "trimEnd" || methodName === "trimStart") && node.arguments.length === 0) return receiverProof;
11437
+ if (methodName !== "replace" && methodName !== "replaceAll") return null;
11438
+ const replacement = node.arguments[1];
11439
+ if (!replacement || !isNodeOfType(replacement, "Literal") || typeof replacement.value !== "string" || replacement.value.includes("<")) return null;
11440
+ return {
11441
+ containsKatex: receiverProof.containsKatex,
11442
+ isConclusive: receiverProof.isConclusive,
11443
+ isSafe: true,
11444
+ isSafeInAttributeContext: receiverProof.isSafeInAttributeContext && !/[&>"']/.test(replacement.value)
11445
+ };
11446
+ };
11447
+ const getTemplateInterpolationContext = (staticPrefix) => {
11448
+ const lowerPrefix = staticPrefix.toLowerCase();
11449
+ for (const tagName of [
11450
+ "script",
11451
+ "style",
11452
+ "textarea",
11453
+ "title"
11454
+ ]) if (lowerPrefix.lastIndexOf(`<${tagName}`) > lowerPrefix.lastIndexOf(`</${tagName}`)) return "raw-text";
11455
+ const lastOpeningAngleIndex = staticPrefix.lastIndexOf("<");
11456
+ if (lastOpeningAngleIndex <= staticPrefix.lastIndexOf(">")) return "text";
11457
+ const currentTagText = staticPrefix.slice(lastOpeningAngleIndex + 1);
11458
+ let openQuote = null;
11459
+ for (let index = 0; index < currentTagText.length; index += 1) {
11460
+ const character = currentTagText[index];
11461
+ if ((character === "\"" || character === "'") && currentTagText[index - 1] !== "\\") {
11462
+ if (openQuote === character) openQuote = null;
11463
+ else if (openQuote === null) openQuote = character;
11464
+ }
11465
+ }
11466
+ return openQuote === null ? "unsafe-tag" : "attribute";
11467
+ };
11468
+ const getTemplateLiteralProof = (node, scopes, visitedSymbolIds, parameterProofs) => {
11469
+ const expressionProofs = node.expressions.map((expression) => getKatexHtmlProof(expression, scopes, new Set(visitedSymbolIds), parameterProofs));
11470
+ let staticPrefix = "";
11471
+ let isSafe = true;
11472
+ for (let expressionIndex = 0; expressionIndex < expressionProofs.length; expressionIndex += 1) {
11473
+ staticPrefix += node.quasis[expressionIndex]?.value.raw ?? "";
11474
+ const context = getTemplateInterpolationContext(staticPrefix);
11475
+ const proof = expressionProofs[expressionIndex] ?? UNKNOWN_HTML_PROOF;
11476
+ if (context === "text") isSafe &&= proof.isSafe;
11477
+ else if (context === "attribute") isSafe &&= proof.isSafeInAttributeContext;
11478
+ else isSafe = false;
11479
+ }
11480
+ return {
11481
+ containsKatex: expressionProofs.some((proof) => proof.containsKatex),
11482
+ isConclusive: isSafe ? expressionProofs.filter((proof) => proof.containsKatex).every((proof) => proof.isConclusive) : expressionProofs.some((proof) => proof.containsKatex && proof.isConclusive && !proof.isSafe) || expressionProofs.some((proof) => proof.containsKatex && proof.isConclusive) && expressionProofs.some((proof) => !proof.containsKatex && !proof.isSafe),
11483
+ isSafe,
11484
+ isSafeInAttributeContext: false
11485
+ };
11486
+ };
11487
+ const isReturnStatementStaticallyUnreachable = (returnStatement, functionBody) => {
11488
+ let current = returnStatement;
11489
+ while (current.parent && current !== functionBody) {
11490
+ const parent = current.parent;
11491
+ if (isNodeOfType(parent, "BlockStatement")) {
11492
+ const statementIndex = parent.body.findIndex((statement) => statement === current);
11493
+ if (statementIndex > 0 && parent.body.slice(0, statementIndex).some((statement) => statementAlwaysExits(statement))) return true;
11494
+ }
11495
+ if (isNodeOfType(parent, "SwitchCase")) {
11496
+ const statementIndex = parent.consequent.findIndex((statement) => statement === current);
11497
+ if (statementIndex > 0 && parent.consequent.slice(0, statementIndex).some((statement) => statementAlwaysExits(statement))) return true;
11498
+ }
11499
+ if (isNodeOfType(parent, "IfStatement") && isNodeOfType(parent.test, "Literal")) {
11500
+ const ifStatementAlternate = parent.alternate;
11501
+ const ifStatementConsequent = parent.consequent;
11502
+ const ifStatementTest = parent.test;
11503
+ const isTruthyTest = Boolean(ifStatementTest.value);
11504
+ if (!isTruthyTest && ifStatementConsequent === current) return true;
11505
+ if (isTruthyTest && ifStatementAlternate === current) return true;
11506
+ }
11507
+ if (isNodeOfType(parent, "WhileStatement") && parent.body === current) {
11508
+ const whileStatementTest = parent.test;
11509
+ if (isNodeOfType(whileStatementTest, "Literal") && !whileStatementTest.value) return true;
11510
+ }
11511
+ if (isNodeOfType(parent, "ForStatement") && parent.body === current) {
11512
+ const forStatementTest = parent.test;
11513
+ if (forStatementTest && isNodeOfType(forStatementTest, "Literal") && !forStatementTest.value) return true;
11514
+ }
11515
+ current = parent;
11516
+ }
11517
+ return false;
11518
+ };
11519
+ const getFunctionHtmlProof = (functionNode, scopes, visitedSymbolIds, parameterProofs = /* @__PURE__ */ new Map()) => {
11520
+ if (!isFunctionLike$1(functionNode)) return UNKNOWN_HTML_PROOF;
11521
+ if (!isNodeOfType(functionNode.body, "BlockStatement")) return getKatexHtmlProof(functionNode.body, scopes, visitedSymbolIds, parameterProofs);
11522
+ const functionBody = functionNode.body;
11523
+ const returnProofs = [];
11524
+ walkAst(functionBody, (child) => {
11525
+ if (child !== functionBody && isFunctionLike$1(child)) return false;
11526
+ if (!isNodeOfType(child, "ReturnStatement")) return;
11527
+ if (isReturnStatementStaticallyUnreachable(child, functionBody)) return false;
11528
+ returnProofs.push(child.argument ? getKatexHtmlProof(child.argument, scopes, new Set(visitedSymbolIds), parameterProofs) : SAFE_STATIC_HTML_PROOF);
11529
+ return false;
11530
+ });
11531
+ return returnProofs.length === 0 ? SAFE_STATIC_HTML_PROOF : combineHtmlProofs(returnProofs);
11532
+ };
11533
+ const getLocalFunctionNode = (node, scopes) => {
11534
+ const expression = stripParenExpression(node);
11535
+ if (!isNodeOfType(expression, "Identifier")) return null;
11536
+ const symbol = resolveConstIdentifierAlias(expression, scopes);
11537
+ if (!symbol || symbol.references.some((reference) => reference.flag !== "read")) return null;
11538
+ if (symbol.kind === "function" && isFunctionLike$1(symbol.declarationNode)) return {
11539
+ functionNode: symbol.declarationNode,
11540
+ symbol
11541
+ };
11542
+ if (symbol.kind !== "const" || !symbol.initializer) return null;
11543
+ const initializer = stripParenExpression(symbol.initializer);
11544
+ return isFunctionLike$1(initializer) ? {
11545
+ functionNode: initializer,
11546
+ symbol
11547
+ } : null;
11548
+ };
11549
+ const getCrossFileFunctionProof = (call, scopes) => {
11550
+ const expression = stripParenExpression(call.callee);
11551
+ if (!isNodeOfType(expression, "Identifier")) return null;
11552
+ const symbol = resolveConstIdentifierAlias(expression, scopes);
11553
+ if (!symbol || symbol.kind !== "import") return null;
11554
+ const importDeclaration = getImportDeclarationForSymbol(symbol);
11555
+ const importedName = getImportedName(symbol.declarationNode);
11556
+ const sourceFilename = sourceFilenameByScopes.get(scopes);
11557
+ const source = importDeclaration?.source.value;
11558
+ const currentDepth = crossFileDepthByScopes.get(scopes) ?? 0;
11559
+ if (!sourceFilename || typeof source !== "string" || !importedName || currentDepth >= 2) return null;
11560
+ const resolved = resolveCrossFileFunctionExportWithFilePath(sourceFilename, source, importedName);
11561
+ if (!resolved || !isFunctionLike$1(resolved.functionNode)) return null;
11562
+ const resolvedScopes = analyzeScopes(resolved.programNode);
11563
+ registerKatexProofSource(resolvedScopes, resolved.filePath, currentDepth + 1);
11564
+ const optionsProofs = /* @__PURE__ */ new Map();
11565
+ for (const [parameterIndex, parameter] of resolved.functionNode.params.entries()) {
11566
+ if (!isNodeOfType(parameter, "ObjectPattern")) continue;
11567
+ const argument = call.arguments[parameterIndex];
11568
+ if (!argument) continue;
11569
+ for (const property of parameter.properties) {
11570
+ if (!isNodeOfType(property, "Property") || !isNodeOfType(property.value, "Identifier")) continue;
11571
+ const propertyName = getStaticPropertyKeyName(property, { allowComputedString: true });
11572
+ if (propertyName === null) continue;
11573
+ const argumentProperty = getOrderedObjectPropertyValue(argument, propertyName);
11574
+ const parameterSymbol = resolvedScopes.symbolFor(property.value);
11575
+ if (!argumentProperty.isKnown || !parameterSymbol || parameterSymbol.references.some((reference) => reference.flag !== "read")) continue;
11576
+ if (argumentProperty.value === null) {
11577
+ optionsProofs.set(parameterSymbol.id, {
11578
+ isConclusive: true,
11579
+ isSafe: true
11580
+ });
11581
+ continue;
11582
+ }
11583
+ optionsProofs.set(parameterSymbol.id, getKatexOptionsProof(argumentProperty.value, call, scopes, /* @__PURE__ */ new Set()));
11584
+ }
11585
+ }
11586
+ setKatexParameterOptionsProofs(resolvedScopes, optionsProofs);
11587
+ return getFunctionHtmlProof(resolved.functionNode, resolvedScopes, /* @__PURE__ */ new Set());
11588
+ };
11589
+ const getKatexCallProof = (node, scopes, visitedSymbolIds, parameterProofs) => {
11590
+ if (!isNodeOfType(node, "CallExpression")) return UNKNOWN_HTML_PROOF;
11591
+ const callee = stripParenExpression(node.callee);
11592
+ if (isNodeOfType(callee, "MemberExpression") && getStaticPropertyName(callee) === "renderToString" && isKatexNamespace(callee.object, node, scopes) || isKatexNamedRenderer(callee, node, scopes)) {
11593
+ const optionsProof = getKatexOptionsProof(node.arguments[1], node, scopes, /* @__PURE__ */ new Set());
11594
+ return {
11595
+ containsKatex: true,
11596
+ isConclusive: optionsProof.isConclusive,
11597
+ isSafe: optionsProof.isSafe,
11598
+ isSafeInAttributeContext: false
11599
+ };
11600
+ }
11601
+ const crossFileFunctionProof = getCrossFileFunctionProof(node, scopes);
11602
+ if (crossFileFunctionProof?.containsKatex) return crossFileFunctionProof;
11603
+ const localFunction = getLocalFunctionNode(callee, scopes);
11604
+ if (localFunction && !visitedSymbolIds.has(localFunction.symbol.id)) {
11605
+ const nextVisitedSymbolIds = new Set(visitedSymbolIds);
11606
+ nextVisitedSymbolIds.add(localFunction.symbol.id);
11607
+ const argumentProofs = node.arguments.map((argument) => {
11608
+ const argumentNode = stripParenExpression(argument);
11609
+ return isFunctionLike$1(argumentNode) ? getFunctionHtmlProof(argumentNode, scopes, new Set(visitedSymbolIds), parameterProofs) : getKatexHtmlProof(argumentNode, scopes, new Set(visitedSymbolIds), parameterProofs);
11610
+ });
11611
+ const localParameterProofs = /* @__PURE__ */ new Map();
11612
+ let hasWrittenKatexParameter = false;
11613
+ if (isFunctionLike$1(localFunction.functionNode)) for (const [parameterIndex, parameter] of localFunction.functionNode.params.entries()) {
11614
+ if (!isNodeOfType(parameter, "Identifier")) continue;
11615
+ const parameterSymbol = scopes.symbolFor(parameter);
11616
+ const argumentProof = argumentProofs[parameterIndex];
11617
+ const isParameterReadOnly = parameterSymbol?.references.every((reference) => reference.flag === "read");
11618
+ if (parameterSymbol && argumentProof && isParameterReadOnly) localParameterProofs.set(parameterSymbol.id, argumentProof);
11619
+ if (argumentProof?.containsKatex && parameterSymbol && !isParameterReadOnly) hasWrittenKatexParameter = true;
11620
+ }
11621
+ const localFunctionProof = getFunctionHtmlProof(localFunction.functionNode, scopes, nextVisitedSymbolIds, localParameterProofs);
11622
+ if (!argumentProofs.some((proof) => proof.containsKatex) || localFunctionProof.containsKatex) return localFunctionProof;
11623
+ return hasWrittenKatexParameter ? UNSUPPORTED_KATEX_PROOF : UNSAFE_KATEX_PROOF;
11624
+ }
11625
+ if (isUnprovenKatexShapedRenderer(callee, scopes)) return {
11626
+ containsKatex: true,
11627
+ isConclusive: true,
11628
+ isSafe: false,
11629
+ isSafeInAttributeContext: false
11630
+ };
11631
+ if (isNodeOfType(callee, "MemberExpression")) {
11632
+ const receiverProof = getKatexHtmlProof(callee.object, scopes, new Set(visitedSymbolIds), parameterProofs);
11633
+ if (receiverProof.containsKatex) {
11634
+ const safeTransformProof = getSafePostTransformProof(node, receiverProof);
11635
+ if (safeTransformProof) return safeTransformProof;
11636
+ return receiverProof.isConclusive ? {
11637
+ containsKatex: true,
11638
+ isConclusive: true,
11639
+ isSafe: false,
11640
+ isSafeInAttributeContext: false
11641
+ } : UNSUPPORTED_KATEX_PROOF;
11642
+ }
11643
+ }
11644
+ const sanitizerProof = getSanitizerProof(node, scopes);
11645
+ if (sanitizerProof) return sanitizerProof;
11646
+ if (isAllOpeningAngleBracketsEscaped(node, scopes)) return SAFE_HTML_FRAGMENT_PROOF;
11647
+ if (isReactUseMemo(callee, scopes)) {
11648
+ const callback = node.arguments[0];
11649
+ if (!callback) return UNKNOWN_HTML_PROOF;
11650
+ const callbackNode = stripParenExpression(callback);
11651
+ if (isFunctionLike$1(callbackNode)) return getFunctionHtmlProof(callbackNode, scopes, new Set(visitedSymbolIds), parameterProofs);
11652
+ const localCallback = getLocalFunctionNode(callbackNode, scopes);
11653
+ if (!localCallback || visitedSymbolIds.has(localCallback.symbol.id)) return UNKNOWN_HTML_PROOF;
11654
+ const nextVisitedSymbolIds = new Set(visitedSymbolIds);
11655
+ nextVisitedSymbolIds.add(localCallback.symbol.id);
11656
+ return getFunctionHtmlProof(localCallback.functionNode, scopes, nextVisitedSymbolIds, parameterProofs);
11657
+ }
11658
+ return node.arguments.some((argument) => {
11659
+ const argumentNode = stripParenExpression(argument);
11660
+ return (isFunctionLike$1(argumentNode) ? getFunctionHtmlProof(argumentNode, scopes, new Set(visitedSymbolIds), parameterProofs) : getKatexHtmlProof(argumentNode, scopes, new Set(visitedSymbolIds), parameterProofs)).containsKatex;
11661
+ }) ? UNSUPPORTED_KATEX_PROOF : UNKNOWN_HTML_PROOF;
11662
+ };
11663
+ const getKatexHtmlProof = (rawNode, scopes, visitedSymbolIds, parameterProofs = /* @__PURE__ */ new Map()) => {
11664
+ const node = stripParenExpression(rawNode);
11665
+ if (isNodeOfType(node, "Literal")) return SAFE_STATIC_HTML_PROOF;
11666
+ if (isNodeOfType(node, "UnaryExpression") && node.operator === "void") return SAFE_STATIC_HTML_PROOF;
11667
+ if (isNodeOfType(node, "Identifier")) {
11668
+ if ((node.name === "undefined" || node.name === "NaN") && scopes.isGlobalReference(node)) return SAFE_STATIC_HTML_PROOF;
11669
+ const symbol = scopes.referenceFor(node)?.resolvedSymbol;
11670
+ const parameterProof = symbol ? parameterProofs.get(symbol.id) : void 0;
11671
+ if (parameterProof) return parameterProof;
11672
+ if (!symbol || symbol.kind !== "const" || !symbol.initializer || visitedSymbolIds.has(symbol.id)) return UNKNOWN_HTML_PROOF;
11673
+ const nextVisitedSymbolIds = new Set(visitedSymbolIds);
11674
+ nextVisitedSymbolIds.add(symbol.id);
11675
+ return getKatexHtmlProof(symbol.initializer, scopes, nextVisitedSymbolIds, parameterProofs);
11676
+ }
11677
+ if (isNodeOfType(node, "CallExpression")) return getKatexCallProof(node, scopes, visitedSymbolIds, parameterProofs);
11678
+ if (isNodeOfType(node, "TemplateLiteral")) return getTemplateLiteralProof(node, scopes, visitedSymbolIds, parameterProofs);
11679
+ if (isNodeOfType(node, "ConditionalExpression")) return combineHtmlProofs([getKatexHtmlProof(node.consequent, scopes, new Set(visitedSymbolIds), parameterProofs), getKatexHtmlProof(node.alternate, scopes, new Set(visitedSymbolIds), parameterProofs)]);
11680
+ if (isNodeOfType(node, "LogicalExpression") && node.operator === "&&") return getKatexHtmlProof(node.right, scopes, visitedSymbolIds, parameterProofs);
11681
+ if (isNodeOfType(node, "BinaryExpression") && node.operator === "+" || isNodeOfType(node, "LogicalExpression")) return combineHtmlProofs([getKatexHtmlProof(node.left, scopes, new Set(visitedSymbolIds), parameterProofs), getKatexHtmlProof(node.right, scopes, new Set(visitedSymbolIds), parameterProofs)]);
11682
+ if (isNodeOfType(node, "SequenceExpression")) {
11683
+ const resultExpression = node.expressions.at(-1);
11684
+ return resultExpression ? getKatexHtmlProof(resultExpression, scopes, visitedSymbolIds, parameterProofs) : UNKNOWN_HTML_PROOF;
11685
+ }
11686
+ return UNKNOWN_HTML_PROOF;
11687
+ };
11688
+ //#endregion
11689
+ //#region src/plugin/rules/security-scan/utils/get-katex-sink-proof-ranges.ts
11690
+ const getDangerouslySetInnerHtmlExpression = (attribute) => {
11691
+ let objectExpression = null;
11692
+ if (isNodeOfType(attribute, "JSXAttribute") && isNodeOfType(attribute.name, "JSXIdentifier") && attribute.name.name === "dangerouslySetInnerHTML" && isNodeOfType(attribute.value, "JSXExpressionContainer") && isNodeOfType(attribute.value.expression, "ObjectExpression")) objectExpression = attribute.value.expression;
11693
+ if (isNodeOfType(attribute, "Property") && getStaticPropertyKeyName(attribute, { allowComputedString: true }) === "dangerouslySetInnerHTML" && isNodeOfType(stripParenExpression(attribute.value), "ObjectExpression")) objectExpression = stripParenExpression(attribute.value);
11694
+ if (!isNodeOfType(objectExpression, "ObjectExpression")) return null;
11695
+ let htmlExpression = null;
11696
+ for (const property of objectExpression.properties) {
11697
+ if (isNodeOfType(property, "SpreadElement")) {
11698
+ htmlExpression = null;
11699
+ continue;
11700
+ }
11701
+ if (!isNodeOfType(property, "Property")) {
11702
+ htmlExpression = null;
11703
+ continue;
11704
+ }
11705
+ const propertyName = getStaticPropertyKeyName(property, { allowComputedString: true });
11706
+ if (propertyName === null) {
11707
+ htmlExpression = null;
11708
+ continue;
11709
+ }
11710
+ if (propertyName === "__html") htmlExpression = property.value;
11711
+ }
11712
+ return htmlExpression;
11713
+ };
11714
+ const collectKatexSinkProofRanges = (fileContent, filename) => {
11715
+ const program = parseSourceText({
11716
+ filename,
11717
+ sourceText: fileContent
11718
+ });
11719
+ if (program === null) return [];
11720
+ const scopes = analyzeScopes(program);
11721
+ registerKatexProofSource(scopes, filename, 0);
11722
+ const ranges = [];
11723
+ walkAst(program, (node) => {
11724
+ const htmlExpression = getDangerouslySetInnerHtmlExpression(node);
11725
+ if (htmlExpression === null) return;
11726
+ const proof = getKatexHtmlProof(htmlExpression, scopes, /* @__PURE__ */ new Set());
11727
+ if (!proof.containsKatex) return;
11728
+ const startIndex = getNodeStartIndex(node);
11729
+ const endIndex = getNodeEndIndex(node);
11730
+ if (startIndex < 0 || endIndex < 0) return;
11731
+ ranges.push({
11732
+ endIndex,
11733
+ proof,
11734
+ startIndex
11735
+ });
11736
+ });
11737
+ return ranges;
11738
+ };
11739
+ //#endregion
10462
11740
  //#region src/plugin/rules/security-scan/dangerous-html-sink.ts
10463
11741
  const DANGEROUS_HTML_PATTERN = /dangerouslySetInnerHTML|(?:\.(?:inner|outer)HTML|\[\s*["'](?:inner|outer)HTML["']\s*\])\s*[+]?=(?!=)|\.insertAdjacentHTML\s*\(|\bdocument\.write(?:ln)?\s*\(|\.(?:createContextualFragment|setHTMLUnsafe)\s*\(/;
10464
11742
  const HTML_VALUE_START_PATTERN = /(?:__html\s*:|(?:\.(?:inner|outer)HTML|\[\s*["'](?:inner|outer)HTML["']\s*\])\s*[+]?=(?!=)|\.insertAdjacentHTML\s*\(\s*[^,]*,|\bdocument\.write(?:ln)?\s*\(|\.(?:createContextualFragment|setHTMLUnsafe)\s*\()\s*([\s\S]*)/;
@@ -10804,6 +12082,7 @@ const dangerousHtmlSink = defineRule({
10804
12082
  if (HIDDEN_TOOLING_DIRECTORY_PATTERN.test(file.relativePath)) return [];
10805
12083
  if (SANITIZER_WRAPPER_PATH_PATTERN.test(file.relativePath)) return [];
10806
12084
  if (!DANGEROUS_HTML_PATTERN.test(file.content)) return [];
12085
+ const katexSinkProofRanges = collectKatexSinkProofRanges(file.content, file.absolutePath);
10807
12086
  const findings = [];
10808
12087
  const lines = file.content.split("\n");
10809
12088
  for (let lineIndex = 0; lineIndex < lines.length; lineIndex += 1) {
@@ -10819,6 +12098,9 @@ const dangerousHtmlSink = defineRule({
10819
12098
  const terminatorIndex = valueTail.search(/[;}]/);
10820
12099
  const valueExpression = terminatorIndex >= 0 ? valueTail.slice(0, terminatorIndex + 1) : valueTail;
10821
12100
  const sinkIndex = lines.slice(0, lineIndex).join("\n").length + (lineIndex > 0 ? 1 : 0) + line.search(DANGEROUS_HTML_PATTERN);
12101
+ const katexSinkProof = katexSinkProofRanges.find((range) => sinkIndex >= range.startIndex && sinkIndex < range.endIndex)?.proof;
12102
+ if (katexSinkProof?.isConclusive && katexSinkProof.isSafe) continue;
12103
+ const hasUnsafeKatexProof = katexSinkProof?.isConclusive === true;
10822
12104
  if (STRING_LITERAL_VALUE_PATTERN.test(valueExpression)) continue;
10823
12105
  if (MODULE_CONSTANT_VALUE_PATTERN.test(valueExpression)) continue;
10824
12106
  if (DOM_CONTENT_SOURCE_VALUE_PATTERN.test(valueExpression) && !valueExpression.includes("+")) {
@@ -10839,21 +12121,21 @@ const dangerousHtmlSink = defineRule({
10839
12121
  if (templateInterpolations === "") continue;
10840
12122
  const judgedExpression = templateInterpolations ?? valueExpression;
10841
12123
  const doesJudgedExpressionCombineValues = splitTopLevelByPlus(judgedExpression).length > 1 || (templateInterpolations?.match(/\$\{/g)?.length ?? 0) > 1;
10842
- if (!doesJudgedExpressionCombineValues && SANITIZER_PATTERN.test(judgedExpression)) continue;
12124
+ if (!hasUnsafeKatexProof && !doesJudgedExpressionCombineValues && SANITIZER_PATTERN.test(judgedExpression)) continue;
10843
12125
  if (!doesJudgedExpressionCombineValues && ENV_CONFIG_VALUE_PATTERN.test(judgedExpression)) continue;
10844
12126
  if (!doesJudgedExpressionCombineValues && I18N_VALUE_PATTERN.test(judgedExpression)) continue;
10845
- if (!isHtmlTainted(judgedExpression, file.content, sinkIndex, /* @__PURE__ */ new Set(), /* @__PURE__ */ new Set())) continue;
10846
- if (ESCAPING_SERIALIZER_CALL_PATTERN.test(valueExpression)) continue;
10847
- if (isTrustedHighlighterValue(valueExpression, file.content, sinkIndex)) continue;
12127
+ if (!hasUnsafeKatexProof && !isHtmlTainted(judgedExpression, file.content, sinkIndex, /* @__PURE__ */ new Set(), /* @__PURE__ */ new Set())) continue;
12128
+ if (!hasUnsafeKatexProof && ESCAPING_SERIALIZER_CALL_PATTERN.test(valueExpression)) continue;
12129
+ if (!hasUnsafeKatexProof && isTrustedHighlighterValue(valueExpression, file.content, sinkIndex)) continue;
10848
12130
  if (valueIdentifier !== void 0) {
10849
12131
  const escapedIdentifier = escapeRegExp(valueIdentifier);
10850
12132
  const visibleDeclaration = findVisibleIdentifierDeclaration(valueIdentifier, sinkIndex, file.content);
10851
12133
  const visibleInitializer = visibleDeclaration?.initializer;
10852
12134
  if (!(visibleInitializer !== void 0 && (splitTopLevelByPlus(visibleInitializer).length > 1 || (visibleInitializer.match(/\$\{/g)?.length ?? 0) > 1))) {
10853
12135
  const fromSerializer = new RegExp(`\\b${escapedIdentifier}\\b\\s*${SERIALIZER_ASSIGNMENT_PATTERN.source}`, "i");
10854
- if (visibleInitializer === void 0 ? fromSerializer.test(file.content) : visibleDeclaration !== null && isDeclarationStable(valueIdentifier, visibleDeclaration, sinkIndex, file.content) && SERIALIZER_CALL_PROVENANCE_PATTERN.test(visibleInitializer)) continue;
12136
+ if (!hasUnsafeKatexProof && (visibleInitializer === void 0 ? fromSerializer.test(file.content) : visibleDeclaration !== null && isDeclarationStable(valueIdentifier, visibleDeclaration, sinkIndex, file.content) && SERIALIZER_CALL_PROVENANCE_PATTERN.test(visibleInitializer))) continue;
10855
12137
  const fromSanitizer = new RegExp(`\\b${escapedIdentifier}\\b\\s*${SANITIZED_ASSIGNMENT_PATTERN.source}`, "i");
10856
- if (visibleInitializer === void 0 ? fromSanitizer.test(file.content) : visibleDeclaration !== null && isDeclarationStable(valueIdentifier, visibleDeclaration, sinkIndex, file.content) && SANITIZED_ASSIGNMENT_PATTERN.test(`=${visibleInitializer}`)) continue;
12138
+ if (!hasUnsafeKatexProof && (visibleInitializer === void 0 ? fromSanitizer.test(file.content) : visibleDeclaration !== null && isDeclarationStable(valueIdentifier, visibleDeclaration, sinkIndex, file.content) && SANITIZED_ASSIGNMENT_PATTERN.test(`=${visibleInitializer}`))) continue;
10857
12139
  }
10858
12140
  if (new RegExp(`\\b${escapedIdentifier}\\b\\s*${DOM_CONTENT_ASSIGNMENT_PATTERN.source}`).test(file.content)) continue;
10859
12141
  if (new RegExp(`highlight[\\w$]*\\s*\\.map\\(\\s*(?:async\\s+)?\\(?\\s*${escapedIdentifier}\\b`, "i").test(file.content) && HIGHLIGHTER_LIBRARY_PATTERN.test(file.content)) continue;
@@ -12543,6 +13825,7 @@ const isNodeReachableWithinFunction = (node, context) => {
12543
13825
  const OBSERVER_REGISTRATION_METHOD_NAME = "observe";
12544
13826
  const CLEANUP_EFFECT_HOOK_NAMES = new Set([...EFFECT_HOOK_NAMES$1, "useInsertionEffect"]);
12545
13827
  const REPLAYABLE_ITERATOR_COLLECTION_CACHE = /* @__PURE__ */ new WeakMap();
13828
+ const REACT_REF_EFFECT_ANALYSIS_CACHE = /* @__PURE__ */ new WeakMap();
12546
13829
  const RESOURCE_NOUN_BY_KIND = {
12547
13830
  subscribe: "subscription",
12548
13831
  timer: "timer",
@@ -13367,7 +14650,7 @@ const hasGuardedDeferredCleanup = (callback, usage, cleanupReturns, context) =>
13367
14650
  const effectHasCleanupForUsage = (callback, usage, context) => {
13368
14651
  if (!isNodeOfType(callback, "ArrowFunctionExpression") && !isNodeOfType(callback, "FunctionExpression")) return false;
13369
14652
  if (callback.async) return false;
13370
- if (usage.kind === "subscribe" && findEnclosingFunction$1(usage.node) === callback && doesResourceResultEscape(usage.node, true) && isCleanupReturningSubscribeLikeCallExpression(usage.node)) return true;
14653
+ if (usage.kind === "subscribe" && findEnclosingFunction$1(usage.node) === callback && doesResourceResultEscape(usage.node, true, true, context) && isCleanupReturningSubscribeLikeCallExpression(usage.node)) return true;
13371
14654
  if (!isNodeOfType(callback.body, "BlockStatement")) return callback.body === usage.node && isCleanupReturningSubscribeLikeCallExpression(callback.body);
13372
14655
  const matchingCleanupReturns = [];
13373
14656
  walkInsideStatementBlocks(callback.body, (child) => {
@@ -13520,9 +14803,11 @@ const doesReleaseCallMatchUsage = (node, usage, context) => {
13520
14803
  return isNodeOfType(handlerArgument, "Literal") && handlerArgument.value === null;
13521
14804
  }
13522
14805
  if (releaseVerbName === "removeEventListener" || releaseVerbName === "removeListener" || releaseVerbName === "off") {
13523
- const releaseHandler = callNode.arguments?.[1];
14806
+ const usesUnaryListenerSignature = usage.registrationVerbName === "addListener" && isNodeOfType(usage.node, "CallExpression") && usage.node.arguments?.length === 1 && callNode.arguments?.length === 1;
14807
+ const releaseHandler = usesUnaryListenerSignature ? callNode.arguments?.[0] : callNode.arguments?.[1];
13524
14808
  if (!releaseHandler) return releaseVerbName === "off";
13525
- return usage.handlerKey !== null && resolveExpressionKey(releaseHandler, context) === usage.handlerKey;
14809
+ const expectedHandlerKey = usesUnaryListenerSignature ? usage.eventKey : usage.handlerKey;
14810
+ return expectedHandlerKey !== null && resolveExpressionKey(releaseHandler, context) === expectedHandlerKey;
13526
14811
  }
13527
14812
  if (releaseVerbName === "unobserve" && usage.eventKey !== null) return releaseEventKey === usage.eventKey;
13528
14813
  return true;
@@ -13553,6 +14838,8 @@ const isReleaseReachableForUsage = (releaseNode, usage, context) => {
13553
14838
  const releaseFunction = findEnclosingFunction$1(releaseNode);
13554
14839
  if (!releaseFunction) return true;
13555
14840
  if (releaseFunction === findEnclosingFunction$1(usage.node)) return true;
14841
+ const usageFunction = findEnclosingFunction$1(usage.node);
14842
+ if (usageFunction && isFunctionLike$1(usageFunction) && getAssignedReactRefSymbol(usageFunction, context) && isCleanupFunctionReferencedByReturn(usageFunction, releaseFunction, context)) return isReactRefCallbackCleanupOwnedByEffect(usageFunction, releaseFunction, usage, context);
13556
14843
  return isPotentiallyReachableFunction(releaseFunction, context);
13557
14844
  };
13558
14845
  const fileContainsReleaseForUsage = (usage, context) => {
@@ -13793,7 +15080,23 @@ const isUseSyncExternalStoreSubscribeFunction = (functionNode, context) => {
13793
15080
  };
13794
15081
  return isSubscribeBinding(bindingIdentifier);
13795
15082
  };
13796
- const doesResourceResultEscape = (resourceNode, allowConciseReturnEscape) => {
15083
+ const findUnconditionalReturnStatement = (expression, ownerFunction) => {
15084
+ let expressionRoot = findTransparentExpressionRoot(expression);
15085
+ while (isNodeOfType(expressionRoot.parent, "SequenceExpression") && expressionRoot.parent.expressions.at(-1) === expressionRoot) expressionRoot = findTransparentExpressionRoot(expressionRoot.parent);
15086
+ const returnStatement = expressionRoot.parent;
15087
+ return isNodeOfType(returnStatement, "ReturnStatement") && returnStatement.argument === expressionRoot && findEnclosingFunction$1(returnStatement) === ownerFunction ? returnStatement : null;
15088
+ };
15089
+ const getFinalSequenceExpressionValue = (expression) => {
15090
+ let finalExpression = stripParenExpression(expression);
15091
+ while (isNodeOfType(finalExpression, "SequenceExpression")) {
15092
+ const sequenceResult = finalExpression.expressions.at(-1);
15093
+ if (!sequenceResult) break;
15094
+ finalExpression = stripParenExpression(sequenceResult);
15095
+ }
15096
+ return finalExpression;
15097
+ };
15098
+ const doesResourceResultEscape = (resourceNode, allowReturnedResourceEscape, allowConciseReturnEscape, context) => {
15099
+ if (!allowReturnedResourceEscape) return false;
13797
15100
  let currentNode = resourceNode;
13798
15101
  let parentNode = currentNode.parent;
13799
15102
  while (parentNode) {
@@ -13804,22 +15107,38 @@ const doesResourceResultEscape = (resourceNode, allowConciseReturnEscape) => {
13804
15107
  parentNode = currentNode.parent;
13805
15108
  continue;
13806
15109
  }
15110
+ if (isNodeOfType(parentNode, "ConditionalExpression") && (parentNode.consequent === currentNode || parentNode.alternate === currentNode) || isNodeOfType(parentNode, "LogicalExpression") && (parentNode.right === currentNode || parentNode.left === currentNode && parentNode.operator !== "&&")) {
15111
+ currentNode = parentNode;
15112
+ parentNode = currentNode.parent;
15113
+ continue;
15114
+ }
15115
+ if (isNodeOfType(parentNode, "VariableDeclarator") && parentNode.init === currentNode && isNodeOfType(parentNode.id, "Identifier") && isNodeOfType(parentNode.parent, "VariableDeclaration") && parentNode.parent.kind === "const") {
15116
+ const ownerFunction = findEnclosingFunction$1(resourceNode);
15117
+ const resourceSymbol = context.scopes.symbolFor(parentNode.id);
15118
+ if (!ownerFunction || !resourceSymbol) return false;
15119
+ return doMatchingNodesCoverEveryPathAfterUsage(resourceNode, resourceSymbol.references.flatMap((reference) => {
15120
+ if (reference.flag !== "read") return [];
15121
+ const returnStatement = findUnconditionalReturnStatement(reference.identifier, ownerFunction);
15122
+ return returnStatement ? [returnStatement] : [];
15123
+ }), context);
15124
+ }
13807
15125
  return false;
13808
15126
  }
13809
15127
  return false;
13810
15128
  };
13811
- const findRetainedFunctionLeak = (retainedFunction, context) => {
15129
+ const findRetainedFunctionLeak = (retainedFunction, context, options) => {
13812
15130
  if (!isFunctionLike$1(retainedFunction)) return null;
13813
15131
  const body = retainedFunction.body;
13814
15132
  if (!body) return null;
13815
15133
  let leak = null;
13816
- const allowConciseReturnEscape = !isInlineRetainedHandlerFunction(retainedFunction, context);
15134
+ const allowReturnedResourceEscape = options?.allowReturnedResourceEscape !== false && !retainedFunction.async && !isInlineRetainedHandlerFunction(retainedFunction, context);
15135
+ const allowReturnedSocketEscape = allowReturnedResourceEscape && options?.requireCallableReturnedResource !== true;
13817
15136
  const isExternalStoreSubscribeFunction = isUseSyncExternalStoreSubscribeFunction(retainedFunction, context);
13818
15137
  const hasReleaseForUsage = (usage) => isExternalStoreSubscribeFunction ? effectHasCleanupForUsage(retainedFunction, usage, context) : fileContainsReleaseForUsage(usage, context) || hasGuaranteedRefOwnedUnmountCleanup(retainedFunction, usage, context);
13819
15138
  walkAst(body, (child) => {
13820
15139
  if (leak !== null) return false;
13821
15140
  if (isFunctionLike$1(child)) return false;
13822
- if (isSocketConstruction(child) && !doesResourceResultEscape(child, false)) {
15141
+ if (isSocketConstruction(child) && !doesResourceResultEscape(child, allowReturnedSocketEscape, false, context)) {
13823
15142
  const socketUsage = {
13824
15143
  kind: "socket",
13825
15144
  node: child,
@@ -13836,14 +15155,14 @@ const findRetainedFunctionLeak = (retainedFunction, context) => {
13836
15155
  }
13837
15156
  }
13838
15157
  if (!isNodeOfType(child, "CallExpression")) return;
13839
- if (isNodeOfType(child.callee, "Identifier") && child.callee.name === "setInterval" && !doesResourceResultEscape(child, allowConciseReturnEscape)) {
15158
+ if (isNodeOfType(child.callee, "Identifier") && (child.callee.name === "setInterval" || options?.includeOneShotTimers === true && child.callee.name === "setTimeout" && context.scopes.isGlobalReference(child.callee)) && (options?.allowReturnedTimerEscape === false || !doesResourceResultEscape(child, true, allowReturnedResourceEscape, context))) {
13840
15159
  const timerUsage = {
13841
15160
  kind: "timer",
13842
15161
  node: child,
13843
- resourceName: "setInterval",
15162
+ resourceName: child.callee.name,
13844
15163
  handleKey: findAssignedResourceKey(child, context),
13845
15164
  receiverKey: null,
13846
- registrationVerbName: "setInterval",
15165
+ registrationVerbName: child.callee.name,
13847
15166
  eventKey: null,
13848
15167
  handlerKey: null
13849
15168
  };
@@ -13852,7 +15171,7 @@ const findRetainedFunctionLeak = (retainedFunction, context) => {
13852
15171
  return false;
13853
15172
  }
13854
15173
  }
13855
- if (isSubscribeOrObserveCall(child) && !doesResourceResultEscape(child, allowConciseReturnEscape)) {
15174
+ if (isSubscribeOrObserveCall(child) && (!doesResourceResultEscape(child, allowReturnedResourceEscape, allowReturnedResourceEscape, context) || options?.requireCallableReturnedResource === true && !isCleanupReturningSubscribeLikeCallExpression(child))) {
13856
15175
  const registrationDetails = getCallRegistrationDetails(child, context);
13857
15176
  const subscriptionUsage = {
13858
15177
  kind: "subscribe",
@@ -13867,6 +15186,184 @@ const findRetainedFunctionLeak = (retainedFunction, context) => {
13867
15186
  });
13868
15187
  return leak;
13869
15188
  };
15189
+ const getAssignedReactRefCallbackDefinition = (functionNode, context) => {
15190
+ if (!isFunctionLike$1(functionNode)) return null;
15191
+ if (functionNode.generator) return null;
15192
+ const functionRoot = findTransparentExpressionRoot(functionNode);
15193
+ const assignment = functionRoot.parent;
15194
+ if (!isNodeOfType(assignment, "AssignmentExpression") || assignment.operator !== "=" || assignment.right !== functionRoot) return null;
15195
+ const refSymbol = resolveReactRefSymbol(stripParenExpression(assignment.left), context.scopes);
15196
+ if (!refSymbol) return null;
15197
+ const componentFunction = findRenderPhaseComponentOrHook(assignment, context.scopes);
15198
+ if (!isFunctionLike$1(componentFunction) || findEnclosingFunction$1(assignment) !== componentFunction || findEnclosingFunction$1(refSymbol.bindingIdentifier) !== componentFunction || !isNodeReachableWithinFunction(assignment, context)) return null;
15199
+ return {
15200
+ assignmentNode: assignment,
15201
+ functionNode,
15202
+ refSymbol
15203
+ };
15204
+ };
15205
+ const getAssignedReactRefSymbol = (functionNode, context) => getAssignedReactRefCallbackDefinition(functionNode, context)?.refSymbol ?? null;
15206
+ const isExpressionReturnedFromFunction = (expression, ownerFunction, context) => {
15207
+ let expressionRoot = findTransparentExpressionRoot(expression);
15208
+ const bindingDeclarator = expressionRoot.parent;
15209
+ if (isNodeOfType(bindingDeclarator, "VariableDeclarator") && bindingDeclarator.init === expressionRoot && isNodeOfType(bindingDeclarator.id, "Identifier") && isNodeOfType(bindingDeclarator.parent, "VariableDeclaration") && bindingDeclarator.parent.kind === "const") {
15210
+ const resultSymbol = context.scopes.symbolFor(bindingDeclarator.id);
15211
+ if (!resultSymbol) return false;
15212
+ return doMatchingNodesCoverEveryPathAfterUsage(expression, resultSymbol.references.flatMap((reference) => {
15213
+ if (reference.flag !== "read") return [];
15214
+ const returnStatement = findUnconditionalReturnStatement(reference.identifier, ownerFunction);
15215
+ return returnStatement ? [returnStatement] : [];
15216
+ }), context);
15217
+ }
15218
+ while (true) {
15219
+ const container = expressionRoot.parent;
15220
+ if (isNodeOfType(container, "ConditionalExpression") && (container.consequent === expressionRoot || container.alternate === expressionRoot)) {
15221
+ expressionRoot = findTransparentExpressionRoot(container);
15222
+ continue;
15223
+ }
15224
+ if (isNodeOfType(container, "SequenceExpression") && container.expressions.at(-1) === expressionRoot) {
15225
+ expressionRoot = findTransparentExpressionRoot(container);
15226
+ continue;
15227
+ }
15228
+ if (isNodeOfType(container, "LogicalExpression") && container.right === expressionRoot) {
15229
+ expressionRoot = findTransparentExpressionRoot(container);
15230
+ continue;
15231
+ }
15232
+ break;
15233
+ }
15234
+ const returnStatement = expressionRoot.parent;
15235
+ return Boolean(isNodeOfType(returnStatement, "ReturnStatement") && returnStatement.argument === expressionRoot && findEnclosingFunction$1(returnStatement) === ownerFunction || isNodeOfType(ownerFunction, "ArrowFunctionExpression") && ownerFunction.body === expressionRoot);
15236
+ };
15237
+ const isReactRefCurrentCall = (node, refSymbol, context) => isNodeOfType(node, "CallExpression") && resolveReactRefSymbol(stripParenExpression(node.callee), context.scopes)?.id === refSymbol.id;
15238
+ const collectAssignedReactRefCallbacks = (componentFunction, context) => {
15239
+ const callbackDefinitionsByRefSymbolId = /* @__PURE__ */ new Map();
15240
+ walkAst(componentFunction.body, (child) => {
15241
+ if (!isFunctionLike$1(child)) return;
15242
+ const callbackDefinition = getAssignedReactRefCallbackDefinition(child, context);
15243
+ if (callbackDefinition) {
15244
+ const existingDefinitions = callbackDefinitionsByRefSymbolId.get(callbackDefinition.refSymbol.id) ?? [];
15245
+ existingDefinitions.push(callbackDefinition);
15246
+ callbackDefinitionsByRefSymbolId.set(callbackDefinition.refSymbol.id, existingDefinitions);
15247
+ }
15248
+ return false;
15249
+ });
15250
+ for (const [refSymbolId, callbackDefinitions] of callbackDefinitionsByRefSymbolId) {
15251
+ const activeDefinitions = callbackDefinitions.filter((callbackDefinition) => !doMatchingNodesCoverEveryPathAfterUsage(callbackDefinition.assignmentNode, callbackDefinitions.filter((otherDefinition) => otherDefinition !== callbackDefinition).map((otherDefinition) => otherDefinition.assignmentNode), context));
15252
+ if (activeDefinitions.length === 0) callbackDefinitionsByRefSymbolId.delete(refSymbolId);
15253
+ else callbackDefinitionsByRefSymbolId.set(refSymbolId, activeDefinitions);
15254
+ }
15255
+ return callbackDefinitionsByRefSymbolId;
15256
+ };
15257
+ const collectUndominatedReactRefCalls = (ownerFunction, refSymbol, context) => {
15258
+ if (!isFunctionLike$1(ownerFunction)) return [];
15259
+ const refWrites = [];
15260
+ const refCalls = [];
15261
+ walkAst(ownerFunction.body, (child) => {
15262
+ if (child !== ownerFunction.body && isFunctionLike$1(child)) return false;
15263
+ if (isNodeOfType(child, "AssignmentExpression") && isNodeReachableWithinFunction(child, context) && resolveReactRefSymbol(stripParenExpression(child.left), context.scopes)?.id === refSymbol.id) refWrites.push(child);
15264
+ if (isReactRefCurrentCall(child, refSymbol, context) && isNodeReachableWithinFunction(child, context)) refCalls.push(child);
15265
+ });
15266
+ return refCalls.filter((refCall) => !doMatchingNodesCoverEveryPathBeforeUsage(refCall, refWrites, ownerFunction, context));
15267
+ };
15268
+ const mergeReactRefEffectUsage = (usageByRefSymbolId, refSymbolId, doesEffectOwnResult) => {
15269
+ const existingUsage = usageByRefSymbolId.get(refSymbolId);
15270
+ if (!existingUsage) {
15271
+ usageByRefSymbolId.set(refSymbolId, { doesEffectOwnEveryResult: doesEffectOwnResult });
15272
+ return true;
15273
+ }
15274
+ if (!existingUsage.doesEffectOwnEveryResult || doesEffectOwnResult) return false;
15275
+ existingUsage.doesEffectOwnEveryResult = false;
15276
+ return true;
15277
+ };
15278
+ const collectReactRefEffectAnalysis = (componentFunction, context) => {
15279
+ let analysisByComponent = REACT_REF_EFFECT_ANALYSIS_CACHE.get(context);
15280
+ if (!analysisByComponent) {
15281
+ analysisByComponent = /* @__PURE__ */ new WeakMap();
15282
+ REACT_REF_EFFECT_ANALYSIS_CACHE.set(context, analysisByComponent);
15283
+ }
15284
+ const cachedAnalysis = analysisByComponent.get(componentFunction);
15285
+ if (cachedAnalysis) return cachedAnalysis;
15286
+ const callbackDefinitionsByRefSymbolId = collectAssignedReactRefCallbacks(componentFunction, context);
15287
+ const usageByRefSymbolId = /* @__PURE__ */ new Map();
15288
+ walkAst(componentFunction.body, (child) => {
15289
+ if (child !== componentFunction.body && isFunctionLike$1(child)) return false;
15290
+ if (!isNodeOfType(child, "CallExpression") || findEnclosingFunction$1(child) !== componentFunction || !isReactApiCall(child, CLEANUP_EFFECT_HOOK_NAMES, context.scopes, { allowGlobalReactNamespace: true })) return;
15291
+ const effectCallback = getEffectCallback(child);
15292
+ if (!isFunctionLike$1(effectCallback)) return;
15293
+ for (const callbackDefinitions of callbackDefinitionsByRefSymbolId.values()) {
15294
+ const refSymbol = callbackDefinitions[0]?.refSymbol;
15295
+ if (!refSymbol) continue;
15296
+ for (const refCall of collectUndominatedReactRefCalls(effectCallback, refSymbol, context)) mergeReactRefEffectUsage(usageByRefSymbolId, refSymbol.id, !effectCallback.async && isExpressionReturnedFromFunction(refCall, effectCallback, context));
15297
+ }
15298
+ });
15299
+ let didUsageChange = true;
15300
+ while (didUsageChange) {
15301
+ didUsageChange = false;
15302
+ for (const callbackDefinitions of callbackDefinitionsByRefSymbolId.values()) {
15303
+ const ownerRefSymbol = callbackDefinitions[0]?.refSymbol;
15304
+ if (!ownerRefSymbol) continue;
15305
+ const ownerUsage = usageByRefSymbolId.get(ownerRefSymbol.id);
15306
+ if (!ownerUsage) continue;
15307
+ for (const callbackDefinition of callbackDefinitions) for (const targetDefinitions of callbackDefinitionsByRefSymbolId.values()) {
15308
+ const targetRefSymbol = targetDefinitions[0]?.refSymbol;
15309
+ if (!targetRefSymbol) continue;
15310
+ for (const refCall of collectUndominatedReactRefCalls(callbackDefinition.functionNode, targetRefSymbol, context)) {
15311
+ const doesEffectOwnResult = ownerUsage.doesEffectOwnEveryResult && !callbackDefinition.functionNode.async && isExpressionReturnedFromFunction(refCall, callbackDefinition.functionNode, context);
15312
+ if (mergeReactRefEffectUsage(usageByRefSymbolId, targetRefSymbol.id, doesEffectOwnResult)) didUsageChange = true;
15313
+ }
15314
+ }
15315
+ }
15316
+ }
15317
+ const analysis = {
15318
+ callbackDefinitionsByRefSymbolId,
15319
+ usageByRefSymbolId
15320
+ };
15321
+ analysisByComponent.set(componentFunction, analysis);
15322
+ return analysis;
15323
+ };
15324
+ const getReactRefEffectUsage = (retainedFunction, context) => {
15325
+ if (!isFunctionLike$1(retainedFunction)) return null;
15326
+ const callbackDefinition = getAssignedReactRefCallbackDefinition(retainedFunction, context);
15327
+ const componentFunction = findRenderPhaseComponentOrHook(retainedFunction, context.scopes);
15328
+ if (!callbackDefinition || !isFunctionLike$1(componentFunction)) return null;
15329
+ const analysis = collectReactRefEffectAnalysis(componentFunction, context);
15330
+ if (!analysis.callbackDefinitionsByRefSymbolId.get(callbackDefinition.refSymbol.id)?.some((activeDefinition) => activeDefinition.functionNode === retainedFunction)) return null;
15331
+ return analysis.usageByRefSymbolId.get(callbackDefinition.refSymbol.id) ?? null;
15332
+ };
15333
+ const isReactRefCallbackCleanupOwnedByEffect = (retainedFunction, cleanupFunction, usage, context) => {
15334
+ if (!isFunctionLike$1(retainedFunction) || retainedFunction.async || getReactRefEffectUsage(retainedFunction, context)?.doesEffectOwnEveryResult !== true) return false;
15335
+ if (!isNodeOfType(retainedFunction.body, "BlockStatement")) return false;
15336
+ const doesReturnedCleanupCallFunction = (returnedValue) => {
15337
+ const returnedCleanupFunction = resolveRefOwnedCleanupFunction(getFinalSequenceExpressionValue(returnedValue), context);
15338
+ if (!returnedCleanupFunction) return false;
15339
+ if (returnedCleanupFunction === cleanupFunction) return true;
15340
+ if (!isFunctionLike$1(returnedCleanupFunction)) return false;
15341
+ const matchingCalls = [];
15342
+ walkAst(returnedCleanupFunction.body, (child) => {
15343
+ if (child !== returnedCleanupFunction.body && isFunctionLike$1(child)) return false;
15344
+ if (isNodeOfType(child, "CallExpression") && resolveRefOwnedCleanupFunction(child.callee, context) === cleanupFunction) matchingCalls.push(child);
15345
+ });
15346
+ return doMatchingNodesCoverEveryPathFromFunctionEntry(returnedCleanupFunction, matchingCalls, context);
15347
+ };
15348
+ const matchingReturns = [];
15349
+ walkInsideStatementBlocks(retainedFunction.body, (child) => {
15350
+ if (isNodeOfType(child, "ReturnStatement") && child.argument && doesReturnedCleanupCallFunction(child.argument)) matchingReturns.push(child);
15351
+ });
15352
+ return doMatchingNodesCoverEveryPathAfterUsage(usage.node, matchingReturns, context);
15353
+ };
15354
+ const isCleanupFunctionReferencedByReturn = (ownerFunction, cleanupFunction, context) => {
15355
+ if (!isFunctionLike$1(ownerFunction) || !isNodeOfType(ownerFunction.body, "BlockStatement")) return false;
15356
+ let isReferencedByReturn = false;
15357
+ walkInsideStatementBlocks(ownerFunction.body, (child) => {
15358
+ if (isReferencedByReturn || !isNodeOfType(child, "ReturnStatement") || !child.argument) return;
15359
+ walkAst(child.argument, (returnedChild) => {
15360
+ if (resolveRefOwnedCleanupFunction(returnedChild, context) !== cleanupFunction) return;
15361
+ isReferencedByReturn = true;
15362
+ return false;
15363
+ });
15364
+ });
15365
+ return isReferencedByReturn;
15366
+ };
13870
15367
  const isRetainedComponentScopeFunction = (functionNode) => {
13871
15368
  if (isNodeOfType(functionNode, "FunctionDeclaration")) return enclosingComponentOrHookName(functionNode) !== null;
13872
15369
  if (!isNodeOfType(functionNode, "ArrowFunctionExpression") && !isNodeOfType(functionNode, "FunctionExpression")) return false;
@@ -13901,8 +15398,14 @@ const effectNeedsCleanup = defineRule({
13901
15398
  recommendation: "Return a cleanup function that stops the subscription or timer: `return () => target.removeEventListener(name, handler)` for listeners, `return () => clearInterval(id)` or `clearTimeout(id)` for timers, `return () => observer.disconnect()` for observers, `return () => socket.close()` for connections, or `return unsubscribe` if the subscribe call already gave you one.",
13902
15399
  create: (context) => {
13903
15400
  const reportRetainedLeak = (retainedFunction) => {
13904
- if (!isPotentiallyReachableFunction(retainedFunction, context)) return;
13905
- const leak = findRetainedFunctionLeak(retainedFunction, context);
15401
+ const refEffectUsage = getReactRefEffectUsage(retainedFunction, context);
15402
+ if (!refEffectUsage && !isPotentiallyReachableFunction(retainedFunction, context)) return;
15403
+ const leak = findRetainedFunctionLeak(retainedFunction, context, refEffectUsage ? {
15404
+ allowReturnedResourceEscape: refEffectUsage.doesEffectOwnEveryResult,
15405
+ allowReturnedTimerEscape: false,
15406
+ includeOneShotTimers: true,
15407
+ requireCallableReturnedResource: true
15408
+ } : void 0);
13906
15409
  if (!leak) return;
13907
15410
  const resourceNoun = RESOURCE_NOUN_BY_KIND[leak.kind];
13908
15411
  context.report({
@@ -13935,10 +15438,10 @@ const effectNeedsCleanup = defineRule({
13935
15438
  if (isRetainedComponentScopeFunction(node)) reportRetainedLeak(node);
13936
15439
  },
13937
15440
  ArrowFunctionExpression(node) {
13938
- if (isRetainedComponentScopeFunction(node) || isInlineRetainedHandlerFunction(node, context)) reportRetainedLeak(node);
15441
+ if (isRetainedComponentScopeFunction(node) || isInlineRetainedHandlerFunction(node, context) || getAssignedReactRefSymbol(node, context)) reportRetainedLeak(node);
13939
15442
  },
13940
15443
  FunctionExpression(node) {
13941
- if (isRetainedComponentScopeFunction(node) || isInlineRetainedHandlerFunction(node, context)) reportRetainedLeak(node);
15444
+ if (isRetainedComponentScopeFunction(node) || isInlineRetainedHandlerFunction(node, context) || getAssignedReactRefSymbol(node, context)) reportRetainedLeak(node);
13942
15445
  }
13943
15446
  };
13944
15447
  }
@@ -17340,6 +18843,7 @@ const iframeHasTitle = defineRule({
17340
18843
  recommendation: "Add a descriptive `title` so screen reader users know what the embedded frame contains.",
17341
18844
  category: "Accessibility",
17342
18845
  create: (context) => ({ JSXOpeningElement(node) {
18846
+ if (isLocalTestScaffoldJsx(node, context)) return;
17343
18847
  const tag = getElementType(node, context.settings);
17344
18848
  if (tag !== "iframe") return;
17345
18849
  if (isInsideStaticallyHiddenJsxSubtree(node, context.scopes)) return;
@@ -17860,6 +19364,7 @@ const interactiveSupportsFocus = defineRule({
17860
19364
  const settings = resolveSettings$37(context.settings);
17861
19365
  const tabbableSet = new Set(settings.tabbable);
17862
19366
  return { JSXOpeningElement(node) {
19367
+ if (isLocalTestScaffoldJsx(node, context)) return;
17863
19368
  if (node.attributes.length === 0) return;
17864
19369
  if (hasJsxSpreadAttribute$1(node.attributes)) return;
17865
19370
  const roleAttribute = hasJsxPropIgnoreCase(node.attributes, "role");
@@ -18573,7 +20078,7 @@ const scanPerIterationLayoutReads = (body) => {
18573
20078
  hasDeliberateForcedReflow
18574
20079
  };
18575
20080
  };
18576
- const getNodeStart$1 = (node) => {
20081
+ const getNodeStart = (node) => {
18577
20082
  const withRange = node;
18578
20083
  return withRange.range ? withRange.range[0] : -1;
18579
20084
  };
@@ -18605,7 +20110,7 @@ const hasAttachmentBefore = (scopeOwner, elementName, beforeStart) => {
18605
20110
  if (!isNodeOfType(child, "CallExpression")) return;
18606
20111
  const callee = child.callee;
18607
20112
  if (!isNodeOfType(callee, "MemberExpression") || !isNodeOfType(callee.property, "Identifier") || !DOM_ATTACHMENT_METHOD_NAMES.has(callee.property.name)) return;
18608
- if (child.arguments.some((argument) => isNodeOfType(argument, "Identifier") && argument.name === elementName) && getNodeStart$1(child) < beforeStart) {
20113
+ if (child.arguments.some((argument) => isNodeOfType(argument, "Identifier") && argument.name === elementName) && getNodeStart(child) < beforeStart) {
18609
20114
  foundAttachment = true;
18610
20115
  return false;
18611
20116
  }
@@ -18627,7 +20132,7 @@ const isProvablyDetachedAtWrite = (styleWriteStatement) => {
18627
20132
  const elementExpression = assignment.left.object.object;
18628
20133
  const creationRoot = resolveDetachedCreationRoot(elementExpression, 0);
18629
20134
  if (!creationRoot) return false;
18630
- return !hasAttachmentBefore(creationRoot.scopeOwner, creationRoot.rootName, getNodeStart$1(styleWriteStatement));
20135
+ return !hasAttachmentBefore(creationRoot.scopeOwner, creationRoot.rootName, getNodeStart(styleWriteStatement));
18631
20136
  };
18632
20137
  const jsBatchDomCss = defineRule({
18633
20138
  id: "js-batch-dom-css",
@@ -19350,7 +20855,7 @@ const globSyncReturnsStringPaths = (node, context) => {
19350
20855
  const callee = stripParenExpression(node.callee);
19351
20856
  let isGlobSyncImport = false;
19352
20857
  if (isNodeOfType(callee, "Identifier")) isGlobSyncImport = context.scopes.symbolFor(callee)?.kind === "import" && getImportedNameFromModule(callee, callee.name, "glob") === "globSync";
19353
- else if (isNodeOfType(callee, "MemberExpression") && !callee.computed && isNodeOfType(callee.object, "Identifier") && isNodeOfType(callee.property, "Identifier") && callee.property.name === "globSync") isGlobSyncImport = context.scopes.symbolFor(callee.object)?.kind === "import" && isNamespaceImportFromModule(callee.object, callee.object.name, "glob");
20858
+ else if (isNodeOfType(callee, "MemberExpression") && !callee.computed && isNodeOfType(callee.object, "Identifier") && isNodeOfType(callee.property, "Identifier") && callee.property.name === "globSync") isGlobSyncImport = context.scopes.symbolFor(callee.object)?.kind === "import" && isNamespaceImportFromModule$1(callee.object, callee.object.name, "glob");
19354
20859
  if (!isGlobSyncImport) return false;
19355
20860
  const options = node.arguments[1];
19356
20861
  if (!options) return true;
@@ -25732,6 +27237,7 @@ const mediaHasCaption = defineRule({
25732
27237
  create: (context) => {
25733
27238
  const settings = resolveSettings$23(context.settings);
25734
27239
  return { JSXOpeningElement(node) {
27240
+ if (isLocalTestScaffoldJsx(node, context)) return;
25735
27241
  const tag = getElementType(node, context.settings);
25736
27242
  if (!(settings.audio.has(tag) || settings.video.has(tag))) return;
25737
27243
  if (evaluateMuted(hasJsxPropIgnoreCase(node.attributes, "muted")) === true) return;
@@ -25799,6 +27305,7 @@ const mouseEventsHaveKeyEvents = defineRule({
25799
27305
  create: (context) => {
25800
27306
  const settings = resolveSettings$22(context.settings);
25801
27307
  return { JSXOpeningElement(node) {
27308
+ if (isLocalTestScaffoldJsx(node, context)) return;
25802
27309
  const tag = getElementType(node, context.settings);
25803
27310
  if (!HTML_TAGS.has(tag)) return;
25804
27311
  for (const handler of settings.hoverInHandlers) {
@@ -25840,7 +27347,11 @@ const mouseEventsHaveKeyEvents = defineRule({
25840
27347
  //#region src/plugin/utils/has-directive.ts
25841
27348
  const hasDirective = (programNode, directive) => {
25842
27349
  if (!isNodeOfType(programNode, "Program")) return false;
25843
- return Boolean(programNode.body?.some((statement) => isNodeOfType(statement, "ExpressionStatement") && isNodeOfType(statement.expression, "Literal") && statement.expression.value === directive));
27350
+ for (const statement of programNode.body) {
27351
+ if (!isNodeOfType(statement, "ExpressionStatement") || statement.directive === void 0) return false;
27352
+ if (statement.directive === directive) return true;
27353
+ }
27354
+ return false;
25844
27355
  };
25845
27356
  //#endregion
25846
27357
  //#region src/plugin/rules/nextjs/nextjs-async-client-component.ts
@@ -28469,7 +29980,11 @@ const isProvenNativeReadMethod = (ref, methodName) => Boolean(ref.resolved?.defs
28469
29980
  }));
28470
29981
  //#endregion
28471
29982
  //#region src/plugin/rules/state-and-effects/utils/effect/react.ts
28472
- const KNOWN_PURE_HOC_NAMES = new Set(["memo", "forwardRef"]);
29983
+ const KNOWN_COMPONENT_WRAPPER_NAMES = new Set([
29984
+ "memo",
29985
+ "forwardRef",
29986
+ "observer"
29987
+ ]);
28473
29988
  const startsWithUppercase = (name) => Boolean(name && name.length > 0 && name[0] >= "A" && name[0] <= "Z");
28474
29989
  const isReactFunctionalComponent = (node) => {
28475
29990
  if (!node) return false;
@@ -28491,7 +30006,7 @@ const isReactFunctionalHOC = (analysis, node) => {
28491
30006
  const isWrappedInline = () => {
28492
30007
  if (!isNodeOfType(init, "CallExpression")) return false;
28493
30008
  if (!isNodeOfType(init.callee, "Identifier")) return false;
28494
- if (KNOWN_PURE_HOC_NAMES.has(init.callee.name)) return false;
30009
+ if (KNOWN_COMPONENT_WRAPPER_NAMES.has(init.callee.name)) return false;
28495
30010
  const firstArg = init.arguments?.[0];
28496
30011
  if (!firstArg) return false;
28497
30012
  return isNodeOfType(firstArg, "ArrowFunctionExpression") || isNodeOfType(firstArg, "FunctionExpression");
@@ -28511,7 +30026,7 @@ const isReactFunctionalHOC = (analysis, node) => {
28511
30026
  if (!args.includes(refId)) continue;
28512
30027
  const callee = parent.callee;
28513
30028
  const calleeName = isNodeOfType(callee, "Identifier") ? callee.name : isNodeOfType(callee, "CallExpression") && isNodeOfType(callee.callee, "Identifier") ? callee.callee.name : null;
28514
- if (calleeName != null && !KNOWN_PURE_HOC_NAMES.has(calleeName)) return true;
30029
+ if (calleeName != null && !KNOWN_COMPONENT_WRAPPER_NAMES.has(calleeName)) return true;
28515
30030
  }
28516
30031
  return false;
28517
30032
  };
@@ -31934,7 +33449,7 @@ const isReactDomCreatePortalCall = (node, scopes) => {
31934
33449
  if (!isNodeOfType(callee, "MemberExpression") || callee.computed || !isNodeOfType(callee.object, "Identifier") || !isNodeOfType(callee.property, "Identifier") || callee.property.name !== "createPortal") return false;
31935
33450
  const symbol = scopes.symbolFor(callee.object);
31936
33451
  if (!symbol || symbol.kind !== "import") return false;
31937
- return isDefaultImportFromModule(callee.object, callee.object.name, "react-dom") || isNamespaceImportFromModule(callee.object, callee.object.name, "react-dom");
33452
+ return isDefaultImportFromModule(callee.object, callee.object.name, "react-dom") || isNamespaceImportFromModule$1(callee.object, callee.object.name, "react-dom");
31938
33453
  };
31939
33454
  const containsRenderOutput$1 = (rootNode, scopes) => {
31940
33455
  let hasRenderOutput = false;
@@ -32663,6 +34178,7 @@ const noChainStateUpdates = defineRule({
32663
34178
  id: "no-chain-state-updates",
32664
34179
  title: "State updates chained through effects",
32665
34180
  severity: "warn",
34181
+ disabledWhen: ["react:18"],
32666
34182
  tags: ["test-noise"],
32667
34183
  recommendation: "Set all the related state together in the event handler that starts it, instead of having one useEffect react to a state change and set more state. See https://react.dev/learn/you-might-not-need-an-effect#chains-of-computations",
32668
34184
  create: (context) => ({ CallExpression(node) {
@@ -32939,16 +34455,7 @@ const isProvenIntrinsicJsxElement = (openingElement, scopes) => {
32939
34455
  return isIntrinsicValue(openingElement.name);
32940
34456
  };
32941
34457
  //#endregion
32942
- //#region src/plugin/rules/react-builtins/is-safe-create-ref-callback-current-write.ts
32943
- const pathStartsWith$1 = (propertyPath, prefix) => prefix.every((propertyName, index) => propertyPath[index] === propertyName);
32944
- const collectMemberExpression = (identifier) => {
32945
- let expression = findTransparentExpressionRoot(identifier);
32946
- while (expression.parent && isNodeOfType(expression.parent, "MemberExpression") && expression.parent.object === expression) {
32947
- if (!getStaticPropertyName(expression.parent)) return null;
32948
- expression = findTransparentExpressionRoot(expression.parent);
32949
- }
32950
- return expression;
32951
- };
34458
+ //#region src/plugin/utils/is-inline-intrinsic-ref-callback.ts
32952
34459
  const isInlineIntrinsicRefCallback = (functionNode, scopes) => {
32953
34460
  const functionExpression = findTransparentExpressionRoot(functionNode);
32954
34461
  if (!isFunctionLike$1(functionExpression) || functionExpression.async || functionExpression.generator) return false;
@@ -32959,6 +34466,17 @@ const isInlineIntrinsicRefCallback = (functionNode, scopes) => {
32959
34466
  const openingElement = attribute.parent;
32960
34467
  return Boolean(openingElement && isNodeOfType(openingElement, "JSXOpeningElement") && isProvenIntrinsicJsxElement(openingElement, scopes));
32961
34468
  };
34469
+ //#endregion
34470
+ //#region src/plugin/rules/react-builtins/is-safe-create-ref-callback-current-write.ts
34471
+ const pathStartsWith$1 = (propertyPath, prefix) => prefix.every((propertyName, index) => propertyPath[index] === propertyName);
34472
+ const collectMemberExpression = (identifier) => {
34473
+ let expression = findTransparentExpressionRoot(identifier);
34474
+ while (expression.parent && isNodeOfType(expression.parent, "MemberExpression") && expression.parent.object === expression) {
34475
+ if (!getStaticPropertyName(expression.parent)) return null;
34476
+ expression = findTransparentExpressionRoot(expression.parent);
34477
+ }
34478
+ return expression;
34479
+ };
32962
34480
  const isSafeCreateRefCallbackCurrentWrite = (referenceNode, accessedPropertyPath, targetPropertyPath, scopes) => {
32963
34481
  if (accessedPropertyPath.length !== targetPropertyPath.length + 1 || !pathStartsWith$1(accessedPropertyPath, targetPropertyPath) || accessedPropertyPath[targetPropertyPath.length] !== "current") return false;
32964
34482
  const memberExpression = collectMemberExpression(referenceNode);
@@ -33445,6 +34963,151 @@ const isCreateRefResultWriteOnly = (createRefCall, filename, scopes) => {
33445
34963
  return ownedValue ? analyzeSymbolValuePath(ownedValue, state, 12) : analyzeValueUse(createRefCall, [], environment, state, 12);
33446
34964
  };
33447
34965
  //#endregion
34966
+ //#region src/plugin/utils/is-jsx-element-or-fragment.ts
34967
+ /**
34968
+ * Type-guard for the two single-node JSX output forms: `JSXElement`
34969
+ * (`<Foo />`) and `JSXFragment` (`<>…</>`). Canonical home for the
34970
+ * `isNodeOfType(x, "JSXElement") || isNodeOfType(x, "JSXFragment")` check
34971
+ * that many rules otherwise inline. Does NOT unwrap parens / TS wrappers —
34972
+ * callers that need the semantic expression should `stripParenExpression`
34973
+ * first.
34974
+ */
34975
+ const isJsxElementOrFragment = (node) => Boolean(node && (isNodeOfType(node, "JSXElement") || isNodeOfType(node, "JSXFragment")));
34976
+ //#endregion
34977
+ //#region src/plugin/rules/react-builtins/is-proven-one-shot-testing-library-component.ts
34978
+ const REACT_TESTING_LIBRARY_MODULE_SOURCE = "@testing-library/react";
34979
+ const REACT_TESTING_LIBRARY_MODULE_SOURCES = new Set([REACT_TESTING_LIBRARY_MODULE_SOURCE]);
34980
+ const TEST_CALLBACK_NAMES = new Set(["it", "test"]);
34981
+ const TEST_RUNNER_MODULE_SOURCES = new Set(["@jest/globals", "vitest"]);
34982
+ const isNamedImportFromModule = (symbol, importedName, moduleSources) => {
34983
+ if (!symbol || symbol.kind !== "import" || !isNodeOfType(symbol.declarationNode, "ImportSpecifier") || getImportedName(symbol.declarationNode) !== importedName) return false;
34984
+ const importDeclaration = symbol.declarationNode.parent;
34985
+ return Boolean(importDeclaration && isNodeOfType(importDeclaration, "ImportDeclaration") && typeof importDeclaration.source.value === "string" && moduleSources.has(importDeclaration.source.value));
34986
+ };
34987
+ const isNamespaceImportFromModule = (symbol, moduleSource) => {
34988
+ if (!symbol || symbol.kind !== "import" || !isNodeOfType(symbol.declarationNode, "ImportNamespaceSpecifier")) return false;
34989
+ const importDeclaration = symbol.declarationNode.parent;
34990
+ return Boolean(importDeclaration && isNodeOfType(importDeclaration, "ImportDeclaration") && importDeclaration.source.value === moduleSource);
34991
+ };
34992
+ const isProvenTestCallback = (functionNode, scopes) => {
34993
+ const callExpression = functionNode.parent;
34994
+ if (!callExpression || !isNodeOfType(callExpression, "CallExpression") || callExpression.arguments[1] !== functionNode) return false;
34995
+ const callee = stripParenExpression(callExpression.callee);
34996
+ if (!isNodeOfType(callee, "Identifier")) return false;
34997
+ if (TEST_CALLBACK_NAMES.has(callee.name) && scopes.isGlobalReference(callee)) return true;
34998
+ const symbol = scopes.symbolFor(callee);
34999
+ if (!symbol || symbol.kind !== "import") return false;
35000
+ const importedName = getImportedName(symbol.declarationNode);
35001
+ return Boolean(importedName && TEST_CALLBACK_NAMES.has(importedName) && isNamedImportFromModule(symbol, importedName, TEST_RUNNER_MODULE_SOURCES));
35002
+ };
35003
+ const getDirectConstComponentSymbol = (functionNode, scopes) => {
35004
+ const declarator = functionNode.parent;
35005
+ if (!declarator || !isNodeOfType(declarator, "VariableDeclarator") || declarator.init !== functionNode || !isNodeOfType(declarator.id, "Identifier")) return null;
35006
+ const declaration = declarator.parent;
35007
+ if (!declaration || !isNodeOfType(declaration, "VariableDeclaration") || declaration.kind !== "const" || declaration.declarations.length !== 1) return null;
35008
+ const testCallback = findEnclosingFunction$1(declarator);
35009
+ if (!testCallback || !isFunctionLike$1(testCallback) || !isProvenTestCallback(testCallback, scopes) || !isNodeOfType(testCallback.body, "BlockStatement") || declaration.parent !== testCallback.body) return null;
35010
+ return scopes.symbolFor(declarator.id);
35011
+ };
35012
+ const isCreateRefDeclaration = (statement, scopes) => isNodeOfType(statement, "VariableDeclaration") && statement.kind === "const" && statement.declarations.length > 0 && statement.declarations.every((declarator) => {
35013
+ const initializer = declarator.init ? stripParenExpression(declarator.init) : null;
35014
+ return Boolean(isNodeOfType(declarator.id, "Identifier") && initializer && isNodeOfType(initializer, "CallExpression") && isReactApiCall(initializer, "createRef", scopes, {
35015
+ allowGlobalReactNamespace: true,
35016
+ allowUnboundBareCalls: true,
35017
+ resolveNamedAliases: true
35018
+ }));
35019
+ });
35020
+ const isSafeReturnedJsx = (returnStatement) => {
35021
+ if (!isNodeOfType(returnStatement, "ReturnStatement") || !returnStatement.argument) return false;
35022
+ const returnedExpression = stripParenExpression(returnStatement.argument);
35023
+ if (!isJsxElementOrFragment(returnedExpression)) return false;
35024
+ let isSafe = true;
35025
+ walkAst(returnedExpression, (node) => {
35026
+ if (isFunctionLike$1(node)) {
35027
+ isSafe = false;
35028
+ return false;
35029
+ }
35030
+ if (isNodeOfType(node, "AssignmentExpression") || isNodeOfType(node, "AwaitExpression") || isNodeOfType(node, "CallExpression") || isNodeOfType(node, "NewExpression") || isNodeOfType(node, "TaggedTemplateExpression") || isNodeOfType(node, "UpdateExpression") || isNodeOfType(node, "YieldExpression")) {
35031
+ isSafe = false;
35032
+ return false;
35033
+ }
35034
+ });
35035
+ return isSafe;
35036
+ };
35037
+ const hasProvenOneShotComponentBody = (functionNode, scopes) => {
35038
+ if (!isFunctionLike$1(functionNode) || !isNodeOfType(functionNode.body, "BlockStatement")) return false;
35039
+ if (!functionNode.params.every((parameter) => isNodeOfType(parameter, "Identifier"))) return false;
35040
+ const statements = functionNode.body.body;
35041
+ if (statements.length < 2) return false;
35042
+ const returnStatement = statements.at(-1);
35043
+ return Boolean(returnStatement && statements.slice(0, -1).every((statement) => isCreateRefDeclaration(statement, scopes)) && isSafeReturnedJsx(returnStatement));
35044
+ };
35045
+ const isProvenReactStrictModeElement = (jsxElement, scopes) => {
35046
+ const elementName = jsxElement.openingElement.name;
35047
+ if (isNodeOfType(elementName, "JSXIdentifier")) {
35048
+ const symbol = scopes.symbolFor(elementName);
35049
+ return Boolean(symbol && isImportedFromReact(symbol) && getImportedName(symbol.declarationNode) === "StrictMode");
35050
+ }
35051
+ return Boolean(isNodeOfType(elementName, "JSXMemberExpression") && isNodeOfType(elementName.object, "JSXIdentifier") && elementName.property.name === "StrictMode" && isReactNamespaceImport(elementName.object, scopes));
35052
+ };
35053
+ const isWhitespaceJsxChild = (node) => isNodeOfType(node, "JSXText") && node.value.trim().length === 0 || isNodeOfType(node, "JSXExpressionContainer") && isNodeOfType(node.expression, "JSXEmptyExpression");
35054
+ const getRootElementForComponentReference = (identifier, scopes) => {
35055
+ const openingElement = identifier.parent;
35056
+ if (!openingElement || !isNodeOfType(openingElement, "JSXOpeningElement") || openingElement.name !== identifier || !openingElement.selfClosing || openingElement.attributes.length !== 0) return null;
35057
+ const componentElement = openingElement.parent;
35058
+ if (!componentElement || !isNodeOfType(componentElement, "JSXElement")) return null;
35059
+ const strictModeElement = componentElement.parent;
35060
+ if (!strictModeElement || !isNodeOfType(strictModeElement, "JSXElement")) return componentElement;
35061
+ if (strictModeElement.openingElement.attributes.length !== 0 || !isProvenReactStrictModeElement(strictModeElement, scopes)) return null;
35062
+ const renderedChildren = strictModeElement.children.filter((child) => !isWhitespaceJsxChild(child));
35063
+ return renderedChildren.length === 1 && renderedChildren[0] === componentElement ? strictModeElement : null;
35064
+ };
35065
+ const isProvenTestingLibraryRenderCall = (callExpression, scopes) => {
35066
+ const callee = stripParenExpression(callExpression.callee);
35067
+ if (isNodeOfType(callee, "Identifier")) return isNamedImportFromModule(scopes.symbolFor(callee), "render", REACT_TESTING_LIBRARY_MODULE_SOURCES);
35068
+ return Boolean(isNodeOfType(callee, "MemberExpression") && getStaticPropertyName(callee) === "render" && isNodeOfType(callee.object, "Identifier") && isNamespaceImportFromModule(scopes.symbolFor(callee.object), REACT_TESTING_LIBRARY_MODULE_SOURCE));
35069
+ };
35070
+ const isSafeRenderResultBinding = (pattern) => {
35071
+ if (!isNodeOfType(pattern, "ObjectPattern")) return false;
35072
+ return pattern.properties.every((property) => {
35073
+ if (!isNodeOfType(property, "Property") || property.computed) return false;
35074
+ return isNodeOfType(property.value, "Identifier") && getStaticPropertyKeyName(property) !== "rerender";
35075
+ });
35076
+ };
35077
+ const isDirectSafeRenderStatement = (callExpression, testCallback) => {
35078
+ if (!isFunctionLike$1(testCallback) || !isNodeOfType(testCallback.body, "BlockStatement")) return false;
35079
+ const expression = findTransparentExpressionRoot(callExpression);
35080
+ const parent = expression.parent;
35081
+ if (!parent) return false;
35082
+ if (isNodeOfType(parent, "ExpressionStatement")) return parent.parent === testCallback.body;
35083
+ if (!isNodeOfType(parent, "VariableDeclarator") || parent.init !== expression || !isSafeRenderResultBinding(parent.id)) return false;
35084
+ const declaration = parent.parent;
35085
+ return Boolean(declaration && isNodeOfType(declaration, "VariableDeclaration") && declaration.declarations.length === 1 && declaration.parent === testCallback.body);
35086
+ };
35087
+ const getProvenIndependentRenderCall = (componentReference, scopes) => {
35088
+ const rootElement = getRootElementForComponentReference(componentReference, scopes);
35089
+ if (!rootElement) return null;
35090
+ const renderedArgument = findTransparentExpressionRoot(rootElement);
35091
+ const callExpression = renderedArgument.parent;
35092
+ if (!callExpression || !isNodeOfType(callExpression, "CallExpression") || callExpression.arguments.length !== 1 || callExpression.arguments[0] !== renderedArgument || !isProvenTestingLibraryRenderCall(callExpression, scopes)) return null;
35093
+ return callExpression;
35094
+ };
35095
+ const isProvenOneShotTestingLibraryComponent = (functionNode, filename, scopes) => {
35096
+ if (!filename || !isTestlikeFilename(filename) || !hasProvenOneShotComponentBody(functionNode, scopes)) return false;
35097
+ const componentSymbol = getDirectConstComponentSymbol(functionNode, scopes);
35098
+ if (!componentSymbol || componentSymbol.references.length === 0) return false;
35099
+ const testCallback = findEnclosingFunction$1(componentSymbol.bindingIdentifier);
35100
+ if (!testCallback) return false;
35101
+ const renderCalls = /* @__PURE__ */ new Set();
35102
+ for (const reference of componentSymbol.references) {
35103
+ if (reference.flag !== "read") return false;
35104
+ const renderCall = getProvenIndependentRenderCall(reference.identifier, scopes);
35105
+ if (!renderCall || findEnclosingFunction$1(renderCall) !== testCallback || !isDirectSafeRenderStatement(renderCall, testCallback)) return false;
35106
+ renderCalls.add(renderCall);
35107
+ }
35108
+ return renderCalls.size > 0;
35109
+ };
35110
+ //#endregion
33448
35111
  //#region src/plugin/rules/react-builtins/no-create-ref-in-function-component.ts
33449
35112
  const MESSAGE$31 = "`createRef()` may escape or be observed beyond the render that created it, so a later render can replace the ref object and detach the observed one. Hoist a `useRef()` call to the component's unconditional top level instead.";
33450
35113
  const isUseMemoCallbackArgument = (functionNode, scopes) => {
@@ -33458,6 +35121,27 @@ const findEnclosingRenderFunction = (node, scopes) => {
33458
35121
  while (enclosingFunction && isUseMemoCallbackArgument(enclosingFunction, scopes)) enclosingFunction = findEnclosingFunction$1(enclosingFunction);
33459
35122
  return enclosingFunction;
33460
35123
  };
35124
+ const isReactUseStateInitialState = (node, scopes) => {
35125
+ const initialState = findTransparentExpressionRoot(node);
35126
+ const stateCall = initialState.parent;
35127
+ return Boolean(stateCall && isNodeOfType(stateCall, "CallExpression") && stateCall.arguments[0] === initialState && isReactApiCall(stateCall, "useState", scopes, {
35128
+ allowGlobalReactNamespace: true,
35129
+ resolveNamedAliases: true
35130
+ }));
35131
+ };
35132
+ const hasDirectExportWrapper = (declarationNode) => {
35133
+ const parent = declarationNode.parent;
35134
+ if (isNodeOfType(parent, "ExportNamedDeclaration") || isNodeOfType(parent, "ExportDefaultDeclaration")) return true;
35135
+ return Boolean(isNodeOfType(declarationNode, "VariableDeclarator") && (isNodeOfType(parent?.parent, "ExportNamedDeclaration") || isNodeOfType(parent?.parent, "ExportDefaultDeclaration")));
35136
+ };
35137
+ const isFunctionExclusivelyUsedAsReactStateInitializer = (functionNode, scopes) => {
35138
+ if (isReactUseStateInitialState(functionNode, scopes)) return true;
35139
+ const bindingIdentifier = getFunctionBindingIdentifier$1(findTransparentExpressionRoot(functionNode));
35140
+ if (!bindingIdentifier) return false;
35141
+ const bindingSymbol = isNodeOfType(functionNode, "FunctionDeclaration") ? scopes.scopeFor(functionNode).symbolsByName.get(bindingIdentifier.name) : scopes.symbolFor(bindingIdentifier);
35142
+ if (!bindingSymbol || bindingSymbol.kind !== "const" && bindingSymbol.kind !== "function" || hasDirectExportWrapper(bindingSymbol.declarationNode) || bindingSymbol.references.length === 0) return false;
35143
+ return bindingSymbol.references.every((reference) => reference.flag === "read" && isReactUseStateInitialState(reference.identifier, scopes));
35144
+ };
33461
35145
  const noCreateRefInFunctionComponent = defineRule({
33462
35146
  id: "no-create-ref-in-function-component",
33463
35147
  title: "createRef in function component",
@@ -33474,6 +35158,8 @@ const noCreateRefInFunctionComponent = defineRule({
33474
35158
  const displayName = componentOrHookDisplayNameForFunction(enclosingFunction);
33475
35159
  if (!displayName) return;
33476
35160
  if (!(isReactHookName(displayName) || functionContainsReactRenderOutput(enclosingFunction, context.scopes, context.cfg))) return;
35161
+ if (isReactUseStateInitialState(node, context.scopes) || isFunctionExclusivelyUsedAsReactStateInitializer(enclosingFunction, context.scopes)) return;
35162
+ if (isProvenOneShotTestingLibraryComponent(enclosingFunction, context.filename, context.scopes)) return;
33477
35163
  if (isCreateRefResultWriteOnly(node, context.filename, context.scopes)) return;
33478
35164
  context.report({
33479
35165
  node,
@@ -34743,7 +36429,6 @@ const isSetStateCallInLifecycle = (setStateCall, lifecycleNames, options = {}) =
34743
36429
  //#region src/plugin/rules/react-builtins/no-did-mount-set-state.ts
34744
36430
  const LIFECYCLE_NAMES$2 = new Set(["componentDidMount"]);
34745
36431
  const MESSAGE$28 = "Your users see an extra render right after mount when you call `setState` in `componentDidMount`.";
34746
- const getNodeStart = (node) => "start" in node && typeof node.start === "number" ? node.start : -1;
34747
36432
  const getEnclosingLifecycleFunction = (setStateCall) => {
34748
36433
  let ancestor = setStateCall.parent;
34749
36434
  while (ancestor) {
@@ -34832,13 +36517,13 @@ const argumentDerivesFromPostMountSource = (setStateCall, lifecycleFunction) =>
34832
36517
  };
34833
36518
  const isAfterAwaitInAsyncLifecycle = (setStateCall, lifecycleFunction) => {
34834
36519
  if (!isFunctionLike$1(lifecycleFunction) || lifecycleFunction.async !== true) return false;
34835
- const callStart = getNodeStart(setStateCall);
36520
+ const callStart = getNodeStartIndex(setStateCall);
34836
36521
  if (callStart < 0) return false;
34837
36522
  let didFindPrecedingAwait = false;
34838
36523
  walkAst(lifecycleFunction, (descendant) => {
34839
36524
  if (didFindPrecedingAwait) return false;
34840
36525
  if (!isNodeOfType(descendant, "AwaitExpression")) return;
34841
- const awaitStart = getNodeStart(descendant);
36526
+ const awaitStart = getNodeStartIndex(descendant);
34842
36527
  if (awaitStart >= 0 && awaitStart < callStart) {
34843
36528
  didFindPrecedingAwait = true;
34844
36529
  return false;
@@ -35902,17 +37587,178 @@ const visitSynchronousFunctionBodies = (analysisFunctions, visitor) => {
35902
37587
  walkInsideStatementBlocks(analysisFunction.body, visitor);
35903
37588
  }
35904
37589
  };
35905
- const collectWrittenStateNamesInEffect = (analysisFunctions, setterToStateName) => {
35906
- const writtenStateNames = /* @__PURE__ */ new Set();
37590
+ const readStaticEffectValue = (expression, scopes, stateSymbolId, stateValue, visitedSymbolIds = /* @__PURE__ */ new Set()) => {
37591
+ const unwrappedExpression = stripParenExpression(expression);
37592
+ if (isNodeOfType(unwrappedExpression, "Literal")) {
37593
+ const literalValue = unwrappedExpression.value;
37594
+ if (literalValue === null || typeof literalValue === "boolean" || typeof literalValue === "number" || typeof literalValue === "string") return { value: literalValue };
37595
+ return null;
37596
+ }
37597
+ if (isNodeOfType(unwrappedExpression, "Identifier")) {
37598
+ if (scopes.symbolFor(unwrappedExpression)?.id === stateSymbolId) return stateValue;
37599
+ if (unwrappedExpression.name === "undefined" && scopes.isGlobalReference(unwrappedExpression)) return { value: void 0 };
37600
+ const immutableSymbol = scopes.symbolFor(unwrappedExpression);
37601
+ if (immutableSymbol?.kind !== "const" || !immutableSymbol.initializer || !isNodeOfType(immutableSymbol.declarationNode, "VariableDeclarator") || immutableSymbol.declarationNode.id !== immutableSymbol.bindingIdentifier || immutableSymbol.declarationNode.init !== immutableSymbol.initializer || immutableSymbol.references.some((reference) => reference.flag !== "read") || visitedSymbolIds.has(immutableSymbol.id)) return null;
37602
+ return readStaticEffectValue(immutableSymbol.initializer, scopes, stateSymbolId, stateValue, new Set(visitedSymbolIds).add(immutableSymbol.id));
37603
+ }
37604
+ if (isNodeOfType(unwrappedExpression, "UnaryExpression")) {
37605
+ if (unwrappedExpression.operator === "void") return { value: void 0 };
37606
+ if (unwrappedExpression.operator !== "!") return null;
37607
+ const argumentValue = readStaticEffectValue(unwrappedExpression.argument, scopes, stateSymbolId, stateValue, visitedSymbolIds);
37608
+ return argumentValue ? { value: !argumentValue.value } : null;
37609
+ }
37610
+ if (isNodeOfType(unwrappedExpression, "CallExpression")) {
37611
+ if (isNodeOfType(unwrappedExpression.callee, "Identifier") && unwrappedExpression.callee.name === "Boolean" && scopes.isGlobalReference(unwrappedExpression.callee) && unwrappedExpression.arguments.length === 1 && unwrappedExpression.arguments[0] && !isNodeOfType(unwrappedExpression.arguments[0], "SpreadElement")) {
37612
+ const argumentValue = readStaticEffectValue(unwrappedExpression.arguments[0], scopes, stateSymbolId, stateValue, visitedSymbolIds);
37613
+ return argumentValue ? { value: Boolean(argumentValue.value) } : null;
37614
+ }
37615
+ return null;
37616
+ }
37617
+ if (isNodeOfType(unwrappedExpression, "LogicalExpression")) {
37618
+ const leftValue = readStaticEffectValue(unwrappedExpression.left, scopes, stateSymbolId, stateValue, visitedSymbolIds);
37619
+ if (!leftValue) return null;
37620
+ if (unwrappedExpression.operator === "&&" && !leftValue.value) return leftValue;
37621
+ if (unwrappedExpression.operator === "||" && leftValue.value) return leftValue;
37622
+ if (unwrappedExpression.operator === "??" && leftValue.value !== null && leftValue.value !== void 0) return leftValue;
37623
+ return readStaticEffectValue(unwrappedExpression.right, scopes, stateSymbolId, stateValue, visitedSymbolIds);
37624
+ }
37625
+ if (isNodeOfType(unwrappedExpression, "ConditionalExpression")) {
37626
+ const testValue = readStaticEffectValue(unwrappedExpression.test, scopes, stateSymbolId, stateValue, visitedSymbolIds);
37627
+ if (!testValue) return null;
37628
+ return readStaticEffectValue(testValue.value ? unwrappedExpression.consequent : unwrappedExpression.alternate, scopes, stateSymbolId, stateValue, visitedSymbolIds);
37629
+ }
37630
+ if (isNodeOfType(unwrappedExpression, "MemberExpression") && unwrappedExpression.optional) {
37631
+ const objectValue = readStaticEffectValue(unwrappedExpression.object, scopes, stateSymbolId, stateValue, visitedSymbolIds);
37632
+ if (objectValue?.value === null || objectValue?.value === void 0) return { value: void 0 };
37633
+ return null;
37634
+ }
37635
+ if (isNodeOfType(unwrappedExpression, "BinaryExpression")) {
37636
+ const leftValue = readStaticEffectValue(unwrappedExpression.left, scopes, stateSymbolId, stateValue, visitedSymbolIds);
37637
+ const rightValue = readStaticEffectValue(unwrappedExpression.right, scopes, stateSymbolId, stateValue, visitedSymbolIds);
37638
+ if (!leftValue || !rightValue) return null;
37639
+ if (unwrappedExpression.operator === "===" || unwrappedExpression.operator === "!==") {
37640
+ const areEqual = leftValue.value === rightValue.value;
37641
+ return { value: unwrappedExpression.operator === "===" ? areEqual : !areEqual };
37642
+ }
37643
+ if (unwrappedExpression.operator === "==" || unwrappedExpression.operator === "!=") {
37644
+ const isLeftNullish = leftValue.value === null || leftValue.value === void 0;
37645
+ const isRightNullish = rightValue.value === null || rightValue.value === void 0;
37646
+ if (!isLeftNullish && !isRightNullish && typeof leftValue.value !== typeof rightValue.value) return null;
37647
+ const areEqual = isLeftNullish || isRightNullish ? isLeftNullish && isRightNullish : leftValue.value === rightValue.value;
37648
+ return { value: unwrappedExpression.operator === "==" ? areEqual : !areEqual };
37649
+ }
37650
+ }
37651
+ return null;
37652
+ };
37653
+ const readStaticUpdaterReturnValue = (updater, scopes) => {
37654
+ if (!isFunctionLike$1(updater) || updater.async || updater.generator) return null;
37655
+ if (!isNodeOfType(updater.body, "BlockStatement")) return readStaticEffectValue(updater.body, scopes, null, null);
37656
+ if (updater.body.body.length === 0) return { value: void 0 };
37657
+ if (updater.body.body.length !== 1) return null;
37658
+ const returnStatement = updater.body.body[0];
37659
+ if (!isNodeOfType(returnStatement, "ReturnStatement")) return null;
37660
+ if (!returnStatement.argument) return { value: void 0 };
37661
+ return readStaticEffectValue(returnStatement.argument, scopes, null, null);
37662
+ };
37663
+ const readStaticSetterValue = (setterCall, scopes) => {
37664
+ const argument = setterCall.arguments[0];
37665
+ if (!argument) return { value: void 0 };
37666
+ if (isNodeOfType(argument, "SpreadElement")) return null;
37667
+ const updater = resolveExactLocalFunction(argument, scopes);
37668
+ if (updater) return readStaticUpdaterReturnValue(updater, scopes);
37669
+ return readStaticEffectValue(argument, scopes, null, null);
37670
+ };
37671
+ const collectStateWritesInEffect = (analysisFunctions, setterToStateName, scopes) => {
37672
+ const stateWrites = /* @__PURE__ */ new Map();
35907
37673
  visitSynchronousFunctionBodies(analysisFunctions, (child) => {
35908
37674
  if (!isNodeOfType(child, "CallExpression")) return;
35909
37675
  if (!isNodeOfType(child.callee, "Identifier")) return;
35910
37676
  const stateName = setterToStateName.get(child.callee.name);
35911
- if (stateName) writtenStateNames.add(stateName);
37677
+ if (!stateName) return;
37678
+ const writeInfo = stateWrites.get(stateName) ?? {
37679
+ values: /* @__PURE__ */ new Set(),
37680
+ hasUnknownValue: false
37681
+ };
37682
+ const staticValue = readStaticSetterValue(child, scopes);
37683
+ if (staticValue) writeInfo.values.add(staticValue.value);
37684
+ else writeInfo.hasUnknownValue = true;
37685
+ stateWrites.set(stateName, writeInfo);
35912
37686
  });
35913
- return writtenStateNames;
37687
+ return stateWrites;
37688
+ };
37689
+ const isGlobalBooleanCall = (node, scopes) => {
37690
+ return isNodeOfType(node, "CallExpression") && isNodeOfType(node.callee, "Identifier") && node.callee.name === "Boolean" && scopes.isGlobalReference(node.callee);
37691
+ };
37692
+ const isWorkNodeReachableForStateValue = (workNode, stateSymbolId, stateValue, scopes) => {
37693
+ let currentNode = workNode;
37694
+ while (currentNode.parent) {
37695
+ const parentNode = currentNode.parent;
37696
+ if (isFunctionLike$1(parentNode)) break;
37697
+ if (isNodeOfType(parentNode, "IfStatement")) {
37698
+ const testValue = readStaticEffectValue(parentNode.test, scopes, stateSymbolId, stateValue);
37699
+ if (testValue) {
37700
+ if (currentNode === parentNode.consequent && !testValue.value) return false;
37701
+ if (currentNode === parentNode.alternate && testValue.value) return false;
37702
+ }
37703
+ }
37704
+ if (isNodeOfType(parentNode, "ConditionalExpression")) {
37705
+ const testValue = readStaticEffectValue(parentNode.test, scopes, stateSymbolId, stateValue);
37706
+ if (testValue) {
37707
+ if (currentNode === parentNode.consequent && !testValue.value) return false;
37708
+ if (currentNode === parentNode.alternate && testValue.value) return false;
37709
+ }
37710
+ }
37711
+ if (isNodeOfType(parentNode, "LogicalExpression") && currentNode === parentNode.right) {
37712
+ const leftValue = readStaticEffectValue(parentNode.left, scopes, stateSymbolId, stateValue);
37713
+ if (leftValue) {
37714
+ if (parentNode.operator === "&&" && !leftValue.value) return false;
37715
+ if (parentNode.operator === "||" && leftValue.value) return false;
37716
+ if (parentNode.operator === "??" && leftValue.value !== null && leftValue.value !== void 0) return false;
37717
+ }
37718
+ }
37719
+ if (isNodeOfType(parentNode, "BlockStatement")) {
37720
+ const statementIndex = parentNode.body.findIndex((statement) => statement === currentNode);
37721
+ if (statementIndex >= 0) for (let index = 0; index < statementIndex; index += 1) {
37722
+ const earlierStatement = parentNode.body[index];
37723
+ if (!isNodeOfType(earlierStatement, "IfStatement") || earlierStatement.alternate || !statementAlwaysExits(earlierStatement.consequent)) continue;
37724
+ if (readStaticEffectValue(earlierStatement.test, scopes, stateSymbolId, stateValue)?.value) return false;
37725
+ }
37726
+ }
37727
+ currentNode = parentNode;
37728
+ }
37729
+ return true;
37730
+ };
37731
+ const isReaderWorkNode = (node, analysisFunctions, scopes) => {
37732
+ if (isNodeOfType(node, "CallExpression")) {
37733
+ if (isGlobalBooleanCall(node, scopes)) return false;
37734
+ const invokedFunction = resolveExactLocalFunction(node.callee, scopes);
37735
+ return !invokedFunction || !analysisFunctions.has(invokedFunction);
37736
+ }
37737
+ return isNodeOfType(node, "AssignmentExpression") || isNodeOfType(node, "UpdateExpression") || isNodeOfType(node, "NewExpression") || isNodeOfType(node, "TaggedTemplateExpression") || isNodeOfType(node, "ThrowStatement") || isNodeOfType(node, "UnaryExpression") && node.operator === "delete";
37738
+ };
37739
+ const canStateWriteReachReaderWork = (writeInfo, readerEffect, stateSymbolId, scopes) => {
37740
+ if (writeInfo.hasUnknownValue || stateSymbolId === null) return true;
37741
+ for (const writtenValue of writeInfo.values) {
37742
+ const stateValue = { value: writtenValue };
37743
+ let didFindReachableWork = false;
37744
+ visitSynchronousFunctionBodies(readerEffect.analysisFunctions, (child) => {
37745
+ if (didFindReachableWork || !isReaderWorkNode(child, readerEffect.analysisFunctions, scopes)) return;
37746
+ if (isWorkNodeReachableForStateValue(child, stateSymbolId, stateValue, scopes)) didFindReachableWork = true;
37747
+ });
37748
+ if (didFindReachableWork) return true;
37749
+ }
37750
+ return false;
35914
37751
  };
35915
37752
  const EMPTY_CLEANUP_NAME_SET = /* @__PURE__ */ new Set();
37753
+ const NON_CONTAMINATING_MAP_METHOD_NAMES = new Set([
37754
+ "clear",
37755
+ "delete",
37756
+ "entries",
37757
+ "get",
37758
+ "has",
37759
+ "keys",
37760
+ "values"
37761
+ ]);
35916
37762
  const isFunctionShapedReturn = (returnedValue, setterToStateName, isExplicitReturnStatement) => {
35917
37763
  if (isNodeOfType(returnedValue, "ArrowFunctionExpression") || isNodeOfType(returnedValue, "FunctionExpression")) return true;
35918
37764
  if (isNodeOfType(returnedValue, "CallExpression")) {
@@ -35963,27 +37809,135 @@ const callsOpaqueExternalSetter = (analysisFunctions, setterToStateName) => {
35963
37809
  });
35964
37810
  return didFindOpaqueSetterCall;
35965
37811
  };
37812
+ const isReactRefCall = (expression, scopes) => isNodeOfType(expression, "CallExpression") && (isReactApiCall(expression, "useRef", scopes, {
37813
+ allowGlobalReactNamespace: true,
37814
+ allowUnboundBareCalls: true,
37815
+ resolveNamedAliases: true
37816
+ }) || isReactApiCall(expression, "createRef", scopes, {
37817
+ allowGlobalReactNamespace: true,
37818
+ allowUnboundBareCalls: true,
37819
+ resolveNamedAliases: true
37820
+ }));
37821
+ const getDirectReactRefSymbol = (rawExpression, scopes) => {
37822
+ const expression = stripParenExpression(rawExpression);
37823
+ if (!isNodeOfType(expression, "Identifier")) return null;
37824
+ const symbol = scopes.symbolFor(expression);
37825
+ if (!symbol) return null;
37826
+ const initializer = getDirectUnreassignedInitializer(symbol);
37827
+ return initializer && isReactRefCall(stripParenExpression(initializer), scopes) ? symbol : null;
37828
+ };
37829
+ const isReactNativeJsxElement = (openingElement, scopes) => {
37830
+ if (!isNodeOfType(openingElement.name, "JSXIdentifier")) return false;
37831
+ const symbol = scopes.symbolFor(openingElement.name);
37832
+ const importDeclaration = symbol?.declarationNode.parent;
37833
+ return Boolean(symbol?.kind === "import" && importDeclaration && isNodeOfType(importDeclaration, "ImportDeclaration") && importDeclaration.source.value === "react-native");
37834
+ };
37835
+ const isDirectHostJsxRef = (symbol, scopes) => {
37836
+ let hostRefCount = 0;
37837
+ for (const reference of symbol.references) {
37838
+ const expression = findTransparentExpressionRoot(reference.identifier);
37839
+ const container = expression.parent;
37840
+ if (isNodeOfType(container, "MemberExpression") && container.object === expression && getStaticPropertyName(container) === "current") continue;
37841
+ if (!container || !isNodeOfType(container, "JSXExpressionContainer") || container.expression !== expression) return false;
37842
+ const attribute = container.parent;
37843
+ if (!attribute || !isNodeOfType(attribute, "JSXAttribute") || getJsxAttributeName(attribute.name) !== "ref") return false;
37844
+ const openingElement = attribute.parent;
37845
+ if (!openingElement || !isNodeOfType(openingElement, "JSXOpeningElement") || !isProvenIntrinsicJsxElement(openingElement, scopes) && !isReactNativeJsxElement(openingElement, scopes)) return false;
37846
+ hostRefCount += 1;
37847
+ }
37848
+ return hostRefCount > 0;
37849
+ };
37850
+ const isIntrinsicRefCallbackParameter = (expression, scopes) => {
37851
+ const identifier = stripParenExpression(expression);
37852
+ if (!isNodeOfType(identifier, "Identifier")) return false;
37853
+ const callback = findEnclosingFunction$1(identifier);
37854
+ if (!callback || !isFunctionLike$1(callback) || !isInlineIntrinsicRefCallback(callback, scopes)) return false;
37855
+ const rawFirstParameter = callback.params?.[0];
37856
+ const firstParameter = isNodeOfType(rawFirstParameter, "AssignmentPattern") ? rawFirstParameter.left : rawFirstParameter;
37857
+ const symbol = scopes.symbolFor(identifier);
37858
+ return Boolean(firstParameter && symbol?.bindingIdentifier === firstParameter);
37859
+ };
37860
+ const getDirectReactRefCall = (symbol, scopes) => {
37861
+ const initializer = getDirectUnreassignedInitializer(symbol);
37862
+ if (!initializer) return null;
37863
+ const expression = stripParenExpression(initializer);
37864
+ return isNodeOfType(expression, "CallExpression") && isReactRefCall(expression, scopes) ? expression : null;
37865
+ };
37866
+ const storesOnlyIntrinsicRefCallbackValues = (symbol, scopes) => {
37867
+ const initialValue = getDirectReactRefCall(symbol, scopes)?.arguments?.[0];
37868
+ if (!initialValue || !isNodeOfType(initialValue, "NewExpression") || !isNodeOfType(initialValue.callee, "Identifier") || initialValue.callee.name !== "Map" || !scopes.isGlobalReference(initialValue.callee) || initialValue.arguments.length !== 0) return false;
37869
+ let intrinsicValueWriteCount = 0;
37870
+ for (const reference of symbol.references) {
37871
+ const identifier = findTransparentExpressionRoot(reference.identifier);
37872
+ const currentMember = identifier.parent;
37873
+ if (!isNodeOfType(currentMember, "MemberExpression") || currentMember.object !== identifier || getStaticPropertyName(currentMember) !== "current") return false;
37874
+ const currentExpression = findTransparentExpressionRoot(currentMember);
37875
+ const methodMember = currentExpression.parent;
37876
+ if (!isNodeOfType(methodMember, "MemberExpression") || methodMember.object !== currentExpression) return false;
37877
+ const methodName = getStaticPropertyName(methodMember);
37878
+ if (methodName === "size") continue;
37879
+ const call = methodMember.parent;
37880
+ if (!isNodeOfType(call, "CallExpression") || call.callee !== methodMember) return false;
37881
+ if (methodName && NON_CONTAMINATING_MAP_METHOD_NAMES.has(methodName)) continue;
37882
+ if (methodName !== "set") return false;
37883
+ const storedValue = call.arguments[1];
37884
+ if (!storedValue || isNodeOfType(storedValue, "SpreadElement") || !isIntrinsicRefCallbackParameter(storedValue, scopes)) return false;
37885
+ intrinsicValueWriteCount += 1;
37886
+ }
37887
+ return intrinsicValueWriteCount > 0;
37888
+ };
37889
+ const isDerivedFromProvenDomRefCurrent = (rawExpression, scopes, didReadCollectionValue = false, visitedSymbolIds = /* @__PURE__ */ new Set()) => {
37890
+ const expression = stripParenExpression(rawExpression);
37891
+ if (isNodeOfType(expression, "Identifier")) {
37892
+ const symbol = scopes.symbolFor(expression);
37893
+ if (!symbol || visitedSymbolIds.has(symbol.id)) return false;
37894
+ const initializer = getDirectUnreassignedInitializer(symbol);
37895
+ if (!initializer) return false;
37896
+ visitedSymbolIds.add(symbol.id);
37897
+ return isDerivedFromProvenDomRefCurrent(initializer, scopes, didReadCollectionValue, visitedSymbolIds);
37898
+ }
37899
+ if (isNodeOfType(expression, "MemberExpression")) {
37900
+ if (getStaticPropertyName(expression) === "current") {
37901
+ const symbol = getDirectReactRefSymbol(expression.object, scopes);
37902
+ return Boolean(symbol && (isDirectHostJsxRef(symbol, scopes) || didReadCollectionValue && storesOnlyIntrinsicRefCallbackValues(symbol, scopes)));
37903
+ }
37904
+ return isDerivedFromProvenDomRefCurrent(expression.object, scopes, didReadCollectionValue, visitedSymbolIds);
37905
+ }
37906
+ if (!isNodeOfType(expression, "CallExpression")) return false;
37907
+ const callee = stripParenExpression(expression.callee);
37908
+ if (!isNodeOfType(callee, "MemberExpression")) return false;
37909
+ return isDerivedFromProvenDomRefCurrent(callee.object, scopes, didReadCollectionValue || getStaticPropertyName(callee) === "get", visitedSymbolIds);
37910
+ };
37911
+ const isCommittedDomSyncNode = (node, scopes) => {
37912
+ if (!isNodeOfType(node, "CallExpression")) return false;
37913
+ const callee = stripParenExpression(node.callee);
37914
+ if (!isNodeOfType(callee, "MemberExpression")) return false;
37915
+ const propertyName = getStaticPropertyName(callee);
37916
+ if (propertyName === null || !EXTERNAL_SYNC_DOM_MEMBER_METHOD_NAMES.has(propertyName)) return false;
37917
+ return isDerivedFromProvenDomRefCurrent(callee.object, scopes) || isProvenBrowserApiReceiver(callee.object, "dom-event-target", scopes);
37918
+ };
35966
37919
  const isExternalSyncNode = (node) => {
35967
37920
  if (isNodeOfType(node, "NewExpression")) return isNodeOfType(node.callee, "Identifier") && EXTERNAL_SYNC_OBSERVER_CONSTRUCTORS.has(node.callee.name);
35968
37921
  if (isNodeOfType(node, "AssignmentExpression")) return isNodeOfType(node.left, "MemberExpression") && isNodeOfType(node.left.property, "Identifier") && node.left.property.name === "current";
35969
37922
  if (!isNodeOfType(node, "CallExpression")) return false;
35970
37923
  if (isNodeOfType(node.callee, "Identifier")) return EXTERNAL_SYNC_DIRECT_CALLEE_NAMES.has(node.callee.name);
35971
- if (!isNodeOfType(node.callee, "MemberExpression") || !isNodeOfType(node.callee.property, "Identifier")) return false;
35972
- const propertyName = node.callee.property.name;
37924
+ if (!isNodeOfType(node.callee, "MemberExpression")) return false;
37925
+ const propertyName = getStaticPropertyName(node.callee);
37926
+ if (propertyName === null) return false;
35973
37927
  if (EXTERNAL_SYNC_MEMBER_METHOD_NAMES.has(propertyName)) return true;
35974
37928
  if (isBrowserStorageReceiver(node.callee.object)) return true;
35975
37929
  if (!EXTERNAL_SYNC_AMBIGUOUS_HTTP_METHOD_NAMES.has(propertyName)) return false;
35976
37930
  const receiverRootName = getRootIdentifierName(node.callee.object);
35977
37931
  return receiverRootName !== null && EXTERNAL_SYNC_HTTP_CLIENT_RECEIVERS.has(receiverRootName);
35978
37932
  };
35979
- const isExternalSyncEffect = (effectCallback, analysisFunctions, setterToStateName) => {
37933
+ const isExternalSyncEffect = (effectCallback, analysisFunctions, setterToStateName, scopes, allowCommittedDomSync) => {
35980
37934
  if (!isFunctionLike$1(effectCallback)) return false;
35981
37935
  if (!isNodeOfType(effectCallback.body, "BlockStatement")) {
35982
37936
  if (isFunctionShapedReturn(effectCallback.body, setterToStateName, false)) return true;
35983
37937
  } else for (const statement of effectCallback.body.body ?? []) if (isNodeOfType(statement, "ReturnStatement") && statement.argument && isFunctionShapedReturn(statement.argument, setterToStateName, true)) return true;
35984
37938
  let didFindExternalCall = false;
35985
37939
  visitSynchronousFunctionBodies(analysisFunctions, (child) => {
35986
- if (isExternalSyncNode(child)) didFindExternalCall = true;
37940
+ if (isExternalSyncNode(child) || allowCommittedDomSync && isCommittedDomSyncNode(child, scopes)) didFindExternalCall = true;
35987
37941
  });
35988
37942
  return didFindExternalCall;
35989
37943
  };
@@ -35999,32 +37953,45 @@ const noEffectChain = defineRule({
35999
37953
  const useStateBindings = collectUseStateBindings(componentBody);
36000
37954
  if (useStateBindings.length === 0) return;
36001
37955
  const setterToStateName = /* @__PURE__ */ new Map();
36002
- for (const binding of useStateBindings) setterToStateName.set(binding.setterName, binding.valueName);
37956
+ const stateSymbolIds = /* @__PURE__ */ new Map();
37957
+ for (const binding of useStateBindings) {
37958
+ setterToStateName.set(binding.setterName, binding.valueName);
37959
+ if (!isNodeOfType(binding.declarator.id, "ArrayPattern")) continue;
37960
+ const stateIdentifier = binding.declarator.id.elements[0];
37961
+ if (isNodeOfType(stateIdentifier, "Identifier")) {
37962
+ const stateSymbol = context.scopes.symbolFor(stateIdentifier);
37963
+ if (stateSymbol) stateSymbolIds.set(binding.valueName, stateSymbol.id);
37964
+ }
37965
+ }
36003
37966
  const storageSetterNames = collectStorageHookSetterNames(componentBody);
36004
37967
  const effectInfos = [];
36005
37968
  for (const effectCall of findTopLevelEffectCalls(componentBody)) {
36006
37969
  const callback = getEffectCallback(effectCall, context.scopes);
36007
37970
  if (!callback || !isFunctionLike$1(callback) || callback.async) continue;
36008
37971
  const analysisFunctions = collectSynchronouslyInvokedFunctions(callback, context.scopes);
36009
- const writtenStateNames = collectWrittenStateNamesInEffect(analysisFunctions, setterToStateName);
37972
+ const stateWrites = collectStateWritesInEffect(analysisFunctions, setterToStateName, context.scopes);
37973
+ const writtenStateNames = new Set(stateWrites.keys());
36010
37974
  effectInfos.push({
36011
37975
  node: effectCall,
36012
37976
  depNames: collectDepIdentifierNames(effectCall),
36013
- writtenStateNames,
36014
- isExternalSync: isExternalSyncEffect(callback, analysisFunctions, setterToStateName) || callsStorageHookSetter(analysisFunctions, storageSetterNames) || writtenStateNames.size === 0 && callsOpaqueExternalSetter(analysisFunctions, setterToStateName)
37977
+ stateWrites,
37978
+ analysisFunctions,
37979
+ isExternalSync: isExternalSyncEffect(callback, analysisFunctions, setterToStateName, context.scopes, writtenStateNames.size === 0) || callsStorageHookSetter(analysisFunctions, storageSetterNames) || writtenStateNames.size === 0 && callsOpaqueExternalSetter(analysisFunctions, setterToStateName)
36015
37980
  });
36016
37981
  }
36017
37982
  if (effectInfos.length < 2) return;
36018
37983
  const reportedNodes = /* @__PURE__ */ new Set();
36019
37984
  for (const writerEffect of effectInfos) {
36020
37985
  if (writerEffect.isExternalSync) continue;
36021
- if (writerEffect.writtenStateNames.size === 0) continue;
37986
+ if (writerEffect.stateWrites.size === 0) continue;
36022
37987
  for (const readerEffect of effectInfos) {
36023
37988
  if (readerEffect === writerEffect) continue;
36024
37989
  if (readerEffect.isExternalSync) continue;
36025
37990
  if (readerEffect.depNames.size === 0) continue;
36026
37991
  let chainedStateName = null;
36027
- for (const writtenName of writerEffect.writtenStateNames) if (readerEffect.depNames.has(writtenName)) {
37992
+ for (const [writtenName, writeInfo] of writerEffect.stateWrites) {
37993
+ if (!readerEffect.depNames.has(writtenName)) continue;
37994
+ if (!canStateWriteReachReaderWork(writeInfo, readerEffect, stateSymbolIds.get(writtenName) ?? null, context.scopes)) continue;
36028
37995
  chainedStateName = writtenName;
36029
37996
  break;
36030
37997
  }
@@ -39691,6 +41658,18 @@ const functionContainsProvenReactHookCall = (functionNode, scopes) => {
39691
41658
  return containsReactHookCall;
39692
41659
  };
39693
41660
  //#endregion
41661
+ //#region src/plugin/utils/function-returns-only-null.ts
41662
+ const isNullExpression = (expression) => {
41663
+ const candidate = stripParenExpression(expression);
41664
+ return isNodeOfType(candidate, "Literal") && candidate.value === null;
41665
+ };
41666
+ const functionReturnsOnlyNull = (functionNode) => {
41667
+ if (!isFunctionLike$1(functionNode)) return false;
41668
+ if (!isNodeOfType(functionNode.body, "BlockStatement")) return isNullExpression(functionNode.body);
41669
+ const returnStatements = collectFunctionReturnStatements(functionNode);
41670
+ return returnStatements.length > 0 && returnStatements.every((returnStatement) => Boolean(returnStatement.argument && isNullExpression(returnStatement.argument)));
41671
+ };
41672
+ //#endregion
39694
41673
  //#region src/plugin/utils/function-returns-props-children.ts
39695
41674
  const functionReturnsPropsChildren = (functionNode, scopes, controlFlow) => {
39696
41675
  if (!isFunctionLike$1(functionNode) || functionNode.params.length === 0) return false;
@@ -39723,17 +41702,8 @@ const functionReturnsPropsChildren = (functionNode, scopes, controlFlow) => {
39723
41702
  }, controlFlow);
39724
41703
  };
39725
41704
  //#endregion
39726
- //#region src/plugin/utils/function-returns-only-null.ts
39727
- const isNullExpression = (expression) => {
39728
- const candidate = stripParenExpression(expression);
39729
- return isNodeOfType(candidate, "Literal") && candidate.value === null;
39730
- };
39731
- const functionReturnsOnlyNull = (functionNode) => {
39732
- if (!isFunctionLike$1(functionNode)) return false;
39733
- if (!isNodeOfType(functionNode.body, "BlockStatement")) return isNullExpression(functionNode.body);
39734
- const returnStatements = collectFunctionReturnStatements(functionNode);
39735
- return returnStatements.length > 0 && returnStatements.every((returnStatement) => Boolean(returnStatement.argument && isNullExpression(returnStatement.argument)));
39736
- };
41705
+ //#region src/plugin/utils/function-has-react-component-evidence.ts
41706
+ const functionHasReactComponentEvidence = (functionNode, scopes, controlFlow) => functionContainsReactRenderOutput(functionNode, scopes, controlFlow) || functionReturnsPropsChildren(functionNode, scopes, controlFlow) || functionContainsProvenReactHookCall(functionNode, scopes) && functionReturnsOnlyNull(functionNode);
39737
41707
  //#endregion
39738
41708
  //#region src/plugin/utils/is-proven-styled-component-expression.ts
39739
41709
  const findFactoryRoot = (node) => {
@@ -39766,17 +41736,16 @@ const isProvenStyledComponentExpression = (expression, scopes) => {
39766
41736
  //#region src/plugin/utils/is-proven-react-component-symbol.ts
39767
41737
  const REACT_COMPONENT_HOC_NAMES = new Set(["memo", "forwardRef"]);
39768
41738
  const LEGACY_REACT_COMPONENT_FACTORY_NAMES = new Set(["createClass", "createReactClass"]);
39769
- const functionHasComponentEvidence = (functionNode, scopes, controlFlow) => functionContainsReactRenderOutput(functionNode, scopes, controlFlow) || functionReturnsPropsChildren(functionNode, scopes, controlFlow) || functionContainsProvenReactHookCall(functionNode, scopes) && functionReturnsOnlyNull(functionNode);
39770
41739
  const isProvenReactComponentExpression = (expression, scopes, controlFlow, visitedSymbolIds = /* @__PURE__ */ new Set()) => {
39771
41740
  const candidate = stripParenExpression(expression);
39772
- if (isInlineFunctionExpression(candidate)) return functionHasComponentEvidence(candidate, scopes, controlFlow);
41741
+ if (isInlineFunctionExpression(candidate)) return functionHasReactComponentEvidence(candidate, scopes, controlFlow);
39773
41742
  if (isNodeOfType(candidate, "ClassExpression")) return isProvenReactClassComponent(candidate, scopes);
39774
41743
  if (isProvenStyledComponentExpression(candidate, scopes)) return true;
39775
41744
  if (isNodeOfType(candidate, "Identifier")) {
39776
41745
  const symbol = scopes.symbolFor(candidate);
39777
41746
  if (!symbol || visitedSymbolIds.has(symbol.id) || hasSymbolWriteBefore(symbol, candidate, scopes)) return false;
39778
41747
  visitedSymbolIds.add(symbol.id);
39779
- if (isNodeOfType(symbol.declarationNode, "FunctionDeclaration")) return functionHasComponentEvidence(symbol.declarationNode, scopes, controlFlow);
41748
+ if (isNodeOfType(symbol.declarationNode, "FunctionDeclaration")) return functionHasReactComponentEvidence(symbol.declarationNode, scopes, controlFlow);
39780
41749
  if (isNodeOfType(symbol.declarationNode, "ClassDeclaration") || isNodeOfType(symbol.declarationNode, "ClassExpression")) return isProvenReactClassComponent(symbol.declarationNode, scopes);
39781
41750
  return Boolean(symbol.initializer && isProvenReactComponentExpression(symbol.initializer, scopes, controlFlow, visitedSymbolIds));
39782
41751
  }
@@ -39803,7 +41772,7 @@ const isProvenReactComponentSymbol = (symbol, scopes, controlFlow, componentRefe
39803
41772
  for (const candidateSymbol of candidateSymbols) {
39804
41773
  if (hasSymbolWriteBefore(candidateSymbol, componentReference, scopes)) continue;
39805
41774
  if (isComponentDeclaration(candidateSymbol.declarationNode)) {
39806
- if (functionHasComponentEvidence(candidateSymbol.declarationNode, scopes, controlFlow)) return true;
41775
+ if (functionHasReactComponentEvidence(candidateSymbol.declarationNode, scopes, controlFlow)) return true;
39807
41776
  continue;
39808
41777
  }
39809
41778
  const initializer = candidateSymbol.initializer ? stripParenExpression(candidateSymbol.initializer) : null;
@@ -41305,11 +43274,6 @@ const SAME_REFERENCE_ARRAY_RETURN_METHODS = new Set([
41305
43274
  "reverse",
41306
43275
  "sort"
41307
43276
  ]);
41308
- const OBJECT_MUTATION_METHODS = new Set([
41309
- "assign",
41310
- "defineProperties",
41311
- "defineProperty"
41312
- ]);
41313
43277
  const REFLECT_MUTATION_METHODS = new Set(["deleteProperty", "set"]);
41314
43278
  const cloneReducerPathState = (state) => ({
41315
43279
  originalStateReferenceNames: new Set(state.originalStateReferenceNames),
@@ -41405,7 +43369,7 @@ const collectReducerStateMutationsInExpressionOrStatement = (node, state) => {
41405
43369
  }
41406
43370
  if (!isNodeOfType(unwrappedChild, "CallExpression")) return;
41407
43371
  const firstArgument = unwrappedChild.arguments?.[0];
41408
- if (firstArgument && isExpressionRootedInMutableReducerStateSource(firstArgument, state) && (isStaticMethodCallOnNamedObject(unwrappedChild, "Object", OBJECT_MUTATION_METHODS) || isStaticMethodCallOnNamedObject(unwrappedChild, "Reflect", REFLECT_MUTATION_METHODS))) {
43372
+ if (firstArgument && isExpressionRootedInMutableReducerStateSource(firstArgument, state) && (isStaticMethodCallOnNamedObject(unwrappedChild, "Object", OBJECT_PROPERTY_MUTATION_METHOD_NAMES) || isStaticMethodCallOnNamedObject(unwrappedChild, "Reflect", REFLECT_MUTATION_METHODS))) {
41409
43373
  mutations.push({ node: unwrappedChild });
41410
43374
  return;
41411
43375
  }
@@ -43589,6 +45553,53 @@ const noPropCallbackInEffect = defineRule({
43589
45553
  });
43590
45554
  //#endregion
43591
45555
  //#region src/plugin/rules/state-and-effects/no-prop-callback-in-render.ts
45556
+ const functionBindingSymbols = (functionNode, scopes) => {
45557
+ let bindingIdentifier = null;
45558
+ if (isNodeOfType(functionNode, "FunctionDeclaration") && functionNode.id) bindingIdentifier = functionNode.id;
45559
+ else {
45560
+ let bindingExpression = findTransparentExpressionRoot(functionNode);
45561
+ let parent = bindingExpression.parent;
45562
+ while (isNodeOfType(parent, "CallExpression") && parent.arguments[0] === bindingExpression) {
45563
+ const callee = parent.callee;
45564
+ const wrapperName = isNodeOfType(callee, "Identifier") ? callee.name : isNodeOfType(callee, "MemberExpression") && isNodeOfType(callee.property, "Identifier") ? callee.property.name : null;
45565
+ if (!isReactApiCall(parent, REACT_HOC_NAMES, scopes, {
45566
+ allowGlobalReactNamespace: true,
45567
+ resolveNamedAliases: true
45568
+ }) && (!wrapperName || REACT_HOC_NAMES.has(wrapperName) || !COMPONENT_HOC_WRAPPER_NAMES.has(wrapperName))) break;
45569
+ bindingExpression = findTransparentExpressionRoot(parent);
45570
+ parent = bindingExpression.parent;
45571
+ }
45572
+ if (isNodeOfType(parent, "VariableDeclarator") && parent.init === bindingExpression && isNodeOfType(parent.id, "Identifier")) bindingIdentifier = parent.id;
45573
+ }
45574
+ if (!bindingIdentifier) return [];
45575
+ let scope = scopes.scopeFor(functionNode);
45576
+ while (scope) {
45577
+ const symbols = scope.symbols.filter((symbol) => symbol.bindingIdentifier === bindingIdentifier);
45578
+ if (symbols.length > 0) return symbols;
45579
+ scope = scope.parent;
45580
+ }
45581
+ return [];
45582
+ };
45583
+ const symbolHasReactComponentUse = (symbol, scopes, visitedSymbolIds = /* @__PURE__ */ new Set()) => {
45584
+ if (visitedSymbolIds.has(symbol.id)) return false;
45585
+ visitedSymbolIds.add(symbol.id);
45586
+ for (const reference of symbol.references) {
45587
+ const identifier = reference.identifier;
45588
+ if (hasSymbolWriteBefore(symbol, identifier, scopes)) continue;
45589
+ const parent = identifier.parent;
45590
+ if (isNodeOfType(parent, "JSXOpeningElement") && isNodeOfType(parent.name, "JSXIdentifier") && parent.name === identifier) return true;
45591
+ const expression = findTransparentExpressionRoot(identifier);
45592
+ const expressionParent = expression.parent;
45593
+ if (isNodeOfType(expressionParent, "CallExpression") && expressionParent.arguments[0] === expression && isReactApiCall(expressionParent, "createElement", scopes, { resolveNamedAliases: true })) return true;
45594
+ if (!isNodeOfType(expressionParent, "VariableDeclarator") || expressionParent.init !== expression || !isNodeOfType(expressionParent.id, "Identifier") || !isNodeOfType(expressionParent.parent, "VariableDeclaration") || expressionParent.parent.kind !== "const") continue;
45595
+ const aliasSymbol = scopes.symbolFor(expressionParent.id);
45596
+ if (aliasSymbol && symbolHasReactComponentUse(aliasSymbol, scopes, visitedSymbolIds)) return true;
45597
+ }
45598
+ return false;
45599
+ };
45600
+ const functionHasReactComponentUse = (functionNode, scopes) => {
45601
+ return functionBindingSymbols(functionNode, scopes).some((symbol) => symbolHasReactComponentUse(symbol, scopes));
45602
+ };
43592
45603
  const isPreservedThroughConciseArrow = (callExpression, scopes) => {
43593
45604
  let node = callExpression;
43594
45605
  let parent = node.parent;
@@ -43635,7 +45646,10 @@ const noPropCallbackInRender = defineRule({
43635
45646
  create: (context) => ({ CallExpression(node) {
43636
45647
  if (!isResultDiscardedCall(node)) return;
43637
45648
  if (isPreservedThroughConciseArrow(node, context.scopes)) return;
43638
- if (!findRenderPhaseComponentOrHook(node, context.scopes)) return;
45649
+ const renderPhaseOwner = findRenderPhaseComponentOrHook(node, context.scopes);
45650
+ if (!renderPhaseOwner) return;
45651
+ const renderPhaseOwnerName = componentOrHookDisplayNameForFunction(renderPhaseOwner);
45652
+ if (!renderPhaseOwnerName || !isReactHookName(renderPhaseOwnerName) && !functionHasReactComponentEvidence(renderPhaseOwner, context.scopes, context.cfg) && !functionHasReactComponentUse(renderPhaseOwner, context.scopes)) return;
43639
45653
  const analysis = getProgramAnalysis(node);
43640
45654
  if (!analysis) return;
43641
45655
  const callee = stripParenExpression(node.callee);
@@ -44292,6 +46306,7 @@ const noRedundantRoles = defineRule({
44292
46306
  create: (context) => {
44293
46307
  const settings = resolveSettings$13(context.settings);
44294
46308
  return { JSXOpeningElement(node) {
46309
+ if (isLocalTestScaffoldJsx(node, context)) return;
44295
46310
  const roleAttr = hasJsxPropIgnoreCase(node.attributes, "role");
44296
46311
  if (!roleAttr) return;
44297
46312
  if (hasJsxPropIgnoreCase(node.attributes, "data-rac")) return;
@@ -44371,11 +46386,110 @@ const isSameRefCurrentMember = (node, refSymbol, scopes) => {
44371
46386
  return isNodeOfType(receiver, "Identifier") && resolveConstIdentifierAlias(receiver, scopes)?.id === refSymbol.id;
44372
46387
  };
44373
46388
  const isSameRefCurrentAlias = (node, refSymbol, scopes) => {
44374
- if (isSameRefCurrentMember(node, refSymbol, scopes)) return true;
44375
- if (!isNodeOfType(node, "Identifier")) return false;
44376
- const aliasSymbol = scopes.symbolFor(node);
46389
+ const expression = stripParenExpression(node);
46390
+ if (isSameRefCurrentMember(expression, refSymbol, scopes)) return true;
46391
+ if (!isNodeOfType(expression, "Identifier")) return false;
46392
+ const aliasSymbol = scopes.symbolFor(expression);
44377
46393
  return aliasSymbol?.kind === "const" && aliasSymbol.initializer !== null && isSameRefCurrentMember(stripParenExpression(aliasSymbol.initializer), refSymbol, scopes);
44378
46394
  };
46395
+ const resolveImmutableInitializationValue = (node, scopes, visitedSymbolIds = /* @__PURE__ */ new Set()) => {
46396
+ const expression = stripParenExpression(node);
46397
+ if (!isNodeOfType(expression, "Identifier")) return expression;
46398
+ const symbol = scopes.symbolFor(expression);
46399
+ if (!symbol || symbol.kind !== "const" || !symbol.initializer || symbol.references.some((reference) => reference.flag !== "read") || visitedSymbolIds.has(symbol.id)) return null;
46400
+ visitedSymbolIds.add(symbol.id);
46401
+ return resolveImmutableInitializationValue(symbol.initializer, scopes, visitedSymbolIds);
46402
+ };
46403
+ const isProvablyTruthyInitializationValue = (node, scopes) => {
46404
+ const expression = resolveImmutableInitializationValue(node, scopes);
46405
+ return Boolean(expression && (isNodeOfType(expression, "NewExpression") || isNodeOfType(expression, "ObjectExpression") || isNodeOfType(expression, "ArrayExpression") || isNodeOfType(expression, "ArrowFunctionExpression") || isNodeOfType(expression, "FunctionExpression") || isNodeOfType(expression, "ClassExpression")));
46406
+ };
46407
+ const getInitializationConstructorName = (node, scopes) => {
46408
+ const expression = resolveImmutableInitializationValue(node, scopes);
46409
+ if (!expression) return null;
46410
+ if (isNodeOfType(expression, "NewExpression")) {
46411
+ const callee = stripParenExpression(expression.callee);
46412
+ return isNodeOfType(callee, "Identifier") ? callee.name : null;
46413
+ }
46414
+ return null;
46415
+ };
46416
+ const isClosedTruthyTypeDomain = (typeNode, initializationValue, scopes) => {
46417
+ const initializationExpression = stripParenExpression(initializationValue);
46418
+ if (isNodeOfType(typeNode, "TSTypeLiteral")) return isNodeOfType(initializationExpression, "ObjectExpression");
46419
+ if (isNodeOfType(typeNode, "TSArrayType") || isNodeOfType(typeNode, "TSTupleType")) return isNodeOfType(initializationExpression, "ArrayExpression");
46420
+ if (isNodeOfType(typeNode, "TSFunctionType") || isNodeOfType(typeNode, "TSConstructorType")) return isNodeOfType(initializationExpression, "ArrowFunctionExpression") || isNodeOfType(initializationExpression, "FunctionExpression") || isNodeOfType(initializationExpression, "ClassExpression");
46421
+ if (isNodeOfType(typeNode, "TSObjectKeyword")) return true;
46422
+ if (!isNodeOfType(typeNode, "TSTypeReference")) return false;
46423
+ const typeName = typeNode.typeName;
46424
+ return isNodeOfType(typeName, "Identifier") && typeName.name === getInitializationConstructorName(initializationExpression, scopes);
46425
+ };
46426
+ const refHasClosedFalsySentinelDomain = (refSymbol, initializationValue, scopes) => {
46427
+ const initializer = refSymbol.initializer ? stripParenExpression(refSymbol.initializer) : null;
46428
+ if (!initializer || !isNodeOfType(initializer, "CallExpression")) return false;
46429
+ const [initialValue] = initializer.arguments ?? [];
46430
+ if (!initialValue || isNodeOfType(initialValue, "SpreadElement") || !isEmptySentinel(initialValue, scopes)) return false;
46431
+ const [declaredType] = initializer.typeArguments?.params ?? [];
46432
+ if (!declaredType || !isNodeOfType(declaredType, "TSUnionType")) return false;
46433
+ let hasEmptySentinel = false;
46434
+ let hasTruthyDomain = false;
46435
+ for (const memberType of declaredType.types ?? []) {
46436
+ if (isNodeOfType(memberType, "TSNullKeyword") || isNodeOfType(memberType, "TSUndefinedKeyword")) {
46437
+ hasEmptySentinel = true;
46438
+ continue;
46439
+ }
46440
+ if (!isClosedTruthyTypeDomain(memberType, initializationValue, scopes)) return false;
46441
+ hasTruthyDomain = true;
46442
+ }
46443
+ return hasEmptySentinel && hasTruthyDomain;
46444
+ };
46445
+ const isSafeRefIdentifierUse = (identifier) => {
46446
+ const expressionRoot = findTransparentExpressionRoot(identifier);
46447
+ const parent = expressionRoot.parent;
46448
+ if (parent && isNodeOfType(parent, "VariableDeclarator") && parent.id === expressionRoot && parent.parent !== null && isNodeOfType(parent.parent, "VariableDeclaration") && parent.parent.kind === "const") return true;
46449
+ if (parent && isNodeOfType(parent, "MemberExpression") && parent.object === expressionRoot && getStaticPropertyName(parent) === "current") return true;
46450
+ if (!parent || !isNodeOfType(parent, "VariableDeclarator") || parent.init !== expressionRoot) return false;
46451
+ return isNodeOfType(parent.id, "Identifier") && parent.parent !== null && isNodeOfType(parent.parent, "VariableDeclaration") && parent.parent.kind === "const";
46452
+ };
46453
+ const refDoesNotEscape = (branchRoot, refSymbol, scopes) => {
46454
+ let didEscape = false;
46455
+ walkAst(branchRoot, (child) => {
46456
+ if (didEscape) return false;
46457
+ if (!isNodeOfType(child, "Identifier")) return;
46458
+ if (resolveConstIdentifierAlias(child, scopes)?.id !== refSymbol.id) return;
46459
+ if (child === refSymbol.bindingIdentifier || isSafeRefIdentifierUse(child)) return;
46460
+ didEscape = true;
46461
+ return false;
46462
+ });
46463
+ return !didEscape;
46464
+ };
46465
+ const expressionContainsRefCurrent = (expression, refSymbol, scopes) => {
46466
+ let didFindRefCurrent = false;
46467
+ walkAst(expression, (child) => {
46468
+ if (didFindRefCurrent) return false;
46469
+ if (resolveReactRefSymbol(child, scopes)?.id !== refSymbol.id) return;
46470
+ didFindRefCurrent = true;
46471
+ return false;
46472
+ });
46473
+ return didFindRefCurrent;
46474
+ };
46475
+ const hasNoCompetingRefCurrentWrite = (branchRoot, assignmentExpression, refSymbol, scopes) => {
46476
+ let writeCount = 0;
46477
+ walkAst(branchRoot, (child) => {
46478
+ if (writeCount > 1) return false;
46479
+ if (isNodeOfType(child, "AssignmentExpression")) {
46480
+ if (expressionContainsRefCurrent(child.left, refSymbol, scopes)) writeCount++;
46481
+ return;
46482
+ }
46483
+ if (isNodeOfType(child, "UpdateExpression") || isNodeOfType(child, "UnaryExpression") && child.operator === "delete") {
46484
+ if (expressionContainsRefCurrent(child.argument, refSymbol, scopes)) writeCount++;
46485
+ return;
46486
+ }
46487
+ if (isNodeOfType(child, "ForInStatement") || isNodeOfType(child, "ForOfStatement")) {
46488
+ if (expressionContainsRefCurrent(child.left, refSymbol, scopes)) writeCount++;
46489
+ }
46490
+ });
46491
+ return writeCount === 1 && expressionContainsRefCurrent(assignmentExpression.left, refSymbol, scopes);
46492
+ };
44379
46493
  const isEmptySentinel = (node, scopes) => isNodeOfType(node, "Literal") && node.value === null || isNodeOfType(node, "Identifier") && node.name === "undefined" && scopes.isGlobalReference(node);
44380
46494
  const hasRepeatedExecutionAncestor = (node, stop) => {
44381
46495
  let ancestor = node.parent;
@@ -44424,18 +46538,22 @@ const hasNoPriorCoExecutableWrite = (assignmentExpression, branchRoot, refSymbol
44424
46538
  const isDocumentedLazyInitialization = (assignmentExpression, refSymbol, scopes) => {
44425
46539
  if (assignmentExpression.operator === "??=" || assignmentExpression.operator === "||=") return true;
44426
46540
  if (assignmentExpression.operator !== "=") return false;
46541
+ const renderOwner = findRenderPhaseComponentOrHook(assignmentExpression, scopes);
46542
+ if (!renderOwner) return false;
44427
46543
  let descendant = assignmentExpression;
44428
46544
  let ancestor = descendant.parent;
44429
46545
  while (ancestor) {
44430
- if (isNodeOfType(ancestor, "IfStatement") && isNodeOfType(ancestor.test, "BinaryExpression") && [
46546
+ const test = isNodeOfType(ancestor, "IfStatement") ? stripParenExpression(ancestor.test) : null;
46547
+ if (isNodeOfType(ancestor, "IfStatement") && test && isNodeOfType(test, "UnaryExpression") && test.operator === "!" && isSameRefCurrentAlias(test.argument, refSymbol, scopes) && ancestor.consequent === descendant && isProvablyTruthyInitializationValue(assignmentExpression.right, scopes) && refHasClosedFalsySentinelDomain(refSymbol, assignmentExpression.right, scopes) && !hasRepeatedExecutionAncestor(assignmentExpression, ancestor.consequent) && !hasRepeatedExecutionAncestor(ancestor, renderOwner) && hasNoPriorCoExecutableWrite(assignmentExpression, ancestor.consequent, refSymbol, scopes) && hasNoCompetingRefCurrentWrite(renderOwner, assignmentExpression, refSymbol, scopes) && refDoesNotEscape(renderOwner, refSymbol, scopes)) return true;
46548
+ if (isNodeOfType(ancestor, "IfStatement") && isNodeOfType(test, "BinaryExpression") && [
44431
46549
  "===",
44432
46550
  "==",
44433
46551
  "!==",
44434
46552
  "!="
44435
- ].includes(ancestor.test.operator)) {
44436
- const { left, right } = ancestor.test;
46553
+ ].includes(test.operator)) {
46554
+ const { left, right } = test;
44437
46555
  const comparesEmptySentinel = isSameRefCurrentAlias(left, refSymbol, scopes) && isEmptySentinel(right, scopes) || isSameRefCurrentAlias(right, refSymbol, scopes) && isEmptySentinel(left, scopes);
44438
- const guardedBranch = ancestor.test.operator === "===" || ancestor.test.operator === "==" ? ancestor.consequent : ancestor.alternate;
46556
+ const guardedBranch = test.operator === "===" || test.operator === "==" ? ancestor.consequent : ancestor.alternate;
44439
46557
  if (comparesEmptySentinel && guardedBranch === descendant && guardedBranch && !hasRepeatedExecutionAncestor(assignmentExpression, guardedBranch) && hasNoPriorCoExecutableWrite(assignmentExpression, guardedBranch, refSymbol, scopes)) return true;
44440
46558
  }
44441
46559
  descendant = ancestor;
@@ -44849,7 +46967,7 @@ const doConditionsImplyFormula = (conditions, target) => {
44849
46967
  }
44850
46968
  return facts.didConflict || evaluateBooleanFormula$1(target, facts.assignments) === true;
44851
46969
  };
44852
- const getFunctionBindingSymbol = (functionNode, scopes) => {
46970
+ const getFunctionBindingSymbol$1 = (functionNode, scopes) => {
44853
46971
  if (isNodeOfType(functionNode, "FunctionDeclaration") && functionNode.id) return scopes.symbolFor(functionNode.id);
44854
46972
  const parent = functionNode.parent;
44855
46973
  if ((isNodeOfType(functionNode, "ArrowFunctionExpression") || isNodeOfType(functionNode, "FunctionExpression")) && isNodeOfType(parent, "VariableDeclarator") && parent.init === functionNode && isNodeOfType(parent.id, "Identifier")) return scopes.symbolFor(parent.id);
@@ -44882,7 +47000,7 @@ const isNodeEvaluatedDuringRender = (node, componentNode, scopes, visitedFunctio
44882
47000
  const synchronousCallbackCall = getSynchronousCallbackCall(functionNode);
44883
47001
  if (synchronousCallbackCall) return isNodeEvaluatedDuringRender(synchronousCallbackCall, componentNode, scopes, visitedFunctionSymbolIds);
44884
47002
  if (executesDuringRender(functionNode, scopes)) return isNodeEvaluatedDuringRender(functionNode.parent ?? functionNode, componentNode, scopes, visitedFunctionSymbolIds);
44885
- const functionSymbol = getFunctionBindingSymbol(functionNode, scopes);
47003
+ const functionSymbol = getFunctionBindingSymbol$1(functionNode, scopes);
44886
47004
  if (!functionSymbol || visitedFunctionSymbolIds.has(functionSymbol.id)) return false;
44887
47005
  visitedFunctionSymbolIds.add(functionSymbol.id);
44888
47006
  let callCount = 0;
@@ -44931,7 +47049,7 @@ const collectExposureConditions = (analysis, context, node, componentNode, prote
44931
47049
  parent = synchronousCallbackCall.parent;
44932
47050
  continue;
44933
47051
  }
44934
- const functionSymbol = getFunctionBindingSymbol(parent, context.scopes);
47052
+ const functionSymbol = getFunctionBindingSymbol$1(parent, context.scopes);
44935
47053
  if (functionSymbol?.references.length === 1) {
44936
47054
  const callExpression = isReferenceDirectlyCalled(functionSymbol.references[0].identifier);
44937
47055
  if (callExpression) {
@@ -45141,7 +47259,7 @@ const getSetterExposureConditions = (analysis, context, setterReference, compone
45141
47259
  const functionNode = findEnclosingFunction$1(setterReference.identifier);
45142
47260
  if (!functionNode) return null;
45143
47261
  if (isInlineJsxCallback(functionNode)) return [collectExposureConditions(analysis, context, functionNode, componentNode, protectedSymbolIds)];
45144
- const functionSymbol = getFunctionBindingSymbol(functionNode, context.scopes);
47262
+ const functionSymbol = getFunctionBindingSymbol$1(functionNode, context.scopes);
45145
47263
  if (!functionSymbol || functionSymbol.references.length === 0) return null;
45146
47264
  const conditionsByReference = [];
45147
47265
  for (const reference of functionSymbol.references) {
@@ -45238,6 +47356,87 @@ const noResetAllStateOnPropChange = defineRule({
45238
47356
  } })
45239
47357
  });
45240
47358
  //#endregion
47359
+ //#region src/plugin/utils/is-proven-framer-motion-jsx-element.ts
47360
+ const MOTION_FACTORY_MODULES = new Set(["framer-motion", "motion/react"]);
47361
+ const MOTION_TAG_NAMESPACE_MODULES = new Set([
47362
+ "framer-motion/client",
47363
+ "framer-motion/m",
47364
+ "motion/react-client",
47365
+ "motion/react-m"
47366
+ ]);
47367
+ const MOTION_FACTORY_EXPORTS = new Set(["m", "motion"]);
47368
+ const getValueImportSource = (symbol) => {
47369
+ if (symbol.kind !== "import") return null;
47370
+ const declaration = symbol.declarationNode.parent;
47371
+ if (!declaration || !isNodeOfType(declaration, "ImportDeclaration") || isTypeOnlyImport(declaration) || isNodeOfType(symbol.declarationNode, "ImportSpecifier") && symbol.declarationNode.importKind === "type") return null;
47372
+ return typeof declaration.source.value === "string" ? declaration.source.value : null;
47373
+ };
47374
+ const getMemberParts = (node) => {
47375
+ if (isNodeOfType(node, "MemberExpression")) {
47376
+ const propertyName = getStaticPropertyName(node);
47377
+ return propertyName ? [node.object, propertyName] : null;
47378
+ }
47379
+ if (isNodeOfType(node, "JSXMemberExpression")) return isNodeOfType(node.property, "JSXIdentifier") ? [node.object, node.property.name] : null;
47380
+ return null;
47381
+ };
47382
+ const resolveSymbol = (node, scopes) => {
47383
+ if (!isNodeOfType(node, "Identifier") && !isNodeOfType(node, "JSXIdentifier")) return null;
47384
+ return resolveConstIdentifierAlias(node, scopes);
47385
+ };
47386
+ const isNamespaceFrom = (node, sources, scopes) => {
47387
+ const symbol = resolveSymbol(stripParenExpression(node), scopes);
47388
+ const source = symbol ? getValueImportSource(symbol) : null;
47389
+ return Boolean(source && sources.has(source) && symbol && isNodeOfType(symbol.declarationNode, "ImportNamespaceSpecifier"));
47390
+ };
47391
+ const isMotionFactory = (rawNode, scopes, visitedSymbolIds) => {
47392
+ const node = stripParenExpression(rawNode);
47393
+ if (isNamespaceFrom(node, MOTION_TAG_NAMESPACE_MODULES, scopes)) return true;
47394
+ const symbol = resolveSymbol(node, scopes);
47395
+ if (symbol?.kind === "import") {
47396
+ const source = getValueImportSource(symbol);
47397
+ const importedName = getImportedName(symbol.declarationNode);
47398
+ return Boolean(source && MOTION_FACTORY_MODULES.has(source) && importedName && MOTION_FACTORY_EXPORTS.has(importedName));
47399
+ }
47400
+ if (symbol?.kind === "const" && symbol.initializer) {
47401
+ if (visitedSymbolIds.has(symbol.id)) return false;
47402
+ visitedSymbolIds.add(symbol.id);
47403
+ return isMotionFactory(symbol.initializer, scopes, visitedSymbolIds);
47404
+ }
47405
+ const memberParts = getMemberParts(node);
47406
+ return Boolean(memberParts && MOTION_FACTORY_EXPORTS.has(memberParts[1]) && isNamespaceFrom(memberParts[0], MOTION_FACTORY_MODULES, scopes));
47407
+ };
47408
+ const isMotionComponent = (rawNode, scopes) => {
47409
+ return isMotionComponentWithVisitedSymbols(rawNode, scopes, /* @__PURE__ */ new Set());
47410
+ };
47411
+ const isMotionComponentWithVisitedSymbols = (rawNode, scopes, visitedSymbolIds) => {
47412
+ const node = stripParenExpression(rawNode);
47413
+ const symbol = resolveSymbol(node, scopes);
47414
+ if (symbol?.kind === "const" && symbol.initializer) {
47415
+ if (visitedSymbolIds.has(symbol.id)) return false;
47416
+ visitedSymbolIds.add(symbol.id);
47417
+ return isMotionComponentWithVisitedSymbols(symbol.initializer, scopes, visitedSymbolIds);
47418
+ }
47419
+ if (symbol?.kind === "import") {
47420
+ const source = getValueImportSource(symbol);
47421
+ return Boolean(source && MOTION_TAG_NAMESPACE_MODULES.has(source) && isNodeOfType(symbol.declarationNode, "ImportSpecifier") && getImportedName(symbol.declarationNode) !== "create");
47422
+ }
47423
+ const memberParts = getMemberParts(node);
47424
+ if (memberParts && isMotionFactory(memberParts[0], scopes, visitedSymbolIds)) return true;
47425
+ if (!isNodeOfType(node, "CallExpression")) return false;
47426
+ if (isMotionFactory(node.callee, scopes, visitedSymbolIds)) return true;
47427
+ const calleeMemberParts = getMemberParts(stripParenExpression(node.callee));
47428
+ return Boolean(calleeMemberParts && calleeMemberParts[1] === "create" && isMotionFactory(calleeMemberParts[0], scopes, visitedSymbolIds));
47429
+ };
47430
+ const isProvenFramerMotionJsxElement = (openingElement, scopes) => {
47431
+ const elementName = openingElement.name;
47432
+ if (isNodeOfType(elementName, "JSXIdentifier")) {
47433
+ if (/^[a-z]/.test(elementName.name)) return false;
47434
+ return isMotionComponent(elementName, scopes);
47435
+ }
47436
+ const memberParts = getMemberParts(elementName);
47437
+ return Boolean(memberParts && isMotionFactory(memberParts[0], scopes, /* @__PURE__ */ new Set()));
47438
+ };
47439
+ //#endregion
45241
47440
  //#region src/plugin/rules/performance/no-scale-from-zero.ts
45242
47441
  const noScaleFromZero = defineRule({
45243
47442
  id: "no-scale-from-zero",
@@ -45248,6 +47447,8 @@ const noScaleFromZero = defineRule({
45248
47447
  create: (context) => ({ JSXAttribute(node) {
45249
47448
  if (!isNodeOfType(node.name, "JSXIdentifier")) return;
45250
47449
  if (node.name.name !== "initial" && node.name.name !== "exit") return;
47450
+ const openingElement = node.parent;
47451
+ if (!openingElement || !isNodeOfType(openingElement, "JSXOpeningElement") || !Object.is(getAuthoritativeJsxAttribute(openingElement.attributes, node.name.name), node) || !isProvenFramerMotionJsxElement(openingElement, context.scopes)) return;
45251
47452
  if (!isNodeOfType(node.value, "JSXExpressionContainer")) return;
45252
47453
  const expression = node.value.expression;
45253
47454
  if (!isNodeOfType(expression, "ObjectExpression")) return;
@@ -45446,7 +47647,7 @@ const isSelfReferentialSentinelValue = (variableName, literalValue) => literalVa
45446
47647
  const isIdentifierLikeKeyNameValue = (literalValue) => {
45447
47648
  const wordSegments = literalValue.replace(/^[_$\s]+|[_$\s]+$/g, "").split(/[_\-:./$]+/).filter((segment) => segment.length > 0);
45448
47649
  if (wordSegments.length < 2) return false;
45449
- return wordSegments.every((segment) => /^[a-z]+$/.test(segment));
47650
+ return wordSegments.every((segment) => /^[a-z]+(?:[A-Z][a-z]+)*$/.test(segment));
45450
47651
  };
45451
47652
  const FRAMEWORK_ENV_ADVICE = [
45452
47653
  [
@@ -45520,7 +47721,7 @@ const noSecretsInClientCode = defineRule({
45520
47721
  const isServerOnlyScope = isInsideServerOnlyScope(node);
45521
47722
  const trailingSuffix = getIdentifierTrailingWord(variableName);
45522
47723
  const isUiConstant = SECRET_FALSE_POSITIVE_SUFFIXES.has(trailingSuffix);
45523
- if (shouldUseVariableNameHeuristic && !isServerOnlyScope && SECRET_VARIABLE_PATTERN.test(variableName) && !isUiConstant && !isPublicUrlValue(literalValue) && !isPlaceholderValueForVariableHeuristic && !isSelfReferentialSentinelValue(variableName, literalValue) && !isIdentifierLikeKeyNameValue(literalValue) && !isSelfReferentialSentinelValue(variableName, literalValue) && !isIdentifierLikeKeyNameValue(literalValue) && literalValue.length > 24) {
47724
+ if (shouldUseVariableNameHeuristic && !isServerOnlyScope && SECRET_VARIABLE_PATTERN.test(variableName) && !isUiConstant && !isPublicUrlValue(literalValue) && !isPlaceholderValueForVariableHeuristic && !isSelfReferentialSentinelValue(variableName, literalValue) && !isIdentifierLikeKeyNameValue(literalValue) && literalValue.length > 24) {
45524
47725
  context.report({
45525
47726
  node,
45526
47727
  message: `Hardcoding "${variableName}" in client code is a security vulnerability: the secret ships to the browser where anyone can read it.`
@@ -50685,10 +52886,9 @@ const isSameApplicationChannelInstance = (targetText, fileContent) => {
50685
52886
  return new RegExp(`(?<![\\w$.])${escapeRegExp(receiverRoot)}\\s*${SAME_APPLICATION_CHANNEL_CONSTRUCTOR_SOURCE}`).test(fileContent);
50686
52887
  };
50687
52888
  const WORKER_FILE_PATH_PATTERN = /worker/i;
50688
- const getNodeStartIndex = (node) => "start" in node && typeof node.start === "number" ? node.start : -1;
50689
52889
  const getNodeText = (content, node) => {
50690
52890
  const startIndex = getNodeStartIndex(node);
50691
- const endIndex = "end" in node && typeof node.end === "number" ? node.end : -1;
52891
+ const endIndex = getNodeEndIndex(node);
50692
52892
  if (startIndex < 0 || endIndex < 0) return "";
50693
52893
  return content.slice(startIndex, endIndex);
50694
52894
  };
@@ -51085,17 +53285,6 @@ const preferEs6Class = defineRule({
51085
53285
  }
51086
53286
  });
51087
53287
  //#endregion
51088
- //#region src/plugin/utils/is-jsx-element-or-fragment.ts
51089
- /**
51090
- * Type-guard for the two single-node JSX output forms: `JSXElement`
51091
- * (`<Foo />`) and `JSXFragment` (`<>…</>`). Canonical home for the
51092
- * `isNodeOfType(x, "JSXElement") || isNodeOfType(x, "JSXFragment")` check
51093
- * that many rules otherwise inline. Does NOT unwrap parens / TS wrappers —
51094
- * callers that need the semantic expression should `stripParenExpression`
51095
- * first.
51096
- */
51097
- const isJsxElementOrFragment = (node) => Boolean(node && (isNodeOfType(node, "JSXElement") || isNodeOfType(node, "JSXFragment")));
51098
- //#endregion
51099
53288
  //#region src/plugin/rules/architecture/prefer-explicit-variants.ts
51100
53289
  const resolveBooleanPropTestName = (testNode, booleanPropBindings) => {
51101
53290
  let identifierNode = stripParenExpression(testNode);
@@ -51588,17 +53777,23 @@ const isProvenNodeCryptoNamespaceReference = (expression, scopes) => {
51588
53777
  //#region src/plugin/rules/architecture/prefer-module-scope-static-value.ts
51589
53778
  const MUTATING_RECEIVER_METHOD_NAMES = new Set([...MUTATING_ARRAY_METHODS, ...MUTATING_COLLECTION_METHODS]);
51590
53779
  const isMutationContext = (referenceIdentifier) => {
51591
- const parent = referenceIdentifier.parent;
51592
- if (!parent) return false;
51593
- if (isNodeOfType(parent, "AssignmentExpression") && parent.left === referenceIdentifier) return true;
51594
- if (isNodeOfType(parent, "UpdateExpression") && parent.argument === referenceIdentifier) return true;
51595
- if (isNodeOfType(parent, "MemberExpression") && parent.object === referenceIdentifier) {
51596
- const grandparent = parent.parent;
51597
- if (!grandparent) return false;
51598
- if (isNodeOfType(grandparent, "AssignmentExpression") && grandparent.left === parent) return true;
51599
- if (isNodeOfType(grandparent, "UpdateExpression") && grandparent.argument === parent) return true;
51600
- if (isNodeOfType(grandparent, "UnaryExpression") && grandparent.operator === "delete" && grandparent.argument === parent) return true;
51601
- if (isNodeOfType(grandparent, "CallExpression") && grandparent.callee === parent && !parent.computed && isNodeOfType(parent.property, "Identifier") && MUTATING_RECEIVER_METHOD_NAMES.has(parent.property.name)) return true;
53780
+ let mutationTarget = referenceIdentifier;
53781
+ let receiverMethodName = null;
53782
+ while (mutationTarget.parent) {
53783
+ const parent = mutationTarget.parent;
53784
+ if (TRANSPARENT_EXPRESSION_WRAPPER_TYPES.has(parent.type) && "expression" in parent && parent.expression === mutationTarget) {
53785
+ mutationTarget = parent;
53786
+ continue;
53787
+ }
53788
+ if (isNodeOfType(parent, "MemberExpression") && parent.object === mutationTarget) {
53789
+ receiverMethodName = getStaticPropertyName(parent);
53790
+ mutationTarget = parent;
53791
+ continue;
53792
+ }
53793
+ if (isNodeOfType(parent, "AssignmentExpression") && parent.left === mutationTarget) return true;
53794
+ if (isNodeOfType(parent, "UpdateExpression") && parent.argument === mutationTarget) return true;
53795
+ if (isNodeOfType(parent, "UnaryExpression") && parent.operator === "delete" && parent.argument === mutationTarget) return true;
53796
+ return Boolean(isNodeOfType(parent, "CallExpression") && parent.callee === mutationTarget && MUTATING_RECEIVER_METHOD_NAMES.has(receiverMethodName ?? ""));
51602
53797
  }
51603
53798
  return false;
51604
53799
  };
@@ -51975,6 +54170,15 @@ const STABLE_REACT_HOOK_VALUE_NAMES = new Set([
51975
54170
  "useState",
51976
54171
  "useTransition"
51977
54172
  ]);
54173
+ const REGISTRATION_METHOD_BY_RELEASE_METHOD = new Map([
54174
+ ["off", "on"],
54175
+ ["removeEventListener", "addEventListener"],
54176
+ ["removeListener", "addListener"],
54177
+ ["unlisten", "listen"],
54178
+ ["unsub", "sub"],
54179
+ ["unsubscribe", "subscribe"],
54180
+ ["unwatch", "watch"]
54181
+ ]);
51978
54182
  const isStableReactHookDependency = (dependency, context) => {
51979
54183
  const unwrappedDependency = stripParenExpression(dependency);
51980
54184
  if (!isNodeOfType(unwrappedDependency, "Identifier")) return false;
@@ -52040,30 +54244,87 @@ const isCallExpressionWithSubHandlerCallee = (callExpression) => {
52040
54244
  if (isNodeOfType(callee, "MemberExpression") && isNodeOfType(callee.property, "Identifier") && SUBSCRIPTION_METHOD_NAMES.has(callee.property.name)) return true;
52041
54245
  return false;
52042
54246
  };
52043
- const findSubHandlerForEnclosingFunction = (enclosingFunction, effectCallback) => {
54247
+ const getStaticMemberCallMethodName = (callExpression) => {
54248
+ if (!isNodeOfType(callExpression, "CallExpression")) return null;
54249
+ const callee = callExpression.callee;
54250
+ return isNodeOfType(callee, "MemberExpression") && !callee.computed && isNodeOfType(callee.property, "Identifier") ? callee.property.name : null;
54251
+ };
54252
+ const getCallArgumentUse = (reference) => {
54253
+ const argument = findTransparentExpressionRoot(reference);
54254
+ const parent = argument.parent;
54255
+ if (!isNodeOfType(parent, "CallExpression")) return null;
54256
+ const argumentIndex = (parent.arguments ?? []).findIndex((candidateArgument) => candidateArgument === argument);
54257
+ return argumentIndex === -1 ? null : {
54258
+ callExpression: parent,
54259
+ argumentIndex
54260
+ };
54261
+ };
54262
+ const isMatchingRegistrationAndRelease = (registration, release, context) => {
54263
+ const releaseMethodName = getStaticMemberCallMethodName(release.callExpression);
54264
+ const expectedRegistrationMethod = releaseMethodName ? REGISTRATION_METHOD_BY_RELEASE_METHOD.get(releaseMethodName) : null;
54265
+ if (getStaticMemberCallMethodName(registration.callExpression) !== expectedRegistrationMethod) return false;
54266
+ if (registration.argumentIndex !== release.argumentIndex) return false;
54267
+ const registrationCallee = registration.callExpression.callee;
54268
+ const releaseCallee = release.callExpression.callee;
54269
+ if (!isNodeOfType(registrationCallee, "MemberExpression") || !isNodeOfType(releaseCallee, "MemberExpression")) return false;
54270
+ const registrationReceiverKey = resolveExpressionKey$1(registrationCallee.object, context);
54271
+ if (registrationReceiverKey === null || registrationReceiverKey !== resolveExpressionKey$1(releaseCallee.object, context)) return false;
54272
+ const registrationArguments = registration.callExpression.arguments ?? [];
54273
+ const releaseArguments = release.callExpression.arguments ?? [];
54274
+ if (registrationArguments.length !== releaseArguments.length) return false;
54275
+ return registrationArguments.every((registrationArgument, argumentIndex) => {
54276
+ if (argumentIndex === registration.argumentIndex) return true;
54277
+ const registrationArgumentKey = resolveExpressionKey$1(registrationArgument, context);
54278
+ return registrationArgumentKey !== null && registrationArgumentKey === resolveExpressionKey$1(releaseArguments[argumentIndex], context);
54279
+ });
54280
+ };
54281
+ const findExclusiveSubHandlerCall = (enclosingFunction, context) => {
52044
54282
  const directParent = enclosingFunction.parent;
52045
54283
  if (isNodeOfType(directParent, "CallExpression") && (directParent.arguments ?? []).some((arg) => arg === enclosingFunction) && isCallExpressionWithSubHandlerCallee(directParent)) return directParent;
52046
- const localName = getFunctionBindingName$1(enclosingFunction);
52047
- if (localName === null) return null;
52048
- let matchingSubHandlerCall = null;
52049
- walkAst(effectCallback, (child) => {
52050
- if (matchingSubHandlerCall) return false;
52051
- if (!isNodeOfType(child, "CallExpression")) return;
52052
- if (!isCallExpressionWithSubHandlerCallee(child)) return;
52053
- for (const argument of child.arguments ?? []) if (isNodeOfType(argument, "Identifier") && argument.name === localName) {
52054
- matchingSubHandlerCall = child;
52055
- return false;
54284
+ const bindingIdentifier = getFunctionBindingIdentifier$1(enclosingFunction);
54285
+ if (!bindingIdentifier) return null;
54286
+ let bindingSymbol = context.scopes.symbolFor(bindingIdentifier);
54287
+ if (isNodeOfType(enclosingFunction, "FunctionDeclaration")) {
54288
+ let bindingScope = context.scopes.scopeFor(enclosingFunction);
54289
+ bindingSymbol = null;
54290
+ while (bindingScope && !bindingSymbol) {
54291
+ bindingSymbol = bindingScope.symbols.find((candidateSymbol) => candidateSymbol.declarationNode === enclosingFunction) ?? null;
54292
+ bindingScope = bindingScope.parent;
52056
54293
  }
52057
- });
52058
- return matchingSubHandlerCall;
54294
+ }
54295
+ if (!bindingSymbol) return null;
54296
+ const registrations = [];
54297
+ const releases = [];
54298
+ for (const reference of bindingSymbol.references) {
54299
+ if (isAstDescendant(reference.identifier, enclosingFunction)) continue;
54300
+ if (reference.identifier === bindingIdentifier) continue;
54301
+ if (reference.flag !== "read") return null;
54302
+ const receivingUse = getCallArgumentUse(reference.identifier);
54303
+ if (!receivingUse) return null;
54304
+ if (isCallExpressionWithSubHandlerCallee(receivingUse.callExpression)) {
54305
+ registrations.push(receivingUse);
54306
+ continue;
54307
+ }
54308
+ const methodName = getStaticMemberCallMethodName(receivingUse.callExpression);
54309
+ if (!methodName || !REGISTRATION_METHOD_BY_RELEASE_METHOD.has(methodName)) return null;
54310
+ releases.push(receivingUse);
54311
+ }
54312
+ if (releases.some((release) => !registrations.some((registration) => isMatchingRegistrationAndRelease(registration, release, context)))) return null;
54313
+ return registrations[0]?.callExpression ?? null;
52059
54314
  };
52060
- const classifyCallableReadsInsideEffect = (callableName, effectCallback) => {
54315
+ const classifyCallableReadsInsideEffect = (callableIdentifier, effectCallback, context) => {
52061
54316
  let hasAnyRead = false;
52062
54317
  let allReadsAreInSubHandlers = true;
52063
54318
  let firstSubHandlerName = null;
54319
+ const callableSymbol = context.scopes.symbolFor(callableIdentifier);
54320
+ if (!callableSymbol) return {
54321
+ hasAnyRead,
54322
+ allReadsAreInSubHandlers,
54323
+ firstSubHandlerName
54324
+ };
52064
54325
  walkAst(effectCallback, (child) => {
52065
54326
  if (!isNodeOfType(child, "Identifier")) return;
52066
- if (child.name !== callableName) return;
54327
+ if (context.scopes.symbolFor(child)?.id !== callableSymbol.id) return;
52067
54328
  const parent = child.parent;
52068
54329
  if (isNodeOfType(parent, "ArrayExpression")) return;
52069
54330
  if (isNodeOfType(parent, "MemberExpression") && !parent.computed && parent.property === child) return;
@@ -52074,7 +54335,7 @@ const classifyCallableReadsInsideEffect = (callableName, effectCallback) => {
52074
54335
  allReadsAreInSubHandlers = false;
52075
54336
  return;
52076
54337
  }
52077
- const subHandlerCall = findSubHandlerForEnclosingFunction(enclosingFunction, effectCallback);
54338
+ const subHandlerCall = findExclusiveSubHandlerCall(enclosingFunction, context);
52078
54339
  if (!subHandlerCall) {
52079
54340
  allReadsAreInSubHandlers = false;
52080
54341
  return;
@@ -52117,7 +54378,7 @@ const preferUseEffectEvent = defineRule({
52117
54378
  const isFunctionTypedPropDep = propStackTracker.isPropName(depName) && REACT_HANDLER_PROP_PATTERN.test(depName);
52118
54379
  const isFunctionTypedLocalDep = potentiallyChangingCallbackBindings.has(depName);
52119
54380
  if (!isFunctionTypedPropDep && !isFunctionTypedLocalDep) continue;
52120
- const classification = classifyCallableReadsInsideEffect(depName, callback);
54381
+ const classification = classifyCallableReadsInsideEffect(depElement, callback, context);
52121
54382
  if (!classification.hasAnyRead) continue;
52122
54383
  if (!classification.allReadsAreInSubHandlers) continue;
52123
54384
  const subHandlerLabel = classification.firstSubHandlerName ? `\`${classification.firstSubHandlerName}\`` : "an async sub-handler";
@@ -53432,12 +55693,7 @@ const DOMPURIFY_MODULES = new Set(["dompurify", "isomorphic-dompurify"]);
53432
55693
  const REACT_MARKDOWN_NAMED_EXPORTS = new Set(["MarkdownAsync", "MarkdownHooks"]);
53433
55694
  const REACT_MARKDOWN_NAMESPACE_EXPORTS = new Set(["default", ...REACT_MARKDOWN_NAMED_EXPORTS]);
53434
55695
  const DEFAULT_EXPORT_NAMES = new Set(["default"]);
53435
- const getImportDeclaration = (symbol) => {
53436
- if (symbol.kind !== "import") return null;
53437
- const importDeclaration = symbol.declarationNode.parent;
53438
- return isNodeOfType(importDeclaration, "ImportDeclaration") ? importDeclaration : null;
53439
- };
53440
- const isImportFromModule = (symbol, moduleName) => getImportDeclaration(symbol)?.source.value === moduleName;
55696
+ const isImportFromModule = (symbol, moduleName) => getImportDeclarationForSymbol(symbol)?.source.value === moduleName;
53441
55697
  const isDefaultImportSymbol = (symbol, moduleName) => {
53442
55698
  if (!isImportFromModule(symbol, moduleName)) return false;
53443
55699
  return isNodeOfType(symbol.declarationNode, "ImportDefaultSpecifier") || getImportedName(symbol.declarationNode) === "default";
@@ -53527,7 +55783,7 @@ const getAttributeExpression = (attribute) => {
53527
55783
  const isDomPurifyNamespace = (node, scopes) => {
53528
55784
  const symbol = resolveImportedIdentifier(node, scopes);
53529
55785
  if (!symbol) return false;
53530
- const importDeclaration = getImportDeclaration(symbol);
55786
+ const importDeclaration = getImportDeclarationForSymbol(symbol);
53531
55787
  if (!importDeclaration || !DOMPURIFY_MODULES.has(String(importDeclaration.source.value))) return false;
53532
55788
  return isNodeOfType(symbol.declarationNode, "ImportDefaultSpecifier") || isNodeOfType(symbol.declarationNode, "ImportNamespaceSpecifier") || getImportedName(symbol.declarationNode) === "default";
53533
55789
  };
@@ -56501,7 +58757,7 @@ const resolveImportedRecyclerName = (node, localName, options) => {
56501
58757
  const jsxMemberObjectName = options?.allowNamespaceMemberAccess ? getJsxMemberObjectName(node) : null;
56502
58758
  for (const [canonicalName, packageSources] of Object.entries(RECYCLABLE_LIST_PACKAGES)) {
56503
58759
  if (jsxMemberObjectName !== null) {
56504
- if (localName === canonicalName && packageSources.some((packageSource) => isNamespaceImportFromModule(node, jsxMemberObjectName, packageSource))) return canonicalName;
58760
+ if (localName === canonicalName && packageSources.some((packageSource) => isNamespaceImportFromModule$1(node, jsxMemberObjectName, packageSource))) return canonicalName;
56505
58761
  continue;
56506
58762
  }
56507
58763
  if (packageSources.some((packageSource) => getImportedNameFromModule(node, localName, packageSource) === canonicalName)) return canonicalName;
@@ -57760,7 +60016,7 @@ const isNamedImportOf = (contextNode, localName, componentName) => {
57760
60016
  return false;
57761
60017
  };
57762
60018
  const isExpoUiNamespaceImport = (contextNode, localName) => {
57763
- for (const moduleSource of EXPO_UI_MODULE_SOURCES) if (isNamespaceImportFromModule(contextNode, localName, moduleSource)) return true;
60019
+ for (const moduleSource of EXPO_UI_MODULE_SOURCES) if (isNamespaceImportFromModule$1(contextNode, localName, moduleSource)) return true;
57764
60020
  return false;
57765
60021
  };
57766
60022
  const isExpoUiComponentElement = (openingElement, contextNode, componentName) => {
@@ -58908,6 +61164,7 @@ const roleHasRequiredAriaProps = defineRule({
58908
61164
  recommendation: "Add every required `aria-*` attribute so assistive tech can expose the role's state correctly.",
58909
61165
  category: "Accessibility",
58910
61166
  create: (context) => ({ JSXOpeningElement(node) {
61167
+ if (isLocalTestScaffoldJsx(node, context)) return;
58911
61168
  const elementType = getElementType(node, context.settings);
58912
61169
  if (!HTML_TAGS.has(elementType)) return;
58913
61170
  const roleAttribute = hasJsxPropIgnoreCase(node.attributes, "role");
@@ -62043,6 +64300,7 @@ const roleSupportsAriaProps = defineRule({
62043
64300
  recommendation: "Only use `aria-*` attributes that the element's role supports.",
62044
64301
  category: "Accessibility",
62045
64302
  create: (context) => ({ JSXOpeningElement(node) {
64303
+ if (isLocalTestScaffoldJsx(node, context)) return;
62046
64304
  let ariaAttributes = null;
62047
64305
  for (const attribute of node.attributes) {
62048
64306
  if (!isNodeOfType(attribute, "JSXAttribute")) continue;
@@ -62788,6 +65046,109 @@ const isDeferrableSideEffectCall = (objectName, methodName) => {
62788
65046
  if (ANALYTICS_DEFERRABLE_OBJECTS.has(objectName)) return ANALYTICS_DEFERRABLE_METHODS.has(methodName);
62789
65047
  return false;
62790
65048
  };
65049
+ const NEXT_SERVER_SOURCE = "next/server";
65050
+ const NEXT_AFTER_EXPORT_NAMES = new Set(["after", "unstable_after"]);
65051
+ const isNextAfterImportSymbol = (symbol, contextNode) => {
65052
+ if (symbol.kind !== "import") return false;
65053
+ const importBinding = getImportBindingForName(contextNode, symbol.name);
65054
+ return Boolean(importBinding && importBinding.source === NEXT_SERVER_SOURCE && !importBinding.isNamespace && importBinding.exportedName && NEXT_AFTER_EXPORT_NAMES.has(importBinding.exportedName));
65055
+ };
65056
+ const isDirectObjectPatternBinding = (symbol) => {
65057
+ if (!isNodeOfType(symbol.declarationNode, "VariableDeclarator")) return false;
65058
+ if (!isNodeOfType(symbol.declarationNode.id, "ObjectPattern")) return false;
65059
+ let bindingNode = symbol.bindingIdentifier;
65060
+ if (isNodeOfType(bindingNode.parent, "AssignmentPattern") && bindingNode.parent.left === bindingNode) bindingNode = bindingNode.parent;
65061
+ const property = bindingNode.parent;
65062
+ return Boolean(isNodeOfType(property, "Property") && property.value === bindingNode && property.parent === symbol.declarationNode.id);
65063
+ };
65064
+ const isNextServerNamespace = (expression, contextNode, scopes) => {
65065
+ let candidate = stripParenExpression(expression);
65066
+ const visitedSymbolIds = /* @__PURE__ */ new Set();
65067
+ while (isNodeOfType(candidate, "Identifier")) {
65068
+ const symbol = scopes.symbolFor(candidate);
65069
+ if (!symbol || visitedSymbolIds.has(symbol.id)) return false;
65070
+ if (symbol.kind === "import") {
65071
+ const importBinding = getImportBindingForName(contextNode, symbol.name);
65072
+ return Boolean(importBinding?.source === NEXT_SERVER_SOURCE && importBinding.isNamespace);
65073
+ }
65074
+ if (symbol.kind !== "const" || !symbol.initializer || !isNodeOfType(symbol.declarationNode, "VariableDeclarator") || symbol.declarationNode.id !== symbol.bindingIdentifier) return false;
65075
+ visitedSymbolIds.add(symbol.id);
65076
+ candidate = stripParenExpression(symbol.initializer);
65077
+ }
65078
+ return false;
65079
+ };
65080
+ const isNextAfterCallee = (callee, contextNode, scopes, visitedSymbolIds = /* @__PURE__ */ new Set()) => {
65081
+ const candidate = stripParenExpression(callee);
65082
+ if (isNodeOfType(candidate, "MemberExpression")) {
65083
+ const propertyName = getStaticPropertyKeyName(candidate, { allowComputedString: true });
65084
+ return Boolean(propertyName && NEXT_AFTER_EXPORT_NAMES.has(propertyName) && isNextServerNamespace(candidate.object, contextNode, scopes));
65085
+ }
65086
+ if (!isNodeOfType(candidate, "Identifier")) return false;
65087
+ const symbol = scopes.symbolFor(candidate);
65088
+ if (!symbol || visitedSymbolIds.has(symbol.id)) return false;
65089
+ if (isNextAfterImportSymbol(symbol, contextNode)) return true;
65090
+ const destructuredPropertyName = getDestructuredBindingPropertyName(symbol.bindingIdentifier);
65091
+ if (symbol.kind === "const" && symbol.initializer && isDirectObjectPatternBinding(symbol) && destructuredPropertyName && NEXT_AFTER_EXPORT_NAMES.has(destructuredPropertyName)) return isNextServerNamespace(symbol.initializer, contextNode, scopes);
65092
+ if (symbol.kind !== "const" || !symbol.initializer || !isNodeOfType(symbol.declarationNode, "VariableDeclarator") || symbol.declarationNode.id !== symbol.bindingIdentifier) return false;
65093
+ visitedSymbolIds.add(symbol.id);
65094
+ return isNextAfterCallee(symbol.initializer, contextNode, scopes, visitedSymbolIds);
65095
+ };
65096
+ const getDirectArgumentCall = (expression) => {
65097
+ const expressionRoot = findTransparentExpressionRoot(expression);
65098
+ const parent = expressionRoot.parent;
65099
+ if (!isNodeOfType(parent, "CallExpression")) return null;
65100
+ return parent.arguments[0] === expressionRoot ? parent : null;
65101
+ };
65102
+ const isScheduledByNextAfter = (expression, scopes) => {
65103
+ const callExpression = getDirectArgumentCall(expression);
65104
+ return Boolean(callExpression && isNextAfterCallee(callExpression.callee, callExpression, scopes));
65105
+ };
65106
+ const getFunctionBindingSymbol = (functionNode, scopes) => {
65107
+ if (isNodeOfType(functionNode, "FunctionDeclaration") && functionNode.id) return scopes.scopeFor(functionNode).symbols.find((symbol) => symbol.declarationNode === functionNode) ?? null;
65108
+ const functionRoot = findTransparentExpressionRoot(functionNode);
65109
+ const parent = functionRoot.parent;
65110
+ if (!isNodeOfType(parent, "VariableDeclarator") || parent.init !== functionRoot || !isNodeOfType(parent.id, "Identifier")) return null;
65111
+ return scopes.symbolFor(parent.id);
65112
+ };
65113
+ const isDirectlyExported = (symbol) => {
65114
+ let declaration = symbol.declarationNode;
65115
+ if (isNodeOfType(declaration, "VariableDeclarator")) declaration = declaration.parent;
65116
+ return Boolean(declaration?.parent && (isNodeOfType(declaration.parent, "ExportNamedDeclaration") || isNodeOfType(declaration.parent, "ExportDefaultDeclaration")));
65117
+ };
65118
+ const isLexicallyInsideFunction = (node, functionNode) => {
65119
+ let enclosingFunction = findEnclosingFunction$1(node);
65120
+ while (enclosingFunction) {
65121
+ if (enclosingFunction === functionNode) return true;
65122
+ enclosingFunction = findEnclosingFunction$1(enclosingFunction);
65123
+ }
65124
+ return false;
65125
+ };
65126
+ const isExclusivelyScheduledByNextAfter = (functionNode, scopes, visitedFunctionSymbolIds) => {
65127
+ if (isScheduledByNextAfter(functionNode, scopes)) return true;
65128
+ const functionSymbol = getFunctionBindingSymbol(functionNode, scopes);
65129
+ if (!functionSymbol || isDirectlyExported(functionSymbol) || visitedFunctionSymbolIds.has(functionSymbol.id)) return false;
65130
+ const nextVisitedFunctionSymbolIds = new Set(visitedFunctionSymbolIds).add(functionSymbol.id);
65131
+ let hasAfterUse = false;
65132
+ for (const reference of functionSymbol.references) {
65133
+ if (reference.flag !== "read") return false;
65134
+ if (isLexicallyInsideFunction(reference.identifier, functionNode)) continue;
65135
+ if (isScheduledByNextAfter(reference.identifier, scopes)) {
65136
+ hasAfterUse = true;
65137
+ continue;
65138
+ }
65139
+ if (!isInsideNextAfterCallback(reference.identifier, scopes, nextVisitedFunctionSymbolIds)) return false;
65140
+ hasAfterUse = true;
65141
+ }
65142
+ return hasAfterUse;
65143
+ };
65144
+ const isInsideNextAfterCallback = (node, scopes, visitedFunctionSymbolIds = /* @__PURE__ */ new Set()) => {
65145
+ let enclosingFunction = findEnclosingFunction$1(node);
65146
+ while (enclosingFunction) {
65147
+ if (isExclusivelyScheduledByNextAfter(enclosingFunction, scopes, visitedFunctionSymbolIds)) return true;
65148
+ enclosingFunction = findEnclosingFunction$1(enclosingFunction);
65149
+ }
65150
+ return false;
65151
+ };
62791
65152
  const serverAfterNonblocking = defineRule({
62792
65153
  id: "server-after-nonblocking",
62793
65154
  title: "Blocking side effect before response",
@@ -62822,6 +65183,7 @@ const serverAfterNonblocking = defineRule({
62822
65183
  if (!objectName) return;
62823
65184
  const methodName = node.callee.property.name;
62824
65185
  if (!isDeferrableSideEffectCall(objectName, methodName)) return;
65186
+ if (isInsideNextAfterCallback(node, context.scopes)) return;
62825
65187
  context.report({
62826
65188
  node,
62827
65189
  message: `${objectName}.${methodName}() runs before the response, so your users wait longer for it.`
@@ -64096,17 +66458,34 @@ const stylePropObject = defineRule({
64096
66458
  };
64097
66459
  }
64098
66460
  });
66461
+ //#endregion
66462
+ //#region src/plugin/rules/security-scan/utils/has-use-server-directive-in-content.ts
66463
+ const hasUseServerDirectiveInContent = (content, relativePath = "source.tsx") => {
66464
+ const programNode = parseSourceText({
66465
+ filename: relativePath,
66466
+ sourceText: content,
66467
+ shouldAttachParentReferences: false
66468
+ });
66469
+ return programNode === null ? false : hasDirective(programNode, "use server");
66470
+ };
66471
+ //#endregion
66472
+ //#region src/plugin/rules/security-scan/supabase-client-owned-authz-field.ts
66473
+ const scanSupabaseClientOwnedAuthzField = scanByPattern({
66474
+ shouldScan: (file) => isClientSourcePath(file.relativePath),
66475
+ pattern: /\b(?:ownerId|ownerID|creatorId|creatorID|userId|userID|uid|providerId|providerID|orgId|orgID|tenantId|tenantID|teamId|teamID|workspaceId|workspaceID|ghostOrg|role|roles|isAdmin|admin)\b/,
66476
+ requireAll: [/\b(?:supabase\b|\.from\s*\(\s*["'][^"']+["']\s*\))[\s\S]{0,700}\b(?:insert|upsert|update)\s*\(\s*(?:\{|\[?\s*\{)[\s\S]{0,700}\b(?:ownerId|creatorId|userId|orgId|tenantId|role|isAdmin)\b/i],
66477
+ message: "Client Supabase code appears to write user, tenant, owner, or role fields that should be enforced by RLS."
66478
+ });
64099
66479
  const supabaseClientOwnedAuthzField = defineRule({
64100
66480
  id: "supabase-client-owned-authz-field",
64101
66481
  title: "Client writes Supabase authorization field",
64102
66482
  severity: "error",
64103
66483
  recommendation: "Use RLS policies based on `auth.uid()` and server-owned membership rows; do not trust client-provided owner, org, or role columns.",
64104
- scan: scanByPattern({
64105
- shouldScan: (file) => isClientSourcePath(file.relativePath),
64106
- pattern: /\b(?:ownerId|ownerID|creatorId|creatorID|userId|userID|uid|providerId|providerID|orgId|orgID|tenantId|tenantID|teamId|teamID|workspaceId|workspaceID|ghostOrg|role|roles|isAdmin|admin)\b/,
64107
- requireAll: [/\b(?:supabase\b|\.from\s*\(\s*["'][^"']+["']\s*\))[\s\S]{0,700}\b(?:insert|upsert|update)\s*\(\s*(?:\{|\[?\s*\{)[\s\S]{0,700}\b(?:ownerId|creatorId|userId|orgId|tenantId|role|isAdmin)\b/i],
64108
- message: "Client Supabase code appears to write user, tenant, owner, or role fields that should be enforced by RLS."
64109
- })
66484
+ scan: (file) => {
66485
+ const findings = scanSupabaseClientOwnedAuthzField(file);
66486
+ if (findings.length === 0) return findings;
66487
+ return hasUseServerDirectiveInContent(file.content, file.relativePath) ? [] : findings;
66488
+ }
64110
66489
  });
64111
66490
  //#endregion
64112
66491
  //#region src/plugin/rules/security-scan/utils/is-supabase-migration-path.ts