@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,298 @@
|
|
|
1
|
+
import { ESLintUtils, ASTUtils } from "@typescript-eslint/utils";
|
|
2
|
+
|
|
3
|
+
import { getScope, getSourceCode } from "../compat.mjs";
|
|
4
|
+
import { isDOMElementName } from "../utils.mjs";
|
|
5
|
+
const createRule = ESLintUtils.RuleCreator.withoutDocs;
|
|
6
|
+
const { getStaticValue } = ASTUtils;
|
|
7
|
+
const COMMON_EVENTS = [
|
|
8
|
+
"onAnimationEnd",
|
|
9
|
+
"onAnimationIteration",
|
|
10
|
+
"onAnimationStart",
|
|
11
|
+
"onBeforeInput",
|
|
12
|
+
"onBlur",
|
|
13
|
+
"onChange",
|
|
14
|
+
"onClick",
|
|
15
|
+
"onContextMenu",
|
|
16
|
+
"onCopy",
|
|
17
|
+
"onCut",
|
|
18
|
+
"onDblClick",
|
|
19
|
+
"onDrag",
|
|
20
|
+
"onDragEnd",
|
|
21
|
+
"onDragEnter",
|
|
22
|
+
"onDragExit",
|
|
23
|
+
"onDragLeave",
|
|
24
|
+
"onDragOver",
|
|
25
|
+
"onDragStart",
|
|
26
|
+
"onDrop",
|
|
27
|
+
"onError",
|
|
28
|
+
"onFocus",
|
|
29
|
+
"onFocusIn",
|
|
30
|
+
"onFocusOut",
|
|
31
|
+
"onGotPointerCapture",
|
|
32
|
+
"onInput",
|
|
33
|
+
"onInvalid",
|
|
34
|
+
"onKeyDown",
|
|
35
|
+
"onKeyPress",
|
|
36
|
+
"onKeyUp",
|
|
37
|
+
"onLoad",
|
|
38
|
+
"onLostPointerCapture",
|
|
39
|
+
"onMouseDown",
|
|
40
|
+
"onMouseEnter",
|
|
41
|
+
"onMouseLeave",
|
|
42
|
+
"onMouseMove",
|
|
43
|
+
"onMouseOut",
|
|
44
|
+
"onMouseOver",
|
|
45
|
+
"onMouseUp",
|
|
46
|
+
"onPaste",
|
|
47
|
+
"onPointerCancel",
|
|
48
|
+
"onPointerDown",
|
|
49
|
+
"onPointerEnter",
|
|
50
|
+
"onPointerLeave",
|
|
51
|
+
"onPointerMove",
|
|
52
|
+
"onPointerOut",
|
|
53
|
+
"onPointerOver",
|
|
54
|
+
"onPointerUp",
|
|
55
|
+
"onReset",
|
|
56
|
+
"onScroll",
|
|
57
|
+
"onSelect",
|
|
58
|
+
"onSubmit",
|
|
59
|
+
"onToggle",
|
|
60
|
+
"onTouchCancel",
|
|
61
|
+
"onTouchEnd",
|
|
62
|
+
"onTouchMove",
|
|
63
|
+
"onTouchStart",
|
|
64
|
+
"onTransitionEnd",
|
|
65
|
+
"onWheel",
|
|
66
|
+
];
|
|
67
|
+
const COMMON_EVENTS_MAP = new Map(
|
|
68
|
+
(function* () {
|
|
69
|
+
for (const event of COMMON_EVENTS) {
|
|
70
|
+
yield [event.toLowerCase(), event];
|
|
71
|
+
}
|
|
72
|
+
})(),
|
|
73
|
+
);
|
|
74
|
+
const NONSTANDARD_EVENTS_MAP = {
|
|
75
|
+
ondoubleclick: "onDblClick",
|
|
76
|
+
};
|
|
77
|
+
const isCommonHandlerName = (lowercaseHandlerName) =>
|
|
78
|
+
COMMON_EVENTS_MAP.has(lowercaseHandlerName);
|
|
79
|
+
const getCommonEventHandlerName = (lowercaseHandlerName) =>
|
|
80
|
+
COMMON_EVENTS_MAP.get(lowercaseHandlerName);
|
|
81
|
+
const isNonstandardEventName = (lowercaseEventName) =>
|
|
82
|
+
Boolean(NONSTANDARD_EVENTS_MAP[lowercaseEventName]);
|
|
83
|
+
const getStandardEventHandlerName = (lowercaseEventName) =>
|
|
84
|
+
NONSTANDARD_EVENTS_MAP[lowercaseEventName];
|
|
85
|
+
export default createRule({
|
|
86
|
+
meta: {
|
|
87
|
+
type: "problem",
|
|
88
|
+
docs: {
|
|
89
|
+
description:
|
|
90
|
+
"Enforce naming DOM element event handlers consistently and prevent Solid's analysis from misunderstanding whether a prop should be an event handler.",
|
|
91
|
+
url: "https://github.com/solidjs-community/eslint-plugin-solid/blob/main/packages/eslint-plugin-solid/docs/event-handlers.md",
|
|
92
|
+
},
|
|
93
|
+
fixable: "code",
|
|
94
|
+
hasSuggestions: true,
|
|
95
|
+
schema: [
|
|
96
|
+
{
|
|
97
|
+
type: "object",
|
|
98
|
+
properties: {
|
|
99
|
+
ignoreCase: {
|
|
100
|
+
type: "boolean",
|
|
101
|
+
description:
|
|
102
|
+
"if true, don't warn on ambiguously named event handlers like `onclick` or `onchange`",
|
|
103
|
+
default: false,
|
|
104
|
+
},
|
|
105
|
+
warnOnSpread: {
|
|
106
|
+
type: "boolean",
|
|
107
|
+
description:
|
|
108
|
+
"if true, warn when spreading event handlers onto JSX. Enable for Solid < v1.6.",
|
|
109
|
+
default: false,
|
|
110
|
+
},
|
|
111
|
+
},
|
|
112
|
+
additionalProperties: false,
|
|
113
|
+
},
|
|
114
|
+
],
|
|
115
|
+
messages: {
|
|
116
|
+
"detected-attr":
|
|
117
|
+
'The {{name}} prop is named as an event handler (starts with "on"), but Solid knows its value ({{staticValue}}) is a string or number, so it will be treated as an attribute. If this is intentional, name this prop attr:{{name}}.',
|
|
118
|
+
naming: "The {{name}} prop is ambiguous. If it is an event handler, change it to {{handlerName}}. If it is an attribute, change it to {{attrName}}.",
|
|
119
|
+
capitalization:
|
|
120
|
+
"The {{name}} prop should be renamed to {{fixedName}} for readability.",
|
|
121
|
+
nonstandard:
|
|
122
|
+
"The {{name}} prop should be renamed to {{fixedName}}, because it's not a standard event handler.",
|
|
123
|
+
"make-handler": "Change the {{name}} prop to {{handlerName}}.",
|
|
124
|
+
"make-attr": "Change the {{name}} prop to {{attrName}}.",
|
|
125
|
+
"spread-handler":
|
|
126
|
+
"The {{name}} prop should be added as a JSX attribute, not spread in. Solid doesn't add listeners when spreading into JSX.",
|
|
127
|
+
},
|
|
128
|
+
},
|
|
129
|
+
defaultOptions: [],
|
|
130
|
+
create(context) {
|
|
131
|
+
const sourceCode = getSourceCode(context);
|
|
132
|
+
return {
|
|
133
|
+
JSXAttribute(node) {
|
|
134
|
+
const openingElement = node.parent;
|
|
135
|
+
if (
|
|
136
|
+
openingElement.name.type !== "JSXIdentifier" ||
|
|
137
|
+
!isDOMElementName(openingElement.name.name)
|
|
138
|
+
) {
|
|
139
|
+
return; // bail if this is not a DOM/SVG element or web component
|
|
140
|
+
}
|
|
141
|
+
if (node.name.type === "JSXNamespacedName") {
|
|
142
|
+
return; // bail early on attr:, on:, oncapture:, etc. props
|
|
143
|
+
}
|
|
144
|
+
// string name of the name node
|
|
145
|
+
const { name } = node.name;
|
|
146
|
+
if (!/^on[a-zA-Z]/.test(name)) {
|
|
147
|
+
return; // bail if Solid doesn't consider the prop name an event handler
|
|
148
|
+
}
|
|
149
|
+
let staticValue = null;
|
|
150
|
+
if (
|
|
151
|
+
node.value?.type === "JSXExpressionContainer" &&
|
|
152
|
+
node.value.expression.type !== "JSXEmptyExpression" &&
|
|
153
|
+
node.value.expression.type !== "ArrayExpression" && // array syntax prevents inlining
|
|
154
|
+
(staticValue = getStaticValue(
|
|
155
|
+
node.value.expression,
|
|
156
|
+
getScope(context, node),
|
|
157
|
+
)) !== null &&
|
|
158
|
+
(typeof staticValue.value === "string" ||
|
|
159
|
+
typeof staticValue.value === "number")
|
|
160
|
+
) {
|
|
161
|
+
// One of the first things Solid (actually babel-plugin-dom-expressions) does with an
|
|
162
|
+
// attribute is determine if it can be inlined into a template string instead of
|
|
163
|
+
// injected programmatically. It runs
|
|
164
|
+
// `attribute.get("value").get("expression").evaluate().value` on attributes with
|
|
165
|
+
// JSXExpressionContainers, and if the statically evaluated value is a string or number,
|
|
166
|
+
// it inlines it. This runs even for attributes that follow the naming convention for
|
|
167
|
+
// event handlers. By starting an attribute name with "on", the user has signalled that
|
|
168
|
+
// they intend the attribute to be an event handler. If the attribute value would be
|
|
169
|
+
// inlined, report that.
|
|
170
|
+
// https://github.com/ryansolid/dom-expressions/blob/cb3be7558c731e2a442e9c7e07d25373c40cf2be/packages/babel-plugin-jsx-dom-expressions/src/dom/element.js#L347
|
|
171
|
+
context.report({
|
|
172
|
+
node,
|
|
173
|
+
messageId: "detected-attr",
|
|
174
|
+
data: {
|
|
175
|
+
name,
|
|
176
|
+
staticValue: staticValue.value,
|
|
177
|
+
},
|
|
178
|
+
});
|
|
179
|
+
} else if (
|
|
180
|
+
node.value === null ||
|
|
181
|
+
node.value?.type === "Literal"
|
|
182
|
+
) {
|
|
183
|
+
// Check for same as above for literal values
|
|
184
|
+
context.report({
|
|
185
|
+
node,
|
|
186
|
+
messageId: "detected-attr",
|
|
187
|
+
data: {
|
|
188
|
+
name,
|
|
189
|
+
staticValue:
|
|
190
|
+
node.value !== null ? node.value.value : true,
|
|
191
|
+
},
|
|
192
|
+
});
|
|
193
|
+
} else if (!context.options[0]?.ignoreCase) {
|
|
194
|
+
const lowercaseHandlerName = name.toLowerCase();
|
|
195
|
+
if (isNonstandardEventName(lowercaseHandlerName)) {
|
|
196
|
+
const fixedName =
|
|
197
|
+
getStandardEventHandlerName(lowercaseHandlerName);
|
|
198
|
+
context.report({
|
|
199
|
+
node: node.name,
|
|
200
|
+
messageId: "nonstandard",
|
|
201
|
+
data: { name, fixedName },
|
|
202
|
+
fix: (fixer) =>
|
|
203
|
+
fixer.replaceText(node.name, fixedName),
|
|
204
|
+
});
|
|
205
|
+
} else if (isCommonHandlerName(lowercaseHandlerName)) {
|
|
206
|
+
const fixedName =
|
|
207
|
+
getCommonEventHandlerName(lowercaseHandlerName);
|
|
208
|
+
if (fixedName !== name) {
|
|
209
|
+
// For common DOM event names, we know the user intended the prop to be an event handler.
|
|
210
|
+
// Fix it to have an uppercase third letter and be properly camel-cased.
|
|
211
|
+
context.report({
|
|
212
|
+
node: node.name,
|
|
213
|
+
messageId: "capitalization",
|
|
214
|
+
data: { name, fixedName },
|
|
215
|
+
fix: (fixer) =>
|
|
216
|
+
fixer.replaceText(node.name, fixedName),
|
|
217
|
+
});
|
|
218
|
+
}
|
|
219
|
+
} else if (name[2] === name[2].toLowerCase()) {
|
|
220
|
+
// this includes words like `only` and `ongoing` as well as unknown handlers like `onfoobar`.
|
|
221
|
+
// Enforce using either /^on[A-Z]/ (event handler) or /^attr:on[a-z]/ (forced regular attribute)
|
|
222
|
+
// to make user intent clear and code maximally readable
|
|
223
|
+
const handlerName = `on${name[2].toUpperCase()}${name.slice(3)}`;
|
|
224
|
+
const attrName = `attr:${name}`;
|
|
225
|
+
context.report({
|
|
226
|
+
node: node.name,
|
|
227
|
+
messageId: "naming",
|
|
228
|
+
data: { name, attrName, handlerName },
|
|
229
|
+
suggest: [
|
|
230
|
+
{
|
|
231
|
+
messageId: "make-handler",
|
|
232
|
+
data: { name, handlerName },
|
|
233
|
+
fix: (fixer) =>
|
|
234
|
+
fixer.replaceText(
|
|
235
|
+
node.name,
|
|
236
|
+
handlerName,
|
|
237
|
+
),
|
|
238
|
+
},
|
|
239
|
+
{
|
|
240
|
+
messageId: "make-attr",
|
|
241
|
+
data: { name, attrName },
|
|
242
|
+
fix: (fixer) =>
|
|
243
|
+
fixer.replaceText(node.name, attrName),
|
|
244
|
+
},
|
|
245
|
+
],
|
|
246
|
+
});
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
},
|
|
250
|
+
Property(node) {
|
|
251
|
+
if (
|
|
252
|
+
context.options[0]?.warnOnSpread &&
|
|
253
|
+
node.parent?.type === "ObjectExpression" &&
|
|
254
|
+
node.parent.parent?.type === "JSXSpreadAttribute" &&
|
|
255
|
+
node.parent.parent.parent?.type === "JSXOpeningElement"
|
|
256
|
+
) {
|
|
257
|
+
const openingElement = node.parent.parent.parent;
|
|
258
|
+
if (
|
|
259
|
+
openingElement.name.type === "JSXIdentifier" &&
|
|
260
|
+
isDOMElementName(openingElement.name.name)
|
|
261
|
+
) {
|
|
262
|
+
if (
|
|
263
|
+
node.key.type === "Identifier" &&
|
|
264
|
+
node.key.name.startsWith("on")
|
|
265
|
+
) {
|
|
266
|
+
const handlerName = node.key.name;
|
|
267
|
+
// An event handler is being spread in (ex. <button {...{ onClick }} />), which doesn't
|
|
268
|
+
// actually add an event listener, just a plain attribute.
|
|
269
|
+
context.report({
|
|
270
|
+
node,
|
|
271
|
+
messageId: "spread-handler",
|
|
272
|
+
data: {
|
|
273
|
+
name: node.key.name,
|
|
274
|
+
},
|
|
275
|
+
*fix(fixer) {
|
|
276
|
+
const commaAfter =
|
|
277
|
+
sourceCode.getTokenAfter(node);
|
|
278
|
+
yield fixer.remove(
|
|
279
|
+
node.parent.properties.length === 1
|
|
280
|
+
? node.parent.parent
|
|
281
|
+
: node,
|
|
282
|
+
);
|
|
283
|
+
if (commaAfter?.value === ",") {
|
|
284
|
+
yield fixer.remove(commaAfter);
|
|
285
|
+
}
|
|
286
|
+
yield fixer.insertTextAfter(
|
|
287
|
+
node.parent.parent,
|
|
288
|
+
` ${handlerName}={${sourceCode.getText(node.value)}}`,
|
|
289
|
+
);
|
|
290
|
+
},
|
|
291
|
+
});
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
},
|
|
296
|
+
};
|
|
297
|
+
},
|
|
298
|
+
});
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
import { ESLintUtils } from "@typescript-eslint/utils";
|
|
2
|
+
|
|
3
|
+
import { getSourceCode } from "../compat.mjs";
|
|
4
|
+
import { appendImports, insertImports, removeSpecifier } from "../utils.mjs";
|
|
5
|
+
const createRule = ESLintUtils.RuleCreator.withoutDocs;
|
|
6
|
+
// Set up map of imports to module
|
|
7
|
+
const primitiveMap = new Map();
|
|
8
|
+
for (const primitive of [
|
|
9
|
+
"createSignal",
|
|
10
|
+
"createEffect",
|
|
11
|
+
"createMemo",
|
|
12
|
+
"createResource",
|
|
13
|
+
"onMount",
|
|
14
|
+
"onCleanup",
|
|
15
|
+
"onError",
|
|
16
|
+
"untrack",
|
|
17
|
+
"batch",
|
|
18
|
+
"on",
|
|
19
|
+
"createRoot",
|
|
20
|
+
"getOwner",
|
|
21
|
+
"runWithOwner",
|
|
22
|
+
"mergeProps",
|
|
23
|
+
"splitProps",
|
|
24
|
+
"useTransition",
|
|
25
|
+
"observable",
|
|
26
|
+
"from",
|
|
27
|
+
"mapArray",
|
|
28
|
+
"indexArray",
|
|
29
|
+
"createContext",
|
|
30
|
+
"useContext",
|
|
31
|
+
"children",
|
|
32
|
+
"lazy",
|
|
33
|
+
"createUniqueId",
|
|
34
|
+
"createDeferred",
|
|
35
|
+
"createRenderEffect",
|
|
36
|
+
"createComputed",
|
|
37
|
+
"createReaction",
|
|
38
|
+
"createSelector",
|
|
39
|
+
"DEV",
|
|
40
|
+
"For",
|
|
41
|
+
"Show",
|
|
42
|
+
"Switch",
|
|
43
|
+
"Match",
|
|
44
|
+
"Index",
|
|
45
|
+
"ErrorBoundary",
|
|
46
|
+
"Suspense",
|
|
47
|
+
"SuspenseList",
|
|
48
|
+
]) {
|
|
49
|
+
primitiveMap.set(primitive, "solid-js");
|
|
50
|
+
}
|
|
51
|
+
for (const primitive of [
|
|
52
|
+
"Portal",
|
|
53
|
+
"render",
|
|
54
|
+
"hydrate",
|
|
55
|
+
"renderToString",
|
|
56
|
+
"renderToStream",
|
|
57
|
+
"isServer",
|
|
58
|
+
"renderToStringAsync",
|
|
59
|
+
"generateHydrationScript",
|
|
60
|
+
"HydrationScript",
|
|
61
|
+
"Dynamic",
|
|
62
|
+
]) {
|
|
63
|
+
primitiveMap.set(primitive, "solid-js/web");
|
|
64
|
+
}
|
|
65
|
+
for (const primitive of [
|
|
66
|
+
"createStore",
|
|
67
|
+
"produce",
|
|
68
|
+
"reconcile",
|
|
69
|
+
"unwrap",
|
|
70
|
+
"createMutable",
|
|
71
|
+
"modifyMutable",
|
|
72
|
+
]) {
|
|
73
|
+
primitiveMap.set(primitive, "solid-js/store");
|
|
74
|
+
}
|
|
75
|
+
// Set up map of type imports to module
|
|
76
|
+
const typeMap = new Map();
|
|
77
|
+
for (const type of [
|
|
78
|
+
"Signal",
|
|
79
|
+
"Accessor",
|
|
80
|
+
"Setter",
|
|
81
|
+
"Resource",
|
|
82
|
+
"ResourceActions",
|
|
83
|
+
"ResourceOptions",
|
|
84
|
+
"ResourceReturn",
|
|
85
|
+
"ResourceFetcher",
|
|
86
|
+
"InitializedResourceReturn",
|
|
87
|
+
"Component",
|
|
88
|
+
"VoidProps",
|
|
89
|
+
"VoidComponent",
|
|
90
|
+
"ParentProps",
|
|
91
|
+
"ParentComponent",
|
|
92
|
+
"FlowProps",
|
|
93
|
+
"FlowComponent",
|
|
94
|
+
"ValidComponent",
|
|
95
|
+
"ComponentProps",
|
|
96
|
+
"Ref",
|
|
97
|
+
"MergeProps",
|
|
98
|
+
"SplitPrips",
|
|
99
|
+
"Context",
|
|
100
|
+
"JSX",
|
|
101
|
+
"ResolvedChildren",
|
|
102
|
+
"MatchProps",
|
|
103
|
+
]) {
|
|
104
|
+
typeMap.set(type, "solid-js");
|
|
105
|
+
}
|
|
106
|
+
for (const type of [/* "JSX", */ "MountableElement"]) {
|
|
107
|
+
typeMap.set(type, "solid-js/web");
|
|
108
|
+
}
|
|
109
|
+
for (const type of ["StoreNode", "Store", "SetStoreFunction"]) {
|
|
110
|
+
typeMap.set(type, "solid-js/store");
|
|
111
|
+
}
|
|
112
|
+
const sourceRegex = /^solid-js(?:\/web|\/store)?$/;
|
|
113
|
+
const isSource = (source) => sourceRegex.test(source);
|
|
114
|
+
export default createRule({
|
|
115
|
+
meta: {
|
|
116
|
+
type: "suggestion",
|
|
117
|
+
docs: {
|
|
118
|
+
description:
|
|
119
|
+
'Enforce consistent imports from "solid-js", "solid-js/web", and "solid-js/store".',
|
|
120
|
+
url: "https://github.com/solidjs-community/eslint-plugin-solid/blob/main/packages/eslint-plugin-solid/docs/imports.md",
|
|
121
|
+
},
|
|
122
|
+
fixable: "code",
|
|
123
|
+
schema: [],
|
|
124
|
+
messages: {
|
|
125
|
+
"prefer-source": 'Prefer importing {{name}} from "{{source}}".',
|
|
126
|
+
},
|
|
127
|
+
},
|
|
128
|
+
defaultOptions: [],
|
|
129
|
+
create(context) {
|
|
130
|
+
return {
|
|
131
|
+
ImportDeclaration(node) {
|
|
132
|
+
const source = node.source.value;
|
|
133
|
+
if (!isSource(source)) return;
|
|
134
|
+
for (const specifier of node.specifiers) {
|
|
135
|
+
if (specifier.type === "ImportSpecifier") {
|
|
136
|
+
const isType =
|
|
137
|
+
specifier.importKind === "type" ||
|
|
138
|
+
node.importKind === "type";
|
|
139
|
+
const map = isType ? typeMap : primitiveMap;
|
|
140
|
+
const correctSource = map.get(specifier.imported.name);
|
|
141
|
+
if (correctSource != null && correctSource !== source) {
|
|
142
|
+
context.report({
|
|
143
|
+
node: specifier,
|
|
144
|
+
messageId: "prefer-source",
|
|
145
|
+
data: {
|
|
146
|
+
name: specifier.imported.name,
|
|
147
|
+
source: correctSource,
|
|
148
|
+
},
|
|
149
|
+
fix(fixer) {
|
|
150
|
+
const sourceCode = getSourceCode(context);
|
|
151
|
+
const program = sourceCode.ast;
|
|
152
|
+
const correctDeclaration =
|
|
153
|
+
program.body.find(
|
|
154
|
+
(node) =>
|
|
155
|
+
node.type ===
|
|
156
|
+
"ImportDeclaration" &&
|
|
157
|
+
node.source.value ===
|
|
158
|
+
correctSource,
|
|
159
|
+
);
|
|
160
|
+
if (correctDeclaration) {
|
|
161
|
+
return [
|
|
162
|
+
removeSpecifier(
|
|
163
|
+
fixer,
|
|
164
|
+
sourceCode,
|
|
165
|
+
specifier,
|
|
166
|
+
),
|
|
167
|
+
appendImports(
|
|
168
|
+
fixer,
|
|
169
|
+
sourceCode,
|
|
170
|
+
correctDeclaration,
|
|
171
|
+
[sourceCode.getText(specifier)],
|
|
172
|
+
),
|
|
173
|
+
].filter(Boolean);
|
|
174
|
+
}
|
|
175
|
+
const firstSolidDeclaration =
|
|
176
|
+
program.body.find(
|
|
177
|
+
(node) =>
|
|
178
|
+
node.type ===
|
|
179
|
+
"ImportDeclaration" &&
|
|
180
|
+
isSource(node.source.value),
|
|
181
|
+
);
|
|
182
|
+
return [
|
|
183
|
+
removeSpecifier(
|
|
184
|
+
fixer,
|
|
185
|
+
sourceCode,
|
|
186
|
+
specifier,
|
|
187
|
+
),
|
|
188
|
+
insertImports(
|
|
189
|
+
fixer,
|
|
190
|
+
sourceCode,
|
|
191
|
+
correctSource,
|
|
192
|
+
[sourceCode.getText(specifier)],
|
|
193
|
+
firstSolidDeclaration,
|
|
194
|
+
isType,
|
|
195
|
+
),
|
|
196
|
+
];
|
|
197
|
+
},
|
|
198
|
+
});
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
},
|
|
203
|
+
};
|
|
204
|
+
},
|
|
205
|
+
});
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { ESLintUtils } from "@typescript-eslint/utils";
|
|
2
|
+
|
|
3
|
+
import { jsxGetAllProps } from "../utils.mjs";
|
|
4
|
+
const createRule = ESLintUtils.RuleCreator.withoutDocs;
|
|
5
|
+
export default createRule({
|
|
6
|
+
meta: {
|
|
7
|
+
type: "problem",
|
|
8
|
+
docs: {
|
|
9
|
+
description: "Disallow passing the same prop twice in JSX.",
|
|
10
|
+
url: "https://github.com/solidjs-community/eslint-plugin-solid/blob/main/packages/eslint-plugin-solid/docs/jsx-no-duplicate-props.md",
|
|
11
|
+
},
|
|
12
|
+
schema: [
|
|
13
|
+
{
|
|
14
|
+
type: "object",
|
|
15
|
+
properties: {
|
|
16
|
+
ignoreCase: {
|
|
17
|
+
type: "boolean",
|
|
18
|
+
description:
|
|
19
|
+
"Consider two prop names differing only by case to be the same.",
|
|
20
|
+
default: false,
|
|
21
|
+
},
|
|
22
|
+
},
|
|
23
|
+
additionalProperties: false,
|
|
24
|
+
},
|
|
25
|
+
],
|
|
26
|
+
messages: {
|
|
27
|
+
noDuplicateProps: "Duplicate props are not allowed.",
|
|
28
|
+
noDuplicateClass:
|
|
29
|
+
"Duplicate `class` props are not allowed; while it might seem to work, it can break unexpectedly. Use `classList` instead.",
|
|
30
|
+
noDuplicateChildren:
|
|
31
|
+
"Using {{used}} at the same time is not allowed.",
|
|
32
|
+
},
|
|
33
|
+
},
|
|
34
|
+
defaultOptions: [],
|
|
35
|
+
create(context) {
|
|
36
|
+
return {
|
|
37
|
+
JSXOpeningElement(node) {
|
|
38
|
+
const ignoreCase = context.options[0]?.ignoreCase ?? false;
|
|
39
|
+
const props = new Set();
|
|
40
|
+
const checkPropName = (name, node) => {
|
|
41
|
+
if (ignoreCase || name.startsWith("on")) {
|
|
42
|
+
name = name
|
|
43
|
+
.toLowerCase()
|
|
44
|
+
.replace(/^on(?:capture)?:/, "on")
|
|
45
|
+
.replace(/^(?:attr|prop):/, "");
|
|
46
|
+
}
|
|
47
|
+
if (props.has(name)) {
|
|
48
|
+
context.report({
|
|
49
|
+
node,
|
|
50
|
+
messageId:
|
|
51
|
+
name === "class"
|
|
52
|
+
? "noDuplicateClass"
|
|
53
|
+
: "noDuplicateProps",
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
props.add(name);
|
|
57
|
+
};
|
|
58
|
+
for (const [name, propNode] of jsxGetAllProps(
|
|
59
|
+
node.attributes,
|
|
60
|
+
)) {
|
|
61
|
+
checkPropName(name, propNode);
|
|
62
|
+
}
|
|
63
|
+
const hasChildrenProp = props.has("children");
|
|
64
|
+
const hasChildren = node.parent.children.length > 0;
|
|
65
|
+
const hasInnerHTML =
|
|
66
|
+
props.has("innerHTML") || props.has("innerhtml");
|
|
67
|
+
const hasTextContent =
|
|
68
|
+
props.has("textContent") || props.has("textContent");
|
|
69
|
+
const used = [
|
|
70
|
+
hasChildrenProp && "`props.children`",
|
|
71
|
+
hasChildren && "JSX children",
|
|
72
|
+
hasInnerHTML && "`props.innerHTML`",
|
|
73
|
+
hasTextContent && "`props.textContent`",
|
|
74
|
+
].filter(Boolean);
|
|
75
|
+
if (used.length > 1) {
|
|
76
|
+
context.report({
|
|
77
|
+
node,
|
|
78
|
+
messageId: "noDuplicateChildren",
|
|
79
|
+
data: {
|
|
80
|
+
used: used.join(", "),
|
|
81
|
+
},
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
},
|
|
85
|
+
};
|
|
86
|
+
},
|
|
87
|
+
});
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { ASTUtils, ESLintUtils } from "@typescript-eslint/utils";
|
|
2
|
+
|
|
3
|
+
import { getScope } from "../compat.mjs";
|
|
4
|
+
|
|
5
|
+
const createRule = ESLintUtils.RuleCreator.withoutDocs;
|
|
6
|
+
const { getStaticValue } = ASTUtils;
|
|
7
|
+
const JAVASCRIPT_PROTOCOL_PATTERN =
|
|
8
|
+
"^[\\\\u0000-\\\\u001F ]*j[\\\\r\\\\n\\\\t]*a[\\\\r\\\\n\\\\t]*v[\\\\r\\\\n\\\\t]*a[\\\\r\\\\n\\\\t]*s[\\\\r\\\\n\\\\t]*c[\\\\r\\\\n\\\\t]*r[\\\\r\\\\n\\\\t]*i[\\\\r\\\\n\\\\t]*p[\\\\r\\\\n\\\\t]*t[\\\\r\\\\n\\\\t]*:";
|
|
9
|
+
const JAVASCRIPT_PROTOCOL_REGEX = new RegExp(JAVASCRIPT_PROTOCOL_PATTERN, "i");
|
|
10
|
+
|
|
11
|
+
export const jsxNoScriptUrlRule = createRule({
|
|
12
|
+
meta: {
|
|
13
|
+
docs: {
|
|
14
|
+
description: "Disallow javascript: URLs.",
|
|
15
|
+
url: "https://github.com/solidjs-community/eslint-plugin-solid/blob/main/packages/eslint-plugin-solid/docs/jsx-no-script-url.md",
|
|
16
|
+
},
|
|
17
|
+
messages: {
|
|
18
|
+
noJSURL:
|
|
19
|
+
"For security, don't use javascript: URLs. Use event handlers instead if you can.",
|
|
20
|
+
},
|
|
21
|
+
schema: [],
|
|
22
|
+
type: "problem",
|
|
23
|
+
},
|
|
24
|
+
defaultOptions: [],
|
|
25
|
+
create(context) {
|
|
26
|
+
return {
|
|
27
|
+
JSXAttribute(node) {
|
|
28
|
+
if (node.name.type !== "JSXIdentifier" || !node.value) {
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const expression =
|
|
33
|
+
node.value.type === "JSXExpressionContainer"
|
|
34
|
+
? node.value.expression
|
|
35
|
+
: node.value;
|
|
36
|
+
const staticValue = getStaticValue(
|
|
37
|
+
expression,
|
|
38
|
+
getScope(context, node),
|
|
39
|
+
);
|
|
40
|
+
|
|
41
|
+
if (
|
|
42
|
+
staticValue &&
|
|
43
|
+
typeof staticValue.value === "string" &&
|
|
44
|
+
JAVASCRIPT_PROTOCOL_REGEX.test(staticValue.value)
|
|
45
|
+
) {
|
|
46
|
+
context.report({
|
|
47
|
+
messageId: "noJSURL",
|
|
48
|
+
node: node.value,
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
},
|
|
52
|
+
};
|
|
53
|
+
},
|
|
54
|
+
});
|