oxlint-plugin-react-doctor 0.4.0-dev.fe5f3de → 0.4.1-dev.7d4b996

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.d.ts CHANGED
@@ -4470,6 +4470,25 @@ declare const REACT_DOCTOR_RULES: readonly [{
4470
4470
  readonly recommendation?: string;
4471
4471
  readonly create: (context: RuleContext) => RuleVisitors;
4472
4472
  };
4473
+ }, {
4474
+ readonly key: "react-doctor/prefer-explicit-variants";
4475
+ readonly id: "prefer-explicit-variants";
4476
+ readonly source: "react-doctor";
4477
+ readonly originallyExternal: false;
4478
+ readonly rule: {
4479
+ readonly framework: "global";
4480
+ readonly category: "Maintainability";
4481
+ readonly id: string;
4482
+ readonly title?: string;
4483
+ readonly severity: RuleSeverity;
4484
+ readonly requires?: ReadonlyArray<string>;
4485
+ readonly disabledBy?: ReadonlyArray<string>;
4486
+ readonly tags?: ReadonlyArray<string>;
4487
+ readonly defaultEnabled?: boolean;
4488
+ readonly lifecycle?: "retired";
4489
+ readonly recommendation?: string;
4490
+ readonly create: (context: RuleContext) => RuleVisitors;
4491
+ };
4473
4492
  }, {
4474
4493
  readonly key: "react-doctor/prefer-function-component";
4475
4494
  readonly id: "prefer-function-component";
@@ -10939,6 +10958,25 @@ declare const RULES: readonly [{
10939
10958
  readonly recommendation?: string;
10940
10959
  readonly create: (context: RuleContext) => RuleVisitors;
10941
10960
  };
10961
+ }, {
10962
+ readonly key: "react-doctor/prefer-explicit-variants";
10963
+ readonly id: "prefer-explicit-variants";
10964
+ readonly source: "react-doctor";
10965
+ readonly originallyExternal: false;
10966
+ readonly rule: {
10967
+ readonly framework: "global";
10968
+ readonly category: "Maintainability";
10969
+ readonly id: string;
10970
+ readonly title?: string;
10971
+ readonly severity: RuleSeverity;
10972
+ readonly requires?: ReadonlyArray<string>;
10973
+ readonly disabledBy?: ReadonlyArray<string>;
10974
+ readonly tags?: ReadonlyArray<string>;
10975
+ readonly defaultEnabled?: boolean;
10976
+ readonly lifecycle?: "retired";
10977
+ readonly recommendation?: string;
10978
+ readonly create: (context: RuleContext) => RuleVisitors;
10979
+ };
10942
10980
  }, {
10943
10981
  readonly key: "react-doctor/prefer-function-component";
10944
10982
  readonly id: "prefer-function-component";
package/dist/index.js CHANGED
@@ -19168,11 +19168,14 @@ const noLongTransitionDuration = defineRule({
19168
19168
  } })
19169
19169
  });
19170
19170
  //#endregion
19171
+ //#region src/plugin/utils/is-boolean-prefixed-prop-name.ts
19172
+ const BOOLEAN_PROP_PREFIX_PATTERN = /^(?:is|has|should|can|show|hide|enable|disable|with)[A-Z]/;
19173
+ const isBooleanPrefixedPropName = (propName) => BOOLEAN_PROP_PREFIX_PATTERN.test(propName);
19174
+ //#endregion
19171
19175
  //#region src/plugin/utils/is-component-declaration.ts
19172
19176
  const isComponentDeclaration = (node) => isNodeOfType(node, "FunctionDeclaration") && node.id !== null && Boolean(node.id?.name) && isUppercaseName(node.id.name);
19173
19177
  //#endregion
19174
19178
  //#region src/plugin/rules/architecture/no-many-boolean-props.ts
19175
- const BOOLEAN_PROP_PREFIX_PATTERN = /^(?:is|has|should|can|show|hide|enable|disable|with)[A-Z]/;
19176
19179
  const collectBooleanLikePropsFromBody = (componentBody, propsParamName) => {
19177
19180
  const found = /* @__PURE__ */ new Set();
19178
19181
  if (!componentBody) return found;
@@ -19182,7 +19185,7 @@ const collectBooleanLikePropsFromBody = (componentBody, propsParamName) => {
19182
19185
  if (!isNodeOfType(child.object, "Identifier")) return;
19183
19186
  if (child.object.name !== propsParamName) return;
19184
19187
  if (!isNodeOfType(child.property, "Identifier")) return;
19185
- if (!BOOLEAN_PROP_PREFIX_PATTERN.test(child.property.name)) return;
19188
+ if (!isBooleanPrefixedPropName(child.property.name)) return;
19186
19189
  found.add(child.property.name);
19187
19190
  });
19188
19191
  return found;
@@ -19208,7 +19211,7 @@ const noManyBooleanProps = defineRule({
19208
19211
  if (!isNodeOfType(property, "Property")) continue;
19209
19212
  const keyName = isNodeOfType(property.key, "Identifier") ? property.key.name : null;
19210
19213
  if (!keyName) continue;
19211
- if (BOOLEAN_PROP_PREFIX_PATTERN.test(keyName)) booleanLikePropNames.push(keyName);
19214
+ if (isBooleanPrefixedPropName(keyName)) booleanLikePropNames.push(keyName);
19212
19215
  }
19213
19216
  reportIfMany(booleanLikePropNames, componentName, reportNode);
19214
19217
  return;
@@ -25204,6 +25207,128 @@ const preferEs6Class = defineRule({
25204
25207
  }
25205
25208
  });
25206
25209
  //#endregion
25210
+ //#region src/plugin/utils/is-jsx-element-or-fragment.ts
25211
+ /**
25212
+ * Type-guard for the two single-node JSX output forms: `JSXElement`
25213
+ * (`<Foo />`) and `JSXFragment` (`<>…</>`). Canonical home for the
25214
+ * `isNodeOfType(x, "JSXElement") || isNodeOfType(x, "JSXFragment")` check
25215
+ * that many rules otherwise inline. Does NOT unwrap parens / TS wrappers —
25216
+ * callers that need the semantic expression should `stripParenExpression`
25217
+ * first.
25218
+ */
25219
+ const isJsxElementOrFragment = (node) => Boolean(node && (isNodeOfType(node, "JSXElement") || isNodeOfType(node, "JSXFragment")));
25220
+ //#endregion
25221
+ //#region src/plugin/rules/architecture/prefer-explicit-variants.ts
25222
+ const resolveBooleanPropTestName = (testNode, booleanPropBindings) => {
25223
+ let identifierNode = stripParenExpression(testNode);
25224
+ if (isNodeOfType(identifierNode, "UnaryExpression") && identifierNode.operator === "!") identifierNode = stripParenExpression(identifierNode.argument);
25225
+ if (!isNodeOfType(identifierNode, "Identifier")) return null;
25226
+ return booleanPropBindings.has(identifierNode.name) ? identifierNode.name : null;
25227
+ };
25228
+ const CROSS_CUTTING_STATE_BOOLEAN_NAMES = new Set([
25229
+ "isLoading",
25230
+ "isPending",
25231
+ "isFetching",
25232
+ "isRefetching",
25233
+ "isSubmitting",
25234
+ "isError",
25235
+ "isSuccess",
25236
+ "isEmpty",
25237
+ "isReady",
25238
+ "isDirty",
25239
+ "isValid",
25240
+ "isInvalid",
25241
+ "isOpen",
25242
+ "isClosed",
25243
+ "isVisible",
25244
+ "isHidden",
25245
+ "isActive",
25246
+ "isInactive",
25247
+ "isExpanded",
25248
+ "isCollapsed",
25249
+ "isSelected",
25250
+ "isChecked",
25251
+ "isDisabled",
25252
+ "isEnabled",
25253
+ "isFocused",
25254
+ "isHovered",
25255
+ "isDragging",
25256
+ "isFullscreen",
25257
+ "isMobile",
25258
+ "isDesktop",
25259
+ "isTablet",
25260
+ "isOnline",
25261
+ "isOffline",
25262
+ "isLoggedIn",
25263
+ "isAuthenticated",
25264
+ "isAuthorized",
25265
+ "isDark",
25266
+ "isLight"
25267
+ ]);
25268
+ const collectBooleanPropBindings = (param) => {
25269
+ const bindings = /* @__PURE__ */ new Set();
25270
+ if (!param || !isNodeOfType(param, "ObjectPattern")) return bindings;
25271
+ for (const property of param.properties ?? []) {
25272
+ if (!isNodeOfType(property, "Property")) continue;
25273
+ if (property.computed) continue;
25274
+ if (!isNodeOfType(property.key, "Identifier")) continue;
25275
+ if (!isBooleanPrefixedPropName(property.key.name)) continue;
25276
+ if (CROSS_CUTTING_STATE_BOOLEAN_NAMES.has(property.key.name)) continue;
25277
+ const propertyValue = property.value;
25278
+ if (isNodeOfType(propertyValue, "Identifier")) bindings.add(propertyValue.name);
25279
+ else if (isNodeOfType(propertyValue, "AssignmentPattern") && isNodeOfType(propertyValue.left, "Identifier")) bindings.add(propertyValue.left.name);
25280
+ }
25281
+ return bindings;
25282
+ };
25283
+ const collectVariantBranchProps = (body, booleanPropBindings) => {
25284
+ const variantBranchProps = /* @__PURE__ */ new Set();
25285
+ if (!body) return variantBranchProps;
25286
+ walkAst(body, (current) => {
25287
+ if (isNodeOfType(current, "FunctionDeclaration") || isInlineFunctionExpression(current)) return false;
25288
+ if (!isNodeOfType(current, "ConditionalExpression")) return;
25289
+ const propName = resolveBooleanPropTestName(current.test, booleanPropBindings);
25290
+ if (!propName) return;
25291
+ const consequent = stripParenExpression(current.consequent);
25292
+ const alternate = stripParenExpression(current.alternate);
25293
+ if (!isJsxElementOrFragment(consequent) || !isJsxElementOrFragment(alternate)) return;
25294
+ variantBranchProps.add(propName);
25295
+ });
25296
+ return variantBranchProps;
25297
+ };
25298
+ const preferExplicitVariants = defineRule({
25299
+ id: "prefer-explicit-variants",
25300
+ title: "Prefer explicit variant components",
25301
+ severity: "warn",
25302
+ tags: ["test-noise", "react-jsx-only"],
25303
+ recommendation: "Replace boolean props that switch whole subtrees with explicit variant components, like `<ThreadComposer />` and `<EditMessageComposer />`, so each variant renders one clear path.",
25304
+ create: (context) => {
25305
+ const checkComponent = (param, body, componentName, reportNode) => {
25306
+ const booleanPropBindings = collectBooleanPropBindings(param);
25307
+ if (booleanPropBindings.size < 2) return;
25308
+ const variantBranchProps = collectVariantBranchProps(body, booleanPropBindings);
25309
+ if (variantBranchProps.size < 2) return;
25310
+ const propList = [...variantBranchProps].slice(0, 3).join(", ");
25311
+ const overflow = variantBranchProps.size > 3 ? "…" : "";
25312
+ context.report({
25313
+ node: reportNode,
25314
+ message: `Component "${componentName}" picks which component to render from ${variantBranchProps.size} boolean props (${propList}${overflow}), which multiplies untestable variants. Split it into explicit variant components so each renders one clear path.`
25315
+ });
25316
+ };
25317
+ return {
25318
+ FunctionDeclaration(node) {
25319
+ if (!isComponentDeclaration(node) || !node.id) return;
25320
+ checkComponent(node.params?.[0], node.body, node.id.name, node.id);
25321
+ },
25322
+ VariableDeclarator(node) {
25323
+ if (!isComponentAssignment(node)) return;
25324
+ if (!isNodeOfType(node.id, "Identifier")) return;
25325
+ if (!isInlineFunctionExpression(node.init)) return;
25326
+ checkComponent(node.init.params?.[0], node.init.body, node.id.name, node.id);
25327
+ }
25328
+ };
25329
+ }
25330
+ });
25331
+ //#endregion
25207
25332
  //#region src/plugin/rules/react-builtins/prefer-function-component.ts
25208
25333
  const MESSAGE$7 = "This class component is harder to maintain than a function component.";
25209
25334
  const resolveSettings$4 = (settings) => {
@@ -37524,6 +37649,17 @@ const reactDoctorRules = [
37524
37649
  category: "Maintainability"
37525
37650
  }
37526
37651
  },
37652
+ {
37653
+ key: "react-doctor/prefer-explicit-variants",
37654
+ id: "prefer-explicit-variants",
37655
+ source: "react-doctor",
37656
+ originallyExternal: false,
37657
+ rule: {
37658
+ ...preferExplicitVariants,
37659
+ framework: "global",
37660
+ category: "Maintainability"
37661
+ }
37662
+ },
37527
37663
  {
37528
37664
  key: "react-doctor/prefer-function-component",
37529
37665
  id: "prefer-function-component",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "oxlint-plugin-react-doctor",
3
- "version": "0.4.0-dev.fe5f3de",
3
+ "version": "0.4.1-dev.7d4b996",
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",