oxlint-plugin-react-doctor 0.6.0 → 0.6.1-dev.f07ee37
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 +217 -190
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -212,8 +212,16 @@ const sliceBelowSourceRoot = (filename) => {
|
|
|
212
212
|
if (cutAt < 0) return filename;
|
|
213
213
|
return filename.slice(cutAt);
|
|
214
214
|
};
|
|
215
|
+
let lastFilename;
|
|
216
|
+
let lastResult = false;
|
|
215
217
|
const isTestlikeFilename = (rawFilename) => {
|
|
216
218
|
if (!rawFilename) return false;
|
|
219
|
+
if (rawFilename === lastFilename) return lastResult;
|
|
220
|
+
lastFilename = rawFilename;
|
|
221
|
+
lastResult = computeIsTestlikeFilename(rawFilename);
|
|
222
|
+
return lastResult;
|
|
223
|
+
};
|
|
224
|
+
const computeIsTestlikeFilename = (rawFilename) => {
|
|
217
225
|
const filename = rawFilename.replaceAll("\\", "/");
|
|
218
226
|
const lastSlash = filename.lastIndexOf("/");
|
|
219
227
|
const basename = lastSlash === -1 ? filename : filename.slice(lastSlash + 1);
|
|
@@ -983,16 +991,8 @@ const buildBindingIndex = (root) => {
|
|
|
983
991
|
}
|
|
984
992
|
}
|
|
985
993
|
}
|
|
986
|
-
const nodeRecord = node;
|
|
987
|
-
for (const key of Object.keys(nodeRecord)) {
|
|
988
|
-
if (key === "parent") continue;
|
|
989
|
-
const child = nodeRecord[key];
|
|
990
|
-
if (Array.isArray(child)) {
|
|
991
|
-
for (const item of child) if (isAstNode(item)) visit(item);
|
|
992
|
-
} else if (isAstNode(child)) visit(child);
|
|
993
|
-
}
|
|
994
994
|
};
|
|
995
|
-
|
|
995
|
+
walkAst(root, visit);
|
|
996
996
|
return out;
|
|
997
997
|
};
|
|
998
998
|
const programRootCache = /* @__PURE__ */ new WeakMap();
|
|
@@ -1423,49 +1423,40 @@ const getElementType = (openingElement, settings) => {
|
|
|
1423
1423
|
//#region src/plugin/utils/find-import-source-for-name.ts
|
|
1424
1424
|
const collectFromProgram = (programRoot) => {
|
|
1425
1425
|
const lookup = /* @__PURE__ */ new Map();
|
|
1426
|
-
const
|
|
1427
|
-
|
|
1428
|
-
|
|
1429
|
-
|
|
1430
|
-
|
|
1431
|
-
|
|
1432
|
-
|
|
1433
|
-
|
|
1434
|
-
|
|
1435
|
-
|
|
1436
|
-
|
|
1437
|
-
|
|
1438
|
-
|
|
1439
|
-
|
|
1440
|
-
|
|
1426
|
+
const bodyStatements = programRoot.body ?? [];
|
|
1427
|
+
for (const node of bodyStatements) {
|
|
1428
|
+
if (node.type !== "ImportDeclaration" || !("source" in node) || !node.source) continue;
|
|
1429
|
+
const source = node.source.value;
|
|
1430
|
+
if (typeof source !== "string") continue;
|
|
1431
|
+
if (!("specifiers" in node) || !Array.isArray(node.specifiers)) continue;
|
|
1432
|
+
for (const specifier of node.specifiers) {
|
|
1433
|
+
if (!("local" in specifier) || !specifier.local) continue;
|
|
1434
|
+
const local = specifier.local;
|
|
1435
|
+
if (typeof local.name !== "string") continue;
|
|
1436
|
+
if (specifier.type === "ImportDefaultSpecifier") lookup.set(local.name, {
|
|
1437
|
+
source,
|
|
1438
|
+
imported: null,
|
|
1439
|
+
isDefault: true,
|
|
1440
|
+
isNamespace: false
|
|
1441
|
+
});
|
|
1442
|
+
else if (specifier.type === "ImportNamespaceSpecifier") lookup.set(local.name, {
|
|
1443
|
+
source,
|
|
1444
|
+
imported: null,
|
|
1445
|
+
isDefault: false,
|
|
1446
|
+
isNamespace: true
|
|
1447
|
+
});
|
|
1448
|
+
else if (specifier.type === "ImportSpecifier") {
|
|
1449
|
+
const importedNode = specifier.imported;
|
|
1450
|
+
const importedName = importedNode?.name ?? (typeof importedNode?.value === "string" ? importedNode.value : null);
|
|
1451
|
+
lookup.set(local.name, {
|
|
1441
1452
|
source,
|
|
1442
|
-
imported:
|
|
1453
|
+
imported: importedName,
|
|
1443
1454
|
isDefault: false,
|
|
1444
|
-
isNamespace:
|
|
1455
|
+
isNamespace: false
|
|
1445
1456
|
});
|
|
1446
|
-
else if (specifier.type === "ImportSpecifier") {
|
|
1447
|
-
const importedNode = specifier.imported;
|
|
1448
|
-
const importedName = importedNode?.name ?? (typeof importedNode?.value === "string" ? importedNode.value : null);
|
|
1449
|
-
lookup.set(local.name, {
|
|
1450
|
-
source,
|
|
1451
|
-
imported: importedName,
|
|
1452
|
-
isDefault: false,
|
|
1453
|
-
isNamespace: false
|
|
1454
|
-
});
|
|
1455
|
-
}
|
|
1456
1457
|
}
|
|
1457
|
-
return;
|
|
1458
|
-
}
|
|
1459
|
-
const nodeRecord = node;
|
|
1460
|
-
for (const key of Object.keys(nodeRecord)) {
|
|
1461
|
-
if (key === "parent") continue;
|
|
1462
|
-
const child = nodeRecord[key];
|
|
1463
|
-
if (Array.isArray(child)) {
|
|
1464
|
-
for (const item of child) if (isAstNode(item)) visit(item);
|
|
1465
|
-
} else if (isAstNode(child)) visit(child);
|
|
1466
1458
|
}
|
|
1467
|
-
}
|
|
1468
|
-
visit(programRoot);
|
|
1459
|
+
}
|
|
1469
1460
|
return lookup;
|
|
1470
1461
|
};
|
|
1471
1462
|
const importLookupCache = /* @__PURE__ */ new WeakMap();
|
|
@@ -1479,6 +1470,12 @@ const getImportLookup = (node) => {
|
|
|
1479
1470
|
}
|
|
1480
1471
|
return cached;
|
|
1481
1472
|
};
|
|
1473
|
+
const hasImportFromModules = (contextNode, moduleSources) => {
|
|
1474
|
+
const lookup = getImportLookup(contextNode);
|
|
1475
|
+
if (!lookup) return false;
|
|
1476
|
+
for (const info of lookup.values()) if (moduleSources.includes(info.source)) return true;
|
|
1477
|
+
return false;
|
|
1478
|
+
};
|
|
1482
1479
|
const isImportedFromModule = (contextNode, localIdentifierName, moduleSource) => {
|
|
1483
1480
|
const lookup = getImportLookup(contextNode);
|
|
1484
1481
|
if (!lookup) return false;
|
|
@@ -1573,7 +1570,6 @@ const LAYOUT_FILE_NAMES = [
|
|
|
1573
1570
|
];
|
|
1574
1571
|
const METADATA_EXPORT_NAMES = ["metadata", "generateMetadata"];
|
|
1575
1572
|
const INTERNAL_PAGE_PATH_PATTERN = /\/(?:(?:\((?:dashboard|admin|settings|account|internal|manage|console|portal|auth|onboarding|app|ee|protected)\))|(?:dashboard|admin|settings|account|internal|manage|console|portal))\//i;
|
|
1576
|
-
const PAGES_DIRECTORY_PATTERN = /\/pages\//;
|
|
1577
1573
|
const NEXTJS_NAVIGATION_FUNCTIONS = new Set([
|
|
1578
1574
|
"redirect",
|
|
1579
1575
|
"permanentRedirect",
|
|
@@ -1591,7 +1587,6 @@ const CACHE_REVALIDATION_FUNCTION_NAMES = new Set([
|
|
|
1591
1587
|
]);
|
|
1592
1588
|
const GOOGLE_FONTS_PATTERN = /fonts\.googleapis\.com/;
|
|
1593
1589
|
const POLYFILL_SCRIPT_PATTERN = /polyfill\.io|polyfill\.min\.js|cdn\.polyfill/;
|
|
1594
|
-
const APP_DIRECTORY_PATTERN = /\/app\//;
|
|
1595
1590
|
const ROUTE_HANDLER_FILE_PATTERN = new RegExp(`/route\\.${NEXTJS_SOURCE_FILE_EXTENSION_GROUP}$`);
|
|
1596
1591
|
const CRON_ROUTE_PATTERN = /\/(?:cron|jobs\/cron)(?:\/|$)/i;
|
|
1597
1592
|
const MUTATING_ROUTE_SEGMENTS = new Set([
|
|
@@ -1628,15 +1623,16 @@ const isNextjsMetadataImageRouteFilename = (rawFilename) => {
|
|
|
1628
1623
|
};
|
|
1629
1624
|
//#endregion
|
|
1630
1625
|
//#region src/plugin/utils/normalize-filename.ts
|
|
1631
|
-
const normalizeFilename
|
|
1626
|
+
const normalizeFilename = (filename) => filename.replaceAll("\\", "/");
|
|
1632
1627
|
//#endregion
|
|
1633
1628
|
//#region src/plugin/utils/is-generated-image-render-context.ts
|
|
1634
1629
|
const IMAGE_RESPONSE_MODULES = ["next/og", "@vercel/og"];
|
|
1635
1630
|
const SATORI_MODULE = "satori";
|
|
1631
|
+
const GENERATED_IMAGE_MODULES = [...IMAGE_RESPONSE_MODULES, SATORI_MODULE];
|
|
1636
1632
|
const generatedImageJsxCache = /* @__PURE__ */ new WeakMap();
|
|
1637
1633
|
const isGeneratedImageRenderFilename = (rawFilename) => {
|
|
1638
1634
|
if (!rawFilename) return false;
|
|
1639
|
-
return isNextjsMetadataImageRouteFilename(normalizeFilename
|
|
1635
|
+
return isNextjsMetadataImageRouteFilename(normalizeFilename(rawFilename));
|
|
1640
1636
|
};
|
|
1641
1637
|
const isImageResponseCallee = (contextNode, callee) => {
|
|
1642
1638
|
if (isNodeOfType(callee, "Identifier")) return IMAGE_RESPONSE_MODULES.some((moduleSource) => getImportedNameFromModule(contextNode, callee.name, moduleSource) === "ImageResponse");
|
|
@@ -1776,7 +1772,7 @@ const collectGeneratedImageJsxNodes = (programRoot) => {
|
|
|
1776
1772
|
const cached = generatedImageJsxCache.get(programRoot);
|
|
1777
1773
|
if (cached) return cached;
|
|
1778
1774
|
const generatedImageJsxNodes = /* @__PURE__ */ new WeakSet();
|
|
1779
|
-
walkAst(programRoot, (descendantNode) => {
|
|
1775
|
+
if (hasImportFromModules(programRoot, GENERATED_IMAGE_MODULES)) walkAst(programRoot, (descendantNode) => {
|
|
1780
1776
|
if (!isGeneratedImageRendererCall(descendantNode)) return;
|
|
1781
1777
|
for (const argument of descendantNode.arguments) markGeneratedImageExpression(argument, programRoot, generatedImageJsxNodes, /* @__PURE__ */ new Set());
|
|
1782
1778
|
});
|
|
@@ -3969,9 +3965,11 @@ const collectScopedReferenceIdentifierNames = (node, into, shadowed) => {
|
|
|
3969
3965
|
return;
|
|
3970
3966
|
}
|
|
3971
3967
|
if (typeof node.type === "string" && node.type.startsWith("TS")) return;
|
|
3972
|
-
|
|
3968
|
+
const nodeRecord = node;
|
|
3969
|
+
for (const key of Object.keys(nodeRecord)) {
|
|
3973
3970
|
if (key === "parent") continue;
|
|
3974
3971
|
if (TYPE_POSITION_CHILD_KEYS.has(key)) continue;
|
|
3972
|
+
const child = nodeRecord[key];
|
|
3975
3973
|
if (Array.isArray(child)) {
|
|
3976
3974
|
for (const item of child) if (isAstNode(item)) collectScopedReferenceIdentifierNames(item, into, shadowed);
|
|
3977
3975
|
} else if (isAstNode(child)) collectScopedReferenceIdentifierNames(child, into, shadowed);
|
|
@@ -4598,7 +4596,7 @@ const asyncParallel = defineRule({
|
|
|
4598
4596
|
severity: "warn",
|
|
4599
4597
|
recommendation: "Use `const [a, b] = await Promise.all([fetchA(), fetchB()])` so independent calls run at the same time",
|
|
4600
4598
|
create: (context) => {
|
|
4601
|
-
const filename = normalizeFilename
|
|
4599
|
+
const filename = normalizeFilename(context.filename ?? "");
|
|
4602
4600
|
const isBrowserTestFile = BROWSER_TEST_FILE_PATTERN.test(filename);
|
|
4603
4601
|
let hasTestLibraryImport = false;
|
|
4604
4602
|
const shouldSkipFile = () => isBrowserTestFile || hasTestLibraryImport;
|
|
@@ -8182,7 +8180,7 @@ const expoNoNonInlinedEnv = defineRule({
|
|
|
8182
8180
|
severity: "warn",
|
|
8183
8181
|
recommendation: "Read env vars with static dotted access (`process.env.EXPO_PUBLIC_NAME`). Computed access and destructuring aren't inlined by babel-preset-expo and resolve to `undefined` at runtime.",
|
|
8184
8182
|
create: (context) => {
|
|
8185
|
-
const filename = normalizeFilename
|
|
8183
|
+
const filename = normalizeFilename(context.filename ?? "");
|
|
8186
8184
|
if (filename && NODE_OR_BUILD_FILE.test(filename)) return EMPTY_VISITORS$3;
|
|
8187
8185
|
return {
|
|
8188
8186
|
MemberExpression(node) {
|
|
@@ -10441,10 +10439,15 @@ const jsCombineIterations = defineRule({
|
|
|
10441
10439
|
severity: "warn",
|
|
10442
10440
|
recommendation: "Combine `.map().filter()` style chains into one pass with `.reduce()` or a `for...of` loop, so you only loop over the list once",
|
|
10443
10441
|
create: (context) => {
|
|
10444
|
-
let
|
|
10442
|
+
let programNode = null;
|
|
10443
|
+
let generatorNamesInFile = null;
|
|
10444
|
+
const getGeneratorNamesInFile = () => {
|
|
10445
|
+
generatorNamesInFile ??= programNode ? collectGeneratorNames(programNode) : /* @__PURE__ */ new Set();
|
|
10446
|
+
return generatorNamesInFile;
|
|
10447
|
+
};
|
|
10445
10448
|
return {
|
|
10446
|
-
Program(
|
|
10447
|
-
|
|
10449
|
+
Program(node) {
|
|
10450
|
+
programNode = node;
|
|
10448
10451
|
},
|
|
10449
10452
|
CallExpression(node) {
|
|
10450
10453
|
if (!isNodeOfType(node.callee, "MemberExpression") || !isNodeOfType(node.callee.property, "Identifier")) return;
|
|
@@ -10466,7 +10469,7 @@ const jsCombineIterations = defineRule({
|
|
|
10466
10469
|
if (isNullFilteringPredicate(filterArgument)) return;
|
|
10467
10470
|
if (isTypePredicateArrow(filterArgument)) return;
|
|
10468
10471
|
}
|
|
10469
|
-
if (isReceiverChainIteratorRooted(innerCall.callee.object,
|
|
10472
|
+
if (isReceiverChainIteratorRooted(innerCall.callee.object, getGeneratorNamesInFile())) return;
|
|
10470
10473
|
if (isSmallLiteralArrayRootedChain(innerCall.callee.object)) return;
|
|
10471
10474
|
if (isStringSplitRootedChain(innerCall.callee.object)) return;
|
|
10472
10475
|
context.report({
|
|
@@ -11617,7 +11620,7 @@ const jsxFilenameExtension = defineRule({
|
|
|
11617
11620
|
create: (context) => {
|
|
11618
11621
|
const settings = resolveSettings$33(context.settings);
|
|
11619
11622
|
const allowedExtensions = normalizeExtensions(settings.extensions);
|
|
11620
|
-
const filename = normalizeFilename
|
|
11623
|
+
const filename = normalizeFilename(context.filename ?? "fixture.tsx");
|
|
11621
11624
|
const extensionOnly = path.extname(filename).slice(1);
|
|
11622
11625
|
const fileHasAllowedExtension = allowedExtensions.has(extensionOnly);
|
|
11623
11626
|
let didReportMismatch = false;
|
|
@@ -15257,6 +15260,52 @@ const nextjsAsyncClientComponent = defineRule({
|
|
|
15257
15260
|
}
|
|
15258
15261
|
});
|
|
15259
15262
|
//#endregion
|
|
15263
|
+
//#region src/plugin/utils/get-project-relative-filename.ts
|
|
15264
|
+
const getProjectRelativeFilename = (filename, rootDirectory) => {
|
|
15265
|
+
const normalizedFilename = normalizeFilename(filename);
|
|
15266
|
+
if (!rootDirectory) return normalizedFilename;
|
|
15267
|
+
const rootDirectoryPrefix = `${normalizeFilename(rootDirectory).replace(/\/+$/, "")}/`;
|
|
15268
|
+
if (!normalizedFilename.startsWith(rootDirectoryPrefix)) return normalizedFilename;
|
|
15269
|
+
return normalizedFilename.slice(rootDirectoryPrefix.length);
|
|
15270
|
+
};
|
|
15271
|
+
//#endregion
|
|
15272
|
+
//#region src/plugin/utils/get-react-doctor-setting.ts
|
|
15273
|
+
const readReactDoctorSettingsBag = (settings) => {
|
|
15274
|
+
const reactDoctorSettings = settings?.["react-doctor"];
|
|
15275
|
+
if (typeof reactDoctorSettings !== "object" || reactDoctorSettings === null || Array.isArray(reactDoctorSettings)) return null;
|
|
15276
|
+
return reactDoctorSettings;
|
|
15277
|
+
};
|
|
15278
|
+
const readOwnPropertyValue = (bag, settingName) => Object.getOwnPropertyDescriptor(bag, settingName)?.value;
|
|
15279
|
+
const getReactDoctorStringSetting = (settings, settingName) => {
|
|
15280
|
+
const bag = readReactDoctorSettingsBag(settings);
|
|
15281
|
+
if (!bag) return void 0;
|
|
15282
|
+
const settingValue = readOwnPropertyValue(bag, settingName);
|
|
15283
|
+
return typeof settingValue === "string" ? settingValue : void 0;
|
|
15284
|
+
};
|
|
15285
|
+
const getReactDoctorNumberSetting = (settings, settingName) => {
|
|
15286
|
+
const bag = readReactDoctorSettingsBag(settings);
|
|
15287
|
+
if (!bag) return void 0;
|
|
15288
|
+
const settingValue = readOwnPropertyValue(bag, settingName);
|
|
15289
|
+
return typeof settingValue === "number" && Number.isFinite(settingValue) ? settingValue : void 0;
|
|
15290
|
+
};
|
|
15291
|
+
const getReactDoctorStringArraySetting = (settings, settingName) => {
|
|
15292
|
+
const bag = readReactDoctorSettingsBag(settings);
|
|
15293
|
+
if (!bag) return [];
|
|
15294
|
+
const settingValue = readOwnPropertyValue(bag, settingName);
|
|
15295
|
+
if (!Array.isArray(settingValue)) return [];
|
|
15296
|
+
return settingValue.filter((entry) => typeof entry === "string" && entry.length > 0);
|
|
15297
|
+
};
|
|
15298
|
+
//#endregion
|
|
15299
|
+
//#region src/plugin/utils/is-in-project-directory.ts
|
|
15300
|
+
const isInProjectDirectory = (context, directoryPath) => {
|
|
15301
|
+
const filename = normalizeFilename(context.filename ?? "");
|
|
15302
|
+
if (filename.length === 0) return false;
|
|
15303
|
+
const directorySegment = `/${directoryPath}/`;
|
|
15304
|
+
const relativeFilename = getProjectRelativeFilename(filename, getReactDoctorStringSetting(context.settings, "rootDirectory"));
|
|
15305
|
+
if (relativeFilename !== filename) return relativeFilename.startsWith(`${directoryPath}/`) || relativeFilename.includes(directorySegment);
|
|
15306
|
+
return filename.indexOf(directorySegment, 1) !== -1;
|
|
15307
|
+
};
|
|
15308
|
+
//#endregion
|
|
15260
15309
|
//#region src/plugin/rules/nextjs/nextjs-error-boundary-missing-use-client.ts
|
|
15261
15310
|
const nextjsErrorBoundaryMissingUseClient = defineRule({
|
|
15262
15311
|
id: "nextjs-error-boundary-missing-use-client",
|
|
@@ -15266,8 +15315,8 @@ const nextjsErrorBoundaryMissingUseClient = defineRule({
|
|
|
15266
15315
|
severity: "error",
|
|
15267
15316
|
recommendation: "Add `'use client'` at the top of this file. Error boundaries must be Client Components to catch and render fallback UI",
|
|
15268
15317
|
create: (context) => ({ Program(programNode) {
|
|
15269
|
-
const filename = normalizeFilename
|
|
15270
|
-
if (!
|
|
15318
|
+
const filename = normalizeFilename(context.filename ?? "");
|
|
15319
|
+
if (!isInProjectDirectory(context, "app")) return;
|
|
15271
15320
|
if (!ERROR_BOUNDARY_FILE_PATTERN.test(filename)) return;
|
|
15272
15321
|
if (hasDirective(programNode, "use client")) return;
|
|
15273
15322
|
context.report({
|
|
@@ -15298,8 +15347,8 @@ const nextjsGlobalErrorMissingHtmlBody = defineRule({
|
|
|
15298
15347
|
severity: "error",
|
|
15299
15348
|
recommendation: "Wrap your error UI in `<html><body>...</body></html>`. The root layout is unmounted when global-error renders",
|
|
15300
15349
|
create: (context) => ({ Program(programNode) {
|
|
15301
|
-
const filename = normalizeFilename
|
|
15302
|
-
if (!
|
|
15350
|
+
const filename = normalizeFilename(context.filename ?? "");
|
|
15351
|
+
if (!isInProjectDirectory(context, "app")) return;
|
|
15303
15352
|
if (!GLOBAL_ERROR_FILE_PATTERN.test(filename)) return;
|
|
15304
15353
|
const foundTags = fileContainsJsxElements(programNode, REQUIRED_HTML_TAGS);
|
|
15305
15354
|
const missingTags = REQUIRED_HTML_TAGS.filter((tagName) => !foundTags.has(tagName)).map((tagName) => `<${tagName}>`);
|
|
@@ -15430,7 +15479,7 @@ const nextjsMissingMetadata = defineRule({
|
|
|
15430
15479
|
severity: "warn",
|
|
15431
15480
|
recommendation: "Add metadata or `generateMetadata()` so search engines and social previews get a title and description.",
|
|
15432
15481
|
create: (context) => ({ Program(programNode) {
|
|
15433
|
-
const filename = normalizeFilename
|
|
15482
|
+
const filename = normalizeFilename(context.filename ?? "");
|
|
15434
15483
|
if (!PAGE_FILE_PATTERN.test(filename)) return;
|
|
15435
15484
|
if (INTERNAL_PAGE_PATH_PATTERN.test(filename)) return;
|
|
15436
15485
|
if (programNode.body?.some((statement) => {
|
|
@@ -15563,8 +15612,8 @@ const nextjsNoClientFetchForServerData = defineRule({
|
|
|
15563
15612
|
if (!fileHasUseClient || !isHookCall$1(node, EFFECT_HOOK_NAMES$1)) return;
|
|
15564
15613
|
const callback = getEffectCallback(node);
|
|
15565
15614
|
if (!callback || !containsFetchCall(callback, { stopAtFunctionBoundary: true })) return;
|
|
15566
|
-
const filename = normalizeFilename
|
|
15567
|
-
if (PAGE_OR_LAYOUT_FILE_PATTERN.test(filename) ||
|
|
15615
|
+
const filename = normalizeFilename(context.filename ?? "");
|
|
15616
|
+
if (PAGE_OR_LAYOUT_FILE_PATTERN.test(filename) || isInProjectDirectory(context, "pages")) context.report({
|
|
15568
15617
|
node,
|
|
15569
15618
|
message: "useEffect + fetch in a page/layout makes your users wait through an extra round trip & loading spinner."
|
|
15570
15619
|
});
|
|
@@ -15663,8 +15712,8 @@ const nextjsNoDefaultExportInRouteHandler = defineRule({
|
|
|
15663
15712
|
let programNode = null;
|
|
15664
15713
|
return {
|
|
15665
15714
|
Program(node) {
|
|
15666
|
-
const filename = normalizeFilename
|
|
15667
|
-
isAppRouteHandler =
|
|
15715
|
+
const filename = normalizeFilename(context.filename ?? "");
|
|
15716
|
+
isAppRouteHandler = isInProjectDirectory(context, "app") && ROUTE_HANDLER_FILE_PATTERN.test(filename);
|
|
15668
15717
|
programNode = node;
|
|
15669
15718
|
},
|
|
15670
15719
|
ExportDefaultDeclaration(node) {
|
|
@@ -15700,7 +15749,7 @@ const nextjsNoEdgeOgRuntime = defineRule({
|
|
|
15700
15749
|
let isOgImageFile = false;
|
|
15701
15750
|
return {
|
|
15702
15751
|
Program() {
|
|
15703
|
-
const filename = normalizeFilename
|
|
15752
|
+
const filename = normalizeFilename(context.filename ?? "");
|
|
15704
15753
|
isOgImageFile = OG_IMAGE_FILE_PATTERN.test(filename);
|
|
15705
15754
|
},
|
|
15706
15755
|
ExportNamedDeclaration(node) {
|
|
@@ -15776,8 +15825,7 @@ const nextjsNoHeadImport = defineRule({
|
|
|
15776
15825
|
recommendation: "Use the Metadata API because `next/head` is ignored in the App Router and meta tags will not render.",
|
|
15777
15826
|
create: (context) => ({ ImportDeclaration(node) {
|
|
15778
15827
|
if (node.source?.value !== "next/head") return;
|
|
15779
|
-
|
|
15780
|
-
if (!APP_DIRECTORY_PATTERN.test(filename)) return;
|
|
15828
|
+
if (!isInProjectDirectory(context, "app")) return;
|
|
15781
15829
|
context.report({
|
|
15782
15830
|
node,
|
|
15783
15831
|
message: "next/head silently does nothing in the App Router, so your meta tags never render."
|
|
@@ -16235,7 +16283,7 @@ const nextjsNoSideEffectInGetHandler = defineRule({
|
|
|
16235
16283
|
resolveBinding = buildProgramBindingLookup(node);
|
|
16236
16284
|
},
|
|
16237
16285
|
ExportNamedDeclaration(node) {
|
|
16238
|
-
const filename = normalizeFilename
|
|
16286
|
+
const filename = normalizeFilename(context.filename ?? "");
|
|
16239
16287
|
if (!ROUTE_HANDLER_FILE_PATTERN.test(filename)) return;
|
|
16240
16288
|
if (CRON_ROUTE_PATTERN.test(filename)) return;
|
|
16241
16289
|
if (!isExportedGetHandler(node)) return;
|
|
@@ -17014,7 +17062,7 @@ const nextjsNoUseSearchParamsWithoutSuspense = defineRule({
|
|
|
17014
17062
|
let suspenseLocalNames = /* @__PURE__ */ new Set();
|
|
17015
17063
|
return {
|
|
17016
17064
|
Program(programNode) {
|
|
17017
|
-
const filename = normalizeFilename
|
|
17065
|
+
const filename = normalizeFilename(context.filename ?? "");
|
|
17018
17066
|
isPageOrLayoutFile = PAGE_OR_LAYOUT_FILE_PATTERN.test(filename);
|
|
17019
17067
|
if (!isPageOrLayoutFile) return;
|
|
17020
17068
|
hasAncestorLayoutSuspense = hasAncestorSuspenseLayout(context.filename ?? "");
|
|
@@ -17386,11 +17434,19 @@ const isSynchronous = (node, within) => {
|
|
|
17386
17434
|
if (isNodeOfType(node, "AwaitExpression") || isNodeOfType(node, "UnaryExpression") && node.operator === "void" || isNodeOfType(node, "FunctionDeclaration") || isNodeOfType(node, "FunctionExpression") || isNodeOfType(node, "ArrowFunctionExpression")) return false;
|
|
17387
17435
|
return isSynchronous(node.parent, within);
|
|
17388
17436
|
};
|
|
17437
|
+
const unwrapUseCallback = (node) => {
|
|
17438
|
+
if (!node || !isNodeOfType(node, "CallExpression")) return node;
|
|
17439
|
+
const callee = node.callee;
|
|
17440
|
+
return isNodeOfType(callee, "Identifier") && callee.name === "useCallback" || isNodeOfType(callee, "MemberExpression") && isNodeOfType(callee.property, "Identifier") && callee.property.name === "useCallback" ? node.arguments?.[0] : node;
|
|
17441
|
+
};
|
|
17389
17442
|
const resolveToFunction = (ref) => {
|
|
17390
17443
|
const definitionNode = ref.resolved?.defs[0]?.node;
|
|
17391
17444
|
if (!definitionNode) return null;
|
|
17392
17445
|
if (isFunctionLike$1(definitionNode)) return definitionNode;
|
|
17393
|
-
if (isNodeOfType(definitionNode, "VariableDeclarator")
|
|
17446
|
+
if (isNodeOfType(definitionNode, "VariableDeclarator")) {
|
|
17447
|
+
const initializer = unwrapUseCallback(definitionNode.init);
|
|
17448
|
+
if (isFunctionLike$1(initializer)) return initializer;
|
|
17449
|
+
}
|
|
17394
17450
|
return null;
|
|
17395
17451
|
};
|
|
17396
17452
|
const resolvesToAsyncFunction = (ref) => Boolean(resolveToFunction(ref)?.async);
|
|
@@ -18658,40 +18714,13 @@ const classifyPackagePlatform = (filename) => {
|
|
|
18658
18714
|
return result;
|
|
18659
18715
|
};
|
|
18660
18716
|
//#endregion
|
|
18661
|
-
//#region src/plugin/utils/get-react-doctor-setting.ts
|
|
18662
|
-
const readReactDoctorSettingsBag = (settings) => {
|
|
18663
|
-
const reactDoctorSettings = settings?.["react-doctor"];
|
|
18664
|
-
if (typeof reactDoctorSettings !== "object" || reactDoctorSettings === null || Array.isArray(reactDoctorSettings)) return null;
|
|
18665
|
-
return reactDoctorSettings;
|
|
18666
|
-
};
|
|
18667
|
-
const readOwnPropertyValue = (bag, settingName) => Object.getOwnPropertyDescriptor(bag, settingName)?.value;
|
|
18668
|
-
const getReactDoctorStringSetting = (settings, settingName) => {
|
|
18669
|
-
const bag = readReactDoctorSettingsBag(settings);
|
|
18670
|
-
if (!bag) return void 0;
|
|
18671
|
-
const settingValue = readOwnPropertyValue(bag, settingName);
|
|
18672
|
-
return typeof settingValue === "string" ? settingValue : void 0;
|
|
18673
|
-
};
|
|
18674
|
-
const getReactDoctorNumberSetting = (settings, settingName) => {
|
|
18675
|
-
const bag = readReactDoctorSettingsBag(settings);
|
|
18676
|
-
if (!bag) return void 0;
|
|
18677
|
-
const settingValue = readOwnPropertyValue(bag, settingName);
|
|
18678
|
-
return typeof settingValue === "number" && Number.isFinite(settingValue) ? settingValue : void 0;
|
|
18679
|
-
};
|
|
18680
|
-
const getReactDoctorStringArraySetting = (settings, settingName) => {
|
|
18681
|
-
const bag = readReactDoctorSettingsBag(settings);
|
|
18682
|
-
if (!bag) return [];
|
|
18683
|
-
const settingValue = readOwnPropertyValue(bag, settingName);
|
|
18684
|
-
if (!Array.isArray(settingValue)) return [];
|
|
18685
|
-
return settingValue.filter((entry) => typeof entry === "string" && entry.length > 0);
|
|
18686
|
-
};
|
|
18687
|
-
//#endregion
|
|
18688
18717
|
//#region src/plugin/utils/is-react-native-file.ts
|
|
18689
18718
|
const WEB_FILE_EXTENSION_PATTERN = /\.web\.[cm]?[jt]sx?$/;
|
|
18690
18719
|
const NATIVE_FILE_EXTENSION_PATTERN = /\.(?:ios|android|native)\.[cm]?[jt]sx?$/;
|
|
18691
18720
|
const classifyReactNativeFileTarget = (context) => {
|
|
18692
18721
|
const rawFilename = context.filename;
|
|
18693
18722
|
if (!rawFilename) return "unknown";
|
|
18694
|
-
const filename = normalizeFilename
|
|
18723
|
+
const filename = normalizeFilename(rawFilename);
|
|
18695
18724
|
if (NATIVE_FILE_EXTENSION_PATTERN.test(filename)) return "react-native";
|
|
18696
18725
|
if (WEB_FILE_EXTENSION_PATTERN.test(filename)) return "web";
|
|
18697
18726
|
const packagePlatform = classifyPackagePlatform(filename);
|
|
@@ -18748,7 +18777,7 @@ const noBarrelImport = defineRule({
|
|
|
18748
18777
|
if (didReportForFile) return;
|
|
18749
18778
|
const source = node.source?.value;
|
|
18750
18779
|
if (typeof source !== "string" || !source.startsWith(".")) return;
|
|
18751
|
-
const filename = normalizeFilename
|
|
18780
|
+
const filename = normalizeFilename(context.filename ?? "");
|
|
18752
18781
|
if (!filename) return;
|
|
18753
18782
|
const importRequests = getRuntimeImportRequests(node);
|
|
18754
18783
|
if (importRequests.length === 0) return;
|
|
@@ -26219,15 +26248,6 @@ const CLIENT_APP_DIRECTORY_FRAMEWORKS = new Set([
|
|
|
26219
26248
|
"vite"
|
|
26220
26249
|
]);
|
|
26221
26250
|
const isInsideDirectory = (pathSegments, directoryNames) => pathSegments.some((pathSegment) => directoryNames.has(pathSegment));
|
|
26222
|
-
const normalizeFilename = (filename) => filename.replaceAll("\\", "/");
|
|
26223
|
-
const normalizeDirectory = (directory) => normalizeFilename(directory).replace(/\/+$/, "");
|
|
26224
|
-
const getProjectRelativeFilename = (filename, rootDirectory) => {
|
|
26225
|
-
const normalizedFilename = normalizeFilename(filename);
|
|
26226
|
-
if (!rootDirectory) return normalizedFilename;
|
|
26227
|
-
const rootDirectoryPrefix = `${normalizeDirectory(rootDirectory)}/`;
|
|
26228
|
-
if (!normalizedFilename.startsWith(rootDirectoryPrefix)) return normalizedFilename;
|
|
26229
|
-
return normalizedFilename.slice(rootDirectoryPrefix.length);
|
|
26230
|
-
};
|
|
26231
26251
|
const getClassifiablePathSegments = (pathSegments) => {
|
|
26232
26252
|
const srcIndex = pathSegments.lastIndexOf("src");
|
|
26233
26253
|
if (srcIndex === -1) return pathSegments;
|
|
@@ -26289,7 +26309,6 @@ const tokenizeIdentifierWords = (identifierName) => {
|
|
|
26289
26309
|
const getIdentifierTrailingWord = (identifierName) => tokenizeIdentifierWords(identifierName).at(-1) ?? identifierName.toLowerCase();
|
|
26290
26310
|
//#endregion
|
|
26291
26311
|
//#region src/plugin/constants/tanstack.ts
|
|
26292
|
-
const TANSTACK_ROUTE_FILE_PATTERN = /\/routes\//;
|
|
26293
26312
|
const TANSTACK_ROOT_ROUTE_FILE_PATTERN = /__root\.(tsx?|jsx?)$/;
|
|
26294
26313
|
const TANSTACK_ROUTE_PROPERTY_ORDER = [
|
|
26295
26314
|
"params",
|
|
@@ -26401,7 +26420,7 @@ const noSecretsInClientCode = defineRule({
|
|
|
26401
26420
|
severity: "warn",
|
|
26402
26421
|
recommendation: "Move secrets to server-only code. Anything in client env variables gets shipped to the browser, so it can't hold secrets.",
|
|
26403
26422
|
create: (context) => {
|
|
26404
|
-
const filename = normalizeFilename
|
|
26423
|
+
const filename = normalizeFilename(context.filename ?? "");
|
|
26405
26424
|
const framework = getReactDoctorStringSetting(context.settings, "framework");
|
|
26406
26425
|
const rootDirectory = getReactDoctorStringSetting(context.settings, "rootDirectory");
|
|
26407
26426
|
let shouldUseVariableNameHeuristic = classifySecretFileExposure(filename, {
|
|
@@ -29507,21 +29526,18 @@ const classifyExport = (name, reportNode, isFunction, initializer, state) => {
|
|
|
29507
29526
|
reportNode
|
|
29508
29527
|
};
|
|
29509
29528
|
};
|
|
29510
|
-
const
|
|
29511
|
-
const
|
|
29512
|
-
const
|
|
29513
|
-
|
|
29514
|
-
const
|
|
29515
|
-
|
|
29516
|
-
|
|
29517
|
-
|
|
29518
|
-
|
|
29519
|
-
|
|
29520
|
-
|
|
29521
|
-
}
|
|
29529
|
+
const collectRelevantNodes = (programRoot) => {
|
|
29530
|
+
const exportNodes = [];
|
|
29531
|
+
const componentCandidates = [];
|
|
29532
|
+
walkAst(programRoot, (child) => {
|
|
29533
|
+
const childType = child.type;
|
|
29534
|
+
if (childType === "ExportAllDeclaration" || childType === "ExportDefaultDeclaration" || childType === "ExportNamedDeclaration") exportNodes.push(child);
|
|
29535
|
+
else if (childType === "FunctionDeclaration" || childType === "VariableDeclarator") componentCandidates.push(child);
|
|
29536
|
+
});
|
|
29537
|
+
return {
|
|
29538
|
+
exportNodes,
|
|
29539
|
+
componentCandidates
|
|
29522
29540
|
};
|
|
29523
|
-
visit(programRoot);
|
|
29524
|
-
return out;
|
|
29525
29541
|
};
|
|
29526
29542
|
const isEntryPointFile = (filename) => {
|
|
29527
29543
|
const lastSlash = Math.max(filename.lastIndexOf("/"), filename.lastIndexOf("\\"));
|
|
@@ -29567,14 +29583,14 @@ const onlyExportComponents = defineRule({
|
|
|
29567
29583
|
allowConstantExport: settings.allowConstantExport
|
|
29568
29584
|
};
|
|
29569
29585
|
return { Program(node) {
|
|
29570
|
-
if (!isFileNameAllowed(normalizeFilename
|
|
29571
|
-
const
|
|
29586
|
+
if (!isFileNameAllowed(normalizeFilename(context.filename ?? ""), settings.checkJS)) return;
|
|
29587
|
+
const { exportNodes, componentCandidates } = collectRelevantNodes(node);
|
|
29572
29588
|
const exports = [];
|
|
29573
29589
|
let hasReactExport = false;
|
|
29574
29590
|
let hasAnyExports = false;
|
|
29575
29591
|
const localComponents = [];
|
|
29576
29592
|
const isExportedNodeIds = /* @__PURE__ */ new WeakSet();
|
|
29577
|
-
for (const child of
|
|
29593
|
+
for (const child of exportNodes) {
|
|
29578
29594
|
if (isNodeOfType(child, "ExportAllDeclaration")) {
|
|
29579
29595
|
if (child.exportKind === "type") continue;
|
|
29580
29596
|
hasAnyExports = true;
|
|
@@ -29710,7 +29726,7 @@ const onlyExportComponents = defineRule({
|
|
|
29710
29726
|
}
|
|
29711
29727
|
return false;
|
|
29712
29728
|
};
|
|
29713
|
-
for (const child of
|
|
29729
|
+
for (const child of componentCandidates) {
|
|
29714
29730
|
if (isNodeOfType(child, "FunctionDeclaration") && child.id) {
|
|
29715
29731
|
if (isReactComponentName(child.id.name) && !isInsideExport(child)) localComponents.push(child.id);
|
|
29716
29732
|
}
|
|
@@ -32001,6 +32017,7 @@ const renderingHydrationMismatchTime = defineRule({
|
|
|
32001
32017
|
});
|
|
32002
32018
|
//#endregion
|
|
32003
32019
|
//#region src/plugin/rules/performance/rendering-hydration-no-flicker.ts
|
|
32020
|
+
const USE_EFFECT_ONLY = new Set(["useEffect"]);
|
|
32004
32021
|
const renderingHydrationNoFlicker = defineRule({
|
|
32005
32022
|
id: "rendering-hydration-no-flicker",
|
|
32006
32023
|
title: "useEffect setState flashes on mount",
|
|
@@ -32008,7 +32025,7 @@ const renderingHydrationNoFlicker = defineRule({
|
|
|
32008
32025
|
severity: "warn",
|
|
32009
32026
|
recommendation: "Use `useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot)` or add `suppressHydrationWarning` to the element",
|
|
32010
32027
|
create: (context) => ({ CallExpression(node) {
|
|
32011
|
-
if (!isHookCall$1(node,
|
|
32028
|
+
if (!isHookCall$1(node, USE_EFFECT_ONLY) || (node.arguments?.length ?? 0) < 2) return;
|
|
32012
32029
|
const depsNode = node.arguments[1];
|
|
32013
32030
|
if (!isNodeOfType(depsNode, "ArrayExpression") || depsNode.elements?.length !== 0) return;
|
|
32014
32031
|
const callback = getEffectCallback(node);
|
|
@@ -32081,7 +32098,7 @@ const renderingSvgPrecision = defineRule({
|
|
|
32081
32098
|
recommendation: "Round path, points, and transform decimals to 1 or 2 digits. The extra precision adds bytes with no visible difference.",
|
|
32082
32099
|
create: (context) => {
|
|
32083
32100
|
const filename = context.filename;
|
|
32084
|
-
const isAutoGenerated = isAutoGeneratedSvgFile(filename ? normalizeFilename
|
|
32101
|
+
const isAutoGenerated = isAutoGeneratedSvgFile(filename ? normalizeFilename(filename) : void 0);
|
|
32085
32102
|
return { JSXAttribute(node) {
|
|
32086
32103
|
if (isAutoGenerated) return;
|
|
32087
32104
|
if (!isNodeOfType(node.name, "JSXIdentifier")) return;
|
|
@@ -33231,7 +33248,7 @@ const rnDetoxMissingAwait = defineRule({
|
|
|
33231
33248
|
severity: "warn",
|
|
33232
33249
|
recommendation: "Prepend `await` to Detox actions, `waitFor(...)` chains, and `expect(element(...))` assertions. They return promises tied to Detox's synchronization, so a missing await runs steps out of order.",
|
|
33233
33250
|
create: (context) => {
|
|
33234
|
-
const filename = normalizeFilename
|
|
33251
|
+
const filename = normalizeFilename(context.filename ?? "");
|
|
33235
33252
|
if (!filename || !DETOX_TEST_FILE.test(filename)) return EMPTY_VISITORS$2;
|
|
33236
33253
|
return { ExpressionStatement(node) {
|
|
33237
33254
|
const expression = node.expression;
|
|
@@ -34961,7 +34978,7 @@ const rnPreferContentInsetAdjustment = defineRetiredRule({
|
|
|
34961
34978
|
//#region src/plugin/utils/is-expo-managed-file.ts
|
|
34962
34979
|
const isExpoManagedFileActive = (context) => {
|
|
34963
34980
|
const rawFilename = context.filename;
|
|
34964
|
-
const filename = rawFilename ? normalizeFilename
|
|
34981
|
+
const filename = rawFilename ? normalizeFilename(rawFilename) : void 0;
|
|
34965
34982
|
if (filename) {
|
|
34966
34983
|
const packagePlatform = classifyPackagePlatform(filename);
|
|
34967
34984
|
if (packagePlatform === "expo") return true;
|
|
@@ -35735,6 +35752,7 @@ const ROLE_SUPPORTS_ARIA_PROPS = {
|
|
|
35735
35752
|
"aria-labelledby",
|
|
35736
35753
|
"aria-live",
|
|
35737
35754
|
"aria-owns",
|
|
35755
|
+
"aria-readonly",
|
|
35738
35756
|
"aria-relevant",
|
|
35739
35757
|
"aria-required",
|
|
35740
35758
|
"aria-roledescription"
|
|
@@ -35785,6 +35803,7 @@ const ROLE_SUPPORTS_ARIA_PROPS = {
|
|
|
35785
35803
|
"aria-labelledby",
|
|
35786
35804
|
"aria-live",
|
|
35787
35805
|
"aria-owns",
|
|
35806
|
+
"aria-readonly",
|
|
35788
35807
|
"aria-relevant",
|
|
35789
35808
|
"aria-required",
|
|
35790
35809
|
"aria-roledescription",
|
|
@@ -35816,6 +35835,7 @@ const ROLE_SUPPORTS_ARIA_PROPS = {
|
|
|
35816
35835
|
"aria-labelledby",
|
|
35817
35836
|
"aria-live",
|
|
35818
35837
|
"aria-owns",
|
|
35838
|
+
"aria-readonly",
|
|
35819
35839
|
"aria-relevant",
|
|
35820
35840
|
"aria-required",
|
|
35821
35841
|
"aria-roledescription"
|
|
@@ -37114,7 +37134,9 @@ const ROLE_SUPPORTS_ARIA_PROPS = {
|
|
|
37114
37134
|
"aria-label",
|
|
37115
37135
|
"aria-labelledby",
|
|
37116
37136
|
"aria-live",
|
|
37137
|
+
"aria-multiselectable",
|
|
37117
37138
|
"aria-owns",
|
|
37139
|
+
"aria-readonly",
|
|
37118
37140
|
"aria-relevant",
|
|
37119
37141
|
"aria-roledescription",
|
|
37120
37142
|
"aria-rowcount"
|
|
@@ -37142,6 +37164,7 @@ const ROLE_SUPPORTS_ARIA_PROPS = {
|
|
|
37142
37164
|
"aria-labelledby",
|
|
37143
37165
|
"aria-live",
|
|
37144
37166
|
"aria-owns",
|
|
37167
|
+
"aria-readonly",
|
|
37145
37168
|
"aria-relevant",
|
|
37146
37169
|
"aria-required",
|
|
37147
37170
|
"aria-roledescription",
|
|
@@ -37331,8 +37354,10 @@ const ROLE_SUPPORTS_ARIA_PROPS = {
|
|
|
37331
37354
|
"aria-label",
|
|
37332
37355
|
"aria-labelledby",
|
|
37333
37356
|
"aria-live",
|
|
37357
|
+
"aria-multiselectable",
|
|
37334
37358
|
"aria-orientation",
|
|
37335
37359
|
"aria-owns",
|
|
37360
|
+
"aria-readonly",
|
|
37336
37361
|
"aria-relevant",
|
|
37337
37362
|
"aria-required",
|
|
37338
37363
|
"aria-roledescription"
|
|
@@ -37548,6 +37573,7 @@ const ROLE_SUPPORTS_ARIA_PROPS = {
|
|
|
37548
37573
|
"aria-live",
|
|
37549
37574
|
"aria-owns",
|
|
37550
37575
|
"aria-posinset",
|
|
37576
|
+
"aria-readonly",
|
|
37551
37577
|
"aria-relevant",
|
|
37552
37578
|
"aria-required",
|
|
37553
37579
|
"aria-roledescription",
|
|
@@ -37576,6 +37602,7 @@ const ROLE_SUPPORTS_ARIA_PROPS = {
|
|
|
37576
37602
|
"aria-live",
|
|
37577
37603
|
"aria-owns",
|
|
37578
37604
|
"aria-posinset",
|
|
37605
|
+
"aria-readonly",
|
|
37579
37606
|
"aria-relevant",
|
|
37580
37607
|
"aria-required",
|
|
37581
37608
|
"aria-roledescription",
|
|
@@ -37778,6 +37805,7 @@ const ROLE_SUPPORTS_ARIA_PROPS = {
|
|
|
37778
37805
|
"aria-live",
|
|
37779
37806
|
"aria-orientation",
|
|
37780
37807
|
"aria-owns",
|
|
37808
|
+
"aria-readonly",
|
|
37781
37809
|
"aria-relevant",
|
|
37782
37810
|
"aria-required",
|
|
37783
37811
|
"aria-roledescription"
|
|
@@ -37912,6 +37940,7 @@ const ROLE_SUPPORTS_ARIA_PROPS = {
|
|
|
37912
37940
|
"aria-labelledby",
|
|
37913
37941
|
"aria-live",
|
|
37914
37942
|
"aria-owns",
|
|
37943
|
+
"aria-readonly",
|
|
37915
37944
|
"aria-relevant",
|
|
37916
37945
|
"aria-required",
|
|
37917
37946
|
"aria-roledescription",
|
|
@@ -37988,6 +38017,7 @@ const ROLE_SUPPORTS_ARIA_PROPS = {
|
|
|
37988
38017
|
"aria-multiline",
|
|
37989
38018
|
"aria-owns",
|
|
37990
38019
|
"aria-placeholder",
|
|
38020
|
+
"aria-readonly",
|
|
37991
38021
|
"aria-relevant",
|
|
37992
38022
|
"aria-required",
|
|
37993
38023
|
"aria-roledescription"
|
|
@@ -38098,10 +38128,11 @@ const ROLE_SUPPORTS_ARIA_PROPS = {
|
|
|
38098
38128
|
"aria-live",
|
|
38099
38129
|
"aria-orientation",
|
|
38100
38130
|
"aria-owns",
|
|
38131
|
+
"aria-readonly",
|
|
38101
38132
|
"aria-relevant",
|
|
38102
38133
|
"aria-roledescription",
|
|
38103
|
-
"aria-valuemin",
|
|
38104
38134
|
"aria-valuemax",
|
|
38135
|
+
"aria-valuemin",
|
|
38105
38136
|
"aria-valuenow",
|
|
38106
38137
|
"aria-valuetext"
|
|
38107
38138
|
]),
|
|
@@ -38125,6 +38156,7 @@ const ROLE_SUPPORTS_ARIA_PROPS = {
|
|
|
38125
38156
|
"aria-labelledby",
|
|
38126
38157
|
"aria-live",
|
|
38127
38158
|
"aria-owns",
|
|
38159
|
+
"aria-readonly",
|
|
38128
38160
|
"aria-relevant",
|
|
38129
38161
|
"aria-required",
|
|
38130
38162
|
"aria-roledescription",
|
|
@@ -38258,6 +38290,7 @@ const ROLE_SUPPORTS_ARIA_PROPS = {
|
|
|
38258
38290
|
"aria-labelledby",
|
|
38259
38291
|
"aria-live",
|
|
38260
38292
|
"aria-owns",
|
|
38293
|
+
"aria-readonly",
|
|
38261
38294
|
"aria-relevant",
|
|
38262
38295
|
"aria-required",
|
|
38263
38296
|
"aria-roledescription"
|
|
@@ -38326,6 +38359,7 @@ const ROLE_SUPPORTS_ARIA_PROPS = {
|
|
|
38326
38359
|
"aria-labelledby",
|
|
38327
38360
|
"aria-level",
|
|
38328
38361
|
"aria-live",
|
|
38362
|
+
"aria-multiselectable",
|
|
38329
38363
|
"aria-orientation",
|
|
38330
38364
|
"aria-owns",
|
|
38331
38365
|
"aria-relevant",
|
|
@@ -38393,6 +38427,7 @@ const ROLE_SUPPORTS_ARIA_PROPS = {
|
|
|
38393
38427
|
"aria-multiline",
|
|
38394
38428
|
"aria-owns",
|
|
38395
38429
|
"aria-placeholder",
|
|
38430
|
+
"aria-readonly",
|
|
38396
38431
|
"aria-relevant",
|
|
38397
38432
|
"aria-required",
|
|
38398
38433
|
"aria-roledescription"
|
|
@@ -38495,6 +38530,7 @@ const ROLE_SUPPORTS_ARIA_PROPS = {
|
|
|
38495
38530
|
"aria-label",
|
|
38496
38531
|
"aria-labelledby",
|
|
38497
38532
|
"aria-live",
|
|
38533
|
+
"aria-multiselectable",
|
|
38498
38534
|
"aria-orientation",
|
|
38499
38535
|
"aria-owns",
|
|
38500
38536
|
"aria-relevant",
|
|
@@ -38512,6 +38548,7 @@ const ROLE_SUPPORTS_ARIA_PROPS = {
|
|
|
38512
38548
|
"aria-details",
|
|
38513
38549
|
"aria-disabled",
|
|
38514
38550
|
"aria-dropeffect",
|
|
38551
|
+
"aria-errormessage",
|
|
38515
38552
|
"aria-flowto",
|
|
38516
38553
|
"aria-grabbed",
|
|
38517
38554
|
"aria-hidden",
|
|
@@ -38520,8 +38557,10 @@ const ROLE_SUPPORTS_ARIA_PROPS = {
|
|
|
38520
38557
|
"aria-label",
|
|
38521
38558
|
"aria-labelledby",
|
|
38522
38559
|
"aria-live",
|
|
38560
|
+
"aria-multiselectable",
|
|
38523
38561
|
"aria-orientation",
|
|
38524
38562
|
"aria-owns",
|
|
38563
|
+
"aria-readonly",
|
|
38525
38564
|
"aria-relevant",
|
|
38526
38565
|
"aria-required",
|
|
38527
38566
|
"aria-roledescription",
|
|
@@ -39553,8 +39592,8 @@ const serverFetchWithoutRevalidate = defineRule({
|
|
|
39553
39592
|
let isServerSideFile = false;
|
|
39554
39593
|
return {
|
|
39555
39594
|
Program(node) {
|
|
39556
|
-
const filename = normalizeFilename
|
|
39557
|
-
if (!APP_ROUTER_FILE_PATTERN.test(filename)) {
|
|
39595
|
+
const filename = normalizeFilename(context.filename ?? "");
|
|
39596
|
+
if (!isInProjectDirectory(context, "app") || !APP_ROUTER_FILE_PATTERN.test(filename)) {
|
|
39558
39597
|
isServerSideFile = false;
|
|
39559
39598
|
return;
|
|
39560
39599
|
}
|
|
@@ -39639,7 +39678,6 @@ const collectRequestTaintedNames = (handlerBody, handlerParamNames) => {
|
|
|
39639
39678
|
});
|
|
39640
39679
|
return taintedNames;
|
|
39641
39680
|
};
|
|
39642
|
-
const PAGES_ROUTER_API_PATH_PATTERN = /\/pages\/api\//;
|
|
39643
39681
|
const inspectHandlerBody = (context, handlerBody, handlerLabel, handlerParamNames) => {
|
|
39644
39682
|
const requestTaintedNames = collectRequestTaintedNames(handlerBody, handlerParamNames);
|
|
39645
39683
|
walkAst(handlerBody, (child) => {
|
|
@@ -39680,8 +39718,7 @@ const serverHoistStaticIo = defineRule({
|
|
|
39680
39718
|
inspectHandlerBody(context, declaration.body, `${handlerName} route handler`, collectHandlerParamNames(declaration.params ?? []));
|
|
39681
39719
|
},
|
|
39682
39720
|
ExportDefaultDeclaration(node) {
|
|
39683
|
-
|
|
39684
|
-
if (!PAGES_ROUTER_API_PATH_PATTERN.test(filename)) return;
|
|
39721
|
+
if (!isInProjectDirectory(context, "pages/api")) return;
|
|
39685
39722
|
const declaration = node.declaration;
|
|
39686
39723
|
if (!isFunctionLike$1(declaration)) return;
|
|
39687
39724
|
if (!declaration.async) return;
|
|
@@ -40735,7 +40772,7 @@ const tanstackStartMissingHeadContent = defineRule({
|
|
|
40735
40772
|
collectVariableAlias(node);
|
|
40736
40773
|
},
|
|
40737
40774
|
JSXOpeningElement(node) {
|
|
40738
|
-
const filename = normalizeFilename
|
|
40775
|
+
const filename = normalizeFilename(context.filename ?? "");
|
|
40739
40776
|
if (!TANSTACK_ROOT_ROUTE_FILE_PATTERN.test(filename)) return;
|
|
40740
40777
|
if (isNodeOfType(node.name, "JSXIdentifier")) {
|
|
40741
40778
|
if (node.name.name === DOCUMENT_HEAD_ELEMENT_NAME) hasDocumentHeadElement = true;
|
|
@@ -40750,7 +40787,7 @@ const tanstackStartMissingHeadContent = defineRule({
|
|
|
40750
40787
|
if (isInsideDocumentHeadElement(node) && isCustomJsxElementName(node.name)) hasCustomHeadChildElement = true;
|
|
40751
40788
|
},
|
|
40752
40789
|
"Program:exit"(programNode) {
|
|
40753
|
-
const filename = normalizeFilename
|
|
40790
|
+
const filename = normalizeFilename(context.filename ?? "");
|
|
40754
40791
|
if (!TANSTACK_ROOT_ROUTE_FILE_PATTERN.test(filename)) return;
|
|
40755
40792
|
if (hasDocumentHeadElement && !hasHeadContentElement && !hasCustomHeadChildElement) context.report({
|
|
40756
40793
|
node: programNode,
|
|
@@ -40776,8 +40813,7 @@ const tanstackStartNoAnchorElement = defineRule({
|
|
|
40776
40813
|
severity: "warn",
|
|
40777
40814
|
recommendation: "Use `Link` from `@tanstack/react-router` so internal navigation keeps client state, preloading, and typed routes.",
|
|
40778
40815
|
create: (context) => ({ JSXOpeningElement(node) {
|
|
40779
|
-
|
|
40780
|
-
if (!TANSTACK_ROUTE_FILE_PATTERN.test(filename)) return;
|
|
40816
|
+
if (!isInProjectDirectory(context, "routes")) return;
|
|
40781
40817
|
if (!isNodeOfType(node.name, "JSXIdentifier") || node.name.name !== "a") return;
|
|
40782
40818
|
const attributes = node.attributes ?? [];
|
|
40783
40819
|
const hrefAttribute = findJsxAttribute(attributes, "href");
|
|
@@ -40860,6 +40896,7 @@ const tanstackStartNoNavigateInRender = defineRule({
|
|
|
40860
40896
|
severity: "warn",
|
|
40861
40897
|
recommendation: "Use `throw redirect({ to: '/path' })` in `beforeLoad` or `loader`. navigate() during render causes hydration issues.",
|
|
40862
40898
|
create: (context) => {
|
|
40899
|
+
const isRouteFile = isInProjectDirectory(context, "routes");
|
|
40863
40900
|
let deferredCallbackDepth = 0;
|
|
40864
40901
|
let eventHandlerDepth = 0;
|
|
40865
40902
|
const isDeferredHookCall = (node) => isHookCall$1(node, EFFECT_HOOK_NAMES$1) || isHookCall$1(node, "useCallback") || isHookCall$1(node, "useMemo");
|
|
@@ -40923,8 +40960,7 @@ const tanstackStartNoNavigateInRender = defineRule({
|
|
|
40923
40960
|
};
|
|
40924
40961
|
return {
|
|
40925
40962
|
CallExpression(node) {
|
|
40926
|
-
|
|
40927
|
-
if (!TANSTACK_ROUTE_FILE_PATTERN.test(filename)) return;
|
|
40963
|
+
if (!isRouteFile) return;
|
|
40928
40964
|
if (isDeferredHookCall(node)) deferredCallbackDepth++;
|
|
40929
40965
|
if (deferredCallbackDepth > 0 || eventHandlerDepth > 0) return;
|
|
40930
40966
|
if (isNodeOfType(node.callee, "Identifier") && node.callee.name === "navigate" && (node.arguments?.length ?? 0) > 0) {
|
|
@@ -40936,48 +40972,39 @@ const tanstackStartNoNavigateInRender = defineRule({
|
|
|
40936
40972
|
}
|
|
40937
40973
|
},
|
|
40938
40974
|
"CallExpression:exit"(node) {
|
|
40939
|
-
|
|
40940
|
-
if (!TANSTACK_ROUTE_FILE_PATTERN.test(filename)) return;
|
|
40975
|
+
if (!isRouteFile) return;
|
|
40941
40976
|
if (isDeferredHookCall(node)) deferredCallbackDepth = Math.max(0, deferredCallbackDepth - 1);
|
|
40942
40977
|
},
|
|
40943
40978
|
JSXAttribute(node) {
|
|
40944
|
-
|
|
40945
|
-
if (!TANSTACK_ROUTE_FILE_PATTERN.test(filename)) return;
|
|
40979
|
+
if (!isRouteFile) return;
|
|
40946
40980
|
if (isEventHandlerAttribute(node)) eventHandlerDepth++;
|
|
40947
40981
|
},
|
|
40948
40982
|
"JSXAttribute:exit"(node) {
|
|
40949
|
-
|
|
40950
|
-
if (!TANSTACK_ROUTE_FILE_PATTERN.test(filename)) return;
|
|
40983
|
+
if (!isRouteFile) return;
|
|
40951
40984
|
if (isEventHandlerAttribute(node)) eventHandlerDepth = Math.max(0, eventHandlerDepth - 1);
|
|
40952
40985
|
},
|
|
40953
40986
|
Property(node) {
|
|
40954
|
-
|
|
40955
|
-
if (!TANSTACK_ROUTE_FILE_PATTERN.test(filename)) return;
|
|
40987
|
+
if (!isRouteFile) return;
|
|
40956
40988
|
if (isEventHandlerProperty(node)) eventHandlerDepth++;
|
|
40957
40989
|
},
|
|
40958
40990
|
"Property:exit"(node) {
|
|
40959
|
-
|
|
40960
|
-
if (!TANSTACK_ROUTE_FILE_PATTERN.test(filename)) return;
|
|
40991
|
+
if (!isRouteFile) return;
|
|
40961
40992
|
if (isEventHandlerProperty(node)) eventHandlerDepth = Math.max(0, eventHandlerDepth - 1);
|
|
40962
40993
|
},
|
|
40963
40994
|
VariableDeclarator(node) {
|
|
40964
|
-
|
|
40965
|
-
if (!TANSTACK_ROUTE_FILE_PATTERN.test(filename)) return;
|
|
40995
|
+
if (!isRouteFile) return;
|
|
40966
40996
|
if (isHandlerNamedVariableDeclarator(node)) eventHandlerDepth++;
|
|
40967
40997
|
},
|
|
40968
40998
|
"VariableDeclarator:exit"(node) {
|
|
40969
|
-
|
|
40970
|
-
if (!TANSTACK_ROUTE_FILE_PATTERN.test(filename)) return;
|
|
40999
|
+
if (!isRouteFile) return;
|
|
40971
41000
|
if (isHandlerNamedVariableDeclarator(node)) eventHandlerDepth = Math.max(0, eventHandlerDepth - 1);
|
|
40972
41001
|
},
|
|
40973
41002
|
FunctionDeclaration(node) {
|
|
40974
|
-
|
|
40975
|
-
if (!TANSTACK_ROUTE_FILE_PATTERN.test(filename)) return;
|
|
41003
|
+
if (!isRouteFile) return;
|
|
40976
41004
|
if (isHandlerNamedFunctionDeclaration(node)) eventHandlerDepth++;
|
|
40977
41005
|
},
|
|
40978
41006
|
"FunctionDeclaration:exit"(node) {
|
|
40979
|
-
|
|
40980
|
-
if (!TANSTACK_ROUTE_FILE_PATTERN.test(filename)) return;
|
|
41007
|
+
if (!isRouteFile) return;
|
|
40981
41008
|
if (isHandlerNamedFunctionDeclaration(node)) eventHandlerDepth = Math.max(0, eventHandlerDepth - 1);
|
|
40982
41009
|
}
|
|
40983
41010
|
};
|
|
@@ -41062,8 +41089,7 @@ const tanstackStartNoUseEffectFetch = defineRule({
|
|
|
41062
41089
|
severity: "warn",
|
|
41063
41090
|
recommendation: "Fetch data in the route `loader` instead. The router loads it before rendering and avoids waterfalls.",
|
|
41064
41091
|
create: (context) => ({ CallExpression(node) {
|
|
41065
|
-
|
|
41066
|
-
if (!TANSTACK_ROUTE_FILE_PATTERN.test(filename)) return;
|
|
41092
|
+
if (!isInProjectDirectory(context, "routes")) return;
|
|
41067
41093
|
if (!isHookCall$1(node, EFFECT_HOOK_NAMES$1)) return;
|
|
41068
41094
|
const callback = node.arguments?.[0];
|
|
41069
41095
|
if (!callback) return;
|
|
@@ -46817,23 +46843,29 @@ const FALLBACK_CFG = {
|
|
|
46817
46843
|
enclosingFunction: () => null,
|
|
46818
46844
|
isUnconditionalFromEntry: () => false
|
|
46819
46845
|
};
|
|
46846
|
+
const scopesByProgram = /* @__PURE__ */ new WeakMap();
|
|
46847
|
+
const cfgByProgram = /* @__PURE__ */ new WeakMap();
|
|
46820
46848
|
const wrapWithSemanticContext = (rule) => ({
|
|
46821
46849
|
...rule,
|
|
46822
46850
|
create: (baseContext) => {
|
|
46823
46851
|
let programRoot = null;
|
|
46824
|
-
let cachedScopes = null;
|
|
46825
|
-
let cachedCfg = null;
|
|
46826
46852
|
const getScopes = () => {
|
|
46827
|
-
if (cachedScopes) return cachedScopes;
|
|
46828
46853
|
if (!programRoot) return buildFallbackScopes();
|
|
46829
|
-
|
|
46830
|
-
|
|
46854
|
+
let scopes = scopesByProgram.get(programRoot);
|
|
46855
|
+
if (!scopes) {
|
|
46856
|
+
scopes = analyzeScopes(programRoot);
|
|
46857
|
+
scopesByProgram.set(programRoot, scopes);
|
|
46858
|
+
}
|
|
46859
|
+
return scopes;
|
|
46831
46860
|
};
|
|
46832
46861
|
const getCfg = () => {
|
|
46833
|
-
if (cachedCfg) return cachedCfg;
|
|
46834
46862
|
if (!programRoot) return FALLBACK_CFG;
|
|
46835
|
-
|
|
46836
|
-
|
|
46863
|
+
let cfg = cfgByProgram.get(programRoot);
|
|
46864
|
+
if (!cfg) {
|
|
46865
|
+
cfg = analyzeControlFlow(programRoot);
|
|
46866
|
+
cfgByProgram.set(programRoot, cfg);
|
|
46867
|
+
}
|
|
46868
|
+
return cfg;
|
|
46837
46869
|
};
|
|
46838
46870
|
const enrichedContext = {
|
|
46839
46871
|
report: baseContext.report,
|
|
@@ -46848,23 +46880,18 @@ const wrapWithSemanticContext = (rule) => ({
|
|
|
46848
46880
|
return getCfg();
|
|
46849
46881
|
}
|
|
46850
46882
|
};
|
|
46851
|
-
const captureRootIfNeeded = (node) => {
|
|
46852
|
-
if (programRoot) return;
|
|
46853
|
-
programRoot = findProgramRoot(node);
|
|
46854
|
-
};
|
|
46855
46883
|
const visitors = rule.create(enrichedContext);
|
|
46856
|
-
const
|
|
46884
|
+
const passthroughVisitors = {};
|
|
46857
46885
|
for (const [nodeType, handler] of Object.entries(visitors)) {
|
|
46858
46886
|
if (typeof handler !== "function") continue;
|
|
46859
|
-
|
|
46860
|
-
captureRootIfNeeded(node);
|
|
46861
|
-
handler(node);
|
|
46862
|
-
});
|
|
46887
|
+
passthroughVisitors[nodeType] = handler;
|
|
46863
46888
|
}
|
|
46864
|
-
|
|
46865
|
-
|
|
46866
|
-
|
|
46867
|
-
|
|
46889
|
+
const innerProgramHandler = passthroughVisitors.Program;
|
|
46890
|
+
passthroughVisitors.Program = ((node) => {
|
|
46891
|
+
programRoot = node;
|
|
46892
|
+
if (innerProgramHandler) innerProgramHandler(node);
|
|
46893
|
+
});
|
|
46894
|
+
return passthroughVisitors;
|
|
46868
46895
|
}
|
|
46869
46896
|
});
|
|
46870
46897
|
//#endregion
|