oxlint-plugin-react-doctor 0.7.2-dev.aa519e5 → 0.7.2-dev.b97a92f

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.
Files changed (3) hide show
  1. package/dist/index.d.ts +2350 -1563
  2. package/dist/index.js +73 -27
  3. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -13508,7 +13508,7 @@ const jsTosortedImmutable = defineRule({
13508
13508
  title: "Spread copy before sort()",
13509
13509
  tags: ["test-noise"],
13510
13510
  severity: "warn",
13511
- disabledBy: ["react-native", "pre-es2023"],
13511
+ disabledWhen: ["react-native", "pre-es2023"],
13512
13512
  recommendation: "Use `array.toSorted()` (ES2023) instead of `[...array].sort()` so you sort without copying the array first",
13513
13513
  create: (context) => ({ CallExpression(node) {
13514
13514
  if (!isMemberProperty(node.callee, "sort")) return;
@@ -14755,7 +14755,7 @@ const jsxNoConstructedContextValues = defineRule({
14755
14755
  title: "Unstable context provider value",
14756
14756
  tags: ["react-jsx-only"],
14757
14757
  severity: "warn",
14758
- disabledBy: ["react-compiler"],
14758
+ disabledWhen: ["react-compiler"],
14759
14759
  recommendation: "Wrap the context value in `useMemo` or move it outside the component so consumers do not redraw every render.",
14760
14760
  category: "Performance",
14761
14761
  create: (context) => {
@@ -15123,7 +15123,7 @@ const jsxNoJsxAsProp = defineRule({
15123
15123
  title: "JSX element passed as a prop",
15124
15124
  tags: ["react-jsx-only"],
15125
15125
  severity: "warn",
15126
- disabledBy: ["react-compiler"],
15126
+ disabledWhen: ["react-compiler"],
15127
15127
  recommendation: "Move the JSX outside the component or wrap it in `useMemo` so memoized children do not redraw every render.",
15128
15128
  category: "Performance",
15129
15129
  create: (context) => {
@@ -15511,7 +15511,7 @@ const jsxNoNewArrayAsProp = defineRule({
15511
15511
  title: "New array passed as a prop",
15512
15512
  tags: ["react-jsx-only"],
15513
15513
  severity: "warn",
15514
- disabledBy: ["react-compiler"],
15514
+ disabledWhen: ["react-compiler"],
15515
15515
  recommendation: "Wrap the array in `useMemo` or move it outside the component so memoized children do not redraw every render.",
15516
15516
  category: "Performance",
15517
15517
  create: (context) => {
@@ -15975,7 +15975,7 @@ const jsxNoNewFunctionAsProp = defineRule({
15975
15975
  title: "New function passed as a prop",
15976
15976
  tags: ["react-jsx-only"],
15977
15977
  severity: "warn",
15978
- disabledBy: ["react-compiler"],
15978
+ disabledWhen: ["react-compiler"],
15979
15979
  recommendation: "Wrap the callback in `useCallback` or move it outside the component so memoized children do not redraw every render.",
15980
15980
  category: "Performance",
15981
15981
  create: (context) => {
@@ -16282,7 +16282,7 @@ const jsxNoNewObjectAsProp = defineRule({
16282
16282
  title: "New object passed as a prop",
16283
16283
  tags: ["react-jsx-only"],
16284
16284
  severity: "warn",
16285
- disabledBy: ["react-compiler"],
16285
+ disabledWhen: ["react-compiler"],
16286
16286
  recommendation: "Wrap the object in `useMemo` or move it outside the component so memoized children do not redraw every render.",
16287
16287
  category: "Performance",
16288
16288
  create: (context) => {
@@ -18106,6 +18106,7 @@ const getReactDoctorStringArraySetting = (settings, settingName) => {
18106
18106
  if (!Array.isArray(settingValue)) return [];
18107
18107
  return settingValue.filter((entry) => typeof entry === "string" && entry.length > 0);
18108
18108
  };
18109
+ const hasCapability = (settings, capability) => getReactDoctorStringArraySetting(settings, "capabilities").includes(capability);
18109
18110
  //#endregion
18110
18111
  //#region src/plugin/utils/is-in-project-directory.ts
18111
18112
  const isInProjectDirectory = (context, directoryPath) => {
@@ -18560,6 +18561,7 @@ const describeClientSideNavigation = (node) => {
18560
18561
  }
18561
18562
  return null;
18562
18563
  };
18564
+ const STATIC_EXPORT_RECOMMENDATION = "Avoid redirects inside useEffect — they flash the wrong page first. Use an event handler (e.g. onClick), or call redirect() from next/navigation during render (it prerenders a client-side redirect under output: \"export\"). Middleware and getServerSideProps redirects aren't available in a static export.";
18563
18565
  const nextjsNoClientSideRedirect = defineRule({
18564
18566
  id: "nextjs-no-client-side-redirect",
18565
18567
  title: "Client-side redirect for navigation",
@@ -18567,6 +18569,7 @@ const nextjsNoClientSideRedirect = defineRule({
18567
18569
  requires: ["nextjs"],
18568
18570
  severity: "warn",
18569
18571
  recommendation: "Avoid redirects inside useEffect. Use an event handler, middleware, or server-side redirect (App Router: redirect() from next/navigation; Pages Router: getServerSideProps redirect)",
18572
+ recommendationFor: (hasCapability) => hasCapability("nextjs:static-export") ? STATIC_EXPORT_RECOMMENDATION : void 0,
18570
18573
  create: (context) => {
18571
18574
  return { CallExpression(node) {
18572
18575
  if (!isHookCall$2(node, EFFECT_HOOK_NAMES$1)) return;
@@ -28928,7 +28931,7 @@ const noJsonParseStringifyClone = defineRule({
28928
28931
  id: "no-json-parse-stringify-clone",
28929
28932
  title: "JSON parse/stringify deep clone",
28930
28933
  severity: "warn",
28931
- disabledBy: ["react-native"],
28934
+ disabledWhen: ["react-native"],
28932
28935
  recommendation: "Replace `JSON.parse(JSON.stringify(value))` with `structuredClone(value)`. It is faster and preserves Dates, Maps, Sets, and cyclic references.",
28933
28936
  create: (context) => ({ CallExpression(node) {
28934
28937
  if (!isJsonMethodCall(node, "parse")) return;
@@ -31752,12 +31755,8 @@ const noPolymorphicChildren = defineRule({
31752
31755
  //#endregion
31753
31756
  //#region src/plugin/rules/correctness/no-prevent-default.ts
31754
31757
  const PREVENT_DEFAULT_ELEMENTS = new Map([["form", ["onSubmit"]], ["a", ["onClick"]]]);
31755
- const SERVER_CAPABLE_FRAMEWORKS = new Set([
31756
- "nextjs",
31757
- "tanstack-start",
31758
- "remix"
31759
- ]);
31760
31758
  const FORM_MESSAGE_SERVER_CAPABLE = "Your users can't submit this <form> without JavaScript because onSubmit calls preventDefault(), so use a server action like `<form action={serverAction}>` to make it work either way.";
31759
+ const FORM_MESSAGE_GENERIC = "Your users can't submit this <form> because onSubmit calls preventDefault().";
31761
31760
  const ANCHOR_MESSAGE = "Your users click this <a> & nothing navigates because onClick calls preventDefault(), so use a <button> or a routing component instead.";
31762
31761
  const collectPreventDefaultCalls = (node) => {
31763
31762
  const preventDefaultCalls = [];
@@ -31894,15 +31893,18 @@ const noPreventDefault = defineRule({
31894
31893
  recommendation: "Use `<form action>` where your framework supports it (it works without JS), or use a `<button>` instead of an `<a>` with preventDefault.",
31895
31894
  create: (context) => {
31896
31895
  const framework = getReactDoctorStringSetting(context.settings, "framework");
31897
- const isServerCapableFramework = framework !== void 0 && SERVER_CAPABLE_FRAMEWORKS.has(framework);
31896
+ const isClientOnlyFramework = hasCapability(context.settings, "client-only");
31897
+ const isServerActionsFramework = hasCapability(context.settings, "server-actions");
31898
+ const formMessage = isServerActionsFramework ? FORM_MESSAGE_SERVER_CAPABLE : FORM_MESSAGE_GENERIC;
31898
31899
  return { JSXOpeningElement(node) {
31899
31900
  const elementName = isNodeOfType(node.name, "JSXIdentifier") ? node.name.name : null;
31900
31901
  if (!elementName) return;
31901
31902
  const targetEventProps = PREVENT_DEFAULT_ELEMENTS.get(elementName);
31902
31903
  if (!targetEventProps) return;
31903
31904
  if (elementName === "form") {
31904
- if (!isServerCapableFramework) return;
31905
- if (framework === "nextjs") {
31905
+ if (isClientOnlyFramework) return;
31906
+ if (framework === void 0 || framework === "unknown") return;
31907
+ if (framework === "nextjs" && isServerActionsFramework) {
31906
31908
  const programRoot = findProgramRoot(node);
31907
31909
  if (!programRoot || !hasDirective(programRoot, "use client")) return;
31908
31910
  }
@@ -31926,7 +31928,7 @@ const noPreventDefault = defineRule({
31926
31928
  }
31927
31929
  context.report({
31928
31930
  node,
31929
- message: elementName === "form" ? FORM_MESSAGE_SERVER_CAPABLE : ANCHOR_MESSAGE
31931
+ message: elementName === "form" ? formMessage : ANCHOR_MESSAGE
31930
31932
  });
31931
31933
  return;
31932
31934
  }
@@ -33196,11 +33198,41 @@ const isIdentifierLikeKeyNameValue = (literalValue) => {
33196
33198
  if (wordSegments.length < 2) return false;
33197
33199
  return wordSegments.every((segment) => /^[a-z]+$/.test(segment));
33198
33200
  };
33201
+ const FRAMEWORK_ENV_ADVICE = [
33202
+ [
33203
+ "nextjs",
33204
+ "Next.js",
33205
+ "NEXT_PUBLIC_*"
33206
+ ],
33207
+ [
33208
+ "vite",
33209
+ "Vite",
33210
+ "VITE_*"
33211
+ ],
33212
+ [
33213
+ "tanstack-start",
33214
+ "TanStack Start",
33215
+ "VITE_*"
33216
+ ],
33217
+ [
33218
+ "cra",
33219
+ "Create React App",
33220
+ "REACT_APP_*"
33221
+ ],
33222
+ [
33223
+ "gatsby",
33224
+ "Gatsby",
33225
+ "GATSBY_*"
33226
+ ]
33227
+ ];
33199
33228
  const noSecretsInClientCode = defineRule({
33200
33229
  id: "no-secrets-in-client-code",
33201
33230
  title: "Secret in client code",
33202
33231
  severity: "warn",
33203
33232
  recommendation: "Move secrets to server-only code. Anything in client env variables gets shipped to the browser, so it can't hold secrets.",
33233
+ recommendationFor: (hasCapability) => {
33234
+ for (const [frameworkToken, frameworkName, publicEnvPrefix] of FRAMEWORK_ENV_ADVICE) if (hasCapability(frameworkToken)) return `Move secrets to server-only code. In ${frameworkName}, only \`${publicEnvPrefix}\` env vars are exposed to the browser, and they must not contain secrets`;
33235
+ },
33204
33236
  create: (context) => {
33205
33237
  const filename = normalizeFilename(context.filename ?? "");
33206
33238
  const framework = getReactDoctorStringSetting(context.settings, "framework");
@@ -33238,7 +33270,7 @@ const noSecretsInClientCode = defineRule({
33238
33270
  const isServerOnlyScope = isInsideServerOnlyScope(node);
33239
33271
  const trailingSuffix = getIdentifierTrailingWord(variableName);
33240
33272
  const isUiConstant = SECRET_FALSE_POSITIVE_SUFFIXES.has(trailingSuffix);
33241
- if (shouldUseVariableNameHeuristic && !isServerOnlyScope && SECRET_VARIABLE_PATTERN.test(variableName) && !isUiConstant && !isPublicUrlValue(literalValue) && !isPlaceholderValueForVariableHeuristic && !isSelfReferentialSentinelValue(variableName, literalValue) && !isIdentifierLikeKeyNameValue(literalValue) && literalValue.length > 24) {
33273
+ if (shouldUseVariableNameHeuristic && !isServerOnlyScope && SECRET_VARIABLE_PATTERN.test(variableName) && !isUiConstant && !isPublicUrlValue(literalValue) && !isPlaceholderValueForVariableHeuristic && !isSelfReferentialSentinelValue(variableName, literalValue) && !isIdentifierLikeKeyNameValue(literalValue) && !isSelfReferentialSentinelValue(variableName, literalValue) && !isIdentifierLikeKeyNameValue(literalValue) && literalValue.length > 24) {
33242
33274
  context.report({
33243
33275
  node,
33244
33276
  message: `Hardcoding "${variableName}" in client code is a security vulnerability: the secret ships to the browser where anyone can read it.`
@@ -37962,7 +37994,7 @@ const preferModuleScopeStaticValue = defineRule({
37962
37994
  tags: ["test-noise"],
37963
37995
  severity: "warn",
37964
37996
  category: "Architecture",
37965
- disabledBy: ["react-compiler"],
37997
+ disabledWhen: ["react-compiler"],
37966
37998
  recommendation: "Move the value above the component, at the top of the file. It doesn't use local state, so rebuilding it each update is wasted and makes it look new every time.",
37967
37999
  create: (context) => ({ VariableDeclarator(node) {
37968
38000
  if (!isNodeOfType(node.id, "Identifier")) return;
@@ -38032,7 +38064,7 @@ const preferStableEmptyFallback = defineRule({
38032
38064
  tags: ["react-jsx-only", "test-noise"],
38033
38065
  severity: "warn",
38034
38066
  category: "Performance",
38035
- disabledBy: ["react-compiler"],
38067
+ disabledWhen: ["react-compiler"],
38036
38068
  recommendation: "Make a `const EMPTY = []` (or `{}`) at module scope and use it as the `||` / `??` fallback, so the value stays the same each render.",
38037
38069
  create: (context) => {
38038
38070
  let memoRegistry = null;
@@ -39361,7 +39393,7 @@ const reduxUseselectorInlineDerivation = defineRule({
39361
39393
  title: "useSelector derives data inline",
39362
39394
  severity: "warn",
39363
39395
  category: "Performance",
39364
- disabledBy: ["react-compiler"],
39396
+ disabledWhen: ["react-compiler"],
39365
39397
  recommendation: "Select the raw slice and memoize derivation so Redux actions do not rebuild a collection and redraw this component.",
39366
39398
  create: (context) => {
39367
39399
  let aliases = /* @__PURE__ */ new Set();
@@ -39421,7 +39453,7 @@ const reduxUseselectorReturnsNewCollection = defineRule({
39421
39453
  title: "useSelector returns a new collection",
39422
39454
  severity: "warn",
39423
39455
  category: "Performance",
39424
- disabledBy: ["react-compiler"],
39456
+ disabledWhen: ["react-compiler"],
39425
39457
  recommendation: "Return a stable selected value, split selectors, or pass `shallowEqual` so every Redux action does not redraw this component.",
39426
39458
  create: (context) => {
39427
39459
  let aliases = /* @__PURE__ */ new Set();
@@ -39721,7 +39753,7 @@ const renderingHydrationMismatchTime = defineRule({
39721
39753
  title: "Time or random value in JSX",
39722
39754
  severity: "warn",
39723
39755
  category: "Correctness",
39724
- disabledBy: ["vite", "cra"],
39756
+ disabledWhen: ["vite", "cra"],
39725
39757
  recommendation: "Move time or random values into useEffect+useState so they only run in the browser, or add suppressHydrationWarning to the parent if it's intentional",
39726
39758
  create: (context) => {
39727
39759
  const isTestlikeFile = isTestlikeFilename(context.filename);
@@ -41688,7 +41720,7 @@ const rnListCallbackPerRow = defineRule({
41688
41720
  tags: ["test-noise"],
41689
41721
  requires: ["react-native"],
41690
41722
  severity: "warn",
41691
- disabledBy: ["react-compiler"],
41723
+ disabledWhen: ["react-compiler"],
41692
41724
  recommendation: "Move the handler out with useCallback at list scope and pass the row id as a prop. Then memo() rows skip redrawing when their data has not changed.",
41693
41725
  create: (context) => {
41694
41726
  const inspect = (node) => {
@@ -42316,7 +42348,7 @@ const rnNoInlineFlatlistRenderitem = defineRule({
42316
42348
  tags: ["test-noise"],
42317
42349
  requires: ["react-native"],
42318
42350
  severity: "warn",
42319
- disabledBy: ["react-compiler"],
42351
+ disabledWhen: ["react-compiler"],
42320
42352
  recommendation: "Move renderItem to a named function or wrap it in useCallback so it is not rebuilt every time the screen redraws.",
42321
42353
  create: (context) => ({ JSXAttribute(node) {
42322
42354
  if (!isNodeOfType(node.name, "JSXIdentifier") || node.name.name !== "renderItem") return;
@@ -42341,7 +42373,7 @@ const rnNoInlineObjectInListItem = defineRule({
42341
42373
  tags: ["test-noise"],
42342
42374
  requires: ["react-native"],
42343
42375
  severity: "warn",
42344
- disabledBy: ["react-compiler"],
42376
+ disabledWhen: ["react-compiler"],
42345
42377
  recommendation: "Move style and object props out of renderItem (StyleSheet.create, useMemo at list scope, or pass primitives) so memo() rows stop redrawing when their data has not changed.",
42346
42378
  create: (context) => {
42347
42379
  const renderPropStack = [];
@@ -48452,7 +48484,7 @@ const serverFetchWithoutRevalidate = defineRule({
48452
48484
  id: "server-fetch-without-revalidate",
48453
48485
  title: "Fetch without revalidate",
48454
48486
  severity: "warn",
48455
- disabledBy: ["nextjs:15"],
48487
+ disabledWhen: ["nextjs:15", "nextjs:static-export"],
48456
48488
  recommendation: "Pass `{ next: { revalidate: <seconds> } }` (or `cache: \"no-store\"`) so old data doesn't stick around.",
48457
48489
  create: (context) => {
48458
48490
  let isServerSideFile = false;
@@ -56227,9 +56259,23 @@ const classifySecurityScanFile = (relativePath) => {
56227
56259
  };
56228
56260
  const shouldReadSecurityScanContent = (relativePath, isGeneratedBundle) => isGeneratedBundle || isProbablyTextFile(relativePath) || isConfigOrCiPath(relativePath) || isRepositorySecretFilePath(relativePath);
56229
56261
  //#endregion
56262
+ //#region src/plugin/utils/capability.ts
56263
+ const FRAMEWORK_TOKENS = [
56264
+ "nextjs",
56265
+ "vite",
56266
+ "cra",
56267
+ "remix",
56268
+ "gatsby",
56269
+ "expo",
56270
+ "react-native",
56271
+ "tanstack-start",
56272
+ "preact",
56273
+ "unknown"
56274
+ ];
56275
+ //#endregion
56230
56276
  //#region src/index.ts
56231
56277
  var src_default = plugin;
56232
56278
  //#endregion
56233
- export { ALL_REACT_DOCTOR_RULES, ALL_REACT_DOCTOR_RULE_KEYS, CROSS_FILE_DEPENDENCY_COLLECTORS, CROSS_FILE_RULE_IDS, EXTERNAL_RULES, FRAMEWORK_SPECIFIC_RULE_KEYS, MOTION_LIBRARY_PACKAGES, NEXTJS_RULES, PREACT_RULES, REACT_COMPILER_RULES, REACT_DOCTOR_RULES, REACT_NATIVE_DEPENDENCY_NAMES, REACT_NATIVE_DEPENDENCY_PREFIXES, REACT_NATIVE_RULES, RECOMMENDED_RULES, RULES, TANSTACK_QUERY_RULES, TANSTACK_START_RULES, UNBOUNDED_CROSS_FILE_RULE_IDS, classifySecurityScanFile, collectCrossFileDependencyProbes, src_default as default, isReactNativeDependencyName, resetManifestCaches, shouldReadSecurityScanContent };
56279
+ export { ALL_REACT_DOCTOR_RULES, ALL_REACT_DOCTOR_RULE_KEYS, CROSS_FILE_DEPENDENCY_COLLECTORS, CROSS_FILE_RULE_IDS, EXTERNAL_RULES, FRAMEWORK_SPECIFIC_RULE_KEYS, FRAMEWORK_TOKENS, MOTION_LIBRARY_PACKAGES, NEXTJS_RULES, PREACT_RULES, REACT_COMPILER_RULES, REACT_DOCTOR_RULES, REACT_NATIVE_DEPENDENCY_NAMES, REACT_NATIVE_DEPENDENCY_PREFIXES, REACT_NATIVE_RULES, RECOMMENDED_RULES, RULES, TANSTACK_QUERY_RULES, TANSTACK_START_RULES, UNBOUNDED_CROSS_FILE_RULE_IDS, classifySecurityScanFile, collectCrossFileDependencyProbes, src_default as default, isReactNativeDependencyName, resetManifestCaches, shouldReadSecurityScanContent };
56234
56280
 
56235
56281
  //# sourceMappingURL=index.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "oxlint-plugin-react-doctor",
3
- "version": "0.7.2-dev.aa519e5",
3
+ "version": "0.7.2-dev.b97a92f",
4
4
  "description": "React Doctor rules for oxlint.",
5
5
  "keywords": [
6
6
  "accessibility",