oxlint-plugin-react-doctor 0.5.6-dev.937a7ca → 0.5.6-dev.a9d2713
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 +0 -1
- package/dist/index.js +52 -94
- package/package.json +1 -1
package/dist/index.d.ts
CHANGED
|
@@ -36,7 +36,6 @@ interface ControlFlowAnalysis {
|
|
|
36
36
|
readonly cfgFor: (functionLike: EsTreeNode) => FunctionCfg | null;
|
|
37
37
|
readonly enclosingFunction: (node: EsTreeNode) => EsTreeNode | null;
|
|
38
38
|
readonly isUnconditionalFromEntry: (node: EsTreeNode) => boolean;
|
|
39
|
-
readonly dominatesExit: (node: EsTreeNode) => boolean;
|
|
40
39
|
}
|
|
41
40
|
//#endregion
|
|
42
41
|
//#region src/plugin/semantic/scope-analysis.d.ts
|
package/dist/index.js
CHANGED
|
@@ -3019,7 +3019,7 @@ const ABSTRACT_ROLES = new Set([
|
|
|
3019
3019
|
"widget",
|
|
3020
3020
|
"window"
|
|
3021
3021
|
]);
|
|
3022
|
-
const PRESENTATION_ROLES$
|
|
3022
|
+
const PRESENTATION_ROLES$1 = new Set(["presentation", "none"]);
|
|
3023
3023
|
//#endregion
|
|
3024
3024
|
//#region src/plugin/rules/a11y/aria-role.ts
|
|
3025
3025
|
const buildBaseMessage = (suffix) => `This \`role\` is not a valid ARIA role, so assistive tech cannot expose it correctly. Use a real, non-abstract role.${suffix}`;
|
|
@@ -4710,6 +4710,14 @@ const checkedRequiresOnchangeOrReadonly = defineRule({
|
|
|
4710
4710
|
}
|
|
4711
4711
|
});
|
|
4712
4712
|
//#endregion
|
|
4713
|
+
//#region src/plugin/utils/is-presentation-role.ts
|
|
4714
|
+
const isPresentationRole = (openingElement) => {
|
|
4715
|
+
const roleAttribute = hasJsxPropIgnoreCase(openingElement.attributes, "role");
|
|
4716
|
+
if (!roleAttribute) return false;
|
|
4717
|
+
const value = getJsxPropStringValue(roleAttribute);
|
|
4718
|
+
return value !== null && PRESENTATION_ROLES$1.has(value);
|
|
4719
|
+
};
|
|
4720
|
+
//#endregion
|
|
4713
4721
|
//#region src/plugin/utils/is-pure-event-blocker-handler.ts
|
|
4714
4722
|
const BLOCKER_METHOD_NAMES = new Set([
|
|
4715
4723
|
"stopPropagation",
|
|
@@ -4747,7 +4755,6 @@ const isPureEventBlockerHandler = (attribute) => {
|
|
|
4747
4755
|
};
|
|
4748
4756
|
//#endregion
|
|
4749
4757
|
//#region src/plugin/rules/a11y/click-events-have-key-events.ts
|
|
4750
|
-
const PRESENTATION_ROLES$1 = new Set(["presentation", "none"]);
|
|
4751
4758
|
const MESSAGE$56 = "Keyboard users can't trigger this click handler because there's no keyboard one, so add `onKeyUp`, `onKeyDown`, or `onKeyPress`.";
|
|
4752
4759
|
const KEY_HANDLERS = [
|
|
4753
4760
|
"onKeyUp",
|
|
@@ -4772,11 +4779,7 @@ const clickEventsHaveKeyEvents = defineRule({
|
|
|
4772
4779
|
if (!onClick) return;
|
|
4773
4780
|
if (isPureEventBlockerHandler(onClick)) return;
|
|
4774
4781
|
if (isHiddenFromScreenReader(node, context.settings)) return;
|
|
4775
|
-
|
|
4776
|
-
if (roleAttribute) {
|
|
4777
|
-
const roleValue = getJsxPropStringValue(roleAttribute);
|
|
4778
|
-
if (roleValue && PRESENTATION_ROLES$1.has(roleValue)) return;
|
|
4779
|
-
}
|
|
4782
|
+
if (isPresentationRole(node)) return;
|
|
4780
4783
|
if (KEY_HANDLERS.some((handler) => hasJsxPropIgnoreCase(node.attributes, handler))) return;
|
|
4781
4784
|
context.report({
|
|
4782
4785
|
node: node.name,
|
|
@@ -5747,7 +5750,7 @@ const displayName = defineRule({
|
|
|
5747
5750
|
category: "Architecture",
|
|
5748
5751
|
create: (context) => {
|
|
5749
5752
|
const settings = resolveSettings$44(context.settings);
|
|
5750
|
-
const ignoreNamed = settings.ignoreTranspilerName
|
|
5753
|
+
const ignoreNamed = !settings.ignoreTranspilerName;
|
|
5751
5754
|
const reportAt = (node) => {
|
|
5752
5755
|
context.report({
|
|
5753
5756
|
node,
|
|
@@ -8170,7 +8173,6 @@ const htmlHasLang = defineRule({
|
|
|
8170
8173
|
return { JSXOpeningElement(node) {
|
|
8171
8174
|
const tag = getElementType(node, context.settings);
|
|
8172
8175
|
if (!tagSet.has(tag)) return;
|
|
8173
|
-
const hasSpread = node.attributes.some((attribute) => isNodeOfType(attribute, "JSXSpreadAttribute"));
|
|
8174
8176
|
const lang = hasJsxPropIgnoreCase(node.attributes, "lang");
|
|
8175
8177
|
if (!lang) {
|
|
8176
8178
|
context.report({
|
|
@@ -8179,16 +8181,8 @@ const htmlHasLang = defineRule({
|
|
|
8179
8181
|
});
|
|
8180
8182
|
return;
|
|
8181
8183
|
}
|
|
8182
|
-
|
|
8183
|
-
|
|
8184
|
-
context.report({
|
|
8185
|
-
node: lang,
|
|
8186
|
-
message: MESSAGE$50
|
|
8187
|
-
});
|
|
8188
|
-
return;
|
|
8189
|
-
}
|
|
8190
|
-
if (hasSpread && !lang) context.report({
|
|
8191
|
-
node: node.name,
|
|
8184
|
+
if (evaluateLang(lang.value) === "empty") context.report({
|
|
8185
|
+
node: lang,
|
|
8192
8186
|
message: MESSAGE$50
|
|
8193
8187
|
});
|
|
8194
8188
|
} };
|
|
@@ -8902,14 +8896,6 @@ const isNonInteractiveElement = (elementType, openingElement) => {
|
|
|
8902
8896
|
//#region src/plugin/utils/is-non-interactive-role.ts
|
|
8903
8897
|
const isNonInteractiveRole = (role) => NON_INTERACTIVE_ROLES.has(role);
|
|
8904
8898
|
//#endregion
|
|
8905
|
-
//#region src/plugin/utils/is-presentation-role.ts
|
|
8906
|
-
const isPresentationRole = (openingElement) => {
|
|
8907
|
-
const roleAttribute = hasJsxPropIgnoreCase(openingElement.attributes, "role");
|
|
8908
|
-
if (!roleAttribute) return false;
|
|
8909
|
-
const value = getJsxPropStringValue(roleAttribute);
|
|
8910
|
-
return value !== null && PRESENTATION_ROLES$2.has(value);
|
|
8911
|
-
};
|
|
8912
|
-
//#endregion
|
|
8913
8899
|
//#region src/plugin/rules/a11y/interactive-supports-focus.ts
|
|
8914
8900
|
const buildTabbableMessage = (role) => `Keyboard users can't tab to this '${role}' because it isn't focusable, so add \`tabIndex={0}\`.`;
|
|
8915
8901
|
const buildFocusableMessage = (role) => `Keyboard users can't focus this '${role}' because it can't receive focus, so add \`tabIndex={0}\` or \`tabIndex={-1}\`.`;
|
|
@@ -10684,6 +10670,24 @@ const hasJsxKeyAttribute = (openingElement) => {
|
|
|
10684
10670
|
return false;
|
|
10685
10671
|
};
|
|
10686
10672
|
//#endregion
|
|
10673
|
+
//#region src/plugin/utils/is-non-children-jsx-attribute-value.ts
|
|
10674
|
+
const ascendThroughJsxValueWrappers = (node) => {
|
|
10675
|
+
let current = node;
|
|
10676
|
+
while (current.parent) {
|
|
10677
|
+
const parent = current.parent;
|
|
10678
|
+
if (!(isNodeOfType(parent, "ChainExpression") || isNodeOfType(parent, "TSAsExpression") || isNodeOfType(parent, "TSSatisfiesExpression") || isNodeOfType(parent, "TSNonNullExpression") || isNodeOfType(parent, "LogicalExpression") || isNodeOfType(parent, "ConditionalExpression") && parent.test !== current)) break;
|
|
10679
|
+
current = parent;
|
|
10680
|
+
}
|
|
10681
|
+
return current;
|
|
10682
|
+
};
|
|
10683
|
+
const isNonChildrenJsxAttributeValue = (node) => {
|
|
10684
|
+
const container = ascendThroughJsxValueWrappers(node).parent;
|
|
10685
|
+
if (!container || !isNodeOfType(container, "JSXExpressionContainer")) return false;
|
|
10686
|
+
const attribute = container.parent;
|
|
10687
|
+
if (!attribute || !isNodeOfType(attribute, "JSXAttribute")) return false;
|
|
10688
|
+
return getJsxAttributeName(attribute.name) !== "children";
|
|
10689
|
+
};
|
|
10690
|
+
//#endregion
|
|
10687
10691
|
//#region src/plugin/rules/react-builtins/jsx-key.ts
|
|
10688
10692
|
const ITERATOR_METHOD_NAMES = new Set([
|
|
10689
10693
|
"map",
|
|
@@ -10722,6 +10726,7 @@ const findEnclosingIteratorContext = (jsxNode) => {
|
|
|
10722
10726
|
const arrayParent = parent.parent;
|
|
10723
10727
|
if (arrayParent && isNodeOfType(arrayParent, "Property")) return null;
|
|
10724
10728
|
if (arrayParent && isNodeOfType(arrayParent, "ArrayExpression")) return null;
|
|
10729
|
+
if (isNonChildrenJsxAttributeValue(parent)) return null;
|
|
10725
10730
|
return { kind: "array" };
|
|
10726
10731
|
} else if (isNodeOfType(parent, "CallExpression")) {
|
|
10727
10732
|
const callee = parent.callee;
|
|
@@ -10734,10 +10739,13 @@ const findEnclosingIteratorContext = (jsxNode) => {
|
|
|
10734
10739
|
if (!targetArg) return null;
|
|
10735
10740
|
let walker = current;
|
|
10736
10741
|
while (walker && walker !== parent) {
|
|
10737
|
-
if (walker === targetArg)
|
|
10738
|
-
|
|
10739
|
-
|
|
10740
|
-
|
|
10742
|
+
if (walker === targetArg) {
|
|
10743
|
+
if (isNonChildrenJsxAttributeValue(parent)) return null;
|
|
10744
|
+
return {
|
|
10745
|
+
kind: "iterator",
|
|
10746
|
+
callExpression: parent
|
|
10747
|
+
};
|
|
10748
|
+
}
|
|
10741
10749
|
walker = walker.parent ?? null;
|
|
10742
10750
|
}
|
|
10743
10751
|
return null;
|
|
@@ -17430,6 +17438,7 @@ const noDanger = defineRule({
|
|
|
17430
17438
|
title: "Raw HTML injection can run unsafe markup",
|
|
17431
17439
|
severity: "warn",
|
|
17432
17440
|
category: "Security",
|
|
17441
|
+
defaultEnabled: false,
|
|
17433
17442
|
recommendation: "Render trusted content as React children so attacker-controlled HTML cannot run in users' browsers.",
|
|
17434
17443
|
create: (context) => ({
|
|
17435
17444
|
JSXOpeningElement(node) {
|
|
@@ -25419,15 +25428,8 @@ const expressionContainsJsxOrCreateElement = (root) => {
|
|
|
25419
25428
|
visit(root);
|
|
25420
25429
|
return found;
|
|
25421
25430
|
};
|
|
25422
|
-
const classExtendsReactComponent$1 = (classNode) => {
|
|
25423
|
-
const superClass = classNode.superClass;
|
|
25424
|
-
if (!superClass) return false;
|
|
25425
|
-
if (isNodeOfType(superClass, "Identifier") && (superClass.name === "Component" || superClass.name === "PureComponent")) return true;
|
|
25426
|
-
if (isNodeOfType(superClass, "MemberExpression") && isNodeOfType(superClass.object, "Identifier") && superClass.object.name === "React" && isNodeOfType(superClass.property, "Identifier") && (superClass.property.name === "Component" || superClass.property.name === "PureComponent")) return true;
|
|
25427
|
-
return false;
|
|
25428
|
-
};
|
|
25429
25431
|
const isReactClassComponent = (classNode) => {
|
|
25430
|
-
if (
|
|
25432
|
+
if (isEs6Component(classNode)) return true;
|
|
25431
25433
|
return expressionContainsJsxOrCreateElement(classNode);
|
|
25432
25434
|
};
|
|
25433
25435
|
const findEnclosingComponent = (node) => {
|
|
@@ -25587,7 +25589,7 @@ const noUnstableNestedComponents = defineRule({
|
|
|
25587
25589
|
create: (context) => {
|
|
25588
25590
|
const settings = resolveSettings$8(context.settings);
|
|
25589
25591
|
const renderPropRegex = compileGlob(settings.propNamePattern);
|
|
25590
|
-
const reportCandidate = (candidateNode, reportNode
|
|
25592
|
+
const reportCandidate = (candidateNode, reportNode) => {
|
|
25591
25593
|
if (isFirstArgumentOfHocCall(candidateNode)) return;
|
|
25592
25594
|
if (isReturnOfMapCallback(candidateNode)) return;
|
|
25593
25595
|
const propInfo = isComponentDeclaredInProp(candidateNode);
|
|
@@ -25608,7 +25610,7 @@ const noUnstableNestedComponents = defineRule({
|
|
|
25608
25610
|
const inferredName = inferFunctionLikeName(node);
|
|
25609
25611
|
const propInfo = isComponentDeclaredInProp(node);
|
|
25610
25612
|
if (!(inferredName !== null && isReactComponentName(inferredName) || propInfo !== null || isObjectCallbackCandidate(node))) return;
|
|
25611
|
-
reportCandidate(node, node
|
|
25613
|
+
reportCandidate(node, node);
|
|
25612
25614
|
};
|
|
25613
25615
|
return {
|
|
25614
25616
|
FunctionDeclaration: checkFunctionLike,
|
|
@@ -25618,18 +25620,18 @@ const noUnstableNestedComponents = defineRule({
|
|
|
25618
25620
|
if (!node.id) return;
|
|
25619
25621
|
if (!isReactComponentName(node.id.name)) return;
|
|
25620
25622
|
if (!isReactClassComponent(node)) return;
|
|
25621
|
-
reportCandidate(node, node
|
|
25623
|
+
reportCandidate(node, node);
|
|
25622
25624
|
},
|
|
25623
25625
|
ClassExpression(node) {
|
|
25624
25626
|
const inferredName = node.id?.name ?? inferFunctionLikeName(node);
|
|
25625
25627
|
if (!inferredName || !isReactComponentName(inferredName)) return;
|
|
25626
25628
|
if (!isReactClassComponent(node)) return;
|
|
25627
|
-
reportCandidate(node, node
|
|
25629
|
+
reportCandidate(node, node);
|
|
25628
25630
|
},
|
|
25629
25631
|
CallExpression(node) {
|
|
25630
25632
|
if (!isHocCallee$1(node)) return;
|
|
25631
25633
|
if (!hocCallContainsComponent(node)) return;
|
|
25632
|
-
reportCandidate(node, node
|
|
25634
|
+
reportCandidate(node, node);
|
|
25633
25635
|
}
|
|
25634
25636
|
};
|
|
25635
25637
|
}
|
|
@@ -26069,13 +26071,6 @@ const skipTsExpression = (expression) => {
|
|
|
26069
26071
|
if (expression.type === "TSAsExpression" || expression.type === "TSSatisfiesExpression" || expression.type === "TSNonNullExpression") return skipTsExpression(expression.expression);
|
|
26070
26072
|
return expression;
|
|
26071
26073
|
};
|
|
26072
|
-
const classExtendsReactComponent = (classNode) => {
|
|
26073
|
-
const superClass = classNode.superClass;
|
|
26074
|
-
if (!superClass) return false;
|
|
26075
|
-
if (isNodeOfType(superClass, "Identifier") && (superClass.name === "Component" || superClass.name === "PureComponent")) return true;
|
|
26076
|
-
if (isNodeOfType(superClass, "MemberExpression") && isNodeOfType(superClass.object, "Identifier") && superClass.object.name === "React" && isNodeOfType(superClass.property, "Identifier") && (superClass.property.name === "Component" || superClass.property.name === "PureComponent")) return true;
|
|
26077
|
-
return false;
|
|
26078
|
-
};
|
|
26079
26074
|
const isReactCreateContext = (initializer) => {
|
|
26080
26075
|
if (!initializer) return false;
|
|
26081
26076
|
const expression = skipTsExpression(initializer);
|
|
@@ -26266,7 +26261,7 @@ const onlyExportComponents = defineRule({
|
|
|
26266
26261
|
if (stripped.id) {
|
|
26267
26262
|
const idNode = stripped.id;
|
|
26268
26263
|
isExportedNodeIds.add(stripped);
|
|
26269
|
-
if (isReactComponentName(idNode.name) &&
|
|
26264
|
+
if (isReactComponentName(idNode.name) && isEs6Component(stripped)) hasReactExport = true;
|
|
26270
26265
|
else exports.push({
|
|
26271
26266
|
kind: "non-component",
|
|
26272
26267
|
reportNode: idNode
|
|
@@ -26326,7 +26321,7 @@ const onlyExportComponents = defineRule({
|
|
|
26326
26321
|
exports.push(classifyExport(declaration.id.name, declaration.id, true, null, state));
|
|
26327
26322
|
} else if (isNodeOfType(declaration, "ClassDeclaration") && declaration.id) {
|
|
26328
26323
|
isExportedNodeIds.add(declaration);
|
|
26329
|
-
if (isReactComponentName(declaration.id.name) &&
|
|
26324
|
+
if (isReactComponentName(declaration.id.name) && isEs6Component(declaration)) exports.push({ kind: "react-component" });
|
|
26330
26325
|
else exports.push({
|
|
26331
26326
|
kind: "non-component",
|
|
26332
26327
|
reportNode: declaration.id
|
|
@@ -35430,6 +35425,7 @@ const serverFetchWithoutRevalidate = defineRule({
|
|
|
35430
35425
|
CallExpression(node) {
|
|
35431
35426
|
if (!isServerSideFile) return;
|
|
35432
35427
|
if (!isFetchCall(node)) return;
|
|
35428
|
+
if (isMutatingFetchCall(node)) return;
|
|
35433
35429
|
const optionsArg = node.arguments?.[1];
|
|
35434
35430
|
if (optionsArg && objectExpressionHasNextRevalidate(optionsArg)) return;
|
|
35435
35431
|
const urlArg = node.arguments?.[0];
|
|
@@ -42282,32 +42278,6 @@ const computeUnconditionalSet = (cfg) => {
|
|
|
42282
42278
|
}
|
|
42283
42279
|
return unconditional;
|
|
42284
42280
|
};
|
|
42285
|
-
const computeDominatesExit = (cfg) => {
|
|
42286
|
-
const reachableToExit = /* @__PURE__ */ new Set();
|
|
42287
|
-
const queue = [cfg.exit];
|
|
42288
|
-
while (queue.length > 0) {
|
|
42289
|
-
const block = queue.shift();
|
|
42290
|
-
if (reachableToExit.has(block)) continue;
|
|
42291
|
-
reachableToExit.add(block);
|
|
42292
|
-
for (const edge of block.predecessors) queue.push(edge.from);
|
|
42293
|
-
}
|
|
42294
|
-
const dominatesExit = /* @__PURE__ */ new Set();
|
|
42295
|
-
const visit = (block) => {
|
|
42296
|
-
if (block === cfg.exit) return true;
|
|
42297
|
-
if (dominatesExit.has(block)) return true;
|
|
42298
|
-
if (block.successors.length === 0) return false;
|
|
42299
|
-
dominatesExit.add(block);
|
|
42300
|
-
let allReach = true;
|
|
42301
|
-
for (const edge of block.successors) if (!visit(edge.to)) {
|
|
42302
|
-
allReach = false;
|
|
42303
|
-
break;
|
|
42304
|
-
}
|
|
42305
|
-
if (!allReach) dominatesExit.delete(block);
|
|
42306
|
-
return allReach;
|
|
42307
|
-
};
|
|
42308
|
-
for (const block of cfg.blocks) visit(block);
|
|
42309
|
-
return dominatesExit;
|
|
42310
|
-
};
|
|
42311
42281
|
const analyzeControlFlow = (program) => {
|
|
42312
42282
|
nextBlockId = 0;
|
|
42313
42283
|
const functionCfgs = /* @__PURE__ */ new Map();
|
|
@@ -42315,8 +42285,7 @@ const analyzeControlFlow = (program) => {
|
|
|
42315
42285
|
const cfg = buildFunctionCfg(functionNode, body);
|
|
42316
42286
|
functionCfgs.set(functionNode, {
|
|
42317
42287
|
cfg,
|
|
42318
|
-
unconditionalSet: computeUnconditionalSet(cfg)
|
|
42319
|
-
dominatesExitSet: computeDominatesExit(cfg)
|
|
42288
|
+
unconditionalSet: computeUnconditionalSet(cfg)
|
|
42320
42289
|
});
|
|
42321
42290
|
};
|
|
42322
42291
|
if (isNodeOfType(program, "Program")) buildFor(program, {
|
|
@@ -42359,20 +42328,10 @@ const analyzeControlFlow = (program) => {
|
|
|
42359
42328
|
if (!block) return true;
|
|
42360
42329
|
return entry.unconditionalSet.has(block);
|
|
42361
42330
|
};
|
|
42362
|
-
const dominatesExit = (node) => {
|
|
42363
|
-
const owner = enclosingFunction(node);
|
|
42364
|
-
if (!owner) return true;
|
|
42365
|
-
const entry = functionCfgs.get(owner);
|
|
42366
|
-
if (!entry) return true;
|
|
42367
|
-
const block = entry.cfg.blockOf(node);
|
|
42368
|
-
if (!block) return true;
|
|
42369
|
-
return entry.dominatesExitSet.has(block);
|
|
42370
|
-
};
|
|
42371
42331
|
return {
|
|
42372
42332
|
cfgFor,
|
|
42373
42333
|
enclosingFunction,
|
|
42374
|
-
isUnconditionalFromEntry
|
|
42375
|
-
dominatesExit
|
|
42334
|
+
isUnconditionalFromEntry
|
|
42376
42335
|
};
|
|
42377
42336
|
};
|
|
42378
42337
|
//#endregion
|
|
@@ -42397,8 +42356,7 @@ const buildFallbackScopes = () => ({
|
|
|
42397
42356
|
const FALLBACK_CFG = {
|
|
42398
42357
|
cfgFor: () => null,
|
|
42399
42358
|
enclosingFunction: () => null,
|
|
42400
|
-
isUnconditionalFromEntry: () => false
|
|
42401
|
-
dominatesExit: () => false
|
|
42359
|
+
isUnconditionalFromEntry: () => false
|
|
42402
42360
|
};
|
|
42403
42361
|
const wrapWithSemanticContext = (rule) => ({
|
|
42404
42362
|
...rule,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "oxlint-plugin-react-doctor",
|
|
3
|
-
"version": "0.5.6-dev.
|
|
3
|
+
"version": "0.5.6-dev.a9d2713",
|
|
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",
|