oxlint-plugin-react-doctor 0.5.6-dev.451beeb → 0.5.6-dev.50999f4
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 +141 -126
- 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
|
@@ -890,24 +890,64 @@ const advancedEventHandlerRefs = defineRule({
|
|
|
890
890
|
});
|
|
891
891
|
//#endregion
|
|
892
892
|
//#region src/plugin/rules/security-scan/utils/strip-comments-preserving-positions.ts
|
|
893
|
-
const
|
|
893
|
+
const WHITESPACE_PATTERN = /\s/;
|
|
894
|
+
const quotedLiteralHasWhitespace = (content, openQuoteIndex, delimiter) => {
|
|
895
|
+
for (let cursor = openQuoteIndex + 1; cursor < content.length; cursor += 1) {
|
|
896
|
+
const character = content[cursor];
|
|
897
|
+
if (character === "\\") {
|
|
898
|
+
cursor += 1;
|
|
899
|
+
continue;
|
|
900
|
+
}
|
|
901
|
+
if (character === delimiter) return false;
|
|
902
|
+
if (WHITESPACE_PATTERN.test(character)) return true;
|
|
903
|
+
}
|
|
904
|
+
return false;
|
|
905
|
+
};
|
|
906
|
+
const blankNonCodePreservingPositions = (content, blankStringContents) => {
|
|
894
907
|
const characters = content.split("");
|
|
895
908
|
let stringDelimiter = null;
|
|
909
|
+
let isBlankingString = false;
|
|
910
|
+
const templateExpressionDepths = [];
|
|
896
911
|
let index = 0;
|
|
912
|
+
const blankUnlessNewline = (offset) => {
|
|
913
|
+
if (offset < content.length && content[offset] !== "\n") characters[offset] = " ";
|
|
914
|
+
};
|
|
897
915
|
while (index < content.length) {
|
|
898
916
|
const character = content[index];
|
|
899
917
|
const nextCharacter = content[index + 1];
|
|
900
918
|
if (stringDelimiter !== null) {
|
|
901
919
|
if (character === "\\") {
|
|
920
|
+
if (isBlankingString) {
|
|
921
|
+
blankUnlessNewline(index);
|
|
922
|
+
blankUnlessNewline(index + 1);
|
|
923
|
+
}
|
|
902
924
|
index += 2;
|
|
903
925
|
continue;
|
|
904
926
|
}
|
|
905
|
-
if (character === stringDelimiter)
|
|
927
|
+
if (character === stringDelimiter) {
|
|
928
|
+
stringDelimiter = null;
|
|
929
|
+
index += 1;
|
|
930
|
+
continue;
|
|
931
|
+
}
|
|
932
|
+
if (blankStringContents && stringDelimiter === "`" && character === "$" && nextCharacter === "{") {
|
|
933
|
+
templateExpressionDepths.push(0);
|
|
934
|
+
stringDelimiter = null;
|
|
935
|
+
index += 2;
|
|
936
|
+
continue;
|
|
937
|
+
}
|
|
938
|
+
if (isBlankingString) blankUnlessNewline(index);
|
|
906
939
|
index += 1;
|
|
907
940
|
continue;
|
|
908
941
|
}
|
|
909
|
-
if (character === "\"" || character === "'"
|
|
942
|
+
if (character === "\"" || character === "'") {
|
|
910
943
|
stringDelimiter = character;
|
|
944
|
+
isBlankingString = blankStringContents && quotedLiteralHasWhitespace(content, index, character);
|
|
945
|
+
index += 1;
|
|
946
|
+
continue;
|
|
947
|
+
}
|
|
948
|
+
if (character === "`") {
|
|
949
|
+
stringDelimiter = "`";
|
|
950
|
+
isBlankingString = blankStringContents;
|
|
911
951
|
index += 1;
|
|
912
952
|
continue;
|
|
913
953
|
}
|
|
@@ -926,29 +966,42 @@ const stripCommentsPreservingPositions = (content) => {
|
|
|
926
966
|
index += 2;
|
|
927
967
|
break;
|
|
928
968
|
}
|
|
929
|
-
|
|
969
|
+
blankUnlessNewline(index);
|
|
930
970
|
index += 1;
|
|
931
971
|
}
|
|
932
972
|
continue;
|
|
933
973
|
}
|
|
974
|
+
if (templateExpressionDepths.length > 0) {
|
|
975
|
+
const innermost = templateExpressionDepths.length - 1;
|
|
976
|
+
if (character === "{") templateExpressionDepths[innermost] += 1;
|
|
977
|
+
else if (character === "}") if (templateExpressionDepths[innermost] === 0) {
|
|
978
|
+
templateExpressionDepths.pop();
|
|
979
|
+
stringDelimiter = "`";
|
|
980
|
+
isBlankingString = blankStringContents;
|
|
981
|
+
} else templateExpressionDepths[innermost] -= 1;
|
|
982
|
+
}
|
|
934
983
|
index += 1;
|
|
935
984
|
}
|
|
936
985
|
return characters.join("");
|
|
937
986
|
};
|
|
987
|
+
const stripCommentsPreservingPositions = (content) => blankNonCodePreservingPositions(content, false);
|
|
988
|
+
const stripCommentsAndStringLiteralsPreservingPositions = (content) => blankNonCodePreservingPositions(content, true);
|
|
938
989
|
//#endregion
|
|
939
990
|
//#region src/plugin/rules/security-scan/utils/scan-by-pattern.ts
|
|
940
991
|
const strippedContentCache = /* @__PURE__ */ new WeakMap();
|
|
941
|
-
const
|
|
992
|
+
const stringStrippedContentCache = /* @__PURE__ */ new WeakMap();
|
|
993
|
+
const getScannableContent = (file, ignoreStringLiterals = false) => {
|
|
942
994
|
if (!SOURCE_FILE_PATTERN.test(file.relativePath)) return file.content;
|
|
943
|
-
const
|
|
995
|
+
const cache = ignoreStringLiterals ? stringStrippedContentCache : strippedContentCache;
|
|
996
|
+
const cachedContent = cache.get(file);
|
|
944
997
|
if (cachedContent !== void 0) return cachedContent;
|
|
945
|
-
const strippedContent = stripCommentsPreservingPositions(file.content);
|
|
946
|
-
|
|
998
|
+
const strippedContent = ignoreStringLiterals ? stripCommentsAndStringLiteralsPreservingPositions(file.content) : stripCommentsPreservingPositions(file.content);
|
|
999
|
+
cache.set(file, strippedContent);
|
|
947
1000
|
return strippedContent;
|
|
948
1001
|
};
|
|
949
|
-
const scanByPattern = ({ shouldScan, pattern, requireAll, suppressWhen, message }) => (file) => {
|
|
1002
|
+
const scanByPattern = ({ shouldScan, pattern, requireAll, suppressWhen, ignoreStringLiterals, message }) => (file) => {
|
|
950
1003
|
if (!shouldScan(file)) return [];
|
|
951
|
-
const content = getScannableContent(file);
|
|
1004
|
+
const content = getScannableContent(file, ignoreStringLiterals);
|
|
952
1005
|
if (requireAll !== void 0 && !requireAll.every((gate) => gate.test(content))) return [];
|
|
953
1006
|
const matchedPattern = (pattern instanceof RegExp ? [pattern] : pattern).find((candidate) => candidate.test(content));
|
|
954
1007
|
if (matchedPattern === void 0) return [];
|
|
@@ -973,6 +1026,7 @@ const agentToolCapabilityRisk = defineRule({
|
|
|
973
1026
|
shouldScan: (file) => isProductionSourcePath(file.relativePath) && AGENT_TOOL_CONTEXT_PATH_PATTERN.test(file.relativePath),
|
|
974
1027
|
pattern: AGENT_TOOL_DEFINITION_PATTERN,
|
|
975
1028
|
requireAll: [AGENT_TOOL_DANGEROUS_CAPABILITY_PATTERN],
|
|
1029
|
+
ignoreStringLiterals: true,
|
|
976
1030
|
message: "An agent-callable tool appears to expose network, filesystem, shell, or code-execution capability."
|
|
977
1031
|
})
|
|
978
1032
|
});
|
|
@@ -2965,7 +3019,7 @@ const ABSTRACT_ROLES = new Set([
|
|
|
2965
3019
|
"widget",
|
|
2966
3020
|
"window"
|
|
2967
3021
|
]);
|
|
2968
|
-
const PRESENTATION_ROLES$
|
|
3022
|
+
const PRESENTATION_ROLES$1 = new Set(["presentation", "none"]);
|
|
2969
3023
|
//#endregion
|
|
2970
3024
|
//#region src/plugin/rules/a11y/aria-role.ts
|
|
2971
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}`;
|
|
@@ -3085,7 +3139,7 @@ const artifactBaasAuthoritySurface = defineRule({
|
|
|
3085
3139
|
scan: scanByPattern({
|
|
3086
3140
|
shouldScan: (file) => isBrowserArtifactPath(file.relativePath, file.isGeneratedBundle),
|
|
3087
3141
|
pattern: /\b(?:collection\s*\(\s*["'](?:boosts|sessions|sessions_admin|users|orgs|candidateJobs|conversations|documents|profiles)|from\s*\(\s*["'](?:users|profiles|documents|organizations|memberships)|creatorID|creatorId|providerId|ghostOrg|ownerId|orgId|tenantId|workspaceId|role|roles|isAdmin|SuperAdmin)\b/i,
|
|
3088
|
-
requireAll: [/\b(?:initializeApp|firebase|firestore|getFirestore
|
|
3142
|
+
requireAll: [/\b(?:initializeApp|firebase|firestore|getFirestore)\b[\s\S]{0,700}\b(?:apiKey|authDomain|projectId|databaseURL|storageBucket)\b|\b(?:apiKey|authDomain|projectId|databaseURL|storageBucket)\b[\s\S]{0,700}\b(?:firebase|firestore|getFirestore|initializeApp)\b|\bcreateClient\b[\s\S]{0,700}\b(?:supabase|SUPABASE_URL)\b|\b(?:supabase|SUPABASE_URL)\b[\s\S]{0,700}\bcreateClient\b/i],
|
|
3089
3143
|
message: "A browser artifact exposes Firebase/Supabase config together with sensitive collections or authorization fields."
|
|
3090
3144
|
})
|
|
3091
3145
|
});
|
|
@@ -4656,6 +4710,14 @@ const checkedRequiresOnchangeOrReadonly = defineRule({
|
|
|
4656
4710
|
}
|
|
4657
4711
|
});
|
|
4658
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
|
|
4659
4721
|
//#region src/plugin/utils/is-pure-event-blocker-handler.ts
|
|
4660
4722
|
const BLOCKER_METHOD_NAMES = new Set([
|
|
4661
4723
|
"stopPropagation",
|
|
@@ -4693,7 +4755,6 @@ const isPureEventBlockerHandler = (attribute) => {
|
|
|
4693
4755
|
};
|
|
4694
4756
|
//#endregion
|
|
4695
4757
|
//#region src/plugin/rules/a11y/click-events-have-key-events.ts
|
|
4696
|
-
const PRESENTATION_ROLES$1 = new Set(["presentation", "none"]);
|
|
4697
4758
|
const MESSAGE$56 = "Keyboard users can't trigger this click handler because there's no keyboard one, so add `onKeyUp`, `onKeyDown`, or `onKeyPress`.";
|
|
4698
4759
|
const KEY_HANDLERS = [
|
|
4699
4760
|
"onKeyUp",
|
|
@@ -4718,11 +4779,7 @@ const clickEventsHaveKeyEvents = defineRule({
|
|
|
4718
4779
|
if (!onClick) return;
|
|
4719
4780
|
if (isPureEventBlockerHandler(onClick)) return;
|
|
4720
4781
|
if (isHiddenFromScreenReader(node, context.settings)) return;
|
|
4721
|
-
|
|
4722
|
-
if (roleAttribute) {
|
|
4723
|
-
const roleValue = getJsxPropStringValue(roleAttribute);
|
|
4724
|
-
if (roleValue && PRESENTATION_ROLES$1.has(roleValue)) return;
|
|
4725
|
-
}
|
|
4782
|
+
if (isPresentationRole(node)) return;
|
|
4726
4783
|
if (KEY_HANDLERS.some((handler) => hasJsxPropIgnoreCase(node.attributes, handler))) return;
|
|
4727
4784
|
context.report({
|
|
4728
4785
|
node: node.name,
|
|
@@ -5693,7 +5750,7 @@ const displayName = defineRule({
|
|
|
5693
5750
|
category: "Architecture",
|
|
5694
5751
|
create: (context) => {
|
|
5695
5752
|
const settings = resolveSettings$44(context.settings);
|
|
5696
|
-
const ignoreNamed = settings.ignoreTranspilerName
|
|
5753
|
+
const ignoreNamed = !settings.ignoreTranspilerName;
|
|
5697
5754
|
const reportAt = (node) => {
|
|
5698
5755
|
context.report({
|
|
5699
5756
|
node,
|
|
@@ -8116,7 +8173,6 @@ const htmlHasLang = defineRule({
|
|
|
8116
8173
|
return { JSXOpeningElement(node) {
|
|
8117
8174
|
const tag = getElementType(node, context.settings);
|
|
8118
8175
|
if (!tagSet.has(tag)) return;
|
|
8119
|
-
const hasSpread = node.attributes.some((attribute) => isNodeOfType(attribute, "JSXSpreadAttribute"));
|
|
8120
8176
|
const lang = hasJsxPropIgnoreCase(node.attributes, "lang");
|
|
8121
8177
|
if (!lang) {
|
|
8122
8178
|
context.report({
|
|
@@ -8125,16 +8181,8 @@ const htmlHasLang = defineRule({
|
|
|
8125
8181
|
});
|
|
8126
8182
|
return;
|
|
8127
8183
|
}
|
|
8128
|
-
|
|
8129
|
-
|
|
8130
|
-
context.report({
|
|
8131
|
-
node: lang,
|
|
8132
|
-
message: MESSAGE$50
|
|
8133
|
-
});
|
|
8134
|
-
return;
|
|
8135
|
-
}
|
|
8136
|
-
if (hasSpread && !lang) context.report({
|
|
8137
|
-
node: node.name,
|
|
8184
|
+
if (evaluateLang(lang.value) === "empty") context.report({
|
|
8185
|
+
node: lang,
|
|
8138
8186
|
message: MESSAGE$50
|
|
8139
8187
|
});
|
|
8140
8188
|
} };
|
|
@@ -8848,14 +8896,6 @@ const isNonInteractiveElement = (elementType, openingElement) => {
|
|
|
8848
8896
|
//#region src/plugin/utils/is-non-interactive-role.ts
|
|
8849
8897
|
const isNonInteractiveRole = (role) => NON_INTERACTIVE_ROLES.has(role);
|
|
8850
8898
|
//#endregion
|
|
8851
|
-
//#region src/plugin/utils/is-presentation-role.ts
|
|
8852
|
-
const isPresentationRole = (openingElement) => {
|
|
8853
|
-
const roleAttribute = hasJsxPropIgnoreCase(openingElement.attributes, "role");
|
|
8854
|
-
if (!roleAttribute) return false;
|
|
8855
|
-
const value = getJsxPropStringValue(roleAttribute);
|
|
8856
|
-
return value !== null && PRESENTATION_ROLES$2.has(value);
|
|
8857
|
-
};
|
|
8858
|
-
//#endregion
|
|
8859
8899
|
//#region src/plugin/rules/a11y/interactive-supports-focus.ts
|
|
8860
8900
|
const buildTabbableMessage = (role) => `Keyboard users can't tab to this '${role}' because it isn't focusable, so add \`tabIndex={0}\`.`;
|
|
8861
8901
|
const buildFocusableMessage = (role) => `Keyboard users can't focus this '${role}' because it can't receive focus, so add \`tabIndex={0}\` or \`tabIndex={-1}\`.`;
|
|
@@ -10630,6 +10670,24 @@ const hasJsxKeyAttribute = (openingElement) => {
|
|
|
10630
10670
|
return false;
|
|
10631
10671
|
};
|
|
10632
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
|
|
10633
10691
|
//#region src/plugin/rules/react-builtins/jsx-key.ts
|
|
10634
10692
|
const ITERATOR_METHOD_NAMES = new Set([
|
|
10635
10693
|
"map",
|
|
@@ -10668,6 +10726,7 @@ const findEnclosingIteratorContext = (jsxNode) => {
|
|
|
10668
10726
|
const arrayParent = parent.parent;
|
|
10669
10727
|
if (arrayParent && isNodeOfType(arrayParent, "Property")) return null;
|
|
10670
10728
|
if (arrayParent && isNodeOfType(arrayParent, "ArrayExpression")) return null;
|
|
10729
|
+
if (isNonChildrenJsxAttributeValue(parent)) return null;
|
|
10671
10730
|
return { kind: "array" };
|
|
10672
10731
|
} else if (isNodeOfType(parent, "CallExpression")) {
|
|
10673
10732
|
const callee = parent.callee;
|
|
@@ -10680,10 +10739,13 @@ const findEnclosingIteratorContext = (jsxNode) => {
|
|
|
10680
10739
|
if (!targetArg) return null;
|
|
10681
10740
|
let walker = current;
|
|
10682
10741
|
while (walker && walker !== parent) {
|
|
10683
|
-
if (walker === targetArg)
|
|
10684
|
-
|
|
10685
|
-
|
|
10686
|
-
|
|
10742
|
+
if (walker === targetArg) {
|
|
10743
|
+
if (isNonChildrenJsxAttributeValue(parent)) return null;
|
|
10744
|
+
return {
|
|
10745
|
+
kind: "iterator",
|
|
10746
|
+
callExpression: parent
|
|
10747
|
+
};
|
|
10748
|
+
}
|
|
10687
10749
|
walker = walker.parent ?? null;
|
|
10688
10750
|
}
|
|
10689
10751
|
return null;
|
|
@@ -13466,6 +13528,7 @@ const mcpToolCapabilityRisk = defineRule({
|
|
|
13466
13528
|
shouldScan: (file) => isProductionSourcePath(file.relativePath),
|
|
13467
13529
|
pattern: /\bserver\.\s*tool\s*\(|\bregisterTool\s*\(|\bsetRequestHandler\s*\(\s*CallToolRequestSchema/,
|
|
13468
13530
|
requireAll: [/\bfrom\s+["']@modelcontextprotocol\/sdk[^"']*["']|\bMcpServer\b|\bMcpAgent\b/, AGENT_TOOL_DANGEROUS_CAPABILITY_PATTERN],
|
|
13531
|
+
ignoreStringLiterals: true,
|
|
13469
13532
|
message: "An MCP tool/resource/prompt handler appears to expose file, shell, network, or code-execution capability."
|
|
13470
13533
|
})
|
|
13471
13534
|
});
|
|
@@ -17375,6 +17438,7 @@ const noDanger = defineRule({
|
|
|
17375
17438
|
title: "Raw HTML injection can run unsafe markup",
|
|
17376
17439
|
severity: "warn",
|
|
17377
17440
|
category: "Security",
|
|
17441
|
+
defaultEnabled: false,
|
|
17378
17442
|
recommendation: "Render trusted content as React children so attacker-controlled HTML cannot run in users' browsers.",
|
|
17379
17443
|
create: (context) => ({
|
|
17380
17444
|
JSXOpeningElement(node) {
|
|
@@ -20135,15 +20199,20 @@ const noInlineExhaustiveStyle = defineRule({
|
|
|
20135
20199
|
severity: "warn",
|
|
20136
20200
|
tags: ["test-noise", "react-jsx-only"],
|
|
20137
20201
|
recommendation: "Move the styles to a CSS class, CSS module, Tailwind utilities, or a styled component. Big inline objects are hard to read and rebuild on every update.",
|
|
20138
|
-
create: (context) =>
|
|
20139
|
-
|
|
20140
|
-
|
|
20141
|
-
|
|
20142
|
-
|
|
20143
|
-
|
|
20144
|
-
|
|
20145
|
-
|
|
20146
|
-
|
|
20202
|
+
create: (context) => {
|
|
20203
|
+
if (isGeneratedImageRenderContext(context)) return {};
|
|
20204
|
+
return { JSXAttribute(node) {
|
|
20205
|
+
const expression = getInlineStyleExpression(node);
|
|
20206
|
+
if (!expression) return;
|
|
20207
|
+
const propertyCount = expression.properties?.filter((property) => isNodeOfType(property, "Property")).length ?? 0;
|
|
20208
|
+
if (propertyCount < 8) return;
|
|
20209
|
+
if (isGeneratedImageRenderContext(context, node.parent ?? void 0)) return;
|
|
20210
|
+
context.report({
|
|
20211
|
+
node: expression,
|
|
20212
|
+
message: `This inline style has ${propertyCount} properties, which is hard to read & rebuilds every render. Move it to a CSS class, CSS module, or styled component.`
|
|
20213
|
+
});
|
|
20214
|
+
} };
|
|
20215
|
+
}
|
|
20147
20216
|
});
|
|
20148
20217
|
//#endregion
|
|
20149
20218
|
//#region src/plugin/rules/performance/no-inline-prop-on-memo-component.ts
|
|
@@ -25359,15 +25428,8 @@ const expressionContainsJsxOrCreateElement = (root) => {
|
|
|
25359
25428
|
visit(root);
|
|
25360
25429
|
return found;
|
|
25361
25430
|
};
|
|
25362
|
-
const classExtendsReactComponent$1 = (classNode) => {
|
|
25363
|
-
const superClass = classNode.superClass;
|
|
25364
|
-
if (!superClass) return false;
|
|
25365
|
-
if (isNodeOfType(superClass, "Identifier") && (superClass.name === "Component" || superClass.name === "PureComponent")) return true;
|
|
25366
|
-
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;
|
|
25367
|
-
return false;
|
|
25368
|
-
};
|
|
25369
25431
|
const isReactClassComponent = (classNode) => {
|
|
25370
|
-
if (
|
|
25432
|
+
if (isEs6Component(classNode)) return true;
|
|
25371
25433
|
return expressionContainsJsxOrCreateElement(classNode);
|
|
25372
25434
|
};
|
|
25373
25435
|
const findEnclosingComponent = (node) => {
|
|
@@ -25527,7 +25589,7 @@ const noUnstableNestedComponents = defineRule({
|
|
|
25527
25589
|
create: (context) => {
|
|
25528
25590
|
const settings = resolveSettings$8(context.settings);
|
|
25529
25591
|
const renderPropRegex = compileGlob(settings.propNamePattern);
|
|
25530
|
-
const reportCandidate = (candidateNode, reportNode
|
|
25592
|
+
const reportCandidate = (candidateNode, reportNode) => {
|
|
25531
25593
|
if (isFirstArgumentOfHocCall(candidateNode)) return;
|
|
25532
25594
|
if (isReturnOfMapCallback(candidateNode)) return;
|
|
25533
25595
|
const propInfo = isComponentDeclaredInProp(candidateNode);
|
|
@@ -25548,7 +25610,7 @@ const noUnstableNestedComponents = defineRule({
|
|
|
25548
25610
|
const inferredName = inferFunctionLikeName(node);
|
|
25549
25611
|
const propInfo = isComponentDeclaredInProp(node);
|
|
25550
25612
|
if (!(inferredName !== null && isReactComponentName(inferredName) || propInfo !== null || isObjectCallbackCandidate(node))) return;
|
|
25551
|
-
reportCandidate(node, node
|
|
25613
|
+
reportCandidate(node, node);
|
|
25552
25614
|
};
|
|
25553
25615
|
return {
|
|
25554
25616
|
FunctionDeclaration: checkFunctionLike,
|
|
@@ -25558,18 +25620,18 @@ const noUnstableNestedComponents = defineRule({
|
|
|
25558
25620
|
if (!node.id) return;
|
|
25559
25621
|
if (!isReactComponentName(node.id.name)) return;
|
|
25560
25622
|
if (!isReactClassComponent(node)) return;
|
|
25561
|
-
reportCandidate(node, node
|
|
25623
|
+
reportCandidate(node, node);
|
|
25562
25624
|
},
|
|
25563
25625
|
ClassExpression(node) {
|
|
25564
25626
|
const inferredName = node.id?.name ?? inferFunctionLikeName(node);
|
|
25565
25627
|
if (!inferredName || !isReactComponentName(inferredName)) return;
|
|
25566
25628
|
if (!isReactClassComponent(node)) return;
|
|
25567
|
-
reportCandidate(node, node
|
|
25629
|
+
reportCandidate(node, node);
|
|
25568
25630
|
},
|
|
25569
25631
|
CallExpression(node) {
|
|
25570
25632
|
if (!isHocCallee$1(node)) return;
|
|
25571
25633
|
if (!hocCallContainsComponent(node)) return;
|
|
25572
|
-
reportCandidate(node, node
|
|
25634
|
+
reportCandidate(node, node);
|
|
25573
25635
|
}
|
|
25574
25636
|
};
|
|
25575
25637
|
}
|
|
@@ -26009,13 +26071,6 @@ const skipTsExpression = (expression) => {
|
|
|
26009
26071
|
if (expression.type === "TSAsExpression" || expression.type === "TSSatisfiesExpression" || expression.type === "TSNonNullExpression") return skipTsExpression(expression.expression);
|
|
26010
26072
|
return expression;
|
|
26011
26073
|
};
|
|
26012
|
-
const classExtendsReactComponent = (classNode) => {
|
|
26013
|
-
const superClass = classNode.superClass;
|
|
26014
|
-
if (!superClass) return false;
|
|
26015
|
-
if (isNodeOfType(superClass, "Identifier") && (superClass.name === "Component" || superClass.name === "PureComponent")) return true;
|
|
26016
|
-
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;
|
|
26017
|
-
return false;
|
|
26018
|
-
};
|
|
26019
26074
|
const isReactCreateContext = (initializer) => {
|
|
26020
26075
|
if (!initializer) return false;
|
|
26021
26076
|
const expression = skipTsExpression(initializer);
|
|
@@ -26206,7 +26261,7 @@ const onlyExportComponents = defineRule({
|
|
|
26206
26261
|
if (stripped.id) {
|
|
26207
26262
|
const idNode = stripped.id;
|
|
26208
26263
|
isExportedNodeIds.add(stripped);
|
|
26209
|
-
if (isReactComponentName(idNode.name) &&
|
|
26264
|
+
if (isReactComponentName(idNode.name) && isEs6Component(stripped)) hasReactExport = true;
|
|
26210
26265
|
else exports.push({
|
|
26211
26266
|
kind: "non-component",
|
|
26212
26267
|
reportNode: idNode
|
|
@@ -26266,7 +26321,7 @@ const onlyExportComponents = defineRule({
|
|
|
26266
26321
|
exports.push(classifyExport(declaration.id.name, declaration.id, true, null, state));
|
|
26267
26322
|
} else if (isNodeOfType(declaration, "ClassDeclaration") && declaration.id) {
|
|
26268
26323
|
isExportedNodeIds.add(declaration);
|
|
26269
|
-
if (isReactComponentName(declaration.id.name) &&
|
|
26324
|
+
if (isReactComponentName(declaration.id.name) && isEs6Component(declaration)) exports.push({ kind: "react-component" });
|
|
26270
26325
|
else exports.push({
|
|
26271
26326
|
kind: "non-component",
|
|
26272
26327
|
reportNode: declaration.id
|
|
@@ -35525,13 +35580,7 @@ const serverNoMutableModuleState = defineRule({
|
|
|
35525
35580
|
const collectDeclaredNames = (declaration) => {
|
|
35526
35581
|
const names = /* @__PURE__ */ new Set();
|
|
35527
35582
|
if (!isNodeOfType(declaration, "VariableDeclaration")) return names;
|
|
35528
|
-
for (const declarator of declaration.declarations ?? [])
|
|
35529
|
-
else if (isNodeOfType(declarator.id, "ObjectPattern")) {
|
|
35530
|
-
for (const property of declarator.id.properties ?? []) if (isNodeOfType(property, "Property") && isNodeOfType(property.value, "Identifier")) names.add(property.value.name);
|
|
35531
|
-
else if (isNodeOfType(property, "RestElement") && isNodeOfType(property.argument, "Identifier")) names.add(property.argument.name);
|
|
35532
|
-
} else if (isNodeOfType(declarator.id, "ArrayPattern")) {
|
|
35533
|
-
for (const element of declarator.id.elements ?? []) if (isNodeOfType(element, "Identifier")) names.add(element.name);
|
|
35534
|
-
}
|
|
35583
|
+
for (const declarator of declaration.declarations ?? []) collectPatternNames(declarator.id, names);
|
|
35535
35584
|
return names;
|
|
35536
35585
|
};
|
|
35537
35586
|
const declarationStartsWithAwait = (declaration) => {
|
|
@@ -35541,11 +35590,15 @@ const declarationStartsWithAwait = (declaration) => {
|
|
|
35541
35590
|
};
|
|
35542
35591
|
const declarationReadsAnyName = (declaration, names) => {
|
|
35543
35592
|
if (names.size === 0) return false;
|
|
35593
|
+
if (!isNodeOfType(declaration, "VariableDeclaration")) return false;
|
|
35544
35594
|
let didRead = false;
|
|
35545
|
-
|
|
35546
|
-
if (
|
|
35547
|
-
|
|
35548
|
-
|
|
35595
|
+
for (const declarator of declaration.declarations ?? []) {
|
|
35596
|
+
if (!declarator.init) continue;
|
|
35597
|
+
walkAst(declarator.init, (child) => {
|
|
35598
|
+
if (didRead) return;
|
|
35599
|
+
if (isNodeOfType(child, "Identifier") && names.has(child.name)) didRead = true;
|
|
35600
|
+
});
|
|
35601
|
+
}
|
|
35549
35602
|
return didRead;
|
|
35550
35603
|
};
|
|
35551
35604
|
const serverSequentialIndependentAwait = defineRule({
|
|
@@ -36805,7 +36858,7 @@ const urlPrefilledPrivilegedAction = defineRule({
|
|
|
36805
36858
|
recommendation: "Require server-side validation and explicit confirmation for URL-sourced invite, role, permission, redirect, or sharing parameters.",
|
|
36806
36859
|
scan: scanByPattern({
|
|
36807
36860
|
shouldScan: (file) => isClientSourcePath(file.relativePath),
|
|
36808
|
-
pattern: /(?<!(?:safe|valid|sanitiz|relativ|allowlist|whitelist)[\w$]*\(\s*(?:new\s+)?)\b(?:searchParams|useSearchParams\s*\(\s*\)|URLSearchParams\s*\([^)]{0,120}\))(?:[?!])?\.get(?:All)?\s*\(\s*["'](?:userstoinvite|role|permission|sharingaction|invite|admin|next|continue|returnTo|redirect_uri|callbackUrl)["']|\bsearchParams\.(?:userstoinvite|role|permission|sharingaction|invite|admin|returnTo|redirect_uri|callbackUrl)\b/i,
|
|
36861
|
+
pattern: /(?<!(?:safe|valid|sanitiz|relativ|allowlist|whitelist)[\w$]*\(\s*(?:new\s+)?(?:[\w$]+\s*\.\s*){0,4})\b(?:searchParams|useSearchParams\s*\(\s*\)|URLSearchParams\s*\([^)]{0,120}\))(?:[?!])?\.get(?:All)?\s*\(\s*["'](?:userstoinvite|role|permission|sharingaction|invite|admin|next|continue|returnTo|redirect_uri|callbackUrl)["']|\bsearchParams\.(?:userstoinvite|role|permission|sharingaction|invite|admin|returnTo|redirect_uri|callbackUrl)\b/i,
|
|
36809
36862
|
message: "Client code reads sensitive action state from the URL, which can pre-fill invites, roles, redirects, or sharing flows with attacker values."
|
|
36810
36863
|
})
|
|
36811
36864
|
});
|
|
@@ -42224,32 +42277,6 @@ const computeUnconditionalSet = (cfg) => {
|
|
|
42224
42277
|
}
|
|
42225
42278
|
return unconditional;
|
|
42226
42279
|
};
|
|
42227
|
-
const computeDominatesExit = (cfg) => {
|
|
42228
|
-
const reachableToExit = /* @__PURE__ */ new Set();
|
|
42229
|
-
const queue = [cfg.exit];
|
|
42230
|
-
while (queue.length > 0) {
|
|
42231
|
-
const block = queue.shift();
|
|
42232
|
-
if (reachableToExit.has(block)) continue;
|
|
42233
|
-
reachableToExit.add(block);
|
|
42234
|
-
for (const edge of block.predecessors) queue.push(edge.from);
|
|
42235
|
-
}
|
|
42236
|
-
const dominatesExit = /* @__PURE__ */ new Set();
|
|
42237
|
-
const visit = (block) => {
|
|
42238
|
-
if (block === cfg.exit) return true;
|
|
42239
|
-
if (dominatesExit.has(block)) return true;
|
|
42240
|
-
if (block.successors.length === 0) return false;
|
|
42241
|
-
dominatesExit.add(block);
|
|
42242
|
-
let allReach = true;
|
|
42243
|
-
for (const edge of block.successors) if (!visit(edge.to)) {
|
|
42244
|
-
allReach = false;
|
|
42245
|
-
break;
|
|
42246
|
-
}
|
|
42247
|
-
if (!allReach) dominatesExit.delete(block);
|
|
42248
|
-
return allReach;
|
|
42249
|
-
};
|
|
42250
|
-
for (const block of cfg.blocks) visit(block);
|
|
42251
|
-
return dominatesExit;
|
|
42252
|
-
};
|
|
42253
42280
|
const analyzeControlFlow = (program) => {
|
|
42254
42281
|
nextBlockId = 0;
|
|
42255
42282
|
const functionCfgs = /* @__PURE__ */ new Map();
|
|
@@ -42257,8 +42284,7 @@ const analyzeControlFlow = (program) => {
|
|
|
42257
42284
|
const cfg = buildFunctionCfg(functionNode, body);
|
|
42258
42285
|
functionCfgs.set(functionNode, {
|
|
42259
42286
|
cfg,
|
|
42260
|
-
unconditionalSet: computeUnconditionalSet(cfg)
|
|
42261
|
-
dominatesExitSet: computeDominatesExit(cfg)
|
|
42287
|
+
unconditionalSet: computeUnconditionalSet(cfg)
|
|
42262
42288
|
});
|
|
42263
42289
|
};
|
|
42264
42290
|
if (isNodeOfType(program, "Program")) buildFor(program, {
|
|
@@ -42301,20 +42327,10 @@ const analyzeControlFlow = (program) => {
|
|
|
42301
42327
|
if (!block) return true;
|
|
42302
42328
|
return entry.unconditionalSet.has(block);
|
|
42303
42329
|
};
|
|
42304
|
-
const dominatesExit = (node) => {
|
|
42305
|
-
const owner = enclosingFunction(node);
|
|
42306
|
-
if (!owner) return true;
|
|
42307
|
-
const entry = functionCfgs.get(owner);
|
|
42308
|
-
if (!entry) return true;
|
|
42309
|
-
const block = entry.cfg.blockOf(node);
|
|
42310
|
-
if (!block) return true;
|
|
42311
|
-
return entry.dominatesExitSet.has(block);
|
|
42312
|
-
};
|
|
42313
42330
|
return {
|
|
42314
42331
|
cfgFor,
|
|
42315
42332
|
enclosingFunction,
|
|
42316
|
-
isUnconditionalFromEntry
|
|
42317
|
-
dominatesExit
|
|
42333
|
+
isUnconditionalFromEntry
|
|
42318
42334
|
};
|
|
42319
42335
|
};
|
|
42320
42336
|
//#endregion
|
|
@@ -42339,8 +42355,7 @@ const buildFallbackScopes = () => ({
|
|
|
42339
42355
|
const FALLBACK_CFG = {
|
|
42340
42356
|
cfgFor: () => null,
|
|
42341
42357
|
enclosingFunction: () => null,
|
|
42342
|
-
isUnconditionalFromEntry: () => false
|
|
42343
|
-
dominatesExit: () => false
|
|
42358
|
+
isUnconditionalFromEntry: () => false
|
|
42344
42359
|
};
|
|
42345
42360
|
const wrapWithSemanticContext = (rule) => ({
|
|
42346
42361
|
...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.50999f4",
|
|
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",
|