Skip to content

Commit 118ae70

Browse files
committed
fix(solid): add receiver check for console methods, extract shared extractStaticStringValue
- bodyContainsSideEffects in solid-no-effect-derived-state now gates console method detection on `node.callee.object.name === "console"`, matching the pattern in solid-no-impure-memo. Previously `validation.error()` or `myApi.fetch()` would false-positive as side effects. - Extract duplicated extractStaticStringValue from solid-no-innerhtml and solid-jsx-no-script-url into utils/extract-static-string-value. Co-authored-by: Aiden Bai <aidenybai@users.noreply.github.com>
1 parent 6fd61fc commit 118ae70

4 files changed

Lines changed: 32 additions & 38 deletions

File tree

packages/oxlint-plugin-react-doctor/src/plugin/rules/solid/solid-jsx-no-script-url.ts

Lines changed: 4 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,14 @@
11
import { defineRule } from "../../utils/define-rule.js";
22
import type { EsTreeNode } from "../../utils/es-tree-node.js";
33
import type { EsTreeNodeOfType } from "../../utils/es-tree-node-of-type.js";
4+
import { extractStaticStringValue } from "../../utils/extract-static-string-value.js";
45
import { isNodeOfType } from "../../utils/is-node-of-type.js";
56
import type { Rule } from "../../utils/rule.js";
67
import type { RuleContext } from "../../utils/rule-context.js";
78

8-
// Mirrors the WHATWG URL parser's pre-scheme step: leading C0
9-
// controls and U+0020 SPACE are stripped, then ASCII tab / LF / CR
10-
// characters inside the URL are also filtered out before the
11-
// scheme is matched. https://url.spec.whatwg.org/#url-parsing
12-
//
13-
// Doing the filter in code (and keeping the regex literal free of
14-
// control characters) avoids `eslint(no-control-regex)` warnings —
15-
// inline `[\u0000-\u001F]` and `[\r\n\t]*` between letters would
16-
// trip the lint at every rule-file load.
9+
// HACK: Mirrors the WHATWG URL parser's pre-scheme step — strip C0
10+
// controls first, then match scheme — because embedding the C0 range
11+
// directly in a regex literal trips `no-control-regex`.
1712
const JAVASCRIPT_SCHEME_PATTERN = /^ *javascript:/i;
1813

1914
const isUrlControlCharacterCode = (characterCode: number): boolean =>
@@ -30,15 +25,6 @@ const stripUrlControlCharacters = (urlValue: string): string => {
3025
const startsWithJavascriptScheme = (urlValue: string): boolean =>
3126
JAVASCRIPT_SCHEME_PATTERN.test(stripUrlControlCharacters(urlValue));
3227

33-
const extractStaticStringValue = (node: EsTreeNode | null | undefined): string | null => {
34-
if (!node) return null;
35-
if (isNodeOfType(node, "Literal") && typeof node.value === "string") return node.value;
36-
if (isNodeOfType(node, "TemplateLiteral") && node.expressions.length === 0) {
37-
return node.quasis.map((quasi) => quasi.value.cooked ?? "").join("");
38-
}
39-
return null;
40-
};
41-
4228
// Port of `solid/jsx-no-script-url` — flags `<a href="javascript:...">`
4329
// and similar `javascript:` URLs in JSX attributes. Adapted from
4430
// `eslint-plugin-react`'s rule of the same name.

packages/oxlint-plugin-react-doctor/src/plugin/rules/solid/solid-no-effect-derived-state.ts

Lines changed: 16 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,9 @@ const bodyContainsOnlySetters = (callback: EsTreeNode): boolean => {
2626
return isSetterCall(callback.body as EsTreeNode);
2727
};
2828

29+
const SIDE_EFFECT_GLOBAL_CALLS = new Set(["fetch", "alert", "confirm", "prompt"]);
30+
const CONSOLE_METHODS = new Set(["log", "warn", "error", "info", "debug"]);
31+
2932
const bodyContainsSideEffects = (callback: EsTreeNode): boolean => {
3033
if (!isFunctionLike(callback)) return false;
3134
let foundSideEffect = false;
@@ -36,21 +39,23 @@ const bodyContainsSideEffects = (callback: EsTreeNode): boolean => {
3639
if (isSetterCall(node)) return;
3740
if (isNodeOfType(node.callee, "MemberExpression")) {
3841
const property = node.callee.property;
39-
if (isNodeOfType(property, "Identifier")) {
40-
const methodName = property.name;
41-
if (["log", "warn", "error", "info", "debug", "fetch"].includes(methodName)) {
42-
foundSideEffect = true;
43-
return false;
44-
}
45-
}
46-
}
47-
if (isNodeOfType(node.callee, "Identifier")) {
48-
const calleeName = node.callee.name;
49-
if (["fetch", "alert", "confirm", "prompt"].includes(calleeName)) {
42+
if (
43+
isNodeOfType(property, "Identifier") &&
44+
CONSOLE_METHODS.has(property.name) &&
45+
isNodeOfType(node.callee.object, "Identifier") &&
46+
node.callee.object.name === "console"
47+
) {
5048
foundSideEffect = true;
5149
return false;
5250
}
5351
}
52+
if (
53+
isNodeOfType(node.callee, "Identifier") &&
54+
SIDE_EFFECT_GLOBAL_CALLS.has(node.callee.name)
55+
) {
56+
foundSideEffect = true;
57+
return false;
58+
}
5459
}
5560
if (isNodeOfType(node, "AwaitExpression")) {
5661
foundSideEffect = true;

packages/oxlint-plugin-react-doctor/src/plugin/rules/solid/solid-no-innerhtml.ts

Lines changed: 1 addition & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { defineRule } from "../../utils/define-rule.js";
22
import type { EsTreeNode } from "../../utils/es-tree-node.js";
33
import type { EsTreeNodeOfType } from "../../utils/es-tree-node-of-type.js";
4+
import { extractStaticStringValue } from "../../utils/extract-static-string-value.js";
45
import { getJsxAttributeName } from "../../utils/get-jsx-attribute-name.js";
56
import { isNodeOfType } from "../../utils/is-node-of-type.js";
67
import type { Rule } from "../../utils/rule.js";
@@ -19,15 +20,6 @@ const extractInnerExpression = (attribute: EsTreeNodeOfType<"JSXAttribute">): Es
1920
return attribute.value as EsTreeNode;
2021
};
2122

22-
const extractStaticStringValue = (node: EsTreeNode | null): string | null => {
23-
if (!node) return null;
24-
if (isNodeOfType(node, "Literal") && typeof node.value === "string") return node.value;
25-
if (isNodeOfType(node, "TemplateLiteral") && node.expressions.length === 0) {
26-
return node.quasis.map((quasi) => quasi.value.cooked ?? "").join("");
27-
}
28-
return null;
29-
};
30-
3123
// Port of `solid/no-innerhtml`. Three distinct diagnostics:
3224
//
3325
// 1. `dangerouslySetInnerHTML={...}` — always flagged. Solid does
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
import type { EsTreeNode } from "./es-tree-node.js";
2+
import { isNodeOfType } from "./is-node-of-type.js";
3+
4+
export const extractStaticStringValue = (node: EsTreeNode | null | undefined): string | null => {
5+
if (!node) return null;
6+
if (isNodeOfType(node, "Literal") && typeof node.value === "string") return node.value;
7+
if (isNodeOfType(node, "TemplateLiteral") && node.expressions.length === 0) {
8+
return node.quasis.map((quasi) => quasi.value.cooked ?? "").join("");
9+
}
10+
return null;
11+
};

0 commit comments

Comments
 (0)