@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.
Files changed (90) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +289 -0
  3. package/dist/chunk-4UZNJ5MS.mjs +32 -0
  4. package/dist/chunk-4UZNJ5MS.mjs.map +1 -0
  5. package/dist/chunk-EBU4C5NV.mjs +77 -0
  6. package/dist/chunk-EBU4C5NV.mjs.map +1 -0
  7. package/dist/chunk-LE6SW2M3.mjs +6231 -0
  8. package/dist/chunk-LE6SW2M3.mjs.map +1 -0
  9. package/dist/chunk-MHDQJMLE.mjs +39 -0
  10. package/dist/chunk-MHDQJMLE.mjs.map +1 -0
  11. package/dist/configs/recommended.d.mts +109 -0
  12. package/dist/configs/recommended.d.ts +109 -0
  13. package/dist/configs/recommended.js +5877 -0
  14. package/dist/configs/recommended.js.map +1 -0
  15. package/dist/configs/recommended.mjs +5 -0
  16. package/dist/configs/recommended.mjs.map +1 -0
  17. package/dist/configs/typescript.d.mts +103 -0
  18. package/dist/configs/typescript.d.ts +103 -0
  19. package/dist/configs/typescript.js +6246 -0
  20. package/dist/configs/typescript.js.map +1 -0
  21. package/dist/configs/typescript.mjs +6 -0
  22. package/dist/configs/typescript.mjs.map +1 -0
  23. package/dist/configs/v2-strict.d.mts +130 -0
  24. package/dist/configs/v2-strict.d.ts +130 -0
  25. package/dist/configs/v2-strict.js +6317 -0
  26. package/dist/configs/v2-strict.js.map +1 -0
  27. package/dist/configs/v2-strict.mjs +7 -0
  28. package/dist/configs/v2-strict.mjs.map +1 -0
  29. package/dist/configs/v2.d.mts +127 -0
  30. package/dist/configs/v2.d.ts +127 -0
  31. package/dist/configs/v2.js +6289 -0
  32. package/dist/configs/v2.js.map +1 -0
  33. package/dist/configs/v2.mjs +6 -0
  34. package/dist/configs/v2.mjs.map +1 -0
  35. package/dist/index.d.mts +740 -0
  36. package/dist/index.d.ts +740 -0
  37. package/dist/index.js +6365 -0
  38. package/dist/index.js.map +1 -0
  39. package/dist/index.mjs +42 -0
  40. package/dist/index.mjs.map +1 -0
  41. package/package.json +109 -0
  42. package/src/compat.ts +53 -0
  43. package/src/configs/recommended.ts +48 -0
  44. package/src/configs/typescript.ts +21 -0
  45. package/src/configs/v2-strict.ts +28 -0
  46. package/src/configs/v2.ts +68 -0
  47. package/src/deps.d.ts +1 -0
  48. package/src/index.ts +29 -0
  49. package/src/plugin.ts +92 -0
  50. package/src/rules/components-return-once.ts +209 -0
  51. package/src/rules/event-handlers.ts +397 -0
  52. package/src/rules/imports.ts +338 -0
  53. package/src/rules/jsx-no-duplicate-props.ts +110 -0
  54. package/src/rules/jsx-no-script-url.ts +61 -0
  55. package/src/rules/jsx-no-undef.ts +230 -0
  56. package/src/rules/jsx-uses-vars.ts +66 -0
  57. package/src/rules/no-accessor-as-prop.ts +147 -0
  58. package/src/rules/no-array-handlers.ts +57 -0
  59. package/src/rules/no-boolean-enumerated-attribute.ts +209 -0
  60. package/src/rules/no-browser-globals-in-server-function.ts +108 -0
  61. package/src/rules/no-destructure.ts +245 -0
  62. package/src/rules/no-innerhtml.ts +143 -0
  63. package/src/rules/no-invalid-server-capture.ts +104 -0
  64. package/src/rules/no-module-scope-reactive-primitive.ts +69 -0
  65. package/src/rules/no-proxy-apis.ts +96 -0
  66. package/src/rules/no-react-deps.ts +68 -0
  67. package/src/rules/no-react-specific-props.ts +60 -0
  68. package/src/rules/no-reactive-read-in-effect-callback.ts +570 -0
  69. package/src/rules/no-restated-default-options.ts +96 -0
  70. package/src/rules/no-single-arg-create-effect.ts +56 -0
  71. package/src/rules/no-store-mutation-outside-setter.ts +137 -0
  72. package/src/rules/no-unknown-namespaces.ts +140 -0
  73. package/src/rules/no-unused-signal.ts +119 -0
  74. package/src/rules/no-write-in-pure-computation.ts +231 -0
  75. package/src/rules/prefer-classlist.ts +93 -0
  76. package/src/rules/prefer-for.ts +93 -0
  77. package/src/rules/prefer-onSettled-for-side-effects.ts +101 -0
  78. package/src/rules/prefer-show.ts +102 -0
  79. package/src/rules/prefer-structured-class.ts +117 -0
  80. package/src/rules/reactivity.ts +1730 -0
  81. package/src/rules/removed-api.ts +212 -0
  82. package/src/rules/require-async-server-function.ts +122 -0
  83. package/src/rules/self-closing-comp.ts +150 -0
  84. package/src/rules/style-prop.ts +140 -0
  85. package/src/rules/valid-use-server.ts +192 -0
  86. package/src/rules/validate-jsx-nesting.ts +1 -0
  87. package/src/utils/reactive-imports.ts +382 -0
  88. package/src/utils/reactive-types.ts +272 -0
  89. package/src/utils/reactive-values.ts +162 -0
  90. package/src/utils.ts +442 -0
package/src/utils.ts ADDED
@@ -0,0 +1,442 @@
1
+ import { TSESTree as T, TSESLint } from "@typescript-eslint/utils";
2
+ import { CompatContext, findVariable } from "./compat";
3
+
4
+ const domElementRegex = /^[a-z]/;
5
+ export const isDOMElementName = (name: string): boolean => domElementRegex.test(name);
6
+
7
+ interface HasSettings {
8
+ settings?: TSESLint.SharedConfigurationSettings;
9
+ }
10
+
11
+ /**
12
+ * Read the targeted Solid version from `settings: { solid: { version: 2 } }`.
13
+ * Returns the major version number, or `null` when unset — meaning rules should
14
+ * use permissive dual-version behavior (accept both 1.x and 2.x patterns).
15
+ */
16
+ export function getSolidVersion(context: HasSettings): number | null {
17
+ const solidSettings = context.settings?.solid;
18
+ if (solidSettings && typeof solidSettings === "object" && "version" in solidSettings) {
19
+ const version = (solidSettings as { version: unknown }).version;
20
+ if (typeof version === "number" && Number.isInteger(version) && version > 0) {
21
+ return version;
22
+ }
23
+ if (typeof version === "string") {
24
+ const major = parseInt(version, 10);
25
+ if (Number.isInteger(major) && major > 0) return major;
26
+ }
27
+ }
28
+ return null;
29
+ }
30
+
31
+ /** Whether the targeted Solid version is 2.x or later. */
32
+ export const isSolidV2 = (context: HasSettings): boolean => (getSolidVersion(context) ?? 0) >= 2;
33
+
34
+ // Matches "solid-js", its submodules ("solid-js/store", etc.), and the Solid 2.0
35
+ // "@solidjs/signals" package, which re-exports the core reactive primitives.
36
+ const DEFAULT_SOURCE_REGEX = /^(?:solid-js(?:\/?|\b)|@solidjs\/signals(?:\/?|\b))/;
37
+
38
+ /**
39
+ * The module sources whose exports are treated as Solid's primitives. Custom
40
+ * renderers and re-exporting wrappers can register their own module names via
41
+ * `settings.solid.moduleSources` (exact names, matched whole) so import-gated
42
+ * rules recognize `import { createSignal } from "my-renderer"` (#183).
43
+ */
44
+ export const getSolidSourceRegex = (context: HasSettings): RegExp => {
45
+ const solidSettings = context.settings?.solid;
46
+ const raw =
47
+ solidSettings && typeof solidSettings === "object" && "moduleSources" in solidSettings
48
+ ? (solidSettings as { moduleSources: unknown }).moduleSources
49
+ : undefined;
50
+ if (!Array.isArray(raw)) return DEFAULT_SOURCE_REGEX;
51
+ const escaped = raw
52
+ .filter((source): source is string => typeof source === "string")
53
+ .map((source) => source.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"));
54
+ if (!escaped.length) return DEFAULT_SOURCE_REGEX;
55
+ return new RegExp(`${DEFAULT_SOURCE_REGEX.source}|^(?:${escaped.join("|")})$`);
56
+ };
57
+
58
+ const propsRegex = /[pP]rops/;
59
+ export const isPropsByName = (name: string): boolean => propsRegex.test(name);
60
+
61
+ export const formatList = (strings: Array<string>): string => {
62
+ if (strings.length === 0) {
63
+ return "";
64
+ } else if (strings.length === 1) {
65
+ return `'${strings[0]}'`;
66
+ } else if (strings.length === 2) {
67
+ return `'${strings[0]}' and '${strings[1]}'`;
68
+ } else {
69
+ const last = strings.length - 1;
70
+ return `${strings
71
+ .slice(0, last)
72
+ .map((s) => `'${s}'`)
73
+ .join(", ")}, and '${strings[last]}'`;
74
+ }
75
+ };
76
+
77
+ export const find = (node: T.Node, predicate: (node: T.Node) => boolean): T.Node | null => {
78
+ let n: T.Node | undefined = node;
79
+ while (n) {
80
+ const result = predicate(n);
81
+ if (result) {
82
+ return n;
83
+ }
84
+ n = n.parent;
85
+ }
86
+ return null;
87
+ };
88
+ export function findParent<Guard extends T.Node>(
89
+ node: T.Node,
90
+ predicate: (node: T.Node) => node is Guard
91
+ ): Guard | null;
92
+ export function findParent(node: T.Node, predicate: (node: T.Node) => boolean): T.Node | null;
93
+ export function findParent(node: T.Node, predicate: (node: T.Node) => boolean): T.Node | null {
94
+ return node.parent ? find(node.parent, predicate) : null;
95
+ }
96
+
97
+ // Try to resolve a variable to its definition
98
+ export function trace(node: T.Node, context: CompatContext): T.Node {
99
+ if (node.type === "Identifier") {
100
+ const variable = findVariable(context, node);
101
+ if (!variable) return node;
102
+
103
+ const def = variable.defs[0];
104
+
105
+ // def is `undefined` for Identifier `undefined`
106
+ switch (def?.type) {
107
+ case "FunctionName":
108
+ case "ClassName":
109
+ case "ImportBinding":
110
+ return def.node;
111
+ case "Variable":
112
+ if (
113
+ ((def.node.parent as T.VariableDeclaration).kind === "const" ||
114
+ variable.references.every((ref) => ref.init || ref.isReadOnly())) &&
115
+ def.node.id.type === "Identifier" &&
116
+ def.node.init
117
+ ) {
118
+ return trace(def.node.init, context);
119
+ }
120
+ }
121
+ }
122
+ return node;
123
+ }
124
+
125
+ /** Get the relevant node when wrapped by a node that doesn't change the behavior */
126
+ export function ignoreTransparentWrappers(node: T.Node, up = false): T.Node {
127
+ if (
128
+ node.type === "TSAsExpression" ||
129
+ node.type === "TSNonNullExpression" ||
130
+ node.type === "TSSatisfiesExpression"
131
+ ) {
132
+ const next = up ? node.parent : node.expression;
133
+ if (next) {
134
+ return ignoreTransparentWrappers(next, up);
135
+ }
136
+ }
137
+ return node;
138
+ }
139
+
140
+ export type FunctionNode = T.FunctionExpression | T.ArrowFunctionExpression | T.FunctionDeclaration;
141
+ const FUNCTION_TYPES = ["FunctionExpression", "ArrowFunctionExpression", "FunctionDeclaration"];
142
+ export const isFunctionNode = (node: T.Node | null | undefined): node is FunctionNode =>
143
+ !!node && FUNCTION_TYPES.includes(node.type);
144
+
145
+ export type ProgramOrFunctionNode = FunctionNode | T.Program;
146
+ const PROGRAM_OR_FUNCTION_TYPES = ["Program"].concat(FUNCTION_TYPES);
147
+ export const isProgramOrFunctionNode = (
148
+ node: T.Node | null | undefined
149
+ ): node is ProgramOrFunctionNode => !!node && PROGRAM_OR_FUNCTION_TYPES.includes(node.type);
150
+
151
+ export const isJSXElementOrFragment = (
152
+ node: T.Node | null | undefined
153
+ ): node is T.JSXElement | T.JSXFragment =>
154
+ node?.type === "JSXElement" || node?.type === "JSXFragment";
155
+
156
+ export const getFunctionName = (node: FunctionNode): string | null => {
157
+ if (
158
+ (node.type === "FunctionDeclaration" || node.type === "FunctionExpression") &&
159
+ node.id != null
160
+ ) {
161
+ return node.id.name;
162
+ }
163
+ if (node.parent?.type === "VariableDeclarator" && node.parent.id.type === "Identifier") {
164
+ return node.parent.id.name;
165
+ }
166
+ return null;
167
+ };
168
+
169
+ export function findInScope(
170
+ node: T.Node,
171
+ scope: ProgramOrFunctionNode,
172
+ predicate: (node: T.Node) => boolean
173
+ ): T.Node | null {
174
+ const found = find(node, (node) => node === scope || predicate(node));
175
+ return found === scope && !predicate(node) ? null : found;
176
+ }
177
+
178
+ // The next two functions were adapted from "eslint-plugin-import" under the MIT license.
179
+
180
+ // Checks whether `node` has a comment (that ends) on the previous line or on
181
+ // the same line as `node` (starts).
182
+ export const getCommentBefore = (
183
+ node: T.Node,
184
+ sourceCode: TSESLint.SourceCode
185
+ ): T.Comment | undefined =>
186
+ sourceCode
187
+ .getCommentsBefore(node)
188
+ .find((comment) => comment.loc!.end.line >= node.loc!.start.line - 1);
189
+
190
+ // Checks whether `node` has a comment (that starts) on the same line as `node`
191
+ // (ends).
192
+ export const getCommentAfter = (
193
+ node: T.Node,
194
+ sourceCode: TSESLint.SourceCode
195
+ ): T.Comment | undefined =>
196
+ sourceCode
197
+ .getCommentsAfter(node)
198
+ .find((comment) => comment.loc!.start.line === node.loc!.end.line);
199
+
200
+ /**
201
+ * The leading string-literal expression statements of a program or function
202
+ * body — the directive prologue. Only statements here are directives; a
203
+ * `"use server"` string anywhere else is an ordinary expression.
204
+ */
205
+ export const getDirectivePrologue = (body: T.Statement[]): T.ExpressionStatement[] => {
206
+ const prologue: T.ExpressionStatement[] = [];
207
+ for (const statement of body) {
208
+ if (
209
+ statement.type === "ExpressionStatement" &&
210
+ statement.expression.type === "Literal" &&
211
+ typeof statement.expression.value === "string"
212
+ ) {
213
+ prologue.push(statement);
214
+ } else {
215
+ break;
216
+ }
217
+ }
218
+ return prologue;
219
+ };
220
+
221
+ const prologueHasDirective = (body: T.Statement[], directive: string): boolean =>
222
+ getDirectivePrologue(body).some(
223
+ (statement) => (statement.expression as T.StringLiteral).value === directive
224
+ );
225
+
226
+ /** Whether a function has a `"use server"` directive in its body's prologue. */
227
+ export const hasUseServerDirective = (fn: FunctionNode): boolean =>
228
+ fn.body?.type === "BlockStatement" && prologueHasDirective(fn.body.body, "use server");
229
+
230
+ /** Whether a program has a module-level `"use server"` directive. */
231
+ export const programHasUseServerDirective = (program: T.Program): boolean =>
232
+ prologueHasDirective(program.body, "use server");
233
+
234
+ /**
235
+ * Whether the `"use server"` transform would extract this function. Mirrors
236
+ * the compiler: object-literal methods, getters/setters, and class methods
237
+ * are never extracted, so directives inside them are silently ignored.
238
+ */
239
+ export type UseServerEligibility = "eligible" | "objectMethod" | "accessor" | "classMethod";
240
+ export const getUseServerEligibility = (fn: FunctionNode): UseServerEligibility => {
241
+ const parent = fn.parent;
242
+ if (parent?.type === "Property" && parent.value === fn) {
243
+ if (parent.kind !== "init") return "accessor";
244
+ if (parent.method) return "objectMethod";
245
+ }
246
+ if (parent?.type === "MethodDefinition") {
247
+ return parent.kind === "get" || parent.kind === "set" ? "accessor" : "classMethod";
248
+ }
249
+ return "eligible";
250
+ };
251
+
252
+ /**
253
+ * Whether `fn` is nested inside another function carrying a `"use server"`
254
+ * directive. The transform extracts the outermost marked function only;
255
+ * directives inside it are ignored (the code already runs on the server).
256
+ */
257
+ export const isInsideUseServerFunction = (fn: FunctionNode): boolean => {
258
+ let parent = findParent(fn, isFunctionNode);
259
+ while (parent) {
260
+ if (isFunctionNode(parent) && hasUseServerDirective(parent)) return true;
261
+ parent = findParent(parent, isFunctionNode);
262
+ }
263
+ return false;
264
+ };
265
+
266
+ /**
267
+ * Compiles a list of user-provided name patterns into a matcher. Entries may
268
+ * be exact names, glob-ish patterns using `*` wildcards ("watch*"), or
269
+ * regexes written as "/pattern/" strings. Invalid regexes fall back to exact
270
+ * string comparison.
271
+ */
272
+ export const createNameMatcher = (patterns: string[]): ((name: string) => boolean) => {
273
+ const matchers: (string | RegExp)[] = patterns.map((entry) => {
274
+ if (entry.length > 2 && entry.startsWith("/") && entry.endsWith("/")) {
275
+ try {
276
+ return new RegExp(entry.slice(1, -1));
277
+ } catch {
278
+ return entry;
279
+ }
280
+ }
281
+ if (entry.includes("*")) {
282
+ const escaped = entry
283
+ .split("*")
284
+ .map((part) => part.replace(/[.+?^${}()|[\]\\]/g, "\\$&"))
285
+ .join("[a-zA-Z0-9_$]*");
286
+ return new RegExp(`^${escaped}$`);
287
+ }
288
+ return entry;
289
+ });
290
+ return (name: string): boolean =>
291
+ matchers.some((matcher) =>
292
+ typeof matcher === "string" ? matcher === name : matcher.test(name)
293
+ );
294
+ };
295
+
296
+ /**
297
+ * Tracks imports from Solid's module sources (see DEFAULT_SOURCE_REGEX /
298
+ * getSolidSourceRegex), handling aliases. Pass `getSolidSourceRegex(context)`
299
+ * to honor the `settings.solid.moduleSources` escape.
300
+ */
301
+ export const trackImports = (fromModule: RegExp = DEFAULT_SOURCE_REGEX) => {
302
+ const importMap = new Map<string, string>();
303
+ const handleImportDeclaration = (node: T.ImportDeclaration) => {
304
+ if (fromModule.test(node.source.value)) {
305
+ for (const specifier of node.specifiers) {
306
+ if (specifier.type === "ImportSpecifier") {
307
+ const importedName =
308
+ specifier.imported.type === "Identifier"
309
+ ? specifier.imported.name
310
+ : specifier.imported.value;
311
+ importMap.set(importedName, specifier.local.name);
312
+ }
313
+ }
314
+ }
315
+ };
316
+ const matchImport = (imports: string | Array<string>, str: string): string | undefined => {
317
+ const importArr = Array.isArray(imports) ? imports : [imports];
318
+ return importArr.find((i) => importMap.get(i) === str);
319
+ };
320
+ return { matchImport, handleImportDeclaration };
321
+ };
322
+
323
+ export function appendImports(
324
+ fixer: TSESLint.RuleFixer,
325
+ sourceCode: TSESLint.SourceCode,
326
+ importNode: T.ImportDeclaration,
327
+ identifiers: Array<string>
328
+ ): TSESLint.RuleFix | null {
329
+ const identifiersString = identifiers.join(", ");
330
+ const reversedSpecifiers = importNode.specifiers.slice().reverse();
331
+ const lastSpecifier = reversedSpecifiers.find((s) => s.type === "ImportSpecifier");
332
+ if (lastSpecifier) {
333
+ // import A, { B } from 'source' => import A, { B, C, D } from 'source'
334
+ // import { B } from 'source' => import { B, C, D } from 'source'
335
+ return fixer.insertTextAfter(lastSpecifier, `, ${identifiersString}`);
336
+ }
337
+ const otherSpecifier = importNode.specifiers.find(
338
+ (s) => s.type === "ImportDefaultSpecifier" || s.type === "ImportNamespaceSpecifier"
339
+ );
340
+ if (otherSpecifier) {
341
+ // import A from 'source' => import A, { B, C, D } from 'source'
342
+ return fixer.insertTextAfter(otherSpecifier, `, { ${identifiersString} }`);
343
+ }
344
+ if (importNode.specifiers.length === 0) {
345
+ const [importToken, maybeBrace] = sourceCode.getFirstTokens(importNode, { count: 2 });
346
+ if (maybeBrace?.value === "{") {
347
+ // import {} from 'source' => import { B, C, D } from 'source'
348
+ return fixer.insertTextAfter(maybeBrace, ` ${identifiersString} `);
349
+ } else {
350
+ // import 'source' => import { B, C, D } from 'source'
351
+ return importToken
352
+ ? fixer.insertTextAfter(importToken, ` { ${identifiersString} } from`)
353
+ : null;
354
+ }
355
+ }
356
+ return null;
357
+ }
358
+ export function insertImports(
359
+ fixer: TSESLint.RuleFixer,
360
+ sourceCode: TSESLint.SourceCode,
361
+ source: string,
362
+ identifiers: Array<string>,
363
+ aboveImport?: T.ImportDeclaration,
364
+ isType = false
365
+ ): TSESLint.RuleFix {
366
+ const identifiersString = identifiers.join(", ");
367
+ const programNode: T.Program = sourceCode.ast;
368
+
369
+ // insert `import { missing, identifiers } from "source"` above given node or at top of module
370
+ const firstImport = aboveImport ?? programNode.body.find((n) => n.type === "ImportDeclaration");
371
+ if (firstImport) {
372
+ return fixer.insertTextBeforeRange(
373
+ (getCommentBefore(firstImport, sourceCode) ?? firstImport).range,
374
+ `import ${isType ? "type " : ""}{ ${identifiersString} } from "${source}";\n`
375
+ );
376
+ }
377
+ return fixer.insertTextBeforeRange(
378
+ [0, 0],
379
+ `import ${isType ? "type " : ""}{ ${identifiersString} } from "${source}";\n`
380
+ );
381
+ }
382
+
383
+ export function removeSpecifier(
384
+ fixer: TSESLint.RuleFixer,
385
+ sourceCode: TSESLint.SourceCode,
386
+ specifier: T.ImportSpecifier,
387
+ pure = true
388
+ ) {
389
+ const declaration = specifier.parent as T.ImportDeclaration;
390
+ if (declaration.specifiers.length === 1 && pure) {
391
+ return fixer.remove(declaration);
392
+ }
393
+ const maybeComma = sourceCode.getTokenAfter(specifier);
394
+ if (maybeComma?.value === ",") {
395
+ return fixer.removeRange([specifier.range[0], maybeComma.range[1]]);
396
+ }
397
+ return fixer.remove(specifier);
398
+ }
399
+
400
+ export function jsxPropName(prop: T.JSXAttribute) {
401
+ if (prop.name.type === "JSXNamespacedName") {
402
+ return `${prop.name.namespace.name}:${prop.name.name.name}`;
403
+ }
404
+
405
+ return prop.name.name;
406
+ }
407
+
408
+ type Props = T.JSXOpeningElement["attributes"];
409
+
410
+ /** Iterate through both attributes and spread object props, yielding the name and the node. */
411
+ export function* jsxGetAllProps(props: Props): Generator<[string, T.Node]> {
412
+ for (const attr of props) {
413
+ if (attr.type === "JSXSpreadAttribute" && attr.argument.type === "ObjectExpression") {
414
+ for (const property of attr.argument.properties) {
415
+ if (property.type === "Property") {
416
+ if (property.key.type === "Identifier") {
417
+ yield [property.key.name, property.key];
418
+ } else if (property.key.type === "Literal") {
419
+ yield [String(property.key.value), property.key];
420
+ }
421
+ }
422
+ }
423
+ } else if (attr.type === "JSXAttribute") {
424
+ yield [jsxPropName(attr), attr.name];
425
+ }
426
+ }
427
+ }
428
+
429
+ /** Returns whether an element has a prop, checking spread object props. */
430
+ export function jsxHasProp(props: Props, prop: string) {
431
+ for (const [p] of jsxGetAllProps(props)) {
432
+ if (p === prop) return true;
433
+ }
434
+ return false;
435
+ }
436
+
437
+ /** Get a JSXAttribute, excluding spread props. */
438
+ export function jsxGetProp(props: Props, prop: string) {
439
+ return props.find(
440
+ (attribute) => attribute.type !== "JSXSpreadAttribute" && prop === jsxPropName(attribute)
441
+ ) as T.JSXAttribute | undefined;
442
+ }