oxlint-plugin-react-doctor 0.5.0 → 0.5.1-dev.038aaf7
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 +193 -90
- package/package.json +2 -2
package/dist/index.js
CHANGED
|
@@ -432,6 +432,7 @@ const SETTER_PATTERN = /^set[A-Z]/;
|
|
|
432
432
|
const RENDER_FUNCTION_PATTERN = /^render[A-Z]/;
|
|
433
433
|
const UPPERCASE_PATTERN = /^[A-Z]/;
|
|
434
434
|
const REACT_HANDLER_PROP_PATTERN = /^on[A-Z]/;
|
|
435
|
+
const HANDLER_FUNCTION_NAME_PATTERN = /^(?:on|handle)[A-Z]/;
|
|
435
436
|
const EFFECT_HOOK_NAMES$1 = new Set(["useEffect", "useLayoutEffect"]);
|
|
436
437
|
const HOOKS_WITH_DEPS = new Set([
|
|
437
438
|
"useEffect",
|
|
@@ -1131,6 +1132,15 @@ const isMemberProperty = (node, propertyName) => Boolean(node && isNodeOfType(no
|
|
|
1131
1132
|
const NEXTJS_SOURCE_FILE_EXTENSION_GROUP = "(?:tsx?|jsx?|mts|mjs)";
|
|
1132
1133
|
const PAGE_FILE_PATTERN = new RegExp(`/page\\.${NEXTJS_SOURCE_FILE_EXTENSION_GROUP}$`);
|
|
1133
1134
|
const PAGE_OR_LAYOUT_FILE_PATTERN = new RegExp(`/(page|layout)\\.${NEXTJS_SOURCE_FILE_EXTENSION_GROUP}$`);
|
|
1135
|
+
const LAYOUT_FILE_NAMES = [
|
|
1136
|
+
"layout.tsx",
|
|
1137
|
+
"layout.jsx",
|
|
1138
|
+
"layout.ts",
|
|
1139
|
+
"layout.js",
|
|
1140
|
+
"layout.mts",
|
|
1141
|
+
"layout.mjs"
|
|
1142
|
+
];
|
|
1143
|
+
const METADATA_EXPORT_NAMES = ["metadata", "generateMetadata"];
|
|
1134
1144
|
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;
|
|
1135
1145
|
const PAGES_DIRECTORY_PATTERN = /\/pages\//;
|
|
1136
1146
|
const NEXTJS_NAVIGATION_FUNCTIONS = new Set([
|
|
@@ -12542,6 +12552,64 @@ const nextjsInlineScriptMissingId = defineRule({
|
|
|
12542
12552
|
} })
|
|
12543
12553
|
});
|
|
12544
12554
|
//#endregion
|
|
12555
|
+
//#region src/plugin/utils/parse-export-specifiers.ts
|
|
12556
|
+
const getSpecifierName = (rawName) => rawName.replace(/^type\s+/, "").trim();
|
|
12557
|
+
const parseExportSpecifiers = (specifiersText, declarationIsTypeOnly) => specifiersText.split(",").map((specifierText) => specifierText.trim()).filter(Boolean).map((specifierText) => {
|
|
12558
|
+
const isTypeOnly = declarationIsTypeOnly || specifierText.startsWith("type ");
|
|
12559
|
+
const [rawLocalName, rawExportedName] = specifierText.split(/\s+as\s+/);
|
|
12560
|
+
const localName = getSpecifierName(rawLocalName ?? "");
|
|
12561
|
+
return {
|
|
12562
|
+
localName,
|
|
12563
|
+
exportedName: getSpecifierName(rawExportedName ?? localName),
|
|
12564
|
+
isTypeOnly
|
|
12565
|
+
};
|
|
12566
|
+
});
|
|
12567
|
+
//#endregion
|
|
12568
|
+
//#region src/plugin/utils/strip-js-comments.ts
|
|
12569
|
+
const BLOCK_COMMENT_PATTERN = /\/\*[\s\S]*?\*\//g;
|
|
12570
|
+
const LINE_COMMENT_PATTERN = /^\s*\/\/.*$/gm;
|
|
12571
|
+
const stripJsComments = (sourceText) => sourceText.replace(BLOCK_COMMENT_PATTERN, "").replace(LINE_COMMENT_PATTERN, "");
|
|
12572
|
+
//#endregion
|
|
12573
|
+
//#region src/plugin/utils/does-module-export-name.ts
|
|
12574
|
+
const DEFAULT_EXPORT_DECLARATION_PATTERN = /^\s*export\s+default\b/m;
|
|
12575
|
+
const NAMED_EXPORT_DECLARATION_PATTERN = /^\s*export\s+(?:declare\s+)?(?:(?:async\s+)?function|(?:abstract\s+)?class|const|let|var|enum|interface|type)\s+([\w$]+)/gm;
|
|
12576
|
+
const LOCAL_EXPORT_SPECIFIER_DECLARATION_PATTERN$1 = /^\s*export\s+(?:type\s+)?\{([\s\S]*?)\}(?:\s+from\s+["'][^"']+["'])?\s*;?\s*(?:(?:\/\/[^\n]*)?\s*)/gm;
|
|
12577
|
+
const doesSourceTextExportName = (sourceText, exportedName) => {
|
|
12578
|
+
const strippedSource = stripJsComments(sourceText);
|
|
12579
|
+
if (exportedName === "default" && DEFAULT_EXPORT_DECLARATION_PATTERN.test(strippedSource)) return true;
|
|
12580
|
+
for (const match of strippedSource.matchAll(NAMED_EXPORT_DECLARATION_PATTERN)) if (match[1] === exportedName) return true;
|
|
12581
|
+
for (const match of strippedSource.matchAll(LOCAL_EXPORT_SPECIFIER_DECLARATION_PATTERN$1)) if (parseExportSpecifiers(match[1] ?? "", false).map((specifier) => specifier.exportedName).includes(exportedName)) return true;
|
|
12582
|
+
return false;
|
|
12583
|
+
};
|
|
12584
|
+
const doesModuleExportName = (filePath, exportedName) => {
|
|
12585
|
+
try {
|
|
12586
|
+
return doesSourceTextExportName(fs.readFileSync(filePath, "utf8"), exportedName);
|
|
12587
|
+
} catch {
|
|
12588
|
+
return false;
|
|
12589
|
+
}
|
|
12590
|
+
};
|
|
12591
|
+
//#endregion
|
|
12592
|
+
//#region src/plugin/utils/has-ancestor-layout-matching.ts
|
|
12593
|
+
const hasAncestorLayoutMatching = (pageFilename, matchesLayout) => {
|
|
12594
|
+
const normalizedPage = pageFilename.replaceAll("\\", "/");
|
|
12595
|
+
let currentDirectory = path.dirname(normalizedPage);
|
|
12596
|
+
for (let level = 0; level < 30; level++) {
|
|
12597
|
+
for (const layoutFileName of LAYOUT_FILE_NAMES) {
|
|
12598
|
+
const layoutPath = path.join(currentDirectory, layoutFileName);
|
|
12599
|
+
if (layoutPath.replaceAll("\\", "/") === normalizedPage) continue;
|
|
12600
|
+
if (matchesLayout(layoutPath)) return true;
|
|
12601
|
+
}
|
|
12602
|
+
const parentDirectory = path.dirname(currentDirectory);
|
|
12603
|
+
if (path.basename(currentDirectory) === "app" && path.basename(parentDirectory) !== "app") break;
|
|
12604
|
+
if (parentDirectory === currentDirectory) break;
|
|
12605
|
+
currentDirectory = parentDirectory;
|
|
12606
|
+
}
|
|
12607
|
+
return false;
|
|
12608
|
+
};
|
|
12609
|
+
//#endregion
|
|
12610
|
+
//#region src/plugin/utils/find-ancestor-metadata-layout.ts
|
|
12611
|
+
const hasAncestorMetadataLayout = (pageFilename) => hasAncestorLayoutMatching(pageFilename, (layoutPath) => METADATA_EXPORT_NAMES.some((exportName) => doesModuleExportName(layoutPath, exportName)));
|
|
12612
|
+
//#endregion
|
|
12545
12613
|
//#region src/plugin/rules/nextjs/nextjs-missing-metadata.ts
|
|
12546
12614
|
const nextjsMissingMetadata = defineRule({
|
|
12547
12615
|
id: "nextjs-missing-metadata",
|
|
@@ -12554,13 +12622,15 @@ const nextjsMissingMetadata = defineRule({
|
|
|
12554
12622
|
const filename = normalizeFilename$1(context.filename ?? "");
|
|
12555
12623
|
if (!PAGE_FILE_PATTERN.test(filename)) return;
|
|
12556
12624
|
if (INTERNAL_PAGE_PATH_PATTERN.test(filename)) return;
|
|
12557
|
-
if (
|
|
12625
|
+
if (programNode.body?.some((statement) => {
|
|
12558
12626
|
if (!isNodeOfType(statement, "ExportNamedDeclaration")) return false;
|
|
12559
12627
|
const declaration = statement.declaration;
|
|
12560
|
-
if (isNodeOfType(declaration, "VariableDeclaration")) return declaration.declarations?.some((declarator) => isNodeOfType(declarator.id, "Identifier") && (declarator.id.name
|
|
12628
|
+
if (isNodeOfType(declaration, "VariableDeclaration")) return declaration.declarations?.some((declarator) => isNodeOfType(declarator.id, "Identifier") && METADATA_EXPORT_NAMES.includes(declarator.id.name));
|
|
12561
12629
|
if (isNodeOfType(declaration, "FunctionDeclaration")) return declaration.id?.name === "generateMetadata";
|
|
12562
|
-
return
|
|
12563
|
-
}))
|
|
12630
|
+
return (statement.specifiers ?? []).some((specifier) => isNodeOfType(specifier, "ExportSpecifier") && isNodeOfType(specifier.exported, "Identifier") && METADATA_EXPORT_NAMES.includes(specifier.exported.name));
|
|
12631
|
+
})) return;
|
|
12632
|
+
if (hasAncestorMetadataLayout(context.filename ?? "")) return;
|
|
12633
|
+
context.report({
|
|
12564
12634
|
node: programNode,
|
|
12565
12635
|
message: "This page has no metadata, so search engines and social previews get no title or description."
|
|
12566
12636
|
});
|
|
@@ -13451,43 +13521,6 @@ const parseSourceFile = (absoluteFilePath) => {
|
|
|
13451
13521
|
return parsedProgram;
|
|
13452
13522
|
};
|
|
13453
13523
|
//#endregion
|
|
13454
|
-
//#region src/plugin/utils/parse-export-specifiers.ts
|
|
13455
|
-
const getSpecifierName = (rawName) => rawName.replace(/^type\s+/, "").trim();
|
|
13456
|
-
const parseExportSpecifiers = (specifiersText, declarationIsTypeOnly) => specifiersText.split(",").map((specifierText) => specifierText.trim()).filter(Boolean).map((specifierText) => {
|
|
13457
|
-
const isTypeOnly = declarationIsTypeOnly || specifierText.startsWith("type ");
|
|
13458
|
-
const [rawLocalName, rawExportedName] = specifierText.split(/\s+as\s+/);
|
|
13459
|
-
const localName = getSpecifierName(rawLocalName ?? "");
|
|
13460
|
-
return {
|
|
13461
|
-
localName,
|
|
13462
|
-
exportedName: getSpecifierName(rawExportedName ?? localName),
|
|
13463
|
-
isTypeOnly
|
|
13464
|
-
};
|
|
13465
|
-
});
|
|
13466
|
-
//#endregion
|
|
13467
|
-
//#region src/plugin/utils/strip-js-comments.ts
|
|
13468
|
-
const BLOCK_COMMENT_PATTERN = /\/\*[\s\S]*?\*\//g;
|
|
13469
|
-
const LINE_COMMENT_PATTERN = /^\s*\/\/.*$/gm;
|
|
13470
|
-
const stripJsComments = (sourceText) => sourceText.replace(BLOCK_COMMENT_PATTERN, "").replace(LINE_COMMENT_PATTERN, "");
|
|
13471
|
-
//#endregion
|
|
13472
|
-
//#region src/plugin/utils/does-module-export-name.ts
|
|
13473
|
-
const DEFAULT_EXPORT_DECLARATION_PATTERN = /^\s*export\s+default\b/m;
|
|
13474
|
-
const NAMED_EXPORT_DECLARATION_PATTERN = /^\s*export\s+(?:declare\s+)?(?:(?:async\s+)?function|(?:abstract\s+)?class|const|let|var|enum|interface|type)\s+([\w$]+)/gm;
|
|
13475
|
-
const LOCAL_EXPORT_SPECIFIER_DECLARATION_PATTERN$1 = /^\s*export\s+(?:type\s+)?\{([\s\S]*?)\}(?:\s+from\s+["'][^"']+["'])?\s*;?\s*(?:(?:\/\/[^\n]*)?\s*)/gm;
|
|
13476
|
-
const doesSourceTextExportName = (sourceText, exportedName) => {
|
|
13477
|
-
const strippedSource = stripJsComments(sourceText);
|
|
13478
|
-
if (exportedName === "default" && DEFAULT_EXPORT_DECLARATION_PATTERN.test(strippedSource)) return true;
|
|
13479
|
-
for (const match of strippedSource.matchAll(NAMED_EXPORT_DECLARATION_PATTERN)) if (match[1] === exportedName) return true;
|
|
13480
|
-
for (const match of strippedSource.matchAll(LOCAL_EXPORT_SPECIFIER_DECLARATION_PATTERN$1)) if (parseExportSpecifiers(match[1] ?? "", false).map((specifier) => specifier.exportedName).includes(exportedName)) return true;
|
|
13481
|
-
return false;
|
|
13482
|
-
};
|
|
13483
|
-
const doesModuleExportName = (filePath, exportedName) => {
|
|
13484
|
-
try {
|
|
13485
|
-
return doesSourceTextExportName(fs.readFileSync(filePath, "utf8"), exportedName);
|
|
13486
|
-
} catch {
|
|
13487
|
-
return false;
|
|
13488
|
-
}
|
|
13489
|
-
};
|
|
13490
|
-
//#endregion
|
|
13491
13524
|
//#region src/plugin/utils/is-barrel-index-module.ts
|
|
13492
13525
|
const INDEX_MODULE_FILE_PATTERN = /^index\.(?:[cm]?[jt]sx?|mjs)$/;
|
|
13493
13526
|
const BINDING_IMPORT_DECLARATION_PATTERN = /^\s*import\s+(type\s+)?(?!["'])([^;]*?)\s+from\s+["']([^"']+)["']\s*;?\s*(?:(?:\/\/[^\n]*)?\s*)/gm;
|
|
@@ -13971,29 +14004,10 @@ const astMentionsSuspense = (programNode) => {
|
|
|
13971
14004
|
};
|
|
13972
14005
|
//#endregion
|
|
13973
14006
|
//#region src/plugin/utils/find-ancestor-suspense-layout.ts
|
|
13974
|
-
const
|
|
13975
|
-
|
|
13976
|
-
|
|
13977
|
-
|
|
13978
|
-
"layout.js"
|
|
13979
|
-
];
|
|
13980
|
-
const hasAncestorSuspenseLayout = (pageFilename) => {
|
|
13981
|
-
const normalizedPage = pageFilename.replaceAll("\\", "/");
|
|
13982
|
-
let currentDirectory = path.dirname(normalizedPage);
|
|
13983
|
-
for (let level = 0; level < 30; level++) {
|
|
13984
|
-
for (const layoutFileName of LAYOUT_FILE_NAMES) {
|
|
13985
|
-
const layoutPath = path.join(currentDirectory, layoutFileName);
|
|
13986
|
-
if (layoutPath.replaceAll("\\", "/") === normalizedPage) continue;
|
|
13987
|
-
const programRoot = parseSourceFile(layoutPath);
|
|
13988
|
-
if (programRoot && astMentionsSuspense(programRoot)) return true;
|
|
13989
|
-
}
|
|
13990
|
-
if (path.basename(currentDirectory) === "app") break;
|
|
13991
|
-
const parentDirectory = path.dirname(currentDirectory);
|
|
13992
|
-
if (parentDirectory === currentDirectory) break;
|
|
13993
|
-
currentDirectory = parentDirectory;
|
|
13994
|
-
}
|
|
13995
|
-
return false;
|
|
13996
|
-
};
|
|
14007
|
+
const hasAncestorSuspenseLayout = (pageFilename) => hasAncestorLayoutMatching(pageFilename, (layoutPath) => {
|
|
14008
|
+
const programRoot = parseSourceFile(layoutPath);
|
|
14009
|
+
return Boolean(programRoot && astMentionsSuspense(programRoot));
|
|
14010
|
+
});
|
|
13997
14011
|
//#endregion
|
|
13998
14012
|
//#region src/plugin/rules/nextjs/nextjs-no-use-search-params-without-suspense.ts
|
|
13999
14013
|
const astContainsUseSearchParams = (root) => {
|
|
@@ -24343,6 +24357,21 @@ const noZIndex9999 = defineRule({
|
|
|
24343
24357
|
})
|
|
24344
24358
|
});
|
|
24345
24359
|
//#endregion
|
|
24360
|
+
//#region src/plugin/utils/is-framework-route-or-special-filename.ts
|
|
24361
|
+
const sourceFileExtensionGroup = NEXTJS_SOURCE_FILE_EXTENSION_GROUP;
|
|
24362
|
+
const FRAMEWORK_ROUTE_FILE_PATTERNS = [
|
|
24363
|
+
new RegExp(`^(page|layout|loading|error|not-found|template|default|global-error|route|_app|_document|_error)\\.${sourceFileExtensionGroup}$`),
|
|
24364
|
+
new RegExp(`^(_layout|\\+html|\\+not-found|\\+native-intent)\\.${sourceFileExtensionGroup}$`),
|
|
24365
|
+
new RegExp(`(?:^__root|\\.lazy)\\.${sourceFileExtensionGroup}$`),
|
|
24366
|
+
new RegExp(`^(root|entry\\.client|entry\\.server)\\.${sourceFileExtensionGroup}$`)
|
|
24367
|
+
];
|
|
24368
|
+
const isFrameworkRouteOrSpecialFilename = (rawFilename) => {
|
|
24369
|
+
if (!rawFilename) return false;
|
|
24370
|
+
if (isNextjsMetadataImageRouteFilename(rawFilename)) return true;
|
|
24371
|
+
const basename = path.basename(rawFilename);
|
|
24372
|
+
return FRAMEWORK_ROUTE_FILE_PATTERNS.some((pattern) => pattern.test(basename));
|
|
24373
|
+
};
|
|
24374
|
+
//#endregion
|
|
24346
24375
|
//#region src/plugin/rules/react-builtins/only-export-components-tables.ts
|
|
24347
24376
|
const NOT_REACT_COMPONENT_EXPRESSION_TYPES = new Set([
|
|
24348
24377
|
"ArrayExpression",
|
|
@@ -24394,32 +24423,6 @@ const ENTRY_POINT_BASENAMES = new Set([
|
|
|
24394
24423
|
"client.jsx",
|
|
24395
24424
|
"server.tsx",
|
|
24396
24425
|
"server.jsx",
|
|
24397
|
-
"page.tsx",
|
|
24398
|
-
"page.jsx",
|
|
24399
|
-
"layout.tsx",
|
|
24400
|
-
"layout.jsx",
|
|
24401
|
-
"loading.tsx",
|
|
24402
|
-
"loading.jsx",
|
|
24403
|
-
"error.tsx",
|
|
24404
|
-
"error.jsx",
|
|
24405
|
-
"not-found.tsx",
|
|
24406
|
-
"not-found.jsx",
|
|
24407
|
-
"template.tsx",
|
|
24408
|
-
"template.jsx",
|
|
24409
|
-
"default.tsx",
|
|
24410
|
-
"default.jsx",
|
|
24411
|
-
"global-error.tsx",
|
|
24412
|
-
"global-error.jsx",
|
|
24413
|
-
"route.tsx",
|
|
24414
|
-
"route.jsx",
|
|
24415
|
-
"_layout.tsx",
|
|
24416
|
-
"_layout.jsx",
|
|
24417
|
-
"_app.tsx",
|
|
24418
|
-
"_app.jsx",
|
|
24419
|
-
"_document.tsx",
|
|
24420
|
-
"_document.jsx",
|
|
24421
|
-
"_error.tsx",
|
|
24422
|
-
"_error.jsx",
|
|
24423
24426
|
"app.tsx",
|
|
24424
24427
|
"app.jsx",
|
|
24425
24428
|
"App.tsx",
|
|
@@ -24499,6 +24502,54 @@ const UTILITY_FILE_BASENAMES = new Set([
|
|
|
24499
24502
|
"defaults.tsx",
|
|
24500
24503
|
"defaults.jsx"
|
|
24501
24504
|
]);
|
|
24505
|
+
const ROUTE_FACTORY_CALLEE_NAMES = new Set([
|
|
24506
|
+
...TANSTACK_ROUTE_CREATION_FUNCTIONS,
|
|
24507
|
+
"createLazyFileRoute",
|
|
24508
|
+
"createLazyRoute",
|
|
24509
|
+
"createAPIFileRoute",
|
|
24510
|
+
"createServerFileRoute",
|
|
24511
|
+
"createServerRootRoute",
|
|
24512
|
+
"createServerRoute",
|
|
24513
|
+
"createBrowserRouter",
|
|
24514
|
+
"createHashRouter",
|
|
24515
|
+
"createMemoryRouter",
|
|
24516
|
+
"createStaticRouter",
|
|
24517
|
+
"createRouter"
|
|
24518
|
+
]);
|
|
24519
|
+
const ROUTE_MODULE_ALLOWED_EXPORT_NAMES = new Set([
|
|
24520
|
+
"loader",
|
|
24521
|
+
"clientLoader",
|
|
24522
|
+
"action",
|
|
24523
|
+
"clientAction",
|
|
24524
|
+
"headers",
|
|
24525
|
+
"meta",
|
|
24526
|
+
"links",
|
|
24527
|
+
"handle",
|
|
24528
|
+
"shouldRevalidate",
|
|
24529
|
+
"middleware",
|
|
24530
|
+
"unstable_middleware",
|
|
24531
|
+
"getServerSideProps",
|
|
24532
|
+
"getStaticProps",
|
|
24533
|
+
"getStaticPaths",
|
|
24534
|
+
"getInitialProps",
|
|
24535
|
+
"reportWebVitals",
|
|
24536
|
+
"metadata",
|
|
24537
|
+
"generateMetadata",
|
|
24538
|
+
"generateStaticParams",
|
|
24539
|
+
"generateImageMetadata",
|
|
24540
|
+
"generateSitemaps",
|
|
24541
|
+
"viewport",
|
|
24542
|
+
"generateViewport",
|
|
24543
|
+
"revalidate",
|
|
24544
|
+
"dynamic",
|
|
24545
|
+
"dynamicParams",
|
|
24546
|
+
"fetchCache",
|
|
24547
|
+
"runtime",
|
|
24548
|
+
"preferredRegion",
|
|
24549
|
+
"maxDuration",
|
|
24550
|
+
"experimental_ppr",
|
|
24551
|
+
"unstable_settings"
|
|
24552
|
+
]);
|
|
24502
24553
|
//#endregion
|
|
24503
24554
|
//#region src/plugin/rules/react-builtins/only-export-components.ts
|
|
24504
24555
|
const NAMED_EXPORT_MESSAGE = "This file exports non-components, so Fast Refresh can't safely preserve component state.";
|
|
@@ -24542,6 +24593,18 @@ const isReactCreateContext = (initializer) => {
|
|
|
24542
24593
|
if (isNodeOfType(callee, "MemberExpression") && isNodeOfType(callee.property, "Identifier") && callee.property.name === "createContext") return true;
|
|
24543
24594
|
return false;
|
|
24544
24595
|
};
|
|
24596
|
+
const isRouteFactoryName = (name) => ROUTE_FACTORY_CALLEE_NAMES.has(name);
|
|
24597
|
+
const isRouteFactoryCall = (expression) => {
|
|
24598
|
+
let currentCall = expression;
|
|
24599
|
+
while (isNodeOfType(currentCall, "CallExpression")) {
|
|
24600
|
+
const callee = currentCall.callee;
|
|
24601
|
+
if (isNodeOfType(callee, "Identifier") && isRouteFactoryName(callee.name)) return true;
|
|
24602
|
+
if (isNodeOfType(callee, "MemberExpression") && isNodeOfType(callee.property, "Identifier") && isRouteFactoryName(callee.property.name)) return true;
|
|
24603
|
+
if (!isNodeOfType(callee, "CallExpression")) return false;
|
|
24604
|
+
currentCall = callee;
|
|
24605
|
+
}
|
|
24606
|
+
return false;
|
|
24607
|
+
};
|
|
24545
24608
|
const isReactHocName = (name, state) => state.customHocs.has(name);
|
|
24546
24609
|
const isHocCallee = (callee, state) => {
|
|
24547
24610
|
if (isNodeOfType(callee, "Identifier")) return isReactHocName(callee.name, state);
|
|
@@ -24575,10 +24638,12 @@ const isReactComponentInitializer = (expression, state) => {
|
|
|
24575
24638
|
const classifyExport = (name, reportNode, isFunction, initializer, state) => {
|
|
24576
24639
|
if (initializer) {
|
|
24577
24640
|
const expression = skipTsExpression(initializer);
|
|
24641
|
+
if (isRouteFactoryCall(expression)) return { kind: "react-component" };
|
|
24578
24642
|
if (isNodeOfType(expression, "CallExpression") && isHocCallee(expression.callee, state) && expression.arguments.length > 0 && isReactComponentName(name)) return { kind: "react-component" };
|
|
24579
24643
|
if (isNodeOfType(expression, "ConditionalExpression") && isReactComponentName(name) && isReactComponentInitializer(expression.consequent, state) && isReactComponentInitializer(expression.alternate, state)) return { kind: "react-component" };
|
|
24580
24644
|
}
|
|
24581
24645
|
if (state.allowExportNames.has(name)) return { kind: "allowed" };
|
|
24646
|
+
if (ROUTE_MODULE_ALLOWED_EXPORT_NAMES.has(name)) return { kind: "allowed" };
|
|
24582
24647
|
if (/^use[A-Z]/.test(name)) return { kind: "allowed" };
|
|
24583
24648
|
if (state.allowConstantExport && initializer) {
|
|
24584
24649
|
const expression = skipTsExpression(initializer);
|
|
@@ -24650,6 +24715,7 @@ const isFileNameAllowed = (filename, checkJS) => {
|
|
|
24650
24715
|
if (filename.includes(".test.") || filename.includes(".spec.") || filename.includes(".cy.") || filename.includes(".stories.")) return false;
|
|
24651
24716
|
for (const segment of NON_FAST_REFRESH_PATH_SEGMENTS) if (filename.includes(segment)) return false;
|
|
24652
24717
|
if (isEntryPointFile(filename)) return false;
|
|
24718
|
+
if (isFrameworkRouteOrSpecialFilename(filename)) return false;
|
|
24653
24719
|
if (isAssetOrUtilityFile(filename)) return false;
|
|
24654
24720
|
if (filename.endsWith(".tsx") || filename.endsWith(".jsx")) return true;
|
|
24655
24721
|
if (checkJS && filename.endsWith(".js")) return true;
|
|
@@ -24724,6 +24790,10 @@ const onlyExportComponents = defineRule({
|
|
|
24724
24790
|
continue;
|
|
24725
24791
|
}
|
|
24726
24792
|
if (isNodeOfType(stripped, "CallExpression")) {
|
|
24793
|
+
if (isRouteFactoryCall(stripped)) {
|
|
24794
|
+
hasReactExport = true;
|
|
24795
|
+
continue;
|
|
24796
|
+
}
|
|
24727
24797
|
const isHoc = isHocCallee(stripped.callee, state);
|
|
24728
24798
|
const firstArg = stripped.arguments[0];
|
|
24729
24799
|
const firstArgIsValid = Boolean(firstArg) && (() => {
|
|
@@ -34290,6 +34360,9 @@ const tanstackStartNoNavigateInRender = defineRule({
|
|
|
34290
34360
|
let eventHandlerDepth = 0;
|
|
34291
34361
|
const isDeferredHookCall = (node) => isHookCall$1(node, EFFECT_HOOK_NAMES$1) || isHookCall$1(node, "useCallback") || isHookCall$1(node, "useMemo");
|
|
34292
34362
|
const isEventHandlerAttribute = (node) => isNodeOfType(node, "JSXAttribute") && isNodeOfType(node.name, "JSXIdentifier") && typeof node.name.name === "string" && node.name.name.startsWith("on") && UPPERCASE_PATTERN.test(node.name.name.charAt(2));
|
|
34363
|
+
const isEventHandlerProperty = (node) => isNodeOfType(node, "Property") && isFunctionLike$2(node.value) && (isNodeOfType(node.key, "Identifier") && typeof node.key.name === "string" && REACT_HANDLER_PROP_PATTERN.test(node.key.name) || isNodeOfType(node.key, "Literal") && typeof node.key.value === "string" && REACT_HANDLER_PROP_PATTERN.test(node.key.value));
|
|
34364
|
+
const isHandlerNamedVariableDeclarator = (node) => isNodeOfType(node, "VariableDeclarator") && isNodeOfType(node.id, "Identifier") && typeof node.id.name === "string" && HANDLER_FUNCTION_NAME_PATTERN.test(node.id.name) && isFunctionLike$2(node.init);
|
|
34365
|
+
const isHandlerNamedFunctionDeclaration = (node) => isNodeOfType(node, "FunctionDeclaration") && isNodeOfType(node.id, "Identifier") && typeof node.id.name === "string" && HANDLER_FUNCTION_NAME_PATTERN.test(node.id.name);
|
|
34293
34366
|
return {
|
|
34294
34367
|
CallExpression(node) {
|
|
34295
34368
|
const filename = normalizeFilename$1(context.filename ?? "");
|
|
@@ -34315,6 +34388,36 @@ const tanstackStartNoNavigateInRender = defineRule({
|
|
|
34315
34388
|
const filename = normalizeFilename$1(context.filename ?? "");
|
|
34316
34389
|
if (!TANSTACK_ROUTE_FILE_PATTERN.test(filename)) return;
|
|
34317
34390
|
if (isEventHandlerAttribute(node)) eventHandlerDepth = Math.max(0, eventHandlerDepth - 1);
|
|
34391
|
+
},
|
|
34392
|
+
Property(node) {
|
|
34393
|
+
const filename = normalizeFilename$1(context.filename ?? "");
|
|
34394
|
+
if (!TANSTACK_ROUTE_FILE_PATTERN.test(filename)) return;
|
|
34395
|
+
if (isEventHandlerProperty(node)) eventHandlerDepth++;
|
|
34396
|
+
},
|
|
34397
|
+
"Property:exit"(node) {
|
|
34398
|
+
const filename = normalizeFilename$1(context.filename ?? "");
|
|
34399
|
+
if (!TANSTACK_ROUTE_FILE_PATTERN.test(filename)) return;
|
|
34400
|
+
if (isEventHandlerProperty(node)) eventHandlerDepth = Math.max(0, eventHandlerDepth - 1);
|
|
34401
|
+
},
|
|
34402
|
+
VariableDeclarator(node) {
|
|
34403
|
+
const filename = normalizeFilename$1(context.filename ?? "");
|
|
34404
|
+
if (!TANSTACK_ROUTE_FILE_PATTERN.test(filename)) return;
|
|
34405
|
+
if (isHandlerNamedVariableDeclarator(node)) eventHandlerDepth++;
|
|
34406
|
+
},
|
|
34407
|
+
"VariableDeclarator:exit"(node) {
|
|
34408
|
+
const filename = normalizeFilename$1(context.filename ?? "");
|
|
34409
|
+
if (!TANSTACK_ROUTE_FILE_PATTERN.test(filename)) return;
|
|
34410
|
+
if (isHandlerNamedVariableDeclarator(node)) eventHandlerDepth = Math.max(0, eventHandlerDepth - 1);
|
|
34411
|
+
},
|
|
34412
|
+
FunctionDeclaration(node) {
|
|
34413
|
+
const filename = normalizeFilename$1(context.filename ?? "");
|
|
34414
|
+
if (!TANSTACK_ROUTE_FILE_PATTERN.test(filename)) return;
|
|
34415
|
+
if (isHandlerNamedFunctionDeclaration(node)) eventHandlerDepth++;
|
|
34416
|
+
},
|
|
34417
|
+
"FunctionDeclaration:exit"(node) {
|
|
34418
|
+
const filename = normalizeFilename$1(context.filename ?? "");
|
|
34419
|
+
if (!TANSTACK_ROUTE_FILE_PATTERN.test(filename)) return;
|
|
34420
|
+
if (isHandlerNamedFunctionDeclaration(node)) eventHandlerDepth = Math.max(0, eventHandlerDepth - 1);
|
|
34318
34421
|
}
|
|
34319
34422
|
};
|
|
34320
34423
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "oxlint-plugin-react-doctor",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.1-dev.038aaf7",
|
|
4
4
|
"description": "oxlint plugin for React Doctor: diagnose React codebases for security, performance, correctness, accessibility, bundle-size, and architecture issues",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"accessibility",
|
|
@@ -50,7 +50,7 @@
|
|
|
50
50
|
"@types/node": "^25.6.0"
|
|
51
51
|
},
|
|
52
52
|
"engines": {
|
|
53
|
-
"node": "^20.19.0 || >=22.
|
|
53
|
+
"node": "^20.19.0 || >=22.13.0"
|
|
54
54
|
},
|
|
55
55
|
"scripts": {
|
|
56
56
|
"dev": "vp pack --watch",
|