Skip to content

Commit c0997f4

Browse files
committed
Add solid/no-proxy-apis rule for envs that don't support ES6 Proxy.
1 parent 8372d35 commit c0997f4

File tree

4 files changed

+186
-23
lines changed

4 files changed

+186
-23
lines changed

README.md

Lines changed: 20 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -119,27 +119,26 @@ const [editedValue, setEditedValue] = createSignal(props.value);
119119
🔧: Fixable with [`eslint --fix`](https://eslint.org/docs/user-guide/command-line-interface#fixing-problems)/IDE auto-fix.
120120

121121
<!-- AUTO-GENERATED-CONTENT:START (RULES) -->
122-
123-
|| 🔧 | Rule | Description |
124-
| :-: | :-: | :--------------------------------------------------------------- | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
125-
|| 🔧 | [solid/components-return-once](docs/components-return-once.md) | Disallow early returns in components. Solid components only run once, and so conditionals should be inside JSX. |
126-
|| 🔧 | [solid/event-handlers](docs/event-handlers.md) | Enforce naming DOM element event handlers consistently and prevent Solid's analysis from misunderstanding whether a prop should be an event handler. |
127-
|| 🔧 | [solid/imports](docs/imports.md) | Enforce consistent imports from "solid-js", "solid-js/web", and "solid-js/store". |
128-
|| | [solid/jsx-no-duplicate-props](docs/jsx-no-duplicate-props.md) | Disallow passing the same prop twice in JSX. |
129-
|| | [solid/jsx-no-script-url](docs/jsx-no-script-url.md) | Disallow javascript: URLs. |
130-
|| 🔧 | [solid/jsx-no-undef](docs/jsx-no-undef.md) | Disallow references to undefined variables in JSX. Handles custom directives. |
131-
|| | [solid/jsx-uses-vars](docs/jsx-uses-vars.md) | Prevent variables used in JSX from being marked as unused. |
132-
|| 🔧 | [solid/no-destructure](docs/no-destructure.md) | Disallow destructuring props. In Solid, props must be used with property accesses (`props.foo`) to preserve reactivity. This rule only tracks destructuring in the parameter list. |
133-
|| 🔧 | [solid/no-innerhtml](docs/no-innerhtml.md) | Disallow usage of the innerHTML attribute, which can often lead to security vulnerabilities. |
134-
|| 🔧 | [solid/no-react-specific-props](docs/no-react-specific-props.md) | Disallow usage of React-specific `className`/`htmlFor` props, which were deprecated in v1.4.0. |
135-
|| | [solid/no-unknown-namespaces](docs/no-unknown-namespaces.md) | Enforce using only Solid-specific namespaced attribute names (i.e. `'on:'` in `<div on:click={...} />`). |
136-
|| 🔧 | [solid/prefer-classlist](docs/prefer-classlist.md) | Enforce using the classlist prop over importing a classnames helper. The classlist prop accepts an object `{ [class: string]: boolean }` just like classnames. |
137-
|| 🔧 | [solid/prefer-for](docs/prefer-for.md) | Enforce using Solid's `<For />` component for mapping an array to JSX elements. |
138-
|| 🔧 | [solid/prefer-show](docs/prefer-show.md) | Enforce using Solid's `<Show />` component for conditionally showing content. Solid's compiler covers this case, so it's a stylistic rule only. |
139-
|| | [solid/reactivity](docs/reactivity.md) | Enforce that reactive expressions (props, signals, memos, etc.) are only used in tracked scopes; otherwise, they won't update the view as expected. |
140-
|| 🔧 | [solid/self-closing-comp](docs/self-closing-comp.md) | Disallow extra closing tags for components without children. |
141-
|| 🔧 | [solid/style-prop](docs/style-prop.md) | Require CSS properties in the `style` prop to be valid and kebab-cased (ex. 'font-size'), not camel-cased (ex. 'fontSize') like in React, and that property values with dimensions are strings, not numbers with implicit 'px' units. |
142-
122+
|| 🔧 | Rule | Description |
123+
| :---: | :---: | :--- | :--- |
124+
|| 🔧 | [solid/components-return-once](docs/components-return-once.md) | Disallow early returns in components. Solid components only run once, and so conditionals should be inside JSX. |
125+
|| 🔧 | [solid/event-handlers](docs/event-handlers.md) | Enforce naming DOM element event handlers consistently and prevent Solid's analysis from misunderstanding whether a prop should be an event handler. |
126+
|| 🔧 | [solid/imports](docs/imports.md) | Enforce consistent imports from "solid-js", "solid-js/web", and "solid-js/store". |
127+
|| | [solid/jsx-no-duplicate-props](docs/jsx-no-duplicate-props.md) | Disallow passing the same prop twice in JSX. |
128+
|| | [solid/jsx-no-script-url](docs/jsx-no-script-url.md) | Disallow javascript: URLs. |
129+
|| 🔧 | [solid/jsx-no-undef](docs/jsx-no-undef.md) | Disallow references to undefined variables in JSX. Handles custom directives. |
130+
|| | [solid/jsx-uses-vars](docs/jsx-uses-vars.md) | Prevent variables used in JSX from being marked as unused. |
131+
|| 🔧 | [solid/no-destructure](docs/no-destructure.md) | Disallow destructuring props. In Solid, props must be used with property accesses (`props.foo`) to preserve reactivity. This rule only tracks destructuring in the parameter list. |
132+
|| 🔧 | [solid/no-innerhtml](docs/no-innerhtml.md) | Disallow usage of the innerHTML attribute, which can often lead to security vulnerabilities. |
133+
|| | [solid/no-proxy-apis](docs/no-proxy-apis.md) | Disallow usage of APIs that use ES6 Proxies, to target environments that don't support them. |
134+
|| 🔧 | [solid/no-react-specific-props](docs/no-react-specific-props.md) | Disallow usage of React-specific `className`/`htmlFor` props, which were deprecated in v1.4.0. |
135+
|| | [solid/no-unknown-namespaces](docs/no-unknown-namespaces.md) | Enforce using only Solid-specific namespaced attribute names (i.e. `'on:'` in `<div on:click={...} />`). |
136+
|| 🔧 | [solid/prefer-classlist](docs/prefer-classlist.md) | Enforce using the classlist prop over importing a classnames helper. The classlist prop accepts an object `{ [class: string]: boolean }` just like classnames. |
137+
|| 🔧 | [solid/prefer-for](docs/prefer-for.md) | Enforce using Solid's `<For />` component for mapping an array to JSX elements. |
138+
|| 🔧 | [solid/prefer-show](docs/prefer-show.md) | Enforce using Solid's `<Show />` component for conditionally showing content. Solid's compiler covers this case, so it's a stylistic rule only. |
139+
|| | [solid/reactivity](docs/reactivity.md) | Enforce that reactive expressions (props, signals, memos, etc.) are only used in tracked scopes; otherwise, they won't update the view as expected. |
140+
|| 🔧 | [solid/self-closing-comp](docs/self-closing-comp.md) | Disallow extra closing tags for components without children. |
141+
|| 🔧 | [solid/style-prop](docs/style-prop.md) | Require CSS properties in the `style` prop to be valid and kebab-cased (ex. 'font-size'), not camel-cased (ex. 'fontSize') like in React, and that property values with dimensions are strings, not numbers with implicit 'px' units. |
143142
<!-- AUTO-GENERATED-CONTENT:END -->
144143

145144
## Versioning
@@ -149,10 +148,8 @@ stable across patch (`0.0.x`) versions, but may change across minor (`0.x`) vers
149148
If you want to pin a minor version, use a tilde in your `package.json`.
150149

151150
<!-- AUTO-GENERATED-CONTENT:START (TILDE) -->
152-
153151
```diff
154152
- "eslint-plugin-solid": "^0.7.4"
155153
+ "eslint-plugin-solid": "~0.7.4"
156154
```
157-
158155
<!-- AUTO-GENERATED-CONTENT:END -->

src/index.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import jsxNoUndef from "./rules/jsx-no-undef";
77
import jsxUsesVars from "./rules/jsx-uses-vars";
88
import noDestructure from "./rules/no-destructure";
99
import noInnerHTML from "./rules/no-innerhtml";
10+
import noProxyApis from "./rules/no-proxy-apis";
1011
import noReactSpecificProps from "./rules/no-react-specific-props";
1112
import noUnknownNamespaces from "./rules/no-unknown-namespaces";
1213
import preferClasslist from "./rules/prefer-classlist";
@@ -27,6 +28,7 @@ const allRules = {
2728
"jsx-uses-vars": jsxUsesVars,
2829
"no-destructure": noDestructure,
2930
"no-innerhtml": noInnerHTML,
31+
"no-proxy-apis": noProxyApis,
3032
"no-react-specific-props": noReactSpecificProps,
3133
"no-unknown-namespaces": noUnknownNamespaces,
3234
"prefer-classlist": preferClasslist,
@@ -79,6 +81,8 @@ const plugin = {
7981
"solid/self-closing-comp": 1,
8082
// handled by Solid compiler, opt-in style suggestion
8183
"solid/prefer-show": 0,
84+
// only necessary for resource-constrained environments
85+
"solid/no-proxy-apis": 0,
8286
},
8387
},
8488
typescript: {
@@ -111,6 +115,8 @@ const plugin = {
111115
"solid/no-unknown-namespaces": 0,
112116
// handled by Solid compiler, opt-in style suggestion
113117
"solid/prefer-show": 0,
118+
// only necessary for resource-constrained environments
119+
"solid/no-proxy-apis": 0,
114120
},
115121
},
116122
},

src/rules/no-proxy-apis.ts

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
import { TSESTree as T, TSESLint } from "@typescript-eslint/utils";
2+
import { isFunctionNode, trackImports, isPropsByName, trace } from "../utils";
3+
4+
const rule: TSESLint.RuleModule<
5+
"noStore" | "spreadCall" | "spreadMember" | "proxyLiteral" | "mergeProps",
6+
[]
7+
> = {
8+
meta: {
9+
type: "problem",
10+
docs: {
11+
recommended: false,
12+
description:
13+
"Disallow usage of APIs that use ES6 Proxies, to target environments that don't support them.",
14+
url: "https://github.com/solidjs-community/eslint-plugin-solid/blob/main/docs/no-proxy-apis.md",
15+
},
16+
schema: [],
17+
messages: {
18+
noStore: "Solid Store APIs use Proxies, which are incompatible with your target environment.",
19+
spreadCall:
20+
"Using a function call in JSX spread makes Solid use Proxies, which are incompatible with your target environment.",
21+
spreadMember:
22+
"Using a property access in JSX spread makes Solid use Proxies, which are incompatible with your target environment.",
23+
proxyLiteral: "Proxies are incompatible with your target environment.",
24+
mergeProps:
25+
"If you pass a function to `mergeProps`, it will create a Proxy, which are incompatible with your target environment.",
26+
},
27+
},
28+
create(context) {
29+
const { matchImport, handleImportDeclaration } = trackImports();
30+
31+
return {
32+
ImportDeclaration(node) {
33+
handleImportDeclaration(node); // track import aliases
34+
35+
const source = node.source.value;
36+
if (source === "solid-js/store") {
37+
context.report({
38+
node,
39+
messageId: "noStore",
40+
});
41+
}
42+
},
43+
"JSXSpreadAttribute MemberExpression"(node: T.MemberExpression) {
44+
context.report({ node, messageId: "spreadMember" });
45+
},
46+
"JSXSpreadAttribute CallExpression"(node: T.CallExpression) {
47+
context.report({ node, messageId: "spreadCall" });
48+
},
49+
CallExpression(node) {
50+
if (node.callee.type === "Identifier") {
51+
if (matchImport("mergeProps", node.callee.name)) {
52+
node.arguments
53+
.filter((arg) => {
54+
if (arg.type === "SpreadElement") return true;
55+
const traced = trace(arg, context.getScope());
56+
return (
57+
(traced.type === "Identifier" && !isPropsByName(traced.name)) ||
58+
isFunctionNode(traced)
59+
);
60+
})
61+
.forEach((badArg) => {
62+
context.report({
63+
node: badArg,
64+
messageId: "mergeProps",
65+
});
66+
});
67+
}
68+
} else if (node.callee.type === "MemberExpression") {
69+
if (
70+
node.callee.object.type === "Identifier" &&
71+
node.callee.object.name === "Proxy" &&
72+
node.callee.property.type === "Identifier" &&
73+
node.callee.property.name === "revocable"
74+
) {
75+
context.report({
76+
node,
77+
messageId: "proxyLiteral",
78+
});
79+
}
80+
}
81+
},
82+
NewExpression(node) {
83+
if (node.callee.type === "Identifier" && node.callee.name === "Proxy") {
84+
context.report({ node, messageId: "proxyLiteral" });
85+
}
86+
},
87+
};
88+
},
89+
};
90+
91+
export default rule;

test/rules/no-proxy-apis.test.ts

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
import { AST_NODE_TYPES as T } from "@typescript-eslint/utils";
2+
import { run } from "../ruleTester";
3+
import rule from "../../src/rules/no-proxy-apis";
4+
5+
// Don't bother checking for imports for every test
6+
jest.mock("../../src/utils", () => {
7+
return {
8+
...jest.requireActual("../../src/utils"),
9+
trackImports: () => {
10+
// eslint-disable-next-line @typescript-eslint/no-empty-function
11+
const handleImportDeclaration = () => {};
12+
const matchImport = (imports: string | Array<string>, str: string) => {
13+
const importArr = Array.isArray(imports) ? imports : [imports];
14+
return importArr.find((i) => i === str);
15+
};
16+
return { matchImport, handleImportDeclaration };
17+
},
18+
};
19+
});
20+
21+
export const cases = run("no-proxy-apis", rule, {
22+
valid: [
23+
`let merged = mergeProps({}, props);`,
24+
`const obj = {}; let merged = mergeProps(obj, props);`,
25+
`let obj = {}; let merged = mergeProps(obj, props);`,
26+
`let merged = mergeProps({ get asdf() { signal() } }, props);`,
27+
`let el = <div {...{ asdf: 'asdf' }} />`,
28+
`let el = <div {...asdf} />`,
29+
`let obj = { Proxy: 1 }`,
30+
],
31+
invalid: [
32+
{
33+
code: `let proxy = new Proxy(asdf, {});`,
34+
errors: [{ messageId: "proxyLiteral" }],
35+
},
36+
{
37+
code: `let proxy = Proxy.revocable(asdf, {});`,
38+
errors: [{ messageId: "proxyLiteral" }],
39+
},
40+
{
41+
code: `import {} from 'solid-js/store';`,
42+
errors: [{ messageId: "noStore", type: T.ImportDeclaration }],
43+
},
44+
{
45+
code: `let el = <div {...maybeSignal()} />`,
46+
errors: [{ messageId: "spreadCall" }],
47+
},
48+
{
49+
code: `let el = <div {...{ ...maybeSignal() }} />`,
50+
errors: [{ messageId: "spreadCall" }],
51+
},
52+
{
53+
code: `let el = <div {...maybeProps.foo} />`,
54+
errors: [{ messageId: "spreadMember" }],
55+
},
56+
{
57+
code: `let el = <div {...{ ...maybeProps.foo }} />`,
58+
errors: [{ messageId: "spreadMember" }],
59+
},
60+
{
61+
code: `let merged = mergeProps(maybeSignal)`,
62+
errors: [{ messageId: "mergeProps" }],
63+
},
64+
{
65+
code: `let func = () => ({}); let merged = mergeProps(func, props)`,
66+
errors: [{ messageId: "mergeProps" }],
67+
},
68+
],
69+
});

0 commit comments

Comments
 (0)