@murky-web/oxlint-plugin-solid 0.0.1
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/README.md +62 -0
- package/package.json +44 -0
- package/src/compat.mjs +53 -0
- package/src/index.mjs +56 -0
- package/src/rules/components_return_once.mjs +202 -0
- package/src/rules/event_handlers.mjs +298 -0
- package/src/rules/imports.mjs +205 -0
- package/src/rules/jsx_no_duplicate_props.mjs +87 -0
- package/src/rules/jsx_no_script_url.mjs +54 -0
- package/src/rules/jsx_no_undef.mjs +217 -0
- package/src/rules/jsx_uses_vars.mjs +55 -0
- package/src/rules/no_array_handlers.mjs +53 -0
- package/src/rules/no_destructure.mjs +210 -0
- package/src/rules/no_innerhtml.mjs +145 -0
- package/src/rules/no_proxy_apis.mjs +96 -0
- package/src/rules/no_react_deps.mjs +65 -0
- package/src/rules/no_react_specific_props.mjs +71 -0
- package/src/rules/no_unknown_namespaces.mjs +100 -0
- package/src/rules/prefer_arrow_components.mjs +411 -0
- package/src/rules/prefer_classlist.mjs +89 -0
- package/src/rules/prefer_for.mjs +92 -0
- package/src/rules/prefer_show.mjs +92 -0
- package/src/rules/reactivity.mjs +1300 -0
- package/src/rules/self_closing_comp.mjs +153 -0
- package/src/rules/style_prop.mjs +155 -0
- package/src/rules/validate_jsx_nesting.mjs +16 -0
- package/src/utils.mjs +337 -0
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
import { ESLintUtils } from "@typescript-eslint/utils";
|
|
2
|
+
|
|
3
|
+
import { getScope, getSourceCode } from "../compat.mjs";
|
|
4
|
+
import {
|
|
5
|
+
isDOMElementName,
|
|
6
|
+
formatList,
|
|
7
|
+
appendImports,
|
|
8
|
+
insertImports,
|
|
9
|
+
} from "../utils.mjs";
|
|
10
|
+
const createRule = ESLintUtils.RuleCreator.withoutDocs;
|
|
11
|
+
// Currently all of the control flow components are from 'solid-js'.
|
|
12
|
+
const AUTO_COMPONENTS = ["Show", "For", "Index", "Switch", "Match"];
|
|
13
|
+
const SOURCE_MODULE = "solid-js";
|
|
14
|
+
export default createRule({
|
|
15
|
+
meta: {
|
|
16
|
+
type: "problem",
|
|
17
|
+
docs: {
|
|
18
|
+
description:
|
|
19
|
+
"Disallow references to undefined variables in JSX. Handles custom directives.",
|
|
20
|
+
url: "https://github.com/solidjs-community/eslint-plugin-solid/blob/main/packages/eslint-plugin-solid/docs/jsx-no-undef.md",
|
|
21
|
+
},
|
|
22
|
+
fixable: "code",
|
|
23
|
+
schema: [
|
|
24
|
+
{
|
|
25
|
+
type: "object",
|
|
26
|
+
properties: {
|
|
27
|
+
allowGlobals: {
|
|
28
|
+
type: "boolean",
|
|
29
|
+
description:
|
|
30
|
+
"When true, the rule will consider the global scope when checking for defined components.",
|
|
31
|
+
default: false,
|
|
32
|
+
},
|
|
33
|
+
autoImport: {
|
|
34
|
+
type: "boolean",
|
|
35
|
+
description:
|
|
36
|
+
'Automatically import certain components from `"solid-js"` if they are undefined.',
|
|
37
|
+
default: true,
|
|
38
|
+
},
|
|
39
|
+
typescriptEnabled: {
|
|
40
|
+
type: "boolean",
|
|
41
|
+
description:
|
|
42
|
+
"Adjusts behavior not to conflict with TypeScript's type checking.",
|
|
43
|
+
default: false,
|
|
44
|
+
},
|
|
45
|
+
},
|
|
46
|
+
additionalProperties: false,
|
|
47
|
+
},
|
|
48
|
+
],
|
|
49
|
+
messages: {
|
|
50
|
+
undefined: "'{{identifier}}' is not defined.",
|
|
51
|
+
customDirectiveUndefined:
|
|
52
|
+
"Custom directive '{{identifier}}' is not defined.",
|
|
53
|
+
autoImport: "{{imports}} should be imported from '{{source}}'.",
|
|
54
|
+
},
|
|
55
|
+
},
|
|
56
|
+
defaultOptions: [],
|
|
57
|
+
create(context) {
|
|
58
|
+
const allowGlobals = context.options[0]?.allowGlobals ?? false;
|
|
59
|
+
const autoImport = context.options[0]?.autoImport !== false;
|
|
60
|
+
const isTypeScriptEnabled =
|
|
61
|
+
context.options[0]?.typescriptEnabled ?? false;
|
|
62
|
+
const missingComponentsSet = new Set();
|
|
63
|
+
/**
|
|
64
|
+
* Compare an identifier with the variables declared in the scope
|
|
65
|
+
* @param {ASTNode} node - Identifier or JSXIdentifier node
|
|
66
|
+
* @returns {void}
|
|
67
|
+
*/
|
|
68
|
+
function checkIdentifierInJSX(
|
|
69
|
+
node,
|
|
70
|
+
{ isComponent, isCustomDirective } = {},
|
|
71
|
+
) {
|
|
72
|
+
let scope = getScope(context, node);
|
|
73
|
+
const sourceCode = getSourceCode(context);
|
|
74
|
+
const sourceType = sourceCode.ast.sourceType;
|
|
75
|
+
const scopeUpperBound =
|
|
76
|
+
!allowGlobals && sourceType === "module" ? "module" : "global";
|
|
77
|
+
const variables = [...scope.variables];
|
|
78
|
+
// Ignore 'this' keyword (also maked as JSXIdentifier when used in JSX)
|
|
79
|
+
if (node.name === "this") {
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
while (
|
|
83
|
+
scope.type !== scopeUpperBound &&
|
|
84
|
+
scope.type !== "global" &&
|
|
85
|
+
scope.upper
|
|
86
|
+
) {
|
|
87
|
+
scope = scope.upper;
|
|
88
|
+
variables.push(...scope.variables);
|
|
89
|
+
}
|
|
90
|
+
if (scope.childScopes.length) {
|
|
91
|
+
variables.push(...scope.childScopes[0].variables);
|
|
92
|
+
// Temporary fix for babel-eslint
|
|
93
|
+
if (scope.childScopes[0].childScopes.length) {
|
|
94
|
+
variables.push(
|
|
95
|
+
...scope.childScopes[0].childScopes[0].variables,
|
|
96
|
+
);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
if (variables.find((variable) => variable.name === node.name)) {
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
if (
|
|
103
|
+
isComponent &&
|
|
104
|
+
autoImport &&
|
|
105
|
+
AUTO_COMPONENTS.includes(node.name) &&
|
|
106
|
+
!missingComponentsSet.has(node.name)
|
|
107
|
+
) {
|
|
108
|
+
// track which names are undefined
|
|
109
|
+
missingComponentsSet.add(node.name);
|
|
110
|
+
} else if (isCustomDirective) {
|
|
111
|
+
context.report({
|
|
112
|
+
node,
|
|
113
|
+
messageId: "customDirectiveUndefined",
|
|
114
|
+
data: {
|
|
115
|
+
identifier: node.name,
|
|
116
|
+
},
|
|
117
|
+
});
|
|
118
|
+
} else if (!isTypeScriptEnabled) {
|
|
119
|
+
context.report({
|
|
120
|
+
node,
|
|
121
|
+
messageId: "undefined",
|
|
122
|
+
data: {
|
|
123
|
+
identifier: node.name,
|
|
124
|
+
},
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
return {
|
|
129
|
+
JSXOpeningElement(node) {
|
|
130
|
+
let n;
|
|
131
|
+
switch (node.name.type) {
|
|
132
|
+
case "JSXIdentifier":
|
|
133
|
+
if (!isDOMElementName(node.name.name)) {
|
|
134
|
+
checkIdentifierInJSX(node.name, {
|
|
135
|
+
isComponent: true,
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
break;
|
|
139
|
+
case "JSXMemberExpression":
|
|
140
|
+
n = node.name;
|
|
141
|
+
do {
|
|
142
|
+
n = n.object;
|
|
143
|
+
} while (n && n.type !== "JSXIdentifier");
|
|
144
|
+
if (n) {
|
|
145
|
+
checkIdentifierInJSX(n);
|
|
146
|
+
}
|
|
147
|
+
break;
|
|
148
|
+
default:
|
|
149
|
+
break;
|
|
150
|
+
}
|
|
151
|
+
},
|
|
152
|
+
"JSXAttribute > JSXNamespacedName": (node) => {
|
|
153
|
+
// <Element use:X /> applies the `X` custom directive to the element, where `X` must be an identifier in scope.
|
|
154
|
+
if (
|
|
155
|
+
node.namespace?.type === "JSXIdentifier" &&
|
|
156
|
+
node.namespace.name === "use" &&
|
|
157
|
+
node.name?.type === "JSXIdentifier"
|
|
158
|
+
) {
|
|
159
|
+
checkIdentifierInJSX(node.name, {
|
|
160
|
+
isCustomDirective: true,
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
},
|
|
164
|
+
"Program:exit": (programNode) => {
|
|
165
|
+
// add in any auto import components used in the program
|
|
166
|
+
const missingComponents = Array.from(
|
|
167
|
+
missingComponentsSet.values(),
|
|
168
|
+
);
|
|
169
|
+
if (autoImport && missingComponents.length) {
|
|
170
|
+
const importNode = programNode.body.find(
|
|
171
|
+
(n) =>
|
|
172
|
+
n.type === "ImportDeclaration" &&
|
|
173
|
+
n.importKind !== "type" &&
|
|
174
|
+
n.source.type === "Literal" &&
|
|
175
|
+
n.source.value === SOURCE_MODULE,
|
|
176
|
+
);
|
|
177
|
+
if (importNode) {
|
|
178
|
+
context.report({
|
|
179
|
+
node: importNode,
|
|
180
|
+
messageId: "autoImport",
|
|
181
|
+
data: {
|
|
182
|
+
imports: formatList(missingComponents), // "Show, For, and Switch"
|
|
183
|
+
source: SOURCE_MODULE,
|
|
184
|
+
},
|
|
185
|
+
fix: (fixer) => {
|
|
186
|
+
return appendImports(
|
|
187
|
+
fixer,
|
|
188
|
+
getSourceCode(context),
|
|
189
|
+
importNode,
|
|
190
|
+
missingComponents,
|
|
191
|
+
);
|
|
192
|
+
},
|
|
193
|
+
});
|
|
194
|
+
} else {
|
|
195
|
+
context.report({
|
|
196
|
+
node: programNode,
|
|
197
|
+
messageId: "autoImport",
|
|
198
|
+
data: {
|
|
199
|
+
imports: formatList(missingComponents),
|
|
200
|
+
source: SOURCE_MODULE,
|
|
201
|
+
},
|
|
202
|
+
fix: (fixer) => {
|
|
203
|
+
// insert `import { missing, identifiers } from "solid-js"` at top of module
|
|
204
|
+
return insertImports(
|
|
205
|
+
fixer,
|
|
206
|
+
getSourceCode(context),
|
|
207
|
+
"solid-js",
|
|
208
|
+
missingComponents,
|
|
209
|
+
);
|
|
210
|
+
},
|
|
211
|
+
});
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
},
|
|
215
|
+
};
|
|
216
|
+
},
|
|
217
|
+
});
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { ESLintUtils } from "@typescript-eslint/utils";
|
|
2
|
+
|
|
3
|
+
import { markVariableAsUsed } from "../compat.mjs";
|
|
4
|
+
|
|
5
|
+
const createRule = ESLintUtils.RuleCreator.withoutDocs;
|
|
6
|
+
|
|
7
|
+
export const jsxUsesVarsRule = createRule({
|
|
8
|
+
meta: {
|
|
9
|
+
docs: {
|
|
10
|
+
description:
|
|
11
|
+
"Prevent variables used in JSX from being marked as unused.",
|
|
12
|
+
url: "https://github.com/solidjs-community/eslint-plugin-solid/blob/main/packages/eslint-plugin-solid/docs/jsx-uses-vars.md",
|
|
13
|
+
},
|
|
14
|
+
messages: {},
|
|
15
|
+
schema: [],
|
|
16
|
+
type: "problem",
|
|
17
|
+
},
|
|
18
|
+
defaultOptions: [],
|
|
19
|
+
create(context) {
|
|
20
|
+
return {
|
|
21
|
+
JSXOpeningElement(node) {
|
|
22
|
+
let parent = null;
|
|
23
|
+
|
|
24
|
+
switch (node.name.type) {
|
|
25
|
+
case "JSXNamespacedName":
|
|
26
|
+
return;
|
|
27
|
+
case "JSXIdentifier":
|
|
28
|
+
markVariableAsUsed(context, node.name.name, node.name);
|
|
29
|
+
return;
|
|
30
|
+
case "JSXMemberExpression":
|
|
31
|
+
parent = node.name.object;
|
|
32
|
+
while (parent?.type === "JSXMemberExpression") {
|
|
33
|
+
parent = parent.object;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
if (parent?.type === "JSXIdentifier") {
|
|
37
|
+
markVariableAsUsed(context, parent.name, parent);
|
|
38
|
+
}
|
|
39
|
+
return;
|
|
40
|
+
default:
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
},
|
|
44
|
+
"JSXAttribute > JSXNamespacedName"(node) {
|
|
45
|
+
if (
|
|
46
|
+
node.namespace?.type === "JSXIdentifier" &&
|
|
47
|
+
node.namespace.name === "use" &&
|
|
48
|
+
node.name?.type === "JSXIdentifier"
|
|
49
|
+
) {
|
|
50
|
+
markVariableAsUsed(context, node.name.name, node.name);
|
|
51
|
+
}
|
|
52
|
+
},
|
|
53
|
+
};
|
|
54
|
+
},
|
|
55
|
+
});
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { ESLintUtils } from "@typescript-eslint/utils";
|
|
2
|
+
|
|
3
|
+
import { isDOMElementName, trace } from "../utils.mjs";
|
|
4
|
+
|
|
5
|
+
const createRule = ESLintUtils.RuleCreator.withoutDocs;
|
|
6
|
+
|
|
7
|
+
export const noArrayHandlersRule = createRule({
|
|
8
|
+
meta: {
|
|
9
|
+
docs: {
|
|
10
|
+
description: "Disallow usage of type-unsafe event handlers.",
|
|
11
|
+
url: "https://github.com/solidjs-community/eslint-plugin-solid/blob/main/packages/eslint-plugin-solid/docs/no-array-handlers.md",
|
|
12
|
+
},
|
|
13
|
+
messages: {
|
|
14
|
+
noArrayHandlers:
|
|
15
|
+
"Passing an array as an event handler is potentially type-unsafe.",
|
|
16
|
+
},
|
|
17
|
+
schema: [],
|
|
18
|
+
type: "problem",
|
|
19
|
+
},
|
|
20
|
+
defaultOptions: [],
|
|
21
|
+
create(context) {
|
|
22
|
+
return {
|
|
23
|
+
JSXAttribute(node) {
|
|
24
|
+
const openingElement = node.parent;
|
|
25
|
+
if (
|
|
26
|
+
openingElement.name.type !== "JSXIdentifier" ||
|
|
27
|
+
!isDOMElementName(openingElement.name.name)
|
|
28
|
+
) {
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const isNamespacedHandler =
|
|
33
|
+
node.name.type === "JSXNamespacedName" &&
|
|
34
|
+
node.name.namespace.name === "on";
|
|
35
|
+
const isNormalEventHandler =
|
|
36
|
+
node.name.type === "JSXIdentifier" &&
|
|
37
|
+
/^on[a-zA-Z]/.test(node.name.name);
|
|
38
|
+
|
|
39
|
+
if (
|
|
40
|
+
(isNamespacedHandler || isNormalEventHandler) &&
|
|
41
|
+
node.value?.type === "JSXExpressionContainer" &&
|
|
42
|
+
trace(node.value.expression, context).type ===
|
|
43
|
+
"ArrayExpression"
|
|
44
|
+
) {
|
|
45
|
+
context.report({
|
|
46
|
+
messageId: "noArrayHandlers",
|
|
47
|
+
node,
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
},
|
|
51
|
+
};
|
|
52
|
+
},
|
|
53
|
+
});
|
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
import { ESLintUtils, ASTUtils } from "@typescript-eslint/utils";
|
|
2
|
+
|
|
3
|
+
import { getSourceCode } from "../compat.mjs";
|
|
4
|
+
const createRule = ESLintUtils.RuleCreator.withoutDocs;
|
|
5
|
+
const { getStringIfConstant } = ASTUtils;
|
|
6
|
+
const getName = (node) => {
|
|
7
|
+
switch (node.type) {
|
|
8
|
+
case "Literal":
|
|
9
|
+
return typeof node.value === "string" ? node.value : null;
|
|
10
|
+
case "Identifier":
|
|
11
|
+
return node.name;
|
|
12
|
+
case "AssignmentPattern":
|
|
13
|
+
return getName(node.left);
|
|
14
|
+
default:
|
|
15
|
+
return getStringIfConstant(node);
|
|
16
|
+
}
|
|
17
|
+
};
|
|
18
|
+
// Given ({ 'hello-world': helloWorld = 5 }), returns { real: Literal('hello-world'), var: 'helloWorld', computed: false, init: Literal(5) }
|
|
19
|
+
const getPropertyInfo = (prop) => {
|
|
20
|
+
const valueName = getName(prop.value);
|
|
21
|
+
if (valueName !== null) {
|
|
22
|
+
return {
|
|
23
|
+
real: prop.key,
|
|
24
|
+
var: valueName,
|
|
25
|
+
computed: prop.computed,
|
|
26
|
+
init:
|
|
27
|
+
prop.value.type === "AssignmentPattern"
|
|
28
|
+
? prop.value.right
|
|
29
|
+
: undefined,
|
|
30
|
+
};
|
|
31
|
+
} else {
|
|
32
|
+
return null;
|
|
33
|
+
}
|
|
34
|
+
};
|
|
35
|
+
export default createRule({
|
|
36
|
+
meta: {
|
|
37
|
+
type: "problem",
|
|
38
|
+
docs: {
|
|
39
|
+
description:
|
|
40
|
+
"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.",
|
|
41
|
+
url: "https://github.com/solidjs-community/eslint-plugin-solid/blob/main/packages/eslint-plugin-solid/docs/no-destructure.md",
|
|
42
|
+
},
|
|
43
|
+
fixable: "code",
|
|
44
|
+
schema: [],
|
|
45
|
+
messages: {
|
|
46
|
+
noDestructure:
|
|
47
|
+
"Destructuring component props breaks Solid's reactivity; use property access instead.",
|
|
48
|
+
// noWriteToProps: "Component props are readonly, writing to props is not supported.",
|
|
49
|
+
},
|
|
50
|
+
},
|
|
51
|
+
defaultOptions: [],
|
|
52
|
+
create(context) {
|
|
53
|
+
const functionStack = [];
|
|
54
|
+
const currentFunction = () => functionStack[functionStack.length - 1];
|
|
55
|
+
const onFunctionEnter = () => {
|
|
56
|
+
functionStack.push({ hasJSX: false });
|
|
57
|
+
};
|
|
58
|
+
const onFunctionExit = (node) => {
|
|
59
|
+
if (node.params.length === 1) {
|
|
60
|
+
const props = node.params[0];
|
|
61
|
+
if (
|
|
62
|
+
props.type === "ObjectPattern" &&
|
|
63
|
+
currentFunction().hasJSX &&
|
|
64
|
+
node.parent?.type !== "JSXExpressionContainer" // "render props" aren't components
|
|
65
|
+
) {
|
|
66
|
+
// Props are destructured in the function params, not the body. We actually don't
|
|
67
|
+
// need to handle the case where props are destructured in the body, because that
|
|
68
|
+
// will be a violation of "solid/reactivity".
|
|
69
|
+
context.report({
|
|
70
|
+
node: props,
|
|
71
|
+
messageId: "noDestructure",
|
|
72
|
+
fix: (fixer) => fixDestructure(node, props, fixer),
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
// Pop on exit
|
|
77
|
+
functionStack.pop();
|
|
78
|
+
};
|
|
79
|
+
function* fixDestructure(func, props, fixer) {
|
|
80
|
+
const propsName = "props";
|
|
81
|
+
const properties = props.properties;
|
|
82
|
+
const propertyInfo = [];
|
|
83
|
+
let rest = null;
|
|
84
|
+
for (const property of properties) {
|
|
85
|
+
if (property.type === "RestElement") {
|
|
86
|
+
rest = property;
|
|
87
|
+
} else {
|
|
88
|
+
const info = getPropertyInfo(property);
|
|
89
|
+
if (info === null) {
|
|
90
|
+
continue;
|
|
91
|
+
}
|
|
92
|
+
propertyInfo.push(info);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
const hasDefaults = propertyInfo.some((info) => info.init);
|
|
96
|
+
// Replace destructured props with a `props` identifier (`_props` in case of rest params/defaults)
|
|
97
|
+
const origProps = !(hasDefaults || rest)
|
|
98
|
+
? propsName
|
|
99
|
+
: "_" + propsName;
|
|
100
|
+
if (props.typeAnnotation) {
|
|
101
|
+
// in `{ prop1, prop2 }: Props`, leave `: Props` alone
|
|
102
|
+
const range = [props.range[0], props.typeAnnotation.range[0]];
|
|
103
|
+
yield fixer.replaceTextRange(range, origProps);
|
|
104
|
+
} else {
|
|
105
|
+
yield fixer.replaceText(props, origProps);
|
|
106
|
+
}
|
|
107
|
+
const sourceCode = getSourceCode(context);
|
|
108
|
+
const defaultsObjectString = () =>
|
|
109
|
+
propertyInfo
|
|
110
|
+
.filter((info) => info.init)
|
|
111
|
+
.map(
|
|
112
|
+
(info) =>
|
|
113
|
+
`${info.computed ? "[" : ""}${sourceCode.getText(info.real)}${info.computed ? "]" : ""}: ${sourceCode.getText(info.init)}`,
|
|
114
|
+
)
|
|
115
|
+
.join(", ");
|
|
116
|
+
const splitPropsArray = () =>
|
|
117
|
+
`[${propertyInfo
|
|
118
|
+
.map((info) =>
|
|
119
|
+
info.real.type === "Identifier"
|
|
120
|
+
? JSON.stringify(info.real.name)
|
|
121
|
+
: sourceCode.getText(info.real),
|
|
122
|
+
)
|
|
123
|
+
.join(", ")}]`;
|
|
124
|
+
let lineToInsert = "";
|
|
125
|
+
if (hasDefaults && rest) {
|
|
126
|
+
// Insert a line that assigns _props
|
|
127
|
+
lineToInsert = ` const [${propsName}, ${(rest.argument.type === "Identifier" && rest.argument.name) || "rest"}] = splitProps(mergeProps({ ${defaultsObjectString()} }, ${origProps}), ${splitPropsArray()});`;
|
|
128
|
+
} else if (hasDefaults) {
|
|
129
|
+
// Insert a line that assigns _props merged with defaults to props
|
|
130
|
+
lineToInsert = ` const ${propsName} = mergeProps({ ${defaultsObjectString()} }, ${origProps});\n`;
|
|
131
|
+
} else if (rest) {
|
|
132
|
+
// Insert a line that keeps named props and extracts the rest into a new reactive rest object
|
|
133
|
+
lineToInsert = ` const [${propsName}, ${(rest.argument.type === "Identifier" && rest.argument.name) || "rest"}] = splitProps(${origProps}, ${splitPropsArray()});\n`;
|
|
134
|
+
}
|
|
135
|
+
if (lineToInsert) {
|
|
136
|
+
const body = func.body;
|
|
137
|
+
if (body.type === "BlockStatement") {
|
|
138
|
+
if (body.body.length > 0) {
|
|
139
|
+
// Inject lines handling defaults/rest params before the first statement in the block.
|
|
140
|
+
yield fixer.insertTextBefore(
|
|
141
|
+
body.body[0],
|
|
142
|
+
lineToInsert,
|
|
143
|
+
);
|
|
144
|
+
}
|
|
145
|
+
// with an empty block statement body, no need to inject code
|
|
146
|
+
} else {
|
|
147
|
+
// The function is an arrow function that implicitly returns an expression, possibly with wrapping parentheses.
|
|
148
|
+
// These must be removed to convert the function body to a block statement for code injection.
|
|
149
|
+
const maybeOpenParen = sourceCode.getTokenBefore(body);
|
|
150
|
+
if (maybeOpenParen?.value === "(") {
|
|
151
|
+
yield fixer.remove(maybeOpenParen);
|
|
152
|
+
}
|
|
153
|
+
const maybeCloseParen = sourceCode.getTokenAfter(body);
|
|
154
|
+
if (maybeCloseParen?.value === ")") {
|
|
155
|
+
yield fixer.remove(maybeCloseParen);
|
|
156
|
+
}
|
|
157
|
+
// Inject lines handling defaults/rest params
|
|
158
|
+
yield fixer.insertTextBefore(
|
|
159
|
+
body,
|
|
160
|
+
`{\n${lineToInsert} return (`,
|
|
161
|
+
);
|
|
162
|
+
yield fixer.insertTextAfter(body, `);\n}`);
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
const scope = sourceCode.scopeManager?.acquire(func);
|
|
166
|
+
if (scope) {
|
|
167
|
+
// iterate through destructured variables, associated with real node
|
|
168
|
+
for (const [info, variable] of propertyInfo.map((info) => [
|
|
169
|
+
info,
|
|
170
|
+
scope.set.get(info.var),
|
|
171
|
+
])) {
|
|
172
|
+
if (variable) {
|
|
173
|
+
// replace all usages of the variable with props accesses
|
|
174
|
+
for (const reference of variable.references) {
|
|
175
|
+
if (reference.isReadOnly()) {
|
|
176
|
+
const access =
|
|
177
|
+
info.real.type === "Identifier" &&
|
|
178
|
+
!info.computed
|
|
179
|
+
? `.${info.real.name}`
|
|
180
|
+
: `[${sourceCode.getText(info.real)}]`;
|
|
181
|
+
yield fixer.replaceText(
|
|
182
|
+
reference.identifier,
|
|
183
|
+
`${propsName}${access}`,
|
|
184
|
+
);
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
return {
|
|
192
|
+
FunctionDeclaration: onFunctionEnter,
|
|
193
|
+
FunctionExpression: onFunctionEnter,
|
|
194
|
+
ArrowFunctionExpression: onFunctionEnter,
|
|
195
|
+
"FunctionDeclaration:exit": onFunctionExit,
|
|
196
|
+
"FunctionExpression:exit": onFunctionExit,
|
|
197
|
+
"ArrowFunctionExpression:exit": onFunctionExit,
|
|
198
|
+
JSXElement() {
|
|
199
|
+
if (functionStack.length) {
|
|
200
|
+
currentFunction().hasJSX = true;
|
|
201
|
+
}
|
|
202
|
+
},
|
|
203
|
+
JSXFragment() {
|
|
204
|
+
if (functionStack.length) {
|
|
205
|
+
currentFunction().hasJSX = true;
|
|
206
|
+
}
|
|
207
|
+
},
|
|
208
|
+
};
|
|
209
|
+
},
|
|
210
|
+
});
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
import { ESLintUtils, ASTUtils } from "@typescript-eslint/utils";
|
|
2
|
+
import isHtml from "is-html";
|
|
3
|
+
|
|
4
|
+
import { jsxPropName } from "../utils.mjs";
|
|
5
|
+
const createRule = ESLintUtils.RuleCreator.withoutDocs;
|
|
6
|
+
const { getStringIfConstant } = ASTUtils;
|
|
7
|
+
export default createRule({
|
|
8
|
+
meta: {
|
|
9
|
+
type: "problem",
|
|
10
|
+
docs: {
|
|
11
|
+
description:
|
|
12
|
+
"Disallow usage of the innerHTML attribute, which can often lead to security vulnerabilities.",
|
|
13
|
+
url: "https://github.com/solidjs-community/eslint-plugin-solid/blob/main/packages/eslint-plugin-solid/docs/no-innerhtml.md",
|
|
14
|
+
},
|
|
15
|
+
fixable: "code",
|
|
16
|
+
hasSuggestions: true,
|
|
17
|
+
schema: [
|
|
18
|
+
{
|
|
19
|
+
type: "object",
|
|
20
|
+
properties: {
|
|
21
|
+
allowStatic: {
|
|
22
|
+
description:
|
|
23
|
+
"if the innerHTML value is guaranteed to be a static HTML string (i.e. no user input), allow it",
|
|
24
|
+
type: "boolean",
|
|
25
|
+
default: true,
|
|
26
|
+
},
|
|
27
|
+
},
|
|
28
|
+
additionalProperties: false,
|
|
29
|
+
},
|
|
30
|
+
],
|
|
31
|
+
messages: {
|
|
32
|
+
dangerous:
|
|
33
|
+
"The innerHTML attribute is dangerous; passing unsanitized input can lead to security vulnerabilities.",
|
|
34
|
+
conflict:
|
|
35
|
+
"The innerHTML attribute should not be used on an element with child elements; they will be overwritten.",
|
|
36
|
+
notHtml:
|
|
37
|
+
"The string passed to innerHTML does not appear to be valid HTML.",
|
|
38
|
+
useInnerText:
|
|
39
|
+
"For text content, using innerText is clearer and safer.",
|
|
40
|
+
dangerouslySetInnerHTML:
|
|
41
|
+
"The dangerouslySetInnerHTML prop is not supported; use innerHTML instead.",
|
|
42
|
+
},
|
|
43
|
+
},
|
|
44
|
+
defaultOptions: [{ allowStatic: true }],
|
|
45
|
+
create(context) {
|
|
46
|
+
const allowStatic = Boolean(context.options[0]?.allowStatic ?? true);
|
|
47
|
+
return {
|
|
48
|
+
JSXAttribute(node) {
|
|
49
|
+
if (jsxPropName(node) === "dangerouslySetInnerHTML") {
|
|
50
|
+
if (
|
|
51
|
+
node.value?.type === "JSXExpressionContainer" &&
|
|
52
|
+
node.value.expression.type === "ObjectExpression" &&
|
|
53
|
+
node.value.expression.properties.length === 1
|
|
54
|
+
) {
|
|
55
|
+
const htmlProp = node.value.expression.properties[0];
|
|
56
|
+
if (
|
|
57
|
+
htmlProp.type === "Property" &&
|
|
58
|
+
htmlProp.key.type === "Identifier" &&
|
|
59
|
+
htmlProp.key.name === "__html"
|
|
60
|
+
) {
|
|
61
|
+
context.report({
|
|
62
|
+
node,
|
|
63
|
+
messageId: "dangerouslySetInnerHTML",
|
|
64
|
+
fix: (fixer) => {
|
|
65
|
+
const propRange = node.range;
|
|
66
|
+
const valueRange = htmlProp.value.range;
|
|
67
|
+
return [
|
|
68
|
+
fixer.replaceTextRange(
|
|
69
|
+
[propRange[0], valueRange[0]],
|
|
70
|
+
"innerHTML={",
|
|
71
|
+
),
|
|
72
|
+
fixer.replaceTextRange(
|
|
73
|
+
[valueRange[1], propRange[1]],
|
|
74
|
+
"}",
|
|
75
|
+
),
|
|
76
|
+
];
|
|
77
|
+
},
|
|
78
|
+
});
|
|
79
|
+
} else {
|
|
80
|
+
context.report({
|
|
81
|
+
node,
|
|
82
|
+
messageId: "dangerouslySetInnerHTML",
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
} else {
|
|
86
|
+
context.report({
|
|
87
|
+
node,
|
|
88
|
+
messageId: "dangerouslySetInnerHTML",
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
return;
|
|
92
|
+
} else if (jsxPropName(node) !== "innerHTML") {
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
if (allowStatic) {
|
|
96
|
+
const innerHtmlNode =
|
|
97
|
+
node.value?.type === "JSXExpressionContainer"
|
|
98
|
+
? node.value.expression
|
|
99
|
+
: node.value;
|
|
100
|
+
const innerHtml =
|
|
101
|
+
innerHtmlNode && getStringIfConstant(innerHtmlNode);
|
|
102
|
+
if (typeof innerHtml === "string") {
|
|
103
|
+
if (isHtml(innerHtml)) {
|
|
104
|
+
// go up to enclosing JSXElement and check if it has children
|
|
105
|
+
if (
|
|
106
|
+
node.parent?.parent?.type === "JSXElement" &&
|
|
107
|
+
node.parent.parent.children?.length
|
|
108
|
+
) {
|
|
109
|
+
context.report({
|
|
110
|
+
node: node.parent.parent, // report error on JSXElement instead of JSXAttribute
|
|
111
|
+
messageId: "conflict",
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
} else {
|
|
115
|
+
context.report({
|
|
116
|
+
node,
|
|
117
|
+
messageId: "notHtml",
|
|
118
|
+
suggest: [
|
|
119
|
+
{
|
|
120
|
+
fix: (fixer) =>
|
|
121
|
+
fixer.replaceText(
|
|
122
|
+
node.name,
|
|
123
|
+
"innerText",
|
|
124
|
+
),
|
|
125
|
+
messageId: "useInnerText",
|
|
126
|
+
},
|
|
127
|
+
],
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
} else {
|
|
131
|
+
context.report({
|
|
132
|
+
node,
|
|
133
|
+
messageId: "dangerous",
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
} else {
|
|
137
|
+
context.report({
|
|
138
|
+
node,
|
|
139
|
+
messageId: "dangerous",
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
},
|
|
143
|
+
};
|
|
144
|
+
},
|
|
145
|
+
});
|