oxlint-plugin-react-doctor 0.5.1-dev.038aaf7 → 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 +439 -213
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -5839,6 +5839,20 @@ const isReactHookName = (name) => {
|
|
|
5839
5839
|
//#region src/plugin/utils/is-react-component-or-hook-name.ts
|
|
5840
5840
|
const isReactComponentOrHookName = (name) => isReactComponentName(name) || isReactHookName(name);
|
|
5841
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
|
|
5842
5856
|
//#region src/plugin/rules/react-builtins/exhaustive-deps-low-level.ts
|
|
5843
5857
|
/**
|
|
5844
5858
|
* Lowest-level helpers consumed by both the main `exhaustive-deps`
|
|
@@ -6069,6 +6083,7 @@ const findEnclosingComponentOrHookFunction$1 = (node) => {
|
|
|
6069
6083
|
let current = node.parent;
|
|
6070
6084
|
while (current) {
|
|
6071
6085
|
if (isNodeOfType(current, "FunctionDeclaration") || isNodeOfType(current, "FunctionExpression") || isNodeOfType(current, "ArrowFunctionExpression")) {
|
|
6086
|
+
if (isReactHocCallbackArgument(current)) return current;
|
|
6072
6087
|
const functionName = inferFunctionName$1(current);
|
|
6073
6088
|
if (functionName && isReactComponentOrHookName(functionName)) return current;
|
|
6074
6089
|
}
|
|
@@ -15337,6 +15352,182 @@ const createRelativeImportSource = (filename, targetFilePath) => {
|
|
|
15337
15352
|
return relativePath.startsWith(".") ? relativePath : `./${relativePath}`;
|
|
15338
15353
|
};
|
|
15339
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
|
|
15340
15531
|
//#region src/plugin/rules/bundle-size/no-barrel-import.ts
|
|
15341
15532
|
const getLiteralName = (node) => {
|
|
15342
15533
|
if (node.type === "Identifier" && typeof node.name === "string") return node.name;
|
|
@@ -15354,7 +15545,8 @@ const getRuntimeImportRequests = (node) => {
|
|
|
15354
15545
|
return [{ importedName: null }];
|
|
15355
15546
|
});
|
|
15356
15547
|
};
|
|
15357
|
-
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.";
|
|
15358
15550
|
const directImportSources = /* @__PURE__ */ new Set();
|
|
15359
15551
|
for (const request of importRequests) {
|
|
15360
15552
|
if (!request.importedName) continue;
|
|
@@ -15363,9 +15555,9 @@ const buildReportMessage = (filename, barrelFilePath, importRequests) => {
|
|
|
15363
15555
|
}
|
|
15364
15556
|
if (directImportSources.size === 1) {
|
|
15365
15557
|
const [directImportSource] = directImportSources;
|
|
15366
|
-
return
|
|
15558
|
+
return `${costSentence} Import directly from "${directImportSource}".`;
|
|
15367
15559
|
}
|
|
15368
|
-
if (directImportSources.size > 1) return
|
|
15560
|
+
if (directImportSources.size > 1) return `${costSentence} Import directly from: ${[...directImportSources].map((source) => `"${source}"`).join(", ")}.`;
|
|
15369
15561
|
return "Importing from an index file pulls in extra code. Import directly from the source file instead.";
|
|
15370
15562
|
};
|
|
15371
15563
|
const noBarrelImport = defineRule({
|
|
@@ -15389,7 +15581,7 @@ const noBarrelImport = defineRule({
|
|
|
15389
15581
|
didReportForFile = true;
|
|
15390
15582
|
context.report({
|
|
15391
15583
|
node,
|
|
15392
|
-
message: buildReportMessage(filename, resolvedImportPath, importRequests)
|
|
15584
|
+
message: buildReportMessage(filename, resolvedImportPath, importRequests, classifyReactNativeFileTarget(context) === "react-native")
|
|
15393
15585
|
});
|
|
15394
15586
|
}
|
|
15395
15587
|
} };
|
|
@@ -20682,33 +20874,6 @@ const noPolymorphicChildren = defineRule({
|
|
|
20682
20874
|
} })
|
|
20683
20875
|
});
|
|
20684
20876
|
//#endregion
|
|
20685
|
-
//#region src/plugin/utils/get-react-doctor-setting.ts
|
|
20686
|
-
const readReactDoctorSettingsBag = (settings) => {
|
|
20687
|
-
const reactDoctorSettings = settings?.["react-doctor"];
|
|
20688
|
-
if (typeof reactDoctorSettings !== "object" || reactDoctorSettings === null || Array.isArray(reactDoctorSettings)) return null;
|
|
20689
|
-
return reactDoctorSettings;
|
|
20690
|
-
};
|
|
20691
|
-
const readOwnPropertyValue = (bag, settingName) => Object.getOwnPropertyDescriptor(bag, settingName)?.value;
|
|
20692
|
-
const getReactDoctorStringSetting = (settings, settingName) => {
|
|
20693
|
-
const bag = readReactDoctorSettingsBag(settings);
|
|
20694
|
-
if (!bag) return void 0;
|
|
20695
|
-
const settingValue = readOwnPropertyValue(bag, settingName);
|
|
20696
|
-
return typeof settingValue === "string" ? settingValue : void 0;
|
|
20697
|
-
};
|
|
20698
|
-
const getReactDoctorNumberSetting = (settings, settingName) => {
|
|
20699
|
-
const bag = readReactDoctorSettingsBag(settings);
|
|
20700
|
-
if (!bag) return void 0;
|
|
20701
|
-
const settingValue = readOwnPropertyValue(bag, settingName);
|
|
20702
|
-
return typeof settingValue === "number" && Number.isFinite(settingValue) ? settingValue : void 0;
|
|
20703
|
-
};
|
|
20704
|
-
const getReactDoctorStringArraySetting = (settings, settingName) => {
|
|
20705
|
-
const bag = readReactDoctorSettingsBag(settings);
|
|
20706
|
-
if (!bag) return [];
|
|
20707
|
-
const settingValue = readOwnPropertyValue(bag, settingName);
|
|
20708
|
-
if (!Array.isArray(settingValue)) return [];
|
|
20709
|
-
return settingValue.filter((entry) => typeof entry === "string" && entry.length > 0);
|
|
20710
|
-
};
|
|
20711
|
-
//#endregion
|
|
20712
20877
|
//#region src/plugin/rules/correctness/no-prevent-default.ts
|
|
20713
20878
|
const PREVENT_DEFAULT_ELEMENTS = new Map([["form", ["onSubmit"]], ["a", ["onClick"]]]);
|
|
20714
20879
|
const SERVER_CAPABLE_FRAMEWORKS = new Set([
|
|
@@ -28588,29 +28753,236 @@ const isInsidePlatformOsWebBranch = (node) => {
|
|
|
28588
28753
|
//#endregion
|
|
28589
28754
|
//#region src/plugin/rules/react-native/utils/collect-text-wrapper-components.ts
|
|
28590
28755
|
const isFunctionNode = (node) => isNodeOfType(node, "ArrowFunctionExpression") || isNodeOfType(node, "FunctionExpression") || isNodeOfType(node, "FunctionDeclaration");
|
|
28591
|
-
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) => {
|
|
28592
28822
|
const { body } = functionNode;
|
|
28593
|
-
if (!body) return
|
|
28594
|
-
|
|
28595
|
-
|
|
28596
|
-
|
|
28597
|
-
|
|
28598
|
-
|
|
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;
|
|
28932
|
+
}
|
|
28933
|
+
return null;
|
|
28934
|
+
};
|
|
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;
|
|
28599
28941
|
}
|
|
28600
28942
|
return null;
|
|
28601
28943
|
};
|
|
28602
|
-
const recordWrapperFromDeclaration = (componentName,
|
|
28944
|
+
const recordWrapperFromDeclaration = (componentName, definitionNode, isTextHandlingElement, wrappers) => {
|
|
28603
28945
|
if (!componentName || !isReactComponentName(componentName)) return;
|
|
28604
|
-
if (
|
|
28605
|
-
|
|
28606
|
-
|
|
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
|
+
}
|
|
28607
28973
|
};
|
|
28974
|
+
const MAX_TRANSITIVE_WRAPPER_PASSES = 3;
|
|
28608
28975
|
const collectTextWrapperComponents = (programNode, isTextHandlingRoot) => {
|
|
28609
28976
|
const wrappers = /* @__PURE__ */ new Set();
|
|
28610
|
-
|
|
28611
|
-
|
|
28612
|
-
|
|
28613
|
-
|
|
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
|
+
}
|
|
28614
28986
|
return wrappers;
|
|
28615
28987
|
};
|
|
28616
28988
|
//#endregion
|
|
@@ -28678,6 +29050,7 @@ const rnNoRawText = defineRule({
|
|
|
28678
29050
|
title: "Raw text outside a Text component",
|
|
28679
29051
|
requires: ["react-native"],
|
|
28680
29052
|
severity: "error",
|
|
29053
|
+
tags: ["test-noise"],
|
|
28681
29054
|
recommendation: "Text outside a `<Text>` component crashes on React Native. Wrap it like `<Text>{value}</Text>`.",
|
|
28682
29055
|
create: (context) => {
|
|
28683
29056
|
let isDomComponentFile = false;
|
|
@@ -28923,136 +29296,6 @@ const rnPreferContentInsetAdjustment = defineRetiredRule({
|
|
|
28923
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."
|
|
28924
29297
|
});
|
|
28925
29298
|
//#endregion
|
|
28926
|
-
//#region src/react-native-dependency-names.ts
|
|
28927
|
-
const EXPO_MANAGED_DEPENDENCY_NAMES = new Set([
|
|
28928
|
-
"expo",
|
|
28929
|
-
"expo-router",
|
|
28930
|
-
"@expo/cli",
|
|
28931
|
-
"@expo/metro-config",
|
|
28932
|
-
"@expo/metro-runtime"
|
|
28933
|
-
]);
|
|
28934
|
-
const REACT_NATIVE_DEPENDENCY_NAMES = new Set([
|
|
28935
|
-
"react-native",
|
|
28936
|
-
"react-native-tvos",
|
|
28937
|
-
...EXPO_MANAGED_DEPENDENCY_NAMES,
|
|
28938
|
-
"react-native-windows",
|
|
28939
|
-
"react-native-macos"
|
|
28940
|
-
]);
|
|
28941
|
-
const REACT_NATIVE_DEPENDENCY_PREFIXES = ["@react-native/", "@react-native-"];
|
|
28942
|
-
const isExpoManagedDependencyName = (dependencyName) => EXPO_MANAGED_DEPENDENCY_NAMES.has(dependencyName);
|
|
28943
|
-
const isReactNativeDependencyName = (dependencyName) => {
|
|
28944
|
-
if (REACT_NATIVE_DEPENDENCY_NAMES.has(dependencyName)) return true;
|
|
28945
|
-
for (const prefix of REACT_NATIVE_DEPENDENCY_PREFIXES) if (dependencyName.startsWith(prefix)) return true;
|
|
28946
|
-
return false;
|
|
28947
|
-
};
|
|
28948
|
-
//#endregion
|
|
28949
|
-
//#region src/plugin/utils/classify-package-platform.ts
|
|
28950
|
-
const WEB_FRAMEWORK_DEPENDENCY_NAMES = new Set([
|
|
28951
|
-
"next",
|
|
28952
|
-
"vite",
|
|
28953
|
-
"react-scripts",
|
|
28954
|
-
"gatsby",
|
|
28955
|
-
"@remix-run/react",
|
|
28956
|
-
"@remix-run/node",
|
|
28957
|
-
"@docusaurus/core",
|
|
28958
|
-
"@docusaurus/preset-classic",
|
|
28959
|
-
"@storybook/react",
|
|
28960
|
-
"@storybook/react-vite",
|
|
28961
|
-
"@storybook/react-webpack5",
|
|
28962
|
-
"@storybook/nextjs",
|
|
28963
|
-
"@storybook/web-components",
|
|
28964
|
-
"storybook",
|
|
28965
|
-
"react-dom",
|
|
28966
|
-
"@vitejs/plugin-react",
|
|
28967
|
-
"@vitejs/plugin-react-swc"
|
|
28968
|
-
]);
|
|
28969
|
-
const cachedPlatformByPackageDirectory = /* @__PURE__ */ new Map();
|
|
28970
|
-
const cachedPackageDirectoryByFilename = /* @__PURE__ */ new Map();
|
|
28971
|
-
const findNearestPackageDirectory = (filename) => {
|
|
28972
|
-
if (!filename) return null;
|
|
28973
|
-
const fromCache = cachedPackageDirectoryByFilename.get(filename);
|
|
28974
|
-
if (fromCache !== void 0) return fromCache;
|
|
28975
|
-
let currentDirectory = path.dirname(filename);
|
|
28976
|
-
while (true) {
|
|
28977
|
-
const candidatePackageJsonPath = path.join(currentDirectory, "package.json");
|
|
28978
|
-
let hasPackageJson = false;
|
|
28979
|
-
try {
|
|
28980
|
-
hasPackageJson = fs.statSync(candidatePackageJsonPath).isFile();
|
|
28981
|
-
} catch {
|
|
28982
|
-
hasPackageJson = false;
|
|
28983
|
-
}
|
|
28984
|
-
if (hasPackageJson) {
|
|
28985
|
-
cachedPackageDirectoryByFilename.set(filename, currentDirectory);
|
|
28986
|
-
return currentDirectory;
|
|
28987
|
-
}
|
|
28988
|
-
const parentDirectory = path.dirname(currentDirectory);
|
|
28989
|
-
if (parentDirectory === currentDirectory) {
|
|
28990
|
-
cachedPackageDirectoryByFilename.set(filename, null);
|
|
28991
|
-
return null;
|
|
28992
|
-
}
|
|
28993
|
-
currentDirectory = parentDirectory;
|
|
28994
|
-
}
|
|
28995
|
-
};
|
|
28996
|
-
const readPackageJsonSafe = (packageJsonPath) => {
|
|
28997
|
-
let rawContents;
|
|
28998
|
-
try {
|
|
28999
|
-
rawContents = fs.readFileSync(packageJsonPath, "utf-8");
|
|
29000
|
-
} catch {
|
|
29001
|
-
return null;
|
|
29002
|
-
}
|
|
29003
|
-
try {
|
|
29004
|
-
const parsed = JSON.parse(rawContents);
|
|
29005
|
-
if (typeof parsed === "object" && parsed !== null) return parsed;
|
|
29006
|
-
return null;
|
|
29007
|
-
} catch {
|
|
29008
|
-
return null;
|
|
29009
|
-
}
|
|
29010
|
-
};
|
|
29011
|
-
const DEPENDENCY_SECTION_NAMES = [
|
|
29012
|
-
"dependencies",
|
|
29013
|
-
"devDependencies",
|
|
29014
|
-
"peerDependencies",
|
|
29015
|
-
"optionalDependencies"
|
|
29016
|
-
];
|
|
29017
|
-
const iterateDependencyNames = function* (packageJson) {
|
|
29018
|
-
for (const sectionName of DEPENDENCY_SECTION_NAMES) {
|
|
29019
|
-
const section = packageJson[sectionName];
|
|
29020
|
-
if (!section) continue;
|
|
29021
|
-
for (const dependencyName of Object.keys(section)) yield dependencyName;
|
|
29022
|
-
}
|
|
29023
|
-
};
|
|
29024
|
-
const isReactNativeAware = (packageJson) => {
|
|
29025
|
-
if (typeof packageJson["react-native"] === "string") return true;
|
|
29026
|
-
for (const dependencyName of iterateDependencyNames(packageJson)) if (isReactNativeDependencyName(dependencyName)) return true;
|
|
29027
|
-
return false;
|
|
29028
|
-
};
|
|
29029
|
-
const isExpoManaged = (packageJson) => {
|
|
29030
|
-
for (const dependencyName of iterateDependencyNames(packageJson)) if (isExpoManagedDependencyName(dependencyName)) return true;
|
|
29031
|
-
return false;
|
|
29032
|
-
};
|
|
29033
|
-
const isWebFrameworkOnly = (packageJson) => {
|
|
29034
|
-
for (const dependencyName of iterateDependencyNames(packageJson)) if (WEB_FRAMEWORK_DEPENDENCY_NAMES.has(dependencyName)) return true;
|
|
29035
|
-
return false;
|
|
29036
|
-
};
|
|
29037
|
-
const classifyPackagePlatform = (filename) => {
|
|
29038
|
-
const packageDirectory = findNearestPackageDirectory(filename);
|
|
29039
|
-
if (!packageDirectory) return "unknown";
|
|
29040
|
-
const cached = cachedPlatformByPackageDirectory.get(packageDirectory);
|
|
29041
|
-
if (cached !== void 0) return cached;
|
|
29042
|
-
const packageJson = readPackageJsonSafe(path.join(packageDirectory, "package.json"));
|
|
29043
|
-
if (!packageJson) {
|
|
29044
|
-
cachedPlatformByPackageDirectory.set(packageDirectory, "unknown");
|
|
29045
|
-
return "unknown";
|
|
29046
|
-
}
|
|
29047
|
-
let result;
|
|
29048
|
-
if (isExpoManaged(packageJson)) result = "expo";
|
|
29049
|
-
else if (isReactNativeAware(packageJson)) result = "react-native";
|
|
29050
|
-
else if (isWebFrameworkOnly(packageJson)) result = "web";
|
|
29051
|
-
else result = "unknown";
|
|
29052
|
-
cachedPlatformByPackageDirectory.set(packageDirectory, result);
|
|
29053
|
-
return result;
|
|
29054
|
-
};
|
|
29055
|
-
//#endregion
|
|
29056
29299
|
//#region src/plugin/utils/is-expo-managed-file.ts
|
|
29057
29300
|
const isExpoManagedFileActive = (context) => {
|
|
29058
29301
|
const rawFilename = context.filename;
|
|
@@ -32952,7 +33195,7 @@ const findEnclosingFunctionInfo = (node) => {
|
|
|
32952
33195
|
name: displayName,
|
|
32953
33196
|
hasResolvedName: resolvedName !== null,
|
|
32954
33197
|
isAsync: Boolean(current.async),
|
|
32955
|
-
isComponentOrHook: resolvedName === null ? false : isReactComponentOrHookName(displayName)
|
|
33198
|
+
isComponentOrHook: isReactHocCallbackArgument(current) || (resolvedName === null ? false : isReactComponentOrHookName(displayName))
|
|
32956
33199
|
};
|
|
32957
33200
|
}
|
|
32958
33201
|
current = current.parent ?? null;
|
|
@@ -33011,6 +33254,7 @@ const findEnclosingComponentOrHookFunction = (node) => {
|
|
|
33011
33254
|
let current = node.parent;
|
|
33012
33255
|
while (current) {
|
|
33013
33256
|
if (isFunctionLike$2(current)) {
|
|
33257
|
+
if (isReactHocCallbackArgument(current)) return current;
|
|
33014
33258
|
const resolvedName = inferFunctionName(current);
|
|
33015
33259
|
if (resolvedName !== null && isReactComponentOrHookName(resolvedName)) return current;
|
|
33016
33260
|
}
|
|
@@ -33110,26 +33354,26 @@ const rulesOfHooks = defineRule({
|
|
|
33110
33354
|
});
|
|
33111
33355
|
return;
|
|
33112
33356
|
}
|
|
33113
|
-
if (!enclosing.
|
|
33114
|
-
|
|
33115
|
-
|
|
33116
|
-
|
|
33117
|
-
|
|
33118
|
-
|
|
33119
|
-
|
|
33120
|
-
|
|
33121
|
-
|
|
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;
|
|
33122
33369
|
}
|
|
33123
|
-
|
|
33370
|
+
if (!outerIsComponentOrHook) return;
|
|
33371
|
+
context.report({
|
|
33372
|
+
node: node.callee,
|
|
33373
|
+
message: buildConditionalMessage(hookName)
|
|
33374
|
+
});
|
|
33375
|
+
return;
|
|
33124
33376
|
}
|
|
33125
|
-
if (!outerIsComponentOrHook) return;
|
|
33126
|
-
context.report({
|
|
33127
|
-
node: node.callee,
|
|
33128
|
-
message: buildConditionalMessage(hookName)
|
|
33129
|
-
});
|
|
33130
|
-
return;
|
|
33131
|
-
}
|
|
33132
|
-
if (!enclosing.isComponentOrHook) {
|
|
33133
33377
|
context.report({
|
|
33134
33378
|
node: node.callee,
|
|
33135
33379
|
message: buildNonComponentMessage(hookName, enclosing.name)
|
|
@@ -39088,24 +39332,6 @@ const reactDoctorRules = [
|
|
|
39088
39332
|
];
|
|
39089
39333
|
const ruleRegistry = Object.fromEntries(reactDoctorRules.map((rule) => [rule.id, rule.rule]));
|
|
39090
39334
|
//#endregion
|
|
39091
|
-
//#region src/plugin/utils/is-react-native-file.ts
|
|
39092
|
-
const WEB_FILE_EXTENSION_PATTERN = /\.web\.[cm]?[jt]sx?$/;
|
|
39093
|
-
const NATIVE_FILE_EXTENSION_PATTERN = /\.(?:ios|android|native)\.[cm]?[jt]sx?$/;
|
|
39094
|
-
const isReactNativeFileActive = (context) => {
|
|
39095
|
-
const rawFilename = context.filename;
|
|
39096
|
-
if (!rawFilename) return true;
|
|
39097
|
-
const filename = normalizeFilename$1(rawFilename);
|
|
39098
|
-
if (NATIVE_FILE_EXTENSION_PATTERN.test(filename)) return true;
|
|
39099
|
-
if (WEB_FILE_EXTENSION_PATTERN.test(filename)) return false;
|
|
39100
|
-
const packagePlatform = classifyPackagePlatform(filename);
|
|
39101
|
-
if (packagePlatform === "web") return false;
|
|
39102
|
-
if (packagePlatform === "expo" || packagePlatform === "react-native") return true;
|
|
39103
|
-
const framework = getReactDoctorStringSetting(context.settings, "framework");
|
|
39104
|
-
if (framework === "react-native" || framework === "expo") return true;
|
|
39105
|
-
if (framework === "nextjs" || framework === "vite" || framework === "cra" || framework === "remix" || framework === "gatsby" || framework === "tanstack-start") return false;
|
|
39106
|
-
return true;
|
|
39107
|
-
};
|
|
39108
|
-
//#endregion
|
|
39109
39335
|
//#region src/plugin/utils/wrap-react-native-rule.ts
|
|
39110
39336
|
const EMPTY_VISITORS = {};
|
|
39111
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.1-dev.
|
|
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",
|