oxlint-plugin-react-doctor 0.7.9-dev.c7f61cd → 0.7.9-dev.c8a2918
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 +2089 -370
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -1765,7 +1765,7 @@ const isImportedFromModule = (contextNode, localIdentifierName, moduleSource) =>
|
|
|
1765
1765
|
if (!info) return false;
|
|
1766
1766
|
return info.source === moduleSource;
|
|
1767
1767
|
};
|
|
1768
|
-
const isNamespaceImportFromModule = (contextNode, localIdentifierName, moduleSource) => {
|
|
1768
|
+
const isNamespaceImportFromModule$1 = (contextNode, localIdentifierName, moduleSource) => {
|
|
1769
1769
|
const lookup = getImportLookup(contextNode);
|
|
1770
1770
|
if (!lookup) return false;
|
|
1771
1771
|
const info = lookup.get(localIdentifierName);
|
|
@@ -1851,7 +1851,7 @@ const GENERATED_IMAGE_RENDERER_MODULES = [
|
|
|
1851
1851
|
"satori"
|
|
1852
1852
|
];
|
|
1853
1853
|
const IMAGE_RESPONSE_MODULES = new Set(["next/og", "@vercel/og"]);
|
|
1854
|
-
const getImportDeclaration
|
|
1854
|
+
const getImportDeclaration = (node) => {
|
|
1855
1855
|
let current = node.parent;
|
|
1856
1856
|
while (current) {
|
|
1857
1857
|
if (isNodeOfType(current, "ImportDeclaration")) return current;
|
|
@@ -1865,7 +1865,7 @@ const isNamedImport = (symbol, importedName, moduleSources) => {
|
|
|
1865
1865
|
if (symbol.kind !== "import") return false;
|
|
1866
1866
|
const declaration = symbol.declarationNode;
|
|
1867
1867
|
if (!isNodeOfType(declaration, "ImportSpecifier")) return false;
|
|
1868
|
-
const importDeclaration = getImportDeclaration
|
|
1868
|
+
const importDeclaration = getImportDeclaration(declaration);
|
|
1869
1869
|
const source = importDeclaration ? getImportSource(importDeclaration) : null;
|
|
1870
1870
|
if (!source || !moduleSources.has(source)) return false;
|
|
1871
1871
|
const imported = declaration.imported;
|
|
@@ -1874,7 +1874,7 @@ const isNamedImport = (symbol, importedName, moduleSources) => {
|
|
|
1874
1874
|
const isSatoriImport = (symbol) => {
|
|
1875
1875
|
if (symbol.kind !== "import") return false;
|
|
1876
1876
|
const declaration = symbol.declarationNode;
|
|
1877
|
-
const importDeclaration = getImportDeclaration
|
|
1877
|
+
const importDeclaration = getImportDeclaration(declaration);
|
|
1878
1878
|
if (!importDeclaration || getImportSource(importDeclaration) !== "satori") return false;
|
|
1879
1879
|
if (isNodeOfType(declaration, "ImportDefaultSpecifier")) return true;
|
|
1880
1880
|
if (!isNodeOfType(declaration, "ImportSpecifier")) return false;
|
|
@@ -1895,7 +1895,7 @@ const isGeneratedImageRendererCall = (node, scopes) => {
|
|
|
1895
1895
|
if (!symbol || symbol.kind !== "import") return false;
|
|
1896
1896
|
const declaration = symbol.declarationNode;
|
|
1897
1897
|
if (!isNodeOfType(declaration, "ImportNamespaceSpecifier")) return false;
|
|
1898
|
-
const importDeclaration = getImportDeclaration
|
|
1898
|
+
const importDeclaration = getImportDeclaration(declaration);
|
|
1899
1899
|
const source = importDeclaration ? getImportSource(importDeclaration) : null;
|
|
1900
1900
|
return Boolean(source && IMAGE_RESPONSE_MODULES.has(source));
|
|
1901
1901
|
};
|
|
@@ -2115,6 +2115,386 @@ const isGeneratedImageRenderContext = (context, node) => {
|
|
|
2115
2115
|
return false;
|
|
2116
2116
|
};
|
|
2117
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
|
|
2118
2498
|
//#region src/plugin/utils/object-has-accessible-child.ts
|
|
2119
2499
|
const objectHasAccessibleChild = (jsxElement, settings) => {
|
|
2120
2500
|
for (const child of jsxElement.children) {
|
|
@@ -2283,6 +2663,7 @@ const altText = defineRule({
|
|
|
2283
2663
|
const inputImageAliases = new Set(settings["input[type=\"image\"]"] ?? []);
|
|
2284
2664
|
const fileHasJsxA11ySettings = hasJsxA11ySettings(context.settings);
|
|
2285
2665
|
return { JSXOpeningElement(node) {
|
|
2666
|
+
if (isLocalTestScaffoldJsx(node, context)) return;
|
|
2286
2667
|
if (!fileHasJsxA11ySettings && isNodeOfType(node.name, "JSXIdentifier")) {
|
|
2287
2668
|
const rawName = node.name.name;
|
|
2288
2669
|
if (rawName !== "img" && rawName !== "object" && rawName !== "area" && rawName.toLowerCase() !== "input" && !imgAliases.has(rawName) && !objectAliases.has(rawName) && !areaAliases.has(rawName) && !inputImageAliases.has(rawName)) return;
|
|
@@ -4151,252 +4532,6 @@ const artifactSecretLeak = defineRule({
|
|
|
4151
4532
|
scan: (file) => scanArtifactLeak(file, (content) => SECRET_VALUE_PATTERNS.find((pattern) => pattern.test(content)), "A browser-delivered artifact contains a secret-looking credential value.")
|
|
4152
4533
|
});
|
|
4153
4534
|
//#endregion
|
|
4154
|
-
//#region src/plugin/constants/js.ts
|
|
4155
|
-
const LOOP_TYPES = [
|
|
4156
|
-
"ForStatement",
|
|
4157
|
-
"ForInStatement",
|
|
4158
|
-
"ForOfStatement",
|
|
4159
|
-
"WhileStatement",
|
|
4160
|
-
"DoWhileStatement"
|
|
4161
|
-
];
|
|
4162
|
-
const FUNCTION_LIKE_TYPES = new Set([
|
|
4163
|
-
"FunctionDeclaration",
|
|
4164
|
-
"FunctionExpression",
|
|
4165
|
-
"ArrowFunctionExpression"
|
|
4166
|
-
]);
|
|
4167
|
-
const BUILTIN_GLOBAL_NAMESPACE_NAMES = new Set([
|
|
4168
|
-
"Math",
|
|
4169
|
-
"Date",
|
|
4170
|
-
"JSON",
|
|
4171
|
-
"Object",
|
|
4172
|
-
"Array",
|
|
4173
|
-
"Number",
|
|
4174
|
-
"String",
|
|
4175
|
-
"Boolean",
|
|
4176
|
-
"RegExp",
|
|
4177
|
-
"Symbol",
|
|
4178
|
-
"BigInt",
|
|
4179
|
-
"Reflect"
|
|
4180
|
-
]);
|
|
4181
|
-
const MUTATING_ARRAY_METHODS = new Set([
|
|
4182
|
-
"push",
|
|
4183
|
-
"pop",
|
|
4184
|
-
"shift",
|
|
4185
|
-
"unshift",
|
|
4186
|
-
"splice",
|
|
4187
|
-
"sort",
|
|
4188
|
-
"reverse",
|
|
4189
|
-
"fill",
|
|
4190
|
-
"copyWithin"
|
|
4191
|
-
]);
|
|
4192
|
-
const MUTATING_COLLECTION_METHODS = new Set([
|
|
4193
|
-
"add",
|
|
4194
|
-
"clear",
|
|
4195
|
-
"delete",
|
|
4196
|
-
"set"
|
|
4197
|
-
]);
|
|
4198
|
-
const CHAINABLE_ITERATION_METHODS = new Set([
|
|
4199
|
-
"map",
|
|
4200
|
-
"filter",
|
|
4201
|
-
"forEach",
|
|
4202
|
-
"flatMap"
|
|
4203
|
-
]);
|
|
4204
|
-
const ITERATOR_PRODUCING_METHOD_NAMES = new Set([
|
|
4205
|
-
"values",
|
|
4206
|
-
"keys",
|
|
4207
|
-
"entries"
|
|
4208
|
-
]);
|
|
4209
|
-
const BROWSER_TEST_FILE_PATTERN = /\.browser\.[cm]?[jt]sx?$/;
|
|
4210
|
-
const TEST_LIBRARY_IMPORT_SOURCES = new Set([
|
|
4211
|
-
"vitest",
|
|
4212
|
-
"jest",
|
|
4213
|
-
"mocha",
|
|
4214
|
-
"chai",
|
|
4215
|
-
"sinon",
|
|
4216
|
-
"expect",
|
|
4217
|
-
"ava",
|
|
4218
|
-
"uvu",
|
|
4219
|
-
"node:test",
|
|
4220
|
-
"bun:test",
|
|
4221
|
-
"@testing-library/react",
|
|
4222
|
-
"@testing-library/react-native",
|
|
4223
|
-
"@testing-library/react-hooks",
|
|
4224
|
-
"@testing-library/dom",
|
|
4225
|
-
"@testing-library/user-event",
|
|
4226
|
-
"@testing-library/jest-dom",
|
|
4227
|
-
"@testing-library/vue",
|
|
4228
|
-
"@testing-library/svelte",
|
|
4229
|
-
"@testing-library/preact",
|
|
4230
|
-
"@testing-library/cypress",
|
|
4231
|
-
"playwright",
|
|
4232
|
-
"playwright-core",
|
|
4233
|
-
"@playwright/test",
|
|
4234
|
-
"@playwright/experimental-ct-react",
|
|
4235
|
-
"@playwright/experimental-ct-react17",
|
|
4236
|
-
"cypress",
|
|
4237
|
-
"@cypress/react",
|
|
4238
|
-
"@cypress/react18",
|
|
4239
|
-
"@storybook/test",
|
|
4240
|
-
"@storybook/test-runner",
|
|
4241
|
-
"@storybook/testing-library",
|
|
4242
|
-
"@storybook/jest",
|
|
4243
|
-
"puppeteer",
|
|
4244
|
-
"puppeteer-core",
|
|
4245
|
-
"webdriverio",
|
|
4246
|
-
"@wdio/globals",
|
|
4247
|
-
"@nuxt/test-utils"
|
|
4248
|
-
]);
|
|
4249
|
-
const TEST_LIBRARY_IMPORT_SOURCE_PREFIXES = [
|
|
4250
|
-
"vitest/",
|
|
4251
|
-
"@vitest/",
|
|
4252
|
-
"@jest/",
|
|
4253
|
-
"@testing-library/",
|
|
4254
|
-
"@playwright/",
|
|
4255
|
-
"@storybook/test/",
|
|
4256
|
-
"@storybook/test-runner/",
|
|
4257
|
-
"@storybook/testing-library/",
|
|
4258
|
-
"@cypress/",
|
|
4259
|
-
"@nuxt/test-utils/"
|
|
4260
|
-
];
|
|
4261
|
-
const ORDERED_UI_FLOW_CALLEE_NAMES = new Set([
|
|
4262
|
-
"render",
|
|
4263
|
-
"rerender",
|
|
4264
|
-
"renderHook",
|
|
4265
|
-
"renderToString",
|
|
4266
|
-
"renderToStaticMarkup",
|
|
4267
|
-
"act",
|
|
4268
|
-
"click",
|
|
4269
|
-
"dblClick",
|
|
4270
|
-
"dblclick",
|
|
4271
|
-
"tripleClick",
|
|
4272
|
-
"tap",
|
|
4273
|
-
"press",
|
|
4274
|
-
"longPress",
|
|
4275
|
-
"type",
|
|
4276
|
-
"clear",
|
|
4277
|
-
"fill",
|
|
4278
|
-
"focus",
|
|
4279
|
-
"blur",
|
|
4280
|
-
"hover",
|
|
4281
|
-
"unhover",
|
|
4282
|
-
"check",
|
|
4283
|
-
"uncheck",
|
|
4284
|
-
"selectOption",
|
|
4285
|
-
"selectOptions",
|
|
4286
|
-
"setChecked",
|
|
4287
|
-
"setInputFiles",
|
|
4288
|
-
"scrollIntoViewIfNeeded",
|
|
4289
|
-
"dragTo",
|
|
4290
|
-
"dragAndDrop",
|
|
4291
|
-
"drop",
|
|
4292
|
-
"evaluate",
|
|
4293
|
-
"evaluateHandle",
|
|
4294
|
-
"waitFor",
|
|
4295
|
-
"waitForLoadState",
|
|
4296
|
-
"waitForSelector",
|
|
4297
|
-
"waitForURL",
|
|
4298
|
-
"waitForResponse",
|
|
4299
|
-
"waitForRequest",
|
|
4300
|
-
"waitForEvent",
|
|
4301
|
-
"waitForFunction",
|
|
4302
|
-
"waitForElementToBeRemoved",
|
|
4303
|
-
"goto",
|
|
4304
|
-
"goBack",
|
|
4305
|
-
"goForward",
|
|
4306
|
-
"reload",
|
|
4307
|
-
"screenshot",
|
|
4308
|
-
"snapshot",
|
|
4309
|
-
"toMatchSnapshot",
|
|
4310
|
-
"toMatchInlineSnapshot",
|
|
4311
|
-
"expect",
|
|
4312
|
-
"expectTypeOf",
|
|
4313
|
-
"step",
|
|
4314
|
-
"describe",
|
|
4315
|
-
"test",
|
|
4316
|
-
"it",
|
|
4317
|
-
"beforeAll",
|
|
4318
|
-
"beforeEach",
|
|
4319
|
-
"afterAll",
|
|
4320
|
-
"afterEach",
|
|
4321
|
-
"play",
|
|
4322
|
-
"userEvent",
|
|
4323
|
-
"screen",
|
|
4324
|
-
"within"
|
|
4325
|
-
]);
|
|
4326
|
-
const ORDERED_UI_FLOW_CALLEE_PREFIXES = ["findBy", "findAllBy"];
|
|
4327
|
-
const INTENTIONAL_SEQUENCING_CALLEE_NAMES = new Set([
|
|
4328
|
-
"sleep",
|
|
4329
|
-
"delay",
|
|
4330
|
-
"wait",
|
|
4331
|
-
"pause",
|
|
4332
|
-
"throttle",
|
|
4333
|
-
"debounce",
|
|
4334
|
-
"tick",
|
|
4335
|
-
"nextTick",
|
|
4336
|
-
"advanceTimersByTime",
|
|
4337
|
-
"advanceTimersByTimeAsync",
|
|
4338
|
-
"runAllTimers",
|
|
4339
|
-
"runAllTimersAsync",
|
|
4340
|
-
"runOnlyPendingTimers",
|
|
4341
|
-
"runOnlyPendingTimersAsync",
|
|
4342
|
-
"setTimeout",
|
|
4343
|
-
"setInterval",
|
|
4344
|
-
"setImmediate",
|
|
4345
|
-
"queueMicrotask",
|
|
4346
|
-
"requestAnimationFrame",
|
|
4347
|
-
"requestIdleCallback",
|
|
4348
|
-
"animate",
|
|
4349
|
-
"transition",
|
|
4350
|
-
"spring",
|
|
4351
|
-
"tween",
|
|
4352
|
-
"stagger",
|
|
4353
|
-
"sequence",
|
|
4354
|
-
"timeline",
|
|
4355
|
-
"scrub",
|
|
4356
|
-
"query",
|
|
4357
|
-
"execute",
|
|
4358
|
-
"exec",
|
|
4359
|
-
"raw",
|
|
4360
|
-
"transaction",
|
|
4361
|
-
"$transaction",
|
|
4362
|
-
"$executeRaw",
|
|
4363
|
-
"$queryRaw",
|
|
4364
|
-
"$executeRawUnsafe",
|
|
4365
|
-
"$queryRawUnsafe",
|
|
4366
|
-
"begin",
|
|
4367
|
-
"commit",
|
|
4368
|
-
"rollback",
|
|
4369
|
-
"savepoint",
|
|
4370
|
-
"lock",
|
|
4371
|
-
"unlock",
|
|
4372
|
-
"spawn",
|
|
4373
|
-
"spawnSync",
|
|
4374
|
-
"execSync",
|
|
4375
|
-
"execFile",
|
|
4376
|
-
"execFileSync",
|
|
4377
|
-
"fork",
|
|
4378
|
-
"$",
|
|
4379
|
-
"sh",
|
|
4380
|
-
"mkdir",
|
|
4381
|
-
"rmdir",
|
|
4382
|
-
"rename",
|
|
4383
|
-
"rm",
|
|
4384
|
-
"unlink",
|
|
4385
|
-
"writeFile",
|
|
4386
|
-
"appendFile",
|
|
4387
|
-
"copyFile",
|
|
4388
|
-
"navigate",
|
|
4389
|
-
"goto",
|
|
4390
|
-
"waitForNavigation",
|
|
4391
|
-
"waitForURL",
|
|
4392
|
-
"waitForLoadState",
|
|
4393
|
-
"waitForResponse",
|
|
4394
|
-
"waitForRequest",
|
|
4395
|
-
"waitForSelector",
|
|
4396
|
-
"waitForFunction",
|
|
4397
|
-
"waitForEvent"
|
|
4398
|
-
]);
|
|
4399
|
-
//#endregion
|
|
4400
4535
|
//#region src/plugin/constants/ts-type-position-keys.ts
|
|
4401
4536
|
const TYPE_POSITION_CHILD_KEYS = new Set([
|
|
4402
4537
|
"implements",
|
|
@@ -4522,13 +4657,6 @@ const containsDirectAwait = (node) => {
|
|
|
4522
4657
|
return foundAwait;
|
|
4523
4658
|
};
|
|
4524
4659
|
//#endregion
|
|
4525
|
-
//#region src/plugin/utils/find-transparent-expression-root.ts
|
|
4526
|
-
const findTransparentExpressionRoot = (node) => {
|
|
4527
|
-
let current = node;
|
|
4528
|
-
while (current.parent && TRANSPARENT_EXPRESSION_WRAPPER_TYPES.has(current.parent.type)) current = current.parent;
|
|
4529
|
-
return current;
|
|
4530
|
-
};
|
|
4531
|
-
//#endregion
|
|
4532
4660
|
//#region src/plugin/utils/get-static-property-key-name.ts
|
|
4533
4661
|
const getStaticPropertyKeyName = (node, options = {}) => {
|
|
4534
4662
|
if (!isNodeOfType(node, "Property") && !isNodeOfType(node, "MethodDefinition") && !isNodeOfType(node, "MemberExpression")) return null;
|
|
@@ -4788,16 +4916,6 @@ const hasStaticPropertyWriteBefore = (identifier, propertyName, referenceNode, s
|
|
|
4788
4916
|
return getEquivalentSymbols(identifier, scopes).some((symbol) => symbolHasStaticPropertyWriteBefore(symbol, propertyName, referenceNode, scopes));
|
|
4789
4917
|
};
|
|
4790
4918
|
//#endregion
|
|
4791
|
-
//#region src/plugin/utils/find-enclosing-function.ts
|
|
4792
|
-
const findEnclosingFunction$1 = (node) => {
|
|
4793
|
-
let cursor = node.parent;
|
|
4794
|
-
while (cursor) {
|
|
4795
|
-
if (isFunctionLike$1(cursor)) return cursor;
|
|
4796
|
-
cursor = cursor.parent ?? null;
|
|
4797
|
-
}
|
|
4798
|
-
return null;
|
|
4799
|
-
};
|
|
4800
|
-
//#endregion
|
|
4801
4919
|
//#region src/plugin/utils/has-symbol-write-before.ts
|
|
4802
4920
|
const hasSymbolWriteBefore = (symbol, referenceNode, scopes) => symbol.references.some((reference) => {
|
|
4803
4921
|
if (reference.flag === "read") return false;
|
|
@@ -6299,13 +6417,6 @@ const getCalleeIdentifierTrail = (call) => {
|
|
|
6299
6417
|
return trail;
|
|
6300
6418
|
};
|
|
6301
6419
|
//#endregion
|
|
6302
|
-
//#region src/plugin/utils/is-test-library-import-source.ts
|
|
6303
|
-
const isTestLibraryImportSource = (source) => {
|
|
6304
|
-
if (typeof source !== "string" || source.length === 0) return false;
|
|
6305
|
-
if (TEST_LIBRARY_IMPORT_SOURCES.has(source)) return true;
|
|
6306
|
-
return TEST_LIBRARY_IMPORT_SOURCE_PREFIXES.some((prefix) => source.startsWith(prefix));
|
|
6307
|
-
};
|
|
6308
|
-
//#endregion
|
|
6309
6420
|
//#region src/plugin/rules/js-performance/async-parallel.ts
|
|
6310
6421
|
const getAwaitedCall = (statement) => {
|
|
6311
6422
|
if (isNodeOfType(statement, "VariableDeclaration")) {
|
|
@@ -8267,7 +8378,27 @@ const collectFunctionReturnStatements = (functionNode) => {
|
|
|
8267
8378
|
//#region src/plugin/utils/statement-always-exits.ts
|
|
8268
8379
|
const statementAlwaysExits = (statement) => {
|
|
8269
8380
|
if (isNodeOfType(statement, "ReturnStatement") || isNodeOfType(statement, "ThrowStatement")) return true;
|
|
8270
|
-
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
|
+
}
|
|
8271
8402
|
if (!isNodeOfType(statement, "BlockStatement")) return false;
|
|
8272
8403
|
return statement.body.some((childStatement) => statementAlwaysExits(childStatement));
|
|
8273
8404
|
};
|
|
@@ -10386,10 +10517,10 @@ const controlHasAssociatedLabel = defineRule({
|
|
|
10386
10517
|
if (isTestlikeFile) return;
|
|
10387
10518
|
const opening = node.openingElement;
|
|
10388
10519
|
const tagName = getElementType(opening, context.settings);
|
|
10389
|
-
if (tagName
|
|
10520
|
+
if (rendersLabelElement(tagName, opening) && hasAccessibleLabelText(node, checkContext) && !isInsideJsxAttribute(node)) {
|
|
10390
10521
|
const htmlForAttribute = hasJsxPropIgnoreCase(opening.attributes, HTML_FOR_ATTRIBUTE);
|
|
10391
10522
|
for (const htmlForKey of getAttributeMatchKeys(htmlForAttribute)) labelHtmlForKeys.add(htmlForKey);
|
|
10392
|
-
collectLabelEmbeddedNames(node, 1, checkContext, labelEmbeddedNames);
|
|
10523
|
+
if (tagName === LABEL_ELEMENT) collectLabelEmbeddedNames(node, 1, checkContext, labelEmbeddedNames);
|
|
10393
10524
|
}
|
|
10394
10525
|
if (DEFAULT_IGNORE_ELEMENTS.includes(tagName)) return;
|
|
10395
10526
|
if (settings.ignoreElements.includes(tagName)) return;
|
|
@@ -10475,6 +10606,1048 @@ const findMatchingBracket = (content, openIndex) => {
|
|
|
10475
10606
|
return -1;
|
|
10476
10607
|
};
|
|
10477
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
|
|
10478
11651
|
//#region src/plugin/rules/security-scan/dangerous-html-sink.ts
|
|
10479
11652
|
const DANGEROUS_HTML_PATTERN = /dangerouslySetInnerHTML|(?:\.(?:inner|outer)HTML|\[\s*["'](?:inner|outer)HTML["']\s*\])\s*[+]?=(?!=)|\.insertAdjacentHTML\s*\(|\bdocument\.write(?:ln)?\s*\(|\.(?:createContextualFragment|setHTMLUnsafe)\s*\(/;
|
|
10480
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]*)/;
|
|
@@ -10820,6 +11993,7 @@ const dangerousHtmlSink = defineRule({
|
|
|
10820
11993
|
if (HIDDEN_TOOLING_DIRECTORY_PATTERN.test(file.relativePath)) return [];
|
|
10821
11994
|
if (SANITIZER_WRAPPER_PATH_PATTERN.test(file.relativePath)) return [];
|
|
10822
11995
|
if (!DANGEROUS_HTML_PATTERN.test(file.content)) return [];
|
|
11996
|
+
const katexSinkProofRanges = collectKatexSinkProofRanges(file.content, file.absolutePath);
|
|
10823
11997
|
const findings = [];
|
|
10824
11998
|
const lines = file.content.split("\n");
|
|
10825
11999
|
for (let lineIndex = 0; lineIndex < lines.length; lineIndex += 1) {
|
|
@@ -10835,6 +12009,9 @@ const dangerousHtmlSink = defineRule({
|
|
|
10835
12009
|
const terminatorIndex = valueTail.search(/[;}]/);
|
|
10836
12010
|
const valueExpression = terminatorIndex >= 0 ? valueTail.slice(0, terminatorIndex + 1) : valueTail;
|
|
10837
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;
|
|
10838
12015
|
if (STRING_LITERAL_VALUE_PATTERN.test(valueExpression)) continue;
|
|
10839
12016
|
if (MODULE_CONSTANT_VALUE_PATTERN.test(valueExpression)) continue;
|
|
10840
12017
|
if (DOM_CONTENT_SOURCE_VALUE_PATTERN.test(valueExpression) && !valueExpression.includes("+")) {
|
|
@@ -10855,21 +12032,21 @@ const dangerousHtmlSink = defineRule({
|
|
|
10855
12032
|
if (templateInterpolations === "") continue;
|
|
10856
12033
|
const judgedExpression = templateInterpolations ?? valueExpression;
|
|
10857
12034
|
const doesJudgedExpressionCombineValues = splitTopLevelByPlus(judgedExpression).length > 1 || (templateInterpolations?.match(/\$\{/g)?.length ?? 0) > 1;
|
|
10858
|
-
if (!doesJudgedExpressionCombineValues && SANITIZER_PATTERN.test(judgedExpression)) continue;
|
|
12035
|
+
if (!hasUnsafeKatexProof && !doesJudgedExpressionCombineValues && SANITIZER_PATTERN.test(judgedExpression)) continue;
|
|
10859
12036
|
if (!doesJudgedExpressionCombineValues && ENV_CONFIG_VALUE_PATTERN.test(judgedExpression)) continue;
|
|
10860
12037
|
if (!doesJudgedExpressionCombineValues && I18N_VALUE_PATTERN.test(judgedExpression)) continue;
|
|
10861
|
-
if (!isHtmlTainted(judgedExpression, file.content, sinkIndex, /* @__PURE__ */ new Set(), /* @__PURE__ */ new Set())) continue;
|
|
10862
|
-
if (ESCAPING_SERIALIZER_CALL_PATTERN.test(valueExpression)) continue;
|
|
10863
|
-
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;
|
|
10864
12041
|
if (valueIdentifier !== void 0) {
|
|
10865
12042
|
const escapedIdentifier = escapeRegExp(valueIdentifier);
|
|
10866
12043
|
const visibleDeclaration = findVisibleIdentifierDeclaration(valueIdentifier, sinkIndex, file.content);
|
|
10867
12044
|
const visibleInitializer = visibleDeclaration?.initializer;
|
|
10868
12045
|
if (!(visibleInitializer !== void 0 && (splitTopLevelByPlus(visibleInitializer).length > 1 || (visibleInitializer.match(/\$\{/g)?.length ?? 0) > 1))) {
|
|
10869
12046
|
const fromSerializer = new RegExp(`\\b${escapedIdentifier}\\b\\s*${SERIALIZER_ASSIGNMENT_PATTERN.source}`, "i");
|
|
10870
|
-
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;
|
|
10871
12048
|
const fromSanitizer = new RegExp(`\\b${escapedIdentifier}\\b\\s*${SANITIZED_ASSIGNMENT_PATTERN.source}`, "i");
|
|
10872
|
-
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;
|
|
10873
12050
|
}
|
|
10874
12051
|
if (new RegExp(`\\b${escapedIdentifier}\\b\\s*${DOM_CONTENT_ASSIGNMENT_PATTERN.source}`).test(file.content)) continue;
|
|
10875
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;
|
|
@@ -12559,6 +13736,7 @@ const isNodeReachableWithinFunction = (node, context) => {
|
|
|
12559
13736
|
const OBSERVER_REGISTRATION_METHOD_NAME = "observe";
|
|
12560
13737
|
const CLEANUP_EFFECT_HOOK_NAMES = new Set([...EFFECT_HOOK_NAMES$1, "useInsertionEffect"]);
|
|
12561
13738
|
const REPLAYABLE_ITERATOR_COLLECTION_CACHE = /* @__PURE__ */ new WeakMap();
|
|
13739
|
+
const REACT_REF_EFFECT_ANALYSIS_CACHE = /* @__PURE__ */ new WeakMap();
|
|
12562
13740
|
const RESOURCE_NOUN_BY_KIND = {
|
|
12563
13741
|
subscribe: "subscription",
|
|
12564
13742
|
timer: "timer",
|
|
@@ -13383,7 +14561,7 @@ const hasGuardedDeferredCleanup = (callback, usage, cleanupReturns, context) =>
|
|
|
13383
14561
|
const effectHasCleanupForUsage = (callback, usage, context) => {
|
|
13384
14562
|
if (!isNodeOfType(callback, "ArrowFunctionExpression") && !isNodeOfType(callback, "FunctionExpression")) return false;
|
|
13385
14563
|
if (callback.async) return false;
|
|
13386
|
-
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;
|
|
13387
14565
|
if (!isNodeOfType(callback.body, "BlockStatement")) return callback.body === usage.node && isCleanupReturningSubscribeLikeCallExpression(callback.body);
|
|
13388
14566
|
const matchingCleanupReturns = [];
|
|
13389
14567
|
walkInsideStatementBlocks(callback.body, (child) => {
|
|
@@ -13569,6 +14747,8 @@ const isReleaseReachableForUsage = (releaseNode, usage, context) => {
|
|
|
13569
14747
|
const releaseFunction = findEnclosingFunction$1(releaseNode);
|
|
13570
14748
|
if (!releaseFunction) return true;
|
|
13571
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);
|
|
13572
14752
|
return isPotentiallyReachableFunction(releaseFunction, context);
|
|
13573
14753
|
};
|
|
13574
14754
|
const fileContainsReleaseForUsage = (usage, context) => {
|
|
@@ -13809,7 +14989,23 @@ const isUseSyncExternalStoreSubscribeFunction = (functionNode, context) => {
|
|
|
13809
14989
|
};
|
|
13810
14990
|
return isSubscribeBinding(bindingIdentifier);
|
|
13811
14991
|
};
|
|
13812
|
-
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;
|
|
13813
15009
|
let currentNode = resourceNode;
|
|
13814
15010
|
let parentNode = currentNode.parent;
|
|
13815
15011
|
while (parentNode) {
|
|
@@ -13820,22 +15016,33 @@ const doesResourceResultEscape = (resourceNode, allowConciseReturnEscape) => {
|
|
|
13820
15016
|
parentNode = currentNode.parent;
|
|
13821
15017
|
continue;
|
|
13822
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
|
+
}
|
|
13823
15029
|
return false;
|
|
13824
15030
|
}
|
|
13825
15031
|
return false;
|
|
13826
15032
|
};
|
|
13827
|
-
const findRetainedFunctionLeak = (retainedFunction, context) => {
|
|
15033
|
+
const findRetainedFunctionLeak = (retainedFunction, context, options) => {
|
|
13828
15034
|
if (!isFunctionLike$1(retainedFunction)) return null;
|
|
13829
15035
|
const body = retainedFunction.body;
|
|
13830
15036
|
if (!body) return null;
|
|
13831
15037
|
let leak = null;
|
|
13832
|
-
const
|
|
15038
|
+
const allowReturnedResourceEscape = options?.allowReturnedResourceEscape !== false && !retainedFunction.async && !isInlineRetainedHandlerFunction(retainedFunction, context);
|
|
15039
|
+
const allowReturnedSocketEscape = allowReturnedResourceEscape && options?.requireCallableReturnedResource !== true;
|
|
13833
15040
|
const isExternalStoreSubscribeFunction = isUseSyncExternalStoreSubscribeFunction(retainedFunction, context);
|
|
13834
15041
|
const hasReleaseForUsage = (usage) => isExternalStoreSubscribeFunction ? effectHasCleanupForUsage(retainedFunction, usage, context) : fileContainsReleaseForUsage(usage, context) || hasGuaranteedRefOwnedUnmountCleanup(retainedFunction, usage, context);
|
|
13835
15042
|
walkAst(body, (child) => {
|
|
13836
15043
|
if (leak !== null) return false;
|
|
13837
15044
|
if (isFunctionLike$1(child)) return false;
|
|
13838
|
-
if (isSocketConstruction(child) && !doesResourceResultEscape(child, false)) {
|
|
15045
|
+
if (isSocketConstruction(child) && !doesResourceResultEscape(child, allowReturnedSocketEscape, false, context)) {
|
|
13839
15046
|
const socketUsage = {
|
|
13840
15047
|
kind: "socket",
|
|
13841
15048
|
node: child,
|
|
@@ -13852,14 +15059,14 @@ const findRetainedFunctionLeak = (retainedFunction, context) => {
|
|
|
13852
15059
|
}
|
|
13853
15060
|
}
|
|
13854
15061
|
if (!isNodeOfType(child, "CallExpression")) return;
|
|
13855
|
-
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))) {
|
|
13856
15063
|
const timerUsage = {
|
|
13857
15064
|
kind: "timer",
|
|
13858
15065
|
node: child,
|
|
13859
|
-
resourceName:
|
|
15066
|
+
resourceName: child.callee.name,
|
|
13860
15067
|
handleKey: findAssignedResourceKey(child, context),
|
|
13861
15068
|
receiverKey: null,
|
|
13862
|
-
registrationVerbName:
|
|
15069
|
+
registrationVerbName: child.callee.name,
|
|
13863
15070
|
eventKey: null,
|
|
13864
15071
|
handlerKey: null
|
|
13865
15072
|
};
|
|
@@ -13868,7 +15075,7 @@ const findRetainedFunctionLeak = (retainedFunction, context) => {
|
|
|
13868
15075
|
return false;
|
|
13869
15076
|
}
|
|
13870
15077
|
}
|
|
13871
|
-
if (isSubscribeOrObserveCall(child) && !doesResourceResultEscape(child,
|
|
15078
|
+
if (isSubscribeOrObserveCall(child) && (!doesResourceResultEscape(child, allowReturnedResourceEscape, allowReturnedResourceEscape, context) || options?.requireCallableReturnedResource === true && !isCleanupReturningSubscribeLikeCallExpression(child))) {
|
|
13872
15079
|
const registrationDetails = getCallRegistrationDetails(child, context);
|
|
13873
15080
|
const subscriptionUsage = {
|
|
13874
15081
|
kind: "subscribe",
|
|
@@ -13883,6 +15090,184 @@ const findRetainedFunctionLeak = (retainedFunction, context) => {
|
|
|
13883
15090
|
});
|
|
13884
15091
|
return leak;
|
|
13885
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
|
+
};
|
|
13886
15271
|
const isRetainedComponentScopeFunction = (functionNode) => {
|
|
13887
15272
|
if (isNodeOfType(functionNode, "FunctionDeclaration")) return enclosingComponentOrHookName(functionNode) !== null;
|
|
13888
15273
|
if (!isNodeOfType(functionNode, "ArrowFunctionExpression") && !isNodeOfType(functionNode, "FunctionExpression")) return false;
|
|
@@ -13917,8 +15302,14 @@ const effectNeedsCleanup = defineRule({
|
|
|
13917
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.",
|
|
13918
15303
|
create: (context) => {
|
|
13919
15304
|
const reportRetainedLeak = (retainedFunction) => {
|
|
13920
|
-
|
|
13921
|
-
|
|
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);
|
|
13922
15313
|
if (!leak) return;
|
|
13923
15314
|
const resourceNoun = RESOURCE_NOUN_BY_KIND[leak.kind];
|
|
13924
15315
|
context.report({
|
|
@@ -13951,10 +15342,10 @@ const effectNeedsCleanup = defineRule({
|
|
|
13951
15342
|
if (isRetainedComponentScopeFunction(node)) reportRetainedLeak(node);
|
|
13952
15343
|
},
|
|
13953
15344
|
ArrowFunctionExpression(node) {
|
|
13954
|
-
if (isRetainedComponentScopeFunction(node) || isInlineRetainedHandlerFunction(node, context)) reportRetainedLeak(node);
|
|
15345
|
+
if (isRetainedComponentScopeFunction(node) || isInlineRetainedHandlerFunction(node, context) || getAssignedReactRefSymbol(node, context)) reportRetainedLeak(node);
|
|
13955
15346
|
},
|
|
13956
15347
|
FunctionExpression(node) {
|
|
13957
|
-
if (isRetainedComponentScopeFunction(node) || isInlineRetainedHandlerFunction(node, context)) reportRetainedLeak(node);
|
|
15348
|
+
if (isRetainedComponentScopeFunction(node) || isInlineRetainedHandlerFunction(node, context) || getAssignedReactRefSymbol(node, context)) reportRetainedLeak(node);
|
|
13958
15349
|
}
|
|
13959
15350
|
};
|
|
13960
15351
|
}
|
|
@@ -17356,6 +18747,7 @@ const iframeHasTitle = defineRule({
|
|
|
17356
18747
|
recommendation: "Add a descriptive `title` so screen reader users know what the embedded frame contains.",
|
|
17357
18748
|
category: "Accessibility",
|
|
17358
18749
|
create: (context) => ({ JSXOpeningElement(node) {
|
|
18750
|
+
if (isLocalTestScaffoldJsx(node, context)) return;
|
|
17359
18751
|
const tag = getElementType(node, context.settings);
|
|
17360
18752
|
if (tag !== "iframe") return;
|
|
17361
18753
|
if (isInsideStaticallyHiddenJsxSubtree(node, context.scopes)) return;
|
|
@@ -17876,6 +19268,7 @@ const interactiveSupportsFocus = defineRule({
|
|
|
17876
19268
|
const settings = resolveSettings$37(context.settings);
|
|
17877
19269
|
const tabbableSet = new Set(settings.tabbable);
|
|
17878
19270
|
return { JSXOpeningElement(node) {
|
|
19271
|
+
if (isLocalTestScaffoldJsx(node, context)) return;
|
|
17879
19272
|
if (node.attributes.length === 0) return;
|
|
17880
19273
|
if (hasJsxSpreadAttribute$1(node.attributes)) return;
|
|
17881
19274
|
const roleAttribute = hasJsxPropIgnoreCase(node.attributes, "role");
|
|
@@ -18589,7 +19982,7 @@ const scanPerIterationLayoutReads = (body) => {
|
|
|
18589
19982
|
hasDeliberateForcedReflow
|
|
18590
19983
|
};
|
|
18591
19984
|
};
|
|
18592
|
-
const getNodeStart
|
|
19985
|
+
const getNodeStart = (node) => {
|
|
18593
19986
|
const withRange = node;
|
|
18594
19987
|
return withRange.range ? withRange.range[0] : -1;
|
|
18595
19988
|
};
|
|
@@ -18621,7 +20014,7 @@ const hasAttachmentBefore = (scopeOwner, elementName, beforeStart) => {
|
|
|
18621
20014
|
if (!isNodeOfType(child, "CallExpression")) return;
|
|
18622
20015
|
const callee = child.callee;
|
|
18623
20016
|
if (!isNodeOfType(callee, "MemberExpression") || !isNodeOfType(callee.property, "Identifier") || !DOM_ATTACHMENT_METHOD_NAMES.has(callee.property.name)) return;
|
|
18624
|
-
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) {
|
|
18625
20018
|
foundAttachment = true;
|
|
18626
20019
|
return false;
|
|
18627
20020
|
}
|
|
@@ -18643,7 +20036,7 @@ const isProvablyDetachedAtWrite = (styleWriteStatement) => {
|
|
|
18643
20036
|
const elementExpression = assignment.left.object.object;
|
|
18644
20037
|
const creationRoot = resolveDetachedCreationRoot(elementExpression, 0);
|
|
18645
20038
|
if (!creationRoot) return false;
|
|
18646
|
-
return !hasAttachmentBefore(creationRoot.scopeOwner, creationRoot.rootName, getNodeStart
|
|
20039
|
+
return !hasAttachmentBefore(creationRoot.scopeOwner, creationRoot.rootName, getNodeStart(styleWriteStatement));
|
|
18647
20040
|
};
|
|
18648
20041
|
const jsBatchDomCss = defineRule({
|
|
18649
20042
|
id: "js-batch-dom-css",
|
|
@@ -19366,7 +20759,7 @@ const globSyncReturnsStringPaths = (node, context) => {
|
|
|
19366
20759
|
const callee = stripParenExpression(node.callee);
|
|
19367
20760
|
let isGlobSyncImport = false;
|
|
19368
20761
|
if (isNodeOfType(callee, "Identifier")) isGlobSyncImport = context.scopes.symbolFor(callee)?.kind === "import" && getImportedNameFromModule(callee, callee.name, "glob") === "globSync";
|
|
19369
|
-
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");
|
|
19370
20763
|
if (!isGlobSyncImport) return false;
|
|
19371
20764
|
const options = node.arguments[1];
|
|
19372
20765
|
if (!options) return true;
|
|
@@ -25748,6 +27141,7 @@ const mediaHasCaption = defineRule({
|
|
|
25748
27141
|
create: (context) => {
|
|
25749
27142
|
const settings = resolveSettings$23(context.settings);
|
|
25750
27143
|
return { JSXOpeningElement(node) {
|
|
27144
|
+
if (isLocalTestScaffoldJsx(node, context)) return;
|
|
25751
27145
|
const tag = getElementType(node, context.settings);
|
|
25752
27146
|
if (!(settings.audio.has(tag) || settings.video.has(tag))) return;
|
|
25753
27147
|
if (evaluateMuted(hasJsxPropIgnoreCase(node.attributes, "muted")) === true) return;
|
|
@@ -25815,6 +27209,7 @@ const mouseEventsHaveKeyEvents = defineRule({
|
|
|
25815
27209
|
create: (context) => {
|
|
25816
27210
|
const settings = resolveSettings$22(context.settings);
|
|
25817
27211
|
return { JSXOpeningElement(node) {
|
|
27212
|
+
if (isLocalTestScaffoldJsx(node, context)) return;
|
|
25818
27213
|
const tag = getElementType(node, context.settings);
|
|
25819
27214
|
if (!HTML_TAGS.has(tag)) return;
|
|
25820
27215
|
for (const handler of settings.hoverInHandlers) {
|
|
@@ -25856,7 +27251,11 @@ const mouseEventsHaveKeyEvents = defineRule({
|
|
|
25856
27251
|
//#region src/plugin/utils/has-directive.ts
|
|
25857
27252
|
const hasDirective = (programNode, directive) => {
|
|
25858
27253
|
if (!isNodeOfType(programNode, "Program")) return false;
|
|
25859
|
-
|
|
27254
|
+
for (const statement of programNode.body) {
|
|
27255
|
+
if (!isNodeOfType(statement, "ExpressionStatement") || statement.directive === void 0) return false;
|
|
27256
|
+
if (statement.directive === directive) return true;
|
|
27257
|
+
}
|
|
27258
|
+
return false;
|
|
25860
27259
|
};
|
|
25861
27260
|
//#endregion
|
|
25862
27261
|
//#region src/plugin/rules/nextjs/nextjs-async-client-component.ts
|
|
@@ -31954,7 +33353,7 @@ const isReactDomCreatePortalCall = (node, scopes) => {
|
|
|
31954
33353
|
if (!isNodeOfType(callee, "MemberExpression") || callee.computed || !isNodeOfType(callee.object, "Identifier") || !isNodeOfType(callee.property, "Identifier") || callee.property.name !== "createPortal") return false;
|
|
31955
33354
|
const symbol = scopes.symbolFor(callee.object);
|
|
31956
33355
|
if (!symbol || symbol.kind !== "import") return false;
|
|
31957
|
-
return isDefaultImportFromModule(callee.object, callee.object.name, "react-dom") || isNamespaceImportFromModule(callee.object, callee.object.name, "react-dom");
|
|
33356
|
+
return isDefaultImportFromModule(callee.object, callee.object.name, "react-dom") || isNamespaceImportFromModule$1(callee.object, callee.object.name, "react-dom");
|
|
31958
33357
|
};
|
|
31959
33358
|
const containsRenderOutput$1 = (rootNode, scopes) => {
|
|
31960
33359
|
let hasRenderOutput = false;
|
|
@@ -33467,6 +34866,151 @@ const isCreateRefResultWriteOnly = (createRefCall, filename, scopes) => {
|
|
|
33467
34866
|
return ownedValue ? analyzeSymbolValuePath(ownedValue, state, 12) : analyzeValueUse(createRefCall, [], environment, state, 12);
|
|
33468
34867
|
};
|
|
33469
34868
|
//#endregion
|
|
34869
|
+
//#region src/plugin/utils/is-jsx-element-or-fragment.ts
|
|
34870
|
+
/**
|
|
34871
|
+
* Type-guard for the two single-node JSX output forms: `JSXElement`
|
|
34872
|
+
* (`<Foo />`) and `JSXFragment` (`<>…</>`). Canonical home for the
|
|
34873
|
+
* `isNodeOfType(x, "JSXElement") || isNodeOfType(x, "JSXFragment")` check
|
|
34874
|
+
* that many rules otherwise inline. Does NOT unwrap parens / TS wrappers —
|
|
34875
|
+
* callers that need the semantic expression should `stripParenExpression`
|
|
34876
|
+
* first.
|
|
34877
|
+
*/
|
|
34878
|
+
const isJsxElementOrFragment = (node) => Boolean(node && (isNodeOfType(node, "JSXElement") || isNodeOfType(node, "JSXFragment")));
|
|
34879
|
+
//#endregion
|
|
34880
|
+
//#region src/plugin/rules/react-builtins/is-proven-one-shot-testing-library-component.ts
|
|
34881
|
+
const REACT_TESTING_LIBRARY_MODULE_SOURCE = "@testing-library/react";
|
|
34882
|
+
const REACT_TESTING_LIBRARY_MODULE_SOURCES = new Set([REACT_TESTING_LIBRARY_MODULE_SOURCE]);
|
|
34883
|
+
const TEST_CALLBACK_NAMES = new Set(["it", "test"]);
|
|
34884
|
+
const TEST_RUNNER_MODULE_SOURCES = new Set(["@jest/globals", "vitest"]);
|
|
34885
|
+
const isNamedImportFromModule = (symbol, importedName, moduleSources) => {
|
|
34886
|
+
if (!symbol || symbol.kind !== "import" || !isNodeOfType(symbol.declarationNode, "ImportSpecifier") || getImportedName(symbol.declarationNode) !== importedName) return false;
|
|
34887
|
+
const importDeclaration = symbol.declarationNode.parent;
|
|
34888
|
+
return Boolean(importDeclaration && isNodeOfType(importDeclaration, "ImportDeclaration") && typeof importDeclaration.source.value === "string" && moduleSources.has(importDeclaration.source.value));
|
|
34889
|
+
};
|
|
34890
|
+
const isNamespaceImportFromModule = (symbol, moduleSource) => {
|
|
34891
|
+
if (!symbol || symbol.kind !== "import" || !isNodeOfType(symbol.declarationNode, "ImportNamespaceSpecifier")) return false;
|
|
34892
|
+
const importDeclaration = symbol.declarationNode.parent;
|
|
34893
|
+
return Boolean(importDeclaration && isNodeOfType(importDeclaration, "ImportDeclaration") && importDeclaration.source.value === moduleSource);
|
|
34894
|
+
};
|
|
34895
|
+
const isProvenTestCallback = (functionNode, scopes) => {
|
|
34896
|
+
const callExpression = functionNode.parent;
|
|
34897
|
+
if (!callExpression || !isNodeOfType(callExpression, "CallExpression") || callExpression.arguments[1] !== functionNode) return false;
|
|
34898
|
+
const callee = stripParenExpression(callExpression.callee);
|
|
34899
|
+
if (!isNodeOfType(callee, "Identifier")) return false;
|
|
34900
|
+
if (TEST_CALLBACK_NAMES.has(callee.name) && scopes.isGlobalReference(callee)) return true;
|
|
34901
|
+
const symbol = scopes.symbolFor(callee);
|
|
34902
|
+
if (!symbol || symbol.kind !== "import") return false;
|
|
34903
|
+
const importedName = getImportedName(symbol.declarationNode);
|
|
34904
|
+
return Boolean(importedName && TEST_CALLBACK_NAMES.has(importedName) && isNamedImportFromModule(symbol, importedName, TEST_RUNNER_MODULE_SOURCES));
|
|
34905
|
+
};
|
|
34906
|
+
const getDirectConstComponentSymbol = (functionNode, scopes) => {
|
|
34907
|
+
const declarator = functionNode.parent;
|
|
34908
|
+
if (!declarator || !isNodeOfType(declarator, "VariableDeclarator") || declarator.init !== functionNode || !isNodeOfType(declarator.id, "Identifier")) return null;
|
|
34909
|
+
const declaration = declarator.parent;
|
|
34910
|
+
if (!declaration || !isNodeOfType(declaration, "VariableDeclaration") || declaration.kind !== "const" || declaration.declarations.length !== 1) return null;
|
|
34911
|
+
const testCallback = findEnclosingFunction$1(declarator);
|
|
34912
|
+
if (!testCallback || !isFunctionLike$1(testCallback) || !isProvenTestCallback(testCallback, scopes) || !isNodeOfType(testCallback.body, "BlockStatement") || declaration.parent !== testCallback.body) return null;
|
|
34913
|
+
return scopes.symbolFor(declarator.id);
|
|
34914
|
+
};
|
|
34915
|
+
const isCreateRefDeclaration = (statement, scopes) => isNodeOfType(statement, "VariableDeclaration") && statement.kind === "const" && statement.declarations.length > 0 && statement.declarations.every((declarator) => {
|
|
34916
|
+
const initializer = declarator.init ? stripParenExpression(declarator.init) : null;
|
|
34917
|
+
return Boolean(isNodeOfType(declarator.id, "Identifier") && initializer && isNodeOfType(initializer, "CallExpression") && isReactApiCall(initializer, "createRef", scopes, {
|
|
34918
|
+
allowGlobalReactNamespace: true,
|
|
34919
|
+
allowUnboundBareCalls: true,
|
|
34920
|
+
resolveNamedAliases: true
|
|
34921
|
+
}));
|
|
34922
|
+
});
|
|
34923
|
+
const isSafeReturnedJsx = (returnStatement) => {
|
|
34924
|
+
if (!isNodeOfType(returnStatement, "ReturnStatement") || !returnStatement.argument) return false;
|
|
34925
|
+
const returnedExpression = stripParenExpression(returnStatement.argument);
|
|
34926
|
+
if (!isJsxElementOrFragment(returnedExpression)) return false;
|
|
34927
|
+
let isSafe = true;
|
|
34928
|
+
walkAst(returnedExpression, (node) => {
|
|
34929
|
+
if (isFunctionLike$1(node)) {
|
|
34930
|
+
isSafe = false;
|
|
34931
|
+
return false;
|
|
34932
|
+
}
|
|
34933
|
+
if (isNodeOfType(node, "AssignmentExpression") || isNodeOfType(node, "AwaitExpression") || isNodeOfType(node, "CallExpression") || isNodeOfType(node, "NewExpression") || isNodeOfType(node, "TaggedTemplateExpression") || isNodeOfType(node, "UpdateExpression") || isNodeOfType(node, "YieldExpression")) {
|
|
34934
|
+
isSafe = false;
|
|
34935
|
+
return false;
|
|
34936
|
+
}
|
|
34937
|
+
});
|
|
34938
|
+
return isSafe;
|
|
34939
|
+
};
|
|
34940
|
+
const hasProvenOneShotComponentBody = (functionNode, scopes) => {
|
|
34941
|
+
if (!isFunctionLike$1(functionNode) || !isNodeOfType(functionNode.body, "BlockStatement")) return false;
|
|
34942
|
+
if (!functionNode.params.every((parameter) => isNodeOfType(parameter, "Identifier"))) return false;
|
|
34943
|
+
const statements = functionNode.body.body;
|
|
34944
|
+
if (statements.length < 2) return false;
|
|
34945
|
+
const returnStatement = statements.at(-1);
|
|
34946
|
+
return Boolean(returnStatement && statements.slice(0, -1).every((statement) => isCreateRefDeclaration(statement, scopes)) && isSafeReturnedJsx(returnStatement));
|
|
34947
|
+
};
|
|
34948
|
+
const isProvenReactStrictModeElement = (jsxElement, scopes) => {
|
|
34949
|
+
const elementName = jsxElement.openingElement.name;
|
|
34950
|
+
if (isNodeOfType(elementName, "JSXIdentifier")) {
|
|
34951
|
+
const symbol = scopes.symbolFor(elementName);
|
|
34952
|
+
return Boolean(symbol && isImportedFromReact(symbol) && getImportedName(symbol.declarationNode) === "StrictMode");
|
|
34953
|
+
}
|
|
34954
|
+
return Boolean(isNodeOfType(elementName, "JSXMemberExpression") && isNodeOfType(elementName.object, "JSXIdentifier") && elementName.property.name === "StrictMode" && isReactNamespaceImport(elementName.object, scopes));
|
|
34955
|
+
};
|
|
34956
|
+
const isWhitespaceJsxChild = (node) => isNodeOfType(node, "JSXText") && node.value.trim().length === 0 || isNodeOfType(node, "JSXExpressionContainer") && isNodeOfType(node.expression, "JSXEmptyExpression");
|
|
34957
|
+
const getRootElementForComponentReference = (identifier, scopes) => {
|
|
34958
|
+
const openingElement = identifier.parent;
|
|
34959
|
+
if (!openingElement || !isNodeOfType(openingElement, "JSXOpeningElement") || openingElement.name !== identifier || !openingElement.selfClosing || openingElement.attributes.length !== 0) return null;
|
|
34960
|
+
const componentElement = openingElement.parent;
|
|
34961
|
+
if (!componentElement || !isNodeOfType(componentElement, "JSXElement")) return null;
|
|
34962
|
+
const strictModeElement = componentElement.parent;
|
|
34963
|
+
if (!strictModeElement || !isNodeOfType(strictModeElement, "JSXElement")) return componentElement;
|
|
34964
|
+
if (strictModeElement.openingElement.attributes.length !== 0 || !isProvenReactStrictModeElement(strictModeElement, scopes)) return null;
|
|
34965
|
+
const renderedChildren = strictModeElement.children.filter((child) => !isWhitespaceJsxChild(child));
|
|
34966
|
+
return renderedChildren.length === 1 && renderedChildren[0] === componentElement ? strictModeElement : null;
|
|
34967
|
+
};
|
|
34968
|
+
const isProvenTestingLibraryRenderCall = (callExpression, scopes) => {
|
|
34969
|
+
const callee = stripParenExpression(callExpression.callee);
|
|
34970
|
+
if (isNodeOfType(callee, "Identifier")) return isNamedImportFromModule(scopes.symbolFor(callee), "render", REACT_TESTING_LIBRARY_MODULE_SOURCES);
|
|
34971
|
+
return Boolean(isNodeOfType(callee, "MemberExpression") && getStaticPropertyName(callee) === "render" && isNodeOfType(callee.object, "Identifier") && isNamespaceImportFromModule(scopes.symbolFor(callee.object), REACT_TESTING_LIBRARY_MODULE_SOURCE));
|
|
34972
|
+
};
|
|
34973
|
+
const isSafeRenderResultBinding = (pattern) => {
|
|
34974
|
+
if (!isNodeOfType(pattern, "ObjectPattern")) return false;
|
|
34975
|
+
return pattern.properties.every((property) => {
|
|
34976
|
+
if (!isNodeOfType(property, "Property") || property.computed) return false;
|
|
34977
|
+
return isNodeOfType(property.value, "Identifier") && getStaticPropertyKeyName(property) !== "rerender";
|
|
34978
|
+
});
|
|
34979
|
+
};
|
|
34980
|
+
const isDirectSafeRenderStatement = (callExpression, testCallback) => {
|
|
34981
|
+
if (!isFunctionLike$1(testCallback) || !isNodeOfType(testCallback.body, "BlockStatement")) return false;
|
|
34982
|
+
const expression = findTransparentExpressionRoot(callExpression);
|
|
34983
|
+
const parent = expression.parent;
|
|
34984
|
+
if (!parent) return false;
|
|
34985
|
+
if (isNodeOfType(parent, "ExpressionStatement")) return parent.parent === testCallback.body;
|
|
34986
|
+
if (!isNodeOfType(parent, "VariableDeclarator") || parent.init !== expression || !isSafeRenderResultBinding(parent.id)) return false;
|
|
34987
|
+
const declaration = parent.parent;
|
|
34988
|
+
return Boolean(declaration && isNodeOfType(declaration, "VariableDeclaration") && declaration.declarations.length === 1 && declaration.parent === testCallback.body);
|
|
34989
|
+
};
|
|
34990
|
+
const getProvenIndependentRenderCall = (componentReference, scopes) => {
|
|
34991
|
+
const rootElement = getRootElementForComponentReference(componentReference, scopes);
|
|
34992
|
+
if (!rootElement) return null;
|
|
34993
|
+
const renderedArgument = findTransparentExpressionRoot(rootElement);
|
|
34994
|
+
const callExpression = renderedArgument.parent;
|
|
34995
|
+
if (!callExpression || !isNodeOfType(callExpression, "CallExpression") || callExpression.arguments.length !== 1 || callExpression.arguments[0] !== renderedArgument || !isProvenTestingLibraryRenderCall(callExpression, scopes)) return null;
|
|
34996
|
+
return callExpression;
|
|
34997
|
+
};
|
|
34998
|
+
const isProvenOneShotTestingLibraryComponent = (functionNode, filename, scopes) => {
|
|
34999
|
+
if (!filename || !isTestlikeFilename(filename) || !hasProvenOneShotComponentBody(functionNode, scopes)) return false;
|
|
35000
|
+
const componentSymbol = getDirectConstComponentSymbol(functionNode, scopes);
|
|
35001
|
+
if (!componentSymbol || componentSymbol.references.length === 0) return false;
|
|
35002
|
+
const testCallback = findEnclosingFunction$1(componentSymbol.bindingIdentifier);
|
|
35003
|
+
if (!testCallback) return false;
|
|
35004
|
+
const renderCalls = /* @__PURE__ */ new Set();
|
|
35005
|
+
for (const reference of componentSymbol.references) {
|
|
35006
|
+
if (reference.flag !== "read") return false;
|
|
35007
|
+
const renderCall = getProvenIndependentRenderCall(reference.identifier, scopes);
|
|
35008
|
+
if (!renderCall || findEnclosingFunction$1(renderCall) !== testCallback || !isDirectSafeRenderStatement(renderCall, testCallback)) return false;
|
|
35009
|
+
renderCalls.add(renderCall);
|
|
35010
|
+
}
|
|
35011
|
+
return renderCalls.size > 0;
|
|
35012
|
+
};
|
|
35013
|
+
//#endregion
|
|
33470
35014
|
//#region src/plugin/rules/react-builtins/no-create-ref-in-function-component.ts
|
|
33471
35015
|
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.";
|
|
33472
35016
|
const isUseMemoCallbackArgument = (functionNode, scopes) => {
|
|
@@ -33480,6 +35024,27 @@ const findEnclosingRenderFunction = (node, scopes) => {
|
|
|
33480
35024
|
while (enclosingFunction && isUseMemoCallbackArgument(enclosingFunction, scopes)) enclosingFunction = findEnclosingFunction$1(enclosingFunction);
|
|
33481
35025
|
return enclosingFunction;
|
|
33482
35026
|
};
|
|
35027
|
+
const isReactUseStateInitialState = (node, scopes) => {
|
|
35028
|
+
const initialState = findTransparentExpressionRoot(node);
|
|
35029
|
+
const stateCall = initialState.parent;
|
|
35030
|
+
return Boolean(stateCall && isNodeOfType(stateCall, "CallExpression") && stateCall.arguments[0] === initialState && isReactApiCall(stateCall, "useState", scopes, {
|
|
35031
|
+
allowGlobalReactNamespace: true,
|
|
35032
|
+
resolveNamedAliases: true
|
|
35033
|
+
}));
|
|
35034
|
+
};
|
|
35035
|
+
const hasDirectExportWrapper = (declarationNode) => {
|
|
35036
|
+
const parent = declarationNode.parent;
|
|
35037
|
+
if (isNodeOfType(parent, "ExportNamedDeclaration") || isNodeOfType(parent, "ExportDefaultDeclaration")) return true;
|
|
35038
|
+
return Boolean(isNodeOfType(declarationNode, "VariableDeclarator") && (isNodeOfType(parent?.parent, "ExportNamedDeclaration") || isNodeOfType(parent?.parent, "ExportDefaultDeclaration")));
|
|
35039
|
+
};
|
|
35040
|
+
const isFunctionExclusivelyUsedAsReactStateInitializer = (functionNode, scopes) => {
|
|
35041
|
+
if (isReactUseStateInitialState(functionNode, scopes)) return true;
|
|
35042
|
+
const bindingIdentifier = getFunctionBindingIdentifier$1(findTransparentExpressionRoot(functionNode));
|
|
35043
|
+
if (!bindingIdentifier) return false;
|
|
35044
|
+
const bindingSymbol = isNodeOfType(functionNode, "FunctionDeclaration") ? scopes.scopeFor(functionNode).symbolsByName.get(bindingIdentifier.name) : scopes.symbolFor(bindingIdentifier);
|
|
35045
|
+
if (!bindingSymbol || bindingSymbol.kind !== "const" && bindingSymbol.kind !== "function" || hasDirectExportWrapper(bindingSymbol.declarationNode) || bindingSymbol.references.length === 0) return false;
|
|
35046
|
+
return bindingSymbol.references.every((reference) => reference.flag === "read" && isReactUseStateInitialState(reference.identifier, scopes));
|
|
35047
|
+
};
|
|
33483
35048
|
const noCreateRefInFunctionComponent = defineRule({
|
|
33484
35049
|
id: "no-create-ref-in-function-component",
|
|
33485
35050
|
title: "createRef in function component",
|
|
@@ -33496,6 +35061,8 @@ const noCreateRefInFunctionComponent = defineRule({
|
|
|
33496
35061
|
const displayName = componentOrHookDisplayNameForFunction(enclosingFunction);
|
|
33497
35062
|
if (!displayName) return;
|
|
33498
35063
|
if (!(isReactHookName(displayName) || functionContainsReactRenderOutput(enclosingFunction, context.scopes, context.cfg))) return;
|
|
35064
|
+
if (isReactUseStateInitialState(node, context.scopes) || isFunctionExclusivelyUsedAsReactStateInitializer(enclosingFunction, context.scopes)) return;
|
|
35065
|
+
if (isProvenOneShotTestingLibraryComponent(enclosingFunction, context.filename, context.scopes)) return;
|
|
33499
35066
|
if (isCreateRefResultWriteOnly(node, context.filename, context.scopes)) return;
|
|
33500
35067
|
context.report({
|
|
33501
35068
|
node,
|
|
@@ -34765,7 +36332,6 @@ const isSetStateCallInLifecycle = (setStateCall, lifecycleNames, options = {}) =
|
|
|
34765
36332
|
//#region src/plugin/rules/react-builtins/no-did-mount-set-state.ts
|
|
34766
36333
|
const LIFECYCLE_NAMES$2 = new Set(["componentDidMount"]);
|
|
34767
36334
|
const MESSAGE$28 = "Your users see an extra render right after mount when you call `setState` in `componentDidMount`.";
|
|
34768
|
-
const getNodeStart = (node) => "start" in node && typeof node.start === "number" ? node.start : -1;
|
|
34769
36335
|
const getEnclosingLifecycleFunction = (setStateCall) => {
|
|
34770
36336
|
let ancestor = setStateCall.parent;
|
|
34771
36337
|
while (ancestor) {
|
|
@@ -34854,13 +36420,13 @@ const argumentDerivesFromPostMountSource = (setStateCall, lifecycleFunction) =>
|
|
|
34854
36420
|
};
|
|
34855
36421
|
const isAfterAwaitInAsyncLifecycle = (setStateCall, lifecycleFunction) => {
|
|
34856
36422
|
if (!isFunctionLike$1(lifecycleFunction) || lifecycleFunction.async !== true) return false;
|
|
34857
|
-
const callStart =
|
|
36423
|
+
const callStart = getNodeStartIndex(setStateCall);
|
|
34858
36424
|
if (callStart < 0) return false;
|
|
34859
36425
|
let didFindPrecedingAwait = false;
|
|
34860
36426
|
walkAst(lifecycleFunction, (descendant) => {
|
|
34861
36427
|
if (didFindPrecedingAwait) return false;
|
|
34862
36428
|
if (!isNodeOfType(descendant, "AwaitExpression")) return;
|
|
34863
|
-
const awaitStart =
|
|
36429
|
+
const awaitStart = getNodeStartIndex(descendant);
|
|
34864
36430
|
if (awaitStart >= 0 && awaitStart < callStart) {
|
|
34865
36431
|
didFindPrecedingAwait = true;
|
|
34866
36432
|
return false;
|
|
@@ -41611,11 +43177,6 @@ const SAME_REFERENCE_ARRAY_RETURN_METHODS = new Set([
|
|
|
41611
43177
|
"reverse",
|
|
41612
43178
|
"sort"
|
|
41613
43179
|
]);
|
|
41614
|
-
const OBJECT_MUTATION_METHODS = new Set([
|
|
41615
|
-
"assign",
|
|
41616
|
-
"defineProperties",
|
|
41617
|
-
"defineProperty"
|
|
41618
|
-
]);
|
|
41619
43180
|
const REFLECT_MUTATION_METHODS = new Set(["deleteProperty", "set"]);
|
|
41620
43181
|
const cloneReducerPathState = (state) => ({
|
|
41621
43182
|
originalStateReferenceNames: new Set(state.originalStateReferenceNames),
|
|
@@ -41711,7 +43272,7 @@ const collectReducerStateMutationsInExpressionOrStatement = (node, state) => {
|
|
|
41711
43272
|
}
|
|
41712
43273
|
if (!isNodeOfType(unwrappedChild, "CallExpression")) return;
|
|
41713
43274
|
const firstArgument = unwrappedChild.arguments?.[0];
|
|
41714
|
-
if (firstArgument && isExpressionRootedInMutableReducerStateSource(firstArgument, state) && (isStaticMethodCallOnNamedObject(unwrappedChild, "Object",
|
|
43275
|
+
if (firstArgument && isExpressionRootedInMutableReducerStateSource(firstArgument, state) && (isStaticMethodCallOnNamedObject(unwrappedChild, "Object", OBJECT_PROPERTY_MUTATION_METHOD_NAMES) || isStaticMethodCallOnNamedObject(unwrappedChild, "Reflect", REFLECT_MUTATION_METHODS))) {
|
|
41715
43276
|
mutations.push({ node: unwrappedChild });
|
|
41716
43277
|
return;
|
|
41717
43278
|
}
|
|
@@ -44648,6 +46209,7 @@ const noRedundantRoles = defineRule({
|
|
|
44648
46209
|
create: (context) => {
|
|
44649
46210
|
const settings = resolveSettings$13(context.settings);
|
|
44650
46211
|
return { JSXOpeningElement(node) {
|
|
46212
|
+
if (isLocalTestScaffoldJsx(node, context)) return;
|
|
44651
46213
|
const roleAttr = hasJsxPropIgnoreCase(node.attributes, "role");
|
|
44652
46214
|
if (!roleAttr) return;
|
|
44653
46215
|
if (hasJsxPropIgnoreCase(node.attributes, "data-rac")) return;
|
|
@@ -45697,6 +47259,87 @@ const noResetAllStateOnPropChange = defineRule({
|
|
|
45697
47259
|
} })
|
|
45698
47260
|
});
|
|
45699
47261
|
//#endregion
|
|
47262
|
+
//#region src/plugin/utils/is-proven-framer-motion-jsx-element.ts
|
|
47263
|
+
const MOTION_FACTORY_MODULES = new Set(["framer-motion", "motion/react"]);
|
|
47264
|
+
const MOTION_TAG_NAMESPACE_MODULES = new Set([
|
|
47265
|
+
"framer-motion/client",
|
|
47266
|
+
"framer-motion/m",
|
|
47267
|
+
"motion/react-client",
|
|
47268
|
+
"motion/react-m"
|
|
47269
|
+
]);
|
|
47270
|
+
const MOTION_FACTORY_EXPORTS = new Set(["m", "motion"]);
|
|
47271
|
+
const getValueImportSource = (symbol) => {
|
|
47272
|
+
if (symbol.kind !== "import") return null;
|
|
47273
|
+
const declaration = symbol.declarationNode.parent;
|
|
47274
|
+
if (!declaration || !isNodeOfType(declaration, "ImportDeclaration") || isTypeOnlyImport(declaration) || isNodeOfType(symbol.declarationNode, "ImportSpecifier") && symbol.declarationNode.importKind === "type") return null;
|
|
47275
|
+
return typeof declaration.source.value === "string" ? declaration.source.value : null;
|
|
47276
|
+
};
|
|
47277
|
+
const getMemberParts = (node) => {
|
|
47278
|
+
if (isNodeOfType(node, "MemberExpression")) {
|
|
47279
|
+
const propertyName = getStaticPropertyName(node);
|
|
47280
|
+
return propertyName ? [node.object, propertyName] : null;
|
|
47281
|
+
}
|
|
47282
|
+
if (isNodeOfType(node, "JSXMemberExpression")) return isNodeOfType(node.property, "JSXIdentifier") ? [node.object, node.property.name] : null;
|
|
47283
|
+
return null;
|
|
47284
|
+
};
|
|
47285
|
+
const resolveSymbol = (node, scopes) => {
|
|
47286
|
+
if (!isNodeOfType(node, "Identifier") && !isNodeOfType(node, "JSXIdentifier")) return null;
|
|
47287
|
+
return resolveConstIdentifierAlias(node, scopes);
|
|
47288
|
+
};
|
|
47289
|
+
const isNamespaceFrom = (node, sources, scopes) => {
|
|
47290
|
+
const symbol = resolveSymbol(stripParenExpression(node), scopes);
|
|
47291
|
+
const source = symbol ? getValueImportSource(symbol) : null;
|
|
47292
|
+
return Boolean(source && sources.has(source) && symbol && isNodeOfType(symbol.declarationNode, "ImportNamespaceSpecifier"));
|
|
47293
|
+
};
|
|
47294
|
+
const isMotionFactory = (rawNode, scopes, visitedSymbolIds) => {
|
|
47295
|
+
const node = stripParenExpression(rawNode);
|
|
47296
|
+
if (isNamespaceFrom(node, MOTION_TAG_NAMESPACE_MODULES, scopes)) return true;
|
|
47297
|
+
const symbol = resolveSymbol(node, scopes);
|
|
47298
|
+
if (symbol?.kind === "import") {
|
|
47299
|
+
const source = getValueImportSource(symbol);
|
|
47300
|
+
const importedName = getImportedName(symbol.declarationNode);
|
|
47301
|
+
return Boolean(source && MOTION_FACTORY_MODULES.has(source) && importedName && MOTION_FACTORY_EXPORTS.has(importedName));
|
|
47302
|
+
}
|
|
47303
|
+
if (symbol?.kind === "const" && symbol.initializer) {
|
|
47304
|
+
if (visitedSymbolIds.has(symbol.id)) return false;
|
|
47305
|
+
visitedSymbolIds.add(symbol.id);
|
|
47306
|
+
return isMotionFactory(symbol.initializer, scopes, visitedSymbolIds);
|
|
47307
|
+
}
|
|
47308
|
+
const memberParts = getMemberParts(node);
|
|
47309
|
+
return Boolean(memberParts && MOTION_FACTORY_EXPORTS.has(memberParts[1]) && isNamespaceFrom(memberParts[0], MOTION_FACTORY_MODULES, scopes));
|
|
47310
|
+
};
|
|
47311
|
+
const isMotionComponent = (rawNode, scopes) => {
|
|
47312
|
+
return isMotionComponentWithVisitedSymbols(rawNode, scopes, /* @__PURE__ */ new Set());
|
|
47313
|
+
};
|
|
47314
|
+
const isMotionComponentWithVisitedSymbols = (rawNode, scopes, visitedSymbolIds) => {
|
|
47315
|
+
const node = stripParenExpression(rawNode);
|
|
47316
|
+
const symbol = resolveSymbol(node, scopes);
|
|
47317
|
+
if (symbol?.kind === "const" && symbol.initializer) {
|
|
47318
|
+
if (visitedSymbolIds.has(symbol.id)) return false;
|
|
47319
|
+
visitedSymbolIds.add(symbol.id);
|
|
47320
|
+
return isMotionComponentWithVisitedSymbols(symbol.initializer, scopes, visitedSymbolIds);
|
|
47321
|
+
}
|
|
47322
|
+
if (symbol?.kind === "import") {
|
|
47323
|
+
const source = getValueImportSource(symbol);
|
|
47324
|
+
return Boolean(source && MOTION_TAG_NAMESPACE_MODULES.has(source) && isNodeOfType(symbol.declarationNode, "ImportSpecifier") && getImportedName(symbol.declarationNode) !== "create");
|
|
47325
|
+
}
|
|
47326
|
+
const memberParts = getMemberParts(node);
|
|
47327
|
+
if (memberParts && isMotionFactory(memberParts[0], scopes, visitedSymbolIds)) return true;
|
|
47328
|
+
if (!isNodeOfType(node, "CallExpression")) return false;
|
|
47329
|
+
if (isMotionFactory(node.callee, scopes, visitedSymbolIds)) return true;
|
|
47330
|
+
const calleeMemberParts = getMemberParts(stripParenExpression(node.callee));
|
|
47331
|
+
return Boolean(calleeMemberParts && calleeMemberParts[1] === "create" && isMotionFactory(calleeMemberParts[0], scopes, visitedSymbolIds));
|
|
47332
|
+
};
|
|
47333
|
+
const isProvenFramerMotionJsxElement = (openingElement, scopes) => {
|
|
47334
|
+
const elementName = openingElement.name;
|
|
47335
|
+
if (isNodeOfType(elementName, "JSXIdentifier")) {
|
|
47336
|
+
if (/^[a-z]/.test(elementName.name)) return false;
|
|
47337
|
+
return isMotionComponent(elementName, scopes);
|
|
47338
|
+
}
|
|
47339
|
+
const memberParts = getMemberParts(elementName);
|
|
47340
|
+
return Boolean(memberParts && isMotionFactory(memberParts[0], scopes, /* @__PURE__ */ new Set()));
|
|
47341
|
+
};
|
|
47342
|
+
//#endregion
|
|
45700
47343
|
//#region src/plugin/rules/performance/no-scale-from-zero.ts
|
|
45701
47344
|
const noScaleFromZero = defineRule({
|
|
45702
47345
|
id: "no-scale-from-zero",
|
|
@@ -45707,6 +47350,8 @@ const noScaleFromZero = defineRule({
|
|
|
45707
47350
|
create: (context) => ({ JSXAttribute(node) {
|
|
45708
47351
|
if (!isNodeOfType(node.name, "JSXIdentifier")) return;
|
|
45709
47352
|
if (node.name.name !== "initial" && node.name.name !== "exit") return;
|
|
47353
|
+
const openingElement = node.parent;
|
|
47354
|
+
if (!openingElement || !isNodeOfType(openingElement, "JSXOpeningElement") || !Object.is(getAuthoritativeJsxAttribute(openingElement.attributes, node.name.name), node) || !isProvenFramerMotionJsxElement(openingElement, context.scopes)) return;
|
|
45710
47355
|
if (!isNodeOfType(node.value, "JSXExpressionContainer")) return;
|
|
45711
47356
|
const expression = node.value.expression;
|
|
45712
47357
|
if (!isNodeOfType(expression, "ObjectExpression")) return;
|
|
@@ -45905,7 +47550,7 @@ const isSelfReferentialSentinelValue = (variableName, literalValue) => literalVa
|
|
|
45905
47550
|
const isIdentifierLikeKeyNameValue = (literalValue) => {
|
|
45906
47551
|
const wordSegments = literalValue.replace(/^[_$\s]+|[_$\s]+$/g, "").split(/[_\-:./$]+/).filter((segment) => segment.length > 0);
|
|
45907
47552
|
if (wordSegments.length < 2) return false;
|
|
45908
|
-
return wordSegments.every((segment) => /^[a-z]
|
|
47553
|
+
return wordSegments.every((segment) => /^[a-z]+(?:[A-Z][a-z]+)*$/.test(segment));
|
|
45909
47554
|
};
|
|
45910
47555
|
const FRAMEWORK_ENV_ADVICE = [
|
|
45911
47556
|
[
|
|
@@ -45979,7 +47624,7 @@ const noSecretsInClientCode = defineRule({
|
|
|
45979
47624
|
const isServerOnlyScope = isInsideServerOnlyScope(node);
|
|
45980
47625
|
const trailingSuffix = getIdentifierTrailingWord(variableName);
|
|
45981
47626
|
const isUiConstant = SECRET_FALSE_POSITIVE_SUFFIXES.has(trailingSuffix);
|
|
45982
|
-
if (shouldUseVariableNameHeuristic && !isServerOnlyScope && SECRET_VARIABLE_PATTERN.test(variableName) && !isUiConstant && !isPublicUrlValue(literalValue) && !isPlaceholderValueForVariableHeuristic && !isSelfReferentialSentinelValue(variableName, literalValue) && !isIdentifierLikeKeyNameValue(literalValue) &&
|
|
47627
|
+
if (shouldUseVariableNameHeuristic && !isServerOnlyScope && SECRET_VARIABLE_PATTERN.test(variableName) && !isUiConstant && !isPublicUrlValue(literalValue) && !isPlaceholderValueForVariableHeuristic && !isSelfReferentialSentinelValue(variableName, literalValue) && !isIdentifierLikeKeyNameValue(literalValue) && literalValue.length > 24) {
|
|
45983
47628
|
context.report({
|
|
45984
47629
|
node,
|
|
45985
47630
|
message: `Hardcoding "${variableName}" in client code is a security vulnerability: the secret ships to the browser where anyone can read it.`
|
|
@@ -51144,10 +52789,9 @@ const isSameApplicationChannelInstance = (targetText, fileContent) => {
|
|
|
51144
52789
|
return new RegExp(`(?<![\\w$.])${escapeRegExp(receiverRoot)}\\s*${SAME_APPLICATION_CHANNEL_CONSTRUCTOR_SOURCE}`).test(fileContent);
|
|
51145
52790
|
};
|
|
51146
52791
|
const WORKER_FILE_PATH_PATTERN = /worker/i;
|
|
51147
|
-
const getNodeStartIndex = (node) => "start" in node && typeof node.start === "number" ? node.start : -1;
|
|
51148
52792
|
const getNodeText = (content, node) => {
|
|
51149
52793
|
const startIndex = getNodeStartIndex(node);
|
|
51150
|
-
const endIndex =
|
|
52794
|
+
const endIndex = getNodeEndIndex(node);
|
|
51151
52795
|
if (startIndex < 0 || endIndex < 0) return "";
|
|
51152
52796
|
return content.slice(startIndex, endIndex);
|
|
51153
52797
|
};
|
|
@@ -51544,17 +53188,6 @@ const preferEs6Class = defineRule({
|
|
|
51544
53188
|
}
|
|
51545
53189
|
});
|
|
51546
53190
|
//#endregion
|
|
51547
|
-
//#region src/plugin/utils/is-jsx-element-or-fragment.ts
|
|
51548
|
-
/**
|
|
51549
|
-
* Type-guard for the two single-node JSX output forms: `JSXElement`
|
|
51550
|
-
* (`<Foo />`) and `JSXFragment` (`<>…</>`). Canonical home for the
|
|
51551
|
-
* `isNodeOfType(x, "JSXElement") || isNodeOfType(x, "JSXFragment")` check
|
|
51552
|
-
* that many rules otherwise inline. Does NOT unwrap parens / TS wrappers —
|
|
51553
|
-
* callers that need the semantic expression should `stripParenExpression`
|
|
51554
|
-
* first.
|
|
51555
|
-
*/
|
|
51556
|
-
const isJsxElementOrFragment = (node) => Boolean(node && (isNodeOfType(node, "JSXElement") || isNodeOfType(node, "JSXFragment")));
|
|
51557
|
-
//#endregion
|
|
51558
53191
|
//#region src/plugin/rules/architecture/prefer-explicit-variants.ts
|
|
51559
53192
|
const resolveBooleanPropTestName = (testNode, booleanPropBindings) => {
|
|
51560
53193
|
let identifierNode = stripParenExpression(testNode);
|
|
@@ -52047,17 +53680,23 @@ const isProvenNodeCryptoNamespaceReference = (expression, scopes) => {
|
|
|
52047
53680
|
//#region src/plugin/rules/architecture/prefer-module-scope-static-value.ts
|
|
52048
53681
|
const MUTATING_RECEIVER_METHOD_NAMES = new Set([...MUTATING_ARRAY_METHODS, ...MUTATING_COLLECTION_METHODS]);
|
|
52049
53682
|
const isMutationContext = (referenceIdentifier) => {
|
|
52050
|
-
|
|
52051
|
-
|
|
52052
|
-
|
|
52053
|
-
|
|
52054
|
-
|
|
52055
|
-
|
|
52056
|
-
|
|
52057
|
-
|
|
52058
|
-
if (isNodeOfType(
|
|
52059
|
-
|
|
52060
|
-
|
|
53683
|
+
let mutationTarget = referenceIdentifier;
|
|
53684
|
+
let receiverMethodName = null;
|
|
53685
|
+
while (mutationTarget.parent) {
|
|
53686
|
+
const parent = mutationTarget.parent;
|
|
53687
|
+
if (TRANSPARENT_EXPRESSION_WRAPPER_TYPES.has(parent.type) && "expression" in parent && parent.expression === mutationTarget) {
|
|
53688
|
+
mutationTarget = parent;
|
|
53689
|
+
continue;
|
|
53690
|
+
}
|
|
53691
|
+
if (isNodeOfType(parent, "MemberExpression") && parent.object === mutationTarget) {
|
|
53692
|
+
receiverMethodName = getStaticPropertyName(parent);
|
|
53693
|
+
mutationTarget = parent;
|
|
53694
|
+
continue;
|
|
53695
|
+
}
|
|
53696
|
+
if (isNodeOfType(parent, "AssignmentExpression") && parent.left === mutationTarget) return true;
|
|
53697
|
+
if (isNodeOfType(parent, "UpdateExpression") && parent.argument === mutationTarget) return true;
|
|
53698
|
+
if (isNodeOfType(parent, "UnaryExpression") && parent.operator === "delete" && parent.argument === mutationTarget) return true;
|
|
53699
|
+
return Boolean(isNodeOfType(parent, "CallExpression") && parent.callee === mutationTarget && MUTATING_RECEIVER_METHOD_NAMES.has(receiverMethodName ?? ""));
|
|
52061
53700
|
}
|
|
52062
53701
|
return false;
|
|
52063
53702
|
};
|
|
@@ -52434,6 +54073,15 @@ const STABLE_REACT_HOOK_VALUE_NAMES = new Set([
|
|
|
52434
54073
|
"useState",
|
|
52435
54074
|
"useTransition"
|
|
52436
54075
|
]);
|
|
54076
|
+
const REGISTRATION_METHOD_BY_RELEASE_METHOD = new Map([
|
|
54077
|
+
["off", "on"],
|
|
54078
|
+
["removeEventListener", "addEventListener"],
|
|
54079
|
+
["removeListener", "addListener"],
|
|
54080
|
+
["unlisten", "listen"],
|
|
54081
|
+
["unsub", "sub"],
|
|
54082
|
+
["unsubscribe", "subscribe"],
|
|
54083
|
+
["unwatch", "watch"]
|
|
54084
|
+
]);
|
|
52437
54085
|
const isStableReactHookDependency = (dependency, context) => {
|
|
52438
54086
|
const unwrappedDependency = stripParenExpression(dependency);
|
|
52439
54087
|
if (!isNodeOfType(unwrappedDependency, "Identifier")) return false;
|
|
@@ -52499,30 +54147,87 @@ const isCallExpressionWithSubHandlerCallee = (callExpression) => {
|
|
|
52499
54147
|
if (isNodeOfType(callee, "MemberExpression") && isNodeOfType(callee.property, "Identifier") && SUBSCRIPTION_METHOD_NAMES.has(callee.property.name)) return true;
|
|
52500
54148
|
return false;
|
|
52501
54149
|
};
|
|
52502
|
-
const
|
|
54150
|
+
const getStaticMemberCallMethodName = (callExpression) => {
|
|
54151
|
+
if (!isNodeOfType(callExpression, "CallExpression")) return null;
|
|
54152
|
+
const callee = callExpression.callee;
|
|
54153
|
+
return isNodeOfType(callee, "MemberExpression") && !callee.computed && isNodeOfType(callee.property, "Identifier") ? callee.property.name : null;
|
|
54154
|
+
};
|
|
54155
|
+
const getCallArgumentUse = (reference) => {
|
|
54156
|
+
const argument = findTransparentExpressionRoot(reference);
|
|
54157
|
+
const parent = argument.parent;
|
|
54158
|
+
if (!isNodeOfType(parent, "CallExpression")) return null;
|
|
54159
|
+
const argumentIndex = (parent.arguments ?? []).findIndex((candidateArgument) => candidateArgument === argument);
|
|
54160
|
+
return argumentIndex === -1 ? null : {
|
|
54161
|
+
callExpression: parent,
|
|
54162
|
+
argumentIndex
|
|
54163
|
+
};
|
|
54164
|
+
};
|
|
54165
|
+
const isMatchingRegistrationAndRelease = (registration, release, context) => {
|
|
54166
|
+
const releaseMethodName = getStaticMemberCallMethodName(release.callExpression);
|
|
54167
|
+
const expectedRegistrationMethod = releaseMethodName ? REGISTRATION_METHOD_BY_RELEASE_METHOD.get(releaseMethodName) : null;
|
|
54168
|
+
if (getStaticMemberCallMethodName(registration.callExpression) !== expectedRegistrationMethod) return false;
|
|
54169
|
+
if (registration.argumentIndex !== release.argumentIndex) return false;
|
|
54170
|
+
const registrationCallee = registration.callExpression.callee;
|
|
54171
|
+
const releaseCallee = release.callExpression.callee;
|
|
54172
|
+
if (!isNodeOfType(registrationCallee, "MemberExpression") || !isNodeOfType(releaseCallee, "MemberExpression")) return false;
|
|
54173
|
+
const registrationReceiverKey = resolveExpressionKey$1(registrationCallee.object, context);
|
|
54174
|
+
if (registrationReceiverKey === null || registrationReceiverKey !== resolveExpressionKey$1(releaseCallee.object, context)) return false;
|
|
54175
|
+
const registrationArguments = registration.callExpression.arguments ?? [];
|
|
54176
|
+
const releaseArguments = release.callExpression.arguments ?? [];
|
|
54177
|
+
if (registrationArguments.length !== releaseArguments.length) return false;
|
|
54178
|
+
return registrationArguments.every((registrationArgument, argumentIndex) => {
|
|
54179
|
+
if (argumentIndex === registration.argumentIndex) return true;
|
|
54180
|
+
const registrationArgumentKey = resolveExpressionKey$1(registrationArgument, context);
|
|
54181
|
+
return registrationArgumentKey !== null && registrationArgumentKey === resolveExpressionKey$1(releaseArguments[argumentIndex], context);
|
|
54182
|
+
});
|
|
54183
|
+
};
|
|
54184
|
+
const findExclusiveSubHandlerCall = (enclosingFunction, context) => {
|
|
52503
54185
|
const directParent = enclosingFunction.parent;
|
|
52504
54186
|
if (isNodeOfType(directParent, "CallExpression") && (directParent.arguments ?? []).some((arg) => arg === enclosingFunction) && isCallExpressionWithSubHandlerCallee(directParent)) return directParent;
|
|
52505
|
-
const
|
|
52506
|
-
if (
|
|
52507
|
-
let
|
|
52508
|
-
|
|
52509
|
-
|
|
52510
|
-
|
|
52511
|
-
|
|
52512
|
-
|
|
52513
|
-
|
|
52514
|
-
return false;
|
|
54187
|
+
const bindingIdentifier = getFunctionBindingIdentifier$1(enclosingFunction);
|
|
54188
|
+
if (!bindingIdentifier) return null;
|
|
54189
|
+
let bindingSymbol = context.scopes.symbolFor(bindingIdentifier);
|
|
54190
|
+
if (isNodeOfType(enclosingFunction, "FunctionDeclaration")) {
|
|
54191
|
+
let bindingScope = context.scopes.scopeFor(enclosingFunction);
|
|
54192
|
+
bindingSymbol = null;
|
|
54193
|
+
while (bindingScope && !bindingSymbol) {
|
|
54194
|
+
bindingSymbol = bindingScope.symbols.find((candidateSymbol) => candidateSymbol.declarationNode === enclosingFunction) ?? null;
|
|
54195
|
+
bindingScope = bindingScope.parent;
|
|
52515
54196
|
}
|
|
52516
|
-
}
|
|
52517
|
-
return
|
|
54197
|
+
}
|
|
54198
|
+
if (!bindingSymbol) return null;
|
|
54199
|
+
const registrations = [];
|
|
54200
|
+
const releases = [];
|
|
54201
|
+
for (const reference of bindingSymbol.references) {
|
|
54202
|
+
if (isAstDescendant(reference.identifier, enclosingFunction)) continue;
|
|
54203
|
+
if (reference.identifier === bindingIdentifier) continue;
|
|
54204
|
+
if (reference.flag !== "read") return null;
|
|
54205
|
+
const receivingUse = getCallArgumentUse(reference.identifier);
|
|
54206
|
+
if (!receivingUse) return null;
|
|
54207
|
+
if (isCallExpressionWithSubHandlerCallee(receivingUse.callExpression)) {
|
|
54208
|
+
registrations.push(receivingUse);
|
|
54209
|
+
continue;
|
|
54210
|
+
}
|
|
54211
|
+
const methodName = getStaticMemberCallMethodName(receivingUse.callExpression);
|
|
54212
|
+
if (!methodName || !REGISTRATION_METHOD_BY_RELEASE_METHOD.has(methodName)) return null;
|
|
54213
|
+
releases.push(receivingUse);
|
|
54214
|
+
}
|
|
54215
|
+
if (releases.some((release) => !registrations.some((registration) => isMatchingRegistrationAndRelease(registration, release, context)))) return null;
|
|
54216
|
+
return registrations[0]?.callExpression ?? null;
|
|
52518
54217
|
};
|
|
52519
|
-
const classifyCallableReadsInsideEffect = (
|
|
54218
|
+
const classifyCallableReadsInsideEffect = (callableIdentifier, effectCallback, context) => {
|
|
52520
54219
|
let hasAnyRead = false;
|
|
52521
54220
|
let allReadsAreInSubHandlers = true;
|
|
52522
54221
|
let firstSubHandlerName = null;
|
|
54222
|
+
const callableSymbol = context.scopes.symbolFor(callableIdentifier);
|
|
54223
|
+
if (!callableSymbol) return {
|
|
54224
|
+
hasAnyRead,
|
|
54225
|
+
allReadsAreInSubHandlers,
|
|
54226
|
+
firstSubHandlerName
|
|
54227
|
+
};
|
|
52523
54228
|
walkAst(effectCallback, (child) => {
|
|
52524
54229
|
if (!isNodeOfType(child, "Identifier")) return;
|
|
52525
|
-
if (child
|
|
54230
|
+
if (context.scopes.symbolFor(child)?.id !== callableSymbol.id) return;
|
|
52526
54231
|
const parent = child.parent;
|
|
52527
54232
|
if (isNodeOfType(parent, "ArrayExpression")) return;
|
|
52528
54233
|
if (isNodeOfType(parent, "MemberExpression") && !parent.computed && parent.property === child) return;
|
|
@@ -52533,7 +54238,7 @@ const classifyCallableReadsInsideEffect = (callableName, effectCallback) => {
|
|
|
52533
54238
|
allReadsAreInSubHandlers = false;
|
|
52534
54239
|
return;
|
|
52535
54240
|
}
|
|
52536
|
-
const subHandlerCall =
|
|
54241
|
+
const subHandlerCall = findExclusiveSubHandlerCall(enclosingFunction, context);
|
|
52537
54242
|
if (!subHandlerCall) {
|
|
52538
54243
|
allReadsAreInSubHandlers = false;
|
|
52539
54244
|
return;
|
|
@@ -52576,7 +54281,7 @@ const preferUseEffectEvent = defineRule({
|
|
|
52576
54281
|
const isFunctionTypedPropDep = propStackTracker.isPropName(depName) && REACT_HANDLER_PROP_PATTERN.test(depName);
|
|
52577
54282
|
const isFunctionTypedLocalDep = potentiallyChangingCallbackBindings.has(depName);
|
|
52578
54283
|
if (!isFunctionTypedPropDep && !isFunctionTypedLocalDep) continue;
|
|
52579
|
-
const classification = classifyCallableReadsInsideEffect(
|
|
54284
|
+
const classification = classifyCallableReadsInsideEffect(depElement, callback, context);
|
|
52580
54285
|
if (!classification.hasAnyRead) continue;
|
|
52581
54286
|
if (!classification.allReadsAreInSubHandlers) continue;
|
|
52582
54287
|
const subHandlerLabel = classification.firstSubHandlerName ? `\`${classification.firstSubHandlerName}\`` : "an async sub-handler";
|
|
@@ -53891,12 +55596,7 @@ const DOMPURIFY_MODULES = new Set(["dompurify", "isomorphic-dompurify"]);
|
|
|
53891
55596
|
const REACT_MARKDOWN_NAMED_EXPORTS = new Set(["MarkdownAsync", "MarkdownHooks"]);
|
|
53892
55597
|
const REACT_MARKDOWN_NAMESPACE_EXPORTS = new Set(["default", ...REACT_MARKDOWN_NAMED_EXPORTS]);
|
|
53893
55598
|
const DEFAULT_EXPORT_NAMES = new Set(["default"]);
|
|
53894
|
-
const
|
|
53895
|
-
if (symbol.kind !== "import") return null;
|
|
53896
|
-
const importDeclaration = symbol.declarationNode.parent;
|
|
53897
|
-
return isNodeOfType(importDeclaration, "ImportDeclaration") ? importDeclaration : null;
|
|
53898
|
-
};
|
|
53899
|
-
const isImportFromModule = (symbol, moduleName) => getImportDeclaration(symbol)?.source.value === moduleName;
|
|
55599
|
+
const isImportFromModule = (symbol, moduleName) => getImportDeclarationForSymbol(symbol)?.source.value === moduleName;
|
|
53900
55600
|
const isDefaultImportSymbol = (symbol, moduleName) => {
|
|
53901
55601
|
if (!isImportFromModule(symbol, moduleName)) return false;
|
|
53902
55602
|
return isNodeOfType(symbol.declarationNode, "ImportDefaultSpecifier") || getImportedName(symbol.declarationNode) === "default";
|
|
@@ -53986,7 +55686,7 @@ const getAttributeExpression = (attribute) => {
|
|
|
53986
55686
|
const isDomPurifyNamespace = (node, scopes) => {
|
|
53987
55687
|
const symbol = resolveImportedIdentifier(node, scopes);
|
|
53988
55688
|
if (!symbol) return false;
|
|
53989
|
-
const importDeclaration =
|
|
55689
|
+
const importDeclaration = getImportDeclarationForSymbol(symbol);
|
|
53990
55690
|
if (!importDeclaration || !DOMPURIFY_MODULES.has(String(importDeclaration.source.value))) return false;
|
|
53991
55691
|
return isNodeOfType(symbol.declarationNode, "ImportDefaultSpecifier") || isNodeOfType(symbol.declarationNode, "ImportNamespaceSpecifier") || getImportedName(symbol.declarationNode) === "default";
|
|
53992
55692
|
};
|
|
@@ -56960,7 +58660,7 @@ const resolveImportedRecyclerName = (node, localName, options) => {
|
|
|
56960
58660
|
const jsxMemberObjectName = options?.allowNamespaceMemberAccess ? getJsxMemberObjectName(node) : null;
|
|
56961
58661
|
for (const [canonicalName, packageSources] of Object.entries(RECYCLABLE_LIST_PACKAGES)) {
|
|
56962
58662
|
if (jsxMemberObjectName !== null) {
|
|
56963
|
-
if (localName === canonicalName && packageSources.some((packageSource) => isNamespaceImportFromModule(node, jsxMemberObjectName, packageSource))) return canonicalName;
|
|
58663
|
+
if (localName === canonicalName && packageSources.some((packageSource) => isNamespaceImportFromModule$1(node, jsxMemberObjectName, packageSource))) return canonicalName;
|
|
56964
58664
|
continue;
|
|
56965
58665
|
}
|
|
56966
58666
|
if (packageSources.some((packageSource) => getImportedNameFromModule(node, localName, packageSource) === canonicalName)) return canonicalName;
|
|
@@ -58219,7 +59919,7 @@ const isNamedImportOf = (contextNode, localName, componentName) => {
|
|
|
58219
59919
|
return false;
|
|
58220
59920
|
};
|
|
58221
59921
|
const isExpoUiNamespaceImport = (contextNode, localName) => {
|
|
58222
|
-
for (const moduleSource of EXPO_UI_MODULE_SOURCES) if (isNamespaceImportFromModule(contextNode, localName, moduleSource)) return true;
|
|
59922
|
+
for (const moduleSource of EXPO_UI_MODULE_SOURCES) if (isNamespaceImportFromModule$1(contextNode, localName, moduleSource)) return true;
|
|
58223
59923
|
return false;
|
|
58224
59924
|
};
|
|
58225
59925
|
const isExpoUiComponentElement = (openingElement, contextNode, componentName) => {
|
|
@@ -59367,6 +61067,7 @@ const roleHasRequiredAriaProps = defineRule({
|
|
|
59367
61067
|
recommendation: "Add every required `aria-*` attribute so assistive tech can expose the role's state correctly.",
|
|
59368
61068
|
category: "Accessibility",
|
|
59369
61069
|
create: (context) => ({ JSXOpeningElement(node) {
|
|
61070
|
+
if (isLocalTestScaffoldJsx(node, context)) return;
|
|
59370
61071
|
const elementType = getElementType(node, context.settings);
|
|
59371
61072
|
if (!HTML_TAGS.has(elementType)) return;
|
|
59372
61073
|
const roleAttribute = hasJsxPropIgnoreCase(node.attributes, "role");
|
|
@@ -62502,6 +64203,7 @@ const roleSupportsAriaProps = defineRule({
|
|
|
62502
64203
|
recommendation: "Only use `aria-*` attributes that the element's role supports.",
|
|
62503
64204
|
category: "Accessibility",
|
|
62504
64205
|
create: (context) => ({ JSXOpeningElement(node) {
|
|
64206
|
+
if (isLocalTestScaffoldJsx(node, context)) return;
|
|
62505
64207
|
let ariaAttributes = null;
|
|
62506
64208
|
for (const attribute of node.attributes) {
|
|
62507
64209
|
if (!isNodeOfType(attribute, "JSXAttribute")) continue;
|
|
@@ -64555,17 +66257,34 @@ const stylePropObject = defineRule({
|
|
|
64555
66257
|
};
|
|
64556
66258
|
}
|
|
64557
66259
|
});
|
|
66260
|
+
//#endregion
|
|
66261
|
+
//#region src/plugin/rules/security-scan/utils/has-use-server-directive-in-content.ts
|
|
66262
|
+
const hasUseServerDirectiveInContent = (content, relativePath = "source.tsx") => {
|
|
66263
|
+
const programNode = parseSourceText({
|
|
66264
|
+
filename: relativePath,
|
|
66265
|
+
sourceText: content,
|
|
66266
|
+
shouldAttachParentReferences: false
|
|
66267
|
+
});
|
|
66268
|
+
return programNode === null ? false : hasDirective(programNode, "use server");
|
|
66269
|
+
};
|
|
66270
|
+
//#endregion
|
|
66271
|
+
//#region src/plugin/rules/security-scan/supabase-client-owned-authz-field.ts
|
|
66272
|
+
const scanSupabaseClientOwnedAuthzField = scanByPattern({
|
|
66273
|
+
shouldScan: (file) => isClientSourcePath(file.relativePath),
|
|
66274
|
+
pattern: /\b(?:ownerId|ownerID|creatorId|creatorID|userId|userID|uid|providerId|providerID|orgId|orgID|tenantId|tenantID|teamId|teamID|workspaceId|workspaceID|ghostOrg|role|roles|isAdmin|admin)\b/,
|
|
66275
|
+
requireAll: [/\b(?:supabase\b|\.from\s*\(\s*["'][^"']+["']\s*\))[\s\S]{0,700}\b(?:insert|upsert|update)\s*\(\s*(?:\{|\[?\s*\{)[\s\S]{0,700}\b(?:ownerId|creatorId|userId|orgId|tenantId|role|isAdmin)\b/i],
|
|
66276
|
+
message: "Client Supabase code appears to write user, tenant, owner, or role fields that should be enforced by RLS."
|
|
66277
|
+
});
|
|
64558
66278
|
const supabaseClientOwnedAuthzField = defineRule({
|
|
64559
66279
|
id: "supabase-client-owned-authz-field",
|
|
64560
66280
|
title: "Client writes Supabase authorization field",
|
|
64561
66281
|
severity: "error",
|
|
64562
66282
|
recommendation: "Use RLS policies based on `auth.uid()` and server-owned membership rows; do not trust client-provided owner, org, or role columns.",
|
|
64563
|
-
scan:
|
|
64564
|
-
|
|
64565
|
-
|
|
64566
|
-
|
|
64567
|
-
|
|
64568
|
-
})
|
|
66283
|
+
scan: (file) => {
|
|
66284
|
+
const findings = scanSupabaseClientOwnedAuthzField(file);
|
|
66285
|
+
if (findings.length === 0) return findings;
|
|
66286
|
+
return hasUseServerDirectiveInContent(file.content, file.relativePath) ? [] : findings;
|
|
66287
|
+
}
|
|
64569
66288
|
});
|
|
64570
66289
|
//#endregion
|
|
64571
66290
|
//#region src/plugin/rules/security-scan/utils/is-supabase-migration-path.ts
|