oxlint-plugin-react-doctor 0.5.0 → 0.5.1-dev.04e72a4
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 +632 -303
- 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([
|
|
@@ -5829,6 +5839,20 @@ const isReactHookName = (name) => {
|
|
|
5829
5839
|
//#region src/plugin/utils/is-react-component-or-hook-name.ts
|
|
5830
5840
|
const isReactComponentOrHookName = (name) => isReactComponentName(name) || isReactHookName(name);
|
|
5831
5841
|
//#endregion
|
|
5842
|
+
//#region src/plugin/utils/is-react-hoc-callback-argument.ts
|
|
5843
|
+
const reactHocCalleeName = (callee) => {
|
|
5844
|
+
if (isNodeOfType(callee, "Identifier")) return callee.name;
|
|
5845
|
+
if (isNodeOfType(callee, "MemberExpression") && !callee.computed && isNodeOfType(callee.object, "Identifier") && callee.object.name === "React" && isNodeOfType(callee.property, "Identifier")) return `React.${callee.property.name}`;
|
|
5846
|
+
return null;
|
|
5847
|
+
};
|
|
5848
|
+
const isReactHocCallbackArgument = (functionNode) => {
|
|
5849
|
+
const parent = functionNode.parent;
|
|
5850
|
+
if (!parent || !isNodeOfType(parent, "CallExpression")) return false;
|
|
5851
|
+
if (parent.arguments[0] !== functionNode) return false;
|
|
5852
|
+
const calleeName = reactHocCalleeName(parent.callee);
|
|
5853
|
+
return calleeName !== null && REACT_HOC_NAMES.has(calleeName);
|
|
5854
|
+
};
|
|
5855
|
+
//#endregion
|
|
5832
5856
|
//#region src/plugin/rules/react-builtins/exhaustive-deps-low-level.ts
|
|
5833
5857
|
/**
|
|
5834
5858
|
* Lowest-level helpers consumed by both the main `exhaustive-deps`
|
|
@@ -6059,6 +6083,7 @@ const findEnclosingComponentOrHookFunction$1 = (node) => {
|
|
|
6059
6083
|
let current = node.parent;
|
|
6060
6084
|
while (current) {
|
|
6061
6085
|
if (isNodeOfType(current, "FunctionDeclaration") || isNodeOfType(current, "FunctionExpression") || isNodeOfType(current, "ArrowFunctionExpression")) {
|
|
6086
|
+
if (isReactHocCallbackArgument(current)) return current;
|
|
6062
6087
|
const functionName = inferFunctionName$1(current);
|
|
6063
6088
|
if (functionName && isReactComponentOrHookName(functionName)) return current;
|
|
6064
6089
|
}
|
|
@@ -12542,6 +12567,64 @@ const nextjsInlineScriptMissingId = defineRule({
|
|
|
12542
12567
|
} })
|
|
12543
12568
|
});
|
|
12544
12569
|
//#endregion
|
|
12570
|
+
//#region src/plugin/utils/parse-export-specifiers.ts
|
|
12571
|
+
const getSpecifierName = (rawName) => rawName.replace(/^type\s+/, "").trim();
|
|
12572
|
+
const parseExportSpecifiers = (specifiersText, declarationIsTypeOnly) => specifiersText.split(",").map((specifierText) => specifierText.trim()).filter(Boolean).map((specifierText) => {
|
|
12573
|
+
const isTypeOnly = declarationIsTypeOnly || specifierText.startsWith("type ");
|
|
12574
|
+
const [rawLocalName, rawExportedName] = specifierText.split(/\s+as\s+/);
|
|
12575
|
+
const localName = getSpecifierName(rawLocalName ?? "");
|
|
12576
|
+
return {
|
|
12577
|
+
localName,
|
|
12578
|
+
exportedName: getSpecifierName(rawExportedName ?? localName),
|
|
12579
|
+
isTypeOnly
|
|
12580
|
+
};
|
|
12581
|
+
});
|
|
12582
|
+
//#endregion
|
|
12583
|
+
//#region src/plugin/utils/strip-js-comments.ts
|
|
12584
|
+
const BLOCK_COMMENT_PATTERN = /\/\*[\s\S]*?\*\//g;
|
|
12585
|
+
const LINE_COMMENT_PATTERN = /^\s*\/\/.*$/gm;
|
|
12586
|
+
const stripJsComments = (sourceText) => sourceText.replace(BLOCK_COMMENT_PATTERN, "").replace(LINE_COMMENT_PATTERN, "");
|
|
12587
|
+
//#endregion
|
|
12588
|
+
//#region src/plugin/utils/does-module-export-name.ts
|
|
12589
|
+
const DEFAULT_EXPORT_DECLARATION_PATTERN = /^\s*export\s+default\b/m;
|
|
12590
|
+
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;
|
|
12591
|
+
const LOCAL_EXPORT_SPECIFIER_DECLARATION_PATTERN$1 = /^\s*export\s+(?:type\s+)?\{([\s\S]*?)\}(?:\s+from\s+["'][^"']+["'])?\s*;?\s*(?:(?:\/\/[^\n]*)?\s*)/gm;
|
|
12592
|
+
const doesSourceTextExportName = (sourceText, exportedName) => {
|
|
12593
|
+
const strippedSource = stripJsComments(sourceText);
|
|
12594
|
+
if (exportedName === "default" && DEFAULT_EXPORT_DECLARATION_PATTERN.test(strippedSource)) return true;
|
|
12595
|
+
for (const match of strippedSource.matchAll(NAMED_EXPORT_DECLARATION_PATTERN)) if (match[1] === exportedName) return true;
|
|
12596
|
+
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;
|
|
12597
|
+
return false;
|
|
12598
|
+
};
|
|
12599
|
+
const doesModuleExportName = (filePath, exportedName) => {
|
|
12600
|
+
try {
|
|
12601
|
+
return doesSourceTextExportName(fs.readFileSync(filePath, "utf8"), exportedName);
|
|
12602
|
+
} catch {
|
|
12603
|
+
return false;
|
|
12604
|
+
}
|
|
12605
|
+
};
|
|
12606
|
+
//#endregion
|
|
12607
|
+
//#region src/plugin/utils/has-ancestor-layout-matching.ts
|
|
12608
|
+
const hasAncestorLayoutMatching = (pageFilename, matchesLayout) => {
|
|
12609
|
+
const normalizedPage = pageFilename.replaceAll("\\", "/");
|
|
12610
|
+
let currentDirectory = path.dirname(normalizedPage);
|
|
12611
|
+
for (let level = 0; level < 30; level++) {
|
|
12612
|
+
for (const layoutFileName of LAYOUT_FILE_NAMES) {
|
|
12613
|
+
const layoutPath = path.join(currentDirectory, layoutFileName);
|
|
12614
|
+
if (layoutPath.replaceAll("\\", "/") === normalizedPage) continue;
|
|
12615
|
+
if (matchesLayout(layoutPath)) return true;
|
|
12616
|
+
}
|
|
12617
|
+
const parentDirectory = path.dirname(currentDirectory);
|
|
12618
|
+
if (path.basename(currentDirectory) === "app" && path.basename(parentDirectory) !== "app") break;
|
|
12619
|
+
if (parentDirectory === currentDirectory) break;
|
|
12620
|
+
currentDirectory = parentDirectory;
|
|
12621
|
+
}
|
|
12622
|
+
return false;
|
|
12623
|
+
};
|
|
12624
|
+
//#endregion
|
|
12625
|
+
//#region src/plugin/utils/find-ancestor-metadata-layout.ts
|
|
12626
|
+
const hasAncestorMetadataLayout = (pageFilename) => hasAncestorLayoutMatching(pageFilename, (layoutPath) => METADATA_EXPORT_NAMES.some((exportName) => doesModuleExportName(layoutPath, exportName)));
|
|
12627
|
+
//#endregion
|
|
12545
12628
|
//#region src/plugin/rules/nextjs/nextjs-missing-metadata.ts
|
|
12546
12629
|
const nextjsMissingMetadata = defineRule({
|
|
12547
12630
|
id: "nextjs-missing-metadata",
|
|
@@ -12554,13 +12637,15 @@ const nextjsMissingMetadata = defineRule({
|
|
|
12554
12637
|
const filename = normalizeFilename$1(context.filename ?? "");
|
|
12555
12638
|
if (!PAGE_FILE_PATTERN.test(filename)) return;
|
|
12556
12639
|
if (INTERNAL_PAGE_PATH_PATTERN.test(filename)) return;
|
|
12557
|
-
if (
|
|
12640
|
+
if (programNode.body?.some((statement) => {
|
|
12558
12641
|
if (!isNodeOfType(statement, "ExportNamedDeclaration")) return false;
|
|
12559
12642
|
const declaration = statement.declaration;
|
|
12560
|
-
if (isNodeOfType(declaration, "VariableDeclaration")) return declaration.declarations?.some((declarator) => isNodeOfType(declarator.id, "Identifier") && (declarator.id.name
|
|
12643
|
+
if (isNodeOfType(declaration, "VariableDeclaration")) return declaration.declarations?.some((declarator) => isNodeOfType(declarator.id, "Identifier") && METADATA_EXPORT_NAMES.includes(declarator.id.name));
|
|
12561
12644
|
if (isNodeOfType(declaration, "FunctionDeclaration")) return declaration.id?.name === "generateMetadata";
|
|
12562
|
-
return
|
|
12563
|
-
}))
|
|
12645
|
+
return (statement.specifiers ?? []).some((specifier) => isNodeOfType(specifier, "ExportSpecifier") && isNodeOfType(specifier.exported, "Identifier") && METADATA_EXPORT_NAMES.includes(specifier.exported.name));
|
|
12646
|
+
})) return;
|
|
12647
|
+
if (hasAncestorMetadataLayout(context.filename ?? "")) return;
|
|
12648
|
+
context.report({
|
|
12564
12649
|
node: programNode,
|
|
12565
12650
|
message: "This page has no metadata, so search engines and social previews get no title or description."
|
|
12566
12651
|
});
|
|
@@ -13451,43 +13536,6 @@ const parseSourceFile = (absoluteFilePath) => {
|
|
|
13451
13536
|
return parsedProgram;
|
|
13452
13537
|
};
|
|
13453
13538
|
//#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
13539
|
//#region src/plugin/utils/is-barrel-index-module.ts
|
|
13492
13540
|
const INDEX_MODULE_FILE_PATTERN = /^index\.(?:[cm]?[jt]sx?|mjs)$/;
|
|
13493
13541
|
const BINDING_IMPORT_DECLARATION_PATTERN = /^\s*import\s+(type\s+)?(?!["'])([^;]*?)\s+from\s+["']([^"']+)["']\s*;?\s*(?:(?:\/\/[^\n]*)?\s*)/gm;
|
|
@@ -13971,29 +14019,10 @@ const astMentionsSuspense = (programNode) => {
|
|
|
13971
14019
|
};
|
|
13972
14020
|
//#endregion
|
|
13973
14021
|
//#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
|
-
};
|
|
14022
|
+
const hasAncestorSuspenseLayout = (pageFilename) => hasAncestorLayoutMatching(pageFilename, (layoutPath) => {
|
|
14023
|
+
const programRoot = parseSourceFile(layoutPath);
|
|
14024
|
+
return Boolean(programRoot && astMentionsSuspense(programRoot));
|
|
14025
|
+
});
|
|
13997
14026
|
//#endregion
|
|
13998
14027
|
//#region src/plugin/rules/nextjs/nextjs-no-use-search-params-without-suspense.ts
|
|
13999
14028
|
const astContainsUseSearchParams = (root) => {
|
|
@@ -15323,6 +15352,182 @@ const createRelativeImportSource = (filename, targetFilePath) => {
|
|
|
15323
15352
|
return relativePath.startsWith(".") ? relativePath : `./${relativePath}`;
|
|
15324
15353
|
};
|
|
15325
15354
|
//#endregion
|
|
15355
|
+
//#region src/react-native-dependency-names.ts
|
|
15356
|
+
const EXPO_MANAGED_DEPENDENCY_NAMES = new Set([
|
|
15357
|
+
"expo",
|
|
15358
|
+
"expo-router",
|
|
15359
|
+
"@expo/cli",
|
|
15360
|
+
"@expo/metro-config",
|
|
15361
|
+
"@expo/metro-runtime"
|
|
15362
|
+
]);
|
|
15363
|
+
const REACT_NATIVE_DEPENDENCY_NAMES = new Set([
|
|
15364
|
+
"react-native",
|
|
15365
|
+
"react-native-tvos",
|
|
15366
|
+
...EXPO_MANAGED_DEPENDENCY_NAMES,
|
|
15367
|
+
"react-native-windows",
|
|
15368
|
+
"react-native-macos"
|
|
15369
|
+
]);
|
|
15370
|
+
const REACT_NATIVE_DEPENDENCY_PREFIXES = ["@react-native/", "@react-native-"];
|
|
15371
|
+
const isExpoManagedDependencyName = (dependencyName) => EXPO_MANAGED_DEPENDENCY_NAMES.has(dependencyName);
|
|
15372
|
+
const isReactNativeDependencyName = (dependencyName) => {
|
|
15373
|
+
if (REACT_NATIVE_DEPENDENCY_NAMES.has(dependencyName)) return true;
|
|
15374
|
+
for (const prefix of REACT_NATIVE_DEPENDENCY_PREFIXES) if (dependencyName.startsWith(prefix)) return true;
|
|
15375
|
+
return false;
|
|
15376
|
+
};
|
|
15377
|
+
//#endregion
|
|
15378
|
+
//#region src/plugin/utils/classify-package-platform.ts
|
|
15379
|
+
const WEB_FRAMEWORK_DEPENDENCY_NAMES = new Set([
|
|
15380
|
+
"next",
|
|
15381
|
+
"vite",
|
|
15382
|
+
"react-scripts",
|
|
15383
|
+
"gatsby",
|
|
15384
|
+
"@remix-run/react",
|
|
15385
|
+
"@remix-run/node",
|
|
15386
|
+
"@docusaurus/core",
|
|
15387
|
+
"@docusaurus/preset-classic",
|
|
15388
|
+
"@storybook/react",
|
|
15389
|
+
"@storybook/react-vite",
|
|
15390
|
+
"@storybook/react-webpack5",
|
|
15391
|
+
"@storybook/nextjs",
|
|
15392
|
+
"@storybook/web-components",
|
|
15393
|
+
"storybook",
|
|
15394
|
+
"react-dom",
|
|
15395
|
+
"@vitejs/plugin-react",
|
|
15396
|
+
"@vitejs/plugin-react-swc"
|
|
15397
|
+
]);
|
|
15398
|
+
const cachedPlatformByPackageDirectory = /* @__PURE__ */ new Map();
|
|
15399
|
+
const cachedPackageDirectoryByFilename = /* @__PURE__ */ new Map();
|
|
15400
|
+
const findNearestPackageDirectory = (filename) => {
|
|
15401
|
+
if (!filename) return null;
|
|
15402
|
+
const fromCache = cachedPackageDirectoryByFilename.get(filename);
|
|
15403
|
+
if (fromCache !== void 0) return fromCache;
|
|
15404
|
+
let currentDirectory = path.dirname(filename);
|
|
15405
|
+
while (true) {
|
|
15406
|
+
const candidatePackageJsonPath = path.join(currentDirectory, "package.json");
|
|
15407
|
+
let hasPackageJson = false;
|
|
15408
|
+
try {
|
|
15409
|
+
hasPackageJson = fs.statSync(candidatePackageJsonPath).isFile();
|
|
15410
|
+
} catch {
|
|
15411
|
+
hasPackageJson = false;
|
|
15412
|
+
}
|
|
15413
|
+
if (hasPackageJson) {
|
|
15414
|
+
cachedPackageDirectoryByFilename.set(filename, currentDirectory);
|
|
15415
|
+
return currentDirectory;
|
|
15416
|
+
}
|
|
15417
|
+
const parentDirectory = path.dirname(currentDirectory);
|
|
15418
|
+
if (parentDirectory === currentDirectory) {
|
|
15419
|
+
cachedPackageDirectoryByFilename.set(filename, null);
|
|
15420
|
+
return null;
|
|
15421
|
+
}
|
|
15422
|
+
currentDirectory = parentDirectory;
|
|
15423
|
+
}
|
|
15424
|
+
};
|
|
15425
|
+
const readPackageJsonSafe = (packageJsonPath) => {
|
|
15426
|
+
let rawContents;
|
|
15427
|
+
try {
|
|
15428
|
+
rawContents = fs.readFileSync(packageJsonPath, "utf-8");
|
|
15429
|
+
} catch {
|
|
15430
|
+
return null;
|
|
15431
|
+
}
|
|
15432
|
+
try {
|
|
15433
|
+
const parsed = JSON.parse(rawContents);
|
|
15434
|
+
if (typeof parsed === "object" && parsed !== null) return parsed;
|
|
15435
|
+
return null;
|
|
15436
|
+
} catch {
|
|
15437
|
+
return null;
|
|
15438
|
+
}
|
|
15439
|
+
};
|
|
15440
|
+
const DEPENDENCY_SECTION_NAMES = [
|
|
15441
|
+
"dependencies",
|
|
15442
|
+
"devDependencies",
|
|
15443
|
+
"peerDependencies",
|
|
15444
|
+
"optionalDependencies"
|
|
15445
|
+
];
|
|
15446
|
+
const iterateDependencyNames = function* (packageJson) {
|
|
15447
|
+
for (const sectionName of DEPENDENCY_SECTION_NAMES) {
|
|
15448
|
+
const section = packageJson[sectionName];
|
|
15449
|
+
if (!section) continue;
|
|
15450
|
+
for (const dependencyName of Object.keys(section)) yield dependencyName;
|
|
15451
|
+
}
|
|
15452
|
+
};
|
|
15453
|
+
const isReactNativeAware = (packageJson) => {
|
|
15454
|
+
if (typeof packageJson["react-native"] === "string") return true;
|
|
15455
|
+
for (const dependencyName of iterateDependencyNames(packageJson)) if (isReactNativeDependencyName(dependencyName)) return true;
|
|
15456
|
+
return false;
|
|
15457
|
+
};
|
|
15458
|
+
const isExpoManaged = (packageJson) => {
|
|
15459
|
+
for (const dependencyName of iterateDependencyNames(packageJson)) if (isExpoManagedDependencyName(dependencyName)) return true;
|
|
15460
|
+
return false;
|
|
15461
|
+
};
|
|
15462
|
+
const isWebFrameworkOnly = (packageJson) => {
|
|
15463
|
+
for (const dependencyName of iterateDependencyNames(packageJson)) if (WEB_FRAMEWORK_DEPENDENCY_NAMES.has(dependencyName)) return true;
|
|
15464
|
+
return false;
|
|
15465
|
+
};
|
|
15466
|
+
const classifyPackagePlatform = (filename) => {
|
|
15467
|
+
const packageDirectory = findNearestPackageDirectory(filename);
|
|
15468
|
+
if (!packageDirectory) return "unknown";
|
|
15469
|
+
const cached = cachedPlatformByPackageDirectory.get(packageDirectory);
|
|
15470
|
+
if (cached !== void 0) return cached;
|
|
15471
|
+
const packageJson = readPackageJsonSafe(path.join(packageDirectory, "package.json"));
|
|
15472
|
+
if (!packageJson) {
|
|
15473
|
+
cachedPlatformByPackageDirectory.set(packageDirectory, "unknown");
|
|
15474
|
+
return "unknown";
|
|
15475
|
+
}
|
|
15476
|
+
let result;
|
|
15477
|
+
if (isExpoManaged(packageJson)) result = "expo";
|
|
15478
|
+
else if (isReactNativeAware(packageJson)) result = "react-native";
|
|
15479
|
+
else if (isWebFrameworkOnly(packageJson)) result = "web";
|
|
15480
|
+
else result = "unknown";
|
|
15481
|
+
cachedPlatformByPackageDirectory.set(packageDirectory, result);
|
|
15482
|
+
return result;
|
|
15483
|
+
};
|
|
15484
|
+
//#endregion
|
|
15485
|
+
//#region src/plugin/utils/get-react-doctor-setting.ts
|
|
15486
|
+
const readReactDoctorSettingsBag = (settings) => {
|
|
15487
|
+
const reactDoctorSettings = settings?.["react-doctor"];
|
|
15488
|
+
if (typeof reactDoctorSettings !== "object" || reactDoctorSettings === null || Array.isArray(reactDoctorSettings)) return null;
|
|
15489
|
+
return reactDoctorSettings;
|
|
15490
|
+
};
|
|
15491
|
+
const readOwnPropertyValue = (bag, settingName) => Object.getOwnPropertyDescriptor(bag, settingName)?.value;
|
|
15492
|
+
const getReactDoctorStringSetting = (settings, settingName) => {
|
|
15493
|
+
const bag = readReactDoctorSettingsBag(settings);
|
|
15494
|
+
if (!bag) return void 0;
|
|
15495
|
+
const settingValue = readOwnPropertyValue(bag, settingName);
|
|
15496
|
+
return typeof settingValue === "string" ? settingValue : void 0;
|
|
15497
|
+
};
|
|
15498
|
+
const getReactDoctorNumberSetting = (settings, settingName) => {
|
|
15499
|
+
const bag = readReactDoctorSettingsBag(settings);
|
|
15500
|
+
if (!bag) return void 0;
|
|
15501
|
+
const settingValue = readOwnPropertyValue(bag, settingName);
|
|
15502
|
+
return typeof settingValue === "number" && Number.isFinite(settingValue) ? settingValue : void 0;
|
|
15503
|
+
};
|
|
15504
|
+
const getReactDoctorStringArraySetting = (settings, settingName) => {
|
|
15505
|
+
const bag = readReactDoctorSettingsBag(settings);
|
|
15506
|
+
if (!bag) return [];
|
|
15507
|
+
const settingValue = readOwnPropertyValue(bag, settingName);
|
|
15508
|
+
if (!Array.isArray(settingValue)) return [];
|
|
15509
|
+
return settingValue.filter((entry) => typeof entry === "string" && entry.length > 0);
|
|
15510
|
+
};
|
|
15511
|
+
//#endregion
|
|
15512
|
+
//#region src/plugin/utils/is-react-native-file.ts
|
|
15513
|
+
const WEB_FILE_EXTENSION_PATTERN = /\.web\.[cm]?[jt]sx?$/;
|
|
15514
|
+
const NATIVE_FILE_EXTENSION_PATTERN = /\.(?:ios|android|native)\.[cm]?[jt]sx?$/;
|
|
15515
|
+
const classifyReactNativeFileTarget = (context) => {
|
|
15516
|
+
const rawFilename = context.filename;
|
|
15517
|
+
if (!rawFilename) return "unknown";
|
|
15518
|
+
const filename = normalizeFilename$1(rawFilename);
|
|
15519
|
+
if (NATIVE_FILE_EXTENSION_PATTERN.test(filename)) return "react-native";
|
|
15520
|
+
if (WEB_FILE_EXTENSION_PATTERN.test(filename)) return "web";
|
|
15521
|
+
const packagePlatform = classifyPackagePlatform(filename);
|
|
15522
|
+
if (packagePlatform === "web") return "web";
|
|
15523
|
+
if (packagePlatform === "expo" || packagePlatform === "react-native") return "react-native";
|
|
15524
|
+
const framework = getReactDoctorStringSetting(context.settings, "framework");
|
|
15525
|
+
if (framework === "react-native" || framework === "expo") return "react-native";
|
|
15526
|
+
if (framework === "nextjs" || framework === "vite" || framework === "cra" || framework === "remix" || framework === "gatsby" || framework === "tanstack-start") return "web";
|
|
15527
|
+
return "unknown";
|
|
15528
|
+
};
|
|
15529
|
+
const isReactNativeFileActive = (context) => classifyReactNativeFileTarget(context) !== "web";
|
|
15530
|
+
//#endregion
|
|
15326
15531
|
//#region src/plugin/rules/bundle-size/no-barrel-import.ts
|
|
15327
15532
|
const getLiteralName = (node) => {
|
|
15328
15533
|
if (node.type === "Identifier" && typeof node.name === "string") return node.name;
|
|
@@ -15340,7 +15545,8 @@ const getRuntimeImportRequests = (node) => {
|
|
|
15340
15545
|
return [{ importedName: null }];
|
|
15341
15546
|
});
|
|
15342
15547
|
};
|
|
15343
|
-
const buildReportMessage = (filename, barrelFilePath, importRequests) => {
|
|
15548
|
+
const buildReportMessage = (filename, barrelFilePath, importRequests, isReactNativeTarget) => {
|
|
15549
|
+
const costSentence = isReactNativeTarget ? "This ships extra code in your app bundle & slows startup." : "This ships extra code to your users & slows page load.";
|
|
15344
15550
|
const directImportSources = /* @__PURE__ */ new Set();
|
|
15345
15551
|
for (const request of importRequests) {
|
|
15346
15552
|
if (!request.importedName) continue;
|
|
@@ -15349,9 +15555,9 @@ const buildReportMessage = (filename, barrelFilePath, importRequests) => {
|
|
|
15349
15555
|
}
|
|
15350
15556
|
if (directImportSources.size === 1) {
|
|
15351
15557
|
const [directImportSource] = directImportSources;
|
|
15352
|
-
return
|
|
15558
|
+
return `${costSentence} Import directly from "${directImportSource}".`;
|
|
15353
15559
|
}
|
|
15354
|
-
if (directImportSources.size > 1) return
|
|
15560
|
+
if (directImportSources.size > 1) return `${costSentence} Import directly from: ${[...directImportSources].map((source) => `"${source}"`).join(", ")}.`;
|
|
15355
15561
|
return "Importing from an index file pulls in extra code. Import directly from the source file instead.";
|
|
15356
15562
|
};
|
|
15357
15563
|
const noBarrelImport = defineRule({
|
|
@@ -15375,7 +15581,7 @@ const noBarrelImport = defineRule({
|
|
|
15375
15581
|
didReportForFile = true;
|
|
15376
15582
|
context.report({
|
|
15377
15583
|
node,
|
|
15378
|
-
message: buildReportMessage(filename, resolvedImportPath, importRequests)
|
|
15584
|
+
message: buildReportMessage(filename, resolvedImportPath, importRequests, classifyReactNativeFileTarget(context) === "react-native")
|
|
15379
15585
|
});
|
|
15380
15586
|
}
|
|
15381
15587
|
} };
|
|
@@ -20668,33 +20874,6 @@ const noPolymorphicChildren = defineRule({
|
|
|
20668
20874
|
} })
|
|
20669
20875
|
});
|
|
20670
20876
|
//#endregion
|
|
20671
|
-
//#region src/plugin/utils/get-react-doctor-setting.ts
|
|
20672
|
-
const readReactDoctorSettingsBag = (settings) => {
|
|
20673
|
-
const reactDoctorSettings = settings?.["react-doctor"];
|
|
20674
|
-
if (typeof reactDoctorSettings !== "object" || reactDoctorSettings === null || Array.isArray(reactDoctorSettings)) return null;
|
|
20675
|
-
return reactDoctorSettings;
|
|
20676
|
-
};
|
|
20677
|
-
const readOwnPropertyValue = (bag, settingName) => Object.getOwnPropertyDescriptor(bag, settingName)?.value;
|
|
20678
|
-
const getReactDoctorStringSetting = (settings, settingName) => {
|
|
20679
|
-
const bag = readReactDoctorSettingsBag(settings);
|
|
20680
|
-
if (!bag) return void 0;
|
|
20681
|
-
const settingValue = readOwnPropertyValue(bag, settingName);
|
|
20682
|
-
return typeof settingValue === "string" ? settingValue : void 0;
|
|
20683
|
-
};
|
|
20684
|
-
const getReactDoctorNumberSetting = (settings, settingName) => {
|
|
20685
|
-
const bag = readReactDoctorSettingsBag(settings);
|
|
20686
|
-
if (!bag) return void 0;
|
|
20687
|
-
const settingValue = readOwnPropertyValue(bag, settingName);
|
|
20688
|
-
return typeof settingValue === "number" && Number.isFinite(settingValue) ? settingValue : void 0;
|
|
20689
|
-
};
|
|
20690
|
-
const getReactDoctorStringArraySetting = (settings, settingName) => {
|
|
20691
|
-
const bag = readReactDoctorSettingsBag(settings);
|
|
20692
|
-
if (!bag) return [];
|
|
20693
|
-
const settingValue = readOwnPropertyValue(bag, settingName);
|
|
20694
|
-
if (!Array.isArray(settingValue)) return [];
|
|
20695
|
-
return settingValue.filter((entry) => typeof entry === "string" && entry.length > 0);
|
|
20696
|
-
};
|
|
20697
|
-
//#endregion
|
|
20698
20877
|
//#region src/plugin/rules/correctness/no-prevent-default.ts
|
|
20699
20878
|
const PREVENT_DEFAULT_ELEMENTS = new Map([["form", ["onSubmit"]], ["a", ["onClick"]]]);
|
|
20700
20879
|
const SERVER_CAPABLE_FRAMEWORKS = new Set([
|
|
@@ -24343,6 +24522,21 @@ const noZIndex9999 = defineRule({
|
|
|
24343
24522
|
})
|
|
24344
24523
|
});
|
|
24345
24524
|
//#endregion
|
|
24525
|
+
//#region src/plugin/utils/is-framework-route-or-special-filename.ts
|
|
24526
|
+
const sourceFileExtensionGroup = NEXTJS_SOURCE_FILE_EXTENSION_GROUP;
|
|
24527
|
+
const FRAMEWORK_ROUTE_FILE_PATTERNS = [
|
|
24528
|
+
new RegExp(`^(page|layout|loading|error|not-found|template|default|global-error|route|_app|_document|_error)\\.${sourceFileExtensionGroup}$`),
|
|
24529
|
+
new RegExp(`^(_layout|\\+html|\\+not-found|\\+native-intent)\\.${sourceFileExtensionGroup}$`),
|
|
24530
|
+
new RegExp(`(?:^__root|\\.lazy)\\.${sourceFileExtensionGroup}$`),
|
|
24531
|
+
new RegExp(`^(root|entry\\.client|entry\\.server)\\.${sourceFileExtensionGroup}$`)
|
|
24532
|
+
];
|
|
24533
|
+
const isFrameworkRouteOrSpecialFilename = (rawFilename) => {
|
|
24534
|
+
if (!rawFilename) return false;
|
|
24535
|
+
if (isNextjsMetadataImageRouteFilename(rawFilename)) return true;
|
|
24536
|
+
const basename = path.basename(rawFilename);
|
|
24537
|
+
return FRAMEWORK_ROUTE_FILE_PATTERNS.some((pattern) => pattern.test(basename));
|
|
24538
|
+
};
|
|
24539
|
+
//#endregion
|
|
24346
24540
|
//#region src/plugin/rules/react-builtins/only-export-components-tables.ts
|
|
24347
24541
|
const NOT_REACT_COMPONENT_EXPRESSION_TYPES = new Set([
|
|
24348
24542
|
"ArrayExpression",
|
|
@@ -24394,32 +24588,6 @@ const ENTRY_POINT_BASENAMES = new Set([
|
|
|
24394
24588
|
"client.jsx",
|
|
24395
24589
|
"server.tsx",
|
|
24396
24590
|
"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
24591
|
"app.tsx",
|
|
24424
24592
|
"app.jsx",
|
|
24425
24593
|
"App.tsx",
|
|
@@ -24499,6 +24667,54 @@ const UTILITY_FILE_BASENAMES = new Set([
|
|
|
24499
24667
|
"defaults.tsx",
|
|
24500
24668
|
"defaults.jsx"
|
|
24501
24669
|
]);
|
|
24670
|
+
const ROUTE_FACTORY_CALLEE_NAMES = new Set([
|
|
24671
|
+
...TANSTACK_ROUTE_CREATION_FUNCTIONS,
|
|
24672
|
+
"createLazyFileRoute",
|
|
24673
|
+
"createLazyRoute",
|
|
24674
|
+
"createAPIFileRoute",
|
|
24675
|
+
"createServerFileRoute",
|
|
24676
|
+
"createServerRootRoute",
|
|
24677
|
+
"createServerRoute",
|
|
24678
|
+
"createBrowserRouter",
|
|
24679
|
+
"createHashRouter",
|
|
24680
|
+
"createMemoryRouter",
|
|
24681
|
+
"createStaticRouter",
|
|
24682
|
+
"createRouter"
|
|
24683
|
+
]);
|
|
24684
|
+
const ROUTE_MODULE_ALLOWED_EXPORT_NAMES = new Set([
|
|
24685
|
+
"loader",
|
|
24686
|
+
"clientLoader",
|
|
24687
|
+
"action",
|
|
24688
|
+
"clientAction",
|
|
24689
|
+
"headers",
|
|
24690
|
+
"meta",
|
|
24691
|
+
"links",
|
|
24692
|
+
"handle",
|
|
24693
|
+
"shouldRevalidate",
|
|
24694
|
+
"middleware",
|
|
24695
|
+
"unstable_middleware",
|
|
24696
|
+
"getServerSideProps",
|
|
24697
|
+
"getStaticProps",
|
|
24698
|
+
"getStaticPaths",
|
|
24699
|
+
"getInitialProps",
|
|
24700
|
+
"reportWebVitals",
|
|
24701
|
+
"metadata",
|
|
24702
|
+
"generateMetadata",
|
|
24703
|
+
"generateStaticParams",
|
|
24704
|
+
"generateImageMetadata",
|
|
24705
|
+
"generateSitemaps",
|
|
24706
|
+
"viewport",
|
|
24707
|
+
"generateViewport",
|
|
24708
|
+
"revalidate",
|
|
24709
|
+
"dynamic",
|
|
24710
|
+
"dynamicParams",
|
|
24711
|
+
"fetchCache",
|
|
24712
|
+
"runtime",
|
|
24713
|
+
"preferredRegion",
|
|
24714
|
+
"maxDuration",
|
|
24715
|
+
"experimental_ppr",
|
|
24716
|
+
"unstable_settings"
|
|
24717
|
+
]);
|
|
24502
24718
|
//#endregion
|
|
24503
24719
|
//#region src/plugin/rules/react-builtins/only-export-components.ts
|
|
24504
24720
|
const NAMED_EXPORT_MESSAGE = "This file exports non-components, so Fast Refresh can't safely preserve component state.";
|
|
@@ -24542,6 +24758,18 @@ const isReactCreateContext = (initializer) => {
|
|
|
24542
24758
|
if (isNodeOfType(callee, "MemberExpression") && isNodeOfType(callee.property, "Identifier") && callee.property.name === "createContext") return true;
|
|
24543
24759
|
return false;
|
|
24544
24760
|
};
|
|
24761
|
+
const isRouteFactoryName = (name) => ROUTE_FACTORY_CALLEE_NAMES.has(name);
|
|
24762
|
+
const isRouteFactoryCall = (expression) => {
|
|
24763
|
+
let currentCall = expression;
|
|
24764
|
+
while (isNodeOfType(currentCall, "CallExpression")) {
|
|
24765
|
+
const callee = currentCall.callee;
|
|
24766
|
+
if (isNodeOfType(callee, "Identifier") && isRouteFactoryName(callee.name)) return true;
|
|
24767
|
+
if (isNodeOfType(callee, "MemberExpression") && isNodeOfType(callee.property, "Identifier") && isRouteFactoryName(callee.property.name)) return true;
|
|
24768
|
+
if (!isNodeOfType(callee, "CallExpression")) return false;
|
|
24769
|
+
currentCall = callee;
|
|
24770
|
+
}
|
|
24771
|
+
return false;
|
|
24772
|
+
};
|
|
24545
24773
|
const isReactHocName = (name, state) => state.customHocs.has(name);
|
|
24546
24774
|
const isHocCallee = (callee, state) => {
|
|
24547
24775
|
if (isNodeOfType(callee, "Identifier")) return isReactHocName(callee.name, state);
|
|
@@ -24575,10 +24803,12 @@ const isReactComponentInitializer = (expression, state) => {
|
|
|
24575
24803
|
const classifyExport = (name, reportNode, isFunction, initializer, state) => {
|
|
24576
24804
|
if (initializer) {
|
|
24577
24805
|
const expression = skipTsExpression(initializer);
|
|
24806
|
+
if (isRouteFactoryCall(expression)) return { kind: "react-component" };
|
|
24578
24807
|
if (isNodeOfType(expression, "CallExpression") && isHocCallee(expression.callee, state) && expression.arguments.length > 0 && isReactComponentName(name)) return { kind: "react-component" };
|
|
24579
24808
|
if (isNodeOfType(expression, "ConditionalExpression") && isReactComponentName(name) && isReactComponentInitializer(expression.consequent, state) && isReactComponentInitializer(expression.alternate, state)) return { kind: "react-component" };
|
|
24580
24809
|
}
|
|
24581
24810
|
if (state.allowExportNames.has(name)) return { kind: "allowed" };
|
|
24811
|
+
if (ROUTE_MODULE_ALLOWED_EXPORT_NAMES.has(name)) return { kind: "allowed" };
|
|
24582
24812
|
if (/^use[A-Z]/.test(name)) return { kind: "allowed" };
|
|
24583
24813
|
if (state.allowConstantExport && initializer) {
|
|
24584
24814
|
const expression = skipTsExpression(initializer);
|
|
@@ -24650,6 +24880,7 @@ const isFileNameAllowed = (filename, checkJS) => {
|
|
|
24650
24880
|
if (filename.includes(".test.") || filename.includes(".spec.") || filename.includes(".cy.") || filename.includes(".stories.")) return false;
|
|
24651
24881
|
for (const segment of NON_FAST_REFRESH_PATH_SEGMENTS) if (filename.includes(segment)) return false;
|
|
24652
24882
|
if (isEntryPointFile(filename)) return false;
|
|
24883
|
+
if (isFrameworkRouteOrSpecialFilename(filename)) return false;
|
|
24653
24884
|
if (isAssetOrUtilityFile(filename)) return false;
|
|
24654
24885
|
if (filename.endsWith(".tsx") || filename.endsWith(".jsx")) return true;
|
|
24655
24886
|
if (checkJS && filename.endsWith(".js")) return true;
|
|
@@ -24724,6 +24955,10 @@ const onlyExportComponents = defineRule({
|
|
|
24724
24955
|
continue;
|
|
24725
24956
|
}
|
|
24726
24957
|
if (isNodeOfType(stripped, "CallExpression")) {
|
|
24958
|
+
if (isRouteFactoryCall(stripped)) {
|
|
24959
|
+
hasReactExport = true;
|
|
24960
|
+
continue;
|
|
24961
|
+
}
|
|
24727
24962
|
const isHoc = isHocCallee(stripped.callee, state);
|
|
24728
24963
|
const firstArg = stripped.arguments[0];
|
|
24729
24964
|
const firstArgIsValid = Boolean(firstArg) && (() => {
|
|
@@ -28518,29 +28753,236 @@ const isInsidePlatformOsWebBranch = (node) => {
|
|
|
28518
28753
|
//#endregion
|
|
28519
28754
|
//#region src/plugin/rules/react-native/utils/collect-text-wrapper-components.ts
|
|
28520
28755
|
const isFunctionNode = (node) => isNodeOfType(node, "ArrowFunctionExpression") || isNodeOfType(node, "FunctionExpression") || isNodeOfType(node, "FunctionDeclaration");
|
|
28521
|
-
const
|
|
28756
|
+
const COMPONENT_WRAPPER_CALLEE_NAMES = new Set(["memo", "forwardRef"]);
|
|
28757
|
+
const resolveCalleeName = (callee) => {
|
|
28758
|
+
if (isNodeOfType(callee, "Identifier")) return callee.name;
|
|
28759
|
+
if (isNodeOfType(callee, "MemberExpression") && isNodeOfType(callee.property, "Identifier")) return callee.property.name;
|
|
28760
|
+
return null;
|
|
28761
|
+
};
|
|
28762
|
+
const unwrapComponentDefinition = (node) => {
|
|
28763
|
+
let current = stripParenExpression(node);
|
|
28764
|
+
while (isNodeOfType(current, "CallExpression")) {
|
|
28765
|
+
const calleeName = resolveCalleeName(current.callee);
|
|
28766
|
+
const firstArgument = current.arguments?.[0];
|
|
28767
|
+
if (!calleeName || !COMPONENT_WRAPPER_CALLEE_NAMES.has(calleeName) || !firstArgument) break;
|
|
28768
|
+
current = stripParenExpression(firstArgument);
|
|
28769
|
+
}
|
|
28770
|
+
return current;
|
|
28771
|
+
};
|
|
28772
|
+
const resolveChildrenPropertyLocalName = (property) => {
|
|
28773
|
+
if (!isNodeOfType(property, "Property")) return null;
|
|
28774
|
+
if (!isNodeOfType(property.key, "Identifier") || property.key.name !== "children") return null;
|
|
28775
|
+
const value = property.value;
|
|
28776
|
+
if (isNodeOfType(value, "Identifier")) return value.name;
|
|
28777
|
+
if (isNodeOfType(value, "AssignmentPattern") && isNodeOfType(value.left, "Identifier")) return value.left.name;
|
|
28778
|
+
return null;
|
|
28779
|
+
};
|
|
28780
|
+
const resolveParamChildrenBindings = (functionNode) => {
|
|
28781
|
+
const bindings = {
|
|
28782
|
+
childrenNames: /* @__PURE__ */ new Set(),
|
|
28783
|
+
propsObjectNames: /* @__PURE__ */ new Set()
|
|
28784
|
+
};
|
|
28785
|
+
const firstParam = functionNode.params?.[0];
|
|
28786
|
+
if (!firstParam) return bindings;
|
|
28787
|
+
if (isNodeOfType(firstParam, "Identifier")) {
|
|
28788
|
+
bindings.propsObjectNames.add(firstParam.name);
|
|
28789
|
+
return bindings;
|
|
28790
|
+
}
|
|
28791
|
+
if (!isNodeOfType(firstParam, "ObjectPattern")) return bindings;
|
|
28792
|
+
let didDestructureChildren = false;
|
|
28793
|
+
let restName = null;
|
|
28794
|
+
for (const property of firstParam.properties ?? []) {
|
|
28795
|
+
if (isNodeOfType(property, "RestElement") && isNodeOfType(property.argument, "Identifier")) {
|
|
28796
|
+
restName = property.argument.name;
|
|
28797
|
+
continue;
|
|
28798
|
+
}
|
|
28799
|
+
const localName = resolveChildrenPropertyLocalName(property);
|
|
28800
|
+
if (localName) {
|
|
28801
|
+
didDestructureChildren = true;
|
|
28802
|
+
bindings.childrenNames.add(localName);
|
|
28803
|
+
}
|
|
28804
|
+
}
|
|
28805
|
+
if (restName && !didDestructureChildren) bindings.propsObjectNames.add(restName);
|
|
28806
|
+
return bindings;
|
|
28807
|
+
};
|
|
28808
|
+
const MAX_CHILDREN_ALIAS_PASSES = 3;
|
|
28809
|
+
const isPropsObjectExpression = (expression, bindings) => {
|
|
28810
|
+
if (!expression) return false;
|
|
28811
|
+
const value = stripParenExpression(expression);
|
|
28812
|
+
if (isNodeOfType(value, "Identifier")) return bindings.propsObjectNames.has(value.name);
|
|
28813
|
+
return isNodeOfType(value, "MemberExpression") && isNodeOfType(value.object, "ThisExpression") && isNodeOfType(value.property, "Identifier") && value.property.name === "props";
|
|
28814
|
+
};
|
|
28815
|
+
const isChildrenValueExpression = (expression, bindings) => {
|
|
28816
|
+
if (!expression) return false;
|
|
28817
|
+
const value = stripParenExpression(expression);
|
|
28818
|
+
if (isNodeOfType(value, "Identifier")) return bindings.childrenNames.has(value.name);
|
|
28819
|
+
return isNodeOfType(value, "MemberExpression") && isNodeOfType(value.property, "Identifier") && value.property.name === "children" && isPropsObjectExpression(value.object, bindings);
|
|
28820
|
+
};
|
|
28821
|
+
const collectChildrenAliases = (functionNode, bindings) => {
|
|
28522
28822
|
const { body } = functionNode;
|
|
28523
|
-
if (!body) return
|
|
28524
|
-
|
|
28525
|
-
|
|
28526
|
-
|
|
28527
|
-
|
|
28528
|
-
|
|
28823
|
+
if (!body || !isNodeOfType(body, "BlockStatement")) return;
|
|
28824
|
+
for (let pass = 0; pass < MAX_CHILDREN_ALIAS_PASSES; pass += 1) {
|
|
28825
|
+
const sizeBeforePass = bindings.childrenNames.size;
|
|
28826
|
+
walkAst(body, (node) => {
|
|
28827
|
+
if (isFunctionNode(node)) return false;
|
|
28828
|
+
if (!isNodeOfType(node, "VariableDeclarator") || !node.init) return void 0;
|
|
28829
|
+
if (isNodeOfType(node.id, "Identifier")) {
|
|
28830
|
+
if (isChildrenValueExpression(node.init, bindings)) bindings.childrenNames.add(node.id.name);
|
|
28831
|
+
return;
|
|
28832
|
+
}
|
|
28833
|
+
if (isNodeOfType(node.id, "ObjectPattern") && isPropsObjectExpression(node.init, bindings)) for (const property of node.id.properties ?? []) {
|
|
28834
|
+
const localName = resolveChildrenPropertyLocalName(property);
|
|
28835
|
+
if (localName) bindings.childrenNames.add(localName);
|
|
28836
|
+
}
|
|
28837
|
+
});
|
|
28838
|
+
if (bindings.childrenNames.size === sizeBeforePass) break;
|
|
28839
|
+
}
|
|
28840
|
+
};
|
|
28841
|
+
const collectJsxRootsFromExpression = (expression, roots) => {
|
|
28842
|
+
const value = stripParenExpression(expression);
|
|
28843
|
+
if (isNodeOfType(value, "JSXElement") || isNodeOfType(value, "JSXFragment")) {
|
|
28844
|
+
roots.push(value);
|
|
28845
|
+
return;
|
|
28846
|
+
}
|
|
28847
|
+
if (isNodeOfType(value, "ConditionalExpression")) {
|
|
28848
|
+
if (value.consequent) collectJsxRootsFromExpression(value.consequent, roots);
|
|
28849
|
+
if (value.alternate) collectJsxRootsFromExpression(value.alternate, roots);
|
|
28850
|
+
return;
|
|
28851
|
+
}
|
|
28852
|
+
if (isNodeOfType(value, "LogicalExpression")) {
|
|
28853
|
+
if (value.left) collectJsxRootsFromExpression(value.left, roots);
|
|
28854
|
+
if (value.right) collectJsxRootsFromExpression(value.right, roots);
|
|
28855
|
+
}
|
|
28856
|
+
};
|
|
28857
|
+
const collectReturnedJsxRoots = (functionNode) => {
|
|
28858
|
+
const roots = [];
|
|
28859
|
+
const { body } = functionNode;
|
|
28860
|
+
if (!body) return roots;
|
|
28861
|
+
if (!isNodeOfType(body, "BlockStatement")) {
|
|
28862
|
+
collectJsxRootsFromExpression(body, roots);
|
|
28863
|
+
return roots;
|
|
28864
|
+
}
|
|
28865
|
+
walkAst(body, (node) => {
|
|
28866
|
+
if (isFunctionNode(node) && node !== functionNode) return false;
|
|
28867
|
+
if (isNodeOfType(node, "ReturnStatement") && node.argument) {
|
|
28868
|
+
collectJsxRootsFromExpression(node.argument, roots);
|
|
28869
|
+
return false;
|
|
28870
|
+
}
|
|
28871
|
+
});
|
|
28872
|
+
return roots;
|
|
28873
|
+
};
|
|
28874
|
+
const isChildrenForwardingJsxChild = (child, bindings) => isNodeOfType(child, "JSXExpressionContainer") && isChildrenValueExpression(child.expression, bindings);
|
|
28875
|
+
const isChildrenForwardingAttribute = (attribute, bindings) => {
|
|
28876
|
+
if (isNodeOfType(attribute, "JSXSpreadAttribute")) return isPropsObjectExpression(attribute.argument, bindings);
|
|
28877
|
+
return isNodeOfType(attribute, "JSXAttribute") && isNodeOfType(attribute.name, "JSXIdentifier") && attribute.name.name === "children" && isNodeOfType(attribute.value, "JSXExpressionContainer") && isChildrenValueExpression(attribute.value.expression, bindings);
|
|
28878
|
+
};
|
|
28879
|
+
const jsxRootForwardsChildrenIntoText = (jsxRoot, bindings, isTextHandlingElement) => {
|
|
28880
|
+
let didForwardIntoText = false;
|
|
28881
|
+
walkAst(jsxRoot, (node) => {
|
|
28882
|
+
if (didForwardIntoText || isFunctionNode(node)) return false;
|
|
28883
|
+
if (!isNodeOfType(node, "JSXElement")) return void 0;
|
|
28884
|
+
const elementName = resolveJsxElementName(node.openingElement);
|
|
28885
|
+
if (!elementName || !isTextHandlingElement(elementName)) return;
|
|
28886
|
+
didForwardIntoText = (node.children ?? []).some((child) => isChildrenForwardingJsxChild(child, bindings)) || (node.openingElement.attributes ?? []).some((attribute) => isChildrenForwardingAttribute(attribute, bindings));
|
|
28887
|
+
});
|
|
28888
|
+
return didForwardIntoText;
|
|
28889
|
+
};
|
|
28890
|
+
const isMeaningfulJsxChild = (child) => !isNodeOfType(child, "JSXText") || Boolean(child.value?.trim());
|
|
28891
|
+
const jsxRootRendersChildrenOutsideText = (jsxRoot, bindings, isTextHandlingElement) => {
|
|
28892
|
+
let didRenderOutsideText = false;
|
|
28893
|
+
walkAst(jsxRoot, (node) => {
|
|
28894
|
+
if (didRenderOutsideText || isFunctionNode(node)) return false;
|
|
28895
|
+
if (!isNodeOfType(node, "JSXElement") && !isNodeOfType(node, "JSXFragment")) return;
|
|
28896
|
+
if (isNodeOfType(node, "JSXElement")) {
|
|
28897
|
+
const elementName = resolveJsxElementName(node.openingElement);
|
|
28898
|
+
if (elementName && isTextHandlingElement(elementName)) return false;
|
|
28899
|
+
if (!(node.children ?? []).some(isMeaningfulJsxChild) && (node.openingElement.attributes ?? []).some((attribute) => isChildrenForwardingAttribute(attribute, bindings))) {
|
|
28900
|
+
didRenderOutsideText = true;
|
|
28901
|
+
return;
|
|
28902
|
+
}
|
|
28903
|
+
}
|
|
28904
|
+
didRenderOutsideText = (node.children ?? []).some((child) => isChildrenForwardingJsxChild(child, bindings));
|
|
28905
|
+
});
|
|
28906
|
+
return didRenderOutsideText;
|
|
28907
|
+
};
|
|
28908
|
+
const resolveStyledFactoryBaseName = (definitionNode) => {
|
|
28909
|
+
let current = stripParenExpression(definitionNode);
|
|
28910
|
+
while (current) {
|
|
28911
|
+
if (isNodeOfType(current, "TaggedTemplateExpression")) {
|
|
28912
|
+
current = stripParenExpression(current.tag);
|
|
28913
|
+
continue;
|
|
28914
|
+
}
|
|
28915
|
+
if (isNodeOfType(current, "CallExpression")) {
|
|
28916
|
+
const callee = stripParenExpression(current.callee);
|
|
28917
|
+
if (isNodeOfType(callee, "Identifier") && callee.name === "styled") {
|
|
28918
|
+
const baseArgument = current.arguments?.[0];
|
|
28919
|
+
if (!baseArgument) return null;
|
|
28920
|
+
const base = stripParenExpression(baseArgument);
|
|
28921
|
+
return isNodeOfType(base, "Identifier") ? base.name : null;
|
|
28922
|
+
}
|
|
28923
|
+
current = callee;
|
|
28924
|
+
continue;
|
|
28925
|
+
}
|
|
28926
|
+
if (isNodeOfType(current, "MemberExpression")) {
|
|
28927
|
+
if (isNodeOfType(current.object, "Identifier") && current.object.name === "styled" && isNodeOfType(current.property, "Identifier")) return current.property.name;
|
|
28928
|
+
current = stripParenExpression(current.object);
|
|
28929
|
+
continue;
|
|
28930
|
+
}
|
|
28931
|
+
return null;
|
|
28529
28932
|
}
|
|
28530
28933
|
return null;
|
|
28531
28934
|
};
|
|
28532
|
-
const
|
|
28935
|
+
const resolveClassRenderFunction = (classNode) => {
|
|
28936
|
+
if (!isNodeOfType(classNode, "ClassDeclaration") && !isNodeOfType(classNode, "ClassExpression")) return null;
|
|
28937
|
+
for (const member of classNode.body?.body ?? []) {
|
|
28938
|
+
if (!isNodeOfType(member, "MethodDefinition")) continue;
|
|
28939
|
+
if (!isNodeOfType(member.key, "Identifier") || member.key.name !== "render") continue;
|
|
28940
|
+
return member.value && isFunctionNode(member.value) ? member.value : null;
|
|
28941
|
+
}
|
|
28942
|
+
return null;
|
|
28943
|
+
};
|
|
28944
|
+
const recordWrapperFromDeclaration = (componentName, definitionNode, isTextHandlingElement, wrappers) => {
|
|
28533
28945
|
if (!componentName || !isReactComponentName(componentName)) return;
|
|
28534
|
-
if (
|
|
28535
|
-
|
|
28536
|
-
|
|
28946
|
+
if (wrappers.has(componentName)) return;
|
|
28947
|
+
if (!definitionNode) return;
|
|
28948
|
+
const unwrapped = unwrapComponentDefinition(definitionNode);
|
|
28949
|
+
const styledBaseName = resolveStyledFactoryBaseName(unwrapped);
|
|
28950
|
+
if (styledBaseName && isTextHandlingElement(styledBaseName)) {
|
|
28951
|
+
wrappers.add(componentName);
|
|
28952
|
+
return;
|
|
28953
|
+
}
|
|
28954
|
+
const functionNode = resolveClassRenderFunction(unwrapped) ?? (isFunctionNode(unwrapped) ? unwrapped : null);
|
|
28955
|
+
if (!functionNode) return;
|
|
28956
|
+
const bindings = resolveParamChildrenBindings(functionNode);
|
|
28957
|
+
collectChildrenAliases(functionNode, bindings);
|
|
28958
|
+
const jsxRoots = collectReturnedJsxRoots(functionNode);
|
|
28959
|
+
if (jsxRoots.some((jsxRoot) => jsxRootRendersChildrenOutsideText(jsxRoot, bindings, isTextHandlingElement))) return;
|
|
28960
|
+
for (const jsxRoot of jsxRoots) {
|
|
28961
|
+
if (isNodeOfType(jsxRoot, "JSXElement")) {
|
|
28962
|
+
const rootName = resolveJsxElementName(jsxRoot.openingElement);
|
|
28963
|
+
if (rootName && isTextHandlingElement(rootName)) {
|
|
28964
|
+
wrappers.add(componentName);
|
|
28965
|
+
return;
|
|
28966
|
+
}
|
|
28967
|
+
}
|
|
28968
|
+
if (jsxRootForwardsChildrenIntoText(jsxRoot, bindings, isTextHandlingElement)) {
|
|
28969
|
+
wrappers.add(componentName);
|
|
28970
|
+
return;
|
|
28971
|
+
}
|
|
28972
|
+
}
|
|
28537
28973
|
};
|
|
28974
|
+
const MAX_TRANSITIVE_WRAPPER_PASSES = 3;
|
|
28538
28975
|
const collectTextWrapperComponents = (programNode, isTextHandlingRoot) => {
|
|
28539
28976
|
const wrappers = /* @__PURE__ */ new Set();
|
|
28540
|
-
|
|
28541
|
-
|
|
28542
|
-
|
|
28543
|
-
|
|
28977
|
+
const isTextHandlingElement = (elementName) => isTextHandlingRoot(elementName) || wrappers.has(elementName);
|
|
28978
|
+
for (let pass = 0; pass < MAX_TRANSITIVE_WRAPPER_PASSES; pass += 1) {
|
|
28979
|
+
const sizeBeforePass = wrappers.size;
|
|
28980
|
+
walkAst(programNode, (node) => {
|
|
28981
|
+
if (isNodeOfType(node, "VariableDeclarator")) recordWrapperFromDeclaration(node.id && isNodeOfType(node.id, "Identifier") ? node.id.name : null, node.init, isTextHandlingElement, wrappers);
|
|
28982
|
+
else if (isNodeOfType(node, "FunctionDeclaration") || isNodeOfType(node, "ClassDeclaration")) recordWrapperFromDeclaration(node.id && isNodeOfType(node.id, "Identifier") ? node.id.name : null, node, isTextHandlingElement, wrappers);
|
|
28983
|
+
});
|
|
28984
|
+
if (wrappers.size === sizeBeforePass) break;
|
|
28985
|
+
}
|
|
28544
28986
|
return wrappers;
|
|
28545
28987
|
};
|
|
28546
28988
|
//#endregion
|
|
@@ -28608,6 +29050,7 @@ const rnNoRawText = defineRule({
|
|
|
28608
29050
|
title: "Raw text outside a Text component",
|
|
28609
29051
|
requires: ["react-native"],
|
|
28610
29052
|
severity: "error",
|
|
29053
|
+
tags: ["test-noise"],
|
|
28611
29054
|
recommendation: "Text outside a `<Text>` component crashes on React Native. Wrap it like `<Text>{value}</Text>`.",
|
|
28612
29055
|
create: (context) => {
|
|
28613
29056
|
let isDomComponentFile = false;
|
|
@@ -28853,136 +29296,6 @@ const rnPreferContentInsetAdjustment = defineRetiredRule({
|
|
|
28853
29296
|
recommendation: "Retired: SafeAreaView wrappers are valid; prefer native content inset adjustment only when manual inset plumbing causes scroll jumps or duplicated safe-area offsets."
|
|
28854
29297
|
});
|
|
28855
29298
|
//#endregion
|
|
28856
|
-
//#region src/react-native-dependency-names.ts
|
|
28857
|
-
const EXPO_MANAGED_DEPENDENCY_NAMES = new Set([
|
|
28858
|
-
"expo",
|
|
28859
|
-
"expo-router",
|
|
28860
|
-
"@expo/cli",
|
|
28861
|
-
"@expo/metro-config",
|
|
28862
|
-
"@expo/metro-runtime"
|
|
28863
|
-
]);
|
|
28864
|
-
const REACT_NATIVE_DEPENDENCY_NAMES = new Set([
|
|
28865
|
-
"react-native",
|
|
28866
|
-
"react-native-tvos",
|
|
28867
|
-
...EXPO_MANAGED_DEPENDENCY_NAMES,
|
|
28868
|
-
"react-native-windows",
|
|
28869
|
-
"react-native-macos"
|
|
28870
|
-
]);
|
|
28871
|
-
const REACT_NATIVE_DEPENDENCY_PREFIXES = ["@react-native/", "@react-native-"];
|
|
28872
|
-
const isExpoManagedDependencyName = (dependencyName) => EXPO_MANAGED_DEPENDENCY_NAMES.has(dependencyName);
|
|
28873
|
-
const isReactNativeDependencyName = (dependencyName) => {
|
|
28874
|
-
if (REACT_NATIVE_DEPENDENCY_NAMES.has(dependencyName)) return true;
|
|
28875
|
-
for (const prefix of REACT_NATIVE_DEPENDENCY_PREFIXES) if (dependencyName.startsWith(prefix)) return true;
|
|
28876
|
-
return false;
|
|
28877
|
-
};
|
|
28878
|
-
//#endregion
|
|
28879
|
-
//#region src/plugin/utils/classify-package-platform.ts
|
|
28880
|
-
const WEB_FRAMEWORK_DEPENDENCY_NAMES = new Set([
|
|
28881
|
-
"next",
|
|
28882
|
-
"vite",
|
|
28883
|
-
"react-scripts",
|
|
28884
|
-
"gatsby",
|
|
28885
|
-
"@remix-run/react",
|
|
28886
|
-
"@remix-run/node",
|
|
28887
|
-
"@docusaurus/core",
|
|
28888
|
-
"@docusaurus/preset-classic",
|
|
28889
|
-
"@storybook/react",
|
|
28890
|
-
"@storybook/react-vite",
|
|
28891
|
-
"@storybook/react-webpack5",
|
|
28892
|
-
"@storybook/nextjs",
|
|
28893
|
-
"@storybook/web-components",
|
|
28894
|
-
"storybook",
|
|
28895
|
-
"react-dom",
|
|
28896
|
-
"@vitejs/plugin-react",
|
|
28897
|
-
"@vitejs/plugin-react-swc"
|
|
28898
|
-
]);
|
|
28899
|
-
const cachedPlatformByPackageDirectory = /* @__PURE__ */ new Map();
|
|
28900
|
-
const cachedPackageDirectoryByFilename = /* @__PURE__ */ new Map();
|
|
28901
|
-
const findNearestPackageDirectory = (filename) => {
|
|
28902
|
-
if (!filename) return null;
|
|
28903
|
-
const fromCache = cachedPackageDirectoryByFilename.get(filename);
|
|
28904
|
-
if (fromCache !== void 0) return fromCache;
|
|
28905
|
-
let currentDirectory = path.dirname(filename);
|
|
28906
|
-
while (true) {
|
|
28907
|
-
const candidatePackageJsonPath = path.join(currentDirectory, "package.json");
|
|
28908
|
-
let hasPackageJson = false;
|
|
28909
|
-
try {
|
|
28910
|
-
hasPackageJson = fs.statSync(candidatePackageJsonPath).isFile();
|
|
28911
|
-
} catch {
|
|
28912
|
-
hasPackageJson = false;
|
|
28913
|
-
}
|
|
28914
|
-
if (hasPackageJson) {
|
|
28915
|
-
cachedPackageDirectoryByFilename.set(filename, currentDirectory);
|
|
28916
|
-
return currentDirectory;
|
|
28917
|
-
}
|
|
28918
|
-
const parentDirectory = path.dirname(currentDirectory);
|
|
28919
|
-
if (parentDirectory === currentDirectory) {
|
|
28920
|
-
cachedPackageDirectoryByFilename.set(filename, null);
|
|
28921
|
-
return null;
|
|
28922
|
-
}
|
|
28923
|
-
currentDirectory = parentDirectory;
|
|
28924
|
-
}
|
|
28925
|
-
};
|
|
28926
|
-
const readPackageJsonSafe = (packageJsonPath) => {
|
|
28927
|
-
let rawContents;
|
|
28928
|
-
try {
|
|
28929
|
-
rawContents = fs.readFileSync(packageJsonPath, "utf-8");
|
|
28930
|
-
} catch {
|
|
28931
|
-
return null;
|
|
28932
|
-
}
|
|
28933
|
-
try {
|
|
28934
|
-
const parsed = JSON.parse(rawContents);
|
|
28935
|
-
if (typeof parsed === "object" && parsed !== null) return parsed;
|
|
28936
|
-
return null;
|
|
28937
|
-
} catch {
|
|
28938
|
-
return null;
|
|
28939
|
-
}
|
|
28940
|
-
};
|
|
28941
|
-
const DEPENDENCY_SECTION_NAMES = [
|
|
28942
|
-
"dependencies",
|
|
28943
|
-
"devDependencies",
|
|
28944
|
-
"peerDependencies",
|
|
28945
|
-
"optionalDependencies"
|
|
28946
|
-
];
|
|
28947
|
-
const iterateDependencyNames = function* (packageJson) {
|
|
28948
|
-
for (const sectionName of DEPENDENCY_SECTION_NAMES) {
|
|
28949
|
-
const section = packageJson[sectionName];
|
|
28950
|
-
if (!section) continue;
|
|
28951
|
-
for (const dependencyName of Object.keys(section)) yield dependencyName;
|
|
28952
|
-
}
|
|
28953
|
-
};
|
|
28954
|
-
const isReactNativeAware = (packageJson) => {
|
|
28955
|
-
if (typeof packageJson["react-native"] === "string") return true;
|
|
28956
|
-
for (const dependencyName of iterateDependencyNames(packageJson)) if (isReactNativeDependencyName(dependencyName)) return true;
|
|
28957
|
-
return false;
|
|
28958
|
-
};
|
|
28959
|
-
const isExpoManaged = (packageJson) => {
|
|
28960
|
-
for (const dependencyName of iterateDependencyNames(packageJson)) if (isExpoManagedDependencyName(dependencyName)) return true;
|
|
28961
|
-
return false;
|
|
28962
|
-
};
|
|
28963
|
-
const isWebFrameworkOnly = (packageJson) => {
|
|
28964
|
-
for (const dependencyName of iterateDependencyNames(packageJson)) if (WEB_FRAMEWORK_DEPENDENCY_NAMES.has(dependencyName)) return true;
|
|
28965
|
-
return false;
|
|
28966
|
-
};
|
|
28967
|
-
const classifyPackagePlatform = (filename) => {
|
|
28968
|
-
const packageDirectory = findNearestPackageDirectory(filename);
|
|
28969
|
-
if (!packageDirectory) return "unknown";
|
|
28970
|
-
const cached = cachedPlatformByPackageDirectory.get(packageDirectory);
|
|
28971
|
-
if (cached !== void 0) return cached;
|
|
28972
|
-
const packageJson = readPackageJsonSafe(path.join(packageDirectory, "package.json"));
|
|
28973
|
-
if (!packageJson) {
|
|
28974
|
-
cachedPlatformByPackageDirectory.set(packageDirectory, "unknown");
|
|
28975
|
-
return "unknown";
|
|
28976
|
-
}
|
|
28977
|
-
let result;
|
|
28978
|
-
if (isExpoManaged(packageJson)) result = "expo";
|
|
28979
|
-
else if (isReactNativeAware(packageJson)) result = "react-native";
|
|
28980
|
-
else if (isWebFrameworkOnly(packageJson)) result = "web";
|
|
28981
|
-
else result = "unknown";
|
|
28982
|
-
cachedPlatformByPackageDirectory.set(packageDirectory, result);
|
|
28983
|
-
return result;
|
|
28984
|
-
};
|
|
28985
|
-
//#endregion
|
|
28986
29299
|
//#region src/plugin/utils/is-expo-managed-file.ts
|
|
28987
29300
|
const isExpoManagedFileActive = (context) => {
|
|
28988
29301
|
const rawFilename = context.filename;
|
|
@@ -32882,7 +33195,7 @@ const findEnclosingFunctionInfo = (node) => {
|
|
|
32882
33195
|
name: displayName,
|
|
32883
33196
|
hasResolvedName: resolvedName !== null,
|
|
32884
33197
|
isAsync: Boolean(current.async),
|
|
32885
|
-
isComponentOrHook: resolvedName === null ? false : isReactComponentOrHookName(displayName)
|
|
33198
|
+
isComponentOrHook: isReactHocCallbackArgument(current) || (resolvedName === null ? false : isReactComponentOrHookName(displayName))
|
|
32886
33199
|
};
|
|
32887
33200
|
}
|
|
32888
33201
|
current = current.parent ?? null;
|
|
@@ -32941,6 +33254,7 @@ const findEnclosingComponentOrHookFunction = (node) => {
|
|
|
32941
33254
|
let current = node.parent;
|
|
32942
33255
|
while (current) {
|
|
32943
33256
|
if (isFunctionLike$2(current)) {
|
|
33257
|
+
if (isReactHocCallbackArgument(current)) return current;
|
|
32944
33258
|
const resolvedName = inferFunctionName(current);
|
|
32945
33259
|
if (resolvedName !== null && isReactComponentOrHookName(resolvedName)) return current;
|
|
32946
33260
|
}
|
|
@@ -33040,26 +33354,26 @@ const rulesOfHooks = defineRule({
|
|
|
33040
33354
|
});
|
|
33041
33355
|
return;
|
|
33042
33356
|
}
|
|
33043
|
-
if (!enclosing.
|
|
33044
|
-
|
|
33045
|
-
|
|
33046
|
-
|
|
33047
|
-
|
|
33048
|
-
|
|
33049
|
-
|
|
33050
|
-
|
|
33051
|
-
|
|
33357
|
+
if (!enclosing.isComponentOrHook) {
|
|
33358
|
+
if (!enclosing.hasResolvedName) {
|
|
33359
|
+
let outerWalker = enclosing.node;
|
|
33360
|
+
let outerIsComponentOrHook = false;
|
|
33361
|
+
while (outerWalker) {
|
|
33362
|
+
const outerInfo = findEnclosingFunctionInfo(outerWalker);
|
|
33363
|
+
if (!outerInfo) break;
|
|
33364
|
+
if (outerInfo.isComponentOrHook) {
|
|
33365
|
+
outerIsComponentOrHook = true;
|
|
33366
|
+
break;
|
|
33367
|
+
}
|
|
33368
|
+
outerWalker = outerInfo.node;
|
|
33052
33369
|
}
|
|
33053
|
-
|
|
33370
|
+
if (!outerIsComponentOrHook) return;
|
|
33371
|
+
context.report({
|
|
33372
|
+
node: node.callee,
|
|
33373
|
+
message: buildConditionalMessage(hookName)
|
|
33374
|
+
});
|
|
33375
|
+
return;
|
|
33054
33376
|
}
|
|
33055
|
-
if (!outerIsComponentOrHook) return;
|
|
33056
|
-
context.report({
|
|
33057
|
-
node: node.callee,
|
|
33058
|
-
message: buildConditionalMessage(hookName)
|
|
33059
|
-
});
|
|
33060
|
-
return;
|
|
33061
|
-
}
|
|
33062
|
-
if (!enclosing.isComponentOrHook) {
|
|
33063
33377
|
context.report({
|
|
33064
33378
|
node: node.callee,
|
|
33065
33379
|
message: buildNonComponentMessage(hookName, enclosing.name)
|
|
@@ -34290,6 +34604,9 @@ const tanstackStartNoNavigateInRender = defineRule({
|
|
|
34290
34604
|
let eventHandlerDepth = 0;
|
|
34291
34605
|
const isDeferredHookCall = (node) => isHookCall$1(node, EFFECT_HOOK_NAMES$1) || isHookCall$1(node, "useCallback") || isHookCall$1(node, "useMemo");
|
|
34292
34606
|
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));
|
|
34607
|
+
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));
|
|
34608
|
+
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);
|
|
34609
|
+
const isHandlerNamedFunctionDeclaration = (node) => isNodeOfType(node, "FunctionDeclaration") && isNodeOfType(node.id, "Identifier") && typeof node.id.name === "string" && HANDLER_FUNCTION_NAME_PATTERN.test(node.id.name);
|
|
34293
34610
|
return {
|
|
34294
34611
|
CallExpression(node) {
|
|
34295
34612
|
const filename = normalizeFilename$1(context.filename ?? "");
|
|
@@ -34315,6 +34632,36 @@ const tanstackStartNoNavigateInRender = defineRule({
|
|
|
34315
34632
|
const filename = normalizeFilename$1(context.filename ?? "");
|
|
34316
34633
|
if (!TANSTACK_ROUTE_FILE_PATTERN.test(filename)) return;
|
|
34317
34634
|
if (isEventHandlerAttribute(node)) eventHandlerDepth = Math.max(0, eventHandlerDepth - 1);
|
|
34635
|
+
},
|
|
34636
|
+
Property(node) {
|
|
34637
|
+
const filename = normalizeFilename$1(context.filename ?? "");
|
|
34638
|
+
if (!TANSTACK_ROUTE_FILE_PATTERN.test(filename)) return;
|
|
34639
|
+
if (isEventHandlerProperty(node)) eventHandlerDepth++;
|
|
34640
|
+
},
|
|
34641
|
+
"Property:exit"(node) {
|
|
34642
|
+
const filename = normalizeFilename$1(context.filename ?? "");
|
|
34643
|
+
if (!TANSTACK_ROUTE_FILE_PATTERN.test(filename)) return;
|
|
34644
|
+
if (isEventHandlerProperty(node)) eventHandlerDepth = Math.max(0, eventHandlerDepth - 1);
|
|
34645
|
+
},
|
|
34646
|
+
VariableDeclarator(node) {
|
|
34647
|
+
const filename = normalizeFilename$1(context.filename ?? "");
|
|
34648
|
+
if (!TANSTACK_ROUTE_FILE_PATTERN.test(filename)) return;
|
|
34649
|
+
if (isHandlerNamedVariableDeclarator(node)) eventHandlerDepth++;
|
|
34650
|
+
},
|
|
34651
|
+
"VariableDeclarator:exit"(node) {
|
|
34652
|
+
const filename = normalizeFilename$1(context.filename ?? "");
|
|
34653
|
+
if (!TANSTACK_ROUTE_FILE_PATTERN.test(filename)) return;
|
|
34654
|
+
if (isHandlerNamedVariableDeclarator(node)) eventHandlerDepth = Math.max(0, eventHandlerDepth - 1);
|
|
34655
|
+
},
|
|
34656
|
+
FunctionDeclaration(node) {
|
|
34657
|
+
const filename = normalizeFilename$1(context.filename ?? "");
|
|
34658
|
+
if (!TANSTACK_ROUTE_FILE_PATTERN.test(filename)) return;
|
|
34659
|
+
if (isHandlerNamedFunctionDeclaration(node)) eventHandlerDepth++;
|
|
34660
|
+
},
|
|
34661
|
+
"FunctionDeclaration:exit"(node) {
|
|
34662
|
+
const filename = normalizeFilename$1(context.filename ?? "");
|
|
34663
|
+
if (!TANSTACK_ROUTE_FILE_PATTERN.test(filename)) return;
|
|
34664
|
+
if (isHandlerNamedFunctionDeclaration(node)) eventHandlerDepth = Math.max(0, eventHandlerDepth - 1);
|
|
34318
34665
|
}
|
|
34319
34666
|
};
|
|
34320
34667
|
}
|
|
@@ -38985,24 +39332,6 @@ const reactDoctorRules = [
|
|
|
38985
39332
|
];
|
|
38986
39333
|
const ruleRegistry = Object.fromEntries(reactDoctorRules.map((rule) => [rule.id, rule.rule]));
|
|
38987
39334
|
//#endregion
|
|
38988
|
-
//#region src/plugin/utils/is-react-native-file.ts
|
|
38989
|
-
const WEB_FILE_EXTENSION_PATTERN = /\.web\.[cm]?[jt]sx?$/;
|
|
38990
|
-
const NATIVE_FILE_EXTENSION_PATTERN = /\.(?:ios|android|native)\.[cm]?[jt]sx?$/;
|
|
38991
|
-
const isReactNativeFileActive = (context) => {
|
|
38992
|
-
const rawFilename = context.filename;
|
|
38993
|
-
if (!rawFilename) return true;
|
|
38994
|
-
const filename = normalizeFilename$1(rawFilename);
|
|
38995
|
-
if (NATIVE_FILE_EXTENSION_PATTERN.test(filename)) return true;
|
|
38996
|
-
if (WEB_FILE_EXTENSION_PATTERN.test(filename)) return false;
|
|
38997
|
-
const packagePlatform = classifyPackagePlatform(filename);
|
|
38998
|
-
if (packagePlatform === "web") return false;
|
|
38999
|
-
if (packagePlatform === "expo" || packagePlatform === "react-native") return true;
|
|
39000
|
-
const framework = getReactDoctorStringSetting(context.settings, "framework");
|
|
39001
|
-
if (framework === "react-native" || framework === "expo") return true;
|
|
39002
|
-
if (framework === "nextjs" || framework === "vite" || framework === "cra" || framework === "remix" || framework === "gatsby" || framework === "tanstack-start") return false;
|
|
39003
|
-
return true;
|
|
39004
|
-
};
|
|
39005
|
-
//#endregion
|
|
39006
39335
|
//#region src/plugin/utils/wrap-react-native-rule.ts
|
|
39007
39336
|
const EMPTY_VISITORS = {};
|
|
39008
39337
|
const wrapReactNativeRule = (rule) => {
|
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.04e72a4",
|
|
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",
|