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 +38 -0
- package/dist/index.js +139 -3
- package/package.json +1 -1
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 (!
|
|
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 (
|
|
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.
|
|
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",
|