@qq99/eslint-plugin-solid 0.18.0-no-reactive-read-in-effect-callback.0
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/LICENSE +21 -0
- package/README.md +289 -0
- package/dist/chunk-4UZNJ5MS.mjs +32 -0
- package/dist/chunk-4UZNJ5MS.mjs.map +1 -0
- package/dist/chunk-EBU4C5NV.mjs +77 -0
- package/dist/chunk-EBU4C5NV.mjs.map +1 -0
- package/dist/chunk-LE6SW2M3.mjs +6231 -0
- package/dist/chunk-LE6SW2M3.mjs.map +1 -0
- package/dist/chunk-MHDQJMLE.mjs +39 -0
- package/dist/chunk-MHDQJMLE.mjs.map +1 -0
- package/dist/configs/recommended.d.mts +109 -0
- package/dist/configs/recommended.d.ts +109 -0
- package/dist/configs/recommended.js +5877 -0
- package/dist/configs/recommended.js.map +1 -0
- package/dist/configs/recommended.mjs +5 -0
- package/dist/configs/recommended.mjs.map +1 -0
- package/dist/configs/typescript.d.mts +103 -0
- package/dist/configs/typescript.d.ts +103 -0
- package/dist/configs/typescript.js +6246 -0
- package/dist/configs/typescript.js.map +1 -0
- package/dist/configs/typescript.mjs +6 -0
- package/dist/configs/typescript.mjs.map +1 -0
- package/dist/configs/v2-strict.d.mts +130 -0
- package/dist/configs/v2-strict.d.ts +130 -0
- package/dist/configs/v2-strict.js +6317 -0
- package/dist/configs/v2-strict.js.map +1 -0
- package/dist/configs/v2-strict.mjs +7 -0
- package/dist/configs/v2-strict.mjs.map +1 -0
- package/dist/configs/v2.d.mts +127 -0
- package/dist/configs/v2.d.ts +127 -0
- package/dist/configs/v2.js +6289 -0
- package/dist/configs/v2.js.map +1 -0
- package/dist/configs/v2.mjs +6 -0
- package/dist/configs/v2.mjs.map +1 -0
- package/dist/index.d.mts +740 -0
- package/dist/index.d.ts +740 -0
- package/dist/index.js +6365 -0
- package/dist/index.js.map +1 -0
- package/dist/index.mjs +42 -0
- package/dist/index.mjs.map +1 -0
- package/package.json +109 -0
- package/src/compat.ts +53 -0
- package/src/configs/recommended.ts +48 -0
- package/src/configs/typescript.ts +21 -0
- package/src/configs/v2-strict.ts +28 -0
- package/src/configs/v2.ts +68 -0
- package/src/deps.d.ts +1 -0
- package/src/index.ts +29 -0
- package/src/plugin.ts +92 -0
- package/src/rules/components-return-once.ts +209 -0
- package/src/rules/event-handlers.ts +397 -0
- package/src/rules/imports.ts +338 -0
- package/src/rules/jsx-no-duplicate-props.ts +110 -0
- package/src/rules/jsx-no-script-url.ts +61 -0
- package/src/rules/jsx-no-undef.ts +230 -0
- package/src/rules/jsx-uses-vars.ts +66 -0
- package/src/rules/no-accessor-as-prop.ts +147 -0
- package/src/rules/no-array-handlers.ts +57 -0
- package/src/rules/no-boolean-enumerated-attribute.ts +209 -0
- package/src/rules/no-browser-globals-in-server-function.ts +108 -0
- package/src/rules/no-destructure.ts +245 -0
- package/src/rules/no-innerhtml.ts +143 -0
- package/src/rules/no-invalid-server-capture.ts +104 -0
- package/src/rules/no-module-scope-reactive-primitive.ts +69 -0
- package/src/rules/no-proxy-apis.ts +96 -0
- package/src/rules/no-react-deps.ts +68 -0
- package/src/rules/no-react-specific-props.ts +60 -0
- package/src/rules/no-reactive-read-in-effect-callback.ts +570 -0
- package/src/rules/no-restated-default-options.ts +96 -0
- package/src/rules/no-single-arg-create-effect.ts +56 -0
- package/src/rules/no-store-mutation-outside-setter.ts +137 -0
- package/src/rules/no-unknown-namespaces.ts +140 -0
- package/src/rules/no-unused-signal.ts +119 -0
- package/src/rules/no-write-in-pure-computation.ts +231 -0
- package/src/rules/prefer-classlist.ts +93 -0
- package/src/rules/prefer-for.ts +93 -0
- package/src/rules/prefer-onSettled-for-side-effects.ts +101 -0
- package/src/rules/prefer-show.ts +102 -0
- package/src/rules/prefer-structured-class.ts +117 -0
- package/src/rules/reactivity.ts +1730 -0
- package/src/rules/removed-api.ts +212 -0
- package/src/rules/require-async-server-function.ts +122 -0
- package/src/rules/self-closing-comp.ts +150 -0
- package/src/rules/style-prop.ts +140 -0
- package/src/rules/valid-use-server.ts +192 -0
- package/src/rules/validate-jsx-nesting.ts +1 -0
- package/src/utils/reactive-imports.ts +382 -0
- package/src/utils/reactive-types.ts +272 -0
- package/src/utils/reactive-values.ts +162 -0
- package/src/utils.ts +442 -0
|
@@ -0,0 +1,338 @@
|
|
|
1
|
+
import { TSESTree as T, TSESLint, ESLintUtils } from "@typescript-eslint/utils";
|
|
2
|
+
import { appendImports, insertImports, isSolidV2, removeSpecifier } from "../utils";
|
|
3
|
+
import { getSourceCode } from "../compat";
|
|
4
|
+
|
|
5
|
+
const createRule = ESLintUtils.RuleCreator.withoutDocs;
|
|
6
|
+
|
|
7
|
+
// Below: create maps of imports and types to designated import source.
|
|
8
|
+
// We could mess with `Object.keys(require("solid-js"))` to generate this, but requiring it from
|
|
9
|
+
// node activates the "node" export condition, which doesn't necessarily match what users will
|
|
10
|
+
// receive i.e. through bundlers. Instead, we're manually listing all of the public exports that
|
|
11
|
+
// should be imported from "solid-js", etc.
|
|
12
|
+
// ==============
|
|
13
|
+
|
|
14
|
+
type Source = "solid-js" | "solid-js/web" | "solid-js/store";
|
|
15
|
+
|
|
16
|
+
// Set up map of imports to module
|
|
17
|
+
const primitiveMap = new Map<string, Source>();
|
|
18
|
+
for (const primitive of [
|
|
19
|
+
"createSignal",
|
|
20
|
+
"createEffect",
|
|
21
|
+
"createMemo",
|
|
22
|
+
"createResource",
|
|
23
|
+
"onMount",
|
|
24
|
+
"onCleanup",
|
|
25
|
+
"onError",
|
|
26
|
+
"untrack",
|
|
27
|
+
"batch",
|
|
28
|
+
"on",
|
|
29
|
+
"createRoot",
|
|
30
|
+
"getOwner",
|
|
31
|
+
"runWithOwner",
|
|
32
|
+
"mergeProps",
|
|
33
|
+
"splitProps",
|
|
34
|
+
"useTransition",
|
|
35
|
+
"observable",
|
|
36
|
+
"from",
|
|
37
|
+
"mapArray",
|
|
38
|
+
"indexArray",
|
|
39
|
+
"createContext",
|
|
40
|
+
"useContext",
|
|
41
|
+
"children",
|
|
42
|
+
"lazy",
|
|
43
|
+
"createUniqueId",
|
|
44
|
+
"createDeferred",
|
|
45
|
+
"createRenderEffect",
|
|
46
|
+
"createComputed",
|
|
47
|
+
"createReaction",
|
|
48
|
+
"createSelector",
|
|
49
|
+
"DEV",
|
|
50
|
+
"For",
|
|
51
|
+
"Show",
|
|
52
|
+
"Switch",
|
|
53
|
+
"Match",
|
|
54
|
+
"Index",
|
|
55
|
+
"ErrorBoundary",
|
|
56
|
+
"Suspense",
|
|
57
|
+
"SuspenseList",
|
|
58
|
+
]) {
|
|
59
|
+
primitiveMap.set(primitive, "solid-js");
|
|
60
|
+
}
|
|
61
|
+
for (const primitive of [
|
|
62
|
+
"Portal",
|
|
63
|
+
"render",
|
|
64
|
+
"hydrate",
|
|
65
|
+
"renderToString",
|
|
66
|
+
"renderToStream",
|
|
67
|
+
"isServer",
|
|
68
|
+
"renderToStringAsync",
|
|
69
|
+
"generateHydrationScript",
|
|
70
|
+
"HydrationScript",
|
|
71
|
+
"Dynamic",
|
|
72
|
+
]) {
|
|
73
|
+
primitiveMap.set(primitive, "solid-js/web");
|
|
74
|
+
}
|
|
75
|
+
for (const primitive of [
|
|
76
|
+
"createStore",
|
|
77
|
+
"produce",
|
|
78
|
+
"reconcile",
|
|
79
|
+
"unwrap",
|
|
80
|
+
"createMutable",
|
|
81
|
+
"modifyMutable",
|
|
82
|
+
]) {
|
|
83
|
+
primitiveMap.set(primitive, "solid-js/store");
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// Set up map of type imports to module
|
|
87
|
+
const typeMap = new Map<string, Source>();
|
|
88
|
+
for (const type of [
|
|
89
|
+
"Signal",
|
|
90
|
+
"Accessor",
|
|
91
|
+
"Setter",
|
|
92
|
+
"Resource",
|
|
93
|
+
"ResourceActions",
|
|
94
|
+
"ResourceOptions",
|
|
95
|
+
"ResourceReturn",
|
|
96
|
+
"ResourceFetcher",
|
|
97
|
+
"InitializedResourceReturn",
|
|
98
|
+
"Component",
|
|
99
|
+
"VoidProps",
|
|
100
|
+
"VoidComponent",
|
|
101
|
+
"ParentProps",
|
|
102
|
+
"ParentComponent",
|
|
103
|
+
"FlowProps",
|
|
104
|
+
"FlowComponent",
|
|
105
|
+
"ValidComponent",
|
|
106
|
+
"ComponentProps",
|
|
107
|
+
"Ref",
|
|
108
|
+
"MergeProps",
|
|
109
|
+
"SplitPrips",
|
|
110
|
+
"Context",
|
|
111
|
+
"JSX",
|
|
112
|
+
"ResolvedChildren",
|
|
113
|
+
"MatchProps",
|
|
114
|
+
]) {
|
|
115
|
+
typeMap.set(type, "solid-js");
|
|
116
|
+
}
|
|
117
|
+
for (const type of [/* "JSX", */ "MountableElement"]) {
|
|
118
|
+
typeMap.set(type, "solid-js/web");
|
|
119
|
+
}
|
|
120
|
+
for (const type of ["StoreNode", "Store", "SetStoreFunction"]) {
|
|
121
|
+
typeMap.set(type, "solid-js/store");
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// Solid 2.0 moved these exports into core "solid-js" (and removed the old locations in
|
|
125
|
+
// some cases). Don't flag them when imported from the source listed here, so the rule
|
|
126
|
+
// gives correct guidance for both Solid 1.x and 2.0 projects.
|
|
127
|
+
const alternateSourceMap = new Map<string, Source>([
|
|
128
|
+
["createStore", "solid-js"],
|
|
129
|
+
["reconcile", "solid-js"],
|
|
130
|
+
["Store", "solid-js"],
|
|
131
|
+
["StoreNode", "solid-js"],
|
|
132
|
+
["SetStoreFunction", "solid-js"],
|
|
133
|
+
]);
|
|
134
|
+
|
|
135
|
+
// ==============
|
|
136
|
+
// Solid 2.0: "solid-js/store" no longer exists (store exports live in core),
|
|
137
|
+
// and "solid-js/web" became the "@solidjs/web" package. When `settings.solid.
|
|
138
|
+
// version` is 2, the rule *requires* the 2.0 locations. Lists verified against
|
|
139
|
+
// the RC source (packages/solid/src/index.ts, packages/solid-web/src/index.ts).
|
|
140
|
+
type SourceV2 = "solid-js" | "@solidjs/web";
|
|
141
|
+
|
|
142
|
+
const primitiveMapV2 = new Map<string, SourceV2>();
|
|
143
|
+
for (const primitive of [
|
|
144
|
+
// reactive primitives
|
|
145
|
+
"createSignal",
|
|
146
|
+
"createMemo",
|
|
147
|
+
"createStore",
|
|
148
|
+
"createEffect",
|
|
149
|
+
"createRenderEffect",
|
|
150
|
+
"createProjection",
|
|
151
|
+
"createOptimistic",
|
|
152
|
+
"createOptimisticStore",
|
|
153
|
+
"createReaction",
|
|
154
|
+
"createRoot",
|
|
155
|
+
"createOwner",
|
|
156
|
+
"createContext",
|
|
157
|
+
"useContext",
|
|
158
|
+
"children",
|
|
159
|
+
"lazy",
|
|
160
|
+
"createUniqueId",
|
|
161
|
+
"createErrorBoundary",
|
|
162
|
+
"createLoadingBoundary",
|
|
163
|
+
"createRevealOrder",
|
|
164
|
+
// lifecycle and utilities
|
|
165
|
+
"onCleanup",
|
|
166
|
+
"onSettled",
|
|
167
|
+
"untrack",
|
|
168
|
+
"flush",
|
|
169
|
+
"merge",
|
|
170
|
+
"omit",
|
|
171
|
+
"mapArray",
|
|
172
|
+
"repeat",
|
|
173
|
+
"getOwner",
|
|
174
|
+
"getObserver",
|
|
175
|
+
"runWithOwner",
|
|
176
|
+
"isEqual",
|
|
177
|
+
"isPending",
|
|
178
|
+
"latest",
|
|
179
|
+
"action",
|
|
180
|
+
"reconcile",
|
|
181
|
+
"snapshot",
|
|
182
|
+
"resolve",
|
|
183
|
+
"refresh",
|
|
184
|
+
"flatten",
|
|
185
|
+
"deep",
|
|
186
|
+
"affects",
|
|
187
|
+
"storePath",
|
|
188
|
+
"DEV",
|
|
189
|
+
// control flow components
|
|
190
|
+
"For",
|
|
191
|
+
"Repeat",
|
|
192
|
+
"Show",
|
|
193
|
+
"Switch",
|
|
194
|
+
"Match",
|
|
195
|
+
"Errored",
|
|
196
|
+
"Loading",
|
|
197
|
+
"Reveal",
|
|
198
|
+
]) {
|
|
199
|
+
primitiveMapV2.set(primitive, "solid-js");
|
|
200
|
+
}
|
|
201
|
+
for (const primitive of [
|
|
202
|
+
"Portal",
|
|
203
|
+
"Dynamic",
|
|
204
|
+
"render",
|
|
205
|
+
"hydrate",
|
|
206
|
+
"isServer",
|
|
207
|
+
"renderToString",
|
|
208
|
+
"renderToStream",
|
|
209
|
+
// renderToStringAsync no longer exists in 2.0 (removed-api reports it)
|
|
210
|
+
"generateHydrationScript",
|
|
211
|
+
"HydrationScript",
|
|
212
|
+
]) {
|
|
213
|
+
primitiveMapV2.set(primitive, "@solidjs/web");
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
const typeMapV2 = new Map<string, SourceV2>();
|
|
217
|
+
for (const type of [
|
|
218
|
+
"Signal",
|
|
219
|
+
"Accessor",
|
|
220
|
+
"Setter",
|
|
221
|
+
"Component",
|
|
222
|
+
"Context",
|
|
223
|
+
"ResolvedChildren",
|
|
224
|
+
"Store",
|
|
225
|
+
"StoreNode",
|
|
226
|
+
"StoreSetter",
|
|
227
|
+
"StoreOptions",
|
|
228
|
+
"SignalOptions",
|
|
229
|
+
"MemoOptions",
|
|
230
|
+
"EffectOptions",
|
|
231
|
+
"Owner",
|
|
232
|
+
]) {
|
|
233
|
+
typeMapV2.set(type, "solid-js");
|
|
234
|
+
}
|
|
235
|
+
typeMapV2.set("JSX", "@solidjs/web");
|
|
236
|
+
|
|
237
|
+
const sourceRegex = /^solid-js(?:\/web|\/store)?$/;
|
|
238
|
+
const isSource = (source: string): source is Source => sourceRegex.test(source);
|
|
239
|
+
|
|
240
|
+
// The legacy "solid-js/store" and "solid-js/web" sources are deliberately
|
|
241
|
+
// excluded here: in v2 mode those modules no longer exist and solid/removed-api
|
|
242
|
+
// owns reporting/rewriting them, so this rule only polices specifier placement
|
|
243
|
+
// between the surviving modules.
|
|
244
|
+
const sourceRegexV2 = /^(?:solid-js|@solidjs\/web)$/;
|
|
245
|
+
const isSourceV2 = (source: string): source is Source | "@solidjs/web" =>
|
|
246
|
+
sourceRegexV2.test(source);
|
|
247
|
+
|
|
248
|
+
export default createRule({
|
|
249
|
+
meta: {
|
|
250
|
+
type: "suggestion",
|
|
251
|
+
docs: {
|
|
252
|
+
description:
|
|
253
|
+
'Enforce consistent imports from "solid-js", "solid-js/web", and "solid-js/store".',
|
|
254
|
+
url: "https://github.com/solidjs-community/eslint-plugin-solid/blob/main/packages/eslint-plugin-solid/docs/imports.md",
|
|
255
|
+
},
|
|
256
|
+
fixable: "code",
|
|
257
|
+
schema: [],
|
|
258
|
+
messages: {
|
|
259
|
+
"prefer-source": 'Prefer importing {{name}} from "{{source}}".',
|
|
260
|
+
},
|
|
261
|
+
},
|
|
262
|
+
defaultOptions: [],
|
|
263
|
+
create(context) {
|
|
264
|
+
const v2 = isSolidV2(context);
|
|
265
|
+
return {
|
|
266
|
+
ImportDeclaration(node) {
|
|
267
|
+
const source = node.source.value;
|
|
268
|
+
if (v2 ? !isSourceV2(source) : !isSource(source)) return;
|
|
269
|
+
|
|
270
|
+
for (const specifier of node.specifiers) {
|
|
271
|
+
if (specifier.type === "ImportSpecifier") {
|
|
272
|
+
const isType = specifier.importKind === "type" || node.importKind === "type";
|
|
273
|
+
const map = v2
|
|
274
|
+
? isType
|
|
275
|
+
? typeMapV2
|
|
276
|
+
: primitiveMapV2
|
|
277
|
+
: isType
|
|
278
|
+
? typeMap
|
|
279
|
+
: primitiveMap;
|
|
280
|
+
const importedName =
|
|
281
|
+
specifier.imported.type === "Identifier"
|
|
282
|
+
? specifier.imported.name
|
|
283
|
+
: specifier.imported.value;
|
|
284
|
+
const correctSource = map.get(importedName);
|
|
285
|
+
if (
|
|
286
|
+
correctSource != null &&
|
|
287
|
+
correctSource !== source &&
|
|
288
|
+
(v2 || alternateSourceMap.get(importedName) !== source)
|
|
289
|
+
) {
|
|
290
|
+
context.report({
|
|
291
|
+
node: specifier,
|
|
292
|
+
messageId: "prefer-source",
|
|
293
|
+
data: {
|
|
294
|
+
name: importedName,
|
|
295
|
+
source: correctSource,
|
|
296
|
+
},
|
|
297
|
+
fix(fixer) {
|
|
298
|
+
const sourceCode = getSourceCode(context);
|
|
299
|
+
const program: T.Program = sourceCode.ast;
|
|
300
|
+
const correctDeclaration = program.body.find(
|
|
301
|
+
(node) =>
|
|
302
|
+
node.type === "ImportDeclaration" && node.source.value === correctSource
|
|
303
|
+
) as T.ImportDeclaration | undefined;
|
|
304
|
+
|
|
305
|
+
if (correctDeclaration) {
|
|
306
|
+
return [
|
|
307
|
+
removeSpecifier(fixer, sourceCode, specifier),
|
|
308
|
+
appendImports(fixer, sourceCode, correctDeclaration, [
|
|
309
|
+
sourceCode.getText(specifier),
|
|
310
|
+
]),
|
|
311
|
+
].filter(Boolean) as Array<TSESLint.RuleFix>;
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
const firstSolidDeclaration = program.body.find(
|
|
315
|
+
(node) =>
|
|
316
|
+
node.type === "ImportDeclaration" &&
|
|
317
|
+
(v2 ? isSourceV2(node.source.value) : isSource(node.source.value))
|
|
318
|
+
) as T.ImportDeclaration | undefined;
|
|
319
|
+
return [
|
|
320
|
+
removeSpecifier(fixer, sourceCode, specifier),
|
|
321
|
+
insertImports(
|
|
322
|
+
fixer,
|
|
323
|
+
sourceCode,
|
|
324
|
+
correctSource,
|
|
325
|
+
[sourceCode.getText(specifier)],
|
|
326
|
+
firstSolidDeclaration,
|
|
327
|
+
node.importKind === "type"
|
|
328
|
+
),
|
|
329
|
+
];
|
|
330
|
+
},
|
|
331
|
+
});
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
},
|
|
336
|
+
};
|
|
337
|
+
},
|
|
338
|
+
});
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* FIXME: remove this comments and import when below issue is fixed.
|
|
3
|
+
* This import is necessary for type generation due to a bug in the TypeScript compiler.
|
|
4
|
+
* See: https://github.com/microsoft/TypeScript/issues/42873
|
|
5
|
+
*/
|
|
6
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
7
|
+
import type { TSESLint } from "@typescript-eslint/utils";
|
|
8
|
+
|
|
9
|
+
import { TSESTree as T, ESLintUtils } from "@typescript-eslint/utils";
|
|
10
|
+
import { isSolidV2, jsxGetAllProps } from "../utils";
|
|
11
|
+
|
|
12
|
+
const createRule = ESLintUtils.RuleCreator.withoutDocs;
|
|
13
|
+
|
|
14
|
+
/*
|
|
15
|
+
* This rule is adapted from eslint-plugin-react's jsx-no-duplicate-props rule under
|
|
16
|
+
* the MIT license, with some enhancements. Thank you for your work!
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
type MessageIds =
|
|
20
|
+
| "noDuplicateProps"
|
|
21
|
+
| "noDuplicateClass"
|
|
22
|
+
| "noDuplicateClassV2"
|
|
23
|
+
| "noDuplicateChildren";
|
|
24
|
+
type Options = [{ ignoreCase?: boolean }?];
|
|
25
|
+
|
|
26
|
+
export default createRule<Options, MessageIds>({
|
|
27
|
+
meta: {
|
|
28
|
+
type: "problem",
|
|
29
|
+
docs: {
|
|
30
|
+
description: "Disallow passing the same prop twice in JSX.",
|
|
31
|
+
url: "https://github.com/solidjs-community/eslint-plugin-solid/blob/main/packages/eslint-plugin-solid/docs/jsx-no-duplicate-props.md",
|
|
32
|
+
},
|
|
33
|
+
schema: [
|
|
34
|
+
{
|
|
35
|
+
type: "object",
|
|
36
|
+
properties: {
|
|
37
|
+
ignoreCase: {
|
|
38
|
+
type: "boolean",
|
|
39
|
+
description: "Consider two prop names differing only by case to be the same.",
|
|
40
|
+
default: false,
|
|
41
|
+
},
|
|
42
|
+
},
|
|
43
|
+
additionalProperties: false,
|
|
44
|
+
},
|
|
45
|
+
],
|
|
46
|
+
messages: {
|
|
47
|
+
noDuplicateProps: "Duplicate props are not allowed.",
|
|
48
|
+
noDuplicateClass:
|
|
49
|
+
"Duplicate `class` props are not allowed; while it might seem to work, it can break unexpectedly. Use `classList` instead.",
|
|
50
|
+
noDuplicateClassV2:
|
|
51
|
+
"Duplicate `class` props are not allowed; while it might seem to work, it can break unexpectedly. Merge them into one `class` — it accepts arrays and objects: `class={[base, { active: active() }]}`.",
|
|
52
|
+
noDuplicateChildren: "Using {{used}} at the same time is not allowed.",
|
|
53
|
+
},
|
|
54
|
+
},
|
|
55
|
+
defaultOptions: [],
|
|
56
|
+
create(context) {
|
|
57
|
+
const v2 = isSolidV2(context);
|
|
58
|
+
return {
|
|
59
|
+
JSXOpeningElement(node) {
|
|
60
|
+
const ignoreCase = context.options[0]?.ignoreCase ?? false;
|
|
61
|
+
const props = new Set();
|
|
62
|
+
const checkPropName = (name: string, node: T.Node) => {
|
|
63
|
+
if (ignoreCase || name.startsWith("on")) {
|
|
64
|
+
name = name
|
|
65
|
+
.toLowerCase()
|
|
66
|
+
.replace(/^on(?:capture)?:/, "on")
|
|
67
|
+
.replace(/^(?:attr|prop):/, "");
|
|
68
|
+
}
|
|
69
|
+
if (props.has(name)) {
|
|
70
|
+
context.report({
|
|
71
|
+
node,
|
|
72
|
+
messageId:
|
|
73
|
+
// classList was removed in 2.0; `class` takes arrays/objects itself
|
|
74
|
+
name === "class"
|
|
75
|
+
? v2
|
|
76
|
+
? "noDuplicateClassV2"
|
|
77
|
+
: "noDuplicateClass"
|
|
78
|
+
: "noDuplicateProps",
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
props.add(name);
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
for (const [name, propNode] of jsxGetAllProps(node.attributes)) {
|
|
85
|
+
checkPropName(name, propNode);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const hasChildrenProp = props.has("children");
|
|
89
|
+
const hasChildren = (node.parent as T.JSXElement | T.JSXFragment).children.length > 0;
|
|
90
|
+
const hasInnerHTML = props.has("innerHTML") || props.has("innerhtml");
|
|
91
|
+
const hasTextContent = props.has("textContent") || props.has("textcontent");
|
|
92
|
+
const used = [
|
|
93
|
+
hasChildrenProp && "`props.children`",
|
|
94
|
+
hasChildren && "JSX children",
|
|
95
|
+
hasInnerHTML && "`props.innerHTML`",
|
|
96
|
+
hasTextContent && "`props.textContent`",
|
|
97
|
+
].filter(Boolean);
|
|
98
|
+
if (used.length > 1) {
|
|
99
|
+
context.report({
|
|
100
|
+
node,
|
|
101
|
+
messageId: "noDuplicateChildren",
|
|
102
|
+
data: {
|
|
103
|
+
used: used.join(", "),
|
|
104
|
+
},
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
},
|
|
108
|
+
};
|
|
109
|
+
},
|
|
110
|
+
});
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* FIXME: remove this comments and import when below issue is fixed.
|
|
3
|
+
* This import is necessary for type generation due to a bug in the TypeScript compiler.
|
|
4
|
+
* See: https://github.com/microsoft/TypeScript/issues/42873
|
|
5
|
+
*/
|
|
6
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
7
|
+
import type { TSESLint } from "@typescript-eslint/utils";
|
|
8
|
+
|
|
9
|
+
import { ESLintUtils, ASTUtils } from "@typescript-eslint/utils";
|
|
10
|
+
import { getScope } from "../compat";
|
|
11
|
+
|
|
12
|
+
const createRule = ESLintUtils.RuleCreator.withoutDocs;
|
|
13
|
+
const { getStaticValue }: { getStaticValue: any } = ASTUtils;
|
|
14
|
+
|
|
15
|
+
// A javascript: URL can contain leading C0 control or \u0020 SPACE,
|
|
16
|
+
// and any newline or tab are filtered out as if they're not part of the URL.
|
|
17
|
+
// https://url.spec.whatwg.org/#url-parsing
|
|
18
|
+
// Tab or newline are defined as \r\n\t:
|
|
19
|
+
// https://infra.spec.whatwg.org/#ascii-tab-or-newline
|
|
20
|
+
// A C0 control is a code point in the range \u0000 NULL to \u001F
|
|
21
|
+
// INFORMATION SEPARATOR ONE, inclusive:
|
|
22
|
+
// https://infra.spec.whatwg.org/#c0-control-or-space
|
|
23
|
+
const isJavaScriptProtocol =
|
|
24
|
+
/^[\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]*:/i; // eslint-disable-line no-control-regex
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* This rule is adapted from eslint-plugin-react's jsx-no-script-url rule under the MIT license.
|
|
28
|
+
* Thank you for your work!
|
|
29
|
+
*/
|
|
30
|
+
export default createRule({
|
|
31
|
+
meta: {
|
|
32
|
+
type: "problem",
|
|
33
|
+
docs: {
|
|
34
|
+
description: "Disallow javascript: URLs.",
|
|
35
|
+
url: "https://github.com/solidjs-community/eslint-plugin-solid/blob/main/packages/eslint-plugin-solid/docs/jsx-no-script-url.md",
|
|
36
|
+
},
|
|
37
|
+
schema: [],
|
|
38
|
+
messages: {
|
|
39
|
+
noJSURL: "For security, don't use javascript: URLs. Use event handlers instead if you can.",
|
|
40
|
+
},
|
|
41
|
+
},
|
|
42
|
+
defaultOptions: [],
|
|
43
|
+
create(context) {
|
|
44
|
+
return {
|
|
45
|
+
JSXAttribute(node) {
|
|
46
|
+
if (node.name.type === "JSXIdentifier" && node.value) {
|
|
47
|
+
const link: { value: unknown } | null = getStaticValue(
|
|
48
|
+
node.value.type === "JSXExpressionContainer" ? node.value.expression : node.value,
|
|
49
|
+
getScope(context, node)
|
|
50
|
+
);
|
|
51
|
+
if (link && typeof link.value === "string" && isJavaScriptProtocol.test(link.value)) {
|
|
52
|
+
context.report({
|
|
53
|
+
node: node.value,
|
|
54
|
+
messageId: "noJSURL",
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
},
|
|
59
|
+
};
|
|
60
|
+
},
|
|
61
|
+
});
|