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