oxlint-plugin-react-doctor 0.7.9-dev.6325f9c → 0.7.9-dev.71892af
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 +3216 -597
- 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) => {
|
|
@@ -13553,6 +14836,8 @@ const isReleaseReachableForUsage = (releaseNode, usage, context) => {
|
|
|
13553
14836
|
const releaseFunction = findEnclosingFunction$1(releaseNode);
|
|
13554
14837
|
if (!releaseFunction) return true;
|
|
13555
14838
|
if (releaseFunction === findEnclosingFunction$1(usage.node)) return true;
|
|
14839
|
+
const usageFunction = findEnclosingFunction$1(usage.node);
|
|
14840
|
+
if (usageFunction && isFunctionLike$1(usageFunction) && getAssignedReactRefSymbol(usageFunction, context) && isCleanupFunctionReferencedByReturn(usageFunction, releaseFunction, context)) return isReactRefCallbackCleanupOwnedByEffect(usageFunction, releaseFunction, usage, context);
|
|
13556
14841
|
return isPotentiallyReachableFunction(releaseFunction, context);
|
|
13557
14842
|
};
|
|
13558
14843
|
const fileContainsReleaseForUsage = (usage, context) => {
|
|
@@ -13793,7 +15078,23 @@ const isUseSyncExternalStoreSubscribeFunction = (functionNode, context) => {
|
|
|
13793
15078
|
};
|
|
13794
15079
|
return isSubscribeBinding(bindingIdentifier);
|
|
13795
15080
|
};
|
|
13796
|
-
const
|
|
15081
|
+
const findUnconditionalReturnStatement = (expression, ownerFunction) => {
|
|
15082
|
+
let expressionRoot = findTransparentExpressionRoot(expression);
|
|
15083
|
+
while (isNodeOfType(expressionRoot.parent, "SequenceExpression") && expressionRoot.parent.expressions.at(-1) === expressionRoot) expressionRoot = findTransparentExpressionRoot(expressionRoot.parent);
|
|
15084
|
+
const returnStatement = expressionRoot.parent;
|
|
15085
|
+
return isNodeOfType(returnStatement, "ReturnStatement") && returnStatement.argument === expressionRoot && findEnclosingFunction$1(returnStatement) === ownerFunction ? returnStatement : null;
|
|
15086
|
+
};
|
|
15087
|
+
const getFinalSequenceExpressionValue = (expression) => {
|
|
15088
|
+
let finalExpression = stripParenExpression(expression);
|
|
15089
|
+
while (isNodeOfType(finalExpression, "SequenceExpression")) {
|
|
15090
|
+
const sequenceResult = finalExpression.expressions.at(-1);
|
|
15091
|
+
if (!sequenceResult) break;
|
|
15092
|
+
finalExpression = stripParenExpression(sequenceResult);
|
|
15093
|
+
}
|
|
15094
|
+
return finalExpression;
|
|
15095
|
+
};
|
|
15096
|
+
const doesResourceResultEscape = (resourceNode, allowReturnedResourceEscape, allowConciseReturnEscape, context) => {
|
|
15097
|
+
if (!allowReturnedResourceEscape) return false;
|
|
13797
15098
|
let currentNode = resourceNode;
|
|
13798
15099
|
let parentNode = currentNode.parent;
|
|
13799
15100
|
while (parentNode) {
|
|
@@ -13804,22 +15105,38 @@ const doesResourceResultEscape = (resourceNode, allowConciseReturnEscape) => {
|
|
|
13804
15105
|
parentNode = currentNode.parent;
|
|
13805
15106
|
continue;
|
|
13806
15107
|
}
|
|
15108
|
+
if (isNodeOfType(parentNode, "ConditionalExpression") && (parentNode.consequent === currentNode || parentNode.alternate === currentNode) || isNodeOfType(parentNode, "LogicalExpression") && (parentNode.right === currentNode || parentNode.left === currentNode && parentNode.operator !== "&&")) {
|
|
15109
|
+
currentNode = parentNode;
|
|
15110
|
+
parentNode = currentNode.parent;
|
|
15111
|
+
continue;
|
|
15112
|
+
}
|
|
15113
|
+
if (isNodeOfType(parentNode, "VariableDeclarator") && parentNode.init === currentNode && isNodeOfType(parentNode.id, "Identifier") && isNodeOfType(parentNode.parent, "VariableDeclaration") && parentNode.parent.kind === "const") {
|
|
15114
|
+
const ownerFunction = findEnclosingFunction$1(resourceNode);
|
|
15115
|
+
const resourceSymbol = context.scopes.symbolFor(parentNode.id);
|
|
15116
|
+
if (!ownerFunction || !resourceSymbol) return false;
|
|
15117
|
+
return doMatchingNodesCoverEveryPathAfterUsage(resourceNode, resourceSymbol.references.flatMap((reference) => {
|
|
15118
|
+
if (reference.flag !== "read") return [];
|
|
15119
|
+
const returnStatement = findUnconditionalReturnStatement(reference.identifier, ownerFunction);
|
|
15120
|
+
return returnStatement ? [returnStatement] : [];
|
|
15121
|
+
}), context);
|
|
15122
|
+
}
|
|
13807
15123
|
return false;
|
|
13808
15124
|
}
|
|
13809
15125
|
return false;
|
|
13810
15126
|
};
|
|
13811
|
-
const findRetainedFunctionLeak = (retainedFunction, context) => {
|
|
15127
|
+
const findRetainedFunctionLeak = (retainedFunction, context, options) => {
|
|
13812
15128
|
if (!isFunctionLike$1(retainedFunction)) return null;
|
|
13813
15129
|
const body = retainedFunction.body;
|
|
13814
15130
|
if (!body) return null;
|
|
13815
15131
|
let leak = null;
|
|
13816
|
-
const
|
|
15132
|
+
const allowReturnedResourceEscape = options?.allowReturnedResourceEscape !== false && !retainedFunction.async && !isInlineRetainedHandlerFunction(retainedFunction, context);
|
|
15133
|
+
const allowReturnedSocketEscape = allowReturnedResourceEscape && options?.requireCallableReturnedResource !== true;
|
|
13817
15134
|
const isExternalStoreSubscribeFunction = isUseSyncExternalStoreSubscribeFunction(retainedFunction, context);
|
|
13818
15135
|
const hasReleaseForUsage = (usage) => isExternalStoreSubscribeFunction ? effectHasCleanupForUsage(retainedFunction, usage, context) : fileContainsReleaseForUsage(usage, context) || hasGuaranteedRefOwnedUnmountCleanup(retainedFunction, usage, context);
|
|
13819
15136
|
walkAst(body, (child) => {
|
|
13820
15137
|
if (leak !== null) return false;
|
|
13821
15138
|
if (isFunctionLike$1(child)) return false;
|
|
13822
|
-
if (isSocketConstruction(child) && !doesResourceResultEscape(child, false)) {
|
|
15139
|
+
if (isSocketConstruction(child) && !doesResourceResultEscape(child, allowReturnedSocketEscape, false, context)) {
|
|
13823
15140
|
const socketUsage = {
|
|
13824
15141
|
kind: "socket",
|
|
13825
15142
|
node: child,
|
|
@@ -13836,14 +15153,14 @@ const findRetainedFunctionLeak = (retainedFunction, context) => {
|
|
|
13836
15153
|
}
|
|
13837
15154
|
}
|
|
13838
15155
|
if (!isNodeOfType(child, "CallExpression")) return;
|
|
13839
|
-
if (isNodeOfType(child.callee, "Identifier") && child.callee.name === "setInterval" && !doesResourceResultEscape(child,
|
|
15156
|
+
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
15157
|
const timerUsage = {
|
|
13841
15158
|
kind: "timer",
|
|
13842
15159
|
node: child,
|
|
13843
|
-
resourceName:
|
|
15160
|
+
resourceName: child.callee.name,
|
|
13844
15161
|
handleKey: findAssignedResourceKey(child, context),
|
|
13845
15162
|
receiverKey: null,
|
|
13846
|
-
registrationVerbName:
|
|
15163
|
+
registrationVerbName: child.callee.name,
|
|
13847
15164
|
eventKey: null,
|
|
13848
15165
|
handlerKey: null
|
|
13849
15166
|
};
|
|
@@ -13852,7 +15169,7 @@ const findRetainedFunctionLeak = (retainedFunction, context) => {
|
|
|
13852
15169
|
return false;
|
|
13853
15170
|
}
|
|
13854
15171
|
}
|
|
13855
|
-
if (isSubscribeOrObserveCall(child) && !doesResourceResultEscape(child,
|
|
15172
|
+
if (isSubscribeOrObserveCall(child) && (!doesResourceResultEscape(child, allowReturnedResourceEscape, allowReturnedResourceEscape, context) || options?.requireCallableReturnedResource === true && !isCleanupReturningSubscribeLikeCallExpression(child))) {
|
|
13856
15173
|
const registrationDetails = getCallRegistrationDetails(child, context);
|
|
13857
15174
|
const subscriptionUsage = {
|
|
13858
15175
|
kind: "subscribe",
|
|
@@ -13867,6 +15184,184 @@ const findRetainedFunctionLeak = (retainedFunction, context) => {
|
|
|
13867
15184
|
});
|
|
13868
15185
|
return leak;
|
|
13869
15186
|
};
|
|
15187
|
+
const getAssignedReactRefCallbackDefinition = (functionNode, context) => {
|
|
15188
|
+
if (!isFunctionLike$1(functionNode)) return null;
|
|
15189
|
+
if (functionNode.generator) return null;
|
|
15190
|
+
const functionRoot = findTransparentExpressionRoot(functionNode);
|
|
15191
|
+
const assignment = functionRoot.parent;
|
|
15192
|
+
if (!isNodeOfType(assignment, "AssignmentExpression") || assignment.operator !== "=" || assignment.right !== functionRoot) return null;
|
|
15193
|
+
const refSymbol = resolveReactRefSymbol(stripParenExpression(assignment.left), context.scopes);
|
|
15194
|
+
if (!refSymbol) return null;
|
|
15195
|
+
const componentFunction = findRenderPhaseComponentOrHook(assignment, context.scopes);
|
|
15196
|
+
if (!isFunctionLike$1(componentFunction) || findEnclosingFunction$1(assignment) !== componentFunction || findEnclosingFunction$1(refSymbol.bindingIdentifier) !== componentFunction || !isNodeReachableWithinFunction(assignment, context)) return null;
|
|
15197
|
+
return {
|
|
15198
|
+
assignmentNode: assignment,
|
|
15199
|
+
functionNode,
|
|
15200
|
+
refSymbol
|
|
15201
|
+
};
|
|
15202
|
+
};
|
|
15203
|
+
const getAssignedReactRefSymbol = (functionNode, context) => getAssignedReactRefCallbackDefinition(functionNode, context)?.refSymbol ?? null;
|
|
15204
|
+
const isExpressionReturnedFromFunction = (expression, ownerFunction, context) => {
|
|
15205
|
+
let expressionRoot = findTransparentExpressionRoot(expression);
|
|
15206
|
+
const bindingDeclarator = expressionRoot.parent;
|
|
15207
|
+
if (isNodeOfType(bindingDeclarator, "VariableDeclarator") && bindingDeclarator.init === expressionRoot && isNodeOfType(bindingDeclarator.id, "Identifier") && isNodeOfType(bindingDeclarator.parent, "VariableDeclaration") && bindingDeclarator.parent.kind === "const") {
|
|
15208
|
+
const resultSymbol = context.scopes.symbolFor(bindingDeclarator.id);
|
|
15209
|
+
if (!resultSymbol) return false;
|
|
15210
|
+
return doMatchingNodesCoverEveryPathAfterUsage(expression, resultSymbol.references.flatMap((reference) => {
|
|
15211
|
+
if (reference.flag !== "read") return [];
|
|
15212
|
+
const returnStatement = findUnconditionalReturnStatement(reference.identifier, ownerFunction);
|
|
15213
|
+
return returnStatement ? [returnStatement] : [];
|
|
15214
|
+
}), context);
|
|
15215
|
+
}
|
|
15216
|
+
while (true) {
|
|
15217
|
+
const container = expressionRoot.parent;
|
|
15218
|
+
if (isNodeOfType(container, "ConditionalExpression") && (container.consequent === expressionRoot || container.alternate === expressionRoot)) {
|
|
15219
|
+
expressionRoot = findTransparentExpressionRoot(container);
|
|
15220
|
+
continue;
|
|
15221
|
+
}
|
|
15222
|
+
if (isNodeOfType(container, "SequenceExpression") && container.expressions.at(-1) === expressionRoot) {
|
|
15223
|
+
expressionRoot = findTransparentExpressionRoot(container);
|
|
15224
|
+
continue;
|
|
15225
|
+
}
|
|
15226
|
+
if (isNodeOfType(container, "LogicalExpression") && container.right === expressionRoot) {
|
|
15227
|
+
expressionRoot = findTransparentExpressionRoot(container);
|
|
15228
|
+
continue;
|
|
15229
|
+
}
|
|
15230
|
+
break;
|
|
15231
|
+
}
|
|
15232
|
+
const returnStatement = expressionRoot.parent;
|
|
15233
|
+
return Boolean(isNodeOfType(returnStatement, "ReturnStatement") && returnStatement.argument === expressionRoot && findEnclosingFunction$1(returnStatement) === ownerFunction || isNodeOfType(ownerFunction, "ArrowFunctionExpression") && ownerFunction.body === expressionRoot);
|
|
15234
|
+
};
|
|
15235
|
+
const isReactRefCurrentCall = (node, refSymbol, context) => isNodeOfType(node, "CallExpression") && resolveReactRefSymbol(stripParenExpression(node.callee), context.scopes)?.id === refSymbol.id;
|
|
15236
|
+
const collectAssignedReactRefCallbacks = (componentFunction, context) => {
|
|
15237
|
+
const callbackDefinitionsByRefSymbolId = /* @__PURE__ */ new Map();
|
|
15238
|
+
walkAst(componentFunction.body, (child) => {
|
|
15239
|
+
if (!isFunctionLike$1(child)) return;
|
|
15240
|
+
const callbackDefinition = getAssignedReactRefCallbackDefinition(child, context);
|
|
15241
|
+
if (callbackDefinition) {
|
|
15242
|
+
const existingDefinitions = callbackDefinitionsByRefSymbolId.get(callbackDefinition.refSymbol.id) ?? [];
|
|
15243
|
+
existingDefinitions.push(callbackDefinition);
|
|
15244
|
+
callbackDefinitionsByRefSymbolId.set(callbackDefinition.refSymbol.id, existingDefinitions);
|
|
15245
|
+
}
|
|
15246
|
+
return false;
|
|
15247
|
+
});
|
|
15248
|
+
for (const [refSymbolId, callbackDefinitions] of callbackDefinitionsByRefSymbolId) {
|
|
15249
|
+
const activeDefinitions = callbackDefinitions.filter((callbackDefinition) => !doMatchingNodesCoverEveryPathAfterUsage(callbackDefinition.assignmentNode, callbackDefinitions.filter((otherDefinition) => otherDefinition !== callbackDefinition).map((otherDefinition) => otherDefinition.assignmentNode), context));
|
|
15250
|
+
if (activeDefinitions.length === 0) callbackDefinitionsByRefSymbolId.delete(refSymbolId);
|
|
15251
|
+
else callbackDefinitionsByRefSymbolId.set(refSymbolId, activeDefinitions);
|
|
15252
|
+
}
|
|
15253
|
+
return callbackDefinitionsByRefSymbolId;
|
|
15254
|
+
};
|
|
15255
|
+
const collectUndominatedReactRefCalls = (ownerFunction, refSymbol, context) => {
|
|
15256
|
+
if (!isFunctionLike$1(ownerFunction)) return [];
|
|
15257
|
+
const refWrites = [];
|
|
15258
|
+
const refCalls = [];
|
|
15259
|
+
walkAst(ownerFunction.body, (child) => {
|
|
15260
|
+
if (child !== ownerFunction.body && isFunctionLike$1(child)) return false;
|
|
15261
|
+
if (isNodeOfType(child, "AssignmentExpression") && isNodeReachableWithinFunction(child, context) && resolveReactRefSymbol(stripParenExpression(child.left), context.scopes)?.id === refSymbol.id) refWrites.push(child);
|
|
15262
|
+
if (isReactRefCurrentCall(child, refSymbol, context) && isNodeReachableWithinFunction(child, context)) refCalls.push(child);
|
|
15263
|
+
});
|
|
15264
|
+
return refCalls.filter((refCall) => !doMatchingNodesCoverEveryPathBeforeUsage(refCall, refWrites, ownerFunction, context));
|
|
15265
|
+
};
|
|
15266
|
+
const mergeReactRefEffectUsage = (usageByRefSymbolId, refSymbolId, doesEffectOwnResult) => {
|
|
15267
|
+
const existingUsage = usageByRefSymbolId.get(refSymbolId);
|
|
15268
|
+
if (!existingUsage) {
|
|
15269
|
+
usageByRefSymbolId.set(refSymbolId, { doesEffectOwnEveryResult: doesEffectOwnResult });
|
|
15270
|
+
return true;
|
|
15271
|
+
}
|
|
15272
|
+
if (!existingUsage.doesEffectOwnEveryResult || doesEffectOwnResult) return false;
|
|
15273
|
+
existingUsage.doesEffectOwnEveryResult = false;
|
|
15274
|
+
return true;
|
|
15275
|
+
};
|
|
15276
|
+
const collectReactRefEffectAnalysis = (componentFunction, context) => {
|
|
15277
|
+
let analysisByComponent = REACT_REF_EFFECT_ANALYSIS_CACHE.get(context);
|
|
15278
|
+
if (!analysisByComponent) {
|
|
15279
|
+
analysisByComponent = /* @__PURE__ */ new WeakMap();
|
|
15280
|
+
REACT_REF_EFFECT_ANALYSIS_CACHE.set(context, analysisByComponent);
|
|
15281
|
+
}
|
|
15282
|
+
const cachedAnalysis = analysisByComponent.get(componentFunction);
|
|
15283
|
+
if (cachedAnalysis) return cachedAnalysis;
|
|
15284
|
+
const callbackDefinitionsByRefSymbolId = collectAssignedReactRefCallbacks(componentFunction, context);
|
|
15285
|
+
const usageByRefSymbolId = /* @__PURE__ */ new Map();
|
|
15286
|
+
walkAst(componentFunction.body, (child) => {
|
|
15287
|
+
if (child !== componentFunction.body && isFunctionLike$1(child)) return false;
|
|
15288
|
+
if (!isNodeOfType(child, "CallExpression") || findEnclosingFunction$1(child) !== componentFunction || !isReactApiCall(child, CLEANUP_EFFECT_HOOK_NAMES, context.scopes, { allowGlobalReactNamespace: true })) return;
|
|
15289
|
+
const effectCallback = getEffectCallback(child);
|
|
15290
|
+
if (!isFunctionLike$1(effectCallback)) return;
|
|
15291
|
+
for (const callbackDefinitions of callbackDefinitionsByRefSymbolId.values()) {
|
|
15292
|
+
const refSymbol = callbackDefinitions[0]?.refSymbol;
|
|
15293
|
+
if (!refSymbol) continue;
|
|
15294
|
+
for (const refCall of collectUndominatedReactRefCalls(effectCallback, refSymbol, context)) mergeReactRefEffectUsage(usageByRefSymbolId, refSymbol.id, !effectCallback.async && isExpressionReturnedFromFunction(refCall, effectCallback, context));
|
|
15295
|
+
}
|
|
15296
|
+
});
|
|
15297
|
+
let didUsageChange = true;
|
|
15298
|
+
while (didUsageChange) {
|
|
15299
|
+
didUsageChange = false;
|
|
15300
|
+
for (const callbackDefinitions of callbackDefinitionsByRefSymbolId.values()) {
|
|
15301
|
+
const ownerRefSymbol = callbackDefinitions[0]?.refSymbol;
|
|
15302
|
+
if (!ownerRefSymbol) continue;
|
|
15303
|
+
const ownerUsage = usageByRefSymbolId.get(ownerRefSymbol.id);
|
|
15304
|
+
if (!ownerUsage) continue;
|
|
15305
|
+
for (const callbackDefinition of callbackDefinitions) for (const targetDefinitions of callbackDefinitionsByRefSymbolId.values()) {
|
|
15306
|
+
const targetRefSymbol = targetDefinitions[0]?.refSymbol;
|
|
15307
|
+
if (!targetRefSymbol) continue;
|
|
15308
|
+
for (const refCall of collectUndominatedReactRefCalls(callbackDefinition.functionNode, targetRefSymbol, context)) {
|
|
15309
|
+
const doesEffectOwnResult = ownerUsage.doesEffectOwnEveryResult && !callbackDefinition.functionNode.async && isExpressionReturnedFromFunction(refCall, callbackDefinition.functionNode, context);
|
|
15310
|
+
if (mergeReactRefEffectUsage(usageByRefSymbolId, targetRefSymbol.id, doesEffectOwnResult)) didUsageChange = true;
|
|
15311
|
+
}
|
|
15312
|
+
}
|
|
15313
|
+
}
|
|
15314
|
+
}
|
|
15315
|
+
const analysis = {
|
|
15316
|
+
callbackDefinitionsByRefSymbolId,
|
|
15317
|
+
usageByRefSymbolId
|
|
15318
|
+
};
|
|
15319
|
+
analysisByComponent.set(componentFunction, analysis);
|
|
15320
|
+
return analysis;
|
|
15321
|
+
};
|
|
15322
|
+
const getReactRefEffectUsage = (retainedFunction, context) => {
|
|
15323
|
+
if (!isFunctionLike$1(retainedFunction)) return null;
|
|
15324
|
+
const callbackDefinition = getAssignedReactRefCallbackDefinition(retainedFunction, context);
|
|
15325
|
+
const componentFunction = findRenderPhaseComponentOrHook(retainedFunction, context.scopes);
|
|
15326
|
+
if (!callbackDefinition || !isFunctionLike$1(componentFunction)) return null;
|
|
15327
|
+
const analysis = collectReactRefEffectAnalysis(componentFunction, context);
|
|
15328
|
+
if (!analysis.callbackDefinitionsByRefSymbolId.get(callbackDefinition.refSymbol.id)?.some((activeDefinition) => activeDefinition.functionNode === retainedFunction)) return null;
|
|
15329
|
+
return analysis.usageByRefSymbolId.get(callbackDefinition.refSymbol.id) ?? null;
|
|
15330
|
+
};
|
|
15331
|
+
const isReactRefCallbackCleanupOwnedByEffect = (retainedFunction, cleanupFunction, usage, context) => {
|
|
15332
|
+
if (!isFunctionLike$1(retainedFunction) || retainedFunction.async || getReactRefEffectUsage(retainedFunction, context)?.doesEffectOwnEveryResult !== true) return false;
|
|
15333
|
+
if (!isNodeOfType(retainedFunction.body, "BlockStatement")) return false;
|
|
15334
|
+
const doesReturnedCleanupCallFunction = (returnedValue) => {
|
|
15335
|
+
const returnedCleanupFunction = resolveRefOwnedCleanupFunction(getFinalSequenceExpressionValue(returnedValue), context);
|
|
15336
|
+
if (!returnedCleanupFunction) return false;
|
|
15337
|
+
if (returnedCleanupFunction === cleanupFunction) return true;
|
|
15338
|
+
if (!isFunctionLike$1(returnedCleanupFunction)) return false;
|
|
15339
|
+
const matchingCalls = [];
|
|
15340
|
+
walkAst(returnedCleanupFunction.body, (child) => {
|
|
15341
|
+
if (child !== returnedCleanupFunction.body && isFunctionLike$1(child)) return false;
|
|
15342
|
+
if (isNodeOfType(child, "CallExpression") && resolveRefOwnedCleanupFunction(child.callee, context) === cleanupFunction) matchingCalls.push(child);
|
|
15343
|
+
});
|
|
15344
|
+
return doMatchingNodesCoverEveryPathFromFunctionEntry(returnedCleanupFunction, matchingCalls, context);
|
|
15345
|
+
};
|
|
15346
|
+
const matchingReturns = [];
|
|
15347
|
+
walkInsideStatementBlocks(retainedFunction.body, (child) => {
|
|
15348
|
+
if (isNodeOfType(child, "ReturnStatement") && child.argument && doesReturnedCleanupCallFunction(child.argument)) matchingReturns.push(child);
|
|
15349
|
+
});
|
|
15350
|
+
return doMatchingNodesCoverEveryPathAfterUsage(usage.node, matchingReturns, context);
|
|
15351
|
+
};
|
|
15352
|
+
const isCleanupFunctionReferencedByReturn = (ownerFunction, cleanupFunction, context) => {
|
|
15353
|
+
if (!isFunctionLike$1(ownerFunction) || !isNodeOfType(ownerFunction.body, "BlockStatement")) return false;
|
|
15354
|
+
let isReferencedByReturn = false;
|
|
15355
|
+
walkInsideStatementBlocks(ownerFunction.body, (child) => {
|
|
15356
|
+
if (isReferencedByReturn || !isNodeOfType(child, "ReturnStatement") || !child.argument) return;
|
|
15357
|
+
walkAst(child.argument, (returnedChild) => {
|
|
15358
|
+
if (resolveRefOwnedCleanupFunction(returnedChild, context) !== cleanupFunction) return;
|
|
15359
|
+
isReferencedByReturn = true;
|
|
15360
|
+
return false;
|
|
15361
|
+
});
|
|
15362
|
+
});
|
|
15363
|
+
return isReferencedByReturn;
|
|
15364
|
+
};
|
|
13870
15365
|
const isRetainedComponentScopeFunction = (functionNode) => {
|
|
13871
15366
|
if (isNodeOfType(functionNode, "FunctionDeclaration")) return enclosingComponentOrHookName(functionNode) !== null;
|
|
13872
15367
|
if (!isNodeOfType(functionNode, "ArrowFunctionExpression") && !isNodeOfType(functionNode, "FunctionExpression")) return false;
|
|
@@ -13901,8 +15396,14 @@ const effectNeedsCleanup = defineRule({
|
|
|
13901
15396
|
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
15397
|
create: (context) => {
|
|
13903
15398
|
const reportRetainedLeak = (retainedFunction) => {
|
|
13904
|
-
|
|
13905
|
-
|
|
15399
|
+
const refEffectUsage = getReactRefEffectUsage(retainedFunction, context);
|
|
15400
|
+
if (!refEffectUsage && !isPotentiallyReachableFunction(retainedFunction, context)) return;
|
|
15401
|
+
const leak = findRetainedFunctionLeak(retainedFunction, context, refEffectUsage ? {
|
|
15402
|
+
allowReturnedResourceEscape: refEffectUsage.doesEffectOwnEveryResult,
|
|
15403
|
+
allowReturnedTimerEscape: false,
|
|
15404
|
+
includeOneShotTimers: true,
|
|
15405
|
+
requireCallableReturnedResource: true
|
|
15406
|
+
} : void 0);
|
|
13906
15407
|
if (!leak) return;
|
|
13907
15408
|
const resourceNoun = RESOURCE_NOUN_BY_KIND[leak.kind];
|
|
13908
15409
|
context.report({
|
|
@@ -13935,10 +15436,10 @@ const effectNeedsCleanup = defineRule({
|
|
|
13935
15436
|
if (isRetainedComponentScopeFunction(node)) reportRetainedLeak(node);
|
|
13936
15437
|
},
|
|
13937
15438
|
ArrowFunctionExpression(node) {
|
|
13938
|
-
if (isRetainedComponentScopeFunction(node) || isInlineRetainedHandlerFunction(node, context)) reportRetainedLeak(node);
|
|
15439
|
+
if (isRetainedComponentScopeFunction(node) || isInlineRetainedHandlerFunction(node, context) || getAssignedReactRefSymbol(node, context)) reportRetainedLeak(node);
|
|
13939
15440
|
},
|
|
13940
15441
|
FunctionExpression(node) {
|
|
13941
|
-
if (isRetainedComponentScopeFunction(node) || isInlineRetainedHandlerFunction(node, context)) reportRetainedLeak(node);
|
|
15442
|
+
if (isRetainedComponentScopeFunction(node) || isInlineRetainedHandlerFunction(node, context) || getAssignedReactRefSymbol(node, context)) reportRetainedLeak(node);
|
|
13942
15443
|
}
|
|
13943
15444
|
};
|
|
13944
15445
|
}
|
|
@@ -14401,14 +15902,14 @@ const DEPENDENCY_HOOK_NAMES = new Set([
|
|
|
14401
15902
|
"useImperativeHandle",
|
|
14402
15903
|
"useInsertionEffect"
|
|
14403
15904
|
]);
|
|
14404
|
-
const crossFileScopes = /* @__PURE__ */ new WeakMap();
|
|
15905
|
+
const crossFileScopes$1 = /* @__PURE__ */ new WeakMap();
|
|
14405
15906
|
const crossFileControlFlow = /* @__PURE__ */ new WeakMap();
|
|
14406
15907
|
const forwardedFreshDependencyCache = /* @__PURE__ */ new WeakMap();
|
|
14407
|
-
const getCrossFileScopes = (resolved) => {
|
|
14408
|
-
const cached = crossFileScopes.get(resolved.programNode);
|
|
15908
|
+
const getCrossFileScopes$1 = (resolved) => {
|
|
15909
|
+
const cached = crossFileScopes$1.get(resolved.programNode);
|
|
14409
15910
|
if (cached) return cached;
|
|
14410
15911
|
const scopes = analyzeScopes(resolved.programNode);
|
|
14411
|
-
crossFileScopes.set(resolved.programNode, scopes);
|
|
15912
|
+
crossFileScopes$1.set(resolved.programNode, scopes);
|
|
14412
15913
|
return scopes;
|
|
14413
15914
|
};
|
|
14414
15915
|
const getCrossFileControlFlow = (resolved) => {
|
|
@@ -14443,7 +15944,7 @@ const isCustomHookFunction = (functionNode, fallbackName) => {
|
|
|
14443
15944
|
const displayName = componentOrHookDisplayNameForFunction(functionNode) ?? fallbackName ?? "";
|
|
14444
15945
|
return /^use[A-Z0-9]/.test(displayName);
|
|
14445
15946
|
};
|
|
14446
|
-
const getImportedHookBinding = (callee, scopes) => {
|
|
15947
|
+
const getImportedHookBinding$1 = (callee, scopes) => {
|
|
14447
15948
|
if (!isNodeOfType(callee, "Identifier")) return null;
|
|
14448
15949
|
const importedSymbol = resolveConstIdentifierAlias(callee, scopes);
|
|
14449
15950
|
if (importedSymbol?.kind !== "import" || !importedSymbol.initializer) return null;
|
|
@@ -14459,7 +15960,7 @@ const getImportedHookBinding = (callee, scopes) => {
|
|
|
14459
15960
|
};
|
|
14460
15961
|
const resolveImportedHookFunction = (callee, scopes, currentFilename) => {
|
|
14461
15962
|
if (!currentFilename) return null;
|
|
14462
|
-
const importedBinding = getImportedHookBinding(callee, scopes);
|
|
15963
|
+
const importedBinding = getImportedHookBinding$1(callee, scopes);
|
|
14463
15964
|
if (!importedBinding) return null;
|
|
14464
15965
|
const resolved = resolveCrossFileFunctionExportWithFilePath(currentFilename, importedBinding.source, importedBinding.exportedName);
|
|
14465
15966
|
if (!resolved || !isCustomHookFunction(resolved.functionNode, importedBinding.exportedName)) return null;
|
|
@@ -14468,7 +15969,7 @@ const resolveImportedHookFunction = (callee, scopes, currentFilename) => {
|
|
|
14468
15969
|
filePath: resolved.filePath,
|
|
14469
15970
|
functionNode: resolved.functionNode,
|
|
14470
15971
|
programNode: resolved.programNode,
|
|
14471
|
-
scopes: getCrossFileScopes(resolved)
|
|
15972
|
+
scopes: getCrossFileScopes$1(resolved)
|
|
14472
15973
|
};
|
|
14473
15974
|
};
|
|
14474
15975
|
const dependencyIndexForReactHookReference = (expression, scopes, dependencyHookNames, visitedSymbolIds = /* @__PURE__ */ new Set()) => {
|
|
@@ -14499,11 +16000,11 @@ const dependencyIndexForReactHookReference = (expression, scopes, dependencyHook
|
|
|
14499
16000
|
};
|
|
14500
16001
|
const getImportedReactDependencyIndex = (callExpression, scopes, currentFilename, dependencyHookNames) => {
|
|
14501
16002
|
if (!currentFilename) return null;
|
|
14502
|
-
const importedBinding = getImportedHookBinding(stripParenExpression(callExpression.callee), scopes);
|
|
16003
|
+
const importedBinding = getImportedHookBinding$1(stripParenExpression(callExpression.callee), scopes);
|
|
14503
16004
|
if (!importedBinding) return null;
|
|
14504
16005
|
const resolved = resolveCrossFileValueExportWithFilePath(currentFilename, importedBinding.source, importedBinding.exportedName);
|
|
14505
16006
|
if (!resolved) return null;
|
|
14506
|
-
return dependencyIndexForReactHookReference(resolved.exportedNode, getCrossFileScopes(resolved), dependencyHookNames);
|
|
16007
|
+
return dependencyIndexForReactHookReference(resolved.exportedNode, getCrossFileScopes$1(resolved), dependencyHookNames);
|
|
14507
16008
|
};
|
|
14508
16009
|
const resolveHookFunction = (callExpression, scopes, cfg, currentFilename) => {
|
|
14509
16010
|
const callee = stripParenExpression(callExpression.callee);
|
|
@@ -17340,6 +18841,7 @@ const iframeHasTitle = defineRule({
|
|
|
17340
18841
|
recommendation: "Add a descriptive `title` so screen reader users know what the embedded frame contains.",
|
|
17341
18842
|
category: "Accessibility",
|
|
17342
18843
|
create: (context) => ({ JSXOpeningElement(node) {
|
|
18844
|
+
if (isLocalTestScaffoldJsx(node, context)) return;
|
|
17343
18845
|
const tag = getElementType(node, context.settings);
|
|
17344
18846
|
if (tag !== "iframe") return;
|
|
17345
18847
|
if (isInsideStaticallyHiddenJsxSubtree(node, context.scopes)) return;
|
|
@@ -17860,6 +19362,7 @@ const interactiveSupportsFocus = defineRule({
|
|
|
17860
19362
|
const settings = resolveSettings$37(context.settings);
|
|
17861
19363
|
const tabbableSet = new Set(settings.tabbable);
|
|
17862
19364
|
return { JSXOpeningElement(node) {
|
|
19365
|
+
if (isLocalTestScaffoldJsx(node, context)) return;
|
|
17863
19366
|
if (node.attributes.length === 0) return;
|
|
17864
19367
|
if (hasJsxSpreadAttribute$1(node.attributes)) return;
|
|
17865
19368
|
const roleAttribute = hasJsxPropIgnoreCase(node.attributes, "role");
|
|
@@ -18573,7 +20076,7 @@ const scanPerIterationLayoutReads = (body) => {
|
|
|
18573
20076
|
hasDeliberateForcedReflow
|
|
18574
20077
|
};
|
|
18575
20078
|
};
|
|
18576
|
-
const getNodeStart
|
|
20079
|
+
const getNodeStart = (node) => {
|
|
18577
20080
|
const withRange = node;
|
|
18578
20081
|
return withRange.range ? withRange.range[0] : -1;
|
|
18579
20082
|
};
|
|
@@ -18605,7 +20108,7 @@ const hasAttachmentBefore = (scopeOwner, elementName, beforeStart) => {
|
|
|
18605
20108
|
if (!isNodeOfType(child, "CallExpression")) return;
|
|
18606
20109
|
const callee = child.callee;
|
|
18607
20110
|
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
|
|
20111
|
+
if (child.arguments.some((argument) => isNodeOfType(argument, "Identifier") && argument.name === elementName) && getNodeStart(child) < beforeStart) {
|
|
18609
20112
|
foundAttachment = true;
|
|
18610
20113
|
return false;
|
|
18611
20114
|
}
|
|
@@ -18627,7 +20130,7 @@ const isProvablyDetachedAtWrite = (styleWriteStatement) => {
|
|
|
18627
20130
|
const elementExpression = assignment.left.object.object;
|
|
18628
20131
|
const creationRoot = resolveDetachedCreationRoot(elementExpression, 0);
|
|
18629
20132
|
if (!creationRoot) return false;
|
|
18630
|
-
return !hasAttachmentBefore(creationRoot.scopeOwner, creationRoot.rootName, getNodeStart
|
|
20133
|
+
return !hasAttachmentBefore(creationRoot.scopeOwner, creationRoot.rootName, getNodeStart(styleWriteStatement));
|
|
18631
20134
|
};
|
|
18632
20135
|
const jsBatchDomCss = defineRule({
|
|
18633
20136
|
id: "js-batch-dom-css",
|
|
@@ -19218,10 +20721,10 @@ const isUncacheableOptionsMergeUtility = (node) => {
|
|
|
19218
20721
|
return (argument.properties ?? []).some((property) => isNodeOfType(property, "SpreadElement") && isNodeOfType(property.argument, "Identifier") && parameterNames.has(property.argument.name));
|
|
19219
20722
|
});
|
|
19220
20723
|
};
|
|
19221
|
-
const isIntlNewExpression = (node) => {
|
|
20724
|
+
const isIntlNewExpression = (node, context) => {
|
|
19222
20725
|
if (!isNodeOfType(node, "NewExpression")) return false;
|
|
19223
20726
|
const callee = node.callee;
|
|
19224
|
-
if (isNodeOfType(callee, "MemberExpression") && isNodeOfType(callee.object, "Identifier") && callee.object.name === "Intl" && isNodeOfType(callee.property, "Identifier") && INTL_CLASSES.has(callee.property.name)) return true;
|
|
20727
|
+
if (isNodeOfType(callee, "MemberExpression") && isNodeOfType(callee.object, "Identifier") && callee.object.name === "Intl" && context.scopes.isGlobalReference(callee.object) && isNodeOfType(callee.property, "Identifier") && INTL_CLASSES.has(callee.property.name)) return true;
|
|
19225
20728
|
return false;
|
|
19226
20729
|
};
|
|
19227
20730
|
const jsHoistIntl = defineRule({
|
|
@@ -19231,7 +20734,7 @@ const jsHoistIntl = defineRule({
|
|
|
19231
20734
|
severity: "warn",
|
|
19232
20735
|
recommendation: "Move `new Intl.NumberFormat(...)` to the top of the file or wrap it in `useMemo`. Building one is slow, so don't redo it on every call",
|
|
19233
20736
|
create: (context) => ({ NewExpression(node) {
|
|
19234
|
-
if (!isIntlNewExpression(node)) return;
|
|
20737
|
+
if (!isIntlNewExpression(node, context)) return;
|
|
19235
20738
|
let cursor = node.parent ?? null;
|
|
19236
20739
|
let inFunctionBody = false;
|
|
19237
20740
|
while (cursor) {
|
|
@@ -19350,7 +20853,7 @@ const globSyncReturnsStringPaths = (node, context) => {
|
|
|
19350
20853
|
const callee = stripParenExpression(node.callee);
|
|
19351
20854
|
let isGlobSyncImport = false;
|
|
19352
20855
|
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");
|
|
20856
|
+
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
20857
|
if (!isGlobSyncImport) return false;
|
|
19355
20858
|
const options = node.arguments[1];
|
|
19356
20859
|
if (!options) return true;
|
|
@@ -19996,6 +21499,28 @@ const jsLengthCheckFirst = defineRule({
|
|
|
19996
21499
|
} })
|
|
19997
21500
|
});
|
|
19998
21501
|
//#endregion
|
|
21502
|
+
//#region src/plugin/utils/is-proven-global-namespace-reference.ts
|
|
21503
|
+
const isProvenGlobalObjectReference = (expression, scopes, visitedSymbolIds = /* @__PURE__ */ new Set()) => {
|
|
21504
|
+
const strippedExpression = stripParenExpression(expression);
|
|
21505
|
+
if (!isNodeOfType(strippedExpression, "Identifier")) return false;
|
|
21506
|
+
if ((strippedExpression.name === "globalThis" || strippedExpression.name === "window" || strippedExpression.name === "self" || strippedExpression.name === "global") && scopes.isGlobalReference(strippedExpression)) return true;
|
|
21507
|
+
const symbol = scopes.symbolFor(strippedExpression);
|
|
21508
|
+
if (!symbol?.initializer || symbol.kind !== "const" || visitedSymbolIds.has(symbol.id)) return false;
|
|
21509
|
+
visitedSymbolIds.add(symbol.id);
|
|
21510
|
+
return isProvenGlobalObjectReference(symbol.initializer, scopes, visitedSymbolIds);
|
|
21511
|
+
};
|
|
21512
|
+
const isProvenGlobalNamespaceReference = (expression, namespaceName, scopes, visitedSymbolIds = /* @__PURE__ */ new Set()) => {
|
|
21513
|
+
const strippedExpression = stripParenExpression(expression);
|
|
21514
|
+
if (isNodeOfType(strippedExpression, "Identifier")) {
|
|
21515
|
+
if (strippedExpression.name === namespaceName && scopes.isGlobalReference(strippedExpression)) return true;
|
|
21516
|
+
const symbol = scopes.symbolFor(strippedExpression);
|
|
21517
|
+
if (!symbol?.initializer || symbol.kind !== "const" || visitedSymbolIds.has(symbol.id)) return false;
|
|
21518
|
+
visitedSymbolIds.add(symbol.id);
|
|
21519
|
+
return isProvenGlobalNamespaceReference(symbol.initializer, namespaceName, scopes, visitedSymbolIds);
|
|
21520
|
+
}
|
|
21521
|
+
return isNodeOfType(strippedExpression, "MemberExpression") && getStaticPropertyName(strippedExpression) === namespaceName && isProvenGlobalObjectReference(strippedExpression.object, scopes);
|
|
21522
|
+
};
|
|
21523
|
+
//#endregion
|
|
19999
21524
|
//#region src/plugin/rules/js-performance/js-min-max-loop.ts
|
|
20000
21525
|
const builtinMutationByProgram = /* @__PURE__ */ new WeakMap();
|
|
20001
21526
|
const RUNTIMELESS_SYMBOL_KINDS = new Set(["ts-interface", "ts-type-alias"]);
|
|
@@ -20052,26 +21577,6 @@ const isSafeFreshNumericArray = (arrayExpression) => {
|
|
|
20052
21577
|
}
|
|
20053
21578
|
return !(didFindPositiveZero && didFindNegativeZero);
|
|
20054
21579
|
};
|
|
20055
|
-
const isGlobalObjectReference = (expression, scopes, visitedSymbols = /* @__PURE__ */ new Set()) => {
|
|
20056
|
-
const strippedExpression = stripParenExpression(expression);
|
|
20057
|
-
if (!isNodeOfType(strippedExpression, "Identifier")) return false;
|
|
20058
|
-
if ((strippedExpression.name === "globalThis" || strippedExpression.name === "window" || strippedExpression.name === "self" || strippedExpression.name === "global") && scopes.isGlobalReference(strippedExpression)) return true;
|
|
20059
|
-
const symbol = scopes.symbolFor(strippedExpression);
|
|
20060
|
-
if (!symbol?.initializer || symbol.kind !== "const" || visitedSymbols.has(symbol.id)) return false;
|
|
20061
|
-
visitedSymbols.add(symbol.id);
|
|
20062
|
-
return isGlobalObjectReference(symbol.initializer, scopes, visitedSymbols);
|
|
20063
|
-
};
|
|
20064
|
-
const resolvesToGlobalNamespace = (expression, namespaceName, scopes, visitedSymbols = /* @__PURE__ */ new Set()) => {
|
|
20065
|
-
const strippedExpression = stripParenExpression(expression);
|
|
20066
|
-
if (isNodeOfType(strippedExpression, "Identifier")) {
|
|
20067
|
-
if (strippedExpression.name === namespaceName && scopes.isGlobalReference(strippedExpression)) return true;
|
|
20068
|
-
const symbol = scopes.symbolFor(strippedExpression);
|
|
20069
|
-
if (!symbol?.initializer || symbol.kind !== "const" || visitedSymbols.has(symbol.id)) return false;
|
|
20070
|
-
visitedSymbols.add(symbol.id);
|
|
20071
|
-
return resolvesToGlobalNamespace(symbol.initializer, namespaceName, scopes, visitedSymbols);
|
|
20072
|
-
}
|
|
20073
|
-
return isNodeOfType(strippedExpression, "MemberExpression") && getStaticPropertyName(strippedExpression) === namespaceName && isGlobalObjectReference(strippedExpression.object, scopes);
|
|
20074
|
-
};
|
|
20075
21580
|
const resolvesToGlobalMethod = (expression, namespaceName, methodNames, scopes, visitedSymbols = /* @__PURE__ */ new Set()) => {
|
|
20076
21581
|
const strippedExpression = stripParenExpression(expression);
|
|
20077
21582
|
if (isNodeOfType(strippedExpression, "Identifier")) {
|
|
@@ -20080,7 +21585,7 @@ const resolvesToGlobalMethod = (expression, namespaceName, methodNames, scopes,
|
|
|
20080
21585
|
visitedSymbols.add(symbol.id);
|
|
20081
21586
|
return resolvesToGlobalMethod(symbol.initializer, namespaceName, methodNames, scopes, visitedSymbols);
|
|
20082
21587
|
}
|
|
20083
|
-
return isNodeOfType(strippedExpression, "MemberExpression") && methodNames.has(getStaticPropertyName(strippedExpression) ?? "") &&
|
|
21588
|
+
return isNodeOfType(strippedExpression, "MemberExpression") && methodNames.has(getStaticPropertyName(strippedExpression) ?? "") && isProvenGlobalNamespaceReference(strippedExpression.object, namespaceName, scopes);
|
|
20084
21589
|
};
|
|
20085
21590
|
const resolvesToNativeArrayPrototype = (expression, scopes, visitedSymbols = /* @__PURE__ */ new Set()) => {
|
|
20086
21591
|
const strippedExpression = stripParenExpression(expression);
|
|
@@ -20092,7 +21597,7 @@ const resolvesToNativeArrayPrototype = (expression, scopes, visitedSymbols = /*
|
|
|
20092
21597
|
}
|
|
20093
21598
|
if (isNodeOfType(strippedExpression, "MemberExpression")) {
|
|
20094
21599
|
const propertyName = getStaticPropertyName(strippedExpression);
|
|
20095
|
-
if (propertyName === "prototype") return
|
|
21600
|
+
if (propertyName === "prototype") return isProvenGlobalNamespaceReference(strippedExpression.object, "Array", scopes);
|
|
20096
21601
|
return propertyName === "__proto__" && isNodeOfType(stripParenExpression(strippedExpression.object), "ArrayExpression");
|
|
20097
21602
|
}
|
|
20098
21603
|
if (!isNodeOfType(strippedExpression, "CallExpression")) return false;
|
|
@@ -20103,23 +21608,23 @@ const resolvesToNativeArrayPrototype = (expression, scopes, visitedSymbols = /*
|
|
|
20103
21608
|
const isGlobalNamespaceReplacementTarget = (target, namespaceName, scopes) => {
|
|
20104
21609
|
const strippedTarget = stripParenExpression(target);
|
|
20105
21610
|
if (isNodeOfType(strippedTarget, "Identifier")) return strippedTarget.name === namespaceName && scopes.isGlobalReference(strippedTarget);
|
|
20106
|
-
return isNodeOfType(strippedTarget, "MemberExpression") && getStaticPropertyName(strippedTarget) === namespaceName &&
|
|
21611
|
+
return isNodeOfType(strippedTarget, "MemberExpression") && getStaticPropertyName(strippedTarget) === namespaceName && isProvenGlobalObjectReference(strippedTarget.object, scopes);
|
|
20107
21612
|
};
|
|
20108
21613
|
const isUnsafeBuiltinMemberTarget = (target, targetFunction, scopes) => {
|
|
20109
21614
|
const strippedTarget = stripParenExpression(target);
|
|
20110
21615
|
if (!isNodeOfType(strippedTarget, "MemberExpression")) return false;
|
|
20111
21616
|
const propertyName = getStaticPropertyName(strippedTarget);
|
|
20112
21617
|
if (resolvesToNativeArrayPrototype(strippedTarget.object, scopes)) return propertyName === null || propertyName === "sort";
|
|
20113
|
-
if (
|
|
20114
|
-
return
|
|
21618
|
+
if (isProvenGlobalNamespaceReference(strippedTarget.object, "Math", scopes)) return propertyName === null || propertyName === targetFunction;
|
|
21619
|
+
return isProvenGlobalObjectReference(strippedTarget.object, scopes) && (propertyName === null || propertyName === "Math");
|
|
20115
21620
|
};
|
|
20116
21621
|
const isUnsafeBuiltinMutationApiCall = (callExpression, targetFunction, scopes) => {
|
|
20117
21622
|
const target = callExpression.arguments[0];
|
|
20118
21623
|
if (!target) return false;
|
|
20119
21624
|
let propertyName = null;
|
|
20120
21625
|
if (resolvesToNativeArrayPrototype(target, scopes)) propertyName = "sort";
|
|
20121
|
-
else if (
|
|
20122
|
-
else if (
|
|
21626
|
+
else if (isProvenGlobalNamespaceReference(target, "Math", scopes)) propertyName = targetFunction;
|
|
21627
|
+
else if (isProvenGlobalObjectReference(target, scopes)) propertyName = "Math";
|
|
20123
21628
|
if (!propertyName) return false;
|
|
20124
21629
|
const canObjectExpressionSetProperty = (properties) => {
|
|
20125
21630
|
if (!isNodeOfType(properties, "ObjectExpression")) return true;
|
|
@@ -20170,7 +21675,7 @@ const hasUnsafeMathBinding = (node, scopes) => {
|
|
|
20170
21675
|
let scope = scopes.scopeFor(node);
|
|
20171
21676
|
while (scope) {
|
|
20172
21677
|
const symbol = scope.symbolsByName.get("Math");
|
|
20173
|
-
if (symbol && !RUNTIMELESS_SYMBOL_KINDS.has(symbol.kind)) return !(symbol.kind === "const" && symbol.initializer &&
|
|
21678
|
+
if (symbol && !RUNTIMELESS_SYMBOL_KINDS.has(symbol.kind)) return !(symbol.kind === "const" && symbol.initializer && isProvenGlobalNamespaceReference(symbol.initializer, "Math", scopes));
|
|
20174
21679
|
scope = scope.parent;
|
|
20175
21680
|
}
|
|
20176
21681
|
return false;
|
|
@@ -25730,6 +27235,7 @@ const mediaHasCaption = defineRule({
|
|
|
25730
27235
|
create: (context) => {
|
|
25731
27236
|
const settings = resolveSettings$23(context.settings);
|
|
25732
27237
|
return { JSXOpeningElement(node) {
|
|
27238
|
+
if (isLocalTestScaffoldJsx(node, context)) return;
|
|
25733
27239
|
const tag = getElementType(node, context.settings);
|
|
25734
27240
|
if (!(settings.audio.has(tag) || settings.video.has(tag))) return;
|
|
25735
27241
|
if (evaluateMuted(hasJsxPropIgnoreCase(node.attributes, "muted")) === true) return;
|
|
@@ -25797,6 +27303,7 @@ const mouseEventsHaveKeyEvents = defineRule({
|
|
|
25797
27303
|
create: (context) => {
|
|
25798
27304
|
const settings = resolveSettings$22(context.settings);
|
|
25799
27305
|
return { JSXOpeningElement(node) {
|
|
27306
|
+
if (isLocalTestScaffoldJsx(node, context)) return;
|
|
25800
27307
|
const tag = getElementType(node, context.settings);
|
|
25801
27308
|
if (!HTML_TAGS.has(tag)) return;
|
|
25802
27309
|
for (const handler of settings.hoverInHandlers) {
|
|
@@ -25838,7 +27345,11 @@ const mouseEventsHaveKeyEvents = defineRule({
|
|
|
25838
27345
|
//#region src/plugin/utils/has-directive.ts
|
|
25839
27346
|
const hasDirective = (programNode, directive) => {
|
|
25840
27347
|
if (!isNodeOfType(programNode, "Program")) return false;
|
|
25841
|
-
|
|
27348
|
+
for (const statement of programNode.body) {
|
|
27349
|
+
if (!isNodeOfType(statement, "ExpressionStatement") || statement.directive === void 0) return false;
|
|
27350
|
+
if (statement.directive === directive) return true;
|
|
27351
|
+
}
|
|
27352
|
+
return false;
|
|
25842
27353
|
};
|
|
25843
27354
|
//#endregion
|
|
25844
27355
|
//#region src/plugin/rules/nextjs/nextjs-async-client-component.ts
|
|
@@ -28467,7 +29978,11 @@ const isProvenNativeReadMethod = (ref, methodName) => Boolean(ref.resolved?.defs
|
|
|
28467
29978
|
}));
|
|
28468
29979
|
//#endregion
|
|
28469
29980
|
//#region src/plugin/rules/state-and-effects/utils/effect/react.ts
|
|
28470
|
-
const
|
|
29981
|
+
const KNOWN_COMPONENT_WRAPPER_NAMES = new Set([
|
|
29982
|
+
"memo",
|
|
29983
|
+
"forwardRef",
|
|
29984
|
+
"observer"
|
|
29985
|
+
]);
|
|
28471
29986
|
const startsWithUppercase = (name) => Boolean(name && name.length > 0 && name[0] >= "A" && name[0] <= "Z");
|
|
28472
29987
|
const isReactFunctionalComponent = (node) => {
|
|
28473
29988
|
if (!node) return false;
|
|
@@ -28489,7 +30004,7 @@ const isReactFunctionalHOC = (analysis, node) => {
|
|
|
28489
30004
|
const isWrappedInline = () => {
|
|
28490
30005
|
if (!isNodeOfType(init, "CallExpression")) return false;
|
|
28491
30006
|
if (!isNodeOfType(init.callee, "Identifier")) return false;
|
|
28492
|
-
if (
|
|
30007
|
+
if (KNOWN_COMPONENT_WRAPPER_NAMES.has(init.callee.name)) return false;
|
|
28493
30008
|
const firstArg = init.arguments?.[0];
|
|
28494
30009
|
if (!firstArg) return false;
|
|
28495
30010
|
return isNodeOfType(firstArg, "ArrowFunctionExpression") || isNodeOfType(firstArg, "FunctionExpression");
|
|
@@ -28509,7 +30024,7 @@ const isReactFunctionalHOC = (analysis, node) => {
|
|
|
28509
30024
|
if (!args.includes(refId)) continue;
|
|
28510
30025
|
const callee = parent.callee;
|
|
28511
30026
|
const calleeName = isNodeOfType(callee, "Identifier") ? callee.name : isNodeOfType(callee, "CallExpression") && isNodeOfType(callee.callee, "Identifier") ? callee.callee.name : null;
|
|
28512
|
-
if (calleeName != null && !
|
|
30027
|
+
if (calleeName != null && !KNOWN_COMPONENT_WRAPPER_NAMES.has(calleeName)) return true;
|
|
28513
30028
|
}
|
|
28514
30029
|
return false;
|
|
28515
30030
|
};
|
|
@@ -31932,7 +33447,7 @@ const isReactDomCreatePortalCall = (node, scopes) => {
|
|
|
31932
33447
|
if (!isNodeOfType(callee, "MemberExpression") || callee.computed || !isNodeOfType(callee.object, "Identifier") || !isNodeOfType(callee.property, "Identifier") || callee.property.name !== "createPortal") return false;
|
|
31933
33448
|
const symbol = scopes.symbolFor(callee.object);
|
|
31934
33449
|
if (!symbol || symbol.kind !== "import") return false;
|
|
31935
|
-
return isDefaultImportFromModule(callee.object, callee.object.name, "react-dom") || isNamespaceImportFromModule(callee.object, callee.object.name, "react-dom");
|
|
33450
|
+
return isDefaultImportFromModule(callee.object, callee.object.name, "react-dom") || isNamespaceImportFromModule$1(callee.object, callee.object.name, "react-dom");
|
|
31936
33451
|
};
|
|
31937
33452
|
const containsRenderOutput$1 = (rootNode, scopes) => {
|
|
31938
33453
|
let hasRenderOutput = false;
|
|
@@ -32613,6 +34128,24 @@ const isBuiltinNamespaceCallee = (callee) => {
|
|
|
32613
34128
|
}
|
|
32614
34129
|
return false;
|
|
32615
34130
|
};
|
|
34131
|
+
const getReactUseCallbackSource = (reference, context) => {
|
|
34132
|
+
const identifier = reference.identifier;
|
|
34133
|
+
const symbol = resolveConstIdentifierAlias(identifier, context.scopes);
|
|
34134
|
+
if (!symbol || symbol.kind !== "const" || !symbol.initializer) return null;
|
|
34135
|
+
const initializer = stripParenExpression(symbol.initializer);
|
|
34136
|
+
if (isNodeOfType(initializer, "CallExpression") && isReactApiCall(initializer, "useCallback", context.scopes, {
|
|
34137
|
+
allowGlobalReactNamespace: true,
|
|
34138
|
+
resolveNamedAliases: true
|
|
34139
|
+
})) return initializer;
|
|
34140
|
+
return null;
|
|
34141
|
+
};
|
|
34142
|
+
const getDependencyStateRefs = (analysis, context, dependencyReference) => {
|
|
34143
|
+
const useCallbackCall = getReactUseCallbackSource(dependencyReference, context);
|
|
34144
|
+
if (!useCallbackCall) return getUpstreamRefs(analysis, dependencyReference).filter((reference) => isState(analysis, reference));
|
|
34145
|
+
const dependencyList = useCallbackCall.arguments?.[1];
|
|
34146
|
+
if (!dependencyList || !isNodeOfType(dependencyList, "ArrayExpression")) return getUpstreamRefs(analysis, dependencyReference).filter((reference) => isState(analysis, reference));
|
|
34147
|
+
return getDownstreamRefs(analysis, dependencyList).flatMap((reference) => getUpstreamRefs(analysis, reference)).filter((reference) => isState(analysis, reference));
|
|
34148
|
+
};
|
|
32616
34149
|
const isSimpleExpression$1 = (analysis, expression, effectFn, visitedDeclarators) => {
|
|
32617
34150
|
let isSimple = true;
|
|
32618
34151
|
walkAst(expression, (child) => {
|
|
@@ -32643,6 +34176,7 @@ const noChainStateUpdates = defineRule({
|
|
|
32643
34176
|
id: "no-chain-state-updates",
|
|
32644
34177
|
title: "State updates chained through effects",
|
|
32645
34178
|
severity: "warn",
|
|
34179
|
+
disabledWhen: ["react:18"],
|
|
32646
34180
|
tags: ["test-noise"],
|
|
32647
34181
|
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",
|
|
32648
34182
|
create: (context) => ({ CallExpression(node) {
|
|
@@ -32660,7 +34194,7 @@ const noChainStateUpdates = defineRule({
|
|
|
32660
34194
|
if (!effectFnRefs || !depsRefs) return;
|
|
32661
34195
|
const effectFn = getEffectFn(analysis, node);
|
|
32662
34196
|
if (!effectFn) return;
|
|
32663
|
-
const stateDeps = depsRefs.flatMap((
|
|
34197
|
+
const stateDeps = depsRefs.flatMap((reference) => getDependencyStateRefs(analysis, context, reference));
|
|
32664
34198
|
if (stateDeps.length === 0) return;
|
|
32665
34199
|
if (stateDeps.every((ref) => isExternallyDrivenState(analysis, ref))) return;
|
|
32666
34200
|
const stateDepDeclarators = new Set(stateDeps.map((ref) => getUseStateDeclarator(ref)).filter((declarator) => declarator !== null));
|
|
@@ -32919,16 +34453,7 @@ const isProvenIntrinsicJsxElement = (openingElement, scopes) => {
|
|
|
32919
34453
|
return isIntrinsicValue(openingElement.name);
|
|
32920
34454
|
};
|
|
32921
34455
|
//#endregion
|
|
32922
|
-
//#region src/plugin/
|
|
32923
|
-
const pathStartsWith$1 = (propertyPath, prefix) => prefix.every((propertyName, index) => propertyPath[index] === propertyName);
|
|
32924
|
-
const collectMemberExpression = (identifier) => {
|
|
32925
|
-
let expression = findTransparentExpressionRoot(identifier);
|
|
32926
|
-
while (expression.parent && isNodeOfType(expression.parent, "MemberExpression") && expression.parent.object === expression) {
|
|
32927
|
-
if (!getStaticPropertyName(expression.parent)) return null;
|
|
32928
|
-
expression = findTransparentExpressionRoot(expression.parent);
|
|
32929
|
-
}
|
|
32930
|
-
return expression;
|
|
32931
|
-
};
|
|
34456
|
+
//#region src/plugin/utils/is-inline-intrinsic-ref-callback.ts
|
|
32932
34457
|
const isInlineIntrinsicRefCallback = (functionNode, scopes) => {
|
|
32933
34458
|
const functionExpression = findTransparentExpressionRoot(functionNode);
|
|
32934
34459
|
if (!isFunctionLike$1(functionExpression) || functionExpression.async || functionExpression.generator) return false;
|
|
@@ -32939,6 +34464,17 @@ const isInlineIntrinsicRefCallback = (functionNode, scopes) => {
|
|
|
32939
34464
|
const openingElement = attribute.parent;
|
|
32940
34465
|
return Boolean(openingElement && isNodeOfType(openingElement, "JSXOpeningElement") && isProvenIntrinsicJsxElement(openingElement, scopes));
|
|
32941
34466
|
};
|
|
34467
|
+
//#endregion
|
|
34468
|
+
//#region src/plugin/rules/react-builtins/is-safe-create-ref-callback-current-write.ts
|
|
34469
|
+
const pathStartsWith$1 = (propertyPath, prefix) => prefix.every((propertyName, index) => propertyPath[index] === propertyName);
|
|
34470
|
+
const collectMemberExpression = (identifier) => {
|
|
34471
|
+
let expression = findTransparentExpressionRoot(identifier);
|
|
34472
|
+
while (expression.parent && isNodeOfType(expression.parent, "MemberExpression") && expression.parent.object === expression) {
|
|
34473
|
+
if (!getStaticPropertyName(expression.parent)) return null;
|
|
34474
|
+
expression = findTransparentExpressionRoot(expression.parent);
|
|
34475
|
+
}
|
|
34476
|
+
return expression;
|
|
34477
|
+
};
|
|
32942
34478
|
const isSafeCreateRefCallbackCurrentWrite = (referenceNode, accessedPropertyPath, targetPropertyPath, scopes) => {
|
|
32943
34479
|
if (accessedPropertyPath.length !== targetPropertyPath.length + 1 || !pathStartsWith$1(accessedPropertyPath, targetPropertyPath) || accessedPropertyPath[targetPropertyPath.length] !== "current") return false;
|
|
32944
34480
|
const memberExpression = collectMemberExpression(referenceNode);
|
|
@@ -33425,6 +34961,151 @@ const isCreateRefResultWriteOnly = (createRefCall, filename, scopes) => {
|
|
|
33425
34961
|
return ownedValue ? analyzeSymbolValuePath(ownedValue, state, 12) : analyzeValueUse(createRefCall, [], environment, state, 12);
|
|
33426
34962
|
};
|
|
33427
34963
|
//#endregion
|
|
34964
|
+
//#region src/plugin/utils/is-jsx-element-or-fragment.ts
|
|
34965
|
+
/**
|
|
34966
|
+
* Type-guard for the two single-node JSX output forms: `JSXElement`
|
|
34967
|
+
* (`<Foo />`) and `JSXFragment` (`<>…</>`). Canonical home for the
|
|
34968
|
+
* `isNodeOfType(x, "JSXElement") || isNodeOfType(x, "JSXFragment")` check
|
|
34969
|
+
* that many rules otherwise inline. Does NOT unwrap parens / TS wrappers —
|
|
34970
|
+
* callers that need the semantic expression should `stripParenExpression`
|
|
34971
|
+
* first.
|
|
34972
|
+
*/
|
|
34973
|
+
const isJsxElementOrFragment = (node) => Boolean(node && (isNodeOfType(node, "JSXElement") || isNodeOfType(node, "JSXFragment")));
|
|
34974
|
+
//#endregion
|
|
34975
|
+
//#region src/plugin/rules/react-builtins/is-proven-one-shot-testing-library-component.ts
|
|
34976
|
+
const REACT_TESTING_LIBRARY_MODULE_SOURCE = "@testing-library/react";
|
|
34977
|
+
const REACT_TESTING_LIBRARY_MODULE_SOURCES = new Set([REACT_TESTING_LIBRARY_MODULE_SOURCE]);
|
|
34978
|
+
const TEST_CALLBACK_NAMES = new Set(["it", "test"]);
|
|
34979
|
+
const TEST_RUNNER_MODULE_SOURCES = new Set(["@jest/globals", "vitest"]);
|
|
34980
|
+
const isNamedImportFromModule = (symbol, importedName, moduleSources) => {
|
|
34981
|
+
if (!symbol || symbol.kind !== "import" || !isNodeOfType(symbol.declarationNode, "ImportSpecifier") || getImportedName(symbol.declarationNode) !== importedName) return false;
|
|
34982
|
+
const importDeclaration = symbol.declarationNode.parent;
|
|
34983
|
+
return Boolean(importDeclaration && isNodeOfType(importDeclaration, "ImportDeclaration") && typeof importDeclaration.source.value === "string" && moduleSources.has(importDeclaration.source.value));
|
|
34984
|
+
};
|
|
34985
|
+
const isNamespaceImportFromModule = (symbol, moduleSource) => {
|
|
34986
|
+
if (!symbol || symbol.kind !== "import" || !isNodeOfType(symbol.declarationNode, "ImportNamespaceSpecifier")) return false;
|
|
34987
|
+
const importDeclaration = symbol.declarationNode.parent;
|
|
34988
|
+
return Boolean(importDeclaration && isNodeOfType(importDeclaration, "ImportDeclaration") && importDeclaration.source.value === moduleSource);
|
|
34989
|
+
};
|
|
34990
|
+
const isProvenTestCallback = (functionNode, scopes) => {
|
|
34991
|
+
const callExpression = functionNode.parent;
|
|
34992
|
+
if (!callExpression || !isNodeOfType(callExpression, "CallExpression") || callExpression.arguments[1] !== functionNode) return false;
|
|
34993
|
+
const callee = stripParenExpression(callExpression.callee);
|
|
34994
|
+
if (!isNodeOfType(callee, "Identifier")) return false;
|
|
34995
|
+
if (TEST_CALLBACK_NAMES.has(callee.name) && scopes.isGlobalReference(callee)) return true;
|
|
34996
|
+
const symbol = scopes.symbolFor(callee);
|
|
34997
|
+
if (!symbol || symbol.kind !== "import") return false;
|
|
34998
|
+
const importedName = getImportedName(symbol.declarationNode);
|
|
34999
|
+
return Boolean(importedName && TEST_CALLBACK_NAMES.has(importedName) && isNamedImportFromModule(symbol, importedName, TEST_RUNNER_MODULE_SOURCES));
|
|
35000
|
+
};
|
|
35001
|
+
const getDirectConstComponentSymbol = (functionNode, scopes) => {
|
|
35002
|
+
const declarator = functionNode.parent;
|
|
35003
|
+
if (!declarator || !isNodeOfType(declarator, "VariableDeclarator") || declarator.init !== functionNode || !isNodeOfType(declarator.id, "Identifier")) return null;
|
|
35004
|
+
const declaration = declarator.parent;
|
|
35005
|
+
if (!declaration || !isNodeOfType(declaration, "VariableDeclaration") || declaration.kind !== "const" || declaration.declarations.length !== 1) return null;
|
|
35006
|
+
const testCallback = findEnclosingFunction$1(declarator);
|
|
35007
|
+
if (!testCallback || !isFunctionLike$1(testCallback) || !isProvenTestCallback(testCallback, scopes) || !isNodeOfType(testCallback.body, "BlockStatement") || declaration.parent !== testCallback.body) return null;
|
|
35008
|
+
return scopes.symbolFor(declarator.id);
|
|
35009
|
+
};
|
|
35010
|
+
const isCreateRefDeclaration = (statement, scopes) => isNodeOfType(statement, "VariableDeclaration") && statement.kind === "const" && statement.declarations.length > 0 && statement.declarations.every((declarator) => {
|
|
35011
|
+
const initializer = declarator.init ? stripParenExpression(declarator.init) : null;
|
|
35012
|
+
return Boolean(isNodeOfType(declarator.id, "Identifier") && initializer && isNodeOfType(initializer, "CallExpression") && isReactApiCall(initializer, "createRef", scopes, {
|
|
35013
|
+
allowGlobalReactNamespace: true,
|
|
35014
|
+
allowUnboundBareCalls: true,
|
|
35015
|
+
resolveNamedAliases: true
|
|
35016
|
+
}));
|
|
35017
|
+
});
|
|
35018
|
+
const isSafeReturnedJsx = (returnStatement) => {
|
|
35019
|
+
if (!isNodeOfType(returnStatement, "ReturnStatement") || !returnStatement.argument) return false;
|
|
35020
|
+
const returnedExpression = stripParenExpression(returnStatement.argument);
|
|
35021
|
+
if (!isJsxElementOrFragment(returnedExpression)) return false;
|
|
35022
|
+
let isSafe = true;
|
|
35023
|
+
walkAst(returnedExpression, (node) => {
|
|
35024
|
+
if (isFunctionLike$1(node)) {
|
|
35025
|
+
isSafe = false;
|
|
35026
|
+
return false;
|
|
35027
|
+
}
|
|
35028
|
+
if (isNodeOfType(node, "AssignmentExpression") || isNodeOfType(node, "AwaitExpression") || isNodeOfType(node, "CallExpression") || isNodeOfType(node, "NewExpression") || isNodeOfType(node, "TaggedTemplateExpression") || isNodeOfType(node, "UpdateExpression") || isNodeOfType(node, "YieldExpression")) {
|
|
35029
|
+
isSafe = false;
|
|
35030
|
+
return false;
|
|
35031
|
+
}
|
|
35032
|
+
});
|
|
35033
|
+
return isSafe;
|
|
35034
|
+
};
|
|
35035
|
+
const hasProvenOneShotComponentBody = (functionNode, scopes) => {
|
|
35036
|
+
if (!isFunctionLike$1(functionNode) || !isNodeOfType(functionNode.body, "BlockStatement")) return false;
|
|
35037
|
+
if (!functionNode.params.every((parameter) => isNodeOfType(parameter, "Identifier"))) return false;
|
|
35038
|
+
const statements = functionNode.body.body;
|
|
35039
|
+
if (statements.length < 2) return false;
|
|
35040
|
+
const returnStatement = statements.at(-1);
|
|
35041
|
+
return Boolean(returnStatement && statements.slice(0, -1).every((statement) => isCreateRefDeclaration(statement, scopes)) && isSafeReturnedJsx(returnStatement));
|
|
35042
|
+
};
|
|
35043
|
+
const isProvenReactStrictModeElement = (jsxElement, scopes) => {
|
|
35044
|
+
const elementName = jsxElement.openingElement.name;
|
|
35045
|
+
if (isNodeOfType(elementName, "JSXIdentifier")) {
|
|
35046
|
+
const symbol = scopes.symbolFor(elementName);
|
|
35047
|
+
return Boolean(symbol && isImportedFromReact(symbol) && getImportedName(symbol.declarationNode) === "StrictMode");
|
|
35048
|
+
}
|
|
35049
|
+
return Boolean(isNodeOfType(elementName, "JSXMemberExpression") && isNodeOfType(elementName.object, "JSXIdentifier") && elementName.property.name === "StrictMode" && isReactNamespaceImport(elementName.object, scopes));
|
|
35050
|
+
};
|
|
35051
|
+
const isWhitespaceJsxChild = (node) => isNodeOfType(node, "JSXText") && node.value.trim().length === 0 || isNodeOfType(node, "JSXExpressionContainer") && isNodeOfType(node.expression, "JSXEmptyExpression");
|
|
35052
|
+
const getRootElementForComponentReference = (identifier, scopes) => {
|
|
35053
|
+
const openingElement = identifier.parent;
|
|
35054
|
+
if (!openingElement || !isNodeOfType(openingElement, "JSXOpeningElement") || openingElement.name !== identifier || !openingElement.selfClosing || openingElement.attributes.length !== 0) return null;
|
|
35055
|
+
const componentElement = openingElement.parent;
|
|
35056
|
+
if (!componentElement || !isNodeOfType(componentElement, "JSXElement")) return null;
|
|
35057
|
+
const strictModeElement = componentElement.parent;
|
|
35058
|
+
if (!strictModeElement || !isNodeOfType(strictModeElement, "JSXElement")) return componentElement;
|
|
35059
|
+
if (strictModeElement.openingElement.attributes.length !== 0 || !isProvenReactStrictModeElement(strictModeElement, scopes)) return null;
|
|
35060
|
+
const renderedChildren = strictModeElement.children.filter((child) => !isWhitespaceJsxChild(child));
|
|
35061
|
+
return renderedChildren.length === 1 && renderedChildren[0] === componentElement ? strictModeElement : null;
|
|
35062
|
+
};
|
|
35063
|
+
const isProvenTestingLibraryRenderCall = (callExpression, scopes) => {
|
|
35064
|
+
const callee = stripParenExpression(callExpression.callee);
|
|
35065
|
+
if (isNodeOfType(callee, "Identifier")) return isNamedImportFromModule(scopes.symbolFor(callee), "render", REACT_TESTING_LIBRARY_MODULE_SOURCES);
|
|
35066
|
+
return Boolean(isNodeOfType(callee, "MemberExpression") && getStaticPropertyName(callee) === "render" && isNodeOfType(callee.object, "Identifier") && isNamespaceImportFromModule(scopes.symbolFor(callee.object), REACT_TESTING_LIBRARY_MODULE_SOURCE));
|
|
35067
|
+
};
|
|
35068
|
+
const isSafeRenderResultBinding = (pattern) => {
|
|
35069
|
+
if (!isNodeOfType(pattern, "ObjectPattern")) return false;
|
|
35070
|
+
return pattern.properties.every((property) => {
|
|
35071
|
+
if (!isNodeOfType(property, "Property") || property.computed) return false;
|
|
35072
|
+
return isNodeOfType(property.value, "Identifier") && getStaticPropertyKeyName(property) !== "rerender";
|
|
35073
|
+
});
|
|
35074
|
+
};
|
|
35075
|
+
const isDirectSafeRenderStatement = (callExpression, testCallback) => {
|
|
35076
|
+
if (!isFunctionLike$1(testCallback) || !isNodeOfType(testCallback.body, "BlockStatement")) return false;
|
|
35077
|
+
const expression = findTransparentExpressionRoot(callExpression);
|
|
35078
|
+
const parent = expression.parent;
|
|
35079
|
+
if (!parent) return false;
|
|
35080
|
+
if (isNodeOfType(parent, "ExpressionStatement")) return parent.parent === testCallback.body;
|
|
35081
|
+
if (!isNodeOfType(parent, "VariableDeclarator") || parent.init !== expression || !isSafeRenderResultBinding(parent.id)) return false;
|
|
35082
|
+
const declaration = parent.parent;
|
|
35083
|
+
return Boolean(declaration && isNodeOfType(declaration, "VariableDeclaration") && declaration.declarations.length === 1 && declaration.parent === testCallback.body);
|
|
35084
|
+
};
|
|
35085
|
+
const getProvenIndependentRenderCall = (componentReference, scopes) => {
|
|
35086
|
+
const rootElement = getRootElementForComponentReference(componentReference, scopes);
|
|
35087
|
+
if (!rootElement) return null;
|
|
35088
|
+
const renderedArgument = findTransparentExpressionRoot(rootElement);
|
|
35089
|
+
const callExpression = renderedArgument.parent;
|
|
35090
|
+
if (!callExpression || !isNodeOfType(callExpression, "CallExpression") || callExpression.arguments.length !== 1 || callExpression.arguments[0] !== renderedArgument || !isProvenTestingLibraryRenderCall(callExpression, scopes)) return null;
|
|
35091
|
+
return callExpression;
|
|
35092
|
+
};
|
|
35093
|
+
const isProvenOneShotTestingLibraryComponent = (functionNode, filename, scopes) => {
|
|
35094
|
+
if (!filename || !isTestlikeFilename(filename) || !hasProvenOneShotComponentBody(functionNode, scopes)) return false;
|
|
35095
|
+
const componentSymbol = getDirectConstComponentSymbol(functionNode, scopes);
|
|
35096
|
+
if (!componentSymbol || componentSymbol.references.length === 0) return false;
|
|
35097
|
+
const testCallback = findEnclosingFunction$1(componentSymbol.bindingIdentifier);
|
|
35098
|
+
if (!testCallback) return false;
|
|
35099
|
+
const renderCalls = /* @__PURE__ */ new Set();
|
|
35100
|
+
for (const reference of componentSymbol.references) {
|
|
35101
|
+
if (reference.flag !== "read") return false;
|
|
35102
|
+
const renderCall = getProvenIndependentRenderCall(reference.identifier, scopes);
|
|
35103
|
+
if (!renderCall || findEnclosingFunction$1(renderCall) !== testCallback || !isDirectSafeRenderStatement(renderCall, testCallback)) return false;
|
|
35104
|
+
renderCalls.add(renderCall);
|
|
35105
|
+
}
|
|
35106
|
+
return renderCalls.size > 0;
|
|
35107
|
+
};
|
|
35108
|
+
//#endregion
|
|
33428
35109
|
//#region src/plugin/rules/react-builtins/no-create-ref-in-function-component.ts
|
|
33429
35110
|
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.";
|
|
33430
35111
|
const isUseMemoCallbackArgument = (functionNode, scopes) => {
|
|
@@ -33438,6 +35119,27 @@ const findEnclosingRenderFunction = (node, scopes) => {
|
|
|
33438
35119
|
while (enclosingFunction && isUseMemoCallbackArgument(enclosingFunction, scopes)) enclosingFunction = findEnclosingFunction$1(enclosingFunction);
|
|
33439
35120
|
return enclosingFunction;
|
|
33440
35121
|
};
|
|
35122
|
+
const isReactUseStateInitialState = (node, scopes) => {
|
|
35123
|
+
const initialState = findTransparentExpressionRoot(node);
|
|
35124
|
+
const stateCall = initialState.parent;
|
|
35125
|
+
return Boolean(stateCall && isNodeOfType(stateCall, "CallExpression") && stateCall.arguments[0] === initialState && isReactApiCall(stateCall, "useState", scopes, {
|
|
35126
|
+
allowGlobalReactNamespace: true,
|
|
35127
|
+
resolveNamedAliases: true
|
|
35128
|
+
}));
|
|
35129
|
+
};
|
|
35130
|
+
const hasDirectExportWrapper = (declarationNode) => {
|
|
35131
|
+
const parent = declarationNode.parent;
|
|
35132
|
+
if (isNodeOfType(parent, "ExportNamedDeclaration") || isNodeOfType(parent, "ExportDefaultDeclaration")) return true;
|
|
35133
|
+
return Boolean(isNodeOfType(declarationNode, "VariableDeclarator") && (isNodeOfType(parent?.parent, "ExportNamedDeclaration") || isNodeOfType(parent?.parent, "ExportDefaultDeclaration")));
|
|
35134
|
+
};
|
|
35135
|
+
const isFunctionExclusivelyUsedAsReactStateInitializer = (functionNode, scopes) => {
|
|
35136
|
+
if (isReactUseStateInitialState(functionNode, scopes)) return true;
|
|
35137
|
+
const bindingIdentifier = getFunctionBindingIdentifier$1(findTransparentExpressionRoot(functionNode));
|
|
35138
|
+
if (!bindingIdentifier) return false;
|
|
35139
|
+
const bindingSymbol = isNodeOfType(functionNode, "FunctionDeclaration") ? scopes.scopeFor(functionNode).symbolsByName.get(bindingIdentifier.name) : scopes.symbolFor(bindingIdentifier);
|
|
35140
|
+
if (!bindingSymbol || bindingSymbol.kind !== "const" && bindingSymbol.kind !== "function" || hasDirectExportWrapper(bindingSymbol.declarationNode) || bindingSymbol.references.length === 0) return false;
|
|
35141
|
+
return bindingSymbol.references.every((reference) => reference.flag === "read" && isReactUseStateInitialState(reference.identifier, scopes));
|
|
35142
|
+
};
|
|
33441
35143
|
const noCreateRefInFunctionComponent = defineRule({
|
|
33442
35144
|
id: "no-create-ref-in-function-component",
|
|
33443
35145
|
title: "createRef in function component",
|
|
@@ -33454,6 +35156,8 @@ const noCreateRefInFunctionComponent = defineRule({
|
|
|
33454
35156
|
const displayName = componentOrHookDisplayNameForFunction(enclosingFunction);
|
|
33455
35157
|
if (!displayName) return;
|
|
33456
35158
|
if (!(isReactHookName(displayName) || functionContainsReactRenderOutput(enclosingFunction, context.scopes, context.cfg))) return;
|
|
35159
|
+
if (isReactUseStateInitialState(node, context.scopes) || isFunctionExclusivelyUsedAsReactStateInitializer(enclosingFunction, context.scopes)) return;
|
|
35160
|
+
if (isProvenOneShotTestingLibraryComponent(enclosingFunction, context.filename, context.scopes)) return;
|
|
33457
35161
|
if (isCreateRefResultWriteOnly(node, context.filename, context.scopes)) return;
|
|
33458
35162
|
context.report({
|
|
33459
35163
|
node,
|
|
@@ -34723,7 +36427,6 @@ const isSetStateCallInLifecycle = (setStateCall, lifecycleNames, options = {}) =
|
|
|
34723
36427
|
//#region src/plugin/rules/react-builtins/no-did-mount-set-state.ts
|
|
34724
36428
|
const LIFECYCLE_NAMES$2 = new Set(["componentDidMount"]);
|
|
34725
36429
|
const MESSAGE$28 = "Your users see an extra render right after mount when you call `setState` in `componentDidMount`.";
|
|
34726
|
-
const getNodeStart = (node) => "start" in node && typeof node.start === "number" ? node.start : -1;
|
|
34727
36430
|
const getEnclosingLifecycleFunction = (setStateCall) => {
|
|
34728
36431
|
let ancestor = setStateCall.parent;
|
|
34729
36432
|
while (ancestor) {
|
|
@@ -34812,13 +36515,13 @@ const argumentDerivesFromPostMountSource = (setStateCall, lifecycleFunction) =>
|
|
|
34812
36515
|
};
|
|
34813
36516
|
const isAfterAwaitInAsyncLifecycle = (setStateCall, lifecycleFunction) => {
|
|
34814
36517
|
if (!isFunctionLike$1(lifecycleFunction) || lifecycleFunction.async !== true) return false;
|
|
34815
|
-
const callStart =
|
|
36518
|
+
const callStart = getNodeStartIndex(setStateCall);
|
|
34816
36519
|
if (callStart < 0) return false;
|
|
34817
36520
|
let didFindPrecedingAwait = false;
|
|
34818
36521
|
walkAst(lifecycleFunction, (descendant) => {
|
|
34819
36522
|
if (didFindPrecedingAwait) return false;
|
|
34820
36523
|
if (!isNodeOfType(descendant, "AwaitExpression")) return;
|
|
34821
|
-
const awaitStart =
|
|
36524
|
+
const awaitStart = getNodeStartIndex(descendant);
|
|
34822
36525
|
if (awaitStart >= 0 && awaitStart < callStart) {
|
|
34823
36526
|
didFindPrecedingAwait = true;
|
|
34824
36527
|
return false;
|
|
@@ -35882,17 +37585,178 @@ const visitSynchronousFunctionBodies = (analysisFunctions, visitor) => {
|
|
|
35882
37585
|
walkInsideStatementBlocks(analysisFunction.body, visitor);
|
|
35883
37586
|
}
|
|
35884
37587
|
};
|
|
35885
|
-
const
|
|
35886
|
-
const
|
|
37588
|
+
const readStaticEffectValue = (expression, scopes, stateSymbolId, stateValue, visitedSymbolIds = /* @__PURE__ */ new Set()) => {
|
|
37589
|
+
const unwrappedExpression = stripParenExpression(expression);
|
|
37590
|
+
if (isNodeOfType(unwrappedExpression, "Literal")) {
|
|
37591
|
+
const literalValue = unwrappedExpression.value;
|
|
37592
|
+
if (literalValue === null || typeof literalValue === "boolean" || typeof literalValue === "number" || typeof literalValue === "string") return { value: literalValue };
|
|
37593
|
+
return null;
|
|
37594
|
+
}
|
|
37595
|
+
if (isNodeOfType(unwrappedExpression, "Identifier")) {
|
|
37596
|
+
if (scopes.symbolFor(unwrappedExpression)?.id === stateSymbolId) return stateValue;
|
|
37597
|
+
if (unwrappedExpression.name === "undefined" && scopes.isGlobalReference(unwrappedExpression)) return { value: void 0 };
|
|
37598
|
+
const immutableSymbol = scopes.symbolFor(unwrappedExpression);
|
|
37599
|
+
if (immutableSymbol?.kind !== "const" || !immutableSymbol.initializer || !isNodeOfType(immutableSymbol.declarationNode, "VariableDeclarator") || immutableSymbol.declarationNode.id !== immutableSymbol.bindingIdentifier || immutableSymbol.declarationNode.init !== immutableSymbol.initializer || immutableSymbol.references.some((reference) => reference.flag !== "read") || visitedSymbolIds.has(immutableSymbol.id)) return null;
|
|
37600
|
+
return readStaticEffectValue(immutableSymbol.initializer, scopes, stateSymbolId, stateValue, new Set(visitedSymbolIds).add(immutableSymbol.id));
|
|
37601
|
+
}
|
|
37602
|
+
if (isNodeOfType(unwrappedExpression, "UnaryExpression")) {
|
|
37603
|
+
if (unwrappedExpression.operator === "void") return { value: void 0 };
|
|
37604
|
+
if (unwrappedExpression.operator !== "!") return null;
|
|
37605
|
+
const argumentValue = readStaticEffectValue(unwrappedExpression.argument, scopes, stateSymbolId, stateValue, visitedSymbolIds);
|
|
37606
|
+
return argumentValue ? { value: !argumentValue.value } : null;
|
|
37607
|
+
}
|
|
37608
|
+
if (isNodeOfType(unwrappedExpression, "CallExpression")) {
|
|
37609
|
+
if (isNodeOfType(unwrappedExpression.callee, "Identifier") && unwrappedExpression.callee.name === "Boolean" && scopes.isGlobalReference(unwrappedExpression.callee) && unwrappedExpression.arguments.length === 1 && unwrappedExpression.arguments[0] && !isNodeOfType(unwrappedExpression.arguments[0], "SpreadElement")) {
|
|
37610
|
+
const argumentValue = readStaticEffectValue(unwrappedExpression.arguments[0], scopes, stateSymbolId, stateValue, visitedSymbolIds);
|
|
37611
|
+
return argumentValue ? { value: Boolean(argumentValue.value) } : null;
|
|
37612
|
+
}
|
|
37613
|
+
return null;
|
|
37614
|
+
}
|
|
37615
|
+
if (isNodeOfType(unwrappedExpression, "LogicalExpression")) {
|
|
37616
|
+
const leftValue = readStaticEffectValue(unwrappedExpression.left, scopes, stateSymbolId, stateValue, visitedSymbolIds);
|
|
37617
|
+
if (!leftValue) return null;
|
|
37618
|
+
if (unwrappedExpression.operator === "&&" && !leftValue.value) return leftValue;
|
|
37619
|
+
if (unwrappedExpression.operator === "||" && leftValue.value) return leftValue;
|
|
37620
|
+
if (unwrappedExpression.operator === "??" && leftValue.value !== null && leftValue.value !== void 0) return leftValue;
|
|
37621
|
+
return readStaticEffectValue(unwrappedExpression.right, scopes, stateSymbolId, stateValue, visitedSymbolIds);
|
|
37622
|
+
}
|
|
37623
|
+
if (isNodeOfType(unwrappedExpression, "ConditionalExpression")) {
|
|
37624
|
+
const testValue = readStaticEffectValue(unwrappedExpression.test, scopes, stateSymbolId, stateValue, visitedSymbolIds);
|
|
37625
|
+
if (!testValue) return null;
|
|
37626
|
+
return readStaticEffectValue(testValue.value ? unwrappedExpression.consequent : unwrappedExpression.alternate, scopes, stateSymbolId, stateValue, visitedSymbolIds);
|
|
37627
|
+
}
|
|
37628
|
+
if (isNodeOfType(unwrappedExpression, "MemberExpression") && unwrappedExpression.optional) {
|
|
37629
|
+
const objectValue = readStaticEffectValue(unwrappedExpression.object, scopes, stateSymbolId, stateValue, visitedSymbolIds);
|
|
37630
|
+
if (objectValue?.value === null || objectValue?.value === void 0) return { value: void 0 };
|
|
37631
|
+
return null;
|
|
37632
|
+
}
|
|
37633
|
+
if (isNodeOfType(unwrappedExpression, "BinaryExpression")) {
|
|
37634
|
+
const leftValue = readStaticEffectValue(unwrappedExpression.left, scopes, stateSymbolId, stateValue, visitedSymbolIds);
|
|
37635
|
+
const rightValue = readStaticEffectValue(unwrappedExpression.right, scopes, stateSymbolId, stateValue, visitedSymbolIds);
|
|
37636
|
+
if (!leftValue || !rightValue) return null;
|
|
37637
|
+
if (unwrappedExpression.operator === "===" || unwrappedExpression.operator === "!==") {
|
|
37638
|
+
const areEqual = leftValue.value === rightValue.value;
|
|
37639
|
+
return { value: unwrappedExpression.operator === "===" ? areEqual : !areEqual };
|
|
37640
|
+
}
|
|
37641
|
+
if (unwrappedExpression.operator === "==" || unwrappedExpression.operator === "!=") {
|
|
37642
|
+
const isLeftNullish = leftValue.value === null || leftValue.value === void 0;
|
|
37643
|
+
const isRightNullish = rightValue.value === null || rightValue.value === void 0;
|
|
37644
|
+
if (!isLeftNullish && !isRightNullish && typeof leftValue.value !== typeof rightValue.value) return null;
|
|
37645
|
+
const areEqual = isLeftNullish || isRightNullish ? isLeftNullish && isRightNullish : leftValue.value === rightValue.value;
|
|
37646
|
+
return { value: unwrappedExpression.operator === "==" ? areEqual : !areEqual };
|
|
37647
|
+
}
|
|
37648
|
+
}
|
|
37649
|
+
return null;
|
|
37650
|
+
};
|
|
37651
|
+
const readStaticUpdaterReturnValue = (updater, scopes) => {
|
|
37652
|
+
if (!isFunctionLike$1(updater) || updater.async || updater.generator) return null;
|
|
37653
|
+
if (!isNodeOfType(updater.body, "BlockStatement")) return readStaticEffectValue(updater.body, scopes, null, null);
|
|
37654
|
+
if (updater.body.body.length === 0) return { value: void 0 };
|
|
37655
|
+
if (updater.body.body.length !== 1) return null;
|
|
37656
|
+
const returnStatement = updater.body.body[0];
|
|
37657
|
+
if (!isNodeOfType(returnStatement, "ReturnStatement")) return null;
|
|
37658
|
+
if (!returnStatement.argument) return { value: void 0 };
|
|
37659
|
+
return readStaticEffectValue(returnStatement.argument, scopes, null, null);
|
|
37660
|
+
};
|
|
37661
|
+
const readStaticSetterValue = (setterCall, scopes) => {
|
|
37662
|
+
const argument = setterCall.arguments[0];
|
|
37663
|
+
if (!argument) return { value: void 0 };
|
|
37664
|
+
if (isNodeOfType(argument, "SpreadElement")) return null;
|
|
37665
|
+
const updater = resolveExactLocalFunction(argument, scopes);
|
|
37666
|
+
if (updater) return readStaticUpdaterReturnValue(updater, scopes);
|
|
37667
|
+
return readStaticEffectValue(argument, scopes, null, null);
|
|
37668
|
+
};
|
|
37669
|
+
const collectStateWritesInEffect = (analysisFunctions, setterToStateName, scopes) => {
|
|
37670
|
+
const stateWrites = /* @__PURE__ */ new Map();
|
|
35887
37671
|
visitSynchronousFunctionBodies(analysisFunctions, (child) => {
|
|
35888
37672
|
if (!isNodeOfType(child, "CallExpression")) return;
|
|
35889
37673
|
if (!isNodeOfType(child.callee, "Identifier")) return;
|
|
35890
37674
|
const stateName = setterToStateName.get(child.callee.name);
|
|
35891
|
-
if (stateName)
|
|
37675
|
+
if (!stateName) return;
|
|
37676
|
+
const writeInfo = stateWrites.get(stateName) ?? {
|
|
37677
|
+
values: /* @__PURE__ */ new Set(),
|
|
37678
|
+
hasUnknownValue: false
|
|
37679
|
+
};
|
|
37680
|
+
const staticValue = readStaticSetterValue(child, scopes);
|
|
37681
|
+
if (staticValue) writeInfo.values.add(staticValue.value);
|
|
37682
|
+
else writeInfo.hasUnknownValue = true;
|
|
37683
|
+
stateWrites.set(stateName, writeInfo);
|
|
35892
37684
|
});
|
|
35893
|
-
return
|
|
37685
|
+
return stateWrites;
|
|
37686
|
+
};
|
|
37687
|
+
const isGlobalBooleanCall = (node, scopes) => {
|
|
37688
|
+
return isNodeOfType(node, "CallExpression") && isNodeOfType(node.callee, "Identifier") && node.callee.name === "Boolean" && scopes.isGlobalReference(node.callee);
|
|
37689
|
+
};
|
|
37690
|
+
const isWorkNodeReachableForStateValue = (workNode, stateSymbolId, stateValue, scopes) => {
|
|
37691
|
+
let currentNode = workNode;
|
|
37692
|
+
while (currentNode.parent) {
|
|
37693
|
+
const parentNode = currentNode.parent;
|
|
37694
|
+
if (isFunctionLike$1(parentNode)) break;
|
|
37695
|
+
if (isNodeOfType(parentNode, "IfStatement")) {
|
|
37696
|
+
const testValue = readStaticEffectValue(parentNode.test, scopes, stateSymbolId, stateValue);
|
|
37697
|
+
if (testValue) {
|
|
37698
|
+
if (currentNode === parentNode.consequent && !testValue.value) return false;
|
|
37699
|
+
if (currentNode === parentNode.alternate && testValue.value) return false;
|
|
37700
|
+
}
|
|
37701
|
+
}
|
|
37702
|
+
if (isNodeOfType(parentNode, "ConditionalExpression")) {
|
|
37703
|
+
const testValue = readStaticEffectValue(parentNode.test, scopes, stateSymbolId, stateValue);
|
|
37704
|
+
if (testValue) {
|
|
37705
|
+
if (currentNode === parentNode.consequent && !testValue.value) return false;
|
|
37706
|
+
if (currentNode === parentNode.alternate && testValue.value) return false;
|
|
37707
|
+
}
|
|
37708
|
+
}
|
|
37709
|
+
if (isNodeOfType(parentNode, "LogicalExpression") && currentNode === parentNode.right) {
|
|
37710
|
+
const leftValue = readStaticEffectValue(parentNode.left, scopes, stateSymbolId, stateValue);
|
|
37711
|
+
if (leftValue) {
|
|
37712
|
+
if (parentNode.operator === "&&" && !leftValue.value) return false;
|
|
37713
|
+
if (parentNode.operator === "||" && leftValue.value) return false;
|
|
37714
|
+
if (parentNode.operator === "??" && leftValue.value !== null && leftValue.value !== void 0) return false;
|
|
37715
|
+
}
|
|
37716
|
+
}
|
|
37717
|
+
if (isNodeOfType(parentNode, "BlockStatement")) {
|
|
37718
|
+
const statementIndex = parentNode.body.findIndex((statement) => statement === currentNode);
|
|
37719
|
+
if (statementIndex >= 0) for (let index = 0; index < statementIndex; index += 1) {
|
|
37720
|
+
const earlierStatement = parentNode.body[index];
|
|
37721
|
+
if (!isNodeOfType(earlierStatement, "IfStatement") || earlierStatement.alternate || !statementAlwaysExits(earlierStatement.consequent)) continue;
|
|
37722
|
+
if (readStaticEffectValue(earlierStatement.test, scopes, stateSymbolId, stateValue)?.value) return false;
|
|
37723
|
+
}
|
|
37724
|
+
}
|
|
37725
|
+
currentNode = parentNode;
|
|
37726
|
+
}
|
|
37727
|
+
return true;
|
|
37728
|
+
};
|
|
37729
|
+
const isReaderWorkNode = (node, analysisFunctions, scopes) => {
|
|
37730
|
+
if (isNodeOfType(node, "CallExpression")) {
|
|
37731
|
+
if (isGlobalBooleanCall(node, scopes)) return false;
|
|
37732
|
+
const invokedFunction = resolveExactLocalFunction(node.callee, scopes);
|
|
37733
|
+
return !invokedFunction || !analysisFunctions.has(invokedFunction);
|
|
37734
|
+
}
|
|
37735
|
+
return isNodeOfType(node, "AssignmentExpression") || isNodeOfType(node, "UpdateExpression") || isNodeOfType(node, "NewExpression") || isNodeOfType(node, "TaggedTemplateExpression") || isNodeOfType(node, "ThrowStatement") || isNodeOfType(node, "UnaryExpression") && node.operator === "delete";
|
|
37736
|
+
};
|
|
37737
|
+
const canStateWriteReachReaderWork = (writeInfo, readerEffect, stateSymbolId, scopes) => {
|
|
37738
|
+
if (writeInfo.hasUnknownValue || stateSymbolId === null) return true;
|
|
37739
|
+
for (const writtenValue of writeInfo.values) {
|
|
37740
|
+
const stateValue = { value: writtenValue };
|
|
37741
|
+
let didFindReachableWork = false;
|
|
37742
|
+
visitSynchronousFunctionBodies(readerEffect.analysisFunctions, (child) => {
|
|
37743
|
+
if (didFindReachableWork || !isReaderWorkNode(child, readerEffect.analysisFunctions, scopes)) return;
|
|
37744
|
+
if (isWorkNodeReachableForStateValue(child, stateSymbolId, stateValue, scopes)) didFindReachableWork = true;
|
|
37745
|
+
});
|
|
37746
|
+
if (didFindReachableWork) return true;
|
|
37747
|
+
}
|
|
37748
|
+
return false;
|
|
35894
37749
|
};
|
|
35895
37750
|
const EMPTY_CLEANUP_NAME_SET = /* @__PURE__ */ new Set();
|
|
37751
|
+
const NON_CONTAMINATING_MAP_METHOD_NAMES = new Set([
|
|
37752
|
+
"clear",
|
|
37753
|
+
"delete",
|
|
37754
|
+
"entries",
|
|
37755
|
+
"get",
|
|
37756
|
+
"has",
|
|
37757
|
+
"keys",
|
|
37758
|
+
"values"
|
|
37759
|
+
]);
|
|
35896
37760
|
const isFunctionShapedReturn = (returnedValue, setterToStateName, isExplicitReturnStatement) => {
|
|
35897
37761
|
if (isNodeOfType(returnedValue, "ArrowFunctionExpression") || isNodeOfType(returnedValue, "FunctionExpression")) return true;
|
|
35898
37762
|
if (isNodeOfType(returnedValue, "CallExpression")) {
|
|
@@ -35943,27 +37807,135 @@ const callsOpaqueExternalSetter = (analysisFunctions, setterToStateName) => {
|
|
|
35943
37807
|
});
|
|
35944
37808
|
return didFindOpaqueSetterCall;
|
|
35945
37809
|
};
|
|
37810
|
+
const isReactRefCall = (expression, scopes) => isNodeOfType(expression, "CallExpression") && (isReactApiCall(expression, "useRef", scopes, {
|
|
37811
|
+
allowGlobalReactNamespace: true,
|
|
37812
|
+
allowUnboundBareCalls: true,
|
|
37813
|
+
resolveNamedAliases: true
|
|
37814
|
+
}) || isReactApiCall(expression, "createRef", scopes, {
|
|
37815
|
+
allowGlobalReactNamespace: true,
|
|
37816
|
+
allowUnboundBareCalls: true,
|
|
37817
|
+
resolveNamedAliases: true
|
|
37818
|
+
}));
|
|
37819
|
+
const getDirectReactRefSymbol = (rawExpression, scopes) => {
|
|
37820
|
+
const expression = stripParenExpression(rawExpression);
|
|
37821
|
+
if (!isNodeOfType(expression, "Identifier")) return null;
|
|
37822
|
+
const symbol = scopes.symbolFor(expression);
|
|
37823
|
+
if (!symbol) return null;
|
|
37824
|
+
const initializer = getDirectUnreassignedInitializer(symbol);
|
|
37825
|
+
return initializer && isReactRefCall(stripParenExpression(initializer), scopes) ? symbol : null;
|
|
37826
|
+
};
|
|
37827
|
+
const isReactNativeJsxElement = (openingElement, scopes) => {
|
|
37828
|
+
if (!isNodeOfType(openingElement.name, "JSXIdentifier")) return false;
|
|
37829
|
+
const symbol = scopes.symbolFor(openingElement.name);
|
|
37830
|
+
const importDeclaration = symbol?.declarationNode.parent;
|
|
37831
|
+
return Boolean(symbol?.kind === "import" && importDeclaration && isNodeOfType(importDeclaration, "ImportDeclaration") && importDeclaration.source.value === "react-native");
|
|
37832
|
+
};
|
|
37833
|
+
const isDirectHostJsxRef = (symbol, scopes) => {
|
|
37834
|
+
let hostRefCount = 0;
|
|
37835
|
+
for (const reference of symbol.references) {
|
|
37836
|
+
const expression = findTransparentExpressionRoot(reference.identifier);
|
|
37837
|
+
const container = expression.parent;
|
|
37838
|
+
if (isNodeOfType(container, "MemberExpression") && container.object === expression && getStaticPropertyName(container) === "current") continue;
|
|
37839
|
+
if (!container || !isNodeOfType(container, "JSXExpressionContainer") || container.expression !== expression) return false;
|
|
37840
|
+
const attribute = container.parent;
|
|
37841
|
+
if (!attribute || !isNodeOfType(attribute, "JSXAttribute") || getJsxAttributeName(attribute.name) !== "ref") return false;
|
|
37842
|
+
const openingElement = attribute.parent;
|
|
37843
|
+
if (!openingElement || !isNodeOfType(openingElement, "JSXOpeningElement") || !isProvenIntrinsicJsxElement(openingElement, scopes) && !isReactNativeJsxElement(openingElement, scopes)) return false;
|
|
37844
|
+
hostRefCount += 1;
|
|
37845
|
+
}
|
|
37846
|
+
return hostRefCount > 0;
|
|
37847
|
+
};
|
|
37848
|
+
const isIntrinsicRefCallbackParameter = (expression, scopes) => {
|
|
37849
|
+
const identifier = stripParenExpression(expression);
|
|
37850
|
+
if (!isNodeOfType(identifier, "Identifier")) return false;
|
|
37851
|
+
const callback = findEnclosingFunction$1(identifier);
|
|
37852
|
+
if (!callback || !isFunctionLike$1(callback) || !isInlineIntrinsicRefCallback(callback, scopes)) return false;
|
|
37853
|
+
const rawFirstParameter = callback.params?.[0];
|
|
37854
|
+
const firstParameter = isNodeOfType(rawFirstParameter, "AssignmentPattern") ? rawFirstParameter.left : rawFirstParameter;
|
|
37855
|
+
const symbol = scopes.symbolFor(identifier);
|
|
37856
|
+
return Boolean(firstParameter && symbol?.bindingIdentifier === firstParameter);
|
|
37857
|
+
};
|
|
37858
|
+
const getDirectReactRefCall = (symbol, scopes) => {
|
|
37859
|
+
const initializer = getDirectUnreassignedInitializer(symbol);
|
|
37860
|
+
if (!initializer) return null;
|
|
37861
|
+
const expression = stripParenExpression(initializer);
|
|
37862
|
+
return isNodeOfType(expression, "CallExpression") && isReactRefCall(expression, scopes) ? expression : null;
|
|
37863
|
+
};
|
|
37864
|
+
const storesOnlyIntrinsicRefCallbackValues = (symbol, scopes) => {
|
|
37865
|
+
const initialValue = getDirectReactRefCall(symbol, scopes)?.arguments?.[0];
|
|
37866
|
+
if (!initialValue || !isNodeOfType(initialValue, "NewExpression") || !isNodeOfType(initialValue.callee, "Identifier") || initialValue.callee.name !== "Map" || !scopes.isGlobalReference(initialValue.callee) || initialValue.arguments.length !== 0) return false;
|
|
37867
|
+
let intrinsicValueWriteCount = 0;
|
|
37868
|
+
for (const reference of symbol.references) {
|
|
37869
|
+
const identifier = findTransparentExpressionRoot(reference.identifier);
|
|
37870
|
+
const currentMember = identifier.parent;
|
|
37871
|
+
if (!isNodeOfType(currentMember, "MemberExpression") || currentMember.object !== identifier || getStaticPropertyName(currentMember) !== "current") return false;
|
|
37872
|
+
const currentExpression = findTransparentExpressionRoot(currentMember);
|
|
37873
|
+
const methodMember = currentExpression.parent;
|
|
37874
|
+
if (!isNodeOfType(methodMember, "MemberExpression") || methodMember.object !== currentExpression) return false;
|
|
37875
|
+
const methodName = getStaticPropertyName(methodMember);
|
|
37876
|
+
if (methodName === "size") continue;
|
|
37877
|
+
const call = methodMember.parent;
|
|
37878
|
+
if (!isNodeOfType(call, "CallExpression") || call.callee !== methodMember) return false;
|
|
37879
|
+
if (methodName && NON_CONTAMINATING_MAP_METHOD_NAMES.has(methodName)) continue;
|
|
37880
|
+
if (methodName !== "set") return false;
|
|
37881
|
+
const storedValue = call.arguments[1];
|
|
37882
|
+
if (!storedValue || isNodeOfType(storedValue, "SpreadElement") || !isIntrinsicRefCallbackParameter(storedValue, scopes)) return false;
|
|
37883
|
+
intrinsicValueWriteCount += 1;
|
|
37884
|
+
}
|
|
37885
|
+
return intrinsicValueWriteCount > 0;
|
|
37886
|
+
};
|
|
37887
|
+
const isDerivedFromProvenDomRefCurrent = (rawExpression, scopes, didReadCollectionValue = false, visitedSymbolIds = /* @__PURE__ */ new Set()) => {
|
|
37888
|
+
const expression = stripParenExpression(rawExpression);
|
|
37889
|
+
if (isNodeOfType(expression, "Identifier")) {
|
|
37890
|
+
const symbol = scopes.symbolFor(expression);
|
|
37891
|
+
if (!symbol || visitedSymbolIds.has(symbol.id)) return false;
|
|
37892
|
+
const initializer = getDirectUnreassignedInitializer(symbol);
|
|
37893
|
+
if (!initializer) return false;
|
|
37894
|
+
visitedSymbolIds.add(symbol.id);
|
|
37895
|
+
return isDerivedFromProvenDomRefCurrent(initializer, scopes, didReadCollectionValue, visitedSymbolIds);
|
|
37896
|
+
}
|
|
37897
|
+
if (isNodeOfType(expression, "MemberExpression")) {
|
|
37898
|
+
if (getStaticPropertyName(expression) === "current") {
|
|
37899
|
+
const symbol = getDirectReactRefSymbol(expression.object, scopes);
|
|
37900
|
+
return Boolean(symbol && (isDirectHostJsxRef(symbol, scopes) || didReadCollectionValue && storesOnlyIntrinsicRefCallbackValues(symbol, scopes)));
|
|
37901
|
+
}
|
|
37902
|
+
return isDerivedFromProvenDomRefCurrent(expression.object, scopes, didReadCollectionValue, visitedSymbolIds);
|
|
37903
|
+
}
|
|
37904
|
+
if (!isNodeOfType(expression, "CallExpression")) return false;
|
|
37905
|
+
const callee = stripParenExpression(expression.callee);
|
|
37906
|
+
if (!isNodeOfType(callee, "MemberExpression")) return false;
|
|
37907
|
+
return isDerivedFromProvenDomRefCurrent(callee.object, scopes, didReadCollectionValue || getStaticPropertyName(callee) === "get", visitedSymbolIds);
|
|
37908
|
+
};
|
|
37909
|
+
const isCommittedDomSyncNode = (node, scopes) => {
|
|
37910
|
+
if (!isNodeOfType(node, "CallExpression")) return false;
|
|
37911
|
+
const callee = stripParenExpression(node.callee);
|
|
37912
|
+
if (!isNodeOfType(callee, "MemberExpression")) return false;
|
|
37913
|
+
const propertyName = getStaticPropertyName(callee);
|
|
37914
|
+
if (propertyName === null || !EXTERNAL_SYNC_DOM_MEMBER_METHOD_NAMES.has(propertyName)) return false;
|
|
37915
|
+
return isDerivedFromProvenDomRefCurrent(callee.object, scopes) || isProvenBrowserApiReceiver(callee.object, "dom-event-target", scopes);
|
|
37916
|
+
};
|
|
35946
37917
|
const isExternalSyncNode = (node) => {
|
|
35947
37918
|
if (isNodeOfType(node, "NewExpression")) return isNodeOfType(node.callee, "Identifier") && EXTERNAL_SYNC_OBSERVER_CONSTRUCTORS.has(node.callee.name);
|
|
35948
37919
|
if (isNodeOfType(node, "AssignmentExpression")) return isNodeOfType(node.left, "MemberExpression") && isNodeOfType(node.left.property, "Identifier") && node.left.property.name === "current";
|
|
35949
37920
|
if (!isNodeOfType(node, "CallExpression")) return false;
|
|
35950
37921
|
if (isNodeOfType(node.callee, "Identifier")) return EXTERNAL_SYNC_DIRECT_CALLEE_NAMES.has(node.callee.name);
|
|
35951
|
-
if (!isNodeOfType(node.callee, "MemberExpression")
|
|
35952
|
-
const propertyName = node.callee
|
|
37922
|
+
if (!isNodeOfType(node.callee, "MemberExpression")) return false;
|
|
37923
|
+
const propertyName = getStaticPropertyName(node.callee);
|
|
37924
|
+
if (propertyName === null) return false;
|
|
35953
37925
|
if (EXTERNAL_SYNC_MEMBER_METHOD_NAMES.has(propertyName)) return true;
|
|
35954
37926
|
if (isBrowserStorageReceiver(node.callee.object)) return true;
|
|
35955
37927
|
if (!EXTERNAL_SYNC_AMBIGUOUS_HTTP_METHOD_NAMES.has(propertyName)) return false;
|
|
35956
37928
|
const receiverRootName = getRootIdentifierName(node.callee.object);
|
|
35957
37929
|
return receiverRootName !== null && EXTERNAL_SYNC_HTTP_CLIENT_RECEIVERS.has(receiverRootName);
|
|
35958
37930
|
};
|
|
35959
|
-
const isExternalSyncEffect = (effectCallback, analysisFunctions, setterToStateName) => {
|
|
37931
|
+
const isExternalSyncEffect = (effectCallback, analysisFunctions, setterToStateName, scopes, allowCommittedDomSync) => {
|
|
35960
37932
|
if (!isFunctionLike$1(effectCallback)) return false;
|
|
35961
37933
|
if (!isNodeOfType(effectCallback.body, "BlockStatement")) {
|
|
35962
37934
|
if (isFunctionShapedReturn(effectCallback.body, setterToStateName, false)) return true;
|
|
35963
37935
|
} else for (const statement of effectCallback.body.body ?? []) if (isNodeOfType(statement, "ReturnStatement") && statement.argument && isFunctionShapedReturn(statement.argument, setterToStateName, true)) return true;
|
|
35964
37936
|
let didFindExternalCall = false;
|
|
35965
37937
|
visitSynchronousFunctionBodies(analysisFunctions, (child) => {
|
|
35966
|
-
if (isExternalSyncNode(child)) didFindExternalCall = true;
|
|
37938
|
+
if (isExternalSyncNode(child) || allowCommittedDomSync && isCommittedDomSyncNode(child, scopes)) didFindExternalCall = true;
|
|
35967
37939
|
});
|
|
35968
37940
|
return didFindExternalCall;
|
|
35969
37941
|
};
|
|
@@ -35979,32 +37951,45 @@ const noEffectChain = defineRule({
|
|
|
35979
37951
|
const useStateBindings = collectUseStateBindings(componentBody);
|
|
35980
37952
|
if (useStateBindings.length === 0) return;
|
|
35981
37953
|
const setterToStateName = /* @__PURE__ */ new Map();
|
|
35982
|
-
|
|
37954
|
+
const stateSymbolIds = /* @__PURE__ */ new Map();
|
|
37955
|
+
for (const binding of useStateBindings) {
|
|
37956
|
+
setterToStateName.set(binding.setterName, binding.valueName);
|
|
37957
|
+
if (!isNodeOfType(binding.declarator.id, "ArrayPattern")) continue;
|
|
37958
|
+
const stateIdentifier = binding.declarator.id.elements[0];
|
|
37959
|
+
if (isNodeOfType(stateIdentifier, "Identifier")) {
|
|
37960
|
+
const stateSymbol = context.scopes.symbolFor(stateIdentifier);
|
|
37961
|
+
if (stateSymbol) stateSymbolIds.set(binding.valueName, stateSymbol.id);
|
|
37962
|
+
}
|
|
37963
|
+
}
|
|
35983
37964
|
const storageSetterNames = collectStorageHookSetterNames(componentBody);
|
|
35984
37965
|
const effectInfos = [];
|
|
35985
37966
|
for (const effectCall of findTopLevelEffectCalls(componentBody)) {
|
|
35986
37967
|
const callback = getEffectCallback(effectCall, context.scopes);
|
|
35987
37968
|
if (!callback || !isFunctionLike$1(callback) || callback.async) continue;
|
|
35988
37969
|
const analysisFunctions = collectSynchronouslyInvokedFunctions(callback, context.scopes);
|
|
35989
|
-
const
|
|
37970
|
+
const stateWrites = collectStateWritesInEffect(analysisFunctions, setterToStateName, context.scopes);
|
|
37971
|
+
const writtenStateNames = new Set(stateWrites.keys());
|
|
35990
37972
|
effectInfos.push({
|
|
35991
37973
|
node: effectCall,
|
|
35992
37974
|
depNames: collectDepIdentifierNames(effectCall),
|
|
35993
|
-
|
|
35994
|
-
|
|
37975
|
+
stateWrites,
|
|
37976
|
+
analysisFunctions,
|
|
37977
|
+
isExternalSync: isExternalSyncEffect(callback, analysisFunctions, setterToStateName, context.scopes, writtenStateNames.size === 0) || callsStorageHookSetter(analysisFunctions, storageSetterNames) || writtenStateNames.size === 0 && callsOpaqueExternalSetter(analysisFunctions, setterToStateName)
|
|
35995
37978
|
});
|
|
35996
37979
|
}
|
|
35997
37980
|
if (effectInfos.length < 2) return;
|
|
35998
37981
|
const reportedNodes = /* @__PURE__ */ new Set();
|
|
35999
37982
|
for (const writerEffect of effectInfos) {
|
|
36000
37983
|
if (writerEffect.isExternalSync) continue;
|
|
36001
|
-
if (writerEffect.
|
|
37984
|
+
if (writerEffect.stateWrites.size === 0) continue;
|
|
36002
37985
|
for (const readerEffect of effectInfos) {
|
|
36003
37986
|
if (readerEffect === writerEffect) continue;
|
|
36004
37987
|
if (readerEffect.isExternalSync) continue;
|
|
36005
37988
|
if (readerEffect.depNames.size === 0) continue;
|
|
36006
37989
|
let chainedStateName = null;
|
|
36007
|
-
for (const writtenName of writerEffect.
|
|
37990
|
+
for (const [writtenName, writeInfo] of writerEffect.stateWrites) {
|
|
37991
|
+
if (!readerEffect.depNames.has(writtenName)) continue;
|
|
37992
|
+
if (!canStateWriteReachReaderWork(writeInfo, readerEffect, stateSymbolIds.get(writtenName) ?? null, context.scopes)) continue;
|
|
36008
37993
|
chainedStateName = writtenName;
|
|
36009
37994
|
break;
|
|
36010
37995
|
}
|
|
@@ -39671,6 +41656,18 @@ const functionContainsProvenReactHookCall = (functionNode, scopes) => {
|
|
|
39671
41656
|
return containsReactHookCall;
|
|
39672
41657
|
};
|
|
39673
41658
|
//#endregion
|
|
41659
|
+
//#region src/plugin/utils/function-returns-only-null.ts
|
|
41660
|
+
const isNullExpression = (expression) => {
|
|
41661
|
+
const candidate = stripParenExpression(expression);
|
|
41662
|
+
return isNodeOfType(candidate, "Literal") && candidate.value === null;
|
|
41663
|
+
};
|
|
41664
|
+
const functionReturnsOnlyNull = (functionNode) => {
|
|
41665
|
+
if (!isFunctionLike$1(functionNode)) return false;
|
|
41666
|
+
if (!isNodeOfType(functionNode.body, "BlockStatement")) return isNullExpression(functionNode.body);
|
|
41667
|
+
const returnStatements = collectFunctionReturnStatements(functionNode);
|
|
41668
|
+
return returnStatements.length > 0 && returnStatements.every((returnStatement) => Boolean(returnStatement.argument && isNullExpression(returnStatement.argument)));
|
|
41669
|
+
};
|
|
41670
|
+
//#endregion
|
|
39674
41671
|
//#region src/plugin/utils/function-returns-props-children.ts
|
|
39675
41672
|
const functionReturnsPropsChildren = (functionNode, scopes, controlFlow) => {
|
|
39676
41673
|
if (!isFunctionLike$1(functionNode) || functionNode.params.length === 0) return false;
|
|
@@ -39703,17 +41700,8 @@ const functionReturnsPropsChildren = (functionNode, scopes, controlFlow) => {
|
|
|
39703
41700
|
}, controlFlow);
|
|
39704
41701
|
};
|
|
39705
41702
|
//#endregion
|
|
39706
|
-
//#region src/plugin/utils/function-
|
|
39707
|
-
const
|
|
39708
|
-
const candidate = stripParenExpression(expression);
|
|
39709
|
-
return isNodeOfType(candidate, "Literal") && candidate.value === null;
|
|
39710
|
-
};
|
|
39711
|
-
const functionReturnsOnlyNull = (functionNode) => {
|
|
39712
|
-
if (!isFunctionLike$1(functionNode)) return false;
|
|
39713
|
-
if (!isNodeOfType(functionNode.body, "BlockStatement")) return isNullExpression(functionNode.body);
|
|
39714
|
-
const returnStatements = collectFunctionReturnStatements(functionNode);
|
|
39715
|
-
return returnStatements.length > 0 && returnStatements.every((returnStatement) => Boolean(returnStatement.argument && isNullExpression(returnStatement.argument)));
|
|
39716
|
-
};
|
|
41703
|
+
//#region src/plugin/utils/function-has-react-component-evidence.ts
|
|
41704
|
+
const functionHasReactComponentEvidence = (functionNode, scopes, controlFlow) => functionContainsReactRenderOutput(functionNode, scopes, controlFlow) || functionReturnsPropsChildren(functionNode, scopes, controlFlow) || functionContainsProvenReactHookCall(functionNode, scopes) && functionReturnsOnlyNull(functionNode);
|
|
39717
41705
|
//#endregion
|
|
39718
41706
|
//#region src/plugin/utils/is-proven-styled-component-expression.ts
|
|
39719
41707
|
const findFactoryRoot = (node) => {
|
|
@@ -39746,17 +41734,16 @@ const isProvenStyledComponentExpression = (expression, scopes) => {
|
|
|
39746
41734
|
//#region src/plugin/utils/is-proven-react-component-symbol.ts
|
|
39747
41735
|
const REACT_COMPONENT_HOC_NAMES = new Set(["memo", "forwardRef"]);
|
|
39748
41736
|
const LEGACY_REACT_COMPONENT_FACTORY_NAMES = new Set(["createClass", "createReactClass"]);
|
|
39749
|
-
const functionHasComponentEvidence = (functionNode, scopes, controlFlow) => functionContainsReactRenderOutput(functionNode, scopes, controlFlow) || functionReturnsPropsChildren(functionNode, scopes, controlFlow) || functionContainsProvenReactHookCall(functionNode, scopes) && functionReturnsOnlyNull(functionNode);
|
|
39750
41737
|
const isProvenReactComponentExpression = (expression, scopes, controlFlow, visitedSymbolIds = /* @__PURE__ */ new Set()) => {
|
|
39751
41738
|
const candidate = stripParenExpression(expression);
|
|
39752
|
-
if (isInlineFunctionExpression(candidate)) return
|
|
41739
|
+
if (isInlineFunctionExpression(candidate)) return functionHasReactComponentEvidence(candidate, scopes, controlFlow);
|
|
39753
41740
|
if (isNodeOfType(candidate, "ClassExpression")) return isProvenReactClassComponent(candidate, scopes);
|
|
39754
41741
|
if (isProvenStyledComponentExpression(candidate, scopes)) return true;
|
|
39755
41742
|
if (isNodeOfType(candidate, "Identifier")) {
|
|
39756
41743
|
const symbol = scopes.symbolFor(candidate);
|
|
39757
41744
|
if (!symbol || visitedSymbolIds.has(symbol.id) || hasSymbolWriteBefore(symbol, candidate, scopes)) return false;
|
|
39758
41745
|
visitedSymbolIds.add(symbol.id);
|
|
39759
|
-
if (isNodeOfType(symbol.declarationNode, "FunctionDeclaration")) return
|
|
41746
|
+
if (isNodeOfType(symbol.declarationNode, "FunctionDeclaration")) return functionHasReactComponentEvidence(symbol.declarationNode, scopes, controlFlow);
|
|
39760
41747
|
if (isNodeOfType(symbol.declarationNode, "ClassDeclaration") || isNodeOfType(symbol.declarationNode, "ClassExpression")) return isProvenReactClassComponent(symbol.declarationNode, scopes);
|
|
39761
41748
|
return Boolean(symbol.initializer && isProvenReactComponentExpression(symbol.initializer, scopes, controlFlow, visitedSymbolIds));
|
|
39762
41749
|
}
|
|
@@ -39783,7 +41770,7 @@ const isProvenReactComponentSymbol = (symbol, scopes, controlFlow, componentRefe
|
|
|
39783
41770
|
for (const candidateSymbol of candidateSymbols) {
|
|
39784
41771
|
if (hasSymbolWriteBefore(candidateSymbol, componentReference, scopes)) continue;
|
|
39785
41772
|
if (isComponentDeclaration(candidateSymbol.declarationNode)) {
|
|
39786
|
-
if (
|
|
41773
|
+
if (functionHasReactComponentEvidence(candidateSymbol.declarationNode, scopes, controlFlow)) return true;
|
|
39787
41774
|
continue;
|
|
39788
41775
|
}
|
|
39789
41776
|
const initializer = candidateSymbol.initializer ? stripParenExpression(candidateSymbol.initializer) : null;
|
|
@@ -41285,11 +43272,6 @@ const SAME_REFERENCE_ARRAY_RETURN_METHODS = new Set([
|
|
|
41285
43272
|
"reverse",
|
|
41286
43273
|
"sort"
|
|
41287
43274
|
]);
|
|
41288
|
-
const OBJECT_MUTATION_METHODS = new Set([
|
|
41289
|
-
"assign",
|
|
41290
|
-
"defineProperties",
|
|
41291
|
-
"defineProperty"
|
|
41292
|
-
]);
|
|
41293
43275
|
const REFLECT_MUTATION_METHODS = new Set(["deleteProperty", "set"]);
|
|
41294
43276
|
const cloneReducerPathState = (state) => ({
|
|
41295
43277
|
originalStateReferenceNames: new Set(state.originalStateReferenceNames),
|
|
@@ -41385,7 +43367,7 @@ const collectReducerStateMutationsInExpressionOrStatement = (node, state) => {
|
|
|
41385
43367
|
}
|
|
41386
43368
|
if (!isNodeOfType(unwrappedChild, "CallExpression")) return;
|
|
41387
43369
|
const firstArgument = unwrappedChild.arguments?.[0];
|
|
41388
|
-
if (firstArgument && isExpressionRootedInMutableReducerStateSource(firstArgument, state) && (isStaticMethodCallOnNamedObject(unwrappedChild, "Object",
|
|
43370
|
+
if (firstArgument && isExpressionRootedInMutableReducerStateSource(firstArgument, state) && (isStaticMethodCallOnNamedObject(unwrappedChild, "Object", OBJECT_PROPERTY_MUTATION_METHOD_NAMES) || isStaticMethodCallOnNamedObject(unwrappedChild, "Reflect", REFLECT_MUTATION_METHODS))) {
|
|
41389
43371
|
mutations.push({ node: unwrappedChild });
|
|
41390
43372
|
return;
|
|
41391
43373
|
}
|
|
@@ -43569,6 +45551,53 @@ const noPropCallbackInEffect = defineRule({
|
|
|
43569
45551
|
});
|
|
43570
45552
|
//#endregion
|
|
43571
45553
|
//#region src/plugin/rules/state-and-effects/no-prop-callback-in-render.ts
|
|
45554
|
+
const functionBindingSymbols = (functionNode, scopes) => {
|
|
45555
|
+
let bindingIdentifier = null;
|
|
45556
|
+
if (isNodeOfType(functionNode, "FunctionDeclaration") && functionNode.id) bindingIdentifier = functionNode.id;
|
|
45557
|
+
else {
|
|
45558
|
+
let bindingExpression = findTransparentExpressionRoot(functionNode);
|
|
45559
|
+
let parent = bindingExpression.parent;
|
|
45560
|
+
while (isNodeOfType(parent, "CallExpression") && parent.arguments[0] === bindingExpression) {
|
|
45561
|
+
const callee = parent.callee;
|
|
45562
|
+
const wrapperName = isNodeOfType(callee, "Identifier") ? callee.name : isNodeOfType(callee, "MemberExpression") && isNodeOfType(callee.property, "Identifier") ? callee.property.name : null;
|
|
45563
|
+
if (!isReactApiCall(parent, REACT_HOC_NAMES, scopes, {
|
|
45564
|
+
allowGlobalReactNamespace: true,
|
|
45565
|
+
resolveNamedAliases: true
|
|
45566
|
+
}) && (!wrapperName || REACT_HOC_NAMES.has(wrapperName) || !COMPONENT_HOC_WRAPPER_NAMES.has(wrapperName))) break;
|
|
45567
|
+
bindingExpression = findTransparentExpressionRoot(parent);
|
|
45568
|
+
parent = bindingExpression.parent;
|
|
45569
|
+
}
|
|
45570
|
+
if (isNodeOfType(parent, "VariableDeclarator") && parent.init === bindingExpression && isNodeOfType(parent.id, "Identifier")) bindingIdentifier = parent.id;
|
|
45571
|
+
}
|
|
45572
|
+
if (!bindingIdentifier) return [];
|
|
45573
|
+
let scope = scopes.scopeFor(functionNode);
|
|
45574
|
+
while (scope) {
|
|
45575
|
+
const symbols = scope.symbols.filter((symbol) => symbol.bindingIdentifier === bindingIdentifier);
|
|
45576
|
+
if (symbols.length > 0) return symbols;
|
|
45577
|
+
scope = scope.parent;
|
|
45578
|
+
}
|
|
45579
|
+
return [];
|
|
45580
|
+
};
|
|
45581
|
+
const symbolHasReactComponentUse = (symbol, scopes, visitedSymbolIds = /* @__PURE__ */ new Set()) => {
|
|
45582
|
+
if (visitedSymbolIds.has(symbol.id)) return false;
|
|
45583
|
+
visitedSymbolIds.add(symbol.id);
|
|
45584
|
+
for (const reference of symbol.references) {
|
|
45585
|
+
const identifier = reference.identifier;
|
|
45586
|
+
if (hasSymbolWriteBefore(symbol, identifier, scopes)) continue;
|
|
45587
|
+
const parent = identifier.parent;
|
|
45588
|
+
if (isNodeOfType(parent, "JSXOpeningElement") && isNodeOfType(parent.name, "JSXIdentifier") && parent.name === identifier) return true;
|
|
45589
|
+
const expression = findTransparentExpressionRoot(identifier);
|
|
45590
|
+
const expressionParent = expression.parent;
|
|
45591
|
+
if (isNodeOfType(expressionParent, "CallExpression") && expressionParent.arguments[0] === expression && isReactApiCall(expressionParent, "createElement", scopes, { resolveNamedAliases: true })) return true;
|
|
45592
|
+
if (!isNodeOfType(expressionParent, "VariableDeclarator") || expressionParent.init !== expression || !isNodeOfType(expressionParent.id, "Identifier") || !isNodeOfType(expressionParent.parent, "VariableDeclaration") || expressionParent.parent.kind !== "const") continue;
|
|
45593
|
+
const aliasSymbol = scopes.symbolFor(expressionParent.id);
|
|
45594
|
+
if (aliasSymbol && symbolHasReactComponentUse(aliasSymbol, scopes, visitedSymbolIds)) return true;
|
|
45595
|
+
}
|
|
45596
|
+
return false;
|
|
45597
|
+
};
|
|
45598
|
+
const functionHasReactComponentUse = (functionNode, scopes) => {
|
|
45599
|
+
return functionBindingSymbols(functionNode, scopes).some((symbol) => symbolHasReactComponentUse(symbol, scopes));
|
|
45600
|
+
};
|
|
43572
45601
|
const isPreservedThroughConciseArrow = (callExpression, scopes) => {
|
|
43573
45602
|
let node = callExpression;
|
|
43574
45603
|
let parent = node.parent;
|
|
@@ -43615,7 +45644,10 @@ const noPropCallbackInRender = defineRule({
|
|
|
43615
45644
|
create: (context) => ({ CallExpression(node) {
|
|
43616
45645
|
if (!isResultDiscardedCall(node)) return;
|
|
43617
45646
|
if (isPreservedThroughConciseArrow(node, context.scopes)) return;
|
|
43618
|
-
|
|
45647
|
+
const renderPhaseOwner = findRenderPhaseComponentOrHook(node, context.scopes);
|
|
45648
|
+
if (!renderPhaseOwner) return;
|
|
45649
|
+
const renderPhaseOwnerName = componentOrHookDisplayNameForFunction(renderPhaseOwner);
|
|
45650
|
+
if (!renderPhaseOwnerName || !isReactHookName(renderPhaseOwnerName) && !functionHasReactComponentEvidence(renderPhaseOwner, context.scopes, context.cfg) && !functionHasReactComponentUse(renderPhaseOwner, context.scopes)) return;
|
|
43619
45651
|
const analysis = getProgramAnalysis(node);
|
|
43620
45652
|
if (!analysis) return;
|
|
43621
45653
|
const callee = stripParenExpression(node.callee);
|
|
@@ -44272,6 +46304,7 @@ const noRedundantRoles = defineRule({
|
|
|
44272
46304
|
create: (context) => {
|
|
44273
46305
|
const settings = resolveSettings$13(context.settings);
|
|
44274
46306
|
return { JSXOpeningElement(node) {
|
|
46307
|
+
if (isLocalTestScaffoldJsx(node, context)) return;
|
|
44275
46308
|
const roleAttr = hasJsxPropIgnoreCase(node.attributes, "role");
|
|
44276
46309
|
if (!roleAttr) return;
|
|
44277
46310
|
if (hasJsxPropIgnoreCase(node.attributes, "data-rac")) return;
|
|
@@ -44351,11 +46384,110 @@ const isSameRefCurrentMember = (node, refSymbol, scopes) => {
|
|
|
44351
46384
|
return isNodeOfType(receiver, "Identifier") && resolveConstIdentifierAlias(receiver, scopes)?.id === refSymbol.id;
|
|
44352
46385
|
};
|
|
44353
46386
|
const isSameRefCurrentAlias = (node, refSymbol, scopes) => {
|
|
44354
|
-
|
|
44355
|
-
if (
|
|
44356
|
-
|
|
46387
|
+
const expression = stripParenExpression(node);
|
|
46388
|
+
if (isSameRefCurrentMember(expression, refSymbol, scopes)) return true;
|
|
46389
|
+
if (!isNodeOfType(expression, "Identifier")) return false;
|
|
46390
|
+
const aliasSymbol = scopes.symbolFor(expression);
|
|
44357
46391
|
return aliasSymbol?.kind === "const" && aliasSymbol.initializer !== null && isSameRefCurrentMember(stripParenExpression(aliasSymbol.initializer), refSymbol, scopes);
|
|
44358
46392
|
};
|
|
46393
|
+
const resolveImmutableInitializationValue = (node, scopes, visitedSymbolIds = /* @__PURE__ */ new Set()) => {
|
|
46394
|
+
const expression = stripParenExpression(node);
|
|
46395
|
+
if (!isNodeOfType(expression, "Identifier")) return expression;
|
|
46396
|
+
const symbol = scopes.symbolFor(expression);
|
|
46397
|
+
if (!symbol || symbol.kind !== "const" || !symbol.initializer || symbol.references.some((reference) => reference.flag !== "read") || visitedSymbolIds.has(symbol.id)) return null;
|
|
46398
|
+
visitedSymbolIds.add(symbol.id);
|
|
46399
|
+
return resolveImmutableInitializationValue(symbol.initializer, scopes, visitedSymbolIds);
|
|
46400
|
+
};
|
|
46401
|
+
const isProvablyTruthyInitializationValue = (node, scopes) => {
|
|
46402
|
+
const expression = resolveImmutableInitializationValue(node, scopes);
|
|
46403
|
+
return Boolean(expression && (isNodeOfType(expression, "NewExpression") || isNodeOfType(expression, "ObjectExpression") || isNodeOfType(expression, "ArrayExpression") || isNodeOfType(expression, "ArrowFunctionExpression") || isNodeOfType(expression, "FunctionExpression") || isNodeOfType(expression, "ClassExpression")));
|
|
46404
|
+
};
|
|
46405
|
+
const getInitializationConstructorName = (node, scopes) => {
|
|
46406
|
+
const expression = resolveImmutableInitializationValue(node, scopes);
|
|
46407
|
+
if (!expression) return null;
|
|
46408
|
+
if (isNodeOfType(expression, "NewExpression")) {
|
|
46409
|
+
const callee = stripParenExpression(expression.callee);
|
|
46410
|
+
return isNodeOfType(callee, "Identifier") ? callee.name : null;
|
|
46411
|
+
}
|
|
46412
|
+
return null;
|
|
46413
|
+
};
|
|
46414
|
+
const isClosedTruthyTypeDomain = (typeNode, initializationValue, scopes) => {
|
|
46415
|
+
const initializationExpression = stripParenExpression(initializationValue);
|
|
46416
|
+
if (isNodeOfType(typeNode, "TSTypeLiteral")) return isNodeOfType(initializationExpression, "ObjectExpression");
|
|
46417
|
+
if (isNodeOfType(typeNode, "TSArrayType") || isNodeOfType(typeNode, "TSTupleType")) return isNodeOfType(initializationExpression, "ArrayExpression");
|
|
46418
|
+
if (isNodeOfType(typeNode, "TSFunctionType") || isNodeOfType(typeNode, "TSConstructorType")) return isNodeOfType(initializationExpression, "ArrowFunctionExpression") || isNodeOfType(initializationExpression, "FunctionExpression") || isNodeOfType(initializationExpression, "ClassExpression");
|
|
46419
|
+
if (isNodeOfType(typeNode, "TSObjectKeyword")) return true;
|
|
46420
|
+
if (!isNodeOfType(typeNode, "TSTypeReference")) return false;
|
|
46421
|
+
const typeName = typeNode.typeName;
|
|
46422
|
+
return isNodeOfType(typeName, "Identifier") && typeName.name === getInitializationConstructorName(initializationExpression, scopes);
|
|
46423
|
+
};
|
|
46424
|
+
const refHasClosedFalsySentinelDomain = (refSymbol, initializationValue, scopes) => {
|
|
46425
|
+
const initializer = refSymbol.initializer ? stripParenExpression(refSymbol.initializer) : null;
|
|
46426
|
+
if (!initializer || !isNodeOfType(initializer, "CallExpression")) return false;
|
|
46427
|
+
const [initialValue] = initializer.arguments ?? [];
|
|
46428
|
+
if (!initialValue || isNodeOfType(initialValue, "SpreadElement") || !isEmptySentinel(initialValue, scopes)) return false;
|
|
46429
|
+
const [declaredType] = initializer.typeArguments?.params ?? [];
|
|
46430
|
+
if (!declaredType || !isNodeOfType(declaredType, "TSUnionType")) return false;
|
|
46431
|
+
let hasEmptySentinel = false;
|
|
46432
|
+
let hasTruthyDomain = false;
|
|
46433
|
+
for (const memberType of declaredType.types ?? []) {
|
|
46434
|
+
if (isNodeOfType(memberType, "TSNullKeyword") || isNodeOfType(memberType, "TSUndefinedKeyword")) {
|
|
46435
|
+
hasEmptySentinel = true;
|
|
46436
|
+
continue;
|
|
46437
|
+
}
|
|
46438
|
+
if (!isClosedTruthyTypeDomain(memberType, initializationValue, scopes)) return false;
|
|
46439
|
+
hasTruthyDomain = true;
|
|
46440
|
+
}
|
|
46441
|
+
return hasEmptySentinel && hasTruthyDomain;
|
|
46442
|
+
};
|
|
46443
|
+
const isSafeRefIdentifierUse = (identifier) => {
|
|
46444
|
+
const expressionRoot = findTransparentExpressionRoot(identifier);
|
|
46445
|
+
const parent = expressionRoot.parent;
|
|
46446
|
+
if (parent && isNodeOfType(parent, "VariableDeclarator") && parent.id === expressionRoot && parent.parent !== null && isNodeOfType(parent.parent, "VariableDeclaration") && parent.parent.kind === "const") return true;
|
|
46447
|
+
if (parent && isNodeOfType(parent, "MemberExpression") && parent.object === expressionRoot && getStaticPropertyName(parent) === "current") return true;
|
|
46448
|
+
if (!parent || !isNodeOfType(parent, "VariableDeclarator") || parent.init !== expressionRoot) return false;
|
|
46449
|
+
return isNodeOfType(parent.id, "Identifier") && parent.parent !== null && isNodeOfType(parent.parent, "VariableDeclaration") && parent.parent.kind === "const";
|
|
46450
|
+
};
|
|
46451
|
+
const refDoesNotEscape = (branchRoot, refSymbol, scopes) => {
|
|
46452
|
+
let didEscape = false;
|
|
46453
|
+
walkAst(branchRoot, (child) => {
|
|
46454
|
+
if (didEscape) return false;
|
|
46455
|
+
if (!isNodeOfType(child, "Identifier")) return;
|
|
46456
|
+
if (resolveConstIdentifierAlias(child, scopes)?.id !== refSymbol.id) return;
|
|
46457
|
+
if (child === refSymbol.bindingIdentifier || isSafeRefIdentifierUse(child)) return;
|
|
46458
|
+
didEscape = true;
|
|
46459
|
+
return false;
|
|
46460
|
+
});
|
|
46461
|
+
return !didEscape;
|
|
46462
|
+
};
|
|
46463
|
+
const expressionContainsRefCurrent = (expression, refSymbol, scopes) => {
|
|
46464
|
+
let didFindRefCurrent = false;
|
|
46465
|
+
walkAst(expression, (child) => {
|
|
46466
|
+
if (didFindRefCurrent) return false;
|
|
46467
|
+
if (resolveReactRefSymbol(child, scopes)?.id !== refSymbol.id) return;
|
|
46468
|
+
didFindRefCurrent = true;
|
|
46469
|
+
return false;
|
|
46470
|
+
});
|
|
46471
|
+
return didFindRefCurrent;
|
|
46472
|
+
};
|
|
46473
|
+
const hasNoCompetingRefCurrentWrite = (branchRoot, assignmentExpression, refSymbol, scopes) => {
|
|
46474
|
+
let writeCount = 0;
|
|
46475
|
+
walkAst(branchRoot, (child) => {
|
|
46476
|
+
if (writeCount > 1) return false;
|
|
46477
|
+
if (isNodeOfType(child, "AssignmentExpression")) {
|
|
46478
|
+
if (expressionContainsRefCurrent(child.left, refSymbol, scopes)) writeCount++;
|
|
46479
|
+
return;
|
|
46480
|
+
}
|
|
46481
|
+
if (isNodeOfType(child, "UpdateExpression") || isNodeOfType(child, "UnaryExpression") && child.operator === "delete") {
|
|
46482
|
+
if (expressionContainsRefCurrent(child.argument, refSymbol, scopes)) writeCount++;
|
|
46483
|
+
return;
|
|
46484
|
+
}
|
|
46485
|
+
if (isNodeOfType(child, "ForInStatement") || isNodeOfType(child, "ForOfStatement")) {
|
|
46486
|
+
if (expressionContainsRefCurrent(child.left, refSymbol, scopes)) writeCount++;
|
|
46487
|
+
}
|
|
46488
|
+
});
|
|
46489
|
+
return writeCount === 1 && expressionContainsRefCurrent(assignmentExpression.left, refSymbol, scopes);
|
|
46490
|
+
};
|
|
44359
46491
|
const isEmptySentinel = (node, scopes) => isNodeOfType(node, "Literal") && node.value === null || isNodeOfType(node, "Identifier") && node.name === "undefined" && scopes.isGlobalReference(node);
|
|
44360
46492
|
const hasRepeatedExecutionAncestor = (node, stop) => {
|
|
44361
46493
|
let ancestor = node.parent;
|
|
@@ -44404,18 +46536,22 @@ const hasNoPriorCoExecutableWrite = (assignmentExpression, branchRoot, refSymbol
|
|
|
44404
46536
|
const isDocumentedLazyInitialization = (assignmentExpression, refSymbol, scopes) => {
|
|
44405
46537
|
if (assignmentExpression.operator === "??=" || assignmentExpression.operator === "||=") return true;
|
|
44406
46538
|
if (assignmentExpression.operator !== "=") return false;
|
|
46539
|
+
const renderOwner = findRenderPhaseComponentOrHook(assignmentExpression, scopes);
|
|
46540
|
+
if (!renderOwner) return false;
|
|
44407
46541
|
let descendant = assignmentExpression;
|
|
44408
46542
|
let ancestor = descendant.parent;
|
|
44409
46543
|
while (ancestor) {
|
|
44410
|
-
|
|
46544
|
+
const test = isNodeOfType(ancestor, "IfStatement") ? stripParenExpression(ancestor.test) : null;
|
|
46545
|
+
if (isNodeOfType(ancestor, "IfStatement") && test && isNodeOfType(test, "UnaryExpression") && test.operator === "!" && isSameRefCurrentAlias(test.argument, refSymbol, scopes) && ancestor.consequent === descendant && isProvablyTruthyInitializationValue(assignmentExpression.right, scopes) && refHasClosedFalsySentinelDomain(refSymbol, assignmentExpression.right, scopes) && !hasRepeatedExecutionAncestor(assignmentExpression, ancestor.consequent) && !hasRepeatedExecutionAncestor(ancestor, renderOwner) && hasNoPriorCoExecutableWrite(assignmentExpression, ancestor.consequent, refSymbol, scopes) && hasNoCompetingRefCurrentWrite(renderOwner, assignmentExpression, refSymbol, scopes) && refDoesNotEscape(renderOwner, refSymbol, scopes)) return true;
|
|
46546
|
+
if (isNodeOfType(ancestor, "IfStatement") && isNodeOfType(test, "BinaryExpression") && [
|
|
44411
46547
|
"===",
|
|
44412
46548
|
"==",
|
|
44413
46549
|
"!==",
|
|
44414
46550
|
"!="
|
|
44415
|
-
].includes(
|
|
44416
|
-
const { left, right } =
|
|
46551
|
+
].includes(test.operator)) {
|
|
46552
|
+
const { left, right } = test;
|
|
44417
46553
|
const comparesEmptySentinel = isSameRefCurrentAlias(left, refSymbol, scopes) && isEmptySentinel(right, scopes) || isSameRefCurrentAlias(right, refSymbol, scopes) && isEmptySentinel(left, scopes);
|
|
44418
|
-
const guardedBranch =
|
|
46554
|
+
const guardedBranch = test.operator === "===" || test.operator === "==" ? ancestor.consequent : ancestor.alternate;
|
|
44419
46555
|
if (comparesEmptySentinel && guardedBranch === descendant && guardedBranch && !hasRepeatedExecutionAncestor(assignmentExpression, guardedBranch) && hasNoPriorCoExecutableWrite(assignmentExpression, guardedBranch, refSymbol, scopes)) return true;
|
|
44420
46556
|
}
|
|
44421
46557
|
descendant = ancestor;
|
|
@@ -44829,7 +46965,7 @@ const doConditionsImplyFormula = (conditions, target) => {
|
|
|
44829
46965
|
}
|
|
44830
46966
|
return facts.didConflict || evaluateBooleanFormula$1(target, facts.assignments) === true;
|
|
44831
46967
|
};
|
|
44832
|
-
const getFunctionBindingSymbol = (functionNode, scopes) => {
|
|
46968
|
+
const getFunctionBindingSymbol$1 = (functionNode, scopes) => {
|
|
44833
46969
|
if (isNodeOfType(functionNode, "FunctionDeclaration") && functionNode.id) return scopes.symbolFor(functionNode.id);
|
|
44834
46970
|
const parent = functionNode.parent;
|
|
44835
46971
|
if ((isNodeOfType(functionNode, "ArrowFunctionExpression") || isNodeOfType(functionNode, "FunctionExpression")) && isNodeOfType(parent, "VariableDeclarator") && parent.init === functionNode && isNodeOfType(parent.id, "Identifier")) return scopes.symbolFor(parent.id);
|
|
@@ -44862,7 +46998,7 @@ const isNodeEvaluatedDuringRender = (node, componentNode, scopes, visitedFunctio
|
|
|
44862
46998
|
const synchronousCallbackCall = getSynchronousCallbackCall(functionNode);
|
|
44863
46999
|
if (synchronousCallbackCall) return isNodeEvaluatedDuringRender(synchronousCallbackCall, componentNode, scopes, visitedFunctionSymbolIds);
|
|
44864
47000
|
if (executesDuringRender(functionNode, scopes)) return isNodeEvaluatedDuringRender(functionNode.parent ?? functionNode, componentNode, scopes, visitedFunctionSymbolIds);
|
|
44865
|
-
const functionSymbol = getFunctionBindingSymbol(functionNode, scopes);
|
|
47001
|
+
const functionSymbol = getFunctionBindingSymbol$1(functionNode, scopes);
|
|
44866
47002
|
if (!functionSymbol || visitedFunctionSymbolIds.has(functionSymbol.id)) return false;
|
|
44867
47003
|
visitedFunctionSymbolIds.add(functionSymbol.id);
|
|
44868
47004
|
let callCount = 0;
|
|
@@ -44911,7 +47047,7 @@ const collectExposureConditions = (analysis, context, node, componentNode, prote
|
|
|
44911
47047
|
parent = synchronousCallbackCall.parent;
|
|
44912
47048
|
continue;
|
|
44913
47049
|
}
|
|
44914
|
-
const functionSymbol = getFunctionBindingSymbol(parent, context.scopes);
|
|
47050
|
+
const functionSymbol = getFunctionBindingSymbol$1(parent, context.scopes);
|
|
44915
47051
|
if (functionSymbol?.references.length === 1) {
|
|
44916
47052
|
const callExpression = isReferenceDirectlyCalled(functionSymbol.references[0].identifier);
|
|
44917
47053
|
if (callExpression) {
|
|
@@ -45121,7 +47257,7 @@ const getSetterExposureConditions = (analysis, context, setterReference, compone
|
|
|
45121
47257
|
const functionNode = findEnclosingFunction$1(setterReference.identifier);
|
|
45122
47258
|
if (!functionNode) return null;
|
|
45123
47259
|
if (isInlineJsxCallback(functionNode)) return [collectExposureConditions(analysis, context, functionNode, componentNode, protectedSymbolIds)];
|
|
45124
|
-
const functionSymbol = getFunctionBindingSymbol(functionNode, context.scopes);
|
|
47260
|
+
const functionSymbol = getFunctionBindingSymbol$1(functionNode, context.scopes);
|
|
45125
47261
|
if (!functionSymbol || functionSymbol.references.length === 0) return null;
|
|
45126
47262
|
const conditionsByReference = [];
|
|
45127
47263
|
for (const reference of functionSymbol.references) {
|
|
@@ -45218,6 +47354,87 @@ const noResetAllStateOnPropChange = defineRule({
|
|
|
45218
47354
|
} })
|
|
45219
47355
|
});
|
|
45220
47356
|
//#endregion
|
|
47357
|
+
//#region src/plugin/utils/is-proven-framer-motion-jsx-element.ts
|
|
47358
|
+
const MOTION_FACTORY_MODULES = new Set(["framer-motion", "motion/react"]);
|
|
47359
|
+
const MOTION_TAG_NAMESPACE_MODULES = new Set([
|
|
47360
|
+
"framer-motion/client",
|
|
47361
|
+
"framer-motion/m",
|
|
47362
|
+
"motion/react-client",
|
|
47363
|
+
"motion/react-m"
|
|
47364
|
+
]);
|
|
47365
|
+
const MOTION_FACTORY_EXPORTS = new Set(["m", "motion"]);
|
|
47366
|
+
const getValueImportSource = (symbol) => {
|
|
47367
|
+
if (symbol.kind !== "import") return null;
|
|
47368
|
+
const declaration = symbol.declarationNode.parent;
|
|
47369
|
+
if (!declaration || !isNodeOfType(declaration, "ImportDeclaration") || isTypeOnlyImport(declaration) || isNodeOfType(symbol.declarationNode, "ImportSpecifier") && symbol.declarationNode.importKind === "type") return null;
|
|
47370
|
+
return typeof declaration.source.value === "string" ? declaration.source.value : null;
|
|
47371
|
+
};
|
|
47372
|
+
const getMemberParts = (node) => {
|
|
47373
|
+
if (isNodeOfType(node, "MemberExpression")) {
|
|
47374
|
+
const propertyName = getStaticPropertyName(node);
|
|
47375
|
+
return propertyName ? [node.object, propertyName] : null;
|
|
47376
|
+
}
|
|
47377
|
+
if (isNodeOfType(node, "JSXMemberExpression")) return isNodeOfType(node.property, "JSXIdentifier") ? [node.object, node.property.name] : null;
|
|
47378
|
+
return null;
|
|
47379
|
+
};
|
|
47380
|
+
const resolveSymbol = (node, scopes) => {
|
|
47381
|
+
if (!isNodeOfType(node, "Identifier") && !isNodeOfType(node, "JSXIdentifier")) return null;
|
|
47382
|
+
return resolveConstIdentifierAlias(node, scopes);
|
|
47383
|
+
};
|
|
47384
|
+
const isNamespaceFrom = (node, sources, scopes) => {
|
|
47385
|
+
const symbol = resolveSymbol(stripParenExpression(node), scopes);
|
|
47386
|
+
const source = symbol ? getValueImportSource(symbol) : null;
|
|
47387
|
+
return Boolean(source && sources.has(source) && symbol && isNodeOfType(symbol.declarationNode, "ImportNamespaceSpecifier"));
|
|
47388
|
+
};
|
|
47389
|
+
const isMotionFactory = (rawNode, scopes, visitedSymbolIds) => {
|
|
47390
|
+
const node = stripParenExpression(rawNode);
|
|
47391
|
+
if (isNamespaceFrom(node, MOTION_TAG_NAMESPACE_MODULES, scopes)) return true;
|
|
47392
|
+
const symbol = resolveSymbol(node, scopes);
|
|
47393
|
+
if (symbol?.kind === "import") {
|
|
47394
|
+
const source = getValueImportSource(symbol);
|
|
47395
|
+
const importedName = getImportedName(symbol.declarationNode);
|
|
47396
|
+
return Boolean(source && MOTION_FACTORY_MODULES.has(source) && importedName && MOTION_FACTORY_EXPORTS.has(importedName));
|
|
47397
|
+
}
|
|
47398
|
+
if (symbol?.kind === "const" && symbol.initializer) {
|
|
47399
|
+
if (visitedSymbolIds.has(symbol.id)) return false;
|
|
47400
|
+
visitedSymbolIds.add(symbol.id);
|
|
47401
|
+
return isMotionFactory(symbol.initializer, scopes, visitedSymbolIds);
|
|
47402
|
+
}
|
|
47403
|
+
const memberParts = getMemberParts(node);
|
|
47404
|
+
return Boolean(memberParts && MOTION_FACTORY_EXPORTS.has(memberParts[1]) && isNamespaceFrom(memberParts[0], MOTION_FACTORY_MODULES, scopes));
|
|
47405
|
+
};
|
|
47406
|
+
const isMotionComponent = (rawNode, scopes) => {
|
|
47407
|
+
return isMotionComponentWithVisitedSymbols(rawNode, scopes, /* @__PURE__ */ new Set());
|
|
47408
|
+
};
|
|
47409
|
+
const isMotionComponentWithVisitedSymbols = (rawNode, scopes, visitedSymbolIds) => {
|
|
47410
|
+
const node = stripParenExpression(rawNode);
|
|
47411
|
+
const symbol = resolveSymbol(node, scopes);
|
|
47412
|
+
if (symbol?.kind === "const" && symbol.initializer) {
|
|
47413
|
+
if (visitedSymbolIds.has(symbol.id)) return false;
|
|
47414
|
+
visitedSymbolIds.add(symbol.id);
|
|
47415
|
+
return isMotionComponentWithVisitedSymbols(symbol.initializer, scopes, visitedSymbolIds);
|
|
47416
|
+
}
|
|
47417
|
+
if (symbol?.kind === "import") {
|
|
47418
|
+
const source = getValueImportSource(symbol);
|
|
47419
|
+
return Boolean(source && MOTION_TAG_NAMESPACE_MODULES.has(source) && isNodeOfType(symbol.declarationNode, "ImportSpecifier") && getImportedName(symbol.declarationNode) !== "create");
|
|
47420
|
+
}
|
|
47421
|
+
const memberParts = getMemberParts(node);
|
|
47422
|
+
if (memberParts && isMotionFactory(memberParts[0], scopes, visitedSymbolIds)) return true;
|
|
47423
|
+
if (!isNodeOfType(node, "CallExpression")) return false;
|
|
47424
|
+
if (isMotionFactory(node.callee, scopes, visitedSymbolIds)) return true;
|
|
47425
|
+
const calleeMemberParts = getMemberParts(stripParenExpression(node.callee));
|
|
47426
|
+
return Boolean(calleeMemberParts && calleeMemberParts[1] === "create" && isMotionFactory(calleeMemberParts[0], scopes, visitedSymbolIds));
|
|
47427
|
+
};
|
|
47428
|
+
const isProvenFramerMotionJsxElement = (openingElement, scopes) => {
|
|
47429
|
+
const elementName = openingElement.name;
|
|
47430
|
+
if (isNodeOfType(elementName, "JSXIdentifier")) {
|
|
47431
|
+
if (/^[a-z]/.test(elementName.name)) return false;
|
|
47432
|
+
return isMotionComponent(elementName, scopes);
|
|
47433
|
+
}
|
|
47434
|
+
const memberParts = getMemberParts(elementName);
|
|
47435
|
+
return Boolean(memberParts && isMotionFactory(memberParts[0], scopes, /* @__PURE__ */ new Set()));
|
|
47436
|
+
};
|
|
47437
|
+
//#endregion
|
|
45221
47438
|
//#region src/plugin/rules/performance/no-scale-from-zero.ts
|
|
45222
47439
|
const noScaleFromZero = defineRule({
|
|
45223
47440
|
id: "no-scale-from-zero",
|
|
@@ -45228,6 +47445,8 @@ const noScaleFromZero = defineRule({
|
|
|
45228
47445
|
create: (context) => ({ JSXAttribute(node) {
|
|
45229
47446
|
if (!isNodeOfType(node.name, "JSXIdentifier")) return;
|
|
45230
47447
|
if (node.name.name !== "initial" && node.name.name !== "exit") return;
|
|
47448
|
+
const openingElement = node.parent;
|
|
47449
|
+
if (!openingElement || !isNodeOfType(openingElement, "JSXOpeningElement") || !Object.is(getAuthoritativeJsxAttribute(openingElement.attributes, node.name.name), node) || !isProvenFramerMotionJsxElement(openingElement, context.scopes)) return;
|
|
45231
47450
|
if (!isNodeOfType(node.value, "JSXExpressionContainer")) return;
|
|
45232
47451
|
const expression = node.value.expression;
|
|
45233
47452
|
if (!isNodeOfType(expression, "ObjectExpression")) return;
|
|
@@ -45426,7 +47645,7 @@ const isSelfReferentialSentinelValue = (variableName, literalValue) => literalVa
|
|
|
45426
47645
|
const isIdentifierLikeKeyNameValue = (literalValue) => {
|
|
45427
47646
|
const wordSegments = literalValue.replace(/^[_$\s]+|[_$\s]+$/g, "").split(/[_\-:./$]+/).filter((segment) => segment.length > 0);
|
|
45428
47647
|
if (wordSegments.length < 2) return false;
|
|
45429
|
-
return wordSegments.every((segment) => /^[a-z]
|
|
47648
|
+
return wordSegments.every((segment) => /^[a-z]+(?:[A-Z][a-z]+)*$/.test(segment));
|
|
45430
47649
|
};
|
|
45431
47650
|
const FRAMEWORK_ENV_ADVICE = [
|
|
45432
47651
|
[
|
|
@@ -45500,7 +47719,7 @@ const noSecretsInClientCode = defineRule({
|
|
|
45500
47719
|
const isServerOnlyScope = isInsideServerOnlyScope(node);
|
|
45501
47720
|
const trailingSuffix = getIdentifierTrailingWord(variableName);
|
|
45502
47721
|
const isUiConstant = SECRET_FALSE_POSITIVE_SUFFIXES.has(trailingSuffix);
|
|
45503
|
-
if (shouldUseVariableNameHeuristic && !isServerOnlyScope && SECRET_VARIABLE_PATTERN.test(variableName) && !isUiConstant && !isPublicUrlValue(literalValue) && !isPlaceholderValueForVariableHeuristic && !isSelfReferentialSentinelValue(variableName, literalValue) && !isIdentifierLikeKeyNameValue(literalValue) &&
|
|
47722
|
+
if (shouldUseVariableNameHeuristic && !isServerOnlyScope && SECRET_VARIABLE_PATTERN.test(variableName) && !isUiConstant && !isPublicUrlValue(literalValue) && !isPlaceholderValueForVariableHeuristic && !isSelfReferentialSentinelValue(variableName, literalValue) && !isIdentifierLikeKeyNameValue(literalValue) && literalValue.length > 24) {
|
|
45504
47723
|
context.report({
|
|
45505
47724
|
node,
|
|
45506
47725
|
message: `Hardcoding "${variableName}" in client code is a security vulnerability: the secret ships to the browser where anyone can read it.`
|
|
@@ -47050,6 +49269,185 @@ const noUnescapedEntities = defineRule({
|
|
|
47050
49269
|
} })
|
|
47051
49270
|
});
|
|
47052
49271
|
//#endregion
|
|
49272
|
+
//#region src/plugin/utils/read-server-snapshot-boolean.ts
|
|
49273
|
+
const crossFileScopes = /* @__PURE__ */ new WeakMap();
|
|
49274
|
+
const getCrossFileScopes = (programNode) => {
|
|
49275
|
+
const cachedScopes = crossFileScopes.get(programNode);
|
|
49276
|
+
if (cachedScopes) return cachedScopes;
|
|
49277
|
+
const scopes = analyzeScopes(programNode);
|
|
49278
|
+
crossFileScopes.set(programNode, scopes);
|
|
49279
|
+
return scopes;
|
|
49280
|
+
};
|
|
49281
|
+
const symbolIsImmutable = (symbol) => (symbol.kind === "const" || symbol.kind === "function") && symbol.references.every((reference) => reference.flag === "read");
|
|
49282
|
+
const identifierAliasChainIsImmutable = (identifier, scopes, allowImportTerminal = false) => {
|
|
49283
|
+
if (!isNodeOfType(identifier, "Identifier")) return false;
|
|
49284
|
+
const visitedSymbolIds = /* @__PURE__ */ new Set();
|
|
49285
|
+
let symbol = scopes.symbolFor(identifier);
|
|
49286
|
+
while (symbol) {
|
|
49287
|
+
if (symbol.kind === "import") return allowImportTerminal;
|
|
49288
|
+
if (visitedSymbolIds.has(symbol.id) || !symbolIsImmutable(symbol)) return false;
|
|
49289
|
+
visitedSymbolIds.add(symbol.id);
|
|
49290
|
+
if (symbol.kind !== "const" || !symbol.initializer) return symbol.kind === "function";
|
|
49291
|
+
const initializer = stripParenExpression(symbol.initializer);
|
|
49292
|
+
if (!isNodeOfType(initializer, "Identifier")) return true;
|
|
49293
|
+
symbol = scopes.symbolFor(initializer);
|
|
49294
|
+
}
|
|
49295
|
+
return false;
|
|
49296
|
+
};
|
|
49297
|
+
const getImportedHookBinding = (callee, scopes) => {
|
|
49298
|
+
if (!isNodeOfType(callee, "Identifier") || !identifierAliasChainIsImmutable(callee, scopes, true)) return null;
|
|
49299
|
+
const importedSymbol = resolveConstIdentifierAlias(callee, scopes);
|
|
49300
|
+
if (importedSymbol?.kind !== "import") return null;
|
|
49301
|
+
const importDeclaration = importedSymbol.declarationNode.parent;
|
|
49302
|
+
if (!importDeclaration || !isNodeOfType(importDeclaration, "ImportDeclaration")) return null;
|
|
49303
|
+
const source = importDeclaration.source.value;
|
|
49304
|
+
if (typeof source !== "string") return null;
|
|
49305
|
+
const exportedName = resolveImportedExportName(importedSymbol.declarationNode);
|
|
49306
|
+
return exportedName ? {
|
|
49307
|
+
exportedName,
|
|
49308
|
+
source
|
|
49309
|
+
} : null;
|
|
49310
|
+
};
|
|
49311
|
+
const functionBindingIsImmutable = (functionNode, scopes) => {
|
|
49312
|
+
if (isNodeOfType(functionNode, "FunctionDeclaration") && functionNode.id) {
|
|
49313
|
+
const symbol = scopes.symbolFor(functionNode.id);
|
|
49314
|
+
return Boolean(symbol && symbolIsImmutable(symbol));
|
|
49315
|
+
}
|
|
49316
|
+
const expressionRoot = findTransparentExpressionRoot(functionNode);
|
|
49317
|
+
const parentNode = expressionRoot.parent;
|
|
49318
|
+
if (isNodeOfType(parentNode, "ExportDefaultDeclaration")) return true;
|
|
49319
|
+
if (!isNodeOfType(parentNode, "VariableDeclarator") || parentNode.init !== expressionRoot || !isNodeOfType(parentNode.id, "Identifier")) return false;
|
|
49320
|
+
const symbol = scopes.symbolFor(parentNode.id);
|
|
49321
|
+
return Boolean(symbol && symbolIsImmutable(symbol));
|
|
49322
|
+
};
|
|
49323
|
+
const getExactFunctionResultExpression = (functionNode) => {
|
|
49324
|
+
if (!isFunctionLike$1(functionNode) || functionNode.async) return null;
|
|
49325
|
+
if (isNodeOfType(functionNode, "FunctionDeclaration") && functionNode.generator) return null;
|
|
49326
|
+
if (isNodeOfType(functionNode, "FunctionExpression") && functionNode.generator) return null;
|
|
49327
|
+
if (!isNodeOfType(functionNode.body, "BlockStatement")) return functionNode.body;
|
|
49328
|
+
const [returnStatement, additionalReturnStatement] = collectFunctionReturnStatements(functionNode);
|
|
49329
|
+
if (!returnStatement || additionalReturnStatement || !returnStatement.argument) return null;
|
|
49330
|
+
if ([...functionNode.body.body].pop() !== returnStatement) return null;
|
|
49331
|
+
return returnStatement.argument;
|
|
49332
|
+
};
|
|
49333
|
+
const functionHasNoParameters = (functionNode) => isFunctionLike$1(functionNode) && (functionNode.params?.length ?? 0) === 0;
|
|
49334
|
+
const readImmutableBooleanLiteral = (expression, scopes, visitedSymbolIds) => {
|
|
49335
|
+
const unwrappedExpression = stripParenExpression(expression);
|
|
49336
|
+
if (isNodeOfType(unwrappedExpression, "Literal") && typeof unwrappedExpression.value === "boolean") return unwrappedExpression.value;
|
|
49337
|
+
if (!isNodeOfType(unwrappedExpression, "Identifier")) return null;
|
|
49338
|
+
const symbol = scopes.symbolFor(unwrappedExpression);
|
|
49339
|
+
if (!symbol || symbol.kind !== "const" || !symbol.initializer || !symbolIsImmutable(symbol) || visitedSymbolIds.has(symbol.id)) return null;
|
|
49340
|
+
visitedSymbolIds.add(symbol.id);
|
|
49341
|
+
return readImmutableBooleanLiteral(symbol.initializer, scopes, visitedSymbolIds);
|
|
49342
|
+
};
|
|
49343
|
+
const readFunctionLiteralBoolean = (expression, scopes) => {
|
|
49344
|
+
const unwrappedExpression = stripParenExpression(expression);
|
|
49345
|
+
if (isNodeOfType(unwrappedExpression, "Identifier") && !identifierAliasChainIsImmutable(unwrappedExpression, scopes)) return null;
|
|
49346
|
+
const functionNode = resolveExactLocalFunction(unwrappedExpression, scopes);
|
|
49347
|
+
if (!functionNode) return null;
|
|
49348
|
+
const resultExpression = getExactFunctionResultExpression(functionNode);
|
|
49349
|
+
return resultExpression ? readImmutableBooleanLiteral(resultExpression, scopes, /* @__PURE__ */ new Set()) : null;
|
|
49350
|
+
};
|
|
49351
|
+
const readServerSnapshotBooleanInternal = (expression, scopes, visitedSymbolIds, visitedFunctionNodes, currentFilename) => {
|
|
49352
|
+
const unwrappedExpression = stripParenExpression(expression);
|
|
49353
|
+
if (isNodeOfType(unwrappedExpression, "Literal") && typeof unwrappedExpression.value === "boolean") return {
|
|
49354
|
+
hasUseSyncExternalStoreOrigin: false,
|
|
49355
|
+
value: unwrappedExpression.value
|
|
49356
|
+
};
|
|
49357
|
+
if (isNodeOfType(unwrappedExpression, "Identifier")) {
|
|
49358
|
+
const symbol = scopes.symbolFor(unwrappedExpression);
|
|
49359
|
+
if (!symbol || symbol.kind !== "const" || !symbol.initializer || !symbolIsImmutable(symbol) || visitedSymbolIds.has(symbol.id)) return null;
|
|
49360
|
+
visitedSymbolIds.add(symbol.id);
|
|
49361
|
+
return readServerSnapshotBooleanInternal(symbol.initializer, scopes, visitedSymbolIds, visitedFunctionNodes, currentFilename);
|
|
49362
|
+
}
|
|
49363
|
+
if (isNodeOfType(unwrappedExpression, "UnaryExpression") && unwrappedExpression.operator === "!") {
|
|
49364
|
+
const argumentResult = readServerSnapshotBooleanInternal(unwrappedExpression.argument, scopes, visitedSymbolIds, visitedFunctionNodes, currentFilename);
|
|
49365
|
+
return argumentResult ? {
|
|
49366
|
+
hasUseSyncExternalStoreOrigin: argumentResult.hasUseSyncExternalStoreOrigin,
|
|
49367
|
+
value: !argumentResult.value
|
|
49368
|
+
} : null;
|
|
49369
|
+
}
|
|
49370
|
+
if (isNodeOfType(unwrappedExpression, "LogicalExpression") && (unwrappedExpression.operator === "&&" || unwrappedExpression.operator === "||")) {
|
|
49371
|
+
const leftResult = readServerSnapshotBooleanInternal(unwrappedExpression.left, scopes, new Set(visitedSymbolIds), new Set(visitedFunctionNodes), currentFilename);
|
|
49372
|
+
if (leftResult && (unwrappedExpression.operator === "&&" && !leftResult.value || unwrappedExpression.operator === "||" && leftResult.value)) return leftResult;
|
|
49373
|
+
const rightResult = readServerSnapshotBooleanInternal(unwrappedExpression.right, scopes, new Set(visitedSymbolIds), new Set(visitedFunctionNodes), currentFilename);
|
|
49374
|
+
if (rightResult && (unwrappedExpression.operator === "&&" && !rightResult.value || unwrappedExpression.operator === "||" && rightResult.value)) return rightResult;
|
|
49375
|
+
return leftResult && rightResult ? {
|
|
49376
|
+
hasUseSyncExternalStoreOrigin: leftResult.hasUseSyncExternalStoreOrigin || rightResult.hasUseSyncExternalStoreOrigin,
|
|
49377
|
+
value: rightResult.value
|
|
49378
|
+
} : null;
|
|
49379
|
+
}
|
|
49380
|
+
if (!isNodeOfType(unwrappedExpression, "CallExpression")) return null;
|
|
49381
|
+
if (isReactApiCall(unwrappedExpression, "useSyncExternalStore", scopes, { resolveNamedAliases: true })) {
|
|
49382
|
+
const [, , serverSnapshotArgument] = unwrappedExpression.arguments ?? [];
|
|
49383
|
+
if (!serverSnapshotArgument || isNodeOfType(serverSnapshotArgument, "SpreadElement")) return null;
|
|
49384
|
+
const serverSnapshotValue = readFunctionLiteralBoolean(serverSnapshotArgument, scopes);
|
|
49385
|
+
return serverSnapshotValue === null ? null : {
|
|
49386
|
+
hasUseSyncExternalStoreOrigin: true,
|
|
49387
|
+
value: serverSnapshotValue
|
|
49388
|
+
};
|
|
49389
|
+
}
|
|
49390
|
+
if ((unwrappedExpression.arguments?.length ?? 0) !== 0) return null;
|
|
49391
|
+
const callee = stripParenExpression(unwrappedExpression.callee);
|
|
49392
|
+
const importedHookBinding = getImportedHookBinding(callee, scopes);
|
|
49393
|
+
if (importedHookBinding && currentFilename) {
|
|
49394
|
+
const resolvedHook = resolveCrossFileFunctionExportWithFilePath(path.resolve(currentFilename), importedHookBinding.source, importedHookBinding.exportedName);
|
|
49395
|
+
if (resolvedHook && !resolvedHook.filePath.split(path.sep).includes("node_modules")) {
|
|
49396
|
+
const resolvedScopes = getCrossFileScopes(resolvedHook.programNode);
|
|
49397
|
+
if (isReactHookName(componentOrHookDisplayNameForFunction(resolvedHook.functionNode) ?? (importedHookBinding.exportedName === "default" && isNodeOfType(callee, "Identifier") ? callee.name : importedHookBinding.exportedName)) && functionHasNoParameters(resolvedHook.functionNode) && functionBindingIsImmutable(resolvedHook.functionNode, resolvedScopes) && !visitedFunctionNodes.has(resolvedHook.functionNode)) {
|
|
49398
|
+
visitedFunctionNodes.add(resolvedHook.functionNode);
|
|
49399
|
+
const resultExpression = getExactFunctionResultExpression(resolvedHook.functionNode);
|
|
49400
|
+
if (resultExpression) return readServerSnapshotBooleanInternal(resultExpression, resolvedScopes, /* @__PURE__ */ new Set(), visitedFunctionNodes, resolvedHook.filePath);
|
|
49401
|
+
}
|
|
49402
|
+
}
|
|
49403
|
+
return null;
|
|
49404
|
+
}
|
|
49405
|
+
if (!isNodeOfType(callee, "Identifier") || !isReactHookName(callee.name) || !identifierAliasChainIsImmutable(callee, scopes)) return null;
|
|
49406
|
+
const hookFunction = resolveExactLocalFunction(callee, scopes);
|
|
49407
|
+
if (!hookFunction || !functionHasNoParameters(hookFunction) || visitedFunctionNodes.has(hookFunction)) return null;
|
|
49408
|
+
visitedFunctionNodes.add(hookFunction);
|
|
49409
|
+
const resultExpression = getExactFunctionResultExpression(hookFunction);
|
|
49410
|
+
return resultExpression ? readServerSnapshotBooleanInternal(resultExpression, scopes, visitedSymbolIds, visitedFunctionNodes, currentFilename) : null;
|
|
49411
|
+
};
|
|
49412
|
+
const readServerSnapshotBoolean = (expression, scopes, currentFilename) => {
|
|
49413
|
+
const result = readServerSnapshotBooleanInternal(expression, scopes, /* @__PURE__ */ new Set(), /* @__PURE__ */ new Set(), currentFilename);
|
|
49414
|
+
return result?.hasUseSyncExternalStoreOrigin ? result.value : null;
|
|
49415
|
+
};
|
|
49416
|
+
//#endregion
|
|
49417
|
+
//#region src/plugin/utils/is-after-falsy-server-snapshot-early-return.ts
|
|
49418
|
+
const isAfterFalsyServerSnapshotEarlyReturn = (node, componentOrHookNode, scopes, filename) => {
|
|
49419
|
+
const enclosingFunction = findEnclosingFunction$1(node);
|
|
49420
|
+
if (!enclosingFunction || enclosingFunction !== componentOrHookNode && !executesDuringRender(enclosingFunction, scopes) || !isFunctionLike$1(enclosingFunction) || !isNodeOfType(enclosingFunction.body, "BlockStatement")) return false;
|
|
49421
|
+
let currentNode = node;
|
|
49422
|
+
while (currentNode !== enclosingFunction) {
|
|
49423
|
+
const parentNode = currentNode.parent;
|
|
49424
|
+
if (!parentNode) return false;
|
|
49425
|
+
if (isNodeOfType(parentNode, "BlockStatement")) for (const statement of parentNode.body) {
|
|
49426
|
+
if (statement === currentNode) break;
|
|
49427
|
+
if (!isNodeOfType(statement, "IfStatement")) continue;
|
|
49428
|
+
const serverResult = readServerSnapshotBoolean(statement.test, scopes, filename);
|
|
49429
|
+
if (serverResult === true && statementAlwaysExits(statement.consequent)) return true;
|
|
49430
|
+
if (serverResult === false && statement.alternate && statementAlwaysExits(statement.alternate)) return true;
|
|
49431
|
+
}
|
|
49432
|
+
currentNode = parentNode;
|
|
49433
|
+
}
|
|
49434
|
+
return false;
|
|
49435
|
+
};
|
|
49436
|
+
//#endregion
|
|
49437
|
+
//#region src/plugin/utils/is-gated-by-falsy-server-snapshot.ts
|
|
49438
|
+
const isGatedByFalsyServerSnapshot = (node, scopes, filename) => {
|
|
49439
|
+
let currentNode = node;
|
|
49440
|
+
let parentNode = node.parent;
|
|
49441
|
+
while (parentNode) {
|
|
49442
|
+
if (isNodeOfType(parentNode, "LogicalExpression") && parentNode.right === currentNode && (parentNode.operator === "&&" && readServerSnapshotBoolean(parentNode.left, scopes, filename) === false || parentNode.operator === "||" && readServerSnapshotBoolean(parentNode.left, scopes, filename) === true)) return true;
|
|
49443
|
+
if (isNodeOfType(parentNode, "ConditionalExpression") && (parentNode.consequent === currentNode && readServerSnapshotBoolean(parentNode.test, scopes, filename) === false || parentNode.alternate === currentNode && readServerSnapshotBoolean(parentNode.test, scopes, filename) === true)) return true;
|
|
49444
|
+
if (isNodeOfType(parentNode, "IfStatement") && (parentNode.consequent === currentNode && readServerSnapshotBoolean(parentNode.test, scopes, filename) === false || parentNode.alternate === currentNode && readServerSnapshotBoolean(parentNode.test, scopes, filename) === true)) return true;
|
|
49445
|
+
currentNode = parentNode;
|
|
49446
|
+
parentNode = parentNode.parent ?? null;
|
|
49447
|
+
}
|
|
49448
|
+
return false;
|
|
49449
|
+
};
|
|
49450
|
+
//#endregion
|
|
47053
49451
|
//#region src/plugin/rules/performance/no-unguarded-browser-global-in-render-or-hook-init.ts
|
|
47054
49452
|
const BROWSER_GLOBAL_NAMES = new Set([
|
|
47055
49453
|
"window",
|
|
@@ -47156,7 +49554,9 @@ const noUnguardedBrowserGlobalInRenderOrHookInit = defineRule({
|
|
|
47156
49554
|
if (fileIsEmailTemplate) return;
|
|
47157
49555
|
if (isGeneratedImageRenderContext(context, findEnclosingJsxOpeningElement(node) ?? node)) return;
|
|
47158
49556
|
if (isGatedByFalsyInitialState(node, context.scopes)) return;
|
|
49557
|
+
if (isGatedByFalsyServerSnapshot(node, context.scopes, context.filename)) return;
|
|
47159
49558
|
if (isAfterClientOnlyEarlyReturn(node, componentOrHookNode, context.scopes)) return;
|
|
49559
|
+
if (isAfterFalsyServerSnapshotEarlyReturn(node, componentOrHookNode, context.scopes, context.filename)) return;
|
|
47160
49560
|
if (isInsideAvailabilityGuard(node, browserGlobalName, context)) return;
|
|
47161
49561
|
if (isAfterAvailabilityEarlyExit(node, componentOrHookNode, browserGlobalName, context)) return;
|
|
47162
49562
|
reportedNodes.add(node);
|
|
@@ -50484,10 +52884,9 @@ const isSameApplicationChannelInstance = (targetText, fileContent) => {
|
|
|
50484
52884
|
return new RegExp(`(?<![\\w$.])${escapeRegExp(receiverRoot)}\\s*${SAME_APPLICATION_CHANNEL_CONSTRUCTOR_SOURCE}`).test(fileContent);
|
|
50485
52885
|
};
|
|
50486
52886
|
const WORKER_FILE_PATH_PATTERN = /worker/i;
|
|
50487
|
-
const getNodeStartIndex = (node) => "start" in node && typeof node.start === "number" ? node.start : -1;
|
|
50488
52887
|
const getNodeText = (content, node) => {
|
|
50489
52888
|
const startIndex = getNodeStartIndex(node);
|
|
50490
|
-
const endIndex =
|
|
52889
|
+
const endIndex = getNodeEndIndex(node);
|
|
50491
52890
|
if (startIndex < 0 || endIndex < 0) return "";
|
|
50492
52891
|
return content.slice(startIndex, endIndex);
|
|
50493
52892
|
};
|
|
@@ -50884,17 +53283,6 @@ const preferEs6Class = defineRule({
|
|
|
50884
53283
|
}
|
|
50885
53284
|
});
|
|
50886
53285
|
//#endregion
|
|
50887
|
-
//#region src/plugin/utils/is-jsx-element-or-fragment.ts
|
|
50888
|
-
/**
|
|
50889
|
-
* Type-guard for the two single-node JSX output forms: `JSXElement`
|
|
50890
|
-
* (`<Foo />`) and `JSXFragment` (`<>…</>`). Canonical home for the
|
|
50891
|
-
* `isNodeOfType(x, "JSXElement") || isNodeOfType(x, "JSXFragment")` check
|
|
50892
|
-
* that many rules otherwise inline. Does NOT unwrap parens / TS wrappers —
|
|
50893
|
-
* callers that need the semantic expression should `stripParenExpression`
|
|
50894
|
-
* first.
|
|
50895
|
-
*/
|
|
50896
|
-
const isJsxElementOrFragment = (node) => Boolean(node && (isNodeOfType(node, "JSXElement") || isNodeOfType(node, "JSXFragment")));
|
|
50897
|
-
//#endregion
|
|
50898
53286
|
//#region src/plugin/rules/architecture/prefer-explicit-variants.ts
|
|
50899
53287
|
const resolveBooleanPropTestName = (testNode, booleanPropBindings) => {
|
|
50900
53288
|
let identifierNode = stripParenExpression(testNode);
|
|
@@ -51359,20 +53747,51 @@ const preferModuleScopePureFunction = defineRule({
|
|
|
51359
53747
|
}
|
|
51360
53748
|
});
|
|
51361
53749
|
//#endregion
|
|
53750
|
+
//#region src/plugin/utils/get-require-call-source.ts
|
|
53751
|
+
const getRequireCallSource = (expression) => {
|
|
53752
|
+
const unwrappedExpression = stripParenExpression(expression);
|
|
53753
|
+
if (isNodeOfType(unwrappedExpression, "MemberExpression")) return getRequireCallSource(unwrappedExpression.object);
|
|
53754
|
+
if (!isNodeOfType(unwrappedExpression, "CallExpression")) return null;
|
|
53755
|
+
if (!isNodeOfType(unwrappedExpression.callee, "Identifier") || unwrappedExpression.callee.name !== "require") return null;
|
|
53756
|
+
const [firstArgument] = unwrappedExpression.arguments ?? [];
|
|
53757
|
+
if (!firstArgument || !isNodeOfType(firstArgument, "Literal")) return null;
|
|
53758
|
+
return typeof firstArgument.value === "string" ? firstArgument.value : null;
|
|
53759
|
+
};
|
|
53760
|
+
//#endregion
|
|
53761
|
+
//#region src/plugin/utils/is-proven-node-crypto-namespace-reference.ts
|
|
53762
|
+
const NODE_CRYPTO_MODULE_SOURCES = new Set(["crypto", "node:crypto"]);
|
|
53763
|
+
const isProvenNodeCryptoNamespaceReference = (expression, scopes) => {
|
|
53764
|
+
const identifier = stripParenExpression(expression);
|
|
53765
|
+
if (!isNodeOfType(identifier, "Identifier")) return false;
|
|
53766
|
+
const symbol = resolveConstIdentifierAlias(identifier, scopes);
|
|
53767
|
+
if (!symbol) return false;
|
|
53768
|
+
if (symbol.kind === "import") {
|
|
53769
|
+
const importBinding = getImportBindingForName(identifier, symbol.name);
|
|
53770
|
+
return Boolean(importBinding && NODE_CRYPTO_MODULE_SOURCES.has(importBinding.source));
|
|
53771
|
+
}
|
|
53772
|
+
return Boolean(symbol.kind === "const" && symbol.initializer && NODE_CRYPTO_MODULE_SOURCES.has(getRequireCallSource(symbol.initializer) ?? ""));
|
|
53773
|
+
};
|
|
53774
|
+
//#endregion
|
|
51362
53775
|
//#region src/plugin/rules/architecture/prefer-module-scope-static-value.ts
|
|
51363
53776
|
const MUTATING_RECEIVER_METHOD_NAMES = new Set([...MUTATING_ARRAY_METHODS, ...MUTATING_COLLECTION_METHODS]);
|
|
51364
53777
|
const isMutationContext = (referenceIdentifier) => {
|
|
51365
|
-
|
|
51366
|
-
|
|
51367
|
-
|
|
51368
|
-
|
|
51369
|
-
|
|
51370
|
-
|
|
51371
|
-
|
|
51372
|
-
|
|
51373
|
-
if (isNodeOfType(
|
|
51374
|
-
|
|
51375
|
-
|
|
53778
|
+
let mutationTarget = referenceIdentifier;
|
|
53779
|
+
let receiverMethodName = null;
|
|
53780
|
+
while (mutationTarget.parent) {
|
|
53781
|
+
const parent = mutationTarget.parent;
|
|
53782
|
+
if (TRANSPARENT_EXPRESSION_WRAPPER_TYPES.has(parent.type) && "expression" in parent && parent.expression === mutationTarget) {
|
|
53783
|
+
mutationTarget = parent;
|
|
53784
|
+
continue;
|
|
53785
|
+
}
|
|
53786
|
+
if (isNodeOfType(parent, "MemberExpression") && parent.object === mutationTarget) {
|
|
53787
|
+
receiverMethodName = getStaticPropertyName(parent);
|
|
53788
|
+
mutationTarget = parent;
|
|
53789
|
+
continue;
|
|
53790
|
+
}
|
|
53791
|
+
if (isNodeOfType(parent, "AssignmentExpression") && parent.left === mutationTarget) return true;
|
|
53792
|
+
if (isNodeOfType(parent, "UpdateExpression") && parent.argument === mutationTarget) return true;
|
|
53793
|
+
if (isNodeOfType(parent, "UnaryExpression") && parent.operator === "delete" && parent.argument === mutationTarget) return true;
|
|
53794
|
+
return Boolean(isNodeOfType(parent, "CallExpression") && parent.callee === mutationTarget && MUTATING_RECEIVER_METHOD_NAMES.has(receiverMethodName ?? ""));
|
|
51376
53795
|
}
|
|
51377
53796
|
return false;
|
|
51378
53797
|
};
|
|
@@ -51472,9 +53891,9 @@ const isImpureCall = (node, scopes) => {
|
|
|
51472
53891
|
const callee = node.callee;
|
|
51473
53892
|
if (isNodeOfType(callee, "Identifier")) return isImpureBareCallee(callee, scopes);
|
|
51474
53893
|
if (!isNodeOfType(callee, "MemberExpression") || callee.computed) return false;
|
|
51475
|
-
if (!isNodeOfType(callee.object, "Identifier")) return false;
|
|
51476
53894
|
if (!isNodeOfType(callee.property, "Identifier")) return false;
|
|
51477
|
-
|
|
53895
|
+
for (const [receiverName, receiverMethodNames] of IMPURE_MEMBER_RECEIVERS) if (receiverMethodNames.has(callee.property.name) && (isProvenGlobalNamespaceReference(callee.object, receiverName, scopes) || receiverName === "crypto" && isProvenNodeCryptoNamespaceReference(callee.object, scopes))) return true;
|
|
53896
|
+
return false;
|
|
51478
53897
|
};
|
|
51479
53898
|
const containsImpureExpression = (expression, scopes) => {
|
|
51480
53899
|
let foundImpure = false;
|
|
@@ -51749,6 +54168,15 @@ const STABLE_REACT_HOOK_VALUE_NAMES = new Set([
|
|
|
51749
54168
|
"useState",
|
|
51750
54169
|
"useTransition"
|
|
51751
54170
|
]);
|
|
54171
|
+
const REGISTRATION_METHOD_BY_RELEASE_METHOD = new Map([
|
|
54172
|
+
["off", "on"],
|
|
54173
|
+
["removeEventListener", "addEventListener"],
|
|
54174
|
+
["removeListener", "addListener"],
|
|
54175
|
+
["unlisten", "listen"],
|
|
54176
|
+
["unsub", "sub"],
|
|
54177
|
+
["unsubscribe", "subscribe"],
|
|
54178
|
+
["unwatch", "watch"]
|
|
54179
|
+
]);
|
|
51752
54180
|
const isStableReactHookDependency = (dependency, context) => {
|
|
51753
54181
|
const unwrappedDependency = stripParenExpression(dependency);
|
|
51754
54182
|
if (!isNodeOfType(unwrappedDependency, "Identifier")) return false;
|
|
@@ -51814,30 +54242,87 @@ const isCallExpressionWithSubHandlerCallee = (callExpression) => {
|
|
|
51814
54242
|
if (isNodeOfType(callee, "MemberExpression") && isNodeOfType(callee.property, "Identifier") && SUBSCRIPTION_METHOD_NAMES.has(callee.property.name)) return true;
|
|
51815
54243
|
return false;
|
|
51816
54244
|
};
|
|
51817
|
-
const
|
|
54245
|
+
const getStaticMemberCallMethodName = (callExpression) => {
|
|
54246
|
+
if (!isNodeOfType(callExpression, "CallExpression")) return null;
|
|
54247
|
+
const callee = callExpression.callee;
|
|
54248
|
+
return isNodeOfType(callee, "MemberExpression") && !callee.computed && isNodeOfType(callee.property, "Identifier") ? callee.property.name : null;
|
|
54249
|
+
};
|
|
54250
|
+
const getCallArgumentUse = (reference) => {
|
|
54251
|
+
const argument = findTransparentExpressionRoot(reference);
|
|
54252
|
+
const parent = argument.parent;
|
|
54253
|
+
if (!isNodeOfType(parent, "CallExpression")) return null;
|
|
54254
|
+
const argumentIndex = (parent.arguments ?? []).findIndex((candidateArgument) => candidateArgument === argument);
|
|
54255
|
+
return argumentIndex === -1 ? null : {
|
|
54256
|
+
callExpression: parent,
|
|
54257
|
+
argumentIndex
|
|
54258
|
+
};
|
|
54259
|
+
};
|
|
54260
|
+
const isMatchingRegistrationAndRelease = (registration, release, context) => {
|
|
54261
|
+
const releaseMethodName = getStaticMemberCallMethodName(release.callExpression);
|
|
54262
|
+
const expectedRegistrationMethod = releaseMethodName ? REGISTRATION_METHOD_BY_RELEASE_METHOD.get(releaseMethodName) : null;
|
|
54263
|
+
if (getStaticMemberCallMethodName(registration.callExpression) !== expectedRegistrationMethod) return false;
|
|
54264
|
+
if (registration.argumentIndex !== release.argumentIndex) return false;
|
|
54265
|
+
const registrationCallee = registration.callExpression.callee;
|
|
54266
|
+
const releaseCallee = release.callExpression.callee;
|
|
54267
|
+
if (!isNodeOfType(registrationCallee, "MemberExpression") || !isNodeOfType(releaseCallee, "MemberExpression")) return false;
|
|
54268
|
+
const registrationReceiverKey = resolveExpressionKey$1(registrationCallee.object, context);
|
|
54269
|
+
if (registrationReceiverKey === null || registrationReceiverKey !== resolveExpressionKey$1(releaseCallee.object, context)) return false;
|
|
54270
|
+
const registrationArguments = registration.callExpression.arguments ?? [];
|
|
54271
|
+
const releaseArguments = release.callExpression.arguments ?? [];
|
|
54272
|
+
if (registrationArguments.length !== releaseArguments.length) return false;
|
|
54273
|
+
return registrationArguments.every((registrationArgument, argumentIndex) => {
|
|
54274
|
+
if (argumentIndex === registration.argumentIndex) return true;
|
|
54275
|
+
const registrationArgumentKey = resolveExpressionKey$1(registrationArgument, context);
|
|
54276
|
+
return registrationArgumentKey !== null && registrationArgumentKey === resolveExpressionKey$1(releaseArguments[argumentIndex], context);
|
|
54277
|
+
});
|
|
54278
|
+
};
|
|
54279
|
+
const findExclusiveSubHandlerCall = (enclosingFunction, context) => {
|
|
51818
54280
|
const directParent = enclosingFunction.parent;
|
|
51819
54281
|
if (isNodeOfType(directParent, "CallExpression") && (directParent.arguments ?? []).some((arg) => arg === enclosingFunction) && isCallExpressionWithSubHandlerCallee(directParent)) return directParent;
|
|
51820
|
-
const
|
|
51821
|
-
if (
|
|
51822
|
-
let
|
|
51823
|
-
|
|
51824
|
-
|
|
51825
|
-
|
|
51826
|
-
|
|
51827
|
-
|
|
51828
|
-
|
|
51829
|
-
return false;
|
|
54282
|
+
const bindingIdentifier = getFunctionBindingIdentifier$1(enclosingFunction);
|
|
54283
|
+
if (!bindingIdentifier) return null;
|
|
54284
|
+
let bindingSymbol = context.scopes.symbolFor(bindingIdentifier);
|
|
54285
|
+
if (isNodeOfType(enclosingFunction, "FunctionDeclaration")) {
|
|
54286
|
+
let bindingScope = context.scopes.scopeFor(enclosingFunction);
|
|
54287
|
+
bindingSymbol = null;
|
|
54288
|
+
while (bindingScope && !bindingSymbol) {
|
|
54289
|
+
bindingSymbol = bindingScope.symbols.find((candidateSymbol) => candidateSymbol.declarationNode === enclosingFunction) ?? null;
|
|
54290
|
+
bindingScope = bindingScope.parent;
|
|
51830
54291
|
}
|
|
51831
|
-
}
|
|
51832
|
-
return
|
|
54292
|
+
}
|
|
54293
|
+
if (!bindingSymbol) return null;
|
|
54294
|
+
const registrations = [];
|
|
54295
|
+
const releases = [];
|
|
54296
|
+
for (const reference of bindingSymbol.references) {
|
|
54297
|
+
if (isAstDescendant(reference.identifier, enclosingFunction)) continue;
|
|
54298
|
+
if (reference.identifier === bindingIdentifier) continue;
|
|
54299
|
+
if (reference.flag !== "read") return null;
|
|
54300
|
+
const receivingUse = getCallArgumentUse(reference.identifier);
|
|
54301
|
+
if (!receivingUse) return null;
|
|
54302
|
+
if (isCallExpressionWithSubHandlerCallee(receivingUse.callExpression)) {
|
|
54303
|
+
registrations.push(receivingUse);
|
|
54304
|
+
continue;
|
|
54305
|
+
}
|
|
54306
|
+
const methodName = getStaticMemberCallMethodName(receivingUse.callExpression);
|
|
54307
|
+
if (!methodName || !REGISTRATION_METHOD_BY_RELEASE_METHOD.has(methodName)) return null;
|
|
54308
|
+
releases.push(receivingUse);
|
|
54309
|
+
}
|
|
54310
|
+
if (releases.some((release) => !registrations.some((registration) => isMatchingRegistrationAndRelease(registration, release, context)))) return null;
|
|
54311
|
+
return registrations[0]?.callExpression ?? null;
|
|
51833
54312
|
};
|
|
51834
|
-
const classifyCallableReadsInsideEffect = (
|
|
54313
|
+
const classifyCallableReadsInsideEffect = (callableIdentifier, effectCallback, context) => {
|
|
51835
54314
|
let hasAnyRead = false;
|
|
51836
54315
|
let allReadsAreInSubHandlers = true;
|
|
51837
54316
|
let firstSubHandlerName = null;
|
|
54317
|
+
const callableSymbol = context.scopes.symbolFor(callableIdentifier);
|
|
54318
|
+
if (!callableSymbol) return {
|
|
54319
|
+
hasAnyRead,
|
|
54320
|
+
allReadsAreInSubHandlers,
|
|
54321
|
+
firstSubHandlerName
|
|
54322
|
+
};
|
|
51838
54323
|
walkAst(effectCallback, (child) => {
|
|
51839
54324
|
if (!isNodeOfType(child, "Identifier")) return;
|
|
51840
|
-
if (child
|
|
54325
|
+
if (context.scopes.symbolFor(child)?.id !== callableSymbol.id) return;
|
|
51841
54326
|
const parent = child.parent;
|
|
51842
54327
|
if (isNodeOfType(parent, "ArrayExpression")) return;
|
|
51843
54328
|
if (isNodeOfType(parent, "MemberExpression") && !parent.computed && parent.property === child) return;
|
|
@@ -51848,7 +54333,7 @@ const classifyCallableReadsInsideEffect = (callableName, effectCallback) => {
|
|
|
51848
54333
|
allReadsAreInSubHandlers = false;
|
|
51849
54334
|
return;
|
|
51850
54335
|
}
|
|
51851
|
-
const subHandlerCall =
|
|
54336
|
+
const subHandlerCall = findExclusiveSubHandlerCall(enclosingFunction, context);
|
|
51852
54337
|
if (!subHandlerCall) {
|
|
51853
54338
|
allReadsAreInSubHandlers = false;
|
|
51854
54339
|
return;
|
|
@@ -51891,7 +54376,7 @@ const preferUseEffectEvent = defineRule({
|
|
|
51891
54376
|
const isFunctionTypedPropDep = propStackTracker.isPropName(depName) && REACT_HANDLER_PROP_PATTERN.test(depName);
|
|
51892
54377
|
const isFunctionTypedLocalDep = potentiallyChangingCallbackBindings.has(depName);
|
|
51893
54378
|
if (!isFunctionTypedPropDep && !isFunctionTypedLocalDep) continue;
|
|
51894
|
-
const classification = classifyCallableReadsInsideEffect(
|
|
54379
|
+
const classification = classifyCallableReadsInsideEffect(depElement, callback, context);
|
|
51895
54380
|
if (!classification.hasAnyRead) continue;
|
|
51896
54381
|
if (!classification.allReadsAreInSubHandlers) continue;
|
|
51897
54382
|
const subHandlerLabel = classification.firstSubHandlerName ? `\`${classification.firstSubHandlerName}\`` : "an async sub-handler";
|
|
@@ -53206,12 +55691,7 @@ const DOMPURIFY_MODULES = new Set(["dompurify", "isomorphic-dompurify"]);
|
|
|
53206
55691
|
const REACT_MARKDOWN_NAMED_EXPORTS = new Set(["MarkdownAsync", "MarkdownHooks"]);
|
|
53207
55692
|
const REACT_MARKDOWN_NAMESPACE_EXPORTS = new Set(["default", ...REACT_MARKDOWN_NAMED_EXPORTS]);
|
|
53208
55693
|
const DEFAULT_EXPORT_NAMES = new Set(["default"]);
|
|
53209
|
-
const
|
|
53210
|
-
if (symbol.kind !== "import") return null;
|
|
53211
|
-
const importDeclaration = symbol.declarationNode.parent;
|
|
53212
|
-
return isNodeOfType(importDeclaration, "ImportDeclaration") ? importDeclaration : null;
|
|
53213
|
-
};
|
|
53214
|
-
const isImportFromModule = (symbol, moduleName) => getImportDeclaration(symbol)?.source.value === moduleName;
|
|
55694
|
+
const isImportFromModule = (symbol, moduleName) => getImportDeclarationForSymbol(symbol)?.source.value === moduleName;
|
|
53215
55695
|
const isDefaultImportSymbol = (symbol, moduleName) => {
|
|
53216
55696
|
if (!isImportFromModule(symbol, moduleName)) return false;
|
|
53217
55697
|
return isNodeOfType(symbol.declarationNode, "ImportDefaultSpecifier") || getImportedName(symbol.declarationNode) === "default";
|
|
@@ -53301,7 +55781,7 @@ const getAttributeExpression = (attribute) => {
|
|
|
53301
55781
|
const isDomPurifyNamespace = (node, scopes) => {
|
|
53302
55782
|
const symbol = resolveImportedIdentifier(node, scopes);
|
|
53303
55783
|
if (!symbol) return false;
|
|
53304
|
-
const importDeclaration =
|
|
55784
|
+
const importDeclaration = getImportDeclarationForSymbol(symbol);
|
|
53305
55785
|
if (!importDeclaration || !DOMPURIFY_MODULES.has(String(importDeclaration.source.value))) return false;
|
|
53306
55786
|
return isNodeOfType(symbol.declarationNode, "ImportDefaultSpecifier") || isNodeOfType(symbol.declarationNode, "ImportNamespaceSpecifier") || getImportedName(symbol.declarationNode) === "default";
|
|
53307
55787
|
};
|
|
@@ -56243,16 +58723,6 @@ const rnListCallbackPerRow = defineRule({
|
|
|
56243
58723
|
}
|
|
56244
58724
|
});
|
|
56245
58725
|
//#endregion
|
|
56246
|
-
//#region src/plugin/utils/get-require-call-source.ts
|
|
56247
|
-
const getRequireCallSource = (expression) => {
|
|
56248
|
-
if (isNodeOfType(expression, "MemberExpression")) return getRequireCallSource(expression.object);
|
|
56249
|
-
if (!isNodeOfType(expression, "CallExpression")) return null;
|
|
56250
|
-
if (!isNodeOfType(expression.callee, "Identifier") || expression.callee.name !== "require") return null;
|
|
56251
|
-
const [firstArgument] = expression.arguments ?? [];
|
|
56252
|
-
if (!firstArgument || !isNodeOfType(firstArgument, "Literal")) return null;
|
|
56253
|
-
return typeof firstArgument.value === "string" ? firstArgument.value : null;
|
|
56254
|
-
};
|
|
56255
|
-
//#endregion
|
|
56256
58726
|
//#region src/plugin/utils/get-initializer-module-source.ts
|
|
56257
58727
|
const getInitializerModuleSource = (contextNode, initializer) => {
|
|
56258
58728
|
const requireSource = getRequireCallSource(initializer);
|
|
@@ -56285,7 +58755,7 @@ const resolveImportedRecyclerName = (node, localName, options) => {
|
|
|
56285
58755
|
const jsxMemberObjectName = options?.allowNamespaceMemberAccess ? getJsxMemberObjectName(node) : null;
|
|
56286
58756
|
for (const [canonicalName, packageSources] of Object.entries(RECYCLABLE_LIST_PACKAGES)) {
|
|
56287
58757
|
if (jsxMemberObjectName !== null) {
|
|
56288
|
-
if (localName === canonicalName && packageSources.some((packageSource) => isNamespaceImportFromModule(node, jsxMemberObjectName, packageSource))) return canonicalName;
|
|
58758
|
+
if (localName === canonicalName && packageSources.some((packageSource) => isNamespaceImportFromModule$1(node, jsxMemberObjectName, packageSource))) return canonicalName;
|
|
56289
58759
|
continue;
|
|
56290
58760
|
}
|
|
56291
58761
|
if (packageSources.some((packageSource) => getImportedNameFromModule(node, localName, packageSource) === canonicalName)) return canonicalName;
|
|
@@ -57544,7 +60014,7 @@ const isNamedImportOf = (contextNode, localName, componentName) => {
|
|
|
57544
60014
|
return false;
|
|
57545
60015
|
};
|
|
57546
60016
|
const isExpoUiNamespaceImport = (contextNode, localName) => {
|
|
57547
|
-
for (const moduleSource of EXPO_UI_MODULE_SOURCES) if (isNamespaceImportFromModule(contextNode, localName, moduleSource)) return true;
|
|
60017
|
+
for (const moduleSource of EXPO_UI_MODULE_SOURCES) if (isNamespaceImportFromModule$1(contextNode, localName, moduleSource)) return true;
|
|
57548
60018
|
return false;
|
|
57549
60019
|
};
|
|
57550
60020
|
const isExpoUiComponentElement = (openingElement, contextNode, componentName) => {
|
|
@@ -58692,6 +61162,7 @@ const roleHasRequiredAriaProps = defineRule({
|
|
|
58692
61162
|
recommendation: "Add every required `aria-*` attribute so assistive tech can expose the role's state correctly.",
|
|
58693
61163
|
category: "Accessibility",
|
|
58694
61164
|
create: (context) => ({ JSXOpeningElement(node) {
|
|
61165
|
+
if (isLocalTestScaffoldJsx(node, context)) return;
|
|
58695
61166
|
const elementType = getElementType(node, context.settings);
|
|
58696
61167
|
if (!HTML_TAGS.has(elementType)) return;
|
|
58697
61168
|
const roleAttribute = hasJsxPropIgnoreCase(node.attributes, "role");
|
|
@@ -61827,6 +64298,7 @@ const roleSupportsAriaProps = defineRule({
|
|
|
61827
64298
|
recommendation: "Only use `aria-*` attributes that the element's role supports.",
|
|
61828
64299
|
category: "Accessibility",
|
|
61829
64300
|
create: (context) => ({ JSXOpeningElement(node) {
|
|
64301
|
+
if (isLocalTestScaffoldJsx(node, context)) return;
|
|
61830
64302
|
let ariaAttributes = null;
|
|
61831
64303
|
for (const attribute of node.attributes) {
|
|
61832
64304
|
if (!isNodeOfType(attribute, "JSXAttribute")) continue;
|
|
@@ -62572,6 +65044,109 @@ const isDeferrableSideEffectCall = (objectName, methodName) => {
|
|
|
62572
65044
|
if (ANALYTICS_DEFERRABLE_OBJECTS.has(objectName)) return ANALYTICS_DEFERRABLE_METHODS.has(methodName);
|
|
62573
65045
|
return false;
|
|
62574
65046
|
};
|
|
65047
|
+
const NEXT_SERVER_SOURCE = "next/server";
|
|
65048
|
+
const NEXT_AFTER_EXPORT_NAMES = new Set(["after", "unstable_after"]);
|
|
65049
|
+
const isNextAfterImportSymbol = (symbol, contextNode) => {
|
|
65050
|
+
if (symbol.kind !== "import") return false;
|
|
65051
|
+
const importBinding = getImportBindingForName(contextNode, symbol.name);
|
|
65052
|
+
return Boolean(importBinding && importBinding.source === NEXT_SERVER_SOURCE && !importBinding.isNamespace && importBinding.exportedName && NEXT_AFTER_EXPORT_NAMES.has(importBinding.exportedName));
|
|
65053
|
+
};
|
|
65054
|
+
const isDirectObjectPatternBinding = (symbol) => {
|
|
65055
|
+
if (!isNodeOfType(symbol.declarationNode, "VariableDeclarator")) return false;
|
|
65056
|
+
if (!isNodeOfType(symbol.declarationNode.id, "ObjectPattern")) return false;
|
|
65057
|
+
let bindingNode = symbol.bindingIdentifier;
|
|
65058
|
+
if (isNodeOfType(bindingNode.parent, "AssignmentPattern") && bindingNode.parent.left === bindingNode) bindingNode = bindingNode.parent;
|
|
65059
|
+
const property = bindingNode.parent;
|
|
65060
|
+
return Boolean(isNodeOfType(property, "Property") && property.value === bindingNode && property.parent === symbol.declarationNode.id);
|
|
65061
|
+
};
|
|
65062
|
+
const isNextServerNamespace = (expression, contextNode, scopes) => {
|
|
65063
|
+
let candidate = stripParenExpression(expression);
|
|
65064
|
+
const visitedSymbolIds = /* @__PURE__ */ new Set();
|
|
65065
|
+
while (isNodeOfType(candidate, "Identifier")) {
|
|
65066
|
+
const symbol = scopes.symbolFor(candidate);
|
|
65067
|
+
if (!symbol || visitedSymbolIds.has(symbol.id)) return false;
|
|
65068
|
+
if (symbol.kind === "import") {
|
|
65069
|
+
const importBinding = getImportBindingForName(contextNode, symbol.name);
|
|
65070
|
+
return Boolean(importBinding?.source === NEXT_SERVER_SOURCE && importBinding.isNamespace);
|
|
65071
|
+
}
|
|
65072
|
+
if (symbol.kind !== "const" || !symbol.initializer || !isNodeOfType(symbol.declarationNode, "VariableDeclarator") || symbol.declarationNode.id !== symbol.bindingIdentifier) return false;
|
|
65073
|
+
visitedSymbolIds.add(symbol.id);
|
|
65074
|
+
candidate = stripParenExpression(symbol.initializer);
|
|
65075
|
+
}
|
|
65076
|
+
return false;
|
|
65077
|
+
};
|
|
65078
|
+
const isNextAfterCallee = (callee, contextNode, scopes, visitedSymbolIds = /* @__PURE__ */ new Set()) => {
|
|
65079
|
+
const candidate = stripParenExpression(callee);
|
|
65080
|
+
if (isNodeOfType(candidate, "MemberExpression")) {
|
|
65081
|
+
const propertyName = getStaticPropertyKeyName(candidate, { allowComputedString: true });
|
|
65082
|
+
return Boolean(propertyName && NEXT_AFTER_EXPORT_NAMES.has(propertyName) && isNextServerNamespace(candidate.object, contextNode, scopes));
|
|
65083
|
+
}
|
|
65084
|
+
if (!isNodeOfType(candidate, "Identifier")) return false;
|
|
65085
|
+
const symbol = scopes.symbolFor(candidate);
|
|
65086
|
+
if (!symbol || visitedSymbolIds.has(symbol.id)) return false;
|
|
65087
|
+
if (isNextAfterImportSymbol(symbol, contextNode)) return true;
|
|
65088
|
+
const destructuredPropertyName = getDestructuredBindingPropertyName(symbol.bindingIdentifier);
|
|
65089
|
+
if (symbol.kind === "const" && symbol.initializer && isDirectObjectPatternBinding(symbol) && destructuredPropertyName && NEXT_AFTER_EXPORT_NAMES.has(destructuredPropertyName)) return isNextServerNamespace(symbol.initializer, contextNode, scopes);
|
|
65090
|
+
if (symbol.kind !== "const" || !symbol.initializer || !isNodeOfType(symbol.declarationNode, "VariableDeclarator") || symbol.declarationNode.id !== symbol.bindingIdentifier) return false;
|
|
65091
|
+
visitedSymbolIds.add(symbol.id);
|
|
65092
|
+
return isNextAfterCallee(symbol.initializer, contextNode, scopes, visitedSymbolIds);
|
|
65093
|
+
};
|
|
65094
|
+
const getDirectArgumentCall = (expression) => {
|
|
65095
|
+
const expressionRoot = findTransparentExpressionRoot(expression);
|
|
65096
|
+
const parent = expressionRoot.parent;
|
|
65097
|
+
if (!isNodeOfType(parent, "CallExpression")) return null;
|
|
65098
|
+
return parent.arguments[0] === expressionRoot ? parent : null;
|
|
65099
|
+
};
|
|
65100
|
+
const isScheduledByNextAfter = (expression, scopes) => {
|
|
65101
|
+
const callExpression = getDirectArgumentCall(expression);
|
|
65102
|
+
return Boolean(callExpression && isNextAfterCallee(callExpression.callee, callExpression, scopes));
|
|
65103
|
+
};
|
|
65104
|
+
const getFunctionBindingSymbol = (functionNode, scopes) => {
|
|
65105
|
+
if (isNodeOfType(functionNode, "FunctionDeclaration") && functionNode.id) return scopes.scopeFor(functionNode).symbols.find((symbol) => symbol.declarationNode === functionNode) ?? null;
|
|
65106
|
+
const functionRoot = findTransparentExpressionRoot(functionNode);
|
|
65107
|
+
const parent = functionRoot.parent;
|
|
65108
|
+
if (!isNodeOfType(parent, "VariableDeclarator") || parent.init !== functionRoot || !isNodeOfType(parent.id, "Identifier")) return null;
|
|
65109
|
+
return scopes.symbolFor(parent.id);
|
|
65110
|
+
};
|
|
65111
|
+
const isDirectlyExported = (symbol) => {
|
|
65112
|
+
let declaration = symbol.declarationNode;
|
|
65113
|
+
if (isNodeOfType(declaration, "VariableDeclarator")) declaration = declaration.parent;
|
|
65114
|
+
return Boolean(declaration?.parent && (isNodeOfType(declaration.parent, "ExportNamedDeclaration") || isNodeOfType(declaration.parent, "ExportDefaultDeclaration")));
|
|
65115
|
+
};
|
|
65116
|
+
const isLexicallyInsideFunction = (node, functionNode) => {
|
|
65117
|
+
let enclosingFunction = findEnclosingFunction$1(node);
|
|
65118
|
+
while (enclosingFunction) {
|
|
65119
|
+
if (enclosingFunction === functionNode) return true;
|
|
65120
|
+
enclosingFunction = findEnclosingFunction$1(enclosingFunction);
|
|
65121
|
+
}
|
|
65122
|
+
return false;
|
|
65123
|
+
};
|
|
65124
|
+
const isExclusivelyScheduledByNextAfter = (functionNode, scopes, visitedFunctionSymbolIds) => {
|
|
65125
|
+
if (isScheduledByNextAfter(functionNode, scopes)) return true;
|
|
65126
|
+
const functionSymbol = getFunctionBindingSymbol(functionNode, scopes);
|
|
65127
|
+
if (!functionSymbol || isDirectlyExported(functionSymbol) || visitedFunctionSymbolIds.has(functionSymbol.id)) return false;
|
|
65128
|
+
const nextVisitedFunctionSymbolIds = new Set(visitedFunctionSymbolIds).add(functionSymbol.id);
|
|
65129
|
+
let hasAfterUse = false;
|
|
65130
|
+
for (const reference of functionSymbol.references) {
|
|
65131
|
+
if (reference.flag !== "read") return false;
|
|
65132
|
+
if (isLexicallyInsideFunction(reference.identifier, functionNode)) continue;
|
|
65133
|
+
if (isScheduledByNextAfter(reference.identifier, scopes)) {
|
|
65134
|
+
hasAfterUse = true;
|
|
65135
|
+
continue;
|
|
65136
|
+
}
|
|
65137
|
+
if (!isInsideNextAfterCallback(reference.identifier, scopes, nextVisitedFunctionSymbolIds)) return false;
|
|
65138
|
+
hasAfterUse = true;
|
|
65139
|
+
}
|
|
65140
|
+
return hasAfterUse;
|
|
65141
|
+
};
|
|
65142
|
+
const isInsideNextAfterCallback = (node, scopes, visitedFunctionSymbolIds = /* @__PURE__ */ new Set()) => {
|
|
65143
|
+
let enclosingFunction = findEnclosingFunction$1(node);
|
|
65144
|
+
while (enclosingFunction) {
|
|
65145
|
+
if (isExclusivelyScheduledByNextAfter(enclosingFunction, scopes, visitedFunctionSymbolIds)) return true;
|
|
65146
|
+
enclosingFunction = findEnclosingFunction$1(enclosingFunction);
|
|
65147
|
+
}
|
|
65148
|
+
return false;
|
|
65149
|
+
};
|
|
62575
65150
|
const serverAfterNonblocking = defineRule({
|
|
62576
65151
|
id: "server-after-nonblocking",
|
|
62577
65152
|
title: "Blocking side effect before response",
|
|
@@ -62606,6 +65181,7 @@ const serverAfterNonblocking = defineRule({
|
|
|
62606
65181
|
if (!objectName) return;
|
|
62607
65182
|
const methodName = node.callee.property.name;
|
|
62608
65183
|
if (!isDeferrableSideEffectCall(objectName, methodName)) return;
|
|
65184
|
+
if (isInsideNextAfterCallback(node, context.scopes)) return;
|
|
62609
65185
|
context.report({
|
|
62610
65186
|
node,
|
|
62611
65187
|
message: `${objectName}.${methodName}() runs before the response, so your users wait longer for it.`
|
|
@@ -63031,9 +65607,35 @@ const serverDedupProps = defineRule({
|
|
|
63031
65607
|
});
|
|
63032
65608
|
//#endregion
|
|
63033
65609
|
//#region src/plugin/rules/server/server-fetch-without-revalidate.ts
|
|
63034
|
-
const
|
|
65610
|
+
const isGlobalThisFetchMember = (node, context) => {
|
|
65611
|
+
const memberExpression = stripParenExpression(node);
|
|
65612
|
+
if (!isNodeOfType(memberExpression, "MemberExpression")) return false;
|
|
65613
|
+
const receiver = stripParenExpression(memberExpression.object);
|
|
65614
|
+
return getStaticPropertyName(memberExpression) === "fetch" && isNodeOfType(receiver, "Identifier") && receiver.name === "globalThis" && context.scopes.isGlobalReference(receiver);
|
|
65615
|
+
};
|
|
65616
|
+
const isGlobalThisIdentifier = (node, context) => {
|
|
65617
|
+
const expression = stripParenExpression(node);
|
|
65618
|
+
return isNodeOfType(expression, "Identifier") && expression.name === "globalThis" && context.scopes.isGlobalReference(expression);
|
|
65619
|
+
};
|
|
65620
|
+
const isGlobalFetchDestructuringBinding = (symbolBinding, declaration, context) => isNodeOfType(declaration.id, "ObjectPattern") && Boolean(declaration.id.properties.some((property) => isNodeOfType(property, "Property") && property.value === symbolBinding && getStaticPropertyKeyName(property, { allowComputedString: true }) === "fetch") && declaration.init && isGlobalThisIdentifier(declaration.init, context));
|
|
65621
|
+
const isExactGlobalFetchValue = (node, context, visitedSymbolIds = /* @__PURE__ */ new Set()) => {
|
|
65622
|
+
const expression = stripParenExpression(node);
|
|
65623
|
+
if (isGlobalThisFetchMember(expression, context)) return true;
|
|
65624
|
+
if (!isNodeOfType(expression, "Identifier")) return false;
|
|
65625
|
+
if (expression.name === "fetch" && context.scopes.isGlobalReference(expression)) return true;
|
|
65626
|
+
const symbol = context.scopes.symbolFor(expression);
|
|
65627
|
+
if (!symbol || symbol.kind !== "const" || visitedSymbolIds.has(symbol.id)) return false;
|
|
65628
|
+
if (!isNodeOfType(symbol.declarationNode, "VariableDeclarator")) return false;
|
|
65629
|
+
if (isGlobalFetchDestructuringBinding(symbol.bindingIdentifier, symbol.declarationNode, context)) return true;
|
|
65630
|
+
if (symbol.declarationNode.id !== symbol.bindingIdentifier || !symbol.initializer) return false;
|
|
65631
|
+
visitedSymbolIds.add(symbol.id);
|
|
65632
|
+
return isExactGlobalFetchValue(symbol.initializer, context, visitedSymbolIds);
|
|
65633
|
+
};
|
|
65634
|
+
const isFetchCall = (node, context) => {
|
|
63035
65635
|
if (!isNodeOfType(node, "CallExpression")) return false;
|
|
63036
|
-
|
|
65636
|
+
const callee = stripParenExpression(node.callee);
|
|
65637
|
+
if (!isNodeOfType(callee, "Identifier") || callee.name !== "fetch") return false;
|
|
65638
|
+
return isExactGlobalFetchValue(callee, context);
|
|
63037
65639
|
};
|
|
63038
65640
|
const getPropertyKeyName$1 = (property) => {
|
|
63039
65641
|
if (!isNodeOfType(property, "Property")) return null;
|
|
@@ -63079,7 +65681,7 @@ const serverFetchWithoutRevalidate = defineRule({
|
|
|
63079
65681
|
},
|
|
63080
65682
|
CallExpression(node) {
|
|
63081
65683
|
if (!isServerSideFile) return;
|
|
63082
|
-
if (!isFetchCall(node)) return;
|
|
65684
|
+
if (!isFetchCall(node, context)) return;
|
|
63083
65685
|
if (isMutatingFetchCall(node)) return;
|
|
63084
65686
|
const optionsArg = node.arguments?.[1];
|
|
63085
65687
|
if (optionsArg) {
|
|
@@ -63854,17 +66456,34 @@ const stylePropObject = defineRule({
|
|
|
63854
66456
|
};
|
|
63855
66457
|
}
|
|
63856
66458
|
});
|
|
66459
|
+
//#endregion
|
|
66460
|
+
//#region src/plugin/rules/security-scan/utils/has-use-server-directive-in-content.ts
|
|
66461
|
+
const hasUseServerDirectiveInContent = (content, relativePath = "source.tsx") => {
|
|
66462
|
+
const programNode = parseSourceText({
|
|
66463
|
+
filename: relativePath,
|
|
66464
|
+
sourceText: content,
|
|
66465
|
+
shouldAttachParentReferences: false
|
|
66466
|
+
});
|
|
66467
|
+
return programNode === null ? false : hasDirective(programNode, "use server");
|
|
66468
|
+
};
|
|
66469
|
+
//#endregion
|
|
66470
|
+
//#region src/plugin/rules/security-scan/supabase-client-owned-authz-field.ts
|
|
66471
|
+
const scanSupabaseClientOwnedAuthzField = scanByPattern({
|
|
66472
|
+
shouldScan: (file) => isClientSourcePath(file.relativePath),
|
|
66473
|
+
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/,
|
|
66474
|
+
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],
|
|
66475
|
+
message: "Client Supabase code appears to write user, tenant, owner, or role fields that should be enforced by RLS."
|
|
66476
|
+
});
|
|
63857
66477
|
const supabaseClientOwnedAuthzField = defineRule({
|
|
63858
66478
|
id: "supabase-client-owned-authz-field",
|
|
63859
66479
|
title: "Client writes Supabase authorization field",
|
|
63860
66480
|
severity: "error",
|
|
63861
66481
|
recommendation: "Use RLS policies based on `auth.uid()` and server-owned membership rows; do not trust client-provided owner, org, or role columns.",
|
|
63862
|
-
scan:
|
|
63863
|
-
|
|
63864
|
-
|
|
63865
|
-
|
|
63866
|
-
|
|
63867
|
-
})
|
|
66482
|
+
scan: (file) => {
|
|
66483
|
+
const findings = scanSupabaseClientOwnedAuthzField(file);
|
|
66484
|
+
if (findings.length === 0) return findings;
|
|
66485
|
+
return hasUseServerDirectiveInContent(file.content, file.relativePath) ? [] : findings;
|
|
66486
|
+
}
|
|
63868
66487
|
});
|
|
63869
66488
|
//#endregion
|
|
63870
66489
|
//#region src/plugin/rules/security-scan/utils/is-supabase-migration-path.ts
|