oxlint-plugin-react-doctor 0.7.9-dev.d7d38d7 → 0.7.9-dev.db5fe10
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.
- package/dist/index.js +2627 -453
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -579,6 +579,22 @@ const EXTERNAL_SYNC_OBSERVER_CONSTRUCTORS = new Set([
|
|
|
579
579
|
"ResizeObserver",
|
|
580
580
|
"PerformanceObserver"
|
|
581
581
|
]);
|
|
582
|
+
const EXTERNAL_SYNC_DOM_MEMBER_METHOD_NAMES = new Set([
|
|
583
|
+
"blur",
|
|
584
|
+
"focus",
|
|
585
|
+
"getBoundingClientRect",
|
|
586
|
+
"getClientRects",
|
|
587
|
+
"measure",
|
|
588
|
+
"measureInWindow",
|
|
589
|
+
"measureLayout",
|
|
590
|
+
"scroll",
|
|
591
|
+
"scrollBy",
|
|
592
|
+
"scrollIntoView",
|
|
593
|
+
"scrollTo",
|
|
594
|
+
"select",
|
|
595
|
+
"setRangeText",
|
|
596
|
+
"setSelectionRange"
|
|
597
|
+
]);
|
|
582
598
|
const STORAGE_OBJECTS = new Set(["localStorage", "sessionStorage"]);
|
|
583
599
|
//#endregion
|
|
584
600
|
//#region src/plugin/constants/react.ts
|
|
@@ -1749,7 +1765,7 @@ const isImportedFromModule = (contextNode, localIdentifierName, moduleSource) =>
|
|
|
1749
1765
|
if (!info) return false;
|
|
1750
1766
|
return info.source === moduleSource;
|
|
1751
1767
|
};
|
|
1752
|
-
const isNamespaceImportFromModule = (contextNode, localIdentifierName, moduleSource) => {
|
|
1768
|
+
const isNamespaceImportFromModule$1 = (contextNode, localIdentifierName, moduleSource) => {
|
|
1753
1769
|
const lookup = getImportLookup(contextNode);
|
|
1754
1770
|
if (!lookup) return false;
|
|
1755
1771
|
const info = lookup.get(localIdentifierName);
|
|
@@ -1835,7 +1851,7 @@ const GENERATED_IMAGE_RENDERER_MODULES = [
|
|
|
1835
1851
|
"satori"
|
|
1836
1852
|
];
|
|
1837
1853
|
const IMAGE_RESPONSE_MODULES = new Set(["next/og", "@vercel/og"]);
|
|
1838
|
-
const getImportDeclaration
|
|
1854
|
+
const getImportDeclaration = (node) => {
|
|
1839
1855
|
let current = node.parent;
|
|
1840
1856
|
while (current) {
|
|
1841
1857
|
if (isNodeOfType(current, "ImportDeclaration")) return current;
|
|
@@ -1849,7 +1865,7 @@ const isNamedImport = (symbol, importedName, moduleSources) => {
|
|
|
1849
1865
|
if (symbol.kind !== "import") return false;
|
|
1850
1866
|
const declaration = symbol.declarationNode;
|
|
1851
1867
|
if (!isNodeOfType(declaration, "ImportSpecifier")) return false;
|
|
1852
|
-
const importDeclaration = getImportDeclaration
|
|
1868
|
+
const importDeclaration = getImportDeclaration(declaration);
|
|
1853
1869
|
const source = importDeclaration ? getImportSource(importDeclaration) : null;
|
|
1854
1870
|
if (!source || !moduleSources.has(source)) return false;
|
|
1855
1871
|
const imported = declaration.imported;
|
|
@@ -1858,7 +1874,7 @@ const isNamedImport = (symbol, importedName, moduleSources) => {
|
|
|
1858
1874
|
const isSatoriImport = (symbol) => {
|
|
1859
1875
|
if (symbol.kind !== "import") return false;
|
|
1860
1876
|
const declaration = symbol.declarationNode;
|
|
1861
|
-
const importDeclaration = getImportDeclaration
|
|
1877
|
+
const importDeclaration = getImportDeclaration(declaration);
|
|
1862
1878
|
if (!importDeclaration || getImportSource(importDeclaration) !== "satori") return false;
|
|
1863
1879
|
if (isNodeOfType(declaration, "ImportDefaultSpecifier")) return true;
|
|
1864
1880
|
if (!isNodeOfType(declaration, "ImportSpecifier")) return false;
|
|
@@ -1879,7 +1895,7 @@ const isGeneratedImageRendererCall = (node, scopes) => {
|
|
|
1879
1895
|
if (!symbol || symbol.kind !== "import") return false;
|
|
1880
1896
|
const declaration = symbol.declarationNode;
|
|
1881
1897
|
if (!isNodeOfType(declaration, "ImportNamespaceSpecifier")) return false;
|
|
1882
|
-
const importDeclaration = getImportDeclaration
|
|
1898
|
+
const importDeclaration = getImportDeclaration(declaration);
|
|
1883
1899
|
const source = importDeclaration ? getImportSource(importDeclaration) : null;
|
|
1884
1900
|
return Boolean(source && IMAGE_RESPONSE_MODULES.has(source));
|
|
1885
1901
|
};
|
|
@@ -2099,6 +2115,386 @@ const isGeneratedImageRenderContext = (context, node) => {
|
|
|
2099
2115
|
return false;
|
|
2100
2116
|
};
|
|
2101
2117
|
//#endregion
|
|
2118
|
+
//#region src/plugin/utils/find-enclosing-function.ts
|
|
2119
|
+
const findEnclosingFunction$1 = (node) => {
|
|
2120
|
+
let cursor = node.parent;
|
|
2121
|
+
while (cursor) {
|
|
2122
|
+
if (isFunctionLike$1(cursor)) return cursor;
|
|
2123
|
+
cursor = cursor.parent ?? null;
|
|
2124
|
+
}
|
|
2125
|
+
return null;
|
|
2126
|
+
};
|
|
2127
|
+
//#endregion
|
|
2128
|
+
//#region src/plugin/utils/find-transparent-expression-root.ts
|
|
2129
|
+
const findTransparentExpressionRoot = (node) => {
|
|
2130
|
+
let current = node;
|
|
2131
|
+
while (current.parent && TRANSPARENT_EXPRESSION_WRAPPER_TYPES.has(current.parent.type)) current = current.parent;
|
|
2132
|
+
return current;
|
|
2133
|
+
};
|
|
2134
|
+
//#endregion
|
|
2135
|
+
//#region src/plugin/constants/js.ts
|
|
2136
|
+
const LOOP_TYPES = [
|
|
2137
|
+
"ForStatement",
|
|
2138
|
+
"ForInStatement",
|
|
2139
|
+
"ForOfStatement",
|
|
2140
|
+
"WhileStatement",
|
|
2141
|
+
"DoWhileStatement"
|
|
2142
|
+
];
|
|
2143
|
+
const FUNCTION_LIKE_TYPES = new Set([
|
|
2144
|
+
"FunctionDeclaration",
|
|
2145
|
+
"FunctionExpression",
|
|
2146
|
+
"ArrowFunctionExpression"
|
|
2147
|
+
]);
|
|
2148
|
+
const BUILTIN_GLOBAL_NAMESPACE_NAMES = new Set([
|
|
2149
|
+
"Math",
|
|
2150
|
+
"Date",
|
|
2151
|
+
"JSON",
|
|
2152
|
+
"Object",
|
|
2153
|
+
"Array",
|
|
2154
|
+
"Number",
|
|
2155
|
+
"String",
|
|
2156
|
+
"Boolean",
|
|
2157
|
+
"RegExp",
|
|
2158
|
+
"Symbol",
|
|
2159
|
+
"BigInt",
|
|
2160
|
+
"Reflect"
|
|
2161
|
+
]);
|
|
2162
|
+
const MUTATING_ARRAY_METHODS = new Set([
|
|
2163
|
+
"push",
|
|
2164
|
+
"pop",
|
|
2165
|
+
"shift",
|
|
2166
|
+
"unshift",
|
|
2167
|
+
"splice",
|
|
2168
|
+
"sort",
|
|
2169
|
+
"reverse",
|
|
2170
|
+
"fill",
|
|
2171
|
+
"copyWithin"
|
|
2172
|
+
]);
|
|
2173
|
+
const MUTATING_COLLECTION_METHODS = new Set([
|
|
2174
|
+
"add",
|
|
2175
|
+
"clear",
|
|
2176
|
+
"delete",
|
|
2177
|
+
"set"
|
|
2178
|
+
]);
|
|
2179
|
+
const CHAINABLE_ITERATION_METHODS = new Set([
|
|
2180
|
+
"map",
|
|
2181
|
+
"filter",
|
|
2182
|
+
"forEach",
|
|
2183
|
+
"flatMap"
|
|
2184
|
+
]);
|
|
2185
|
+
const ITERATOR_PRODUCING_METHOD_NAMES = new Set([
|
|
2186
|
+
"values",
|
|
2187
|
+
"keys",
|
|
2188
|
+
"entries"
|
|
2189
|
+
]);
|
|
2190
|
+
const BROWSER_TEST_FILE_PATTERN = /\.browser\.[cm]?[jt]sx?$/;
|
|
2191
|
+
const TEST_LIBRARY_IMPORT_SOURCES = new Set([
|
|
2192
|
+
"vitest",
|
|
2193
|
+
"jest",
|
|
2194
|
+
"mocha",
|
|
2195
|
+
"chai",
|
|
2196
|
+
"sinon",
|
|
2197
|
+
"expect",
|
|
2198
|
+
"ava",
|
|
2199
|
+
"uvu",
|
|
2200
|
+
"node:test",
|
|
2201
|
+
"bun:test",
|
|
2202
|
+
"@testing-library/react",
|
|
2203
|
+
"@testing-library/react-native",
|
|
2204
|
+
"@testing-library/react-hooks",
|
|
2205
|
+
"@testing-library/dom",
|
|
2206
|
+
"@testing-library/user-event",
|
|
2207
|
+
"@testing-library/jest-dom",
|
|
2208
|
+
"@testing-library/vue",
|
|
2209
|
+
"@testing-library/svelte",
|
|
2210
|
+
"@testing-library/preact",
|
|
2211
|
+
"@testing-library/cypress",
|
|
2212
|
+
"playwright",
|
|
2213
|
+
"playwright-core",
|
|
2214
|
+
"@playwright/test",
|
|
2215
|
+
"@playwright/experimental-ct-react",
|
|
2216
|
+
"@playwright/experimental-ct-react17",
|
|
2217
|
+
"cypress",
|
|
2218
|
+
"@cypress/react",
|
|
2219
|
+
"@cypress/react18",
|
|
2220
|
+
"@storybook/test",
|
|
2221
|
+
"@storybook/test-runner",
|
|
2222
|
+
"@storybook/testing-library",
|
|
2223
|
+
"@storybook/jest",
|
|
2224
|
+
"puppeteer",
|
|
2225
|
+
"puppeteer-core",
|
|
2226
|
+
"webdriverio",
|
|
2227
|
+
"@wdio/globals",
|
|
2228
|
+
"@nuxt/test-utils"
|
|
2229
|
+
]);
|
|
2230
|
+
const TEST_LIBRARY_IMPORT_SOURCE_PREFIXES = [
|
|
2231
|
+
"vitest/",
|
|
2232
|
+
"@vitest/",
|
|
2233
|
+
"@jest/",
|
|
2234
|
+
"@testing-library/",
|
|
2235
|
+
"@playwright/",
|
|
2236
|
+
"@storybook/test/",
|
|
2237
|
+
"@storybook/test-runner/",
|
|
2238
|
+
"@storybook/testing-library/",
|
|
2239
|
+
"@cypress/",
|
|
2240
|
+
"@nuxt/test-utils/"
|
|
2241
|
+
];
|
|
2242
|
+
const ORDERED_UI_FLOW_CALLEE_NAMES = new Set([
|
|
2243
|
+
"render",
|
|
2244
|
+
"rerender",
|
|
2245
|
+
"renderHook",
|
|
2246
|
+
"renderToString",
|
|
2247
|
+
"renderToStaticMarkup",
|
|
2248
|
+
"act",
|
|
2249
|
+
"click",
|
|
2250
|
+
"dblClick",
|
|
2251
|
+
"dblclick",
|
|
2252
|
+
"tripleClick",
|
|
2253
|
+
"tap",
|
|
2254
|
+
"press",
|
|
2255
|
+
"longPress",
|
|
2256
|
+
"type",
|
|
2257
|
+
"clear",
|
|
2258
|
+
"fill",
|
|
2259
|
+
"focus",
|
|
2260
|
+
"blur",
|
|
2261
|
+
"hover",
|
|
2262
|
+
"unhover",
|
|
2263
|
+
"check",
|
|
2264
|
+
"uncheck",
|
|
2265
|
+
"selectOption",
|
|
2266
|
+
"selectOptions",
|
|
2267
|
+
"setChecked",
|
|
2268
|
+
"setInputFiles",
|
|
2269
|
+
"scrollIntoViewIfNeeded",
|
|
2270
|
+
"dragTo",
|
|
2271
|
+
"dragAndDrop",
|
|
2272
|
+
"drop",
|
|
2273
|
+
"evaluate",
|
|
2274
|
+
"evaluateHandle",
|
|
2275
|
+
"waitFor",
|
|
2276
|
+
"waitForLoadState",
|
|
2277
|
+
"waitForSelector",
|
|
2278
|
+
"waitForURL",
|
|
2279
|
+
"waitForResponse",
|
|
2280
|
+
"waitForRequest",
|
|
2281
|
+
"waitForEvent",
|
|
2282
|
+
"waitForFunction",
|
|
2283
|
+
"waitForElementToBeRemoved",
|
|
2284
|
+
"goto",
|
|
2285
|
+
"goBack",
|
|
2286
|
+
"goForward",
|
|
2287
|
+
"reload",
|
|
2288
|
+
"screenshot",
|
|
2289
|
+
"snapshot",
|
|
2290
|
+
"toMatchSnapshot",
|
|
2291
|
+
"toMatchInlineSnapshot",
|
|
2292
|
+
"expect",
|
|
2293
|
+
"expectTypeOf",
|
|
2294
|
+
"step",
|
|
2295
|
+
"describe",
|
|
2296
|
+
"test",
|
|
2297
|
+
"it",
|
|
2298
|
+
"beforeAll",
|
|
2299
|
+
"beforeEach",
|
|
2300
|
+
"afterAll",
|
|
2301
|
+
"afterEach",
|
|
2302
|
+
"play",
|
|
2303
|
+
"userEvent",
|
|
2304
|
+
"screen",
|
|
2305
|
+
"within"
|
|
2306
|
+
]);
|
|
2307
|
+
const ORDERED_UI_FLOW_CALLEE_PREFIXES = ["findBy", "findAllBy"];
|
|
2308
|
+
const INTENTIONAL_SEQUENCING_CALLEE_NAMES = new Set([
|
|
2309
|
+
"sleep",
|
|
2310
|
+
"delay",
|
|
2311
|
+
"wait",
|
|
2312
|
+
"pause",
|
|
2313
|
+
"throttle",
|
|
2314
|
+
"debounce",
|
|
2315
|
+
"tick",
|
|
2316
|
+
"nextTick",
|
|
2317
|
+
"advanceTimersByTime",
|
|
2318
|
+
"advanceTimersByTimeAsync",
|
|
2319
|
+
"runAllTimers",
|
|
2320
|
+
"runAllTimersAsync",
|
|
2321
|
+
"runOnlyPendingTimers",
|
|
2322
|
+
"runOnlyPendingTimersAsync",
|
|
2323
|
+
"setTimeout",
|
|
2324
|
+
"setInterval",
|
|
2325
|
+
"setImmediate",
|
|
2326
|
+
"queueMicrotask",
|
|
2327
|
+
"requestAnimationFrame",
|
|
2328
|
+
"requestIdleCallback",
|
|
2329
|
+
"animate",
|
|
2330
|
+
"transition",
|
|
2331
|
+
"spring",
|
|
2332
|
+
"tween",
|
|
2333
|
+
"stagger",
|
|
2334
|
+
"sequence",
|
|
2335
|
+
"timeline",
|
|
2336
|
+
"scrub",
|
|
2337
|
+
"query",
|
|
2338
|
+
"execute",
|
|
2339
|
+
"exec",
|
|
2340
|
+
"raw",
|
|
2341
|
+
"transaction",
|
|
2342
|
+
"$transaction",
|
|
2343
|
+
"$executeRaw",
|
|
2344
|
+
"$queryRaw",
|
|
2345
|
+
"$executeRawUnsafe",
|
|
2346
|
+
"$queryRawUnsafe",
|
|
2347
|
+
"begin",
|
|
2348
|
+
"commit",
|
|
2349
|
+
"rollback",
|
|
2350
|
+
"savepoint",
|
|
2351
|
+
"lock",
|
|
2352
|
+
"unlock",
|
|
2353
|
+
"spawn",
|
|
2354
|
+
"spawnSync",
|
|
2355
|
+
"execSync",
|
|
2356
|
+
"execFile",
|
|
2357
|
+
"execFileSync",
|
|
2358
|
+
"fork",
|
|
2359
|
+
"$",
|
|
2360
|
+
"sh",
|
|
2361
|
+
"mkdir",
|
|
2362
|
+
"rmdir",
|
|
2363
|
+
"rename",
|
|
2364
|
+
"rm",
|
|
2365
|
+
"unlink",
|
|
2366
|
+
"writeFile",
|
|
2367
|
+
"appendFile",
|
|
2368
|
+
"copyFile",
|
|
2369
|
+
"navigate",
|
|
2370
|
+
"goto",
|
|
2371
|
+
"waitForNavigation",
|
|
2372
|
+
"waitForURL",
|
|
2373
|
+
"waitForLoadState",
|
|
2374
|
+
"waitForResponse",
|
|
2375
|
+
"waitForRequest",
|
|
2376
|
+
"waitForSelector",
|
|
2377
|
+
"waitForFunction",
|
|
2378
|
+
"waitForEvent"
|
|
2379
|
+
]);
|
|
2380
|
+
//#endregion
|
|
2381
|
+
//#region src/plugin/utils/is-test-library-import-source.ts
|
|
2382
|
+
const isTestLibraryImportSource = (source) => {
|
|
2383
|
+
if (typeof source !== "string" || source.length === 0) return false;
|
|
2384
|
+
if (TEST_LIBRARY_IMPORT_SOURCES.has(source)) return true;
|
|
2385
|
+
return TEST_LIBRARY_IMPORT_SOURCE_PREFIXES.some((prefix) => source.startsWith(prefix));
|
|
2386
|
+
};
|
|
2387
|
+
//#endregion
|
|
2388
|
+
//#region src/plugin/utils/is-local-test-scaffold-jsx.ts
|
|
2389
|
+
const TEST_CALLBACK_EXPORT_NAMES = new Set(["it", "test"]);
|
|
2390
|
+
const TEST_CALLBACK_MEMBER_NAMES = new Set([
|
|
2391
|
+
"concurrent",
|
|
2392
|
+
"only",
|
|
2393
|
+
"skip"
|
|
2394
|
+
]);
|
|
2395
|
+
const TEST_CALLBACK_TABLE_MEMBER_NAME = "each";
|
|
2396
|
+
const TEST_MOCK_METHOD_NAMES = new Set([
|
|
2397
|
+
"doMock",
|
|
2398
|
+
"mock",
|
|
2399
|
+
"unstable_mockModule"
|
|
2400
|
+
]);
|
|
2401
|
+
const TEST_RUNTIME_EXPORT_NAMES = new Set(["jest", "vi"]);
|
|
2402
|
+
const TEST_RUNTIME_MODULE_SOURCES = new Set([
|
|
2403
|
+
"@jest/globals",
|
|
2404
|
+
"bun:test",
|
|
2405
|
+
"node:test",
|
|
2406
|
+
"vitest"
|
|
2407
|
+
]);
|
|
2408
|
+
const REACT_MODULE_SOURCES = new Set([
|
|
2409
|
+
"react",
|
|
2410
|
+
"react/jsx-dev-runtime",
|
|
2411
|
+
"react/jsx-runtime"
|
|
2412
|
+
]);
|
|
2413
|
+
const hasUnitTestFilename = (rawFilename) => {
|
|
2414
|
+
if (!rawFilename) return false;
|
|
2415
|
+
const filename = `/${rawFilename.replaceAll("\\", "/")}`;
|
|
2416
|
+
const basename = filename.slice(filename.lastIndexOf("/") + 1);
|
|
2417
|
+
return basename.includes(".test.") || basename.includes(".spec.") || filename.includes("/__tests__/") || filename.includes("/__test__/") || filename.includes("/__mocks__/");
|
|
2418
|
+
};
|
|
2419
|
+
const isExactImportedBinding = (identifier, expectedExportNames, context) => {
|
|
2420
|
+
if (context.scopes.referenceFor(identifier)?.resolvedSymbol?.kind !== "import") return false;
|
|
2421
|
+
const importBinding = getImportBindingForName(identifier, identifier.name);
|
|
2422
|
+
return Boolean(importBinding && TEST_RUNTIME_MODULE_SOURCES.has(importBinding.source) && importBinding.exportedName && expectedExportNames.has(importBinding.exportedName));
|
|
2423
|
+
};
|
|
2424
|
+
const isRecognizedTestGlobal = (identifier, expectedNames, context) => hasUnitTestFilename(context.filename) && expectedNames.has(identifier.name) && context.scopes.isGlobalReference(identifier);
|
|
2425
|
+
const isRecognizedTestBinding = (identifier, expectedNames, context) => isExactImportedBinding(identifier, expectedNames, context) || isRecognizedTestGlobal(identifier, expectedNames, context);
|
|
2426
|
+
const getTestCallbackBaseIdentifier = (callee) => {
|
|
2427
|
+
const unwrappedCallee = stripParenExpression(callee);
|
|
2428
|
+
if (isNodeOfType(unwrappedCallee, "Identifier")) return unwrappedCallee;
|
|
2429
|
+
if (isNodeOfType(unwrappedCallee, "MemberExpression")) {
|
|
2430
|
+
const memberName = getStaticPropertyName(unwrappedCallee);
|
|
2431
|
+
if (!memberName || !TEST_CALLBACK_MEMBER_NAMES.has(memberName)) return null;
|
|
2432
|
+
return getTestCallbackBaseIdentifier(unwrappedCallee.object);
|
|
2433
|
+
}
|
|
2434
|
+
const tableBuilderCallee = isNodeOfType(unwrappedCallee, "CallExpression") ? stripParenExpression(unwrappedCallee.callee) : isNodeOfType(unwrappedCallee, "TaggedTemplateExpression") ? stripParenExpression(unwrappedCallee.tag) : null;
|
|
2435
|
+
if (!isNodeOfType(tableBuilderCallee, "MemberExpression")) return null;
|
|
2436
|
+
if (getStaticPropertyName(tableBuilderCallee) !== TEST_CALLBACK_TABLE_MEMBER_NAME) return null;
|
|
2437
|
+
return getTestCallbackBaseIdentifier(tableBuilderCallee.object);
|
|
2438
|
+
};
|
|
2439
|
+
const isDirectTestCallback = (functionNode, context) => {
|
|
2440
|
+
const callbackRoot = findTransparentExpressionRoot(functionNode);
|
|
2441
|
+
const callExpression = callbackRoot.parent;
|
|
2442
|
+
if (!callExpression || !isNodeOfType(callExpression, "CallExpression")) return false;
|
|
2443
|
+
if (!callExpression.arguments.some((argument) => argument === callbackRoot)) return false;
|
|
2444
|
+
const baseIdentifier = getTestCallbackBaseIdentifier(callExpression.callee);
|
|
2445
|
+
return Boolean(baseIdentifier && isRecognizedTestBinding(baseIdentifier, TEST_CALLBACK_EXPORT_NAMES, context));
|
|
2446
|
+
};
|
|
2447
|
+
const isRecognizedMockFactoryCall = (callExpression, factoryRoot, context) => {
|
|
2448
|
+
if (callExpression.arguments[1] !== factoryRoot) return false;
|
|
2449
|
+
const moduleSpecifier = callExpression.arguments[0];
|
|
2450
|
+
if (!moduleSpecifier || !isNodeOfType(moduleSpecifier, "Literal")) return false;
|
|
2451
|
+
if (typeof moduleSpecifier.value !== "string") return false;
|
|
2452
|
+
const callee = stripParenExpression(callExpression.callee);
|
|
2453
|
+
if (!isNodeOfType(callee, "MemberExpression")) return false;
|
|
2454
|
+
const methodName = getStaticPropertyName(callee);
|
|
2455
|
+
if (!methodName || !TEST_MOCK_METHOD_NAMES.has(methodName)) return false;
|
|
2456
|
+
const receiver = stripParenExpression(callee.object);
|
|
2457
|
+
return isNodeOfType(receiver, "Identifier") && isRecognizedTestBinding(receiver, TEST_RUNTIME_EXPORT_NAMES, context);
|
|
2458
|
+
};
|
|
2459
|
+
const isInsideRecognizedMockFactory = (node, context) => {
|
|
2460
|
+
let current = node.parent;
|
|
2461
|
+
while (current) {
|
|
2462
|
+
if (isFunctionLike$1(current)) {
|
|
2463
|
+
const factoryRoot = findTransparentExpressionRoot(current);
|
|
2464
|
+
const callExpression = factoryRoot.parent;
|
|
2465
|
+
if (callExpression && isNodeOfType(callExpression, "CallExpression") && isRecognizedMockFactoryCall(callExpression, factoryRoot, context)) return true;
|
|
2466
|
+
}
|
|
2467
|
+
current = current.parent;
|
|
2468
|
+
}
|
|
2469
|
+
return false;
|
|
2470
|
+
};
|
|
2471
|
+
const hasImportedProductComponentAttributeAncestor = (node, enclosingFunction, context) => {
|
|
2472
|
+
let current = node.parent;
|
|
2473
|
+
let attributeAncestor = null;
|
|
2474
|
+
if (current && isNodeOfType(current, "JSXElement") && current.openingElement === node) current = current.parent;
|
|
2475
|
+
while (current && current !== enclosingFunction) {
|
|
2476
|
+
if (isFunctionLike$1(current)) return false;
|
|
2477
|
+
if (isNodeOfType(current, "JSXAttribute")) attributeAncestor = current;
|
|
2478
|
+
if (isNodeOfType(current, "JSXElement")) {
|
|
2479
|
+
const componentName = current.openingElement.name;
|
|
2480
|
+
if (isNodeOfType(componentName, "JSXIdentifier")) {
|
|
2481
|
+
const reference = context.scopes.referenceFor(componentName);
|
|
2482
|
+
const importBinding = getImportBindingForName(componentName, componentName.name);
|
|
2483
|
+
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;
|
|
2484
|
+
}
|
|
2485
|
+
attributeAncestor = null;
|
|
2486
|
+
}
|
|
2487
|
+
current = current.parent;
|
|
2488
|
+
}
|
|
2489
|
+
return false;
|
|
2490
|
+
};
|
|
2491
|
+
const isLocalTestScaffoldJsx = (node, context) => {
|
|
2492
|
+
if (isInsideRecognizedMockFactory(node, context)) return true;
|
|
2493
|
+
const enclosingFunction = findEnclosingFunction$1(node);
|
|
2494
|
+
if (!enclosingFunction || !isDirectTestCallback(enclosingFunction, context)) return false;
|
|
2495
|
+
return hasImportedProductComponentAttributeAncestor(node, enclosingFunction, context);
|
|
2496
|
+
};
|
|
2497
|
+
//#endregion
|
|
2102
2498
|
//#region src/plugin/utils/object-has-accessible-child.ts
|
|
2103
2499
|
const objectHasAccessibleChild = (jsxElement, settings) => {
|
|
2104
2500
|
for (const child of jsxElement.children) {
|
|
@@ -2267,6 +2663,7 @@ const altText = defineRule({
|
|
|
2267
2663
|
const inputImageAliases = new Set(settings["input[type=\"image\"]"] ?? []);
|
|
2268
2664
|
const fileHasJsxA11ySettings = hasJsxA11ySettings(context.settings);
|
|
2269
2665
|
return { JSXOpeningElement(node) {
|
|
2666
|
+
if (isLocalTestScaffoldJsx(node, context)) return;
|
|
2270
2667
|
if (!fileHasJsxA11ySettings && isNodeOfType(node.name, "JSXIdentifier")) {
|
|
2271
2668
|
const rawName = node.name.name;
|
|
2272
2669
|
if (rawName !== "img" && rawName !== "object" && rawName !== "area" && rawName.toLowerCase() !== "input" && !imgAliases.has(rawName) && !objectAliases.has(rawName) && !areaAliases.has(rawName) && !inputImageAliases.has(rawName)) return;
|
|
@@ -4135,252 +4532,6 @@ const artifactSecretLeak = defineRule({
|
|
|
4135
4532
|
scan: (file) => scanArtifactLeak(file, (content) => SECRET_VALUE_PATTERNS.find((pattern) => pattern.test(content)), "A browser-delivered artifact contains a secret-looking credential value.")
|
|
4136
4533
|
});
|
|
4137
4534
|
//#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
4535
|
//#region src/plugin/constants/ts-type-position-keys.ts
|
|
4385
4536
|
const TYPE_POSITION_CHILD_KEYS = new Set([
|
|
4386
4537
|
"implements",
|
|
@@ -4506,13 +4657,6 @@ const containsDirectAwait = (node) => {
|
|
|
4506
4657
|
return foundAwait;
|
|
4507
4658
|
};
|
|
4508
4659
|
//#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
4660
|
//#region src/plugin/utils/get-static-property-key-name.ts
|
|
4517
4661
|
const getStaticPropertyKeyName = (node, options = {}) => {
|
|
4518
4662
|
if (!isNodeOfType(node, "Property") && !isNodeOfType(node, "MethodDefinition") && !isNodeOfType(node, "MemberExpression")) return null;
|
|
@@ -4772,16 +4916,6 @@ const hasStaticPropertyWriteBefore = (identifier, propertyName, referenceNode, s
|
|
|
4772
4916
|
return getEquivalentSymbols(identifier, scopes).some((symbol) => symbolHasStaticPropertyWriteBefore(symbol, propertyName, referenceNode, scopes));
|
|
4773
4917
|
};
|
|
4774
4918
|
//#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
4919
|
//#region src/plugin/utils/has-symbol-write-before.ts
|
|
4786
4920
|
const hasSymbolWriteBefore = (symbol, referenceNode, scopes) => symbol.references.some((reference) => {
|
|
4787
4921
|
if (reference.flag === "read") return false;
|
|
@@ -6283,13 +6417,6 @@ const getCalleeIdentifierTrail = (call) => {
|
|
|
6283
6417
|
return trail;
|
|
6284
6418
|
};
|
|
6285
6419
|
//#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
6420
|
//#region src/plugin/rules/js-performance/async-parallel.ts
|
|
6294
6421
|
const getAwaitedCall = (statement) => {
|
|
6295
6422
|
if (isNodeOfType(statement, "VariableDeclaration")) {
|
|
@@ -8251,7 +8378,27 @@ const collectFunctionReturnStatements = (functionNode) => {
|
|
|
8251
8378
|
//#region src/plugin/utils/statement-always-exits.ts
|
|
8252
8379
|
const statementAlwaysExits = (statement) => {
|
|
8253
8380
|
if (isNodeOfType(statement, "ReturnStatement") || isNodeOfType(statement, "ThrowStatement")) return true;
|
|
8254
|
-
if (isNodeOfType(statement, "IfStatement"))
|
|
8381
|
+
if (isNodeOfType(statement, "IfStatement")) {
|
|
8382
|
+
if (isNodeOfType(statement.test, "Literal")) {
|
|
8383
|
+
const reachableBranch = statement.test.value ? statement.consequent : statement.alternate;
|
|
8384
|
+
return reachableBranch ? statementAlwaysExits(reachableBranch) : false;
|
|
8385
|
+
}
|
|
8386
|
+
return Boolean(statement.alternate && statementAlwaysExits(statement.consequent) && statementAlwaysExits(statement.alternate));
|
|
8387
|
+
}
|
|
8388
|
+
if (isNodeOfType(statement, "TryStatement")) {
|
|
8389
|
+
if (statement.finalizer && statementAlwaysExits(statement.finalizer)) return true;
|
|
8390
|
+
if (!statementAlwaysExits(statement.block)) return false;
|
|
8391
|
+
return statement.handler ? statementAlwaysExits(statement.handler.body) : true;
|
|
8392
|
+
}
|
|
8393
|
+
if (isNodeOfType(statement, "DoWhileStatement")) return statementAlwaysExits(statement.body);
|
|
8394
|
+
if (isNodeOfType(statement, "WhileStatement")) {
|
|
8395
|
+
const whileStatementTest = statement.test;
|
|
8396
|
+
return Boolean(isNodeOfType(whileStatementTest, "Literal") && whileStatementTest.value && statementAlwaysExits(statement.body));
|
|
8397
|
+
}
|
|
8398
|
+
if (isNodeOfType(statement, "ForStatement")) {
|
|
8399
|
+
const forStatementTest = statement.test;
|
|
8400
|
+
return Boolean((!forStatementTest || isNodeOfType(forStatementTest, "Literal") && forStatementTest.value) && statementAlwaysExits(statement.body));
|
|
8401
|
+
}
|
|
8255
8402
|
if (!isNodeOfType(statement, "BlockStatement")) return false;
|
|
8256
8403
|
return statement.body.some((childStatement) => statementAlwaysExits(childStatement));
|
|
8257
8404
|
};
|
|
@@ -10370,10 +10517,10 @@ const controlHasAssociatedLabel = defineRule({
|
|
|
10370
10517
|
if (isTestlikeFile) return;
|
|
10371
10518
|
const opening = node.openingElement;
|
|
10372
10519
|
const tagName = getElementType(opening, context.settings);
|
|
10373
|
-
if (tagName
|
|
10520
|
+
if (rendersLabelElement(tagName, opening) && hasAccessibleLabelText(node, checkContext) && !isInsideJsxAttribute(node)) {
|
|
10374
10521
|
const htmlForAttribute = hasJsxPropIgnoreCase(opening.attributes, HTML_FOR_ATTRIBUTE);
|
|
10375
10522
|
for (const htmlForKey of getAttributeMatchKeys(htmlForAttribute)) labelHtmlForKeys.add(htmlForKey);
|
|
10376
|
-
collectLabelEmbeddedNames(node, 1, checkContext, labelEmbeddedNames);
|
|
10523
|
+
if (tagName === LABEL_ELEMENT) collectLabelEmbeddedNames(node, 1, checkContext, labelEmbeddedNames);
|
|
10377
10524
|
}
|
|
10378
10525
|
if (DEFAULT_IGNORE_ELEMENTS.includes(tagName)) return;
|
|
10379
10526
|
if (settings.ignoreElements.includes(tagName)) return;
|
|
@@ -10459,6 +10606,1048 @@ const findMatchingBracket = (content, openIndex) => {
|
|
|
10459
10606
|
return -1;
|
|
10460
10607
|
};
|
|
10461
10608
|
//#endregion
|
|
10609
|
+
//#region src/plugin/utils/get-node-end-index.ts
|
|
10610
|
+
const getNodeEndIndex = (node) => "end" in node && typeof node.end === "number" ? node.end : -1;
|
|
10611
|
+
//#endregion
|
|
10612
|
+
//#region src/plugin/utils/get-node-start-index.ts
|
|
10613
|
+
const getNodeStartIndex = (node) => "start" in node && typeof node.start === "number" ? node.start : -1;
|
|
10614
|
+
//#endregion
|
|
10615
|
+
//#region src/plugin/utils/get-import-declaration-for-symbol.ts
|
|
10616
|
+
const getImportDeclarationForSymbol = (symbol) => {
|
|
10617
|
+
if (symbol.kind !== "import") return null;
|
|
10618
|
+
const importDeclaration = symbol.declarationNode.parent;
|
|
10619
|
+
return isNodeOfType(importDeclaration, "ImportDeclaration") ? importDeclaration : null;
|
|
10620
|
+
};
|
|
10621
|
+
//#endregion
|
|
10622
|
+
//#region src/plugin/constants/mutation-methods.ts
|
|
10623
|
+
const OBJECT_PROPERTY_MUTATION_METHOD_NAMES = new Set([
|
|
10624
|
+
"assign",
|
|
10625
|
+
"defineProperties",
|
|
10626
|
+
"defineProperty"
|
|
10627
|
+
]);
|
|
10628
|
+
const REFLECT_PROPERTY_MUTATION_METHOD_NAMES = new Set(["defineProperty", "set"]);
|
|
10629
|
+
//#endregion
|
|
10630
|
+
//#region src/plugin/rules/security-scan/utils/get-symbol-mutation-inspector.ts
|
|
10631
|
+
const inspectorCache = /* @__PURE__ */ new WeakMap();
|
|
10632
|
+
const getOutermostTarget = (node) => {
|
|
10633
|
+
let current = findTransparentExpressionRoot(node);
|
|
10634
|
+
while (current.parent) {
|
|
10635
|
+
const parent = current.parent;
|
|
10636
|
+
if (!isNodeOfType(parent, "MemberExpression") || parent.object !== current) break;
|
|
10637
|
+
current = findTransparentExpressionRoot(parent);
|
|
10638
|
+
}
|
|
10639
|
+
return current;
|
|
10640
|
+
};
|
|
10641
|
+
const getExecutionOwner = (node) => {
|
|
10642
|
+
let current = node;
|
|
10643
|
+
while (current) {
|
|
10644
|
+
if (isFunctionLike$1(current) || isNodeOfType(current, "Program")) return current;
|
|
10645
|
+
current = current.parent;
|
|
10646
|
+
}
|
|
10647
|
+
return node;
|
|
10648
|
+
};
|
|
10649
|
+
const isAbruptCompletionStatement = (node, includesContinue) => {
|
|
10650
|
+
if (isNodeOfType(node, "ReturnStatement") || isNodeOfType(node, "ThrowStatement") || isNodeOfType(node, "BreakStatement") || includesContinue && isNodeOfType(node, "ContinueStatement")) return true;
|
|
10651
|
+
if (isNodeOfType(node, "BlockStatement")) return node.body.some((statement) => isAbruptCompletionStatement(statement, includesContinue));
|
|
10652
|
+
if (!isNodeOfType(node, "IfStatement")) return false;
|
|
10653
|
+
if (isNodeOfType(node.test, "Literal")) {
|
|
10654
|
+
const reachableBranch = node.test.value ? node.consequent : node.alternate;
|
|
10655
|
+
return reachableBranch ? isAbruptCompletionStatement(reachableBranch, includesContinue) : false;
|
|
10656
|
+
}
|
|
10657
|
+
return Boolean(node.alternate && isAbruptCompletionStatement(node.consequent, includesContinue) && isAbruptCompletionStatement(node.alternate, includesContinue));
|
|
10658
|
+
};
|
|
10659
|
+
const isTerminalStatement = (node) => isAbruptCompletionStatement(node, true);
|
|
10660
|
+
const isAfterTerminalStatement = (node, statements) => {
|
|
10661
|
+
const statementIndex = statements.indexOf(node);
|
|
10662
|
+
return statementIndex > 0 && statements.slice(0, statementIndex).some(isTerminalStatement);
|
|
10663
|
+
};
|
|
10664
|
+
const isStaticallyUnreachable = (node, owner) => {
|
|
10665
|
+
let current = node;
|
|
10666
|
+
while (current.parent && current !== owner) {
|
|
10667
|
+
const parent = current.parent;
|
|
10668
|
+
if ((isNodeOfType(parent, "BlockStatement") || isNodeOfType(parent, "Program")) && isAfterTerminalStatement(current, parent.body)) return true;
|
|
10669
|
+
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;
|
|
10670
|
+
if (isNodeOfType(parent, "SwitchCase") && isAfterTerminalStatement(current, parent.consequent)) return true;
|
|
10671
|
+
if (isNodeOfType(parent, "IfStatement") && isNodeOfType(parent.test, "Literal")) {
|
|
10672
|
+
if (parent.test.value === false && parent.consequent === current) return true;
|
|
10673
|
+
if (parent.test.value === true && parent.alternate === current) return true;
|
|
10674
|
+
}
|
|
10675
|
+
if (isNodeOfType(parent, "ConditionalExpression") && isNodeOfType(parent.test, "Literal")) {
|
|
10676
|
+
if (parent.test.value === false && parent.consequent === current) return true;
|
|
10677
|
+
if (parent.test.value === true && parent.alternate === current) return true;
|
|
10678
|
+
}
|
|
10679
|
+
if (isNodeOfType(parent, "LogicalExpression") && parent.right === current && isNodeOfType(parent.left, "Literal")) {
|
|
10680
|
+
if (parent.operator === "&&" && !parent.left.value) return true;
|
|
10681
|
+
if (parent.operator === "||" && Boolean(parent.left.value)) return true;
|
|
10682
|
+
}
|
|
10683
|
+
current = parent;
|
|
10684
|
+
}
|
|
10685
|
+
return false;
|
|
10686
|
+
};
|
|
10687
|
+
const isConditionallyExecuted = (node, owner) => {
|
|
10688
|
+
let current = node;
|
|
10689
|
+
while (current.parent && current !== owner) {
|
|
10690
|
+
const parent = current.parent;
|
|
10691
|
+
if (isNodeOfType(parent, "IfStatement")) {
|
|
10692
|
+
if (!isNodeOfType(parent.test, "Literal")) return true;
|
|
10693
|
+
if (parent.test.value === true && parent.alternate === current) return true;
|
|
10694
|
+
if (parent.test.value === false && parent.consequent === current) return true;
|
|
10695
|
+
}
|
|
10696
|
+
if (isNodeOfType(parent, "ConditionalExpression")) {
|
|
10697
|
+
if (!isNodeOfType(parent.test, "Literal")) return true;
|
|
10698
|
+
if (parent.test.value === true && parent.alternate === current) return true;
|
|
10699
|
+
if (parent.test.value === false && parent.consequent === current) return true;
|
|
10700
|
+
}
|
|
10701
|
+
if (isNodeOfType(parent, "LogicalExpression") && parent.right === current) {
|
|
10702
|
+
if (!isNodeOfType(parent.left, "Literal")) return true;
|
|
10703
|
+
if (parent.operator === "&&" && !parent.left.value) return true;
|
|
10704
|
+
if (parent.operator === "||" && Boolean(parent.left.value)) return true;
|
|
10705
|
+
}
|
|
10706
|
+
if (isNodeOfType(parent, "DoWhileStatement")) {
|
|
10707
|
+
if (!(parent.body === current && isNodeOfType(parent.test, "Literal") && !parent.test.value)) return true;
|
|
10708
|
+
}
|
|
10709
|
+
if (isNodeOfType(parent, "TryStatement") && parent.block === current) return true;
|
|
10710
|
+
if (isNodeOfType(parent, "ForStatement") || isNodeOfType(parent, "ForInStatement") || isNodeOfType(parent, "ForOfStatement") || isNodeOfType(parent, "WhileStatement") || isNodeOfType(parent, "SwitchCase") || isNodeOfType(parent, "CatchClause")) return true;
|
|
10711
|
+
if ((isNodeOfType(parent, "CallExpression") || isNodeOfType(parent, "MemberExpression")) && parent.optional) return true;
|
|
10712
|
+
current = parent;
|
|
10713
|
+
}
|
|
10714
|
+
return false;
|
|
10715
|
+
};
|
|
10716
|
+
const getSymbolMutationInspector = (scopes) => {
|
|
10717
|
+
const cached = inspectorCache.get(scopes);
|
|
10718
|
+
if (cached) return cached;
|
|
10719
|
+
const isGlobalNamespaceMethod = (node, namespaceName, methodNames) => {
|
|
10720
|
+
const callee = stripParenExpression(node);
|
|
10721
|
+
if (!isNodeOfType(callee, "MemberExpression")) return false;
|
|
10722
|
+
const receiver = stripParenExpression(callee.object);
|
|
10723
|
+
return Boolean(isNodeOfType(receiver, "Identifier") && receiver.name === namespaceName && scopes.isGlobalReference(receiver) && methodNames.has(getStaticPropertyName(callee) ?? ""));
|
|
10724
|
+
};
|
|
10725
|
+
const getObjectExpressionPropertyNames = (node) => {
|
|
10726
|
+
const expression = stripParenExpression(node);
|
|
10727
|
+
if (!isNodeOfType(expression, "ObjectExpression")) return null;
|
|
10728
|
+
const propertyNames = /* @__PURE__ */ new Set();
|
|
10729
|
+
for (const property of expression.properties) {
|
|
10730
|
+
if (!isNodeOfType(property, "Property")) return null;
|
|
10731
|
+
const propertyName = getStaticPropertyKeyName(property, { allowComputedString: true });
|
|
10732
|
+
if (propertyName === null) return null;
|
|
10733
|
+
propertyNames.add(propertyName);
|
|
10734
|
+
}
|
|
10735
|
+
return propertyNames;
|
|
10736
|
+
};
|
|
10737
|
+
const getMutationPropertyNames = (node) => {
|
|
10738
|
+
const target = getOutermostTarget(node);
|
|
10739
|
+
const parent = target.parent;
|
|
10740
|
+
if (!parent) return void 0;
|
|
10741
|
+
if (isNodeOfType(parent, "AssignmentExpression") && parent.left === target || isNodeOfType(parent, "UpdateExpression") && parent.argument === target || isNodeOfType(parent, "UnaryExpression") && parent.operator === "delete") {
|
|
10742
|
+
if (!isNodeOfType(target, "MemberExpression")) return null;
|
|
10743
|
+
const propertyName = getStaticPropertyName(target);
|
|
10744
|
+
return propertyName === null ? null : new Set([propertyName]);
|
|
10745
|
+
}
|
|
10746
|
+
if (!isNodeOfType(parent, "CallExpression") || parent.arguments[0] !== target) return void 0;
|
|
10747
|
+
if (isGlobalNamespaceMethod(parent.callee, "Object", OBJECT_PROPERTY_MUTATION_METHOD_NAMES)) {
|
|
10748
|
+
const callee = stripParenExpression(parent.callee);
|
|
10749
|
+
if (!isNodeOfType(callee, "MemberExpression")) return void 0;
|
|
10750
|
+
const methodName = getStaticPropertyName(callee);
|
|
10751
|
+
if (methodName === "assign") {
|
|
10752
|
+
const assignedProperties = parent.arguments.slice(1).map(getObjectExpressionPropertyNames);
|
|
10753
|
+
if (assignedProperties.some((properties) => properties === null)) return null;
|
|
10754
|
+
return new Set(assignedProperties.flatMap((properties) => [...properties ?? []]));
|
|
10755
|
+
}
|
|
10756
|
+
if (methodName === "defineProperties") {
|
|
10757
|
+
const propertyDescriptors = parent.arguments[1];
|
|
10758
|
+
return propertyDescriptors ? getObjectExpressionPropertyNames(propertyDescriptors) : null;
|
|
10759
|
+
}
|
|
10760
|
+
const propertyKey = parent.arguments[1];
|
|
10761
|
+
return propertyKey && isNodeOfType(propertyKey, "Literal") && typeof propertyKey.value === "string" ? new Set([propertyKey.value]) : null;
|
|
10762
|
+
}
|
|
10763
|
+
if (isGlobalNamespaceMethod(parent.callee, "Reflect", REFLECT_PROPERTY_MUTATION_METHOD_NAMES)) {
|
|
10764
|
+
const propertyKey = parent.arguments[1];
|
|
10765
|
+
return propertyKey && isNodeOfType(propertyKey, "Literal") && typeof propertyKey.value === "string" ? new Set([propertyKey.value]) : null;
|
|
10766
|
+
}
|
|
10767
|
+
};
|
|
10768
|
+
const getLocalCallTarget = (call) => {
|
|
10769
|
+
const callee = stripParenExpression(call.callee);
|
|
10770
|
+
if (isFunctionLike$1(callee)) return callee;
|
|
10771
|
+
if (!isNodeOfType(callee, "Identifier")) return null;
|
|
10772
|
+
const symbol = resolveConstIdentifierAlias(callee, scopes);
|
|
10773
|
+
if (!symbol) return null;
|
|
10774
|
+
if (symbol.kind === "function" && isFunctionLike$1(symbol.declarationNode)) return symbol.declarationNode;
|
|
10775
|
+
if (symbol.kind !== "const" || !symbol.initializer) return null;
|
|
10776
|
+
const initializer = stripParenExpression(symbol.initializer);
|
|
10777
|
+
return isFunctionLike$1(initializer) ? initializer : null;
|
|
10778
|
+
};
|
|
10779
|
+
const calls = [];
|
|
10780
|
+
const eventsBySymbolId = /* @__PURE__ */ new Map();
|
|
10781
|
+
walkAst(scopes.rootScope.node, (node) => {
|
|
10782
|
+
if (isNodeOfType(node, "CallExpression")) {
|
|
10783
|
+
const owner = getExecutionOwner(node);
|
|
10784
|
+
const targetOwner = getLocalCallTarget(node);
|
|
10785
|
+
if (targetOwner && !isStaticallyUnreachable(node, owner)) calls.push({
|
|
10786
|
+
call: node,
|
|
10787
|
+
owner,
|
|
10788
|
+
targetOwner
|
|
10789
|
+
});
|
|
10790
|
+
}
|
|
10791
|
+
if (!isNodeOfType(node, "Identifier")) return;
|
|
10792
|
+
const propertyNames = getMutationPropertyNames(node);
|
|
10793
|
+
if (propertyNames === void 0) return;
|
|
10794
|
+
const symbol = resolveConstIdentifierAlias(node, scopes);
|
|
10795
|
+
if (!symbol) return;
|
|
10796
|
+
const owner = getExecutionOwner(node);
|
|
10797
|
+
if (isStaticallyUnreachable(node, owner)) return;
|
|
10798
|
+
const events = eventsBySymbolId.get(symbol.id) ?? [];
|
|
10799
|
+
events.push({
|
|
10800
|
+
node,
|
|
10801
|
+
owner,
|
|
10802
|
+
propertyNames
|
|
10803
|
+
});
|
|
10804
|
+
eventsBySymbolId.set(symbol.id, events);
|
|
10805
|
+
});
|
|
10806
|
+
const getInvokedOwnersBefore = (checkpoint) => {
|
|
10807
|
+
const checkpointOwner = getExecutionOwner(checkpoint);
|
|
10808
|
+
const checkpointStartIndex = getNodeStartIndex(checkpoint);
|
|
10809
|
+
const invokedOwners = /* @__PURE__ */ new Set();
|
|
10810
|
+
const visitOwner = (owner, cutoffIndex) => {
|
|
10811
|
+
for (const call of calls) {
|
|
10812
|
+
if (call.owner !== owner || getNodeStartIndex(call.call) >= cutoffIndex) continue;
|
|
10813
|
+
if (invokedOwners.has(call.targetOwner)) continue;
|
|
10814
|
+
invokedOwners.add(call.targetOwner);
|
|
10815
|
+
visitOwner(call.targetOwner, Number.POSITIVE_INFINITY);
|
|
10816
|
+
}
|
|
10817
|
+
};
|
|
10818
|
+
visitOwner(checkpointOwner, checkpointStartIndex);
|
|
10819
|
+
if (!isNodeOfType(checkpointOwner, "Program")) visitOwner(scopes.rootScope.node, Number.POSITIVE_INFINITY);
|
|
10820
|
+
return invokedOwners;
|
|
10821
|
+
};
|
|
10822
|
+
const getProgramCutoffIndex = (usageOwner) => {
|
|
10823
|
+
if (isNodeOfType(usageOwner, "Program")) return Number.POSITIVE_INFINITY;
|
|
10824
|
+
const directProgramCall = calls.find((call) => isNodeOfType(call.owner, "Program") && call.targetOwner === usageOwner);
|
|
10825
|
+
return directProgramCall ? getNodeStartIndex(directProgramCall.call) : Number.POSITIVE_INFINITY;
|
|
10826
|
+
};
|
|
10827
|
+
const callsByOwner = /* @__PURE__ */ new Map();
|
|
10828
|
+
for (const call of calls) {
|
|
10829
|
+
const ownerCalls = callsByOwner.get(call.owner) ?? [];
|
|
10830
|
+
ownerCalls.push(call);
|
|
10831
|
+
callsByOwner.set(call.owner, ownerCalls);
|
|
10832
|
+
}
|
|
10833
|
+
const ownerReachabilityCache = /* @__PURE__ */ new WeakMap();
|
|
10834
|
+
const canOwnerReach = (owner, targetOwner) => {
|
|
10835
|
+
const cachedResult = ownerReachabilityCache.get(owner)?.get(targetOwner);
|
|
10836
|
+
if (cachedResult !== void 0) return cachedResult;
|
|
10837
|
+
const pendingOwners = [owner];
|
|
10838
|
+
const visitedOwners = /* @__PURE__ */ new Set();
|
|
10839
|
+
let canReach = false;
|
|
10840
|
+
while (pendingOwners.length > 0) {
|
|
10841
|
+
const currentOwner = pendingOwners.pop();
|
|
10842
|
+
if (!currentOwner || visitedOwners.has(currentOwner)) continue;
|
|
10843
|
+
if (currentOwner === targetOwner) {
|
|
10844
|
+
canReach = true;
|
|
10845
|
+
break;
|
|
10846
|
+
}
|
|
10847
|
+
visitedOwners.add(currentOwner);
|
|
10848
|
+
for (const call of callsByOwner.get(currentOwner) ?? []) pendingOwners.push(call.targetOwner);
|
|
10849
|
+
}
|
|
10850
|
+
const cachedTargets = ownerReachabilityCache.get(owner) ?? /* @__PURE__ */ new WeakMap();
|
|
10851
|
+
cachedTargets.set(targetOwner, canReach);
|
|
10852
|
+
ownerReachabilityCache.set(owner, cachedTargets);
|
|
10853
|
+
return canReach;
|
|
10854
|
+
};
|
|
10855
|
+
const getRepeatedControlFlowAncestors = (node, owner) => {
|
|
10856
|
+
const ancestors = /* @__PURE__ */ new Set();
|
|
10857
|
+
let current = node;
|
|
10858
|
+
while (current?.parent && current !== owner) {
|
|
10859
|
+
const parent = current.parent;
|
|
10860
|
+
const isSingleIterationDoWhile = isNodeOfType(parent, "DoWhileStatement") && isNodeOfType(parent.test, "Literal") && !parent.test.value;
|
|
10861
|
+
const loopBody = isNodeOfType(parent, "ForStatement") || isNodeOfType(parent, "ForInStatement") || isNodeOfType(parent, "ForOfStatement") || isNodeOfType(parent, "WhileStatement") || isNodeOfType(parent, "DoWhileStatement") ? parent.body : null;
|
|
10862
|
+
let bodyStatement = node;
|
|
10863
|
+
while (loopBody && isNodeOfType(loopBody, "BlockStatement") && bodyStatement && bodyStatement.parent !== loopBody) bodyStatement = bodyStatement.parent ?? null;
|
|
10864
|
+
const bodyStatementIndex = loopBody && isNodeOfType(loopBody, "BlockStatement") && bodyStatement ? loopBody.body.findIndex((statement) => statement === bodyStatement) : -1;
|
|
10865
|
+
const hasFollowingLoopExit = Boolean(loopBody && isNodeOfType(loopBody, "BlockStatement") && bodyStatementIndex >= 0 && loopBody.body.slice(bodyStatementIndex + 1).some((statement) => isAbruptCompletionStatement(statement, false)));
|
|
10866
|
+
if (loopBody && !isSingleIterationDoWhile && !hasFollowingLoopExit) ancestors.add(parent);
|
|
10867
|
+
current = parent;
|
|
10868
|
+
}
|
|
10869
|
+
return ancestors;
|
|
10870
|
+
};
|
|
10871
|
+
const nodesShareRepeatedControlFlow = (leftNode, rightNode, owner) => {
|
|
10872
|
+
const leftAncestors = getRepeatedControlFlowAncestors(leftNode, owner);
|
|
10873
|
+
if (leftAncestors.size === 0) return false;
|
|
10874
|
+
return [...getRepeatedControlFlowAncestors(rightNode, owner)].some((ancestor) => leftAncestors.has(ancestor));
|
|
10875
|
+
};
|
|
10876
|
+
const callsReachingOwnerCache = /* @__PURE__ */ new WeakMap();
|
|
10877
|
+
const getCallsReachingOwnerByCaller = (targetOwner) => {
|
|
10878
|
+
const cachedCalls = callsReachingOwnerCache.get(targetOwner);
|
|
10879
|
+
if (cachedCalls) return cachedCalls;
|
|
10880
|
+
const reachingCalls = /* @__PURE__ */ new Map();
|
|
10881
|
+
for (const call of calls) {
|
|
10882
|
+
if (!canOwnerReach(call.targetOwner, targetOwner)) continue;
|
|
10883
|
+
const ownerCalls = reachingCalls.get(call.owner) ?? [];
|
|
10884
|
+
ownerCalls.push(call);
|
|
10885
|
+
reachingCalls.set(call.owner, ownerCalls);
|
|
10886
|
+
}
|
|
10887
|
+
callsReachingOwnerCache.set(targetOwner, reachingCalls);
|
|
10888
|
+
return reachingCalls;
|
|
10889
|
+
};
|
|
10890
|
+
const canMutationReachUsageAcrossCalls = (mutationOwner, usageOwner) => {
|
|
10891
|
+
const mutationCallsByOwner = getCallsReachingOwnerByCaller(mutationOwner);
|
|
10892
|
+
const usageCallsByOwner = getCallsReachingOwnerByCaller(usageOwner);
|
|
10893
|
+
for (const [owner, mutationCalls] of mutationCallsByOwner) {
|
|
10894
|
+
const usageCalls = usageCallsByOwner.get(owner);
|
|
10895
|
+
if (!usageCalls) continue;
|
|
10896
|
+
for (const mutationCall of mutationCalls) for (const usageCall of usageCalls) {
|
|
10897
|
+
if (mutationCall === usageCall) continue;
|
|
10898
|
+
if (getNodeStartIndex(mutationCall.call) < getNodeStartIndex(usageCall.call) || isFunctionLike$1(owner) || nodesShareRepeatedControlFlow(mutationCall.call, usageCall.call, owner)) return true;
|
|
10899
|
+
}
|
|
10900
|
+
}
|
|
10901
|
+
return false;
|
|
10902
|
+
};
|
|
10903
|
+
const isExecutionOrderAmbiguous = (usageNode) => {
|
|
10904
|
+
const usageOwner = getExecutionOwner(usageNode);
|
|
10905
|
+
if (isNodeOfType(usageOwner, "Program")) return false;
|
|
10906
|
+
const reachingProgramCalls = calls.filter((call) => isNodeOfType(call.owner, "Program") && canOwnerReach(call.targetOwner, usageOwner));
|
|
10907
|
+
if (reachingProgramCalls.length === 0) return false;
|
|
10908
|
+
return reachingProgramCalls.length !== 1 || reachingProgramCalls[0]?.targetOwner !== usageOwner;
|
|
10909
|
+
};
|
|
10910
|
+
const isMutationOrderAmbiguous = (symbol, usageNode, relevantPropertyName) => {
|
|
10911
|
+
const usageOwner = getExecutionOwner(usageNode);
|
|
10912
|
+
const usageStartIndex = getNodeStartIndex(usageNode);
|
|
10913
|
+
return (eventsBySymbolId.get(symbol.id) ?? []).some((event) => {
|
|
10914
|
+
if (relevantPropertyName !== null && event.propertyNames !== null && !event.propertyNames.has(relevantPropertyName)) return false;
|
|
10915
|
+
if (event.owner === usageOwner) return getNodeStartIndex(event.node) >= usageStartIndex && (isFunctionLike$1(usageOwner) || nodesShareRepeatedControlFlow(event.node, usageNode, usageOwner));
|
|
10916
|
+
if (isNodeOfType(event.owner, "Program")) return (getCallsReachingOwnerByCaller(usageOwner).get(event.owner) ?? []).some((usageCall) => nodesShareRepeatedControlFlow(event.node, usageCall.call, event.owner));
|
|
10917
|
+
return canOwnerReach(event.owner, usageOwner) || canMutationReachUsageAcrossCalls(event.owner, usageOwner);
|
|
10918
|
+
});
|
|
10919
|
+
};
|
|
10920
|
+
const getEventsBefore = (symbol, usageNode) => {
|
|
10921
|
+
const symbolEvents = eventsBySymbolId.get(symbol.id) ?? [];
|
|
10922
|
+
const mutationEvents = [];
|
|
10923
|
+
const visitOwner = (owner, cutoffIndex, activeOwners, isConditionalPath) => {
|
|
10924
|
+
if (activeOwners.has(owner)) return;
|
|
10925
|
+
const nextActiveOwners = new Set(activeOwners);
|
|
10926
|
+
nextActiveOwners.add(owner);
|
|
10927
|
+
const operations = [...symbolEvents.filter((event) => event.owner === owner).map((event) => ({
|
|
10928
|
+
event,
|
|
10929
|
+
index: getNodeStartIndex(event.node)
|
|
10930
|
+
})), ...calls.filter((call) => call.owner === owner).map((call) => ({
|
|
10931
|
+
call,
|
|
10932
|
+
index: getNodeStartIndex(call.call)
|
|
10933
|
+
}))].sort((left, right) => left.index - right.index);
|
|
10934
|
+
for (const operation of operations) {
|
|
10935
|
+
if (operation.index >= cutoffIndex) break;
|
|
10936
|
+
if ("event" in operation) {
|
|
10937
|
+
mutationEvents.push({
|
|
10938
|
+
isConditional: isConditionalPath || isConditionallyExecuted(operation.event.node, operation.event.owner),
|
|
10939
|
+
node: operation.event.node
|
|
10940
|
+
});
|
|
10941
|
+
continue;
|
|
10942
|
+
}
|
|
10943
|
+
visitOwner(operation.call.targetOwner, Number.POSITIVE_INFINITY, nextActiveOwners, isConditionalPath || isConditionallyExecuted(operation.call.call, operation.call.owner));
|
|
10944
|
+
}
|
|
10945
|
+
};
|
|
10946
|
+
const usageOwner = getExecutionOwner(usageNode);
|
|
10947
|
+
if (!isNodeOfType(usageOwner, "Program")) visitOwner(scopes.rootScope.node, getProgramCutoffIndex(usageOwner), /* @__PURE__ */ new Set(), false);
|
|
10948
|
+
visitOwner(usageOwner, getNodeStartIndex(usageNode), /* @__PURE__ */ new Set(), false);
|
|
10949
|
+
return mutationEvents;
|
|
10950
|
+
};
|
|
10951
|
+
const isMutatedBefore = (symbol, usageNode, relevantPropertyName) => {
|
|
10952
|
+
const events = eventsBySymbolId.get(symbol.id);
|
|
10953
|
+
if (!events) return false;
|
|
10954
|
+
const usageStartIndex = getNodeStartIndex(usageNode);
|
|
10955
|
+
const usageOwner = getExecutionOwner(usageNode);
|
|
10956
|
+
const invokedOwners = getInvokedOwnersBefore(usageNode);
|
|
10957
|
+
return events.some((event) => {
|
|
10958
|
+
if (relevantPropertyName !== null && event.propertyNames !== null && !event.propertyNames.has(relevantPropertyName)) return false;
|
|
10959
|
+
if (event.owner === usageOwner) return getNodeStartIndex(event.node) < usageStartIndex;
|
|
10960
|
+
if (isNodeOfType(event.owner, "Program") && !isNodeOfType(usageOwner, "Program")) return getNodeStartIndex(event.node) < getProgramCutoffIndex(usageOwner);
|
|
10961
|
+
return invokedOwners.has(event.owner);
|
|
10962
|
+
});
|
|
10963
|
+
};
|
|
10964
|
+
const inspector = {
|
|
10965
|
+
getEventsBefore,
|
|
10966
|
+
getOutermostTarget,
|
|
10967
|
+
isGlobalNamespaceMethod,
|
|
10968
|
+
isExecutionOrderAmbiguous,
|
|
10969
|
+
isMutationOrderAmbiguous,
|
|
10970
|
+
isMutatedBefore
|
|
10971
|
+
};
|
|
10972
|
+
inspectorCache.set(scopes, inspector);
|
|
10973
|
+
return inspector;
|
|
10974
|
+
};
|
|
10975
|
+
//#endregion
|
|
10976
|
+
//#region src/plugin/rules/security-scan/utils/get-katex-renderer-provenance.ts
|
|
10977
|
+
const isExpectedModuleName = (actualModuleName, expectedModuleName) => expectedModuleName === "katex" ? actualModuleName === "katex" || actualModuleName.startsWith("katex/") : actualModuleName === expectedModuleName;
|
|
10978
|
+
const isGlobalRequireCall = (node, moduleName, scopes) => {
|
|
10979
|
+
const expression = stripParenExpression(node);
|
|
10980
|
+
if (!isNodeOfType(expression, "CallExpression")) return false;
|
|
10981
|
+
const callee = stripParenExpression(expression.callee);
|
|
10982
|
+
const firstArgument = expression.arguments[0];
|
|
10983
|
+
return Boolean(isNodeOfType(callee, "Identifier") && callee.name === "require" && scopes.isGlobalReference(callee) && firstArgument && isNodeOfType(firstArgument, "Literal") && typeof firstArgument.value === "string" && isExpectedModuleName(firstArgument.value, moduleName));
|
|
10984
|
+
};
|
|
10985
|
+
const isTypeScriptImportEqualsFromModule = (symbol, moduleName) => {
|
|
10986
|
+
if (symbol.kind !== "ts-import-equals") return false;
|
|
10987
|
+
const declaration = symbol.declarationNode;
|
|
10988
|
+
if (!isNodeOfType(declaration, "TSImportEqualsDeclaration")) return false;
|
|
10989
|
+
const moduleReference = declaration.moduleReference;
|
|
10990
|
+
return Boolean(isNodeOfType(moduleReference, "TSExternalModuleReference") && isNodeOfType(moduleReference.expression, "Literal") && typeof moduleReference.expression.value === "string" && isExpectedModuleName(moduleReference.expression.value, moduleName));
|
|
10991
|
+
};
|
|
10992
|
+
const isAwaitedImportFromModule = (node, moduleName) => {
|
|
10993
|
+
const expression = stripParenExpression(node);
|
|
10994
|
+
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));
|
|
10995
|
+
};
|
|
10996
|
+
const getModuleNamespaceSymbol = (node, moduleName, namespacePropertyName, usageNode, scopes) => {
|
|
10997
|
+
const symbol = resolveConstIdentifierAlias(stripParenExpression(node), scopes);
|
|
10998
|
+
const mutationInspector = getSymbolMutationInspector(scopes);
|
|
10999
|
+
if (!symbol || mutationInspector.isExecutionOrderAmbiguous(usageNode) || mutationInspector.isMutationOrderAmbiguous(symbol, usageNode, namespacePropertyName) || mutationInspector.isMutatedBefore(symbol, usageNode, namespacePropertyName)) return null;
|
|
11000
|
+
const importDeclaration = getImportDeclarationForSymbol(symbol);
|
|
11001
|
+
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;
|
|
11002
|
+
if (isTypeScriptImportEqualsFromModule(symbol, moduleName)) return symbol;
|
|
11003
|
+
if (symbol.kind !== "const" || !symbol.initializer) return null;
|
|
11004
|
+
const initializer = stripParenExpression(symbol.initializer);
|
|
11005
|
+
if (isGlobalRequireCall(initializer, moduleName, scopes)) return symbol;
|
|
11006
|
+
if (isNodeOfType(initializer, "MemberExpression") && getStaticPropertyName(initializer) === "default" && (isGlobalRequireCall(initializer.object, moduleName, scopes) || isAwaitedImportFromModule(initializer.object, moduleName))) return symbol;
|
|
11007
|
+
if (isAwaitedImportFromModule(initializer, moduleName)) return symbol;
|
|
11008
|
+
return null;
|
|
11009
|
+
};
|
|
11010
|
+
const getNamedImportSymbol = (node, moduleName, importedName, usageNode, scopes) => {
|
|
11011
|
+
const symbol = resolveConstIdentifierAlias(stripParenExpression(node), scopes);
|
|
11012
|
+
if (!symbol) return null;
|
|
11013
|
+
const importDeclaration = getImportDeclarationForSymbol(symbol);
|
|
11014
|
+
const mutationInspector = getSymbolMutationInspector(scopes);
|
|
11015
|
+
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;
|
|
11016
|
+
return symbol;
|
|
11017
|
+
};
|
|
11018
|
+
const isKatexNamespace = (node, usageNode, scopes) => getModuleNamespaceSymbol(node, "katex", "renderToString", usageNode, scopes) !== null || isGlobalRequireCall(node, "katex", scopes);
|
|
11019
|
+
const isKatexNamedRenderer = (node, usageNode, scopes) => {
|
|
11020
|
+
if (getNamedImportSymbol(node, "katex", "renderToString", usageNode, scopes)) return true;
|
|
11021
|
+
const expression = stripParenExpression(node);
|
|
11022
|
+
if (!isNodeOfType(expression, "Identifier")) return false;
|
|
11023
|
+
const symbol = scopes.referenceFor(expression)?.resolvedSymbol;
|
|
11024
|
+
if (!symbol || symbol.kind !== "const" || !symbol.initializer || getSymbolMutationInspector(scopes).isMutatedBefore(symbol, usageNode, null)) return false;
|
|
11025
|
+
const initializer = stripParenExpression(symbol.initializer);
|
|
11026
|
+
const bindingProperty = symbol.bindingIdentifier.parent;
|
|
11027
|
+
if (isNodeOfType(bindingProperty, "Property") && getStaticPropertyKeyName(bindingProperty, { allowComputedString: true }) === "renderToString") return isKatexNamespace(initializer, symbol.declarationNode, scopes);
|
|
11028
|
+
if (isNodeOfType(initializer, "MemberExpression") && getStaticPropertyName(initializer) === "renderToString" && isKatexNamespace(initializer.object, symbol.declarationNode, scopes)) return true;
|
|
11029
|
+
if (isNodeOfType(initializer, "Identifier")) return isKatexNamedRenderer(initializer, symbol.declarationNode, scopes);
|
|
11030
|
+
return false;
|
|
11031
|
+
};
|
|
11032
|
+
const isUnprovenKatexShapedRenderer = (node, scopes) => {
|
|
11033
|
+
const expression = stripParenExpression(node);
|
|
11034
|
+
if (isNodeOfType(expression, "Identifier")) {
|
|
11035
|
+
if (!/katex/i.test(expression.name)) return false;
|
|
11036
|
+
const symbol = scopes.referenceFor(expression)?.resolvedSymbol;
|
|
11037
|
+
return Boolean(symbol && symbol.kind !== "parameter" && symbol.kind !== "let");
|
|
11038
|
+
}
|
|
11039
|
+
if (!isNodeOfType(expression, "MemberExpression")) return false;
|
|
11040
|
+
if (getStaticPropertyName(expression) !== "renderToString") return false;
|
|
11041
|
+
const receiver = stripParenExpression(expression.object);
|
|
11042
|
+
if (!isNodeOfType(receiver, "Identifier") || !/katex/i.test(receiver.name)) return false;
|
|
11043
|
+
const symbol = scopes.referenceFor(receiver)?.resolvedSymbol;
|
|
11044
|
+
if (!symbol) return false;
|
|
11045
|
+
if (symbol.kind === "import") {
|
|
11046
|
+
if (!isExpectedModuleName(String(getImportDeclarationForSymbol(symbol)?.source.value ?? ""), "katex")) return true;
|
|
11047
|
+
const mutationInspector = getSymbolMutationInspector(scopes);
|
|
11048
|
+
if (mutationInspector.isExecutionOrderAmbiguous(expression) || mutationInspector.isMutationOrderAmbiguous(symbol, expression, "renderToString")) return false;
|
|
11049
|
+
return mutationInspector.isMutatedBefore(symbol, expression, "renderToString");
|
|
11050
|
+
}
|
|
11051
|
+
if (symbol.kind === "parameter" || symbol.kind === "let" || symbol.kind === "var") return true;
|
|
11052
|
+
if (symbol.kind !== "const" || !symbol.initializer) return false;
|
|
11053
|
+
const initializer = stripParenExpression(symbol.initializer);
|
|
11054
|
+
if (isNodeOfType(initializer, "ObjectExpression")) return true;
|
|
11055
|
+
if (isNodeOfType(initializer, "CallExpression")) {
|
|
11056
|
+
const callee = stripParenExpression(initializer.callee);
|
|
11057
|
+
return isNodeOfType(callee, "Identifier") && callee.name === "require";
|
|
11058
|
+
}
|
|
11059
|
+
return isNodeOfType(initializer, "AwaitExpression") && isNodeOfType(initializer.argument, "ImportExpression");
|
|
11060
|
+
};
|
|
11061
|
+
//#endregion
|
|
11062
|
+
//#region src/plugin/rules/security-scan/utils/get-katex-options-proof.ts
|
|
11063
|
+
const parameterOptionsProofsByScopes = /* @__PURE__ */ new WeakMap();
|
|
11064
|
+
const isStaticallyDisabledTrustValue = (node, scopes) => {
|
|
11065
|
+
const expression = stripParenExpression(node);
|
|
11066
|
+
if (isNodeOfType(expression, "Identifier")) return expression.name === "undefined" && scopes.isGlobalReference(expression);
|
|
11067
|
+
return isNodeOfType(expression, "Literal") && !expression.value;
|
|
11068
|
+
};
|
|
11069
|
+
const getStaticObjectPropertyValue = (node, expectedPropertyName) => {
|
|
11070
|
+
const expression = stripParenExpression(node);
|
|
11071
|
+
if (!isNodeOfType(expression, "ObjectExpression")) return null;
|
|
11072
|
+
let propertyValue;
|
|
11073
|
+
for (const property of expression.properties) {
|
|
11074
|
+
if (!isNodeOfType(property, "Property")) return null;
|
|
11075
|
+
const propertyName = getStaticPropertyKeyName(property, { allowComputedString: true });
|
|
11076
|
+
if (propertyName === null) return null;
|
|
11077
|
+
if (propertyName !== expectedPropertyName) continue;
|
|
11078
|
+
if (property.kind !== "init") return null;
|
|
11079
|
+
propertyValue = property.value;
|
|
11080
|
+
}
|
|
11081
|
+
return propertyValue;
|
|
11082
|
+
};
|
|
11083
|
+
const getPropertyDescriptorValue = (node) => {
|
|
11084
|
+
const expression = stripParenExpression(node);
|
|
11085
|
+
if (!isNodeOfType(expression, "ObjectExpression")) return null;
|
|
11086
|
+
for (const property of expression.properties) {
|
|
11087
|
+
if (!isNodeOfType(property, "Property")) return null;
|
|
11088
|
+
const propertyName = getStaticPropertyKeyName(property, { allowComputedString: true });
|
|
11089
|
+
if (propertyName === null || propertyName === "get" || propertyName === "set") return null;
|
|
11090
|
+
}
|
|
11091
|
+
return getStaticObjectPropertyValue(expression, "value");
|
|
11092
|
+
};
|
|
11093
|
+
const getTrustStateAfterPropertyDescriptor = (currentState, propertyDescriptor, scopes) => {
|
|
11094
|
+
const propertyValue = getPropertyDescriptorValue(propertyDescriptor);
|
|
11095
|
+
if (propertyValue === null) return "trusted";
|
|
11096
|
+
if (propertyValue === void 0) return currentState;
|
|
11097
|
+
return isStaticallyDisabledTrustValue(propertyValue, scopes) ? "untrusted" : "trusted";
|
|
11098
|
+
};
|
|
11099
|
+
const mergeConditionalTrustStates = (currentState, conditionalState) => {
|
|
11100
|
+
if (currentState === conditionalState) return currentState;
|
|
11101
|
+
if (currentState === "trusted" || conditionalState === "trusted") return "trusted";
|
|
11102
|
+
if (currentState === "unsupported" || conditionalState === "unsupported") return "unsupported";
|
|
11103
|
+
return "untrusted";
|
|
11104
|
+
};
|
|
11105
|
+
const applyTrustMutation = (currentState, eventNode, scopes, visitedSymbolIds) => {
|
|
11106
|
+
const mutationInspector = getSymbolMutationInspector(scopes);
|
|
11107
|
+
const target = mutationInspector.getOutermostTarget(eventNode);
|
|
11108
|
+
const parent = target.parent;
|
|
11109
|
+
if (!parent) return "unsupported";
|
|
11110
|
+
if (isNodeOfType(parent, "AssignmentExpression") && parent.left === target) {
|
|
11111
|
+
if (!isNodeOfType(target, "MemberExpression")) return "unsupported";
|
|
11112
|
+
const propertyName = getStaticPropertyName(target);
|
|
11113
|
+
if (propertyName === null) return "trusted";
|
|
11114
|
+
if (propertyName !== "trust") return currentState;
|
|
11115
|
+
return isStaticallyDisabledTrustValue(parent.right, scopes) ? "untrusted" : "trusted";
|
|
11116
|
+
}
|
|
11117
|
+
if (isNodeOfType(parent, "UnaryExpression") && parent.operator === "delete") {
|
|
11118
|
+
if (!isNodeOfType(target, "MemberExpression")) return "unsupported";
|
|
11119
|
+
const propertyName = getStaticPropertyName(target);
|
|
11120
|
+
if (propertyName === null) return "trusted";
|
|
11121
|
+
return propertyName === "trust" ? "absent" : currentState;
|
|
11122
|
+
}
|
|
11123
|
+
if (isNodeOfType(parent, "UpdateExpression")) {
|
|
11124
|
+
if (!isNodeOfType(target, "MemberExpression")) return "unsupported";
|
|
11125
|
+
const propertyName = getStaticPropertyName(target);
|
|
11126
|
+
return propertyName === "trust" || propertyName === null ? "trusted" : currentState;
|
|
11127
|
+
}
|
|
11128
|
+
if (!isNodeOfType(parent, "CallExpression") || parent.arguments[0] !== target) return "unsupported";
|
|
11129
|
+
if (mutationInspector.isGlobalNamespaceMethod(parent.callee, "Object", OBJECT_PROPERTY_MUTATION_METHOD_NAMES)) {
|
|
11130
|
+
const callee = stripParenExpression(parent.callee);
|
|
11131
|
+
if (!isNodeOfType(callee, "MemberExpression")) return "unsupported";
|
|
11132
|
+
const methodName = getStaticPropertyName(callee);
|
|
11133
|
+
if (methodName === "assign") {
|
|
11134
|
+
let nextState = currentState;
|
|
11135
|
+
for (const source of parent.arguments.slice(1)) {
|
|
11136
|
+
const sourceState = getKatexOptionsTrustState(source, source, scopes, new Set(visitedSymbolIds));
|
|
11137
|
+
if (sourceState !== "absent") nextState = sourceState;
|
|
11138
|
+
}
|
|
11139
|
+
return nextState;
|
|
11140
|
+
}
|
|
11141
|
+
if (methodName === "defineProperties") {
|
|
11142
|
+
const propertyDescriptors = parent.arguments[1];
|
|
11143
|
+
if (!propertyDescriptors) return "unsupported";
|
|
11144
|
+
const trustDescriptor = getStaticObjectPropertyValue(propertyDescriptors, "trust");
|
|
11145
|
+
if (trustDescriptor === null) return "trusted";
|
|
11146
|
+
if (trustDescriptor === void 0) return currentState;
|
|
11147
|
+
return getTrustStateAfterPropertyDescriptor(currentState, trustDescriptor, scopes);
|
|
11148
|
+
}
|
|
11149
|
+
const propertyKey = parent.arguments[1];
|
|
11150
|
+
if (!propertyKey || !isNodeOfType(propertyKey, "Literal") || typeof propertyKey.value !== "string") return "trusted";
|
|
11151
|
+
if (propertyKey.value !== "trust") return currentState;
|
|
11152
|
+
const propertyDescriptor = parent.arguments[2];
|
|
11153
|
+
if (!propertyDescriptor) return "unsupported";
|
|
11154
|
+
return getTrustStateAfterPropertyDescriptor(currentState, propertyDescriptor, scopes);
|
|
11155
|
+
}
|
|
11156
|
+
if (mutationInspector.isGlobalNamespaceMethod(parent.callee, "Reflect", REFLECT_PROPERTY_MUTATION_METHOD_NAMES)) {
|
|
11157
|
+
const callee = stripParenExpression(parent.callee);
|
|
11158
|
+
if (!isNodeOfType(callee, "MemberExpression")) return "unsupported";
|
|
11159
|
+
const methodName = getStaticPropertyName(callee);
|
|
11160
|
+
const propertyKey = parent.arguments[1];
|
|
11161
|
+
if (!propertyKey || !isNodeOfType(propertyKey, "Literal") || typeof propertyKey.value !== "string") return "trusted";
|
|
11162
|
+
if (propertyKey.value !== "trust") return currentState;
|
|
11163
|
+
const propertyValue = parent.arguments[2];
|
|
11164
|
+
if (!propertyValue) return "unsupported";
|
|
11165
|
+
if (methodName === "defineProperty") return getTrustStateAfterPropertyDescriptor(currentState, propertyValue, scopes);
|
|
11166
|
+
return isStaticallyDisabledTrustValue(propertyValue, scopes) ? "untrusted" : "trusted";
|
|
11167
|
+
}
|
|
11168
|
+
return "unsupported";
|
|
11169
|
+
};
|
|
11170
|
+
const getKatexOptionsTrustState = (rawNode, usageNode, scopes, visitedSymbolIds) => {
|
|
11171
|
+
if (rawNode === void 0) return "absent";
|
|
11172
|
+
const node = stripParenExpression(rawNode);
|
|
11173
|
+
if (isNodeOfType(node, "Identifier")) {
|
|
11174
|
+
if (node.name === "undefined" && scopes.isGlobalReference(node)) return "absent";
|
|
11175
|
+
const symbol = resolveConstIdentifierAlias(node, scopes);
|
|
11176
|
+
if (!symbol || symbol.kind !== "const" || !symbol.initializer || visitedSymbolIds.has(symbol.id)) return "unsupported";
|
|
11177
|
+
const nextVisitedSymbolIds = new Set(visitedSymbolIds);
|
|
11178
|
+
nextVisitedSymbolIds.add(symbol.id);
|
|
11179
|
+
const mutationInspector = getSymbolMutationInspector(scopes);
|
|
11180
|
+
if (mutationInspector.isMutationOrderAmbiguous(symbol, usageNode, "trust")) return "unsupported";
|
|
11181
|
+
let trustState = getKatexOptionsTrustState(symbol.initializer, usageNode, scopes, nextVisitedSymbolIds);
|
|
11182
|
+
for (const replayedEvent of mutationInspector.getEventsBefore(symbol, usageNode)) {
|
|
11183
|
+
const nextTrustState = applyTrustMutation(trustState, replayedEvent.node, scopes, nextVisitedSymbolIds);
|
|
11184
|
+
if (replayedEvent.isConditional) trustState = mergeConditionalTrustStates(trustState, nextTrustState);
|
|
11185
|
+
else trustState = nextTrustState;
|
|
11186
|
+
}
|
|
11187
|
+
return trustState;
|
|
11188
|
+
}
|
|
11189
|
+
if (!isNodeOfType(node, "ObjectExpression")) return "unsupported";
|
|
11190
|
+
let trustState = "absent";
|
|
11191
|
+
for (const property of node.properties) {
|
|
11192
|
+
if (isNodeOfType(property, "SpreadElement")) {
|
|
11193
|
+
const spreadState = getKatexOptionsTrustState(property.argument, property.argument, scopes, new Set(visitedSymbolIds));
|
|
11194
|
+
if (spreadState !== "absent") trustState = spreadState === "unsupported" ? "trusted" : spreadState;
|
|
11195
|
+
continue;
|
|
11196
|
+
}
|
|
11197
|
+
if (!isNodeOfType(property, "Property")) {
|
|
11198
|
+
trustState = "trusted";
|
|
11199
|
+
continue;
|
|
11200
|
+
}
|
|
11201
|
+
const propertyName = getStaticPropertyKeyName(property, { allowComputedString: true });
|
|
11202
|
+
if (propertyName === null) {
|
|
11203
|
+
trustState = "trusted";
|
|
11204
|
+
continue;
|
|
11205
|
+
}
|
|
11206
|
+
if (propertyName === "trust") trustState = isStaticallyDisabledTrustValue(property.value, scopes) ? "untrusted" : "trusted";
|
|
11207
|
+
}
|
|
11208
|
+
return trustState;
|
|
11209
|
+
};
|
|
11210
|
+
const setKatexParameterOptionsProofs = (scopes, proofs) => {
|
|
11211
|
+
parameterOptionsProofsByScopes.set(scopes, proofs);
|
|
11212
|
+
};
|
|
11213
|
+
const getKatexOptionsProof = (rawNode, usageNode, scopes, visitedSymbolIds) => {
|
|
11214
|
+
const node = rawNode ? stripParenExpression(rawNode) : void 0;
|
|
11215
|
+
if (node && isNodeOfType(node, "Identifier")) {
|
|
11216
|
+
const parameterSymbol = scopes.referenceFor(node)?.resolvedSymbol;
|
|
11217
|
+
const parameterProof = parameterSymbol ? parameterOptionsProofsByScopes.get(scopes)?.get(parameterSymbol.id) : void 0;
|
|
11218
|
+
if (parameterProof) return parameterProof;
|
|
11219
|
+
}
|
|
11220
|
+
const trustState = getKatexOptionsTrustState(rawNode, usageNode, scopes, visitedSymbolIds);
|
|
11221
|
+
return {
|
|
11222
|
+
isConclusive: trustState !== "unsupported",
|
|
11223
|
+
isSafe: trustState === "absent" || trustState === "untrusted"
|
|
11224
|
+
};
|
|
11225
|
+
};
|
|
11226
|
+
//#endregion
|
|
11227
|
+
//#region src/plugin/rules/security-scan/utils/get-katex-html-proof.ts
|
|
11228
|
+
const SAFE_STATIC_HTML_PROOF = {
|
|
11229
|
+
containsKatex: false,
|
|
11230
|
+
isConclusive: true,
|
|
11231
|
+
isSafe: true,
|
|
11232
|
+
isSafeInAttributeContext: true
|
|
11233
|
+
};
|
|
11234
|
+
const SAFE_HTML_FRAGMENT_PROOF = {
|
|
11235
|
+
containsKatex: false,
|
|
11236
|
+
isConclusive: true,
|
|
11237
|
+
isSafe: true,
|
|
11238
|
+
isSafeInAttributeContext: false
|
|
11239
|
+
};
|
|
11240
|
+
const UNKNOWN_HTML_PROOF = {
|
|
11241
|
+
containsKatex: false,
|
|
11242
|
+
isConclusive: false,
|
|
11243
|
+
isSafe: false,
|
|
11244
|
+
isSafeInAttributeContext: false
|
|
11245
|
+
};
|
|
11246
|
+
const UNSUPPORTED_KATEX_PROOF = {
|
|
11247
|
+
containsKatex: true,
|
|
11248
|
+
isConclusive: false,
|
|
11249
|
+
isSafe: false,
|
|
11250
|
+
isSafeInAttributeContext: false
|
|
11251
|
+
};
|
|
11252
|
+
const UNSAFE_KATEX_PROOF = {
|
|
11253
|
+
containsKatex: true,
|
|
11254
|
+
isConclusive: true,
|
|
11255
|
+
isSafe: false,
|
|
11256
|
+
isSafeInAttributeContext: false
|
|
11257
|
+
};
|
|
11258
|
+
const sourceFilenameByScopes = /* @__PURE__ */ new WeakMap();
|
|
11259
|
+
const crossFileDepthByScopes = /* @__PURE__ */ new WeakMap();
|
|
11260
|
+
const registerKatexProofSource = (scopes, filename, depth) => {
|
|
11261
|
+
sourceFilenameByScopes.set(scopes, filename);
|
|
11262
|
+
crossFileDepthByScopes.set(scopes, depth);
|
|
11263
|
+
};
|
|
11264
|
+
const combineHtmlProofs = (proofs) => ({
|
|
11265
|
+
containsKatex: proofs.some((proof) => proof.containsKatex),
|
|
11266
|
+
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),
|
|
11267
|
+
isSafe: proofs.every((proof) => proof.isSafe),
|
|
11268
|
+
isSafeInAttributeContext: proofs.every((proof) => proof.isSafeInAttributeContext)
|
|
11269
|
+
});
|
|
11270
|
+
const getOrderedObjectPropertyValue = (node, propertyName) => {
|
|
11271
|
+
const expression = stripParenExpression(node);
|
|
11272
|
+
if (!isNodeOfType(expression, "ObjectExpression")) return {
|
|
11273
|
+
isKnown: false,
|
|
11274
|
+
value: null
|
|
11275
|
+
};
|
|
11276
|
+
let isKnown = true;
|
|
11277
|
+
let propertyValue = null;
|
|
11278
|
+
for (const property of expression.properties) {
|
|
11279
|
+
if (!isNodeOfType(property, "Property")) {
|
|
11280
|
+
isKnown = false;
|
|
11281
|
+
propertyValue = null;
|
|
11282
|
+
continue;
|
|
11283
|
+
}
|
|
11284
|
+
const currentPropertyName = getStaticPropertyKeyName(property, { allowComputedString: true });
|
|
11285
|
+
if (currentPropertyName === null) {
|
|
11286
|
+
isKnown = false;
|
|
11287
|
+
propertyValue = null;
|
|
11288
|
+
} else if (currentPropertyName === propertyName) {
|
|
11289
|
+
isKnown = true;
|
|
11290
|
+
propertyValue = property.value;
|
|
11291
|
+
}
|
|
11292
|
+
}
|
|
11293
|
+
return {
|
|
11294
|
+
isKnown,
|
|
11295
|
+
value: propertyValue
|
|
11296
|
+
};
|
|
11297
|
+
};
|
|
11298
|
+
const isReactUseMemo = (node, scopes) => {
|
|
11299
|
+
const expression = stripParenExpression(node);
|
|
11300
|
+
if (isNodeOfType(expression, "Identifier")) {
|
|
11301
|
+
const symbol = resolveConstIdentifierAlias(expression, scopes);
|
|
11302
|
+
return Boolean(symbol && symbol.kind === "import" && getImportDeclarationForSymbol(symbol)?.source.value === "react" && getImportedName(symbol.declarationNode) === "useMemo");
|
|
11303
|
+
}
|
|
11304
|
+
if (!isNodeOfType(expression, "MemberExpression") || getStaticPropertyName(expression) !== "useMemo") return false;
|
|
11305
|
+
const symbol = resolveConstIdentifierAlias(stripParenExpression(expression.object), scopes);
|
|
11306
|
+
return Boolean(symbol && symbol.kind === "import" && getImportDeclarationForSymbol(symbol)?.source.value === "react" && (isNodeOfType(symbol.declarationNode, "ImportDefaultSpecifier") || isNodeOfType(symbol.declarationNode, "ImportNamespaceSpecifier") || getImportedName(symbol.declarationNode) === "default"));
|
|
11307
|
+
};
|
|
11308
|
+
const isAllOpeningAngleBracketsEscaped = (node, scopes) => {
|
|
11309
|
+
let current = stripParenExpression(node);
|
|
11310
|
+
let didEscapeEveryOpeningAngleBracket = false;
|
|
11311
|
+
while (isNodeOfType(current, "CallExpression")) {
|
|
11312
|
+
const callee = stripParenExpression(current.callee);
|
|
11313
|
+
if (!isNodeOfType(callee, "MemberExpression")) return false;
|
|
11314
|
+
const methodName = getStaticPropertyName(callee);
|
|
11315
|
+
if (methodName !== "replace" && methodName !== "replaceAll") return false;
|
|
11316
|
+
const searchValue = current.arguments[0];
|
|
11317
|
+
const replacementValue = current.arguments[1];
|
|
11318
|
+
if (!searchValue || !replacementValue || !isNodeOfType(replacementValue, "Literal") || typeof replacementValue.value !== "string" || replacementValue.value.includes("<") || replacementValue.value.includes("$")) return false;
|
|
11319
|
+
if (isNodeOfType(searchValue, "Literal")) {
|
|
11320
|
+
const regularExpression = "regex" in searchValue ? searchValue.regex : void 0;
|
|
11321
|
+
const replacesLiteralOpeningAngleBracket = methodName === "replaceAll" && searchValue.value === "<";
|
|
11322
|
+
const replacesGlobalOpeningAngleBracketPattern = regularExpression?.pattern === "<" && regularExpression.flags.includes("g");
|
|
11323
|
+
if (replacesLiteralOpeningAngleBracket || replacesGlobalOpeningAngleBracketPattern) didEscapeEveryOpeningAngleBracket = true;
|
|
11324
|
+
}
|
|
11325
|
+
current = stripParenExpression(callee.object);
|
|
11326
|
+
}
|
|
11327
|
+
if (!didEscapeEveryOpeningAngleBracket || !isNodeOfType(current, "Identifier")) return false;
|
|
11328
|
+
return scopes.referenceFor(current)?.resolvedSymbol?.kind === "parameter";
|
|
11329
|
+
};
|
|
11330
|
+
const getSanitizerProof = (node, scopes) => {
|
|
11331
|
+
const callee = stripParenExpression(node.callee);
|
|
11332
|
+
if (isNodeOfType(callee, "MemberExpression") && getStaticPropertyName(callee) === "sanitize") {
|
|
11333
|
+
if (getModuleNamespaceSymbol(callee.object, "dompurify", "sanitize", node, scopes) || getModuleNamespaceSymbol(callee.object, "isomorphic-dompurify", "sanitize", node, scopes)) return SAFE_HTML_FRAGMENT_PROOF;
|
|
11334
|
+
}
|
|
11335
|
+
if (isNodeOfType(callee, "MemberExpression") && getStaticPropertyName(callee) === "escape") {
|
|
11336
|
+
if (getModuleNamespaceSymbol(callee.object, "html-escaper", "escape", node, scopes)) return SAFE_STATIC_HTML_PROOF;
|
|
11337
|
+
}
|
|
11338
|
+
if (getNamedImportSymbol(callee, "html-escaper", "escape", node, scopes)) return SAFE_STATIC_HTML_PROOF;
|
|
11339
|
+
if (getNamedImportSymbol(callee, "dompurify", "sanitize", node, scopes) || getNamedImportSymbol(callee, "isomorphic-dompurify", "sanitize", node, scopes)) return SAFE_HTML_FRAGMENT_PROOF;
|
|
11340
|
+
return null;
|
|
11341
|
+
};
|
|
11342
|
+
const getSafePostTransformProof = (node, receiverProof) => {
|
|
11343
|
+
if (!receiverProof.isSafe) return null;
|
|
11344
|
+
const callee = stripParenExpression(node.callee);
|
|
11345
|
+
if (!isNodeOfType(callee, "MemberExpression")) return null;
|
|
11346
|
+
const methodName = getStaticPropertyName(callee);
|
|
11347
|
+
if ((methodName === "trim" || methodName === "trimEnd" || methodName === "trimStart") && node.arguments.length === 0) return receiverProof;
|
|
11348
|
+
if (methodName !== "replace" && methodName !== "replaceAll") return null;
|
|
11349
|
+
const replacement = node.arguments[1];
|
|
11350
|
+
if (!replacement || !isNodeOfType(replacement, "Literal") || typeof replacement.value !== "string" || replacement.value.includes("<")) return null;
|
|
11351
|
+
return {
|
|
11352
|
+
containsKatex: receiverProof.containsKatex,
|
|
11353
|
+
isConclusive: receiverProof.isConclusive,
|
|
11354
|
+
isSafe: true,
|
|
11355
|
+
isSafeInAttributeContext: receiverProof.isSafeInAttributeContext && !/[&>"']/.test(replacement.value)
|
|
11356
|
+
};
|
|
11357
|
+
};
|
|
11358
|
+
const getTemplateInterpolationContext = (staticPrefix) => {
|
|
11359
|
+
const lowerPrefix = staticPrefix.toLowerCase();
|
|
11360
|
+
for (const tagName of [
|
|
11361
|
+
"script",
|
|
11362
|
+
"style",
|
|
11363
|
+
"textarea",
|
|
11364
|
+
"title"
|
|
11365
|
+
]) if (lowerPrefix.lastIndexOf(`<${tagName}`) > lowerPrefix.lastIndexOf(`</${tagName}`)) return "raw-text";
|
|
11366
|
+
const lastOpeningAngleIndex = staticPrefix.lastIndexOf("<");
|
|
11367
|
+
if (lastOpeningAngleIndex <= staticPrefix.lastIndexOf(">")) return "text";
|
|
11368
|
+
const currentTagText = staticPrefix.slice(lastOpeningAngleIndex + 1);
|
|
11369
|
+
let openQuote = null;
|
|
11370
|
+
for (let index = 0; index < currentTagText.length; index += 1) {
|
|
11371
|
+
const character = currentTagText[index];
|
|
11372
|
+
if ((character === "\"" || character === "'") && currentTagText[index - 1] !== "\\") {
|
|
11373
|
+
if (openQuote === character) openQuote = null;
|
|
11374
|
+
else if (openQuote === null) openQuote = character;
|
|
11375
|
+
}
|
|
11376
|
+
}
|
|
11377
|
+
return openQuote === null ? "unsafe-tag" : "attribute";
|
|
11378
|
+
};
|
|
11379
|
+
const getTemplateLiteralProof = (node, scopes, visitedSymbolIds, parameterProofs) => {
|
|
11380
|
+
const expressionProofs = node.expressions.map((expression) => getKatexHtmlProof(expression, scopes, new Set(visitedSymbolIds), parameterProofs));
|
|
11381
|
+
let staticPrefix = "";
|
|
11382
|
+
let isSafe = true;
|
|
11383
|
+
for (let expressionIndex = 0; expressionIndex < expressionProofs.length; expressionIndex += 1) {
|
|
11384
|
+
staticPrefix += node.quasis[expressionIndex]?.value.raw ?? "";
|
|
11385
|
+
const context = getTemplateInterpolationContext(staticPrefix);
|
|
11386
|
+
const proof = expressionProofs[expressionIndex] ?? UNKNOWN_HTML_PROOF;
|
|
11387
|
+
if (context === "text") isSafe &&= proof.isSafe;
|
|
11388
|
+
else if (context === "attribute") isSafe &&= proof.isSafeInAttributeContext;
|
|
11389
|
+
else isSafe = false;
|
|
11390
|
+
}
|
|
11391
|
+
return {
|
|
11392
|
+
containsKatex: expressionProofs.some((proof) => proof.containsKatex),
|
|
11393
|
+
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),
|
|
11394
|
+
isSafe,
|
|
11395
|
+
isSafeInAttributeContext: false
|
|
11396
|
+
};
|
|
11397
|
+
};
|
|
11398
|
+
const isReturnStatementStaticallyUnreachable = (returnStatement, functionBody) => {
|
|
11399
|
+
let current = returnStatement;
|
|
11400
|
+
while (current.parent && current !== functionBody) {
|
|
11401
|
+
const parent = current.parent;
|
|
11402
|
+
if (isNodeOfType(parent, "BlockStatement")) {
|
|
11403
|
+
const statementIndex = parent.body.findIndex((statement) => statement === current);
|
|
11404
|
+
if (statementIndex > 0 && parent.body.slice(0, statementIndex).some((statement) => statementAlwaysExits(statement))) return true;
|
|
11405
|
+
}
|
|
11406
|
+
if (isNodeOfType(parent, "SwitchCase")) {
|
|
11407
|
+
const statementIndex = parent.consequent.findIndex((statement) => statement === current);
|
|
11408
|
+
if (statementIndex > 0 && parent.consequent.slice(0, statementIndex).some((statement) => statementAlwaysExits(statement))) return true;
|
|
11409
|
+
}
|
|
11410
|
+
if (isNodeOfType(parent, "IfStatement") && isNodeOfType(parent.test, "Literal")) {
|
|
11411
|
+
const ifStatementAlternate = parent.alternate;
|
|
11412
|
+
const ifStatementConsequent = parent.consequent;
|
|
11413
|
+
const ifStatementTest = parent.test;
|
|
11414
|
+
const isTruthyTest = Boolean(ifStatementTest.value);
|
|
11415
|
+
if (!isTruthyTest && ifStatementConsequent === current) return true;
|
|
11416
|
+
if (isTruthyTest && ifStatementAlternate === current) return true;
|
|
11417
|
+
}
|
|
11418
|
+
if (isNodeOfType(parent, "WhileStatement") && parent.body === current) {
|
|
11419
|
+
const whileStatementTest = parent.test;
|
|
11420
|
+
if (isNodeOfType(whileStatementTest, "Literal") && !whileStatementTest.value) return true;
|
|
11421
|
+
}
|
|
11422
|
+
if (isNodeOfType(parent, "ForStatement") && parent.body === current) {
|
|
11423
|
+
const forStatementTest = parent.test;
|
|
11424
|
+
if (forStatementTest && isNodeOfType(forStatementTest, "Literal") && !forStatementTest.value) return true;
|
|
11425
|
+
}
|
|
11426
|
+
current = parent;
|
|
11427
|
+
}
|
|
11428
|
+
return false;
|
|
11429
|
+
};
|
|
11430
|
+
const getFunctionHtmlProof = (functionNode, scopes, visitedSymbolIds, parameterProofs = /* @__PURE__ */ new Map()) => {
|
|
11431
|
+
if (!isFunctionLike$1(functionNode)) return UNKNOWN_HTML_PROOF;
|
|
11432
|
+
if (!isNodeOfType(functionNode.body, "BlockStatement")) return getKatexHtmlProof(functionNode.body, scopes, visitedSymbolIds, parameterProofs);
|
|
11433
|
+
const functionBody = functionNode.body;
|
|
11434
|
+
const returnProofs = [];
|
|
11435
|
+
walkAst(functionBody, (child) => {
|
|
11436
|
+
if (child !== functionBody && isFunctionLike$1(child)) return false;
|
|
11437
|
+
if (!isNodeOfType(child, "ReturnStatement")) return;
|
|
11438
|
+
if (isReturnStatementStaticallyUnreachable(child, functionBody)) return false;
|
|
11439
|
+
returnProofs.push(child.argument ? getKatexHtmlProof(child.argument, scopes, new Set(visitedSymbolIds), parameterProofs) : SAFE_STATIC_HTML_PROOF);
|
|
11440
|
+
return false;
|
|
11441
|
+
});
|
|
11442
|
+
return returnProofs.length === 0 ? SAFE_STATIC_HTML_PROOF : combineHtmlProofs(returnProofs);
|
|
11443
|
+
};
|
|
11444
|
+
const getLocalFunctionNode = (node, scopes) => {
|
|
11445
|
+
const expression = stripParenExpression(node);
|
|
11446
|
+
if (!isNodeOfType(expression, "Identifier")) return null;
|
|
11447
|
+
const symbol = resolveConstIdentifierAlias(expression, scopes);
|
|
11448
|
+
if (!symbol || symbol.references.some((reference) => reference.flag !== "read")) return null;
|
|
11449
|
+
if (symbol.kind === "function" && isFunctionLike$1(symbol.declarationNode)) return {
|
|
11450
|
+
functionNode: symbol.declarationNode,
|
|
11451
|
+
symbol
|
|
11452
|
+
};
|
|
11453
|
+
if (symbol.kind !== "const" || !symbol.initializer) return null;
|
|
11454
|
+
const initializer = stripParenExpression(symbol.initializer);
|
|
11455
|
+
return isFunctionLike$1(initializer) ? {
|
|
11456
|
+
functionNode: initializer,
|
|
11457
|
+
symbol
|
|
11458
|
+
} : null;
|
|
11459
|
+
};
|
|
11460
|
+
const getCrossFileFunctionProof = (call, scopes) => {
|
|
11461
|
+
const expression = stripParenExpression(call.callee);
|
|
11462
|
+
if (!isNodeOfType(expression, "Identifier")) return null;
|
|
11463
|
+
const symbol = resolveConstIdentifierAlias(expression, scopes);
|
|
11464
|
+
if (!symbol || symbol.kind !== "import") return null;
|
|
11465
|
+
const importDeclaration = getImportDeclarationForSymbol(symbol);
|
|
11466
|
+
const importedName = getImportedName(symbol.declarationNode);
|
|
11467
|
+
const sourceFilename = sourceFilenameByScopes.get(scopes);
|
|
11468
|
+
const source = importDeclaration?.source.value;
|
|
11469
|
+
const currentDepth = crossFileDepthByScopes.get(scopes) ?? 0;
|
|
11470
|
+
if (!sourceFilename || typeof source !== "string" || !importedName || currentDepth >= 2) return null;
|
|
11471
|
+
const resolved = resolveCrossFileFunctionExportWithFilePath(sourceFilename, source, importedName);
|
|
11472
|
+
if (!resolved || !isFunctionLike$1(resolved.functionNode)) return null;
|
|
11473
|
+
const resolvedScopes = analyzeScopes(resolved.programNode);
|
|
11474
|
+
registerKatexProofSource(resolvedScopes, resolved.filePath, currentDepth + 1);
|
|
11475
|
+
const optionsProofs = /* @__PURE__ */ new Map();
|
|
11476
|
+
for (const [parameterIndex, parameter] of resolved.functionNode.params.entries()) {
|
|
11477
|
+
if (!isNodeOfType(parameter, "ObjectPattern")) continue;
|
|
11478
|
+
const argument = call.arguments[parameterIndex];
|
|
11479
|
+
if (!argument) continue;
|
|
11480
|
+
for (const property of parameter.properties) {
|
|
11481
|
+
if (!isNodeOfType(property, "Property") || !isNodeOfType(property.value, "Identifier")) continue;
|
|
11482
|
+
const propertyName = getStaticPropertyKeyName(property, { allowComputedString: true });
|
|
11483
|
+
if (propertyName === null) continue;
|
|
11484
|
+
const argumentProperty = getOrderedObjectPropertyValue(argument, propertyName);
|
|
11485
|
+
const parameterSymbol = resolvedScopes.symbolFor(property.value);
|
|
11486
|
+
if (!argumentProperty.isKnown || !parameterSymbol || parameterSymbol.references.some((reference) => reference.flag !== "read")) continue;
|
|
11487
|
+
if (argumentProperty.value === null) {
|
|
11488
|
+
optionsProofs.set(parameterSymbol.id, {
|
|
11489
|
+
isConclusive: true,
|
|
11490
|
+
isSafe: true
|
|
11491
|
+
});
|
|
11492
|
+
continue;
|
|
11493
|
+
}
|
|
11494
|
+
optionsProofs.set(parameterSymbol.id, getKatexOptionsProof(argumentProperty.value, call, scopes, /* @__PURE__ */ new Set()));
|
|
11495
|
+
}
|
|
11496
|
+
}
|
|
11497
|
+
setKatexParameterOptionsProofs(resolvedScopes, optionsProofs);
|
|
11498
|
+
return getFunctionHtmlProof(resolved.functionNode, resolvedScopes, /* @__PURE__ */ new Set());
|
|
11499
|
+
};
|
|
11500
|
+
const getKatexCallProof = (node, scopes, visitedSymbolIds, parameterProofs) => {
|
|
11501
|
+
if (!isNodeOfType(node, "CallExpression")) return UNKNOWN_HTML_PROOF;
|
|
11502
|
+
const callee = stripParenExpression(node.callee);
|
|
11503
|
+
if (isNodeOfType(callee, "MemberExpression") && getStaticPropertyName(callee) === "renderToString" && isKatexNamespace(callee.object, node, scopes) || isKatexNamedRenderer(callee, node, scopes)) {
|
|
11504
|
+
const optionsProof = getKatexOptionsProof(node.arguments[1], node, scopes, /* @__PURE__ */ new Set());
|
|
11505
|
+
return {
|
|
11506
|
+
containsKatex: true,
|
|
11507
|
+
isConclusive: optionsProof.isConclusive,
|
|
11508
|
+
isSafe: optionsProof.isSafe,
|
|
11509
|
+
isSafeInAttributeContext: false
|
|
11510
|
+
};
|
|
11511
|
+
}
|
|
11512
|
+
const crossFileFunctionProof = getCrossFileFunctionProof(node, scopes);
|
|
11513
|
+
if (crossFileFunctionProof?.containsKatex) return crossFileFunctionProof;
|
|
11514
|
+
const localFunction = getLocalFunctionNode(callee, scopes);
|
|
11515
|
+
if (localFunction && !visitedSymbolIds.has(localFunction.symbol.id)) {
|
|
11516
|
+
const nextVisitedSymbolIds = new Set(visitedSymbolIds);
|
|
11517
|
+
nextVisitedSymbolIds.add(localFunction.symbol.id);
|
|
11518
|
+
const argumentProofs = node.arguments.map((argument) => {
|
|
11519
|
+
const argumentNode = stripParenExpression(argument);
|
|
11520
|
+
return isFunctionLike$1(argumentNode) ? getFunctionHtmlProof(argumentNode, scopes, new Set(visitedSymbolIds), parameterProofs) : getKatexHtmlProof(argumentNode, scopes, new Set(visitedSymbolIds), parameterProofs);
|
|
11521
|
+
});
|
|
11522
|
+
const localParameterProofs = /* @__PURE__ */ new Map();
|
|
11523
|
+
let hasWrittenKatexParameter = false;
|
|
11524
|
+
if (isFunctionLike$1(localFunction.functionNode)) for (const [parameterIndex, parameter] of localFunction.functionNode.params.entries()) {
|
|
11525
|
+
if (!isNodeOfType(parameter, "Identifier")) continue;
|
|
11526
|
+
const parameterSymbol = scopes.symbolFor(parameter);
|
|
11527
|
+
const argumentProof = argumentProofs[parameterIndex];
|
|
11528
|
+
const isParameterReadOnly = parameterSymbol?.references.every((reference) => reference.flag === "read");
|
|
11529
|
+
if (parameterSymbol && argumentProof && isParameterReadOnly) localParameterProofs.set(parameterSymbol.id, argumentProof);
|
|
11530
|
+
if (argumentProof?.containsKatex && parameterSymbol && !isParameterReadOnly) hasWrittenKatexParameter = true;
|
|
11531
|
+
}
|
|
11532
|
+
const localFunctionProof = getFunctionHtmlProof(localFunction.functionNode, scopes, nextVisitedSymbolIds, localParameterProofs);
|
|
11533
|
+
if (!argumentProofs.some((proof) => proof.containsKatex) || localFunctionProof.containsKatex) return localFunctionProof;
|
|
11534
|
+
return hasWrittenKatexParameter ? UNSUPPORTED_KATEX_PROOF : UNSAFE_KATEX_PROOF;
|
|
11535
|
+
}
|
|
11536
|
+
if (isUnprovenKatexShapedRenderer(callee, scopes)) return {
|
|
11537
|
+
containsKatex: true,
|
|
11538
|
+
isConclusive: true,
|
|
11539
|
+
isSafe: false,
|
|
11540
|
+
isSafeInAttributeContext: false
|
|
11541
|
+
};
|
|
11542
|
+
if (isNodeOfType(callee, "MemberExpression")) {
|
|
11543
|
+
const receiverProof = getKatexHtmlProof(callee.object, scopes, new Set(visitedSymbolIds), parameterProofs);
|
|
11544
|
+
if (receiverProof.containsKatex) {
|
|
11545
|
+
const safeTransformProof = getSafePostTransformProof(node, receiverProof);
|
|
11546
|
+
if (safeTransformProof) return safeTransformProof;
|
|
11547
|
+
return receiverProof.isConclusive ? {
|
|
11548
|
+
containsKatex: true,
|
|
11549
|
+
isConclusive: true,
|
|
11550
|
+
isSafe: false,
|
|
11551
|
+
isSafeInAttributeContext: false
|
|
11552
|
+
} : UNSUPPORTED_KATEX_PROOF;
|
|
11553
|
+
}
|
|
11554
|
+
}
|
|
11555
|
+
const sanitizerProof = getSanitizerProof(node, scopes);
|
|
11556
|
+
if (sanitizerProof) return sanitizerProof;
|
|
11557
|
+
if (isAllOpeningAngleBracketsEscaped(node, scopes)) return SAFE_HTML_FRAGMENT_PROOF;
|
|
11558
|
+
if (isReactUseMemo(callee, scopes)) {
|
|
11559
|
+
const callback = node.arguments[0];
|
|
11560
|
+
if (!callback) return UNKNOWN_HTML_PROOF;
|
|
11561
|
+
const callbackNode = stripParenExpression(callback);
|
|
11562
|
+
if (isFunctionLike$1(callbackNode)) return getFunctionHtmlProof(callbackNode, scopes, new Set(visitedSymbolIds), parameterProofs);
|
|
11563
|
+
const localCallback = getLocalFunctionNode(callbackNode, scopes);
|
|
11564
|
+
if (!localCallback || visitedSymbolIds.has(localCallback.symbol.id)) return UNKNOWN_HTML_PROOF;
|
|
11565
|
+
const nextVisitedSymbolIds = new Set(visitedSymbolIds);
|
|
11566
|
+
nextVisitedSymbolIds.add(localCallback.symbol.id);
|
|
11567
|
+
return getFunctionHtmlProof(localCallback.functionNode, scopes, nextVisitedSymbolIds, parameterProofs);
|
|
11568
|
+
}
|
|
11569
|
+
return node.arguments.some((argument) => {
|
|
11570
|
+
const argumentNode = stripParenExpression(argument);
|
|
11571
|
+
return (isFunctionLike$1(argumentNode) ? getFunctionHtmlProof(argumentNode, scopes, new Set(visitedSymbolIds), parameterProofs) : getKatexHtmlProof(argumentNode, scopes, new Set(visitedSymbolIds), parameterProofs)).containsKatex;
|
|
11572
|
+
}) ? UNSUPPORTED_KATEX_PROOF : UNKNOWN_HTML_PROOF;
|
|
11573
|
+
};
|
|
11574
|
+
const getKatexHtmlProof = (rawNode, scopes, visitedSymbolIds, parameterProofs = /* @__PURE__ */ new Map()) => {
|
|
11575
|
+
const node = stripParenExpression(rawNode);
|
|
11576
|
+
if (isNodeOfType(node, "Literal")) return SAFE_STATIC_HTML_PROOF;
|
|
11577
|
+
if (isNodeOfType(node, "UnaryExpression") && node.operator === "void") return SAFE_STATIC_HTML_PROOF;
|
|
11578
|
+
if (isNodeOfType(node, "Identifier")) {
|
|
11579
|
+
if ((node.name === "undefined" || node.name === "NaN") && scopes.isGlobalReference(node)) return SAFE_STATIC_HTML_PROOF;
|
|
11580
|
+
const symbol = scopes.referenceFor(node)?.resolvedSymbol;
|
|
11581
|
+
const parameterProof = symbol ? parameterProofs.get(symbol.id) : void 0;
|
|
11582
|
+
if (parameterProof) return parameterProof;
|
|
11583
|
+
if (!symbol || symbol.kind !== "const" || !symbol.initializer || visitedSymbolIds.has(symbol.id)) return UNKNOWN_HTML_PROOF;
|
|
11584
|
+
const nextVisitedSymbolIds = new Set(visitedSymbolIds);
|
|
11585
|
+
nextVisitedSymbolIds.add(symbol.id);
|
|
11586
|
+
return getKatexHtmlProof(symbol.initializer, scopes, nextVisitedSymbolIds, parameterProofs);
|
|
11587
|
+
}
|
|
11588
|
+
if (isNodeOfType(node, "CallExpression")) return getKatexCallProof(node, scopes, visitedSymbolIds, parameterProofs);
|
|
11589
|
+
if (isNodeOfType(node, "TemplateLiteral")) return getTemplateLiteralProof(node, scopes, visitedSymbolIds, parameterProofs);
|
|
11590
|
+
if (isNodeOfType(node, "ConditionalExpression")) return combineHtmlProofs([getKatexHtmlProof(node.consequent, scopes, new Set(visitedSymbolIds), parameterProofs), getKatexHtmlProof(node.alternate, scopes, new Set(visitedSymbolIds), parameterProofs)]);
|
|
11591
|
+
if (isNodeOfType(node, "LogicalExpression") && node.operator === "&&") return getKatexHtmlProof(node.right, scopes, visitedSymbolIds, parameterProofs);
|
|
11592
|
+
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)]);
|
|
11593
|
+
if (isNodeOfType(node, "SequenceExpression")) {
|
|
11594
|
+
const resultExpression = node.expressions.at(-1);
|
|
11595
|
+
return resultExpression ? getKatexHtmlProof(resultExpression, scopes, visitedSymbolIds, parameterProofs) : UNKNOWN_HTML_PROOF;
|
|
11596
|
+
}
|
|
11597
|
+
return UNKNOWN_HTML_PROOF;
|
|
11598
|
+
};
|
|
11599
|
+
//#endregion
|
|
11600
|
+
//#region src/plugin/rules/security-scan/utils/get-katex-sink-proof-ranges.ts
|
|
11601
|
+
const getDangerouslySetInnerHtmlExpression = (attribute) => {
|
|
11602
|
+
let objectExpression = null;
|
|
11603
|
+
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;
|
|
11604
|
+
if (isNodeOfType(attribute, "Property") && getStaticPropertyKeyName(attribute, { allowComputedString: true }) === "dangerouslySetInnerHTML" && isNodeOfType(stripParenExpression(attribute.value), "ObjectExpression")) objectExpression = stripParenExpression(attribute.value);
|
|
11605
|
+
if (!isNodeOfType(objectExpression, "ObjectExpression")) return null;
|
|
11606
|
+
let htmlExpression = null;
|
|
11607
|
+
for (const property of objectExpression.properties) {
|
|
11608
|
+
if (isNodeOfType(property, "SpreadElement")) {
|
|
11609
|
+
htmlExpression = null;
|
|
11610
|
+
continue;
|
|
11611
|
+
}
|
|
11612
|
+
if (!isNodeOfType(property, "Property")) {
|
|
11613
|
+
htmlExpression = null;
|
|
11614
|
+
continue;
|
|
11615
|
+
}
|
|
11616
|
+
const propertyName = getStaticPropertyKeyName(property, { allowComputedString: true });
|
|
11617
|
+
if (propertyName === null) {
|
|
11618
|
+
htmlExpression = null;
|
|
11619
|
+
continue;
|
|
11620
|
+
}
|
|
11621
|
+
if (propertyName === "__html") htmlExpression = property.value;
|
|
11622
|
+
}
|
|
11623
|
+
return htmlExpression;
|
|
11624
|
+
};
|
|
11625
|
+
const collectKatexSinkProofRanges = (fileContent, filename) => {
|
|
11626
|
+
const program = parseSourceText({
|
|
11627
|
+
filename,
|
|
11628
|
+
sourceText: fileContent
|
|
11629
|
+
});
|
|
11630
|
+
if (program === null) return [];
|
|
11631
|
+
const scopes = analyzeScopes(program);
|
|
11632
|
+
registerKatexProofSource(scopes, filename, 0);
|
|
11633
|
+
const ranges = [];
|
|
11634
|
+
walkAst(program, (node) => {
|
|
11635
|
+
const htmlExpression = getDangerouslySetInnerHtmlExpression(node);
|
|
11636
|
+
if (htmlExpression === null) return;
|
|
11637
|
+
const proof = getKatexHtmlProof(htmlExpression, scopes, /* @__PURE__ */ new Set());
|
|
11638
|
+
if (!proof.containsKatex) return;
|
|
11639
|
+
const startIndex = getNodeStartIndex(node);
|
|
11640
|
+
const endIndex = getNodeEndIndex(node);
|
|
11641
|
+
if (startIndex < 0 || endIndex < 0) return;
|
|
11642
|
+
ranges.push({
|
|
11643
|
+
endIndex,
|
|
11644
|
+
proof,
|
|
11645
|
+
startIndex
|
|
11646
|
+
});
|
|
11647
|
+
});
|
|
11648
|
+
return ranges;
|
|
11649
|
+
};
|
|
11650
|
+
//#endregion
|
|
10462
11651
|
//#region src/plugin/rules/security-scan/dangerous-html-sink.ts
|
|
10463
11652
|
const DANGEROUS_HTML_PATTERN = /dangerouslySetInnerHTML|(?:\.(?:inner|outer)HTML|\[\s*["'](?:inner|outer)HTML["']\s*\])\s*[+]?=(?!=)|\.insertAdjacentHTML\s*\(|\bdocument\.write(?:ln)?\s*\(|\.(?:createContextualFragment|setHTMLUnsafe)\s*\(/;
|
|
10464
11653
|
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 +11993,7 @@ const dangerousHtmlSink = defineRule({
|
|
|
10804
11993
|
if (HIDDEN_TOOLING_DIRECTORY_PATTERN.test(file.relativePath)) return [];
|
|
10805
11994
|
if (SANITIZER_WRAPPER_PATH_PATTERN.test(file.relativePath)) return [];
|
|
10806
11995
|
if (!DANGEROUS_HTML_PATTERN.test(file.content)) return [];
|
|
11996
|
+
const katexSinkProofRanges = collectKatexSinkProofRanges(file.content, file.absolutePath);
|
|
10807
11997
|
const findings = [];
|
|
10808
11998
|
const lines = file.content.split("\n");
|
|
10809
11999
|
for (let lineIndex = 0; lineIndex < lines.length; lineIndex += 1) {
|
|
@@ -10819,6 +12009,9 @@ const dangerousHtmlSink = defineRule({
|
|
|
10819
12009
|
const terminatorIndex = valueTail.search(/[;}]/);
|
|
10820
12010
|
const valueExpression = terminatorIndex >= 0 ? valueTail.slice(0, terminatorIndex + 1) : valueTail;
|
|
10821
12011
|
const sinkIndex = lines.slice(0, lineIndex).join("\n").length + (lineIndex > 0 ? 1 : 0) + line.search(DANGEROUS_HTML_PATTERN);
|
|
12012
|
+
const katexSinkProof = katexSinkProofRanges.find((range) => sinkIndex >= range.startIndex && sinkIndex < range.endIndex)?.proof;
|
|
12013
|
+
if (katexSinkProof?.isConclusive && katexSinkProof.isSafe) continue;
|
|
12014
|
+
const hasUnsafeKatexProof = katexSinkProof?.isConclusive === true;
|
|
10822
12015
|
if (STRING_LITERAL_VALUE_PATTERN.test(valueExpression)) continue;
|
|
10823
12016
|
if (MODULE_CONSTANT_VALUE_PATTERN.test(valueExpression)) continue;
|
|
10824
12017
|
if (DOM_CONTENT_SOURCE_VALUE_PATTERN.test(valueExpression) && !valueExpression.includes("+")) {
|
|
@@ -10839,21 +12032,21 @@ const dangerousHtmlSink = defineRule({
|
|
|
10839
12032
|
if (templateInterpolations === "") continue;
|
|
10840
12033
|
const judgedExpression = templateInterpolations ?? valueExpression;
|
|
10841
12034
|
const doesJudgedExpressionCombineValues = splitTopLevelByPlus(judgedExpression).length > 1 || (templateInterpolations?.match(/\$\{/g)?.length ?? 0) > 1;
|
|
10842
|
-
if (!doesJudgedExpressionCombineValues && SANITIZER_PATTERN.test(judgedExpression)) continue;
|
|
12035
|
+
if (!hasUnsafeKatexProof && !doesJudgedExpressionCombineValues && SANITIZER_PATTERN.test(judgedExpression)) continue;
|
|
10843
12036
|
if (!doesJudgedExpressionCombineValues && ENV_CONFIG_VALUE_PATTERN.test(judgedExpression)) continue;
|
|
10844
12037
|
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;
|
|
12038
|
+
if (!hasUnsafeKatexProof && !isHtmlTainted(judgedExpression, file.content, sinkIndex, /* @__PURE__ */ new Set(), /* @__PURE__ */ new Set())) continue;
|
|
12039
|
+
if (!hasUnsafeKatexProof && ESCAPING_SERIALIZER_CALL_PATTERN.test(valueExpression)) continue;
|
|
12040
|
+
if (!hasUnsafeKatexProof && isTrustedHighlighterValue(valueExpression, file.content, sinkIndex)) continue;
|
|
10848
12041
|
if (valueIdentifier !== void 0) {
|
|
10849
12042
|
const escapedIdentifier = escapeRegExp(valueIdentifier);
|
|
10850
12043
|
const visibleDeclaration = findVisibleIdentifierDeclaration(valueIdentifier, sinkIndex, file.content);
|
|
10851
12044
|
const visibleInitializer = visibleDeclaration?.initializer;
|
|
10852
12045
|
if (!(visibleInitializer !== void 0 && (splitTopLevelByPlus(visibleInitializer).length > 1 || (visibleInitializer.match(/\$\{/g)?.length ?? 0) > 1))) {
|
|
10853
12046
|
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;
|
|
12047
|
+
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
12048
|
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;
|
|
12049
|
+
if (!hasUnsafeKatexProof && (visibleInitializer === void 0 ? fromSanitizer.test(file.content) : visibleDeclaration !== null && isDeclarationStable(valueIdentifier, visibleDeclaration, sinkIndex, file.content) && SANITIZED_ASSIGNMENT_PATTERN.test(`=${visibleInitializer}`))) continue;
|
|
10857
12050
|
}
|
|
10858
12051
|
if (new RegExp(`\\b${escapedIdentifier}\\b\\s*${DOM_CONTENT_ASSIGNMENT_PATTERN.source}`).test(file.content)) continue;
|
|
10859
12052
|
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 +13736,7 @@ const isNodeReachableWithinFunction = (node, context) => {
|
|
|
12543
13736
|
const OBSERVER_REGISTRATION_METHOD_NAME = "observe";
|
|
12544
13737
|
const CLEANUP_EFFECT_HOOK_NAMES = new Set([...EFFECT_HOOK_NAMES$1, "useInsertionEffect"]);
|
|
12545
13738
|
const REPLAYABLE_ITERATOR_COLLECTION_CACHE = /* @__PURE__ */ new WeakMap();
|
|
13739
|
+
const REACT_REF_EFFECT_ANALYSIS_CACHE = /* @__PURE__ */ new WeakMap();
|
|
12546
13740
|
const RESOURCE_NOUN_BY_KIND = {
|
|
12547
13741
|
subscribe: "subscription",
|
|
12548
13742
|
timer: "timer",
|
|
@@ -13367,7 +14561,7 @@ const hasGuardedDeferredCleanup = (callback, usage, cleanupReturns, context) =>
|
|
|
13367
14561
|
const effectHasCleanupForUsage = (callback, usage, context) => {
|
|
13368
14562
|
if (!isNodeOfType(callback, "ArrowFunctionExpression") && !isNodeOfType(callback, "FunctionExpression")) return false;
|
|
13369
14563
|
if (callback.async) return false;
|
|
13370
|
-
if (usage.kind === "subscribe" && findEnclosingFunction$1(usage.node) === callback && doesResourceResultEscape(usage.node, true) && isCleanupReturningSubscribeLikeCallExpression(usage.node)) return true;
|
|
14564
|
+
if (usage.kind === "subscribe" && findEnclosingFunction$1(usage.node) === callback && doesResourceResultEscape(usage.node, true, true, context) && isCleanupReturningSubscribeLikeCallExpression(usage.node)) return true;
|
|
13371
14565
|
if (!isNodeOfType(callback.body, "BlockStatement")) return callback.body === usage.node && isCleanupReturningSubscribeLikeCallExpression(callback.body);
|
|
13372
14566
|
const matchingCleanupReturns = [];
|
|
13373
14567
|
walkInsideStatementBlocks(callback.body, (child) => {
|
|
@@ -13553,6 +14747,8 @@ const isReleaseReachableForUsage = (releaseNode, usage, context) => {
|
|
|
13553
14747
|
const releaseFunction = findEnclosingFunction$1(releaseNode);
|
|
13554
14748
|
if (!releaseFunction) return true;
|
|
13555
14749
|
if (releaseFunction === findEnclosingFunction$1(usage.node)) return true;
|
|
14750
|
+
const usageFunction = findEnclosingFunction$1(usage.node);
|
|
14751
|
+
if (usageFunction && isFunctionLike$1(usageFunction) && getAssignedReactRefSymbol(usageFunction, context) && isCleanupFunctionReferencedByReturn(usageFunction, releaseFunction, context)) return isReactRefCallbackCleanupOwnedByEffect(usageFunction, releaseFunction, usage, context);
|
|
13556
14752
|
return isPotentiallyReachableFunction(releaseFunction, context);
|
|
13557
14753
|
};
|
|
13558
14754
|
const fileContainsReleaseForUsage = (usage, context) => {
|
|
@@ -13793,7 +14989,23 @@ const isUseSyncExternalStoreSubscribeFunction = (functionNode, context) => {
|
|
|
13793
14989
|
};
|
|
13794
14990
|
return isSubscribeBinding(bindingIdentifier);
|
|
13795
14991
|
};
|
|
13796
|
-
const
|
|
14992
|
+
const findUnconditionalReturnStatement = (expression, ownerFunction) => {
|
|
14993
|
+
let expressionRoot = findTransparentExpressionRoot(expression);
|
|
14994
|
+
while (isNodeOfType(expressionRoot.parent, "SequenceExpression") && expressionRoot.parent.expressions.at(-1) === expressionRoot) expressionRoot = findTransparentExpressionRoot(expressionRoot.parent);
|
|
14995
|
+
const returnStatement = expressionRoot.parent;
|
|
14996
|
+
return isNodeOfType(returnStatement, "ReturnStatement") && returnStatement.argument === expressionRoot && findEnclosingFunction$1(returnStatement) === ownerFunction ? returnStatement : null;
|
|
14997
|
+
};
|
|
14998
|
+
const getFinalSequenceExpressionValue = (expression) => {
|
|
14999
|
+
let finalExpression = stripParenExpression(expression);
|
|
15000
|
+
while (isNodeOfType(finalExpression, "SequenceExpression")) {
|
|
15001
|
+
const sequenceResult = finalExpression.expressions.at(-1);
|
|
15002
|
+
if (!sequenceResult) break;
|
|
15003
|
+
finalExpression = stripParenExpression(sequenceResult);
|
|
15004
|
+
}
|
|
15005
|
+
return finalExpression;
|
|
15006
|
+
};
|
|
15007
|
+
const doesResourceResultEscape = (resourceNode, allowReturnedResourceEscape, allowConciseReturnEscape, context) => {
|
|
15008
|
+
if (!allowReturnedResourceEscape) return false;
|
|
13797
15009
|
let currentNode = resourceNode;
|
|
13798
15010
|
let parentNode = currentNode.parent;
|
|
13799
15011
|
while (parentNode) {
|
|
@@ -13804,22 +15016,33 @@ const doesResourceResultEscape = (resourceNode, allowConciseReturnEscape) => {
|
|
|
13804
15016
|
parentNode = currentNode.parent;
|
|
13805
15017
|
continue;
|
|
13806
15018
|
}
|
|
15019
|
+
if (isNodeOfType(parentNode, "VariableDeclarator") && parentNode.init === currentNode && isNodeOfType(parentNode.id, "Identifier") && isNodeOfType(parentNode.parent, "VariableDeclaration") && parentNode.parent.kind === "const") {
|
|
15020
|
+
const ownerFunction = findEnclosingFunction$1(resourceNode);
|
|
15021
|
+
const resourceSymbol = context.scopes.symbolFor(parentNode.id);
|
|
15022
|
+
if (!ownerFunction || !resourceSymbol) return false;
|
|
15023
|
+
return doMatchingNodesCoverEveryPathAfterUsage(resourceNode, resourceSymbol.references.flatMap((reference) => {
|
|
15024
|
+
if (reference.flag !== "read") return [];
|
|
15025
|
+
const returnStatement = findUnconditionalReturnStatement(reference.identifier, ownerFunction);
|
|
15026
|
+
return returnStatement ? [returnStatement] : [];
|
|
15027
|
+
}), context);
|
|
15028
|
+
}
|
|
13807
15029
|
return false;
|
|
13808
15030
|
}
|
|
13809
15031
|
return false;
|
|
13810
15032
|
};
|
|
13811
|
-
const findRetainedFunctionLeak = (retainedFunction, context) => {
|
|
15033
|
+
const findRetainedFunctionLeak = (retainedFunction, context, options) => {
|
|
13812
15034
|
if (!isFunctionLike$1(retainedFunction)) return null;
|
|
13813
15035
|
const body = retainedFunction.body;
|
|
13814
15036
|
if (!body) return null;
|
|
13815
15037
|
let leak = null;
|
|
13816
|
-
const
|
|
15038
|
+
const allowReturnedResourceEscape = options?.allowReturnedResourceEscape !== false && !retainedFunction.async && !isInlineRetainedHandlerFunction(retainedFunction, context);
|
|
15039
|
+
const allowReturnedSocketEscape = allowReturnedResourceEscape && options?.requireCallableReturnedResource !== true;
|
|
13817
15040
|
const isExternalStoreSubscribeFunction = isUseSyncExternalStoreSubscribeFunction(retainedFunction, context);
|
|
13818
15041
|
const hasReleaseForUsage = (usage) => isExternalStoreSubscribeFunction ? effectHasCleanupForUsage(retainedFunction, usage, context) : fileContainsReleaseForUsage(usage, context) || hasGuaranteedRefOwnedUnmountCleanup(retainedFunction, usage, context);
|
|
13819
15042
|
walkAst(body, (child) => {
|
|
13820
15043
|
if (leak !== null) return false;
|
|
13821
15044
|
if (isFunctionLike$1(child)) return false;
|
|
13822
|
-
if (isSocketConstruction(child) && !doesResourceResultEscape(child, false)) {
|
|
15045
|
+
if (isSocketConstruction(child) && !doesResourceResultEscape(child, allowReturnedSocketEscape, false, context)) {
|
|
13823
15046
|
const socketUsage = {
|
|
13824
15047
|
kind: "socket",
|
|
13825
15048
|
node: child,
|
|
@@ -13836,14 +15059,14 @@ const findRetainedFunctionLeak = (retainedFunction, context) => {
|
|
|
13836
15059
|
}
|
|
13837
15060
|
}
|
|
13838
15061
|
if (!isNodeOfType(child, "CallExpression")) return;
|
|
13839
|
-
if (isNodeOfType(child.callee, "Identifier") && child.callee.name === "setInterval" && !doesResourceResultEscape(child,
|
|
15062
|
+
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
15063
|
const timerUsage = {
|
|
13841
15064
|
kind: "timer",
|
|
13842
15065
|
node: child,
|
|
13843
|
-
resourceName:
|
|
15066
|
+
resourceName: child.callee.name,
|
|
13844
15067
|
handleKey: findAssignedResourceKey(child, context),
|
|
13845
15068
|
receiverKey: null,
|
|
13846
|
-
registrationVerbName:
|
|
15069
|
+
registrationVerbName: child.callee.name,
|
|
13847
15070
|
eventKey: null,
|
|
13848
15071
|
handlerKey: null
|
|
13849
15072
|
};
|
|
@@ -13852,7 +15075,7 @@ const findRetainedFunctionLeak = (retainedFunction, context) => {
|
|
|
13852
15075
|
return false;
|
|
13853
15076
|
}
|
|
13854
15077
|
}
|
|
13855
|
-
if (isSubscribeOrObserveCall(child) && !doesResourceResultEscape(child,
|
|
15078
|
+
if (isSubscribeOrObserveCall(child) && (!doesResourceResultEscape(child, allowReturnedResourceEscape, allowReturnedResourceEscape, context) || options?.requireCallableReturnedResource === true && !isCleanupReturningSubscribeLikeCallExpression(child))) {
|
|
13856
15079
|
const registrationDetails = getCallRegistrationDetails(child, context);
|
|
13857
15080
|
const subscriptionUsage = {
|
|
13858
15081
|
kind: "subscribe",
|
|
@@ -13867,6 +15090,184 @@ const findRetainedFunctionLeak = (retainedFunction, context) => {
|
|
|
13867
15090
|
});
|
|
13868
15091
|
return leak;
|
|
13869
15092
|
};
|
|
15093
|
+
const getAssignedReactRefCallbackDefinition = (functionNode, context) => {
|
|
15094
|
+
if (!isFunctionLike$1(functionNode)) return null;
|
|
15095
|
+
if (functionNode.generator) return null;
|
|
15096
|
+
const functionRoot = findTransparentExpressionRoot(functionNode);
|
|
15097
|
+
const assignment = functionRoot.parent;
|
|
15098
|
+
if (!isNodeOfType(assignment, "AssignmentExpression") || assignment.operator !== "=" || assignment.right !== functionRoot) return null;
|
|
15099
|
+
const refSymbol = resolveReactRefSymbol(stripParenExpression(assignment.left), context.scopes);
|
|
15100
|
+
if (!refSymbol) return null;
|
|
15101
|
+
const componentFunction = findRenderPhaseComponentOrHook(assignment, context.scopes);
|
|
15102
|
+
if (!isFunctionLike$1(componentFunction) || findEnclosingFunction$1(assignment) !== componentFunction || findEnclosingFunction$1(refSymbol.bindingIdentifier) !== componentFunction || !isNodeReachableWithinFunction(assignment, context)) return null;
|
|
15103
|
+
return {
|
|
15104
|
+
assignmentNode: assignment,
|
|
15105
|
+
functionNode,
|
|
15106
|
+
refSymbol
|
|
15107
|
+
};
|
|
15108
|
+
};
|
|
15109
|
+
const getAssignedReactRefSymbol = (functionNode, context) => getAssignedReactRefCallbackDefinition(functionNode, context)?.refSymbol ?? null;
|
|
15110
|
+
const isExpressionReturnedFromFunction = (expression, ownerFunction, context) => {
|
|
15111
|
+
let expressionRoot = findTransparentExpressionRoot(expression);
|
|
15112
|
+
const bindingDeclarator = expressionRoot.parent;
|
|
15113
|
+
if (isNodeOfType(bindingDeclarator, "VariableDeclarator") && bindingDeclarator.init === expressionRoot && isNodeOfType(bindingDeclarator.id, "Identifier") && isNodeOfType(bindingDeclarator.parent, "VariableDeclaration") && bindingDeclarator.parent.kind === "const") {
|
|
15114
|
+
const resultSymbol = context.scopes.symbolFor(bindingDeclarator.id);
|
|
15115
|
+
if (!resultSymbol) return false;
|
|
15116
|
+
return doMatchingNodesCoverEveryPathAfterUsage(expression, resultSymbol.references.flatMap((reference) => {
|
|
15117
|
+
if (reference.flag !== "read") return [];
|
|
15118
|
+
const returnStatement = findUnconditionalReturnStatement(reference.identifier, ownerFunction);
|
|
15119
|
+
return returnStatement ? [returnStatement] : [];
|
|
15120
|
+
}), context);
|
|
15121
|
+
}
|
|
15122
|
+
while (true) {
|
|
15123
|
+
const container = expressionRoot.parent;
|
|
15124
|
+
if (isNodeOfType(container, "ConditionalExpression") && (container.consequent === expressionRoot || container.alternate === expressionRoot)) {
|
|
15125
|
+
expressionRoot = findTransparentExpressionRoot(container);
|
|
15126
|
+
continue;
|
|
15127
|
+
}
|
|
15128
|
+
if (isNodeOfType(container, "SequenceExpression") && container.expressions.at(-1) === expressionRoot) {
|
|
15129
|
+
expressionRoot = findTransparentExpressionRoot(container);
|
|
15130
|
+
continue;
|
|
15131
|
+
}
|
|
15132
|
+
if (isNodeOfType(container, "LogicalExpression") && container.right === expressionRoot) {
|
|
15133
|
+
expressionRoot = findTransparentExpressionRoot(container);
|
|
15134
|
+
continue;
|
|
15135
|
+
}
|
|
15136
|
+
break;
|
|
15137
|
+
}
|
|
15138
|
+
const returnStatement = expressionRoot.parent;
|
|
15139
|
+
return Boolean(isNodeOfType(returnStatement, "ReturnStatement") && returnStatement.argument === expressionRoot && findEnclosingFunction$1(returnStatement) === ownerFunction || isNodeOfType(ownerFunction, "ArrowFunctionExpression") && ownerFunction.body === expressionRoot);
|
|
15140
|
+
};
|
|
15141
|
+
const isReactRefCurrentCall = (node, refSymbol, context) => isNodeOfType(node, "CallExpression") && resolveReactRefSymbol(stripParenExpression(node.callee), context.scopes)?.id === refSymbol.id;
|
|
15142
|
+
const collectAssignedReactRefCallbacks = (componentFunction, context) => {
|
|
15143
|
+
const callbackDefinitionsByRefSymbolId = /* @__PURE__ */ new Map();
|
|
15144
|
+
walkAst(componentFunction.body, (child) => {
|
|
15145
|
+
if (!isFunctionLike$1(child)) return;
|
|
15146
|
+
const callbackDefinition = getAssignedReactRefCallbackDefinition(child, context);
|
|
15147
|
+
if (callbackDefinition) {
|
|
15148
|
+
const existingDefinitions = callbackDefinitionsByRefSymbolId.get(callbackDefinition.refSymbol.id) ?? [];
|
|
15149
|
+
existingDefinitions.push(callbackDefinition);
|
|
15150
|
+
callbackDefinitionsByRefSymbolId.set(callbackDefinition.refSymbol.id, existingDefinitions);
|
|
15151
|
+
}
|
|
15152
|
+
return false;
|
|
15153
|
+
});
|
|
15154
|
+
for (const [refSymbolId, callbackDefinitions] of callbackDefinitionsByRefSymbolId) {
|
|
15155
|
+
const activeDefinitions = callbackDefinitions.filter((callbackDefinition) => !doMatchingNodesCoverEveryPathAfterUsage(callbackDefinition.assignmentNode, callbackDefinitions.filter((otherDefinition) => otherDefinition !== callbackDefinition).map((otherDefinition) => otherDefinition.assignmentNode), context));
|
|
15156
|
+
if (activeDefinitions.length === 0) callbackDefinitionsByRefSymbolId.delete(refSymbolId);
|
|
15157
|
+
else callbackDefinitionsByRefSymbolId.set(refSymbolId, activeDefinitions);
|
|
15158
|
+
}
|
|
15159
|
+
return callbackDefinitionsByRefSymbolId;
|
|
15160
|
+
};
|
|
15161
|
+
const collectUndominatedReactRefCalls = (ownerFunction, refSymbol, context) => {
|
|
15162
|
+
if (!isFunctionLike$1(ownerFunction)) return [];
|
|
15163
|
+
const refWrites = [];
|
|
15164
|
+
const refCalls = [];
|
|
15165
|
+
walkAst(ownerFunction.body, (child) => {
|
|
15166
|
+
if (child !== ownerFunction.body && isFunctionLike$1(child)) return false;
|
|
15167
|
+
if (isNodeOfType(child, "AssignmentExpression") && isNodeReachableWithinFunction(child, context) && resolveReactRefSymbol(stripParenExpression(child.left), context.scopes)?.id === refSymbol.id) refWrites.push(child);
|
|
15168
|
+
if (isReactRefCurrentCall(child, refSymbol, context) && isNodeReachableWithinFunction(child, context)) refCalls.push(child);
|
|
15169
|
+
});
|
|
15170
|
+
return refCalls.filter((refCall) => !doMatchingNodesCoverEveryPathBeforeUsage(refCall, refWrites, ownerFunction, context));
|
|
15171
|
+
};
|
|
15172
|
+
const mergeReactRefEffectUsage = (usageByRefSymbolId, refSymbolId, doesEffectOwnResult) => {
|
|
15173
|
+
const existingUsage = usageByRefSymbolId.get(refSymbolId);
|
|
15174
|
+
if (!existingUsage) {
|
|
15175
|
+
usageByRefSymbolId.set(refSymbolId, { doesEffectOwnEveryResult: doesEffectOwnResult });
|
|
15176
|
+
return true;
|
|
15177
|
+
}
|
|
15178
|
+
if (!existingUsage.doesEffectOwnEveryResult || doesEffectOwnResult) return false;
|
|
15179
|
+
existingUsage.doesEffectOwnEveryResult = false;
|
|
15180
|
+
return true;
|
|
15181
|
+
};
|
|
15182
|
+
const collectReactRefEffectAnalysis = (componentFunction, context) => {
|
|
15183
|
+
let analysisByComponent = REACT_REF_EFFECT_ANALYSIS_CACHE.get(context);
|
|
15184
|
+
if (!analysisByComponent) {
|
|
15185
|
+
analysisByComponent = /* @__PURE__ */ new WeakMap();
|
|
15186
|
+
REACT_REF_EFFECT_ANALYSIS_CACHE.set(context, analysisByComponent);
|
|
15187
|
+
}
|
|
15188
|
+
const cachedAnalysis = analysisByComponent.get(componentFunction);
|
|
15189
|
+
if (cachedAnalysis) return cachedAnalysis;
|
|
15190
|
+
const callbackDefinitionsByRefSymbolId = collectAssignedReactRefCallbacks(componentFunction, context);
|
|
15191
|
+
const usageByRefSymbolId = /* @__PURE__ */ new Map();
|
|
15192
|
+
walkAst(componentFunction.body, (child) => {
|
|
15193
|
+
if (child !== componentFunction.body && isFunctionLike$1(child)) return false;
|
|
15194
|
+
if (!isNodeOfType(child, "CallExpression") || findEnclosingFunction$1(child) !== componentFunction || !isReactApiCall(child, CLEANUP_EFFECT_HOOK_NAMES, context.scopes, { allowGlobalReactNamespace: true })) return;
|
|
15195
|
+
const effectCallback = getEffectCallback(child);
|
|
15196
|
+
if (!isFunctionLike$1(effectCallback)) return;
|
|
15197
|
+
for (const callbackDefinitions of callbackDefinitionsByRefSymbolId.values()) {
|
|
15198
|
+
const refSymbol = callbackDefinitions[0]?.refSymbol;
|
|
15199
|
+
if (!refSymbol) continue;
|
|
15200
|
+
for (const refCall of collectUndominatedReactRefCalls(effectCallback, refSymbol, context)) mergeReactRefEffectUsage(usageByRefSymbolId, refSymbol.id, !effectCallback.async && isExpressionReturnedFromFunction(refCall, effectCallback, context));
|
|
15201
|
+
}
|
|
15202
|
+
});
|
|
15203
|
+
let didUsageChange = true;
|
|
15204
|
+
while (didUsageChange) {
|
|
15205
|
+
didUsageChange = false;
|
|
15206
|
+
for (const callbackDefinitions of callbackDefinitionsByRefSymbolId.values()) {
|
|
15207
|
+
const ownerRefSymbol = callbackDefinitions[0]?.refSymbol;
|
|
15208
|
+
if (!ownerRefSymbol) continue;
|
|
15209
|
+
const ownerUsage = usageByRefSymbolId.get(ownerRefSymbol.id);
|
|
15210
|
+
if (!ownerUsage) continue;
|
|
15211
|
+
for (const callbackDefinition of callbackDefinitions) for (const targetDefinitions of callbackDefinitionsByRefSymbolId.values()) {
|
|
15212
|
+
const targetRefSymbol = targetDefinitions[0]?.refSymbol;
|
|
15213
|
+
if (!targetRefSymbol) continue;
|
|
15214
|
+
for (const refCall of collectUndominatedReactRefCalls(callbackDefinition.functionNode, targetRefSymbol, context)) {
|
|
15215
|
+
const doesEffectOwnResult = ownerUsage.doesEffectOwnEveryResult && !callbackDefinition.functionNode.async && isExpressionReturnedFromFunction(refCall, callbackDefinition.functionNode, context);
|
|
15216
|
+
if (mergeReactRefEffectUsage(usageByRefSymbolId, targetRefSymbol.id, doesEffectOwnResult)) didUsageChange = true;
|
|
15217
|
+
}
|
|
15218
|
+
}
|
|
15219
|
+
}
|
|
15220
|
+
}
|
|
15221
|
+
const analysis = {
|
|
15222
|
+
callbackDefinitionsByRefSymbolId,
|
|
15223
|
+
usageByRefSymbolId
|
|
15224
|
+
};
|
|
15225
|
+
analysisByComponent.set(componentFunction, analysis);
|
|
15226
|
+
return analysis;
|
|
15227
|
+
};
|
|
15228
|
+
const getReactRefEffectUsage = (retainedFunction, context) => {
|
|
15229
|
+
if (!isFunctionLike$1(retainedFunction)) return null;
|
|
15230
|
+
const callbackDefinition = getAssignedReactRefCallbackDefinition(retainedFunction, context);
|
|
15231
|
+
const componentFunction = findRenderPhaseComponentOrHook(retainedFunction, context.scopes);
|
|
15232
|
+
if (!callbackDefinition || !isFunctionLike$1(componentFunction)) return null;
|
|
15233
|
+
const analysis = collectReactRefEffectAnalysis(componentFunction, context);
|
|
15234
|
+
if (!analysis.callbackDefinitionsByRefSymbolId.get(callbackDefinition.refSymbol.id)?.some((activeDefinition) => activeDefinition.functionNode === retainedFunction)) return null;
|
|
15235
|
+
return analysis.usageByRefSymbolId.get(callbackDefinition.refSymbol.id) ?? null;
|
|
15236
|
+
};
|
|
15237
|
+
const isReactRefCallbackCleanupOwnedByEffect = (retainedFunction, cleanupFunction, usage, context) => {
|
|
15238
|
+
if (!isFunctionLike$1(retainedFunction) || retainedFunction.async || getReactRefEffectUsage(retainedFunction, context)?.doesEffectOwnEveryResult !== true) return false;
|
|
15239
|
+
if (!isNodeOfType(retainedFunction.body, "BlockStatement")) return false;
|
|
15240
|
+
const doesReturnedCleanupCallFunction = (returnedValue) => {
|
|
15241
|
+
const returnedCleanupFunction = resolveRefOwnedCleanupFunction(getFinalSequenceExpressionValue(returnedValue), context);
|
|
15242
|
+
if (!returnedCleanupFunction) return false;
|
|
15243
|
+
if (returnedCleanupFunction === cleanupFunction) return true;
|
|
15244
|
+
if (!isFunctionLike$1(returnedCleanupFunction)) return false;
|
|
15245
|
+
const matchingCalls = [];
|
|
15246
|
+
walkAst(returnedCleanupFunction.body, (child) => {
|
|
15247
|
+
if (child !== returnedCleanupFunction.body && isFunctionLike$1(child)) return false;
|
|
15248
|
+
if (isNodeOfType(child, "CallExpression") && resolveRefOwnedCleanupFunction(child.callee, context) === cleanupFunction) matchingCalls.push(child);
|
|
15249
|
+
});
|
|
15250
|
+
return doMatchingNodesCoverEveryPathFromFunctionEntry(returnedCleanupFunction, matchingCalls, context);
|
|
15251
|
+
};
|
|
15252
|
+
const matchingReturns = [];
|
|
15253
|
+
walkInsideStatementBlocks(retainedFunction.body, (child) => {
|
|
15254
|
+
if (isNodeOfType(child, "ReturnStatement") && child.argument && doesReturnedCleanupCallFunction(child.argument)) matchingReturns.push(child);
|
|
15255
|
+
});
|
|
15256
|
+
return doMatchingNodesCoverEveryPathAfterUsage(usage.node, matchingReturns, context);
|
|
15257
|
+
};
|
|
15258
|
+
const isCleanupFunctionReferencedByReturn = (ownerFunction, cleanupFunction, context) => {
|
|
15259
|
+
if (!isFunctionLike$1(ownerFunction) || !isNodeOfType(ownerFunction.body, "BlockStatement")) return false;
|
|
15260
|
+
let isReferencedByReturn = false;
|
|
15261
|
+
walkInsideStatementBlocks(ownerFunction.body, (child) => {
|
|
15262
|
+
if (isReferencedByReturn || !isNodeOfType(child, "ReturnStatement") || !child.argument) return;
|
|
15263
|
+
walkAst(child.argument, (returnedChild) => {
|
|
15264
|
+
if (resolveRefOwnedCleanupFunction(returnedChild, context) !== cleanupFunction) return;
|
|
15265
|
+
isReferencedByReturn = true;
|
|
15266
|
+
return false;
|
|
15267
|
+
});
|
|
15268
|
+
});
|
|
15269
|
+
return isReferencedByReturn;
|
|
15270
|
+
};
|
|
13870
15271
|
const isRetainedComponentScopeFunction = (functionNode) => {
|
|
13871
15272
|
if (isNodeOfType(functionNode, "FunctionDeclaration")) return enclosingComponentOrHookName(functionNode) !== null;
|
|
13872
15273
|
if (!isNodeOfType(functionNode, "ArrowFunctionExpression") && !isNodeOfType(functionNode, "FunctionExpression")) return false;
|
|
@@ -13901,8 +15302,14 @@ const effectNeedsCleanup = defineRule({
|
|
|
13901
15302
|
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
15303
|
create: (context) => {
|
|
13903
15304
|
const reportRetainedLeak = (retainedFunction) => {
|
|
13904
|
-
|
|
13905
|
-
|
|
15305
|
+
const refEffectUsage = getReactRefEffectUsage(retainedFunction, context);
|
|
15306
|
+
if (!refEffectUsage && !isPotentiallyReachableFunction(retainedFunction, context)) return;
|
|
15307
|
+
const leak = findRetainedFunctionLeak(retainedFunction, context, refEffectUsage ? {
|
|
15308
|
+
allowReturnedResourceEscape: refEffectUsage.doesEffectOwnEveryResult,
|
|
15309
|
+
allowReturnedTimerEscape: false,
|
|
15310
|
+
includeOneShotTimers: true,
|
|
15311
|
+
requireCallableReturnedResource: true
|
|
15312
|
+
} : void 0);
|
|
13906
15313
|
if (!leak) return;
|
|
13907
15314
|
const resourceNoun = RESOURCE_NOUN_BY_KIND[leak.kind];
|
|
13908
15315
|
context.report({
|
|
@@ -13935,10 +15342,10 @@ const effectNeedsCleanup = defineRule({
|
|
|
13935
15342
|
if (isRetainedComponentScopeFunction(node)) reportRetainedLeak(node);
|
|
13936
15343
|
},
|
|
13937
15344
|
ArrowFunctionExpression(node) {
|
|
13938
|
-
if (isRetainedComponentScopeFunction(node) || isInlineRetainedHandlerFunction(node, context)) reportRetainedLeak(node);
|
|
15345
|
+
if (isRetainedComponentScopeFunction(node) || isInlineRetainedHandlerFunction(node, context) || getAssignedReactRefSymbol(node, context)) reportRetainedLeak(node);
|
|
13939
15346
|
},
|
|
13940
15347
|
FunctionExpression(node) {
|
|
13941
|
-
if (isRetainedComponentScopeFunction(node) || isInlineRetainedHandlerFunction(node, context)) reportRetainedLeak(node);
|
|
15348
|
+
if (isRetainedComponentScopeFunction(node) || isInlineRetainedHandlerFunction(node, context) || getAssignedReactRefSymbol(node, context)) reportRetainedLeak(node);
|
|
13942
15349
|
}
|
|
13943
15350
|
};
|
|
13944
15351
|
}
|
|
@@ -17340,6 +18747,7 @@ const iframeHasTitle = defineRule({
|
|
|
17340
18747
|
recommendation: "Add a descriptive `title` so screen reader users know what the embedded frame contains.",
|
|
17341
18748
|
category: "Accessibility",
|
|
17342
18749
|
create: (context) => ({ JSXOpeningElement(node) {
|
|
18750
|
+
if (isLocalTestScaffoldJsx(node, context)) return;
|
|
17343
18751
|
const tag = getElementType(node, context.settings);
|
|
17344
18752
|
if (tag !== "iframe") return;
|
|
17345
18753
|
if (isInsideStaticallyHiddenJsxSubtree(node, context.scopes)) return;
|
|
@@ -17860,6 +19268,7 @@ const interactiveSupportsFocus = defineRule({
|
|
|
17860
19268
|
const settings = resolveSettings$37(context.settings);
|
|
17861
19269
|
const tabbableSet = new Set(settings.tabbable);
|
|
17862
19270
|
return { JSXOpeningElement(node) {
|
|
19271
|
+
if (isLocalTestScaffoldJsx(node, context)) return;
|
|
17863
19272
|
if (node.attributes.length === 0) return;
|
|
17864
19273
|
if (hasJsxSpreadAttribute$1(node.attributes)) return;
|
|
17865
19274
|
const roleAttribute = hasJsxPropIgnoreCase(node.attributes, "role");
|
|
@@ -18573,7 +19982,7 @@ const scanPerIterationLayoutReads = (body) => {
|
|
|
18573
19982
|
hasDeliberateForcedReflow
|
|
18574
19983
|
};
|
|
18575
19984
|
};
|
|
18576
|
-
const getNodeStart
|
|
19985
|
+
const getNodeStart = (node) => {
|
|
18577
19986
|
const withRange = node;
|
|
18578
19987
|
return withRange.range ? withRange.range[0] : -1;
|
|
18579
19988
|
};
|
|
@@ -18605,7 +20014,7 @@ const hasAttachmentBefore = (scopeOwner, elementName, beforeStart) => {
|
|
|
18605
20014
|
if (!isNodeOfType(child, "CallExpression")) return;
|
|
18606
20015
|
const callee = child.callee;
|
|
18607
20016
|
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
|
|
20017
|
+
if (child.arguments.some((argument) => isNodeOfType(argument, "Identifier") && argument.name === elementName) && getNodeStart(child) < beforeStart) {
|
|
18609
20018
|
foundAttachment = true;
|
|
18610
20019
|
return false;
|
|
18611
20020
|
}
|
|
@@ -18627,7 +20036,7 @@ const isProvablyDetachedAtWrite = (styleWriteStatement) => {
|
|
|
18627
20036
|
const elementExpression = assignment.left.object.object;
|
|
18628
20037
|
const creationRoot = resolveDetachedCreationRoot(elementExpression, 0);
|
|
18629
20038
|
if (!creationRoot) return false;
|
|
18630
|
-
return !hasAttachmentBefore(creationRoot.scopeOwner, creationRoot.rootName, getNodeStart
|
|
20039
|
+
return !hasAttachmentBefore(creationRoot.scopeOwner, creationRoot.rootName, getNodeStart(styleWriteStatement));
|
|
18631
20040
|
};
|
|
18632
20041
|
const jsBatchDomCss = defineRule({
|
|
18633
20042
|
id: "js-batch-dom-css",
|
|
@@ -19350,7 +20759,7 @@ const globSyncReturnsStringPaths = (node, context) => {
|
|
|
19350
20759
|
const callee = stripParenExpression(node.callee);
|
|
19351
20760
|
let isGlobSyncImport = false;
|
|
19352
20761
|
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");
|
|
20762
|
+
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
20763
|
if (!isGlobSyncImport) return false;
|
|
19355
20764
|
const options = node.arguments[1];
|
|
19356
20765
|
if (!options) return true;
|
|
@@ -19996,6 +21405,28 @@ const jsLengthCheckFirst = defineRule({
|
|
|
19996
21405
|
} })
|
|
19997
21406
|
});
|
|
19998
21407
|
//#endregion
|
|
21408
|
+
//#region src/plugin/utils/is-proven-global-namespace-reference.ts
|
|
21409
|
+
const isProvenGlobalObjectReference = (expression, scopes, visitedSymbolIds = /* @__PURE__ */ new Set()) => {
|
|
21410
|
+
const strippedExpression = stripParenExpression(expression);
|
|
21411
|
+
if (!isNodeOfType(strippedExpression, "Identifier")) return false;
|
|
21412
|
+
if ((strippedExpression.name === "globalThis" || strippedExpression.name === "window" || strippedExpression.name === "self" || strippedExpression.name === "global") && scopes.isGlobalReference(strippedExpression)) return true;
|
|
21413
|
+
const symbol = scopes.symbolFor(strippedExpression);
|
|
21414
|
+
if (!symbol?.initializer || symbol.kind !== "const" || visitedSymbolIds.has(symbol.id)) return false;
|
|
21415
|
+
visitedSymbolIds.add(symbol.id);
|
|
21416
|
+
return isProvenGlobalObjectReference(symbol.initializer, scopes, visitedSymbolIds);
|
|
21417
|
+
};
|
|
21418
|
+
const isProvenGlobalNamespaceReference = (expression, namespaceName, scopes, visitedSymbolIds = /* @__PURE__ */ new Set()) => {
|
|
21419
|
+
const strippedExpression = stripParenExpression(expression);
|
|
21420
|
+
if (isNodeOfType(strippedExpression, "Identifier")) {
|
|
21421
|
+
if (strippedExpression.name === namespaceName && scopes.isGlobalReference(strippedExpression)) return true;
|
|
21422
|
+
const symbol = scopes.symbolFor(strippedExpression);
|
|
21423
|
+
if (!symbol?.initializer || symbol.kind !== "const" || visitedSymbolIds.has(symbol.id)) return false;
|
|
21424
|
+
visitedSymbolIds.add(symbol.id);
|
|
21425
|
+
return isProvenGlobalNamespaceReference(symbol.initializer, namespaceName, scopes, visitedSymbolIds);
|
|
21426
|
+
}
|
|
21427
|
+
return isNodeOfType(strippedExpression, "MemberExpression") && getStaticPropertyName(strippedExpression) === namespaceName && isProvenGlobalObjectReference(strippedExpression.object, scopes);
|
|
21428
|
+
};
|
|
21429
|
+
//#endregion
|
|
19999
21430
|
//#region src/plugin/rules/js-performance/js-min-max-loop.ts
|
|
20000
21431
|
const builtinMutationByProgram = /* @__PURE__ */ new WeakMap();
|
|
20001
21432
|
const RUNTIMELESS_SYMBOL_KINDS = new Set(["ts-interface", "ts-type-alias"]);
|
|
@@ -20052,26 +21483,6 @@ const isSafeFreshNumericArray = (arrayExpression) => {
|
|
|
20052
21483
|
}
|
|
20053
21484
|
return !(didFindPositiveZero && didFindNegativeZero);
|
|
20054
21485
|
};
|
|
20055
|
-
const isGlobalObjectReference = (expression, scopes, visitedSymbols = /* @__PURE__ */ new Set()) => {
|
|
20056
|
-
const strippedExpression = stripParenExpression(expression);
|
|
20057
|
-
if (!isNodeOfType(strippedExpression, "Identifier")) return false;
|
|
20058
|
-
if ((strippedExpression.name === "globalThis" || strippedExpression.name === "window" || strippedExpression.name === "self" || strippedExpression.name === "global") && scopes.isGlobalReference(strippedExpression)) return true;
|
|
20059
|
-
const symbol = scopes.symbolFor(strippedExpression);
|
|
20060
|
-
if (!symbol?.initializer || symbol.kind !== "const" || visitedSymbols.has(symbol.id)) return false;
|
|
20061
|
-
visitedSymbols.add(symbol.id);
|
|
20062
|
-
return isGlobalObjectReference(symbol.initializer, scopes, visitedSymbols);
|
|
20063
|
-
};
|
|
20064
|
-
const resolvesToGlobalNamespace = (expression, namespaceName, scopes, visitedSymbols = /* @__PURE__ */ new Set()) => {
|
|
20065
|
-
const strippedExpression = stripParenExpression(expression);
|
|
20066
|
-
if (isNodeOfType(strippedExpression, "Identifier")) {
|
|
20067
|
-
if (strippedExpression.name === namespaceName && scopes.isGlobalReference(strippedExpression)) return true;
|
|
20068
|
-
const symbol = scopes.symbolFor(strippedExpression);
|
|
20069
|
-
if (!symbol?.initializer || symbol.kind !== "const" || visitedSymbols.has(symbol.id)) return false;
|
|
20070
|
-
visitedSymbols.add(symbol.id);
|
|
20071
|
-
return resolvesToGlobalNamespace(symbol.initializer, namespaceName, scopes, visitedSymbols);
|
|
20072
|
-
}
|
|
20073
|
-
return isNodeOfType(strippedExpression, "MemberExpression") && getStaticPropertyName(strippedExpression) === namespaceName && isGlobalObjectReference(strippedExpression.object, scopes);
|
|
20074
|
-
};
|
|
20075
21486
|
const resolvesToGlobalMethod = (expression, namespaceName, methodNames, scopes, visitedSymbols = /* @__PURE__ */ new Set()) => {
|
|
20076
21487
|
const strippedExpression = stripParenExpression(expression);
|
|
20077
21488
|
if (isNodeOfType(strippedExpression, "Identifier")) {
|
|
@@ -20080,7 +21491,7 @@ const resolvesToGlobalMethod = (expression, namespaceName, methodNames, scopes,
|
|
|
20080
21491
|
visitedSymbols.add(symbol.id);
|
|
20081
21492
|
return resolvesToGlobalMethod(symbol.initializer, namespaceName, methodNames, scopes, visitedSymbols);
|
|
20082
21493
|
}
|
|
20083
|
-
return isNodeOfType(strippedExpression, "MemberExpression") && methodNames.has(getStaticPropertyName(strippedExpression) ?? "") &&
|
|
21494
|
+
return isNodeOfType(strippedExpression, "MemberExpression") && methodNames.has(getStaticPropertyName(strippedExpression) ?? "") && isProvenGlobalNamespaceReference(strippedExpression.object, namespaceName, scopes);
|
|
20084
21495
|
};
|
|
20085
21496
|
const resolvesToNativeArrayPrototype = (expression, scopes, visitedSymbols = /* @__PURE__ */ new Set()) => {
|
|
20086
21497
|
const strippedExpression = stripParenExpression(expression);
|
|
@@ -20092,7 +21503,7 @@ const resolvesToNativeArrayPrototype = (expression, scopes, visitedSymbols = /*
|
|
|
20092
21503
|
}
|
|
20093
21504
|
if (isNodeOfType(strippedExpression, "MemberExpression")) {
|
|
20094
21505
|
const propertyName = getStaticPropertyName(strippedExpression);
|
|
20095
|
-
if (propertyName === "prototype") return
|
|
21506
|
+
if (propertyName === "prototype") return isProvenGlobalNamespaceReference(strippedExpression.object, "Array", scopes);
|
|
20096
21507
|
return propertyName === "__proto__" && isNodeOfType(stripParenExpression(strippedExpression.object), "ArrayExpression");
|
|
20097
21508
|
}
|
|
20098
21509
|
if (!isNodeOfType(strippedExpression, "CallExpression")) return false;
|
|
@@ -20103,23 +21514,23 @@ const resolvesToNativeArrayPrototype = (expression, scopes, visitedSymbols = /*
|
|
|
20103
21514
|
const isGlobalNamespaceReplacementTarget = (target, namespaceName, scopes) => {
|
|
20104
21515
|
const strippedTarget = stripParenExpression(target);
|
|
20105
21516
|
if (isNodeOfType(strippedTarget, "Identifier")) return strippedTarget.name === namespaceName && scopes.isGlobalReference(strippedTarget);
|
|
20106
|
-
return isNodeOfType(strippedTarget, "MemberExpression") && getStaticPropertyName(strippedTarget) === namespaceName &&
|
|
21517
|
+
return isNodeOfType(strippedTarget, "MemberExpression") && getStaticPropertyName(strippedTarget) === namespaceName && isProvenGlobalObjectReference(strippedTarget.object, scopes);
|
|
20107
21518
|
};
|
|
20108
21519
|
const isUnsafeBuiltinMemberTarget = (target, targetFunction, scopes) => {
|
|
20109
21520
|
const strippedTarget = stripParenExpression(target);
|
|
20110
21521
|
if (!isNodeOfType(strippedTarget, "MemberExpression")) return false;
|
|
20111
21522
|
const propertyName = getStaticPropertyName(strippedTarget);
|
|
20112
21523
|
if (resolvesToNativeArrayPrototype(strippedTarget.object, scopes)) return propertyName === null || propertyName === "sort";
|
|
20113
|
-
if (
|
|
20114
|
-
return
|
|
21524
|
+
if (isProvenGlobalNamespaceReference(strippedTarget.object, "Math", scopes)) return propertyName === null || propertyName === targetFunction;
|
|
21525
|
+
return isProvenGlobalObjectReference(strippedTarget.object, scopes) && (propertyName === null || propertyName === "Math");
|
|
20115
21526
|
};
|
|
20116
21527
|
const isUnsafeBuiltinMutationApiCall = (callExpression, targetFunction, scopes) => {
|
|
20117
21528
|
const target = callExpression.arguments[0];
|
|
20118
21529
|
if (!target) return false;
|
|
20119
21530
|
let propertyName = null;
|
|
20120
21531
|
if (resolvesToNativeArrayPrototype(target, scopes)) propertyName = "sort";
|
|
20121
|
-
else if (
|
|
20122
|
-
else if (
|
|
21532
|
+
else if (isProvenGlobalNamespaceReference(target, "Math", scopes)) propertyName = targetFunction;
|
|
21533
|
+
else if (isProvenGlobalObjectReference(target, scopes)) propertyName = "Math";
|
|
20123
21534
|
if (!propertyName) return false;
|
|
20124
21535
|
const canObjectExpressionSetProperty = (properties) => {
|
|
20125
21536
|
if (!isNodeOfType(properties, "ObjectExpression")) return true;
|
|
@@ -20170,7 +21581,7 @@ const hasUnsafeMathBinding = (node, scopes) => {
|
|
|
20170
21581
|
let scope = scopes.scopeFor(node);
|
|
20171
21582
|
while (scope) {
|
|
20172
21583
|
const symbol = scope.symbolsByName.get("Math");
|
|
20173
|
-
if (symbol && !RUNTIMELESS_SYMBOL_KINDS.has(symbol.kind)) return !(symbol.kind === "const" && symbol.initializer &&
|
|
21584
|
+
if (symbol && !RUNTIMELESS_SYMBOL_KINDS.has(symbol.kind)) return !(symbol.kind === "const" && symbol.initializer && isProvenGlobalNamespaceReference(symbol.initializer, "Math", scopes));
|
|
20174
21585
|
scope = scope.parent;
|
|
20175
21586
|
}
|
|
20176
21587
|
return false;
|
|
@@ -25730,6 +27141,7 @@ const mediaHasCaption = defineRule({
|
|
|
25730
27141
|
create: (context) => {
|
|
25731
27142
|
const settings = resolveSettings$23(context.settings);
|
|
25732
27143
|
return { JSXOpeningElement(node) {
|
|
27144
|
+
if (isLocalTestScaffoldJsx(node, context)) return;
|
|
25733
27145
|
const tag = getElementType(node, context.settings);
|
|
25734
27146
|
if (!(settings.audio.has(tag) || settings.video.has(tag))) return;
|
|
25735
27147
|
if (evaluateMuted(hasJsxPropIgnoreCase(node.attributes, "muted")) === true) return;
|
|
@@ -25797,6 +27209,7 @@ const mouseEventsHaveKeyEvents = defineRule({
|
|
|
25797
27209
|
create: (context) => {
|
|
25798
27210
|
const settings = resolveSettings$22(context.settings);
|
|
25799
27211
|
return { JSXOpeningElement(node) {
|
|
27212
|
+
if (isLocalTestScaffoldJsx(node, context)) return;
|
|
25800
27213
|
const tag = getElementType(node, context.settings);
|
|
25801
27214
|
if (!HTML_TAGS.has(tag)) return;
|
|
25802
27215
|
for (const handler of settings.hoverInHandlers) {
|
|
@@ -28467,7 +29880,11 @@ const isProvenNativeReadMethod = (ref, methodName) => Boolean(ref.resolved?.defs
|
|
|
28467
29880
|
}));
|
|
28468
29881
|
//#endregion
|
|
28469
29882
|
//#region src/plugin/rules/state-and-effects/utils/effect/react.ts
|
|
28470
|
-
const
|
|
29883
|
+
const KNOWN_COMPONENT_WRAPPER_NAMES = new Set([
|
|
29884
|
+
"memo",
|
|
29885
|
+
"forwardRef",
|
|
29886
|
+
"observer"
|
|
29887
|
+
]);
|
|
28471
29888
|
const startsWithUppercase = (name) => Boolean(name && name.length > 0 && name[0] >= "A" && name[0] <= "Z");
|
|
28472
29889
|
const isReactFunctionalComponent = (node) => {
|
|
28473
29890
|
if (!node) return false;
|
|
@@ -28489,7 +29906,7 @@ const isReactFunctionalHOC = (analysis, node) => {
|
|
|
28489
29906
|
const isWrappedInline = () => {
|
|
28490
29907
|
if (!isNodeOfType(init, "CallExpression")) return false;
|
|
28491
29908
|
if (!isNodeOfType(init.callee, "Identifier")) return false;
|
|
28492
|
-
if (
|
|
29909
|
+
if (KNOWN_COMPONENT_WRAPPER_NAMES.has(init.callee.name)) return false;
|
|
28493
29910
|
const firstArg = init.arguments?.[0];
|
|
28494
29911
|
if (!firstArg) return false;
|
|
28495
29912
|
return isNodeOfType(firstArg, "ArrowFunctionExpression") || isNodeOfType(firstArg, "FunctionExpression");
|
|
@@ -28509,7 +29926,7 @@ const isReactFunctionalHOC = (analysis, node) => {
|
|
|
28509
29926
|
if (!args.includes(refId)) continue;
|
|
28510
29927
|
const callee = parent.callee;
|
|
28511
29928
|
const calleeName = isNodeOfType(callee, "Identifier") ? callee.name : isNodeOfType(callee, "CallExpression") && isNodeOfType(callee.callee, "Identifier") ? callee.callee.name : null;
|
|
28512
|
-
if (calleeName != null && !
|
|
29929
|
+
if (calleeName != null && !KNOWN_COMPONENT_WRAPPER_NAMES.has(calleeName)) return true;
|
|
28513
29930
|
}
|
|
28514
29931
|
return false;
|
|
28515
29932
|
};
|
|
@@ -31932,7 +33349,7 @@ const isReactDomCreatePortalCall = (node, scopes) => {
|
|
|
31932
33349
|
if (!isNodeOfType(callee, "MemberExpression") || callee.computed || !isNodeOfType(callee.object, "Identifier") || !isNodeOfType(callee.property, "Identifier") || callee.property.name !== "createPortal") return false;
|
|
31933
33350
|
const symbol = scopes.symbolFor(callee.object);
|
|
31934
33351
|
if (!symbol || symbol.kind !== "import") return false;
|
|
31935
|
-
return isDefaultImportFromModule(callee.object, callee.object.name, "react-dom") || isNamespaceImportFromModule(callee.object, callee.object.name, "react-dom");
|
|
33352
|
+
return isDefaultImportFromModule(callee.object, callee.object.name, "react-dom") || isNamespaceImportFromModule$1(callee.object, callee.object.name, "react-dom");
|
|
31936
33353
|
};
|
|
31937
33354
|
const containsRenderOutput$1 = (rootNode, scopes) => {
|
|
31938
33355
|
let hasRenderOutput = false;
|
|
@@ -32937,16 +34354,7 @@ const isProvenIntrinsicJsxElement = (openingElement, scopes) => {
|
|
|
32937
34354
|
return isIntrinsicValue(openingElement.name);
|
|
32938
34355
|
};
|
|
32939
34356
|
//#endregion
|
|
32940
|
-
//#region src/plugin/
|
|
32941
|
-
const pathStartsWith$1 = (propertyPath, prefix) => prefix.every((propertyName, index) => propertyPath[index] === propertyName);
|
|
32942
|
-
const collectMemberExpression = (identifier) => {
|
|
32943
|
-
let expression = findTransparentExpressionRoot(identifier);
|
|
32944
|
-
while (expression.parent && isNodeOfType(expression.parent, "MemberExpression") && expression.parent.object === expression) {
|
|
32945
|
-
if (!getStaticPropertyName(expression.parent)) return null;
|
|
32946
|
-
expression = findTransparentExpressionRoot(expression.parent);
|
|
32947
|
-
}
|
|
32948
|
-
return expression;
|
|
32949
|
-
};
|
|
34357
|
+
//#region src/plugin/utils/is-inline-intrinsic-ref-callback.ts
|
|
32950
34358
|
const isInlineIntrinsicRefCallback = (functionNode, scopes) => {
|
|
32951
34359
|
const functionExpression = findTransparentExpressionRoot(functionNode);
|
|
32952
34360
|
if (!isFunctionLike$1(functionExpression) || functionExpression.async || functionExpression.generator) return false;
|
|
@@ -32957,6 +34365,17 @@ const isInlineIntrinsicRefCallback = (functionNode, scopes) => {
|
|
|
32957
34365
|
const openingElement = attribute.parent;
|
|
32958
34366
|
return Boolean(openingElement && isNodeOfType(openingElement, "JSXOpeningElement") && isProvenIntrinsicJsxElement(openingElement, scopes));
|
|
32959
34367
|
};
|
|
34368
|
+
//#endregion
|
|
34369
|
+
//#region src/plugin/rules/react-builtins/is-safe-create-ref-callback-current-write.ts
|
|
34370
|
+
const pathStartsWith$1 = (propertyPath, prefix) => prefix.every((propertyName, index) => propertyPath[index] === propertyName);
|
|
34371
|
+
const collectMemberExpression = (identifier) => {
|
|
34372
|
+
let expression = findTransparentExpressionRoot(identifier);
|
|
34373
|
+
while (expression.parent && isNodeOfType(expression.parent, "MemberExpression") && expression.parent.object === expression) {
|
|
34374
|
+
if (!getStaticPropertyName(expression.parent)) return null;
|
|
34375
|
+
expression = findTransparentExpressionRoot(expression.parent);
|
|
34376
|
+
}
|
|
34377
|
+
return expression;
|
|
34378
|
+
};
|
|
32960
34379
|
const isSafeCreateRefCallbackCurrentWrite = (referenceNode, accessedPropertyPath, targetPropertyPath, scopes) => {
|
|
32961
34380
|
if (accessedPropertyPath.length !== targetPropertyPath.length + 1 || !pathStartsWith$1(accessedPropertyPath, targetPropertyPath) || accessedPropertyPath[targetPropertyPath.length] !== "current") return false;
|
|
32962
34381
|
const memberExpression = collectMemberExpression(referenceNode);
|
|
@@ -33443,6 +34862,151 @@ const isCreateRefResultWriteOnly = (createRefCall, filename, scopes) => {
|
|
|
33443
34862
|
return ownedValue ? analyzeSymbolValuePath(ownedValue, state, 12) : analyzeValueUse(createRefCall, [], environment, state, 12);
|
|
33444
34863
|
};
|
|
33445
34864
|
//#endregion
|
|
34865
|
+
//#region src/plugin/utils/is-jsx-element-or-fragment.ts
|
|
34866
|
+
/**
|
|
34867
|
+
* Type-guard for the two single-node JSX output forms: `JSXElement`
|
|
34868
|
+
* (`<Foo />`) and `JSXFragment` (`<>…</>`). Canonical home for the
|
|
34869
|
+
* `isNodeOfType(x, "JSXElement") || isNodeOfType(x, "JSXFragment")` check
|
|
34870
|
+
* that many rules otherwise inline. Does NOT unwrap parens / TS wrappers —
|
|
34871
|
+
* callers that need the semantic expression should `stripParenExpression`
|
|
34872
|
+
* first.
|
|
34873
|
+
*/
|
|
34874
|
+
const isJsxElementOrFragment = (node) => Boolean(node && (isNodeOfType(node, "JSXElement") || isNodeOfType(node, "JSXFragment")));
|
|
34875
|
+
//#endregion
|
|
34876
|
+
//#region src/plugin/rules/react-builtins/is-proven-one-shot-testing-library-component.ts
|
|
34877
|
+
const REACT_TESTING_LIBRARY_MODULE_SOURCE = "@testing-library/react";
|
|
34878
|
+
const REACT_TESTING_LIBRARY_MODULE_SOURCES = new Set([REACT_TESTING_LIBRARY_MODULE_SOURCE]);
|
|
34879
|
+
const TEST_CALLBACK_NAMES = new Set(["it", "test"]);
|
|
34880
|
+
const TEST_RUNNER_MODULE_SOURCES = new Set(["@jest/globals", "vitest"]);
|
|
34881
|
+
const isNamedImportFromModule = (symbol, importedName, moduleSources) => {
|
|
34882
|
+
if (!symbol || symbol.kind !== "import" || !isNodeOfType(symbol.declarationNode, "ImportSpecifier") || getImportedName(symbol.declarationNode) !== importedName) return false;
|
|
34883
|
+
const importDeclaration = symbol.declarationNode.parent;
|
|
34884
|
+
return Boolean(importDeclaration && isNodeOfType(importDeclaration, "ImportDeclaration") && typeof importDeclaration.source.value === "string" && moduleSources.has(importDeclaration.source.value));
|
|
34885
|
+
};
|
|
34886
|
+
const isNamespaceImportFromModule = (symbol, moduleSource) => {
|
|
34887
|
+
if (!symbol || symbol.kind !== "import" || !isNodeOfType(symbol.declarationNode, "ImportNamespaceSpecifier")) return false;
|
|
34888
|
+
const importDeclaration = symbol.declarationNode.parent;
|
|
34889
|
+
return Boolean(importDeclaration && isNodeOfType(importDeclaration, "ImportDeclaration") && importDeclaration.source.value === moduleSource);
|
|
34890
|
+
};
|
|
34891
|
+
const isProvenTestCallback = (functionNode, scopes) => {
|
|
34892
|
+
const callExpression = functionNode.parent;
|
|
34893
|
+
if (!callExpression || !isNodeOfType(callExpression, "CallExpression") || callExpression.arguments[1] !== functionNode) return false;
|
|
34894
|
+
const callee = stripParenExpression(callExpression.callee);
|
|
34895
|
+
if (!isNodeOfType(callee, "Identifier")) return false;
|
|
34896
|
+
if (TEST_CALLBACK_NAMES.has(callee.name) && scopes.isGlobalReference(callee)) return true;
|
|
34897
|
+
const symbol = scopes.symbolFor(callee);
|
|
34898
|
+
if (!symbol || symbol.kind !== "import") return false;
|
|
34899
|
+
const importedName = getImportedName(symbol.declarationNode);
|
|
34900
|
+
return Boolean(importedName && TEST_CALLBACK_NAMES.has(importedName) && isNamedImportFromModule(symbol, importedName, TEST_RUNNER_MODULE_SOURCES));
|
|
34901
|
+
};
|
|
34902
|
+
const getDirectConstComponentSymbol = (functionNode, scopes) => {
|
|
34903
|
+
const declarator = functionNode.parent;
|
|
34904
|
+
if (!declarator || !isNodeOfType(declarator, "VariableDeclarator") || declarator.init !== functionNode || !isNodeOfType(declarator.id, "Identifier")) return null;
|
|
34905
|
+
const declaration = declarator.parent;
|
|
34906
|
+
if (!declaration || !isNodeOfType(declaration, "VariableDeclaration") || declaration.kind !== "const" || declaration.declarations.length !== 1) return null;
|
|
34907
|
+
const testCallback = findEnclosingFunction$1(declarator);
|
|
34908
|
+
if (!testCallback || !isFunctionLike$1(testCallback) || !isProvenTestCallback(testCallback, scopes) || !isNodeOfType(testCallback.body, "BlockStatement") || declaration.parent !== testCallback.body) return null;
|
|
34909
|
+
return scopes.symbolFor(declarator.id);
|
|
34910
|
+
};
|
|
34911
|
+
const isCreateRefDeclaration = (statement, scopes) => isNodeOfType(statement, "VariableDeclaration") && statement.kind === "const" && statement.declarations.length > 0 && statement.declarations.every((declarator) => {
|
|
34912
|
+
const initializer = declarator.init ? stripParenExpression(declarator.init) : null;
|
|
34913
|
+
return Boolean(isNodeOfType(declarator.id, "Identifier") && initializer && isNodeOfType(initializer, "CallExpression") && isReactApiCall(initializer, "createRef", scopes, {
|
|
34914
|
+
allowGlobalReactNamespace: true,
|
|
34915
|
+
allowUnboundBareCalls: true,
|
|
34916
|
+
resolveNamedAliases: true
|
|
34917
|
+
}));
|
|
34918
|
+
});
|
|
34919
|
+
const isSafeReturnedJsx = (returnStatement) => {
|
|
34920
|
+
if (!isNodeOfType(returnStatement, "ReturnStatement") || !returnStatement.argument) return false;
|
|
34921
|
+
const returnedExpression = stripParenExpression(returnStatement.argument);
|
|
34922
|
+
if (!isJsxElementOrFragment(returnedExpression)) return false;
|
|
34923
|
+
let isSafe = true;
|
|
34924
|
+
walkAst(returnedExpression, (node) => {
|
|
34925
|
+
if (isFunctionLike$1(node)) {
|
|
34926
|
+
isSafe = false;
|
|
34927
|
+
return false;
|
|
34928
|
+
}
|
|
34929
|
+
if (isNodeOfType(node, "AssignmentExpression") || isNodeOfType(node, "AwaitExpression") || isNodeOfType(node, "CallExpression") || isNodeOfType(node, "NewExpression") || isNodeOfType(node, "TaggedTemplateExpression") || isNodeOfType(node, "UpdateExpression") || isNodeOfType(node, "YieldExpression")) {
|
|
34930
|
+
isSafe = false;
|
|
34931
|
+
return false;
|
|
34932
|
+
}
|
|
34933
|
+
});
|
|
34934
|
+
return isSafe;
|
|
34935
|
+
};
|
|
34936
|
+
const hasProvenOneShotComponentBody = (functionNode, scopes) => {
|
|
34937
|
+
if (!isFunctionLike$1(functionNode) || !isNodeOfType(functionNode.body, "BlockStatement")) return false;
|
|
34938
|
+
if (!functionNode.params.every((parameter) => isNodeOfType(parameter, "Identifier"))) return false;
|
|
34939
|
+
const statements = functionNode.body.body;
|
|
34940
|
+
if (statements.length < 2) return false;
|
|
34941
|
+
const returnStatement = statements.at(-1);
|
|
34942
|
+
return Boolean(returnStatement && statements.slice(0, -1).every((statement) => isCreateRefDeclaration(statement, scopes)) && isSafeReturnedJsx(returnStatement));
|
|
34943
|
+
};
|
|
34944
|
+
const isProvenReactStrictModeElement = (jsxElement, scopes) => {
|
|
34945
|
+
const elementName = jsxElement.openingElement.name;
|
|
34946
|
+
if (isNodeOfType(elementName, "JSXIdentifier")) {
|
|
34947
|
+
const symbol = scopes.symbolFor(elementName);
|
|
34948
|
+
return Boolean(symbol && isImportedFromReact(symbol) && getImportedName(symbol.declarationNode) === "StrictMode");
|
|
34949
|
+
}
|
|
34950
|
+
return Boolean(isNodeOfType(elementName, "JSXMemberExpression") && isNodeOfType(elementName.object, "JSXIdentifier") && elementName.property.name === "StrictMode" && isReactNamespaceImport(elementName.object, scopes));
|
|
34951
|
+
};
|
|
34952
|
+
const isWhitespaceJsxChild = (node) => isNodeOfType(node, "JSXText") && node.value.trim().length === 0 || isNodeOfType(node, "JSXExpressionContainer") && isNodeOfType(node.expression, "JSXEmptyExpression");
|
|
34953
|
+
const getRootElementForComponentReference = (identifier, scopes) => {
|
|
34954
|
+
const openingElement = identifier.parent;
|
|
34955
|
+
if (!openingElement || !isNodeOfType(openingElement, "JSXOpeningElement") || openingElement.name !== identifier || !openingElement.selfClosing || openingElement.attributes.length !== 0) return null;
|
|
34956
|
+
const componentElement = openingElement.parent;
|
|
34957
|
+
if (!componentElement || !isNodeOfType(componentElement, "JSXElement")) return null;
|
|
34958
|
+
const strictModeElement = componentElement.parent;
|
|
34959
|
+
if (!strictModeElement || !isNodeOfType(strictModeElement, "JSXElement")) return componentElement;
|
|
34960
|
+
if (strictModeElement.openingElement.attributes.length !== 0 || !isProvenReactStrictModeElement(strictModeElement, scopes)) return null;
|
|
34961
|
+
const renderedChildren = strictModeElement.children.filter((child) => !isWhitespaceJsxChild(child));
|
|
34962
|
+
return renderedChildren.length === 1 && renderedChildren[0] === componentElement ? strictModeElement : null;
|
|
34963
|
+
};
|
|
34964
|
+
const isProvenTestingLibraryRenderCall = (callExpression, scopes) => {
|
|
34965
|
+
const callee = stripParenExpression(callExpression.callee);
|
|
34966
|
+
if (isNodeOfType(callee, "Identifier")) return isNamedImportFromModule(scopes.symbolFor(callee), "render", REACT_TESTING_LIBRARY_MODULE_SOURCES);
|
|
34967
|
+
return Boolean(isNodeOfType(callee, "MemberExpression") && getStaticPropertyName(callee) === "render" && isNodeOfType(callee.object, "Identifier") && isNamespaceImportFromModule(scopes.symbolFor(callee.object), REACT_TESTING_LIBRARY_MODULE_SOURCE));
|
|
34968
|
+
};
|
|
34969
|
+
const isSafeRenderResultBinding = (pattern) => {
|
|
34970
|
+
if (!isNodeOfType(pattern, "ObjectPattern")) return false;
|
|
34971
|
+
return pattern.properties.every((property) => {
|
|
34972
|
+
if (!isNodeOfType(property, "Property") || property.computed) return false;
|
|
34973
|
+
return isNodeOfType(property.value, "Identifier") && getStaticPropertyKeyName(property) !== "rerender";
|
|
34974
|
+
});
|
|
34975
|
+
};
|
|
34976
|
+
const isDirectSafeRenderStatement = (callExpression, testCallback) => {
|
|
34977
|
+
if (!isFunctionLike$1(testCallback) || !isNodeOfType(testCallback.body, "BlockStatement")) return false;
|
|
34978
|
+
const expression = findTransparentExpressionRoot(callExpression);
|
|
34979
|
+
const parent = expression.parent;
|
|
34980
|
+
if (!parent) return false;
|
|
34981
|
+
if (isNodeOfType(parent, "ExpressionStatement")) return parent.parent === testCallback.body;
|
|
34982
|
+
if (!isNodeOfType(parent, "VariableDeclarator") || parent.init !== expression || !isSafeRenderResultBinding(parent.id)) return false;
|
|
34983
|
+
const declaration = parent.parent;
|
|
34984
|
+
return Boolean(declaration && isNodeOfType(declaration, "VariableDeclaration") && declaration.declarations.length === 1 && declaration.parent === testCallback.body);
|
|
34985
|
+
};
|
|
34986
|
+
const getProvenIndependentRenderCall = (componentReference, scopes) => {
|
|
34987
|
+
const rootElement = getRootElementForComponentReference(componentReference, scopes);
|
|
34988
|
+
if (!rootElement) return null;
|
|
34989
|
+
const renderedArgument = findTransparentExpressionRoot(rootElement);
|
|
34990
|
+
const callExpression = renderedArgument.parent;
|
|
34991
|
+
if (!callExpression || !isNodeOfType(callExpression, "CallExpression") || callExpression.arguments.length !== 1 || callExpression.arguments[0] !== renderedArgument || !isProvenTestingLibraryRenderCall(callExpression, scopes)) return null;
|
|
34992
|
+
return callExpression;
|
|
34993
|
+
};
|
|
34994
|
+
const isProvenOneShotTestingLibraryComponent = (functionNode, filename, scopes) => {
|
|
34995
|
+
if (!filename || !isTestlikeFilename(filename) || !hasProvenOneShotComponentBody(functionNode, scopes)) return false;
|
|
34996
|
+
const componentSymbol = getDirectConstComponentSymbol(functionNode, scopes);
|
|
34997
|
+
if (!componentSymbol || componentSymbol.references.length === 0) return false;
|
|
34998
|
+
const testCallback = findEnclosingFunction$1(componentSymbol.bindingIdentifier);
|
|
34999
|
+
if (!testCallback) return false;
|
|
35000
|
+
const renderCalls = /* @__PURE__ */ new Set();
|
|
35001
|
+
for (const reference of componentSymbol.references) {
|
|
35002
|
+
if (reference.flag !== "read") return false;
|
|
35003
|
+
const renderCall = getProvenIndependentRenderCall(reference.identifier, scopes);
|
|
35004
|
+
if (!renderCall || findEnclosingFunction$1(renderCall) !== testCallback || !isDirectSafeRenderStatement(renderCall, testCallback)) return false;
|
|
35005
|
+
renderCalls.add(renderCall);
|
|
35006
|
+
}
|
|
35007
|
+
return renderCalls.size > 0;
|
|
35008
|
+
};
|
|
35009
|
+
//#endregion
|
|
33446
35010
|
//#region src/plugin/rules/react-builtins/no-create-ref-in-function-component.ts
|
|
33447
35011
|
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.";
|
|
33448
35012
|
const isUseMemoCallbackArgument = (functionNode, scopes) => {
|
|
@@ -33456,6 +35020,27 @@ const findEnclosingRenderFunction = (node, scopes) => {
|
|
|
33456
35020
|
while (enclosingFunction && isUseMemoCallbackArgument(enclosingFunction, scopes)) enclosingFunction = findEnclosingFunction$1(enclosingFunction);
|
|
33457
35021
|
return enclosingFunction;
|
|
33458
35022
|
};
|
|
35023
|
+
const isReactUseStateInitialState = (node, scopes) => {
|
|
35024
|
+
const initialState = findTransparentExpressionRoot(node);
|
|
35025
|
+
const stateCall = initialState.parent;
|
|
35026
|
+
return Boolean(stateCall && isNodeOfType(stateCall, "CallExpression") && stateCall.arguments[0] === initialState && isReactApiCall(stateCall, "useState", scopes, {
|
|
35027
|
+
allowGlobalReactNamespace: true,
|
|
35028
|
+
resolveNamedAliases: true
|
|
35029
|
+
}));
|
|
35030
|
+
};
|
|
35031
|
+
const hasDirectExportWrapper = (declarationNode) => {
|
|
35032
|
+
const parent = declarationNode.parent;
|
|
35033
|
+
if (isNodeOfType(parent, "ExportNamedDeclaration") || isNodeOfType(parent, "ExportDefaultDeclaration")) return true;
|
|
35034
|
+
return Boolean(isNodeOfType(declarationNode, "VariableDeclarator") && (isNodeOfType(parent?.parent, "ExportNamedDeclaration") || isNodeOfType(parent?.parent, "ExportDefaultDeclaration")));
|
|
35035
|
+
};
|
|
35036
|
+
const isFunctionExclusivelyUsedAsReactStateInitializer = (functionNode, scopes) => {
|
|
35037
|
+
if (isReactUseStateInitialState(functionNode, scopes)) return true;
|
|
35038
|
+
const bindingIdentifier = getFunctionBindingIdentifier$1(findTransparentExpressionRoot(functionNode));
|
|
35039
|
+
if (!bindingIdentifier) return false;
|
|
35040
|
+
const bindingSymbol = isNodeOfType(functionNode, "FunctionDeclaration") ? scopes.scopeFor(functionNode).symbolsByName.get(bindingIdentifier.name) : scopes.symbolFor(bindingIdentifier);
|
|
35041
|
+
if (!bindingSymbol || bindingSymbol.kind !== "const" && bindingSymbol.kind !== "function" || hasDirectExportWrapper(bindingSymbol.declarationNode) || bindingSymbol.references.length === 0) return false;
|
|
35042
|
+
return bindingSymbol.references.every((reference) => reference.flag === "read" && isReactUseStateInitialState(reference.identifier, scopes));
|
|
35043
|
+
};
|
|
33459
35044
|
const noCreateRefInFunctionComponent = defineRule({
|
|
33460
35045
|
id: "no-create-ref-in-function-component",
|
|
33461
35046
|
title: "createRef in function component",
|
|
@@ -33472,6 +35057,8 @@ const noCreateRefInFunctionComponent = defineRule({
|
|
|
33472
35057
|
const displayName = componentOrHookDisplayNameForFunction(enclosingFunction);
|
|
33473
35058
|
if (!displayName) return;
|
|
33474
35059
|
if (!(isReactHookName(displayName) || functionContainsReactRenderOutput(enclosingFunction, context.scopes, context.cfg))) return;
|
|
35060
|
+
if (isReactUseStateInitialState(node, context.scopes) || isFunctionExclusivelyUsedAsReactStateInitializer(enclosingFunction, context.scopes)) return;
|
|
35061
|
+
if (isProvenOneShotTestingLibraryComponent(enclosingFunction, context.filename, context.scopes)) return;
|
|
33475
35062
|
if (isCreateRefResultWriteOnly(node, context.filename, context.scopes)) return;
|
|
33476
35063
|
context.report({
|
|
33477
35064
|
node,
|
|
@@ -34741,7 +36328,6 @@ const isSetStateCallInLifecycle = (setStateCall, lifecycleNames, options = {}) =
|
|
|
34741
36328
|
//#region src/plugin/rules/react-builtins/no-did-mount-set-state.ts
|
|
34742
36329
|
const LIFECYCLE_NAMES$2 = new Set(["componentDidMount"]);
|
|
34743
36330
|
const MESSAGE$28 = "Your users see an extra render right after mount when you call `setState` in `componentDidMount`.";
|
|
34744
|
-
const getNodeStart = (node) => "start" in node && typeof node.start === "number" ? node.start : -1;
|
|
34745
36331
|
const getEnclosingLifecycleFunction = (setStateCall) => {
|
|
34746
36332
|
let ancestor = setStateCall.parent;
|
|
34747
36333
|
while (ancestor) {
|
|
@@ -34830,13 +36416,13 @@ const argumentDerivesFromPostMountSource = (setStateCall, lifecycleFunction) =>
|
|
|
34830
36416
|
};
|
|
34831
36417
|
const isAfterAwaitInAsyncLifecycle = (setStateCall, lifecycleFunction) => {
|
|
34832
36418
|
if (!isFunctionLike$1(lifecycleFunction) || lifecycleFunction.async !== true) return false;
|
|
34833
|
-
const callStart =
|
|
36419
|
+
const callStart = getNodeStartIndex(setStateCall);
|
|
34834
36420
|
if (callStart < 0) return false;
|
|
34835
36421
|
let didFindPrecedingAwait = false;
|
|
34836
36422
|
walkAst(lifecycleFunction, (descendant) => {
|
|
34837
36423
|
if (didFindPrecedingAwait) return false;
|
|
34838
36424
|
if (!isNodeOfType(descendant, "AwaitExpression")) return;
|
|
34839
|
-
const awaitStart =
|
|
36425
|
+
const awaitStart = getNodeStartIndex(descendant);
|
|
34840
36426
|
if (awaitStart >= 0 && awaitStart < callStart) {
|
|
34841
36427
|
didFindPrecedingAwait = true;
|
|
34842
36428
|
return false;
|
|
@@ -35900,17 +37486,178 @@ const visitSynchronousFunctionBodies = (analysisFunctions, visitor) => {
|
|
|
35900
37486
|
walkInsideStatementBlocks(analysisFunction.body, visitor);
|
|
35901
37487
|
}
|
|
35902
37488
|
};
|
|
35903
|
-
const
|
|
35904
|
-
const
|
|
37489
|
+
const readStaticEffectValue = (expression, scopes, stateSymbolId, stateValue, visitedSymbolIds = /* @__PURE__ */ new Set()) => {
|
|
37490
|
+
const unwrappedExpression = stripParenExpression(expression);
|
|
37491
|
+
if (isNodeOfType(unwrappedExpression, "Literal")) {
|
|
37492
|
+
const literalValue = unwrappedExpression.value;
|
|
37493
|
+
if (literalValue === null || typeof literalValue === "boolean" || typeof literalValue === "number" || typeof literalValue === "string") return { value: literalValue };
|
|
37494
|
+
return null;
|
|
37495
|
+
}
|
|
37496
|
+
if (isNodeOfType(unwrappedExpression, "Identifier")) {
|
|
37497
|
+
if (scopes.symbolFor(unwrappedExpression)?.id === stateSymbolId) return stateValue;
|
|
37498
|
+
if (unwrappedExpression.name === "undefined" && scopes.isGlobalReference(unwrappedExpression)) return { value: void 0 };
|
|
37499
|
+
const immutableSymbol = scopes.symbolFor(unwrappedExpression);
|
|
37500
|
+
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;
|
|
37501
|
+
return readStaticEffectValue(immutableSymbol.initializer, scopes, stateSymbolId, stateValue, new Set(visitedSymbolIds).add(immutableSymbol.id));
|
|
37502
|
+
}
|
|
37503
|
+
if (isNodeOfType(unwrappedExpression, "UnaryExpression")) {
|
|
37504
|
+
if (unwrappedExpression.operator === "void") return { value: void 0 };
|
|
37505
|
+
if (unwrappedExpression.operator !== "!") return null;
|
|
37506
|
+
const argumentValue = readStaticEffectValue(unwrappedExpression.argument, scopes, stateSymbolId, stateValue, visitedSymbolIds);
|
|
37507
|
+
return argumentValue ? { value: !argumentValue.value } : null;
|
|
37508
|
+
}
|
|
37509
|
+
if (isNodeOfType(unwrappedExpression, "CallExpression")) {
|
|
37510
|
+
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")) {
|
|
37511
|
+
const argumentValue = readStaticEffectValue(unwrappedExpression.arguments[0], scopes, stateSymbolId, stateValue, visitedSymbolIds);
|
|
37512
|
+
return argumentValue ? { value: Boolean(argumentValue.value) } : null;
|
|
37513
|
+
}
|
|
37514
|
+
return null;
|
|
37515
|
+
}
|
|
37516
|
+
if (isNodeOfType(unwrappedExpression, "LogicalExpression")) {
|
|
37517
|
+
const leftValue = readStaticEffectValue(unwrappedExpression.left, scopes, stateSymbolId, stateValue, visitedSymbolIds);
|
|
37518
|
+
if (!leftValue) return null;
|
|
37519
|
+
if (unwrappedExpression.operator === "&&" && !leftValue.value) return leftValue;
|
|
37520
|
+
if (unwrappedExpression.operator === "||" && leftValue.value) return leftValue;
|
|
37521
|
+
if (unwrappedExpression.operator === "??" && leftValue.value !== null && leftValue.value !== void 0) return leftValue;
|
|
37522
|
+
return readStaticEffectValue(unwrappedExpression.right, scopes, stateSymbolId, stateValue, visitedSymbolIds);
|
|
37523
|
+
}
|
|
37524
|
+
if (isNodeOfType(unwrappedExpression, "ConditionalExpression")) {
|
|
37525
|
+
const testValue = readStaticEffectValue(unwrappedExpression.test, scopes, stateSymbolId, stateValue, visitedSymbolIds);
|
|
37526
|
+
if (!testValue) return null;
|
|
37527
|
+
return readStaticEffectValue(testValue.value ? unwrappedExpression.consequent : unwrappedExpression.alternate, scopes, stateSymbolId, stateValue, visitedSymbolIds);
|
|
37528
|
+
}
|
|
37529
|
+
if (isNodeOfType(unwrappedExpression, "MemberExpression") && unwrappedExpression.optional) {
|
|
37530
|
+
const objectValue = readStaticEffectValue(unwrappedExpression.object, scopes, stateSymbolId, stateValue, visitedSymbolIds);
|
|
37531
|
+
if (objectValue?.value === null || objectValue?.value === void 0) return { value: void 0 };
|
|
37532
|
+
return null;
|
|
37533
|
+
}
|
|
37534
|
+
if (isNodeOfType(unwrappedExpression, "BinaryExpression")) {
|
|
37535
|
+
const leftValue = readStaticEffectValue(unwrappedExpression.left, scopes, stateSymbolId, stateValue, visitedSymbolIds);
|
|
37536
|
+
const rightValue = readStaticEffectValue(unwrappedExpression.right, scopes, stateSymbolId, stateValue, visitedSymbolIds);
|
|
37537
|
+
if (!leftValue || !rightValue) return null;
|
|
37538
|
+
if (unwrappedExpression.operator === "===" || unwrappedExpression.operator === "!==") {
|
|
37539
|
+
const areEqual = leftValue.value === rightValue.value;
|
|
37540
|
+
return { value: unwrappedExpression.operator === "===" ? areEqual : !areEqual };
|
|
37541
|
+
}
|
|
37542
|
+
if (unwrappedExpression.operator === "==" || unwrappedExpression.operator === "!=") {
|
|
37543
|
+
const isLeftNullish = leftValue.value === null || leftValue.value === void 0;
|
|
37544
|
+
const isRightNullish = rightValue.value === null || rightValue.value === void 0;
|
|
37545
|
+
if (!isLeftNullish && !isRightNullish && typeof leftValue.value !== typeof rightValue.value) return null;
|
|
37546
|
+
const areEqual = isLeftNullish || isRightNullish ? isLeftNullish && isRightNullish : leftValue.value === rightValue.value;
|
|
37547
|
+
return { value: unwrappedExpression.operator === "==" ? areEqual : !areEqual };
|
|
37548
|
+
}
|
|
37549
|
+
}
|
|
37550
|
+
return null;
|
|
37551
|
+
};
|
|
37552
|
+
const readStaticUpdaterReturnValue = (updater, scopes) => {
|
|
37553
|
+
if (!isFunctionLike$1(updater) || updater.async || updater.generator) return null;
|
|
37554
|
+
if (!isNodeOfType(updater.body, "BlockStatement")) return readStaticEffectValue(updater.body, scopes, null, null);
|
|
37555
|
+
if (updater.body.body.length === 0) return { value: void 0 };
|
|
37556
|
+
if (updater.body.body.length !== 1) return null;
|
|
37557
|
+
const returnStatement = updater.body.body[0];
|
|
37558
|
+
if (!isNodeOfType(returnStatement, "ReturnStatement")) return null;
|
|
37559
|
+
if (!returnStatement.argument) return { value: void 0 };
|
|
37560
|
+
return readStaticEffectValue(returnStatement.argument, scopes, null, null);
|
|
37561
|
+
};
|
|
37562
|
+
const readStaticSetterValue = (setterCall, scopes) => {
|
|
37563
|
+
const argument = setterCall.arguments[0];
|
|
37564
|
+
if (!argument) return { value: void 0 };
|
|
37565
|
+
if (isNodeOfType(argument, "SpreadElement")) return null;
|
|
37566
|
+
const updater = resolveExactLocalFunction(argument, scopes);
|
|
37567
|
+
if (updater) return readStaticUpdaterReturnValue(updater, scopes);
|
|
37568
|
+
return readStaticEffectValue(argument, scopes, null, null);
|
|
37569
|
+
};
|
|
37570
|
+
const collectStateWritesInEffect = (analysisFunctions, setterToStateName, scopes) => {
|
|
37571
|
+
const stateWrites = /* @__PURE__ */ new Map();
|
|
35905
37572
|
visitSynchronousFunctionBodies(analysisFunctions, (child) => {
|
|
35906
37573
|
if (!isNodeOfType(child, "CallExpression")) return;
|
|
35907
37574
|
if (!isNodeOfType(child.callee, "Identifier")) return;
|
|
35908
37575
|
const stateName = setterToStateName.get(child.callee.name);
|
|
35909
|
-
if (stateName)
|
|
37576
|
+
if (!stateName) return;
|
|
37577
|
+
const writeInfo = stateWrites.get(stateName) ?? {
|
|
37578
|
+
values: /* @__PURE__ */ new Set(),
|
|
37579
|
+
hasUnknownValue: false
|
|
37580
|
+
};
|
|
37581
|
+
const staticValue = readStaticSetterValue(child, scopes);
|
|
37582
|
+
if (staticValue) writeInfo.values.add(staticValue.value);
|
|
37583
|
+
else writeInfo.hasUnknownValue = true;
|
|
37584
|
+
stateWrites.set(stateName, writeInfo);
|
|
35910
37585
|
});
|
|
35911
|
-
return
|
|
37586
|
+
return stateWrites;
|
|
37587
|
+
};
|
|
37588
|
+
const isGlobalBooleanCall = (node, scopes) => {
|
|
37589
|
+
return isNodeOfType(node, "CallExpression") && isNodeOfType(node.callee, "Identifier") && node.callee.name === "Boolean" && scopes.isGlobalReference(node.callee);
|
|
37590
|
+
};
|
|
37591
|
+
const isWorkNodeReachableForStateValue = (workNode, stateSymbolId, stateValue, scopes) => {
|
|
37592
|
+
let currentNode = workNode;
|
|
37593
|
+
while (currentNode.parent) {
|
|
37594
|
+
const parentNode = currentNode.parent;
|
|
37595
|
+
if (isFunctionLike$1(parentNode)) break;
|
|
37596
|
+
if (isNodeOfType(parentNode, "IfStatement")) {
|
|
37597
|
+
const testValue = readStaticEffectValue(parentNode.test, scopes, stateSymbolId, stateValue);
|
|
37598
|
+
if (testValue) {
|
|
37599
|
+
if (currentNode === parentNode.consequent && !testValue.value) return false;
|
|
37600
|
+
if (currentNode === parentNode.alternate && testValue.value) return false;
|
|
37601
|
+
}
|
|
37602
|
+
}
|
|
37603
|
+
if (isNodeOfType(parentNode, "ConditionalExpression")) {
|
|
37604
|
+
const testValue = readStaticEffectValue(parentNode.test, scopes, stateSymbolId, stateValue);
|
|
37605
|
+
if (testValue) {
|
|
37606
|
+
if (currentNode === parentNode.consequent && !testValue.value) return false;
|
|
37607
|
+
if (currentNode === parentNode.alternate && testValue.value) return false;
|
|
37608
|
+
}
|
|
37609
|
+
}
|
|
37610
|
+
if (isNodeOfType(parentNode, "LogicalExpression") && currentNode === parentNode.right) {
|
|
37611
|
+
const leftValue = readStaticEffectValue(parentNode.left, scopes, stateSymbolId, stateValue);
|
|
37612
|
+
if (leftValue) {
|
|
37613
|
+
if (parentNode.operator === "&&" && !leftValue.value) return false;
|
|
37614
|
+
if (parentNode.operator === "||" && leftValue.value) return false;
|
|
37615
|
+
if (parentNode.operator === "??" && leftValue.value !== null && leftValue.value !== void 0) return false;
|
|
37616
|
+
}
|
|
37617
|
+
}
|
|
37618
|
+
if (isNodeOfType(parentNode, "BlockStatement")) {
|
|
37619
|
+
const statementIndex = parentNode.body.findIndex((statement) => statement === currentNode);
|
|
37620
|
+
if (statementIndex >= 0) for (let index = 0; index < statementIndex; index += 1) {
|
|
37621
|
+
const earlierStatement = parentNode.body[index];
|
|
37622
|
+
if (!isNodeOfType(earlierStatement, "IfStatement") || earlierStatement.alternate || !statementAlwaysExits(earlierStatement.consequent)) continue;
|
|
37623
|
+
if (readStaticEffectValue(earlierStatement.test, scopes, stateSymbolId, stateValue)?.value) return false;
|
|
37624
|
+
}
|
|
37625
|
+
}
|
|
37626
|
+
currentNode = parentNode;
|
|
37627
|
+
}
|
|
37628
|
+
return true;
|
|
37629
|
+
};
|
|
37630
|
+
const isReaderWorkNode = (node, analysisFunctions, scopes) => {
|
|
37631
|
+
if (isNodeOfType(node, "CallExpression")) {
|
|
37632
|
+
if (isGlobalBooleanCall(node, scopes)) return false;
|
|
37633
|
+
const invokedFunction = resolveExactLocalFunction(node.callee, scopes);
|
|
37634
|
+
return !invokedFunction || !analysisFunctions.has(invokedFunction);
|
|
37635
|
+
}
|
|
37636
|
+
return isNodeOfType(node, "AssignmentExpression") || isNodeOfType(node, "UpdateExpression") || isNodeOfType(node, "NewExpression") || isNodeOfType(node, "TaggedTemplateExpression") || isNodeOfType(node, "ThrowStatement") || isNodeOfType(node, "UnaryExpression") && node.operator === "delete";
|
|
37637
|
+
};
|
|
37638
|
+
const canStateWriteReachReaderWork = (writeInfo, readerEffect, stateSymbolId, scopes) => {
|
|
37639
|
+
if (writeInfo.hasUnknownValue || stateSymbolId === null) return true;
|
|
37640
|
+
for (const writtenValue of writeInfo.values) {
|
|
37641
|
+
const stateValue = { value: writtenValue };
|
|
37642
|
+
let didFindReachableWork = false;
|
|
37643
|
+
visitSynchronousFunctionBodies(readerEffect.analysisFunctions, (child) => {
|
|
37644
|
+
if (didFindReachableWork || !isReaderWorkNode(child, readerEffect.analysisFunctions, scopes)) return;
|
|
37645
|
+
if (isWorkNodeReachableForStateValue(child, stateSymbolId, stateValue, scopes)) didFindReachableWork = true;
|
|
37646
|
+
});
|
|
37647
|
+
if (didFindReachableWork) return true;
|
|
37648
|
+
}
|
|
37649
|
+
return false;
|
|
35912
37650
|
};
|
|
35913
37651
|
const EMPTY_CLEANUP_NAME_SET = /* @__PURE__ */ new Set();
|
|
37652
|
+
const NON_CONTAMINATING_MAP_METHOD_NAMES = new Set([
|
|
37653
|
+
"clear",
|
|
37654
|
+
"delete",
|
|
37655
|
+
"entries",
|
|
37656
|
+
"get",
|
|
37657
|
+
"has",
|
|
37658
|
+
"keys",
|
|
37659
|
+
"values"
|
|
37660
|
+
]);
|
|
35914
37661
|
const isFunctionShapedReturn = (returnedValue, setterToStateName, isExplicitReturnStatement) => {
|
|
35915
37662
|
if (isNodeOfType(returnedValue, "ArrowFunctionExpression") || isNodeOfType(returnedValue, "FunctionExpression")) return true;
|
|
35916
37663
|
if (isNodeOfType(returnedValue, "CallExpression")) {
|
|
@@ -35961,27 +37708,135 @@ const callsOpaqueExternalSetter = (analysisFunctions, setterToStateName) => {
|
|
|
35961
37708
|
});
|
|
35962
37709
|
return didFindOpaqueSetterCall;
|
|
35963
37710
|
};
|
|
37711
|
+
const isReactRefCall = (expression, scopes) => isNodeOfType(expression, "CallExpression") && (isReactApiCall(expression, "useRef", scopes, {
|
|
37712
|
+
allowGlobalReactNamespace: true,
|
|
37713
|
+
allowUnboundBareCalls: true,
|
|
37714
|
+
resolveNamedAliases: true
|
|
37715
|
+
}) || isReactApiCall(expression, "createRef", scopes, {
|
|
37716
|
+
allowGlobalReactNamespace: true,
|
|
37717
|
+
allowUnboundBareCalls: true,
|
|
37718
|
+
resolveNamedAliases: true
|
|
37719
|
+
}));
|
|
37720
|
+
const getDirectReactRefSymbol = (rawExpression, scopes) => {
|
|
37721
|
+
const expression = stripParenExpression(rawExpression);
|
|
37722
|
+
if (!isNodeOfType(expression, "Identifier")) return null;
|
|
37723
|
+
const symbol = scopes.symbolFor(expression);
|
|
37724
|
+
if (!symbol) return null;
|
|
37725
|
+
const initializer = getDirectUnreassignedInitializer(symbol);
|
|
37726
|
+
return initializer && isReactRefCall(stripParenExpression(initializer), scopes) ? symbol : null;
|
|
37727
|
+
};
|
|
37728
|
+
const isReactNativeJsxElement = (openingElement, scopes) => {
|
|
37729
|
+
if (!isNodeOfType(openingElement.name, "JSXIdentifier")) return false;
|
|
37730
|
+
const symbol = scopes.symbolFor(openingElement.name);
|
|
37731
|
+
const importDeclaration = symbol?.declarationNode.parent;
|
|
37732
|
+
return Boolean(symbol?.kind === "import" && importDeclaration && isNodeOfType(importDeclaration, "ImportDeclaration") && importDeclaration.source.value === "react-native");
|
|
37733
|
+
};
|
|
37734
|
+
const isDirectHostJsxRef = (symbol, scopes) => {
|
|
37735
|
+
let hostRefCount = 0;
|
|
37736
|
+
for (const reference of symbol.references) {
|
|
37737
|
+
const expression = findTransparentExpressionRoot(reference.identifier);
|
|
37738
|
+
const container = expression.parent;
|
|
37739
|
+
if (isNodeOfType(container, "MemberExpression") && container.object === expression && getStaticPropertyName(container) === "current") continue;
|
|
37740
|
+
if (!container || !isNodeOfType(container, "JSXExpressionContainer") || container.expression !== expression) return false;
|
|
37741
|
+
const attribute = container.parent;
|
|
37742
|
+
if (!attribute || !isNodeOfType(attribute, "JSXAttribute") || getJsxAttributeName(attribute.name) !== "ref") return false;
|
|
37743
|
+
const openingElement = attribute.parent;
|
|
37744
|
+
if (!openingElement || !isNodeOfType(openingElement, "JSXOpeningElement") || !isProvenIntrinsicJsxElement(openingElement, scopes) && !isReactNativeJsxElement(openingElement, scopes)) return false;
|
|
37745
|
+
hostRefCount += 1;
|
|
37746
|
+
}
|
|
37747
|
+
return hostRefCount > 0;
|
|
37748
|
+
};
|
|
37749
|
+
const isIntrinsicRefCallbackParameter = (expression, scopes) => {
|
|
37750
|
+
const identifier = stripParenExpression(expression);
|
|
37751
|
+
if (!isNodeOfType(identifier, "Identifier")) return false;
|
|
37752
|
+
const callback = findEnclosingFunction$1(identifier);
|
|
37753
|
+
if (!callback || !isFunctionLike$1(callback) || !isInlineIntrinsicRefCallback(callback, scopes)) return false;
|
|
37754
|
+
const rawFirstParameter = callback.params?.[0];
|
|
37755
|
+
const firstParameter = isNodeOfType(rawFirstParameter, "AssignmentPattern") ? rawFirstParameter.left : rawFirstParameter;
|
|
37756
|
+
const symbol = scopes.symbolFor(identifier);
|
|
37757
|
+
return Boolean(firstParameter && symbol?.bindingIdentifier === firstParameter);
|
|
37758
|
+
};
|
|
37759
|
+
const getDirectReactRefCall = (symbol, scopes) => {
|
|
37760
|
+
const initializer = getDirectUnreassignedInitializer(symbol);
|
|
37761
|
+
if (!initializer) return null;
|
|
37762
|
+
const expression = stripParenExpression(initializer);
|
|
37763
|
+
return isNodeOfType(expression, "CallExpression") && isReactRefCall(expression, scopes) ? expression : null;
|
|
37764
|
+
};
|
|
37765
|
+
const storesOnlyIntrinsicRefCallbackValues = (symbol, scopes) => {
|
|
37766
|
+
const initialValue = getDirectReactRefCall(symbol, scopes)?.arguments?.[0];
|
|
37767
|
+
if (!initialValue || !isNodeOfType(initialValue, "NewExpression") || !isNodeOfType(initialValue.callee, "Identifier") || initialValue.callee.name !== "Map" || !scopes.isGlobalReference(initialValue.callee) || initialValue.arguments.length !== 0) return false;
|
|
37768
|
+
let intrinsicValueWriteCount = 0;
|
|
37769
|
+
for (const reference of symbol.references) {
|
|
37770
|
+
const identifier = findTransparentExpressionRoot(reference.identifier);
|
|
37771
|
+
const currentMember = identifier.parent;
|
|
37772
|
+
if (!isNodeOfType(currentMember, "MemberExpression") || currentMember.object !== identifier || getStaticPropertyName(currentMember) !== "current") return false;
|
|
37773
|
+
const currentExpression = findTransparentExpressionRoot(currentMember);
|
|
37774
|
+
const methodMember = currentExpression.parent;
|
|
37775
|
+
if (!isNodeOfType(methodMember, "MemberExpression") || methodMember.object !== currentExpression) return false;
|
|
37776
|
+
const methodName = getStaticPropertyName(methodMember);
|
|
37777
|
+
if (methodName === "size") continue;
|
|
37778
|
+
const call = methodMember.parent;
|
|
37779
|
+
if (!isNodeOfType(call, "CallExpression") || call.callee !== methodMember) return false;
|
|
37780
|
+
if (methodName && NON_CONTAMINATING_MAP_METHOD_NAMES.has(methodName)) continue;
|
|
37781
|
+
if (methodName !== "set") return false;
|
|
37782
|
+
const storedValue = call.arguments[1];
|
|
37783
|
+
if (!storedValue || isNodeOfType(storedValue, "SpreadElement") || !isIntrinsicRefCallbackParameter(storedValue, scopes)) return false;
|
|
37784
|
+
intrinsicValueWriteCount += 1;
|
|
37785
|
+
}
|
|
37786
|
+
return intrinsicValueWriteCount > 0;
|
|
37787
|
+
};
|
|
37788
|
+
const isDerivedFromProvenDomRefCurrent = (rawExpression, scopes, didReadCollectionValue = false, visitedSymbolIds = /* @__PURE__ */ new Set()) => {
|
|
37789
|
+
const expression = stripParenExpression(rawExpression);
|
|
37790
|
+
if (isNodeOfType(expression, "Identifier")) {
|
|
37791
|
+
const symbol = scopes.symbolFor(expression);
|
|
37792
|
+
if (!symbol || visitedSymbolIds.has(symbol.id)) return false;
|
|
37793
|
+
const initializer = getDirectUnreassignedInitializer(symbol);
|
|
37794
|
+
if (!initializer) return false;
|
|
37795
|
+
visitedSymbolIds.add(symbol.id);
|
|
37796
|
+
return isDerivedFromProvenDomRefCurrent(initializer, scopes, didReadCollectionValue, visitedSymbolIds);
|
|
37797
|
+
}
|
|
37798
|
+
if (isNodeOfType(expression, "MemberExpression")) {
|
|
37799
|
+
if (getStaticPropertyName(expression) === "current") {
|
|
37800
|
+
const symbol = getDirectReactRefSymbol(expression.object, scopes);
|
|
37801
|
+
return Boolean(symbol && (isDirectHostJsxRef(symbol, scopes) || didReadCollectionValue && storesOnlyIntrinsicRefCallbackValues(symbol, scopes)));
|
|
37802
|
+
}
|
|
37803
|
+
return isDerivedFromProvenDomRefCurrent(expression.object, scopes, didReadCollectionValue, visitedSymbolIds);
|
|
37804
|
+
}
|
|
37805
|
+
if (!isNodeOfType(expression, "CallExpression")) return false;
|
|
37806
|
+
const callee = stripParenExpression(expression.callee);
|
|
37807
|
+
if (!isNodeOfType(callee, "MemberExpression")) return false;
|
|
37808
|
+
return isDerivedFromProvenDomRefCurrent(callee.object, scopes, didReadCollectionValue || getStaticPropertyName(callee) === "get", visitedSymbolIds);
|
|
37809
|
+
};
|
|
37810
|
+
const isCommittedDomSyncNode = (node, scopes) => {
|
|
37811
|
+
if (!isNodeOfType(node, "CallExpression")) return false;
|
|
37812
|
+
const callee = stripParenExpression(node.callee);
|
|
37813
|
+
if (!isNodeOfType(callee, "MemberExpression")) return false;
|
|
37814
|
+
const propertyName = getStaticPropertyName(callee);
|
|
37815
|
+
if (propertyName === null || !EXTERNAL_SYNC_DOM_MEMBER_METHOD_NAMES.has(propertyName)) return false;
|
|
37816
|
+
return isDerivedFromProvenDomRefCurrent(callee.object, scopes) || isProvenBrowserApiReceiver(callee.object, "dom-event-target", scopes);
|
|
37817
|
+
};
|
|
35964
37818
|
const isExternalSyncNode = (node) => {
|
|
35965
37819
|
if (isNodeOfType(node, "NewExpression")) return isNodeOfType(node.callee, "Identifier") && EXTERNAL_SYNC_OBSERVER_CONSTRUCTORS.has(node.callee.name);
|
|
35966
37820
|
if (isNodeOfType(node, "AssignmentExpression")) return isNodeOfType(node.left, "MemberExpression") && isNodeOfType(node.left.property, "Identifier") && node.left.property.name === "current";
|
|
35967
37821
|
if (!isNodeOfType(node, "CallExpression")) return false;
|
|
35968
37822
|
if (isNodeOfType(node.callee, "Identifier")) return EXTERNAL_SYNC_DIRECT_CALLEE_NAMES.has(node.callee.name);
|
|
35969
|
-
if (!isNodeOfType(node.callee, "MemberExpression")
|
|
35970
|
-
const propertyName = node.callee
|
|
37823
|
+
if (!isNodeOfType(node.callee, "MemberExpression")) return false;
|
|
37824
|
+
const propertyName = getStaticPropertyName(node.callee);
|
|
37825
|
+
if (propertyName === null) return false;
|
|
35971
37826
|
if (EXTERNAL_SYNC_MEMBER_METHOD_NAMES.has(propertyName)) return true;
|
|
35972
37827
|
if (isBrowserStorageReceiver(node.callee.object)) return true;
|
|
35973
37828
|
if (!EXTERNAL_SYNC_AMBIGUOUS_HTTP_METHOD_NAMES.has(propertyName)) return false;
|
|
35974
37829
|
const receiverRootName = getRootIdentifierName(node.callee.object);
|
|
35975
37830
|
return receiverRootName !== null && EXTERNAL_SYNC_HTTP_CLIENT_RECEIVERS.has(receiverRootName);
|
|
35976
37831
|
};
|
|
35977
|
-
const isExternalSyncEffect = (effectCallback, analysisFunctions, setterToStateName) => {
|
|
37832
|
+
const isExternalSyncEffect = (effectCallback, analysisFunctions, setterToStateName, scopes, allowCommittedDomSync) => {
|
|
35978
37833
|
if (!isFunctionLike$1(effectCallback)) return false;
|
|
35979
37834
|
if (!isNodeOfType(effectCallback.body, "BlockStatement")) {
|
|
35980
37835
|
if (isFunctionShapedReturn(effectCallback.body, setterToStateName, false)) return true;
|
|
35981
37836
|
} else for (const statement of effectCallback.body.body ?? []) if (isNodeOfType(statement, "ReturnStatement") && statement.argument && isFunctionShapedReturn(statement.argument, setterToStateName, true)) return true;
|
|
35982
37837
|
let didFindExternalCall = false;
|
|
35983
37838
|
visitSynchronousFunctionBodies(analysisFunctions, (child) => {
|
|
35984
|
-
if (isExternalSyncNode(child)) didFindExternalCall = true;
|
|
37839
|
+
if (isExternalSyncNode(child) || allowCommittedDomSync && isCommittedDomSyncNode(child, scopes)) didFindExternalCall = true;
|
|
35985
37840
|
});
|
|
35986
37841
|
return didFindExternalCall;
|
|
35987
37842
|
};
|
|
@@ -35997,32 +37852,45 @@ const noEffectChain = defineRule({
|
|
|
35997
37852
|
const useStateBindings = collectUseStateBindings(componentBody);
|
|
35998
37853
|
if (useStateBindings.length === 0) return;
|
|
35999
37854
|
const setterToStateName = /* @__PURE__ */ new Map();
|
|
36000
|
-
|
|
37855
|
+
const stateSymbolIds = /* @__PURE__ */ new Map();
|
|
37856
|
+
for (const binding of useStateBindings) {
|
|
37857
|
+
setterToStateName.set(binding.setterName, binding.valueName);
|
|
37858
|
+
if (!isNodeOfType(binding.declarator.id, "ArrayPattern")) continue;
|
|
37859
|
+
const stateIdentifier = binding.declarator.id.elements[0];
|
|
37860
|
+
if (isNodeOfType(stateIdentifier, "Identifier")) {
|
|
37861
|
+
const stateSymbol = context.scopes.symbolFor(stateIdentifier);
|
|
37862
|
+
if (stateSymbol) stateSymbolIds.set(binding.valueName, stateSymbol.id);
|
|
37863
|
+
}
|
|
37864
|
+
}
|
|
36001
37865
|
const storageSetterNames = collectStorageHookSetterNames(componentBody);
|
|
36002
37866
|
const effectInfos = [];
|
|
36003
37867
|
for (const effectCall of findTopLevelEffectCalls(componentBody)) {
|
|
36004
37868
|
const callback = getEffectCallback(effectCall, context.scopes);
|
|
36005
37869
|
if (!callback || !isFunctionLike$1(callback) || callback.async) continue;
|
|
36006
37870
|
const analysisFunctions = collectSynchronouslyInvokedFunctions(callback, context.scopes);
|
|
36007
|
-
const
|
|
37871
|
+
const stateWrites = collectStateWritesInEffect(analysisFunctions, setterToStateName, context.scopes);
|
|
37872
|
+
const writtenStateNames = new Set(stateWrites.keys());
|
|
36008
37873
|
effectInfos.push({
|
|
36009
37874
|
node: effectCall,
|
|
36010
37875
|
depNames: collectDepIdentifierNames(effectCall),
|
|
36011
|
-
|
|
36012
|
-
|
|
37876
|
+
stateWrites,
|
|
37877
|
+
analysisFunctions,
|
|
37878
|
+
isExternalSync: isExternalSyncEffect(callback, analysisFunctions, setterToStateName, context.scopes, writtenStateNames.size === 0) || callsStorageHookSetter(analysisFunctions, storageSetterNames) || writtenStateNames.size === 0 && callsOpaqueExternalSetter(analysisFunctions, setterToStateName)
|
|
36013
37879
|
});
|
|
36014
37880
|
}
|
|
36015
37881
|
if (effectInfos.length < 2) return;
|
|
36016
37882
|
const reportedNodes = /* @__PURE__ */ new Set();
|
|
36017
37883
|
for (const writerEffect of effectInfos) {
|
|
36018
37884
|
if (writerEffect.isExternalSync) continue;
|
|
36019
|
-
if (writerEffect.
|
|
37885
|
+
if (writerEffect.stateWrites.size === 0) continue;
|
|
36020
37886
|
for (const readerEffect of effectInfos) {
|
|
36021
37887
|
if (readerEffect === writerEffect) continue;
|
|
36022
37888
|
if (readerEffect.isExternalSync) continue;
|
|
36023
37889
|
if (readerEffect.depNames.size === 0) continue;
|
|
36024
37890
|
let chainedStateName = null;
|
|
36025
|
-
for (const writtenName of writerEffect.
|
|
37891
|
+
for (const [writtenName, writeInfo] of writerEffect.stateWrites) {
|
|
37892
|
+
if (!readerEffect.depNames.has(writtenName)) continue;
|
|
37893
|
+
if (!canStateWriteReachReaderWork(writeInfo, readerEffect, stateSymbolIds.get(writtenName) ?? null, context.scopes)) continue;
|
|
36026
37894
|
chainedStateName = writtenName;
|
|
36027
37895
|
break;
|
|
36028
37896
|
}
|
|
@@ -39689,6 +41557,18 @@ const functionContainsProvenReactHookCall = (functionNode, scopes) => {
|
|
|
39689
41557
|
return containsReactHookCall;
|
|
39690
41558
|
};
|
|
39691
41559
|
//#endregion
|
|
41560
|
+
//#region src/plugin/utils/function-returns-only-null.ts
|
|
41561
|
+
const isNullExpression = (expression) => {
|
|
41562
|
+
const candidate = stripParenExpression(expression);
|
|
41563
|
+
return isNodeOfType(candidate, "Literal") && candidate.value === null;
|
|
41564
|
+
};
|
|
41565
|
+
const functionReturnsOnlyNull = (functionNode) => {
|
|
41566
|
+
if (!isFunctionLike$1(functionNode)) return false;
|
|
41567
|
+
if (!isNodeOfType(functionNode.body, "BlockStatement")) return isNullExpression(functionNode.body);
|
|
41568
|
+
const returnStatements = collectFunctionReturnStatements(functionNode);
|
|
41569
|
+
return returnStatements.length > 0 && returnStatements.every((returnStatement) => Boolean(returnStatement.argument && isNullExpression(returnStatement.argument)));
|
|
41570
|
+
};
|
|
41571
|
+
//#endregion
|
|
39692
41572
|
//#region src/plugin/utils/function-returns-props-children.ts
|
|
39693
41573
|
const functionReturnsPropsChildren = (functionNode, scopes, controlFlow) => {
|
|
39694
41574
|
if (!isFunctionLike$1(functionNode) || functionNode.params.length === 0) return false;
|
|
@@ -39721,17 +41601,8 @@ const functionReturnsPropsChildren = (functionNode, scopes, controlFlow) => {
|
|
|
39721
41601
|
}, controlFlow);
|
|
39722
41602
|
};
|
|
39723
41603
|
//#endregion
|
|
39724
|
-
//#region src/plugin/utils/function-
|
|
39725
|
-
const
|
|
39726
|
-
const candidate = stripParenExpression(expression);
|
|
39727
|
-
return isNodeOfType(candidate, "Literal") && candidate.value === null;
|
|
39728
|
-
};
|
|
39729
|
-
const functionReturnsOnlyNull = (functionNode) => {
|
|
39730
|
-
if (!isFunctionLike$1(functionNode)) return false;
|
|
39731
|
-
if (!isNodeOfType(functionNode.body, "BlockStatement")) return isNullExpression(functionNode.body);
|
|
39732
|
-
const returnStatements = collectFunctionReturnStatements(functionNode);
|
|
39733
|
-
return returnStatements.length > 0 && returnStatements.every((returnStatement) => Boolean(returnStatement.argument && isNullExpression(returnStatement.argument)));
|
|
39734
|
-
};
|
|
41604
|
+
//#region src/plugin/utils/function-has-react-component-evidence.ts
|
|
41605
|
+
const functionHasReactComponentEvidence = (functionNode, scopes, controlFlow) => functionContainsReactRenderOutput(functionNode, scopes, controlFlow) || functionReturnsPropsChildren(functionNode, scopes, controlFlow) || functionContainsProvenReactHookCall(functionNode, scopes) && functionReturnsOnlyNull(functionNode);
|
|
39735
41606
|
//#endregion
|
|
39736
41607
|
//#region src/plugin/utils/is-proven-styled-component-expression.ts
|
|
39737
41608
|
const findFactoryRoot = (node) => {
|
|
@@ -39764,17 +41635,16 @@ const isProvenStyledComponentExpression = (expression, scopes) => {
|
|
|
39764
41635
|
//#region src/plugin/utils/is-proven-react-component-symbol.ts
|
|
39765
41636
|
const REACT_COMPONENT_HOC_NAMES = new Set(["memo", "forwardRef"]);
|
|
39766
41637
|
const LEGACY_REACT_COMPONENT_FACTORY_NAMES = new Set(["createClass", "createReactClass"]);
|
|
39767
|
-
const functionHasComponentEvidence = (functionNode, scopes, controlFlow) => functionContainsReactRenderOutput(functionNode, scopes, controlFlow) || functionReturnsPropsChildren(functionNode, scopes, controlFlow) || functionContainsProvenReactHookCall(functionNode, scopes) && functionReturnsOnlyNull(functionNode);
|
|
39768
41638
|
const isProvenReactComponentExpression = (expression, scopes, controlFlow, visitedSymbolIds = /* @__PURE__ */ new Set()) => {
|
|
39769
41639
|
const candidate = stripParenExpression(expression);
|
|
39770
|
-
if (isInlineFunctionExpression(candidate)) return
|
|
41640
|
+
if (isInlineFunctionExpression(candidate)) return functionHasReactComponentEvidence(candidate, scopes, controlFlow);
|
|
39771
41641
|
if (isNodeOfType(candidate, "ClassExpression")) return isProvenReactClassComponent(candidate, scopes);
|
|
39772
41642
|
if (isProvenStyledComponentExpression(candidate, scopes)) return true;
|
|
39773
41643
|
if (isNodeOfType(candidate, "Identifier")) {
|
|
39774
41644
|
const symbol = scopes.symbolFor(candidate);
|
|
39775
41645
|
if (!symbol || visitedSymbolIds.has(symbol.id) || hasSymbolWriteBefore(symbol, candidate, scopes)) return false;
|
|
39776
41646
|
visitedSymbolIds.add(symbol.id);
|
|
39777
|
-
if (isNodeOfType(symbol.declarationNode, "FunctionDeclaration")) return
|
|
41647
|
+
if (isNodeOfType(symbol.declarationNode, "FunctionDeclaration")) return functionHasReactComponentEvidence(symbol.declarationNode, scopes, controlFlow);
|
|
39778
41648
|
if (isNodeOfType(symbol.declarationNode, "ClassDeclaration") || isNodeOfType(symbol.declarationNode, "ClassExpression")) return isProvenReactClassComponent(symbol.declarationNode, scopes);
|
|
39779
41649
|
return Boolean(symbol.initializer && isProvenReactComponentExpression(symbol.initializer, scopes, controlFlow, visitedSymbolIds));
|
|
39780
41650
|
}
|
|
@@ -39801,7 +41671,7 @@ const isProvenReactComponentSymbol = (symbol, scopes, controlFlow, componentRefe
|
|
|
39801
41671
|
for (const candidateSymbol of candidateSymbols) {
|
|
39802
41672
|
if (hasSymbolWriteBefore(candidateSymbol, componentReference, scopes)) continue;
|
|
39803
41673
|
if (isComponentDeclaration(candidateSymbol.declarationNode)) {
|
|
39804
|
-
if (
|
|
41674
|
+
if (functionHasReactComponentEvidence(candidateSymbol.declarationNode, scopes, controlFlow)) return true;
|
|
39805
41675
|
continue;
|
|
39806
41676
|
}
|
|
39807
41677
|
const initializer = candidateSymbol.initializer ? stripParenExpression(candidateSymbol.initializer) : null;
|
|
@@ -41303,11 +43173,6 @@ const SAME_REFERENCE_ARRAY_RETURN_METHODS = new Set([
|
|
|
41303
43173
|
"reverse",
|
|
41304
43174
|
"sort"
|
|
41305
43175
|
]);
|
|
41306
|
-
const OBJECT_MUTATION_METHODS = new Set([
|
|
41307
|
-
"assign",
|
|
41308
|
-
"defineProperties",
|
|
41309
|
-
"defineProperty"
|
|
41310
|
-
]);
|
|
41311
43176
|
const REFLECT_MUTATION_METHODS = new Set(["deleteProperty", "set"]);
|
|
41312
43177
|
const cloneReducerPathState = (state) => ({
|
|
41313
43178
|
originalStateReferenceNames: new Set(state.originalStateReferenceNames),
|
|
@@ -41403,7 +43268,7 @@ const collectReducerStateMutationsInExpressionOrStatement = (node, state) => {
|
|
|
41403
43268
|
}
|
|
41404
43269
|
if (!isNodeOfType(unwrappedChild, "CallExpression")) return;
|
|
41405
43270
|
const firstArgument = unwrappedChild.arguments?.[0];
|
|
41406
|
-
if (firstArgument && isExpressionRootedInMutableReducerStateSource(firstArgument, state) && (isStaticMethodCallOnNamedObject(unwrappedChild, "Object",
|
|
43271
|
+
if (firstArgument && isExpressionRootedInMutableReducerStateSource(firstArgument, state) && (isStaticMethodCallOnNamedObject(unwrappedChild, "Object", OBJECT_PROPERTY_MUTATION_METHOD_NAMES) || isStaticMethodCallOnNamedObject(unwrappedChild, "Reflect", REFLECT_MUTATION_METHODS))) {
|
|
41407
43272
|
mutations.push({ node: unwrappedChild });
|
|
41408
43273
|
return;
|
|
41409
43274
|
}
|
|
@@ -43587,6 +45452,53 @@ const noPropCallbackInEffect = defineRule({
|
|
|
43587
45452
|
});
|
|
43588
45453
|
//#endregion
|
|
43589
45454
|
//#region src/plugin/rules/state-and-effects/no-prop-callback-in-render.ts
|
|
45455
|
+
const functionBindingSymbols = (functionNode, scopes) => {
|
|
45456
|
+
let bindingIdentifier = null;
|
|
45457
|
+
if (isNodeOfType(functionNode, "FunctionDeclaration") && functionNode.id) bindingIdentifier = functionNode.id;
|
|
45458
|
+
else {
|
|
45459
|
+
let bindingExpression = findTransparentExpressionRoot(functionNode);
|
|
45460
|
+
let parent = bindingExpression.parent;
|
|
45461
|
+
while (isNodeOfType(parent, "CallExpression") && parent.arguments[0] === bindingExpression) {
|
|
45462
|
+
const callee = parent.callee;
|
|
45463
|
+
const wrapperName = isNodeOfType(callee, "Identifier") ? callee.name : isNodeOfType(callee, "MemberExpression") && isNodeOfType(callee.property, "Identifier") ? callee.property.name : null;
|
|
45464
|
+
if (!isReactApiCall(parent, REACT_HOC_NAMES, scopes, {
|
|
45465
|
+
allowGlobalReactNamespace: true,
|
|
45466
|
+
resolveNamedAliases: true
|
|
45467
|
+
}) && (!wrapperName || REACT_HOC_NAMES.has(wrapperName) || !COMPONENT_HOC_WRAPPER_NAMES.has(wrapperName))) break;
|
|
45468
|
+
bindingExpression = findTransparentExpressionRoot(parent);
|
|
45469
|
+
parent = bindingExpression.parent;
|
|
45470
|
+
}
|
|
45471
|
+
if (isNodeOfType(parent, "VariableDeclarator") && parent.init === bindingExpression && isNodeOfType(parent.id, "Identifier")) bindingIdentifier = parent.id;
|
|
45472
|
+
}
|
|
45473
|
+
if (!bindingIdentifier) return [];
|
|
45474
|
+
let scope = scopes.scopeFor(functionNode);
|
|
45475
|
+
while (scope) {
|
|
45476
|
+
const symbols = scope.symbols.filter((symbol) => symbol.bindingIdentifier === bindingIdentifier);
|
|
45477
|
+
if (symbols.length > 0) return symbols;
|
|
45478
|
+
scope = scope.parent;
|
|
45479
|
+
}
|
|
45480
|
+
return [];
|
|
45481
|
+
};
|
|
45482
|
+
const symbolHasReactComponentUse = (symbol, scopes, visitedSymbolIds = /* @__PURE__ */ new Set()) => {
|
|
45483
|
+
if (visitedSymbolIds.has(symbol.id)) return false;
|
|
45484
|
+
visitedSymbolIds.add(symbol.id);
|
|
45485
|
+
for (const reference of symbol.references) {
|
|
45486
|
+
const identifier = reference.identifier;
|
|
45487
|
+
if (hasSymbolWriteBefore(symbol, identifier, scopes)) continue;
|
|
45488
|
+
const parent = identifier.parent;
|
|
45489
|
+
if (isNodeOfType(parent, "JSXOpeningElement") && isNodeOfType(parent.name, "JSXIdentifier") && parent.name === identifier) return true;
|
|
45490
|
+
const expression = findTransparentExpressionRoot(identifier);
|
|
45491
|
+
const expressionParent = expression.parent;
|
|
45492
|
+
if (isNodeOfType(expressionParent, "CallExpression") && expressionParent.arguments[0] === expression && isReactApiCall(expressionParent, "createElement", scopes, { resolveNamedAliases: true })) return true;
|
|
45493
|
+
if (!isNodeOfType(expressionParent, "VariableDeclarator") || expressionParent.init !== expression || !isNodeOfType(expressionParent.id, "Identifier") || !isNodeOfType(expressionParent.parent, "VariableDeclaration") || expressionParent.parent.kind !== "const") continue;
|
|
45494
|
+
const aliasSymbol = scopes.symbolFor(expressionParent.id);
|
|
45495
|
+
if (aliasSymbol && symbolHasReactComponentUse(aliasSymbol, scopes, visitedSymbolIds)) return true;
|
|
45496
|
+
}
|
|
45497
|
+
return false;
|
|
45498
|
+
};
|
|
45499
|
+
const functionHasReactComponentUse = (functionNode, scopes) => {
|
|
45500
|
+
return functionBindingSymbols(functionNode, scopes).some((symbol) => symbolHasReactComponentUse(symbol, scopes));
|
|
45501
|
+
};
|
|
43590
45502
|
const isPreservedThroughConciseArrow = (callExpression, scopes) => {
|
|
43591
45503
|
let node = callExpression;
|
|
43592
45504
|
let parent = node.parent;
|
|
@@ -43633,7 +45545,10 @@ const noPropCallbackInRender = defineRule({
|
|
|
43633
45545
|
create: (context) => ({ CallExpression(node) {
|
|
43634
45546
|
if (!isResultDiscardedCall(node)) return;
|
|
43635
45547
|
if (isPreservedThroughConciseArrow(node, context.scopes)) return;
|
|
43636
|
-
|
|
45548
|
+
const renderPhaseOwner = findRenderPhaseComponentOrHook(node, context.scopes);
|
|
45549
|
+
if (!renderPhaseOwner) return;
|
|
45550
|
+
const renderPhaseOwnerName = componentOrHookDisplayNameForFunction(renderPhaseOwner);
|
|
45551
|
+
if (!renderPhaseOwnerName || !isReactHookName(renderPhaseOwnerName) && !functionHasReactComponentEvidence(renderPhaseOwner, context.scopes, context.cfg) && !functionHasReactComponentUse(renderPhaseOwner, context.scopes)) return;
|
|
43637
45552
|
const analysis = getProgramAnalysis(node);
|
|
43638
45553
|
if (!analysis) return;
|
|
43639
45554
|
const callee = stripParenExpression(node.callee);
|
|
@@ -44290,6 +46205,7 @@ const noRedundantRoles = defineRule({
|
|
|
44290
46205
|
create: (context) => {
|
|
44291
46206
|
const settings = resolveSettings$13(context.settings);
|
|
44292
46207
|
return { JSXOpeningElement(node) {
|
|
46208
|
+
if (isLocalTestScaffoldJsx(node, context)) return;
|
|
44293
46209
|
const roleAttr = hasJsxPropIgnoreCase(node.attributes, "role");
|
|
44294
46210
|
if (!roleAttr) return;
|
|
44295
46211
|
if (hasJsxPropIgnoreCase(node.attributes, "data-rac")) return;
|
|
@@ -44369,11 +46285,110 @@ const isSameRefCurrentMember = (node, refSymbol, scopes) => {
|
|
|
44369
46285
|
return isNodeOfType(receiver, "Identifier") && resolveConstIdentifierAlias(receiver, scopes)?.id === refSymbol.id;
|
|
44370
46286
|
};
|
|
44371
46287
|
const isSameRefCurrentAlias = (node, refSymbol, scopes) => {
|
|
44372
|
-
|
|
44373
|
-
if (
|
|
44374
|
-
|
|
46288
|
+
const expression = stripParenExpression(node);
|
|
46289
|
+
if (isSameRefCurrentMember(expression, refSymbol, scopes)) return true;
|
|
46290
|
+
if (!isNodeOfType(expression, "Identifier")) return false;
|
|
46291
|
+
const aliasSymbol = scopes.symbolFor(expression);
|
|
44375
46292
|
return aliasSymbol?.kind === "const" && aliasSymbol.initializer !== null && isSameRefCurrentMember(stripParenExpression(aliasSymbol.initializer), refSymbol, scopes);
|
|
44376
46293
|
};
|
|
46294
|
+
const resolveImmutableInitializationValue = (node, scopes, visitedSymbolIds = /* @__PURE__ */ new Set()) => {
|
|
46295
|
+
const expression = stripParenExpression(node);
|
|
46296
|
+
if (!isNodeOfType(expression, "Identifier")) return expression;
|
|
46297
|
+
const symbol = scopes.symbolFor(expression);
|
|
46298
|
+
if (!symbol || symbol.kind !== "const" || !symbol.initializer || symbol.references.some((reference) => reference.flag !== "read") || visitedSymbolIds.has(symbol.id)) return null;
|
|
46299
|
+
visitedSymbolIds.add(symbol.id);
|
|
46300
|
+
return resolveImmutableInitializationValue(symbol.initializer, scopes, visitedSymbolIds);
|
|
46301
|
+
};
|
|
46302
|
+
const isProvablyTruthyInitializationValue = (node, scopes) => {
|
|
46303
|
+
const expression = resolveImmutableInitializationValue(node, scopes);
|
|
46304
|
+
return Boolean(expression && (isNodeOfType(expression, "NewExpression") || isNodeOfType(expression, "ObjectExpression") || isNodeOfType(expression, "ArrayExpression") || isNodeOfType(expression, "ArrowFunctionExpression") || isNodeOfType(expression, "FunctionExpression") || isNodeOfType(expression, "ClassExpression")));
|
|
46305
|
+
};
|
|
46306
|
+
const getInitializationConstructorName = (node, scopes) => {
|
|
46307
|
+
const expression = resolveImmutableInitializationValue(node, scopes);
|
|
46308
|
+
if (!expression) return null;
|
|
46309
|
+
if (isNodeOfType(expression, "NewExpression")) {
|
|
46310
|
+
const callee = stripParenExpression(expression.callee);
|
|
46311
|
+
return isNodeOfType(callee, "Identifier") ? callee.name : null;
|
|
46312
|
+
}
|
|
46313
|
+
return null;
|
|
46314
|
+
};
|
|
46315
|
+
const isClosedTruthyTypeDomain = (typeNode, initializationValue, scopes) => {
|
|
46316
|
+
const initializationExpression = stripParenExpression(initializationValue);
|
|
46317
|
+
if (isNodeOfType(typeNode, "TSTypeLiteral")) return isNodeOfType(initializationExpression, "ObjectExpression");
|
|
46318
|
+
if (isNodeOfType(typeNode, "TSArrayType") || isNodeOfType(typeNode, "TSTupleType")) return isNodeOfType(initializationExpression, "ArrayExpression");
|
|
46319
|
+
if (isNodeOfType(typeNode, "TSFunctionType") || isNodeOfType(typeNode, "TSConstructorType")) return isNodeOfType(initializationExpression, "ArrowFunctionExpression") || isNodeOfType(initializationExpression, "FunctionExpression") || isNodeOfType(initializationExpression, "ClassExpression");
|
|
46320
|
+
if (isNodeOfType(typeNode, "TSObjectKeyword")) return true;
|
|
46321
|
+
if (!isNodeOfType(typeNode, "TSTypeReference")) return false;
|
|
46322
|
+
const typeName = typeNode.typeName;
|
|
46323
|
+
return isNodeOfType(typeName, "Identifier") && typeName.name === getInitializationConstructorName(initializationExpression, scopes);
|
|
46324
|
+
};
|
|
46325
|
+
const refHasClosedFalsySentinelDomain = (refSymbol, initializationValue, scopes) => {
|
|
46326
|
+
const initializer = refSymbol.initializer ? stripParenExpression(refSymbol.initializer) : null;
|
|
46327
|
+
if (!initializer || !isNodeOfType(initializer, "CallExpression")) return false;
|
|
46328
|
+
const [initialValue] = initializer.arguments ?? [];
|
|
46329
|
+
if (!initialValue || isNodeOfType(initialValue, "SpreadElement") || !isEmptySentinel(initialValue, scopes)) return false;
|
|
46330
|
+
const [declaredType] = initializer.typeArguments?.params ?? [];
|
|
46331
|
+
if (!declaredType || !isNodeOfType(declaredType, "TSUnionType")) return false;
|
|
46332
|
+
let hasEmptySentinel = false;
|
|
46333
|
+
let hasTruthyDomain = false;
|
|
46334
|
+
for (const memberType of declaredType.types ?? []) {
|
|
46335
|
+
if (isNodeOfType(memberType, "TSNullKeyword") || isNodeOfType(memberType, "TSUndefinedKeyword")) {
|
|
46336
|
+
hasEmptySentinel = true;
|
|
46337
|
+
continue;
|
|
46338
|
+
}
|
|
46339
|
+
if (!isClosedTruthyTypeDomain(memberType, initializationValue, scopes)) return false;
|
|
46340
|
+
hasTruthyDomain = true;
|
|
46341
|
+
}
|
|
46342
|
+
return hasEmptySentinel && hasTruthyDomain;
|
|
46343
|
+
};
|
|
46344
|
+
const isSafeRefIdentifierUse = (identifier) => {
|
|
46345
|
+
const expressionRoot = findTransparentExpressionRoot(identifier);
|
|
46346
|
+
const parent = expressionRoot.parent;
|
|
46347
|
+
if (parent && isNodeOfType(parent, "VariableDeclarator") && parent.id === expressionRoot && parent.parent !== null && isNodeOfType(parent.parent, "VariableDeclaration") && parent.parent.kind === "const") return true;
|
|
46348
|
+
if (parent && isNodeOfType(parent, "MemberExpression") && parent.object === expressionRoot && getStaticPropertyName(parent) === "current") return true;
|
|
46349
|
+
if (!parent || !isNodeOfType(parent, "VariableDeclarator") || parent.init !== expressionRoot) return false;
|
|
46350
|
+
return isNodeOfType(parent.id, "Identifier") && parent.parent !== null && isNodeOfType(parent.parent, "VariableDeclaration") && parent.parent.kind === "const";
|
|
46351
|
+
};
|
|
46352
|
+
const refDoesNotEscape = (branchRoot, refSymbol, scopes) => {
|
|
46353
|
+
let didEscape = false;
|
|
46354
|
+
walkAst(branchRoot, (child) => {
|
|
46355
|
+
if (didEscape) return false;
|
|
46356
|
+
if (!isNodeOfType(child, "Identifier")) return;
|
|
46357
|
+
if (resolveConstIdentifierAlias(child, scopes)?.id !== refSymbol.id) return;
|
|
46358
|
+
if (child === refSymbol.bindingIdentifier || isSafeRefIdentifierUse(child)) return;
|
|
46359
|
+
didEscape = true;
|
|
46360
|
+
return false;
|
|
46361
|
+
});
|
|
46362
|
+
return !didEscape;
|
|
46363
|
+
};
|
|
46364
|
+
const expressionContainsRefCurrent = (expression, refSymbol, scopes) => {
|
|
46365
|
+
let didFindRefCurrent = false;
|
|
46366
|
+
walkAst(expression, (child) => {
|
|
46367
|
+
if (didFindRefCurrent) return false;
|
|
46368
|
+
if (resolveReactRefSymbol(child, scopes)?.id !== refSymbol.id) return;
|
|
46369
|
+
didFindRefCurrent = true;
|
|
46370
|
+
return false;
|
|
46371
|
+
});
|
|
46372
|
+
return didFindRefCurrent;
|
|
46373
|
+
};
|
|
46374
|
+
const hasNoCompetingRefCurrentWrite = (branchRoot, assignmentExpression, refSymbol, scopes) => {
|
|
46375
|
+
let writeCount = 0;
|
|
46376
|
+
walkAst(branchRoot, (child) => {
|
|
46377
|
+
if (writeCount > 1) return false;
|
|
46378
|
+
if (isNodeOfType(child, "AssignmentExpression")) {
|
|
46379
|
+
if (expressionContainsRefCurrent(child.left, refSymbol, scopes)) writeCount++;
|
|
46380
|
+
return;
|
|
46381
|
+
}
|
|
46382
|
+
if (isNodeOfType(child, "UpdateExpression") || isNodeOfType(child, "UnaryExpression") && child.operator === "delete") {
|
|
46383
|
+
if (expressionContainsRefCurrent(child.argument, refSymbol, scopes)) writeCount++;
|
|
46384
|
+
return;
|
|
46385
|
+
}
|
|
46386
|
+
if (isNodeOfType(child, "ForInStatement") || isNodeOfType(child, "ForOfStatement")) {
|
|
46387
|
+
if (expressionContainsRefCurrent(child.left, refSymbol, scopes)) writeCount++;
|
|
46388
|
+
}
|
|
46389
|
+
});
|
|
46390
|
+
return writeCount === 1 && expressionContainsRefCurrent(assignmentExpression.left, refSymbol, scopes);
|
|
46391
|
+
};
|
|
44377
46392
|
const isEmptySentinel = (node, scopes) => isNodeOfType(node, "Literal") && node.value === null || isNodeOfType(node, "Identifier") && node.name === "undefined" && scopes.isGlobalReference(node);
|
|
44378
46393
|
const hasRepeatedExecutionAncestor = (node, stop) => {
|
|
44379
46394
|
let ancestor = node.parent;
|
|
@@ -44422,18 +46437,22 @@ const hasNoPriorCoExecutableWrite = (assignmentExpression, branchRoot, refSymbol
|
|
|
44422
46437
|
const isDocumentedLazyInitialization = (assignmentExpression, refSymbol, scopes) => {
|
|
44423
46438
|
if (assignmentExpression.operator === "??=" || assignmentExpression.operator === "||=") return true;
|
|
44424
46439
|
if (assignmentExpression.operator !== "=") return false;
|
|
46440
|
+
const renderOwner = findRenderPhaseComponentOrHook(assignmentExpression, scopes);
|
|
46441
|
+
if (!renderOwner) return false;
|
|
44425
46442
|
let descendant = assignmentExpression;
|
|
44426
46443
|
let ancestor = descendant.parent;
|
|
44427
46444
|
while (ancestor) {
|
|
44428
|
-
|
|
46445
|
+
const test = isNodeOfType(ancestor, "IfStatement") ? stripParenExpression(ancestor.test) : null;
|
|
46446
|
+
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;
|
|
46447
|
+
if (isNodeOfType(ancestor, "IfStatement") && isNodeOfType(test, "BinaryExpression") && [
|
|
44429
46448
|
"===",
|
|
44430
46449
|
"==",
|
|
44431
46450
|
"!==",
|
|
44432
46451
|
"!="
|
|
44433
|
-
].includes(
|
|
44434
|
-
const { left, right } =
|
|
46452
|
+
].includes(test.operator)) {
|
|
46453
|
+
const { left, right } = test;
|
|
44435
46454
|
const comparesEmptySentinel = isSameRefCurrentAlias(left, refSymbol, scopes) && isEmptySentinel(right, scopes) || isSameRefCurrentAlias(right, refSymbol, scopes) && isEmptySentinel(left, scopes);
|
|
44436
|
-
const guardedBranch =
|
|
46455
|
+
const guardedBranch = test.operator === "===" || test.operator === "==" ? ancestor.consequent : ancestor.alternate;
|
|
44437
46456
|
if (comparesEmptySentinel && guardedBranch === descendant && guardedBranch && !hasRepeatedExecutionAncestor(assignmentExpression, guardedBranch) && hasNoPriorCoExecutableWrite(assignmentExpression, guardedBranch, refSymbol, scopes)) return true;
|
|
44438
46457
|
}
|
|
44439
46458
|
descendant = ancestor;
|
|
@@ -45236,6 +47255,87 @@ const noResetAllStateOnPropChange = defineRule({
|
|
|
45236
47255
|
} })
|
|
45237
47256
|
});
|
|
45238
47257
|
//#endregion
|
|
47258
|
+
//#region src/plugin/utils/is-proven-framer-motion-jsx-element.ts
|
|
47259
|
+
const MOTION_FACTORY_MODULES = new Set(["framer-motion", "motion/react"]);
|
|
47260
|
+
const MOTION_TAG_NAMESPACE_MODULES = new Set([
|
|
47261
|
+
"framer-motion/client",
|
|
47262
|
+
"framer-motion/m",
|
|
47263
|
+
"motion/react-client",
|
|
47264
|
+
"motion/react-m"
|
|
47265
|
+
]);
|
|
47266
|
+
const MOTION_FACTORY_EXPORTS = new Set(["m", "motion"]);
|
|
47267
|
+
const getValueImportSource = (symbol) => {
|
|
47268
|
+
if (symbol.kind !== "import") return null;
|
|
47269
|
+
const declaration = symbol.declarationNode.parent;
|
|
47270
|
+
if (!declaration || !isNodeOfType(declaration, "ImportDeclaration") || isTypeOnlyImport(declaration) || isNodeOfType(symbol.declarationNode, "ImportSpecifier") && symbol.declarationNode.importKind === "type") return null;
|
|
47271
|
+
return typeof declaration.source.value === "string" ? declaration.source.value : null;
|
|
47272
|
+
};
|
|
47273
|
+
const getMemberParts = (node) => {
|
|
47274
|
+
if (isNodeOfType(node, "MemberExpression")) {
|
|
47275
|
+
const propertyName = getStaticPropertyName(node);
|
|
47276
|
+
return propertyName ? [node.object, propertyName] : null;
|
|
47277
|
+
}
|
|
47278
|
+
if (isNodeOfType(node, "JSXMemberExpression")) return isNodeOfType(node.property, "JSXIdentifier") ? [node.object, node.property.name] : null;
|
|
47279
|
+
return null;
|
|
47280
|
+
};
|
|
47281
|
+
const resolveSymbol = (node, scopes) => {
|
|
47282
|
+
if (!isNodeOfType(node, "Identifier") && !isNodeOfType(node, "JSXIdentifier")) return null;
|
|
47283
|
+
return resolveConstIdentifierAlias(node, scopes);
|
|
47284
|
+
};
|
|
47285
|
+
const isNamespaceFrom = (node, sources, scopes) => {
|
|
47286
|
+
const symbol = resolveSymbol(stripParenExpression(node), scopes);
|
|
47287
|
+
const source = symbol ? getValueImportSource(symbol) : null;
|
|
47288
|
+
return Boolean(source && sources.has(source) && symbol && isNodeOfType(symbol.declarationNode, "ImportNamespaceSpecifier"));
|
|
47289
|
+
};
|
|
47290
|
+
const isMotionFactory = (rawNode, scopes, visitedSymbolIds) => {
|
|
47291
|
+
const node = stripParenExpression(rawNode);
|
|
47292
|
+
if (isNamespaceFrom(node, MOTION_TAG_NAMESPACE_MODULES, scopes)) return true;
|
|
47293
|
+
const symbol = resolveSymbol(node, scopes);
|
|
47294
|
+
if (symbol?.kind === "import") {
|
|
47295
|
+
const source = getValueImportSource(symbol);
|
|
47296
|
+
const importedName = getImportedName(symbol.declarationNode);
|
|
47297
|
+
return Boolean(source && MOTION_FACTORY_MODULES.has(source) && importedName && MOTION_FACTORY_EXPORTS.has(importedName));
|
|
47298
|
+
}
|
|
47299
|
+
if (symbol?.kind === "const" && symbol.initializer) {
|
|
47300
|
+
if (visitedSymbolIds.has(symbol.id)) return false;
|
|
47301
|
+
visitedSymbolIds.add(symbol.id);
|
|
47302
|
+
return isMotionFactory(symbol.initializer, scopes, visitedSymbolIds);
|
|
47303
|
+
}
|
|
47304
|
+
const memberParts = getMemberParts(node);
|
|
47305
|
+
return Boolean(memberParts && MOTION_FACTORY_EXPORTS.has(memberParts[1]) && isNamespaceFrom(memberParts[0], MOTION_FACTORY_MODULES, scopes));
|
|
47306
|
+
};
|
|
47307
|
+
const isMotionComponent = (rawNode, scopes) => {
|
|
47308
|
+
return isMotionComponentWithVisitedSymbols(rawNode, scopes, /* @__PURE__ */ new Set());
|
|
47309
|
+
};
|
|
47310
|
+
const isMotionComponentWithVisitedSymbols = (rawNode, scopes, visitedSymbolIds) => {
|
|
47311
|
+
const node = stripParenExpression(rawNode);
|
|
47312
|
+
const symbol = resolveSymbol(node, scopes);
|
|
47313
|
+
if (symbol?.kind === "const" && symbol.initializer) {
|
|
47314
|
+
if (visitedSymbolIds.has(symbol.id)) return false;
|
|
47315
|
+
visitedSymbolIds.add(symbol.id);
|
|
47316
|
+
return isMotionComponentWithVisitedSymbols(symbol.initializer, scopes, visitedSymbolIds);
|
|
47317
|
+
}
|
|
47318
|
+
if (symbol?.kind === "import") {
|
|
47319
|
+
const source = getValueImportSource(symbol);
|
|
47320
|
+
return Boolean(source && MOTION_TAG_NAMESPACE_MODULES.has(source) && isNodeOfType(symbol.declarationNode, "ImportSpecifier") && getImportedName(symbol.declarationNode) !== "create");
|
|
47321
|
+
}
|
|
47322
|
+
const memberParts = getMemberParts(node);
|
|
47323
|
+
if (memberParts && isMotionFactory(memberParts[0], scopes, visitedSymbolIds)) return true;
|
|
47324
|
+
if (!isNodeOfType(node, "CallExpression")) return false;
|
|
47325
|
+
if (isMotionFactory(node.callee, scopes, visitedSymbolIds)) return true;
|
|
47326
|
+
const calleeMemberParts = getMemberParts(stripParenExpression(node.callee));
|
|
47327
|
+
return Boolean(calleeMemberParts && calleeMemberParts[1] === "create" && isMotionFactory(calleeMemberParts[0], scopes, visitedSymbolIds));
|
|
47328
|
+
};
|
|
47329
|
+
const isProvenFramerMotionJsxElement = (openingElement, scopes) => {
|
|
47330
|
+
const elementName = openingElement.name;
|
|
47331
|
+
if (isNodeOfType(elementName, "JSXIdentifier")) {
|
|
47332
|
+
if (/^[a-z]/.test(elementName.name)) return false;
|
|
47333
|
+
return isMotionComponent(elementName, scopes);
|
|
47334
|
+
}
|
|
47335
|
+
const memberParts = getMemberParts(elementName);
|
|
47336
|
+
return Boolean(memberParts && isMotionFactory(memberParts[0], scopes, /* @__PURE__ */ new Set()));
|
|
47337
|
+
};
|
|
47338
|
+
//#endregion
|
|
45239
47339
|
//#region src/plugin/rules/performance/no-scale-from-zero.ts
|
|
45240
47340
|
const noScaleFromZero = defineRule({
|
|
45241
47341
|
id: "no-scale-from-zero",
|
|
@@ -45246,6 +47346,8 @@ const noScaleFromZero = defineRule({
|
|
|
45246
47346
|
create: (context) => ({ JSXAttribute(node) {
|
|
45247
47347
|
if (!isNodeOfType(node.name, "JSXIdentifier")) return;
|
|
45248
47348
|
if (node.name.name !== "initial" && node.name.name !== "exit") return;
|
|
47349
|
+
const openingElement = node.parent;
|
|
47350
|
+
if (!openingElement || !isNodeOfType(openingElement, "JSXOpeningElement") || !Object.is(getAuthoritativeJsxAttribute(openingElement.attributes, node.name.name), node) || !isProvenFramerMotionJsxElement(openingElement, context.scopes)) return;
|
|
45249
47351
|
if (!isNodeOfType(node.value, "JSXExpressionContainer")) return;
|
|
45250
47352
|
const expression = node.value.expression;
|
|
45251
47353
|
if (!isNodeOfType(expression, "ObjectExpression")) return;
|
|
@@ -45444,7 +47546,7 @@ const isSelfReferentialSentinelValue = (variableName, literalValue) => literalVa
|
|
|
45444
47546
|
const isIdentifierLikeKeyNameValue = (literalValue) => {
|
|
45445
47547
|
const wordSegments = literalValue.replace(/^[_$\s]+|[_$\s]+$/g, "").split(/[_\-:./$]+/).filter((segment) => segment.length > 0);
|
|
45446
47548
|
if (wordSegments.length < 2) return false;
|
|
45447
|
-
return wordSegments.every((segment) => /^[a-z]
|
|
47549
|
+
return wordSegments.every((segment) => /^[a-z]+(?:[A-Z][a-z]+)*$/.test(segment));
|
|
45448
47550
|
};
|
|
45449
47551
|
const FRAMEWORK_ENV_ADVICE = [
|
|
45450
47552
|
[
|
|
@@ -45518,7 +47620,7 @@ const noSecretsInClientCode = defineRule({
|
|
|
45518
47620
|
const isServerOnlyScope = isInsideServerOnlyScope(node);
|
|
45519
47621
|
const trailingSuffix = getIdentifierTrailingWord(variableName);
|
|
45520
47622
|
const isUiConstant = SECRET_FALSE_POSITIVE_SUFFIXES.has(trailingSuffix);
|
|
45521
|
-
if (shouldUseVariableNameHeuristic && !isServerOnlyScope && SECRET_VARIABLE_PATTERN.test(variableName) && !isUiConstant && !isPublicUrlValue(literalValue) && !isPlaceholderValueForVariableHeuristic && !isSelfReferentialSentinelValue(variableName, literalValue) && !isIdentifierLikeKeyNameValue(literalValue) &&
|
|
47623
|
+
if (shouldUseVariableNameHeuristic && !isServerOnlyScope && SECRET_VARIABLE_PATTERN.test(variableName) && !isUiConstant && !isPublicUrlValue(literalValue) && !isPlaceholderValueForVariableHeuristic && !isSelfReferentialSentinelValue(variableName, literalValue) && !isIdentifierLikeKeyNameValue(literalValue) && literalValue.length > 24) {
|
|
45522
47624
|
context.report({
|
|
45523
47625
|
node,
|
|
45524
47626
|
message: `Hardcoding "${variableName}" in client code is a security vulnerability: the secret ships to the browser where anyone can read it.`
|
|
@@ -50683,10 +52785,9 @@ const isSameApplicationChannelInstance = (targetText, fileContent) => {
|
|
|
50683
52785
|
return new RegExp(`(?<![\\w$.])${escapeRegExp(receiverRoot)}\\s*${SAME_APPLICATION_CHANNEL_CONSTRUCTOR_SOURCE}`).test(fileContent);
|
|
50684
52786
|
};
|
|
50685
52787
|
const WORKER_FILE_PATH_PATTERN = /worker/i;
|
|
50686
|
-
const getNodeStartIndex = (node) => "start" in node && typeof node.start === "number" ? node.start : -1;
|
|
50687
52788
|
const getNodeText = (content, node) => {
|
|
50688
52789
|
const startIndex = getNodeStartIndex(node);
|
|
50689
|
-
const endIndex =
|
|
52790
|
+
const endIndex = getNodeEndIndex(node);
|
|
50690
52791
|
if (startIndex < 0 || endIndex < 0) return "";
|
|
50691
52792
|
return content.slice(startIndex, endIndex);
|
|
50692
52793
|
};
|
|
@@ -51083,17 +53184,6 @@ const preferEs6Class = defineRule({
|
|
|
51083
53184
|
}
|
|
51084
53185
|
});
|
|
51085
53186
|
//#endregion
|
|
51086
|
-
//#region src/plugin/utils/is-jsx-element-or-fragment.ts
|
|
51087
|
-
/**
|
|
51088
|
-
* Type-guard for the two single-node JSX output forms: `JSXElement`
|
|
51089
|
-
* (`<Foo />`) and `JSXFragment` (`<>…</>`). Canonical home for the
|
|
51090
|
-
* `isNodeOfType(x, "JSXElement") || isNodeOfType(x, "JSXFragment")` check
|
|
51091
|
-
* that many rules otherwise inline. Does NOT unwrap parens / TS wrappers —
|
|
51092
|
-
* callers that need the semantic expression should `stripParenExpression`
|
|
51093
|
-
* first.
|
|
51094
|
-
*/
|
|
51095
|
-
const isJsxElementOrFragment = (node) => Boolean(node && (isNodeOfType(node, "JSXElement") || isNodeOfType(node, "JSXFragment")));
|
|
51096
|
-
//#endregion
|
|
51097
53187
|
//#region src/plugin/rules/architecture/prefer-explicit-variants.ts
|
|
51098
53188
|
const resolveBooleanPropTestName = (testNode, booleanPropBindings) => {
|
|
51099
53189
|
let identifierNode = stripParenExpression(testNode);
|
|
@@ -51558,20 +53648,51 @@ const preferModuleScopePureFunction = defineRule({
|
|
|
51558
53648
|
}
|
|
51559
53649
|
});
|
|
51560
53650
|
//#endregion
|
|
53651
|
+
//#region src/plugin/utils/get-require-call-source.ts
|
|
53652
|
+
const getRequireCallSource = (expression) => {
|
|
53653
|
+
const unwrappedExpression = stripParenExpression(expression);
|
|
53654
|
+
if (isNodeOfType(unwrappedExpression, "MemberExpression")) return getRequireCallSource(unwrappedExpression.object);
|
|
53655
|
+
if (!isNodeOfType(unwrappedExpression, "CallExpression")) return null;
|
|
53656
|
+
if (!isNodeOfType(unwrappedExpression.callee, "Identifier") || unwrappedExpression.callee.name !== "require") return null;
|
|
53657
|
+
const [firstArgument] = unwrappedExpression.arguments ?? [];
|
|
53658
|
+
if (!firstArgument || !isNodeOfType(firstArgument, "Literal")) return null;
|
|
53659
|
+
return typeof firstArgument.value === "string" ? firstArgument.value : null;
|
|
53660
|
+
};
|
|
53661
|
+
//#endregion
|
|
53662
|
+
//#region src/plugin/utils/is-proven-node-crypto-namespace-reference.ts
|
|
53663
|
+
const NODE_CRYPTO_MODULE_SOURCES = new Set(["crypto", "node:crypto"]);
|
|
53664
|
+
const isProvenNodeCryptoNamespaceReference = (expression, scopes) => {
|
|
53665
|
+
const identifier = stripParenExpression(expression);
|
|
53666
|
+
if (!isNodeOfType(identifier, "Identifier")) return false;
|
|
53667
|
+
const symbol = resolveConstIdentifierAlias(identifier, scopes);
|
|
53668
|
+
if (!symbol) return false;
|
|
53669
|
+
if (symbol.kind === "import") {
|
|
53670
|
+
const importBinding = getImportBindingForName(identifier, symbol.name);
|
|
53671
|
+
return Boolean(importBinding && NODE_CRYPTO_MODULE_SOURCES.has(importBinding.source));
|
|
53672
|
+
}
|
|
53673
|
+
return Boolean(symbol.kind === "const" && symbol.initializer && NODE_CRYPTO_MODULE_SOURCES.has(getRequireCallSource(symbol.initializer) ?? ""));
|
|
53674
|
+
};
|
|
53675
|
+
//#endregion
|
|
51561
53676
|
//#region src/plugin/rules/architecture/prefer-module-scope-static-value.ts
|
|
51562
53677
|
const MUTATING_RECEIVER_METHOD_NAMES = new Set([...MUTATING_ARRAY_METHODS, ...MUTATING_COLLECTION_METHODS]);
|
|
51563
53678
|
const isMutationContext = (referenceIdentifier) => {
|
|
51564
|
-
|
|
51565
|
-
|
|
51566
|
-
|
|
51567
|
-
|
|
51568
|
-
|
|
51569
|
-
|
|
51570
|
-
|
|
51571
|
-
|
|
51572
|
-
if (isNodeOfType(
|
|
51573
|
-
|
|
51574
|
-
|
|
53679
|
+
let mutationTarget = referenceIdentifier;
|
|
53680
|
+
let receiverMethodName = null;
|
|
53681
|
+
while (mutationTarget.parent) {
|
|
53682
|
+
const parent = mutationTarget.parent;
|
|
53683
|
+
if (TRANSPARENT_EXPRESSION_WRAPPER_TYPES.has(parent.type) && "expression" in parent && parent.expression === mutationTarget) {
|
|
53684
|
+
mutationTarget = parent;
|
|
53685
|
+
continue;
|
|
53686
|
+
}
|
|
53687
|
+
if (isNodeOfType(parent, "MemberExpression") && parent.object === mutationTarget) {
|
|
53688
|
+
receiverMethodName = getStaticPropertyName(parent);
|
|
53689
|
+
mutationTarget = parent;
|
|
53690
|
+
continue;
|
|
53691
|
+
}
|
|
53692
|
+
if (isNodeOfType(parent, "AssignmentExpression") && parent.left === mutationTarget) return true;
|
|
53693
|
+
if (isNodeOfType(parent, "UpdateExpression") && parent.argument === mutationTarget) return true;
|
|
53694
|
+
if (isNodeOfType(parent, "UnaryExpression") && parent.operator === "delete" && parent.argument === mutationTarget) return true;
|
|
53695
|
+
return Boolean(isNodeOfType(parent, "CallExpression") && parent.callee === mutationTarget && MUTATING_RECEIVER_METHOD_NAMES.has(receiverMethodName ?? ""));
|
|
51575
53696
|
}
|
|
51576
53697
|
return false;
|
|
51577
53698
|
};
|
|
@@ -51671,9 +53792,9 @@ const isImpureCall = (node, scopes) => {
|
|
|
51671
53792
|
const callee = node.callee;
|
|
51672
53793
|
if (isNodeOfType(callee, "Identifier")) return isImpureBareCallee(callee, scopes);
|
|
51673
53794
|
if (!isNodeOfType(callee, "MemberExpression") || callee.computed) return false;
|
|
51674
|
-
if (!isNodeOfType(callee.object, "Identifier")) return false;
|
|
51675
53795
|
if (!isNodeOfType(callee.property, "Identifier")) return false;
|
|
51676
|
-
|
|
53796
|
+
for (const [receiverName, receiverMethodNames] of IMPURE_MEMBER_RECEIVERS) if (receiverMethodNames.has(callee.property.name) && (isProvenGlobalNamespaceReference(callee.object, receiverName, scopes) || receiverName === "crypto" && isProvenNodeCryptoNamespaceReference(callee.object, scopes))) return true;
|
|
53797
|
+
return false;
|
|
51677
53798
|
};
|
|
51678
53799
|
const containsImpureExpression = (expression, scopes) => {
|
|
51679
53800
|
let foundImpure = false;
|
|
@@ -51948,6 +54069,15 @@ const STABLE_REACT_HOOK_VALUE_NAMES = new Set([
|
|
|
51948
54069
|
"useState",
|
|
51949
54070
|
"useTransition"
|
|
51950
54071
|
]);
|
|
54072
|
+
const REGISTRATION_METHOD_BY_RELEASE_METHOD = new Map([
|
|
54073
|
+
["off", "on"],
|
|
54074
|
+
["removeEventListener", "addEventListener"],
|
|
54075
|
+
["removeListener", "addListener"],
|
|
54076
|
+
["unlisten", "listen"],
|
|
54077
|
+
["unsub", "sub"],
|
|
54078
|
+
["unsubscribe", "subscribe"],
|
|
54079
|
+
["unwatch", "watch"]
|
|
54080
|
+
]);
|
|
51951
54081
|
const isStableReactHookDependency = (dependency, context) => {
|
|
51952
54082
|
const unwrappedDependency = stripParenExpression(dependency);
|
|
51953
54083
|
if (!isNodeOfType(unwrappedDependency, "Identifier")) return false;
|
|
@@ -52013,30 +54143,87 @@ const isCallExpressionWithSubHandlerCallee = (callExpression) => {
|
|
|
52013
54143
|
if (isNodeOfType(callee, "MemberExpression") && isNodeOfType(callee.property, "Identifier") && SUBSCRIPTION_METHOD_NAMES.has(callee.property.name)) return true;
|
|
52014
54144
|
return false;
|
|
52015
54145
|
};
|
|
52016
|
-
const
|
|
54146
|
+
const getStaticMemberCallMethodName = (callExpression) => {
|
|
54147
|
+
if (!isNodeOfType(callExpression, "CallExpression")) return null;
|
|
54148
|
+
const callee = callExpression.callee;
|
|
54149
|
+
return isNodeOfType(callee, "MemberExpression") && !callee.computed && isNodeOfType(callee.property, "Identifier") ? callee.property.name : null;
|
|
54150
|
+
};
|
|
54151
|
+
const getCallArgumentUse = (reference) => {
|
|
54152
|
+
const argument = findTransparentExpressionRoot(reference);
|
|
54153
|
+
const parent = argument.parent;
|
|
54154
|
+
if (!isNodeOfType(parent, "CallExpression")) return null;
|
|
54155
|
+
const argumentIndex = (parent.arguments ?? []).findIndex((candidateArgument) => candidateArgument === argument);
|
|
54156
|
+
return argumentIndex === -1 ? null : {
|
|
54157
|
+
callExpression: parent,
|
|
54158
|
+
argumentIndex
|
|
54159
|
+
};
|
|
54160
|
+
};
|
|
54161
|
+
const isMatchingRegistrationAndRelease = (registration, release, context) => {
|
|
54162
|
+
const releaseMethodName = getStaticMemberCallMethodName(release.callExpression);
|
|
54163
|
+
const expectedRegistrationMethod = releaseMethodName ? REGISTRATION_METHOD_BY_RELEASE_METHOD.get(releaseMethodName) : null;
|
|
54164
|
+
if (getStaticMemberCallMethodName(registration.callExpression) !== expectedRegistrationMethod) return false;
|
|
54165
|
+
if (registration.argumentIndex !== release.argumentIndex) return false;
|
|
54166
|
+
const registrationCallee = registration.callExpression.callee;
|
|
54167
|
+
const releaseCallee = release.callExpression.callee;
|
|
54168
|
+
if (!isNodeOfType(registrationCallee, "MemberExpression") || !isNodeOfType(releaseCallee, "MemberExpression")) return false;
|
|
54169
|
+
const registrationReceiverKey = resolveExpressionKey$1(registrationCallee.object, context);
|
|
54170
|
+
if (registrationReceiverKey === null || registrationReceiverKey !== resolveExpressionKey$1(releaseCallee.object, context)) return false;
|
|
54171
|
+
const registrationArguments = registration.callExpression.arguments ?? [];
|
|
54172
|
+
const releaseArguments = release.callExpression.arguments ?? [];
|
|
54173
|
+
if (registrationArguments.length !== releaseArguments.length) return false;
|
|
54174
|
+
return registrationArguments.every((registrationArgument, argumentIndex) => {
|
|
54175
|
+
if (argumentIndex === registration.argumentIndex) return true;
|
|
54176
|
+
const registrationArgumentKey = resolveExpressionKey$1(registrationArgument, context);
|
|
54177
|
+
return registrationArgumentKey !== null && registrationArgumentKey === resolveExpressionKey$1(releaseArguments[argumentIndex], context);
|
|
54178
|
+
});
|
|
54179
|
+
};
|
|
54180
|
+
const findExclusiveSubHandlerCall = (enclosingFunction, context) => {
|
|
52017
54181
|
const directParent = enclosingFunction.parent;
|
|
52018
54182
|
if (isNodeOfType(directParent, "CallExpression") && (directParent.arguments ?? []).some((arg) => arg === enclosingFunction) && isCallExpressionWithSubHandlerCallee(directParent)) return directParent;
|
|
52019
|
-
const
|
|
52020
|
-
if (
|
|
52021
|
-
let
|
|
52022
|
-
|
|
52023
|
-
|
|
52024
|
-
|
|
52025
|
-
|
|
52026
|
-
|
|
52027
|
-
|
|
52028
|
-
return false;
|
|
54183
|
+
const bindingIdentifier = getFunctionBindingIdentifier$1(enclosingFunction);
|
|
54184
|
+
if (!bindingIdentifier) return null;
|
|
54185
|
+
let bindingSymbol = context.scopes.symbolFor(bindingIdentifier);
|
|
54186
|
+
if (isNodeOfType(enclosingFunction, "FunctionDeclaration")) {
|
|
54187
|
+
let bindingScope = context.scopes.scopeFor(enclosingFunction);
|
|
54188
|
+
bindingSymbol = null;
|
|
54189
|
+
while (bindingScope && !bindingSymbol) {
|
|
54190
|
+
bindingSymbol = bindingScope.symbols.find((candidateSymbol) => candidateSymbol.declarationNode === enclosingFunction) ?? null;
|
|
54191
|
+
bindingScope = bindingScope.parent;
|
|
52029
54192
|
}
|
|
52030
|
-
}
|
|
52031
|
-
return
|
|
54193
|
+
}
|
|
54194
|
+
if (!bindingSymbol) return null;
|
|
54195
|
+
const registrations = [];
|
|
54196
|
+
const releases = [];
|
|
54197
|
+
for (const reference of bindingSymbol.references) {
|
|
54198
|
+
if (isAstDescendant(reference.identifier, enclosingFunction)) continue;
|
|
54199
|
+
if (reference.identifier === bindingIdentifier) continue;
|
|
54200
|
+
if (reference.flag !== "read") return null;
|
|
54201
|
+
const receivingUse = getCallArgumentUse(reference.identifier);
|
|
54202
|
+
if (!receivingUse) return null;
|
|
54203
|
+
if (isCallExpressionWithSubHandlerCallee(receivingUse.callExpression)) {
|
|
54204
|
+
registrations.push(receivingUse);
|
|
54205
|
+
continue;
|
|
54206
|
+
}
|
|
54207
|
+
const methodName = getStaticMemberCallMethodName(receivingUse.callExpression);
|
|
54208
|
+
if (!methodName || !REGISTRATION_METHOD_BY_RELEASE_METHOD.has(methodName)) return null;
|
|
54209
|
+
releases.push(receivingUse);
|
|
54210
|
+
}
|
|
54211
|
+
if (releases.some((release) => !registrations.some((registration) => isMatchingRegistrationAndRelease(registration, release, context)))) return null;
|
|
54212
|
+
return registrations[0]?.callExpression ?? null;
|
|
52032
54213
|
};
|
|
52033
|
-
const classifyCallableReadsInsideEffect = (
|
|
54214
|
+
const classifyCallableReadsInsideEffect = (callableIdentifier, effectCallback, context) => {
|
|
52034
54215
|
let hasAnyRead = false;
|
|
52035
54216
|
let allReadsAreInSubHandlers = true;
|
|
52036
54217
|
let firstSubHandlerName = null;
|
|
54218
|
+
const callableSymbol = context.scopes.symbolFor(callableIdentifier);
|
|
54219
|
+
if (!callableSymbol) return {
|
|
54220
|
+
hasAnyRead,
|
|
54221
|
+
allReadsAreInSubHandlers,
|
|
54222
|
+
firstSubHandlerName
|
|
54223
|
+
};
|
|
52037
54224
|
walkAst(effectCallback, (child) => {
|
|
52038
54225
|
if (!isNodeOfType(child, "Identifier")) return;
|
|
52039
|
-
if (child
|
|
54226
|
+
if (context.scopes.symbolFor(child)?.id !== callableSymbol.id) return;
|
|
52040
54227
|
const parent = child.parent;
|
|
52041
54228
|
if (isNodeOfType(parent, "ArrayExpression")) return;
|
|
52042
54229
|
if (isNodeOfType(parent, "MemberExpression") && !parent.computed && parent.property === child) return;
|
|
@@ -52047,7 +54234,7 @@ const classifyCallableReadsInsideEffect = (callableName, effectCallback) => {
|
|
|
52047
54234
|
allReadsAreInSubHandlers = false;
|
|
52048
54235
|
return;
|
|
52049
54236
|
}
|
|
52050
|
-
const subHandlerCall =
|
|
54237
|
+
const subHandlerCall = findExclusiveSubHandlerCall(enclosingFunction, context);
|
|
52051
54238
|
if (!subHandlerCall) {
|
|
52052
54239
|
allReadsAreInSubHandlers = false;
|
|
52053
54240
|
return;
|
|
@@ -52090,7 +54277,7 @@ const preferUseEffectEvent = defineRule({
|
|
|
52090
54277
|
const isFunctionTypedPropDep = propStackTracker.isPropName(depName) && REACT_HANDLER_PROP_PATTERN.test(depName);
|
|
52091
54278
|
const isFunctionTypedLocalDep = potentiallyChangingCallbackBindings.has(depName);
|
|
52092
54279
|
if (!isFunctionTypedPropDep && !isFunctionTypedLocalDep) continue;
|
|
52093
|
-
const classification = classifyCallableReadsInsideEffect(
|
|
54280
|
+
const classification = classifyCallableReadsInsideEffect(depElement, callback, context);
|
|
52094
54281
|
if (!classification.hasAnyRead) continue;
|
|
52095
54282
|
if (!classification.allReadsAreInSubHandlers) continue;
|
|
52096
54283
|
const subHandlerLabel = classification.firstSubHandlerName ? `\`${classification.firstSubHandlerName}\`` : "an async sub-handler";
|
|
@@ -53405,12 +55592,7 @@ const DOMPURIFY_MODULES = new Set(["dompurify", "isomorphic-dompurify"]);
|
|
|
53405
55592
|
const REACT_MARKDOWN_NAMED_EXPORTS = new Set(["MarkdownAsync", "MarkdownHooks"]);
|
|
53406
55593
|
const REACT_MARKDOWN_NAMESPACE_EXPORTS = new Set(["default", ...REACT_MARKDOWN_NAMED_EXPORTS]);
|
|
53407
55594
|
const DEFAULT_EXPORT_NAMES = new Set(["default"]);
|
|
53408
|
-
const
|
|
53409
|
-
if (symbol.kind !== "import") return null;
|
|
53410
|
-
const importDeclaration = symbol.declarationNode.parent;
|
|
53411
|
-
return isNodeOfType(importDeclaration, "ImportDeclaration") ? importDeclaration : null;
|
|
53412
|
-
};
|
|
53413
|
-
const isImportFromModule = (symbol, moduleName) => getImportDeclaration(symbol)?.source.value === moduleName;
|
|
55595
|
+
const isImportFromModule = (symbol, moduleName) => getImportDeclarationForSymbol(symbol)?.source.value === moduleName;
|
|
53414
55596
|
const isDefaultImportSymbol = (symbol, moduleName) => {
|
|
53415
55597
|
if (!isImportFromModule(symbol, moduleName)) return false;
|
|
53416
55598
|
return isNodeOfType(symbol.declarationNode, "ImportDefaultSpecifier") || getImportedName(symbol.declarationNode) === "default";
|
|
@@ -53500,7 +55682,7 @@ const getAttributeExpression = (attribute) => {
|
|
|
53500
55682
|
const isDomPurifyNamespace = (node, scopes) => {
|
|
53501
55683
|
const symbol = resolveImportedIdentifier(node, scopes);
|
|
53502
55684
|
if (!symbol) return false;
|
|
53503
|
-
const importDeclaration =
|
|
55685
|
+
const importDeclaration = getImportDeclarationForSymbol(symbol);
|
|
53504
55686
|
if (!importDeclaration || !DOMPURIFY_MODULES.has(String(importDeclaration.source.value))) return false;
|
|
53505
55687
|
return isNodeOfType(symbol.declarationNode, "ImportDefaultSpecifier") || isNodeOfType(symbol.declarationNode, "ImportNamespaceSpecifier") || getImportedName(symbol.declarationNode) === "default";
|
|
53506
55688
|
};
|
|
@@ -56442,16 +58624,6 @@ const rnListCallbackPerRow = defineRule({
|
|
|
56442
58624
|
}
|
|
56443
58625
|
});
|
|
56444
58626
|
//#endregion
|
|
56445
|
-
//#region src/plugin/utils/get-require-call-source.ts
|
|
56446
|
-
const getRequireCallSource = (expression) => {
|
|
56447
|
-
if (isNodeOfType(expression, "MemberExpression")) return getRequireCallSource(expression.object);
|
|
56448
|
-
if (!isNodeOfType(expression, "CallExpression")) return null;
|
|
56449
|
-
if (!isNodeOfType(expression.callee, "Identifier") || expression.callee.name !== "require") return null;
|
|
56450
|
-
const [firstArgument] = expression.arguments ?? [];
|
|
56451
|
-
if (!firstArgument || !isNodeOfType(firstArgument, "Literal")) return null;
|
|
56452
|
-
return typeof firstArgument.value === "string" ? firstArgument.value : null;
|
|
56453
|
-
};
|
|
56454
|
-
//#endregion
|
|
56455
58627
|
//#region src/plugin/utils/get-initializer-module-source.ts
|
|
56456
58628
|
const getInitializerModuleSource = (contextNode, initializer) => {
|
|
56457
58629
|
const requireSource = getRequireCallSource(initializer);
|
|
@@ -56484,7 +58656,7 @@ const resolveImportedRecyclerName = (node, localName, options) => {
|
|
|
56484
58656
|
const jsxMemberObjectName = options?.allowNamespaceMemberAccess ? getJsxMemberObjectName(node) : null;
|
|
56485
58657
|
for (const [canonicalName, packageSources] of Object.entries(RECYCLABLE_LIST_PACKAGES)) {
|
|
56486
58658
|
if (jsxMemberObjectName !== null) {
|
|
56487
|
-
if (localName === canonicalName && packageSources.some((packageSource) => isNamespaceImportFromModule(node, jsxMemberObjectName, packageSource))) return canonicalName;
|
|
58659
|
+
if (localName === canonicalName && packageSources.some((packageSource) => isNamespaceImportFromModule$1(node, jsxMemberObjectName, packageSource))) return canonicalName;
|
|
56488
58660
|
continue;
|
|
56489
58661
|
}
|
|
56490
58662
|
if (packageSources.some((packageSource) => getImportedNameFromModule(node, localName, packageSource) === canonicalName)) return canonicalName;
|
|
@@ -57743,7 +59915,7 @@ const isNamedImportOf = (contextNode, localName, componentName) => {
|
|
|
57743
59915
|
return false;
|
|
57744
59916
|
};
|
|
57745
59917
|
const isExpoUiNamespaceImport = (contextNode, localName) => {
|
|
57746
|
-
for (const moduleSource of EXPO_UI_MODULE_SOURCES) if (isNamespaceImportFromModule(contextNode, localName, moduleSource)) return true;
|
|
59918
|
+
for (const moduleSource of EXPO_UI_MODULE_SOURCES) if (isNamespaceImportFromModule$1(contextNode, localName, moduleSource)) return true;
|
|
57747
59919
|
return false;
|
|
57748
59920
|
};
|
|
57749
59921
|
const isExpoUiComponentElement = (openingElement, contextNode, componentName) => {
|
|
@@ -58891,6 +61063,7 @@ const roleHasRequiredAriaProps = defineRule({
|
|
|
58891
61063
|
recommendation: "Add every required `aria-*` attribute so assistive tech can expose the role's state correctly.",
|
|
58892
61064
|
category: "Accessibility",
|
|
58893
61065
|
create: (context) => ({ JSXOpeningElement(node) {
|
|
61066
|
+
if (isLocalTestScaffoldJsx(node, context)) return;
|
|
58894
61067
|
const elementType = getElementType(node, context.settings);
|
|
58895
61068
|
if (!HTML_TAGS.has(elementType)) return;
|
|
58896
61069
|
const roleAttribute = hasJsxPropIgnoreCase(node.attributes, "role");
|
|
@@ -62026,6 +64199,7 @@ const roleSupportsAriaProps = defineRule({
|
|
|
62026
64199
|
recommendation: "Only use `aria-*` attributes that the element's role supports.",
|
|
62027
64200
|
category: "Accessibility",
|
|
62028
64201
|
create: (context) => ({ JSXOpeningElement(node) {
|
|
64202
|
+
if (isLocalTestScaffoldJsx(node, context)) return;
|
|
62029
64203
|
let ariaAttributes = null;
|
|
62030
64204
|
for (const attribute of node.attributes) {
|
|
62031
64205
|
if (!isNodeOfType(attribute, "JSXAttribute")) continue;
|