UNPKG

eslint-plugin-solid

Version:
1 lines 235 kB
{"version":3,"sources":["../src/compat.ts","../src/utils.ts","../src/rules/components-return-once.ts","../src/rules/event-handlers.ts","../src/rules/imports.ts","../src/rules/jsx-no-duplicate-props.ts","../src/rules/jsx-no-script-url.ts","../src/rules/jsx-no-undef.ts","../src/rules/jsx-uses-vars.ts","../src/rules/no-destructure.ts","../src/rules/no-innerhtml.ts","../src/rules/no-proxy-apis.ts","../src/rules/no-react-deps.ts","../src/rules/no-react-specific-props.ts","../src/rules/no-unknown-namespaces.ts","../src/rules/prefer-classlist.ts","../src/rules/prefer-for.ts","../src/rules/prefer-show.ts","../src/rules/reactivity.ts","../src/rules/self-closing-comp.ts","../src/rules/style-prop.ts","../src/rules/no-array-handlers.ts","../package.json","../src/plugin.ts","../src/configs/recommended.ts"],"sourcesContent":["import { type TSESLint, type TSESTree, ASTUtils } from \"@typescript-eslint/utils\";\n\nexport type CompatContext =\n | {\n sourceCode: Readonly<TSESLint.SourceCode>;\n getSourceCode: undefined;\n getScope: undefined;\n markVariableAsUsed: undefined;\n }\n | {\n sourceCode?: Readonly<TSESLint.SourceCode>;\n getSourceCode: () => Readonly<TSESLint.SourceCode>;\n getScope: () => TSESLint.Scope.Scope;\n markVariableAsUsed: (name: string) => void;\n };\n\nexport function getSourceCode(context: CompatContext) {\n if (typeof context.getSourceCode === \"function\") {\n return context.getSourceCode();\n }\n return context.sourceCode;\n}\n\nexport function getScope(context: CompatContext, node: TSESTree.Node): TSESLint.Scope.Scope {\n const sourceCode = getSourceCode(context);\n\n if (typeof sourceCode.getScope === \"function\") {\n return sourceCode.getScope(node); // >= v8, I think\n }\n if (typeof context.getScope === \"function\") {\n return context.getScope();\n }\n return context.sourceCode.getScope(node);\n}\n\nexport function findVariable(\n context: CompatContext,\n node: TSESTree.Identifier\n): TSESLint.Scope.Variable | null {\n return ASTUtils.findVariable(getScope(context, node), node);\n}\n\nexport function markVariableAsUsed(\n context: CompatContext,\n name: string,\n node: TSESTree.Node\n): void {\n if (typeof context.markVariableAsUsed === \"function\") {\n context.markVariableAsUsed(name);\n } else {\n getSourceCode(context).markVariableAsUsed(name, node);\n }\n}\n","import { TSESTree as T, TSESLint } from \"@typescript-eslint/utils\";\nimport { CompatContext, findVariable } from \"./compat\";\n\nconst domElementRegex = /^[a-z]/;\nexport const isDOMElementName = (name: string): boolean => domElementRegex.test(name);\n\nconst propsRegex = /[pP]rops/;\nexport const isPropsByName = (name: string): boolean => propsRegex.test(name);\n\nexport const formatList = (strings: Array<string>): string => {\n if (strings.length === 0) {\n return \"\";\n } else if (strings.length === 1) {\n return `'${strings[0]}'`;\n } else if (strings.length === 2) {\n return `'${strings[0]}' and '${strings[1]}'`;\n } else {\n const last = strings.length - 1;\n return `${strings\n .slice(0, last)\n .map((s) => `'${s}'`)\n .join(\", \")}, and '${strings[last]}'`;\n }\n};\n\nexport const find = (node: T.Node, predicate: (node: T.Node) => boolean): T.Node | null => {\n let n: T.Node | undefined = node;\n while (n) {\n const result = predicate(n);\n if (result) {\n return n;\n }\n n = n.parent;\n }\n return null;\n};\nexport function findParent<Guard extends T.Node>(\n node: T.Node,\n predicate: (node: T.Node) => node is Guard\n): Guard | null;\nexport function findParent(node: T.Node, predicate: (node: T.Node) => boolean): T.Node | null;\nexport function findParent(node: T.Node, predicate: (node: T.Node) => boolean): T.Node | null {\n return node.parent ? find(node.parent, predicate) : null;\n}\n\n// Try to resolve a variable to its definition\nexport function trace(node: T.Node, context: CompatContext): T.Node {\n if (node.type === \"Identifier\") {\n const variable = findVariable(context, node);\n if (!variable) return node;\n\n const def = variable.defs[0];\n\n // def is `undefined` for Identifier `undefined`\n switch (def?.type) {\n case \"FunctionName\":\n case \"ClassName\":\n case \"ImportBinding\":\n return def.node;\n case \"Variable\":\n if (\n ((def.node.parent as T.VariableDeclaration).kind === \"const\" ||\n variable.references.every((ref) => ref.init || ref.isReadOnly())) &&\n def.node.id.type === \"Identifier\" &&\n def.node.init\n ) {\n return trace(def.node.init, context);\n }\n }\n }\n return node;\n}\n\n/** Get the relevant node when wrapped by a node that doesn't change the behavior */\nexport function ignoreTransparentWrappers(node: T.Node, up = false): T.Node {\n if (\n node.type === \"TSAsExpression\" ||\n node.type === \"TSNonNullExpression\" ||\n node.type === \"TSSatisfiesExpression\"\n ) {\n const next = up ? node.parent : node.expression;\n if (next) {\n return ignoreTransparentWrappers(next, up);\n }\n }\n return node;\n}\n\nexport type FunctionNode = T.FunctionExpression | T.ArrowFunctionExpression | T.FunctionDeclaration;\nconst FUNCTION_TYPES = [\"FunctionExpression\", \"ArrowFunctionExpression\", \"FunctionDeclaration\"];\nexport const isFunctionNode = (node: T.Node | null | undefined): node is FunctionNode =>\n !!node && FUNCTION_TYPES.includes(node.type);\n\nexport type ProgramOrFunctionNode = FunctionNode | T.Program;\nconst PROGRAM_OR_FUNCTION_TYPES = [\"Program\"].concat(FUNCTION_TYPES);\nexport const isProgramOrFunctionNode = (\n node: T.Node | null | undefined\n): node is ProgramOrFunctionNode => !!node && PROGRAM_OR_FUNCTION_TYPES.includes(node.type);\n\nexport const isJSXElementOrFragment = (\n node: T.Node | null | undefined\n): node is T.JSXElement | T.JSXFragment =>\n node?.type === \"JSXElement\" || node?.type === \"JSXFragment\";\n\nexport const getFunctionName = (node: FunctionNode): string | null => {\n if (\n (node.type === \"FunctionDeclaration\" || node.type === \"FunctionExpression\") &&\n node.id != null\n ) {\n return node.id.name;\n }\n if (node.parent?.type === \"VariableDeclarator\" && node.parent.id.type === \"Identifier\") {\n return node.parent.id.name;\n }\n return null;\n};\n\nexport function findInScope(\n node: T.Node,\n scope: ProgramOrFunctionNode,\n predicate: (node: T.Node) => boolean\n): T.Node | null {\n const found = find(node, (node) => node === scope || predicate(node));\n return found === scope && !predicate(node) ? null : found;\n}\n\n// The next two functions were adapted from \"eslint-plugin-import\" under the MIT license.\n\n// Checks whether `node` has a comment (that ends) on the previous line or on\n// the same line as `node` (starts).\nexport const getCommentBefore = (\n node: T.Node,\n sourceCode: TSESLint.SourceCode\n): T.Comment | undefined =>\n sourceCode\n .getCommentsBefore(node)\n .find((comment) => comment.loc!.end.line >= node.loc!.start.line - 1);\n\n// Checks whether `node` has a comment (that starts) on the same line as `node`\n// (ends).\nexport const getCommentAfter = (\n node: T.Node,\n sourceCode: TSESLint.SourceCode\n): T.Comment | undefined =>\n sourceCode\n .getCommentsAfter(node)\n .find((comment) => comment.loc!.start.line === node.loc!.end.line);\n\nexport const trackImports = (fromModule = /^solid-js(?:\\/?|\\b)/) => {\n const importMap = new Map<string, string>();\n const handleImportDeclaration = (node: T.ImportDeclaration) => {\n if (fromModule.test(node.source.value)) {\n for (const specifier of node.specifiers) {\n if (specifier.type === \"ImportSpecifier\") {\n importMap.set(specifier.imported.name, specifier.local.name);\n }\n }\n }\n };\n const matchImport = (imports: string | Array<string>, str: string): string | undefined => {\n const importArr = Array.isArray(imports) ? imports : [imports];\n return importArr.find((i) => importMap.get(i) === str);\n };\n return { matchImport, handleImportDeclaration };\n};\n\nexport function appendImports(\n fixer: TSESLint.RuleFixer,\n sourceCode: TSESLint.SourceCode,\n importNode: T.ImportDeclaration,\n identifiers: Array<string>\n): TSESLint.RuleFix | null {\n const identifiersString = identifiers.join(\", \");\n const reversedSpecifiers = importNode.specifiers.slice().reverse();\n const lastSpecifier = reversedSpecifiers.find((s) => s.type === \"ImportSpecifier\");\n if (lastSpecifier) {\n // import A, { B } from 'source' => import A, { B, C, D } from 'source'\n // import { B } from 'source' => import { B, C, D } from 'source'\n return fixer.insertTextAfter(lastSpecifier, `, ${identifiersString}`);\n }\n const otherSpecifier = importNode.specifiers.find(\n (s) => s.type === \"ImportDefaultSpecifier\" || s.type === \"ImportNamespaceSpecifier\"\n );\n if (otherSpecifier) {\n // import A from 'source' => import A, { B, C, D } from 'source'\n return fixer.insertTextAfter(otherSpecifier, `, { ${identifiersString} }`);\n }\n if (importNode.specifiers.length === 0) {\n const [importToken, maybeBrace] = sourceCode.getFirstTokens(importNode, { count: 2 });\n if (maybeBrace?.value === \"{\") {\n // import {} from 'source' => import { B, C, D } from 'source'\n return fixer.insertTextAfter(maybeBrace, ` ${identifiersString} `);\n } else {\n // import 'source' => import { B, C, D } from 'source'\n return importToken\n ? fixer.insertTextAfter(importToken, ` { ${identifiersString} } from`)\n : null;\n }\n }\n return null;\n}\nexport function insertImports(\n fixer: TSESLint.RuleFixer,\n sourceCode: TSESLint.SourceCode,\n source: string,\n identifiers: Array<string>,\n aboveImport?: T.ImportDeclaration,\n isType = false\n): TSESLint.RuleFix {\n const identifiersString = identifiers.join(\", \");\n const programNode: T.Program = sourceCode.ast;\n\n // insert `import { missing, identifiers } from \"source\"` above given node or at top of module\n const firstImport = aboveImport ?? programNode.body.find((n) => n.type === \"ImportDeclaration\");\n if (firstImport) {\n return fixer.insertTextBeforeRange(\n (getCommentBefore(firstImport, sourceCode) ?? firstImport).range,\n `import ${isType ? \"type \" : \"\"}{ ${identifiersString} } from \"${source}\";\\n`\n );\n }\n return fixer.insertTextBeforeRange(\n [0, 0],\n `import ${isType ? \"type \" : \"\"}{ ${identifiersString} } from \"${source}\";\\n`\n );\n}\n\nexport function removeSpecifier(\n fixer: TSESLint.RuleFixer,\n sourceCode: TSESLint.SourceCode,\n specifier: T.ImportSpecifier,\n pure = true\n) {\n const declaration = specifier.parent as T.ImportDeclaration;\n if (declaration.specifiers.length === 1 && pure) {\n return fixer.remove(declaration);\n }\n const maybeComma = sourceCode.getTokenAfter(specifier);\n if (maybeComma?.value === \",\") {\n return fixer.removeRange([specifier.range[0], maybeComma.range[1]]);\n }\n return fixer.remove(specifier);\n}\n\nexport function jsxPropName(prop: T.JSXAttribute) {\n if (prop.name.type === \"JSXNamespacedName\") {\n return `${prop.name.namespace.name}:${prop.name.name.name}`;\n }\n\n return prop.name.name;\n}\n\ntype Props = T.JSXOpeningElement[\"attributes\"];\n\n/** Iterate through both attributes and spread object props, yielding the name and the node. */\nexport function* jsxGetAllProps(props: Props): Generator<[string, T.Node]> {\n for (const attr of props) {\n if (attr.type === \"JSXSpreadAttribute\" && attr.argument.type === \"ObjectExpression\") {\n for (const property of attr.argument.properties) {\n if (property.type === \"Property\") {\n if (property.key.type === \"Identifier\") {\n yield [property.key.name, property.key];\n } else if (property.key.type === \"Literal\") {\n yield [String(property.key.value), property.key];\n }\n }\n }\n } else if (attr.type === \"JSXAttribute\") {\n yield [jsxPropName(attr), attr.name];\n }\n }\n}\n\n/** Returns whether an element has a prop, checking spread object props. */\nexport function jsxHasProp(props: Props, prop: string) {\n for (const [p] of jsxGetAllProps(props)) {\n if (p === prop) return true;\n }\n return false;\n}\n\n/** Get a JSXAttribute, excluding spread props. */\nexport function jsxGetProp(props: Props, prop: string) {\n return props.find(\n (attribute) => attribute.type !== \"JSXSpreadAttribute\" && prop === jsxPropName(attribute)\n ) as T.JSXAttribute | undefined;\n}\n","/**\n * FIXME: remove this comments and import when below issue is fixed.\n * This import is necessary for type generation due to a bug in the TypeScript compiler.\n * See: https://github.com/microsoft/TypeScript/issues/42873\n */\n// eslint-disable-next-line @typescript-eslint/no-unused-vars\nimport type { TSESLint } from \"@typescript-eslint/utils\";\n\nimport { TSESTree as T, ESLintUtils } from \"@typescript-eslint/utils\";\nimport { getFunctionName, type FunctionNode } from \"../utils\";\nimport { getSourceCode } from \"../compat\";\n\nconst createRule = ESLintUtils.RuleCreator.withoutDocs;\n\nconst isNothing = (node?: T.Node): boolean => {\n if (!node) {\n return true;\n }\n switch (node.type) {\n case \"Literal\":\n return ([null, undefined, false, \"\"] as Array<unknown>).includes(node.value);\n case \"JSXFragment\":\n return !node.children || node.children.every(isNothing);\n default:\n return false;\n }\n};\n\nconst getLineLength = (loc: T.SourceLocation) => loc.end.line - loc.start.line + 1;\n\nexport default createRule({\n meta: {\n type: \"problem\",\n docs: {\n description:\n \"Disallow early returns in components. Solid components only run once, and so conditionals should be inside JSX.\",\n url: \"https://github.com/solidjs-community/eslint-plugin-solid/blob/main/packages/eslint-plugin-solid/docs/components-return-once.md\",\n },\n fixable: \"code\",\n schema: [],\n messages: {\n noEarlyReturn:\n \"Solid components run once, so an early return breaks reactivity. Move the condition inside a JSX element, such as a fragment or <Show />.\",\n noConditionalReturn:\n \"Solid components run once, so a conditional return breaks reactivity. Move the condition inside a JSX element, such as a fragment or <Show />.\",\n },\n },\n defaultOptions: [],\n create(context) {\n const functionStack: Array<{\n /** switched to true by :exit if the current function is detected to be a component */\n isComponent: boolean;\n lastReturn: T.ReturnStatement | undefined;\n earlyReturns: Array<T.ReturnStatement>;\n }> = [];\n const putIntoJSX = (node: T.Node): string => {\n const text = getSourceCode(context).getText(node);\n return node.type === \"JSXElement\" || node.type === \"JSXFragment\" ? text : `{${text}}`;\n };\n const currentFunction = () => functionStack[functionStack.length - 1];\n const onFunctionEnter = (node: FunctionNode) => {\n let lastReturn: T.ReturnStatement | undefined;\n if (node.body.type === \"BlockStatement\") {\n // find last statement, ignoring function/class/variable declarations (hoisting)\n const last = node.body.body.findLast((node) => !node.type.endsWith(\"Declaration\"));\n // if it's a return, store it\n if (last && last.type === \"ReturnStatement\") {\n lastReturn = last;\n }\n }\n functionStack.push({ isComponent: false, lastReturn, earlyReturns: [] });\n };\n\n const onFunctionExit = (node: FunctionNode) => {\n if (\n // \"render props\" aren't components\n getFunctionName(node)?.match(/^[a-z]/) ||\n node.parent?.type === \"JSXExpressionContainer\" ||\n // ignore createMemo(() => conditional JSX), report HOC(() => conditional JSX)\n (node.parent?.type === \"CallExpression\" &&\n node.parent.arguments.some((n) => n === node) &&\n !(node.parent.callee as T.Identifier).name?.match(/^[A-Z]/))\n ) {\n currentFunction().isComponent = false;\n }\n if (currentFunction().isComponent) {\n // Warn on each early return\n currentFunction().earlyReturns.forEach((earlyReturn) => {\n context.report({\n node: earlyReturn,\n messageId: \"noEarlyReturn\",\n });\n });\n\n const argument = currentFunction().lastReturn?.argument;\n if (argument?.type === \"ConditionalExpression\") {\n const sourceCode = getSourceCode(context);\n context.report({\n node: argument.parent!,\n messageId: \"noConditionalReturn\",\n fix: (fixer) => {\n const { test, consequent, alternate } = argument;\n const conditions = [{ test, consequent }];\n let fallback = alternate;\n\n while (fallback.type === \"ConditionalExpression\") {\n conditions.push({ test: fallback.test, consequent: fallback.consequent });\n fallback = fallback.alternate;\n }\n\n if (conditions.length >= 2) {\n // we have a nested ternary, use <Switch><Match /></Switch>\n const fallbackStr = !isNothing(fallback)\n ? ` fallback={${sourceCode.getText(fallback)}}`\n : \"\";\n return fixer.replaceText(\n argument,\n `<Switch${fallbackStr}>\\n${conditions\n .map(\n ({ test, consequent }) =>\n `<Match when={${sourceCode.getText(test)}}>${putIntoJSX(\n consequent\n )}</Match>`\n )\n .join(\"\\n\")}\\n</Switch>`\n );\n }\n if (isNothing(consequent)) {\n // we have a single ternary and the consequent is nothing. Negate the condition and use a <Show>.\n return fixer.replaceText(\n argument,\n `<Show when={!(${sourceCode.getText(test)})}>${putIntoJSX(alternate)}</Show>`\n );\n }\n if (\n isNothing(fallback) ||\n getLineLength(consequent.loc) >= getLineLength(alternate.loc) * 1.5\n ) {\n // we have a standard ternary, and the alternate is a bit shorter in LOC than the consequent, which\n // should be enough to tell that it's logically a fallback instead of an equal branch.\n const fallbackStr = !isNothing(fallback)\n ? ` fallback={${sourceCode.getText(fallback)}}`\n : \"\";\n return fixer.replaceText(\n argument,\n `<Show when={${sourceCode.getText(test)}}${fallbackStr}>${putIntoJSX(\n consequent\n )}</Show>`\n );\n }\n\n // we have a standard ternary, but no signal from the user as to which branch is the \"fallback\" and\n // which is the children. Move the whole conditional inside a JSX fragment.\n return fixer.replaceText(argument, `<>${putIntoJSX(argument)}</>`);\n },\n });\n } else if (argument?.type === \"LogicalExpression\") {\n if (argument.operator === \"&&\") {\n const sourceCode = getSourceCode(context);\n // we have a `return condition && expression`--put that in a <Show />\n context.report({\n node: argument,\n messageId: \"noConditionalReturn\",\n fix: (fixer) => {\n const { left: test, right: consequent } = argument;\n return fixer.replaceText(\n argument,\n `<Show when={${sourceCode.getText(test)}}>${putIntoJSX(consequent)}</Show>`\n );\n },\n });\n } else {\n // we have some other kind of conditional, warn\n context.report({\n node: argument,\n messageId: \"noConditionalReturn\",\n });\n }\n }\n }\n\n // Pop on exit\n functionStack.pop();\n };\n return {\n FunctionDeclaration: onFunctionEnter,\n FunctionExpression: onFunctionEnter,\n ArrowFunctionExpression: onFunctionEnter,\n \"FunctionDeclaration:exit\": onFunctionExit,\n \"FunctionExpression:exit\": onFunctionExit,\n \"ArrowFunctionExpression:exit\": onFunctionExit,\n JSXElement() {\n if (functionStack.length) {\n currentFunction().isComponent = true;\n }\n },\n JSXFragment() {\n if (functionStack.length) {\n currentFunction().isComponent = true;\n }\n },\n ReturnStatement(node) {\n if (functionStack.length && node !== currentFunction().lastReturn) {\n currentFunction().earlyReturns.push(node);\n }\n },\n };\n },\n});\n","/**\n * FIXME: remove this comments and import when below issue is fixed.\n * This import is necessary for type generation due to a bug in the TypeScript compiler.\n * See: https://github.com/microsoft/TypeScript/issues/42873\n */\n// eslint-disable-next-line @typescript-eslint/no-unused-vars\nimport type { TSESLint } from \"@typescript-eslint/utils\";\n\nimport { TSESTree as T, ESLintUtils, ASTUtils } from \"@typescript-eslint/utils\";\nimport { isDOMElementName } from \"../utils\";\nimport { getScope, getSourceCode } from \"../compat\";\n\nconst createRule = ESLintUtils.RuleCreator.withoutDocs;\nconst { getStaticValue } = ASTUtils;\n\nconst COMMON_EVENTS = [\n \"onAnimationEnd\",\n \"onAnimationIteration\",\n \"onAnimationStart\",\n \"onBeforeInput\",\n \"onBlur\",\n \"onChange\",\n \"onClick\",\n \"onContextMenu\",\n \"onCopy\",\n \"onCut\",\n \"onDblClick\",\n \"onDrag\",\n \"onDragEnd\",\n \"onDragEnter\",\n \"onDragExit\",\n \"onDragLeave\",\n \"onDragOver\",\n \"onDragStart\",\n \"onDrop\",\n \"onError\",\n \"onFocus\",\n \"onFocusIn\",\n \"onFocusOut\",\n \"onGotPointerCapture\",\n \"onInput\",\n \"onInvalid\",\n \"onKeyDown\",\n \"onKeyPress\",\n \"onKeyUp\",\n \"onLoad\",\n \"onLostPointerCapture\",\n \"onMouseDown\",\n \"onMouseEnter\",\n \"onMouseLeave\",\n \"onMouseMove\",\n \"onMouseOut\",\n \"onMouseOver\",\n \"onMouseUp\",\n \"onPaste\",\n \"onPointerCancel\",\n \"onPointerDown\",\n \"onPointerEnter\",\n \"onPointerLeave\",\n \"onPointerMove\",\n \"onPointerOut\",\n \"onPointerOver\",\n \"onPointerUp\",\n \"onReset\",\n \"onScroll\",\n \"onSelect\",\n \"onSubmit\",\n \"onToggle\",\n \"onTouchCancel\",\n \"onTouchEnd\",\n \"onTouchMove\",\n \"onTouchStart\",\n \"onTransitionEnd\",\n \"onWheel\",\n] as const;\ntype CommonEvent = (typeof COMMON_EVENTS)[number];\n\nconst COMMON_EVENTS_MAP = new Map<string, CommonEvent>(\n (function* () {\n for (const event of COMMON_EVENTS) {\n yield [event.toLowerCase(), event] as const;\n }\n })()\n);\n\nconst NONSTANDARD_EVENTS_MAP = {\n ondoubleclick: \"onDblClick\",\n};\n\nconst isCommonHandlerName = (\n lowercaseHandlerName: string\n): lowercaseHandlerName is Lowercase<CommonEvent> => COMMON_EVENTS_MAP.has(lowercaseHandlerName);\nconst getCommonEventHandlerName = (lowercaseHandlerName: Lowercase<CommonEvent>): CommonEvent =>\n COMMON_EVENTS_MAP.get(lowercaseHandlerName)!;\n\nconst isNonstandardEventName = (\n lowercaseEventName: string\n): lowercaseEventName is keyof typeof NONSTANDARD_EVENTS_MAP =>\n Boolean((NONSTANDARD_EVENTS_MAP as Record<string, string>)[lowercaseEventName]);\nconst getStandardEventHandlerName = (lowercaseEventName: keyof typeof NONSTANDARD_EVENTS_MAP) =>\n NONSTANDARD_EVENTS_MAP[lowercaseEventName];\n\ntype MessageIds =\n | \"naming\"\n | \"capitalization\"\n | \"nonstandard\"\n | \"make-handler\"\n | \"make-attr\"\n | \"detected-attr\"\n | \"spread-handler\";\ntype Options = [{ ignoreCase?: boolean; warnOnSpread?: boolean }?];\n\nexport default createRule<Options, MessageIds>({\n meta: {\n type: \"problem\",\n docs: {\n description:\n \"Enforce naming DOM element event handlers consistently and prevent Solid's analysis from misunderstanding whether a prop should be an event handler.\",\n url: \"https://github.com/solidjs-community/eslint-plugin-solid/blob/main/packages/eslint-plugin-solid/docs/event-handlers.md\",\n },\n fixable: \"code\",\n hasSuggestions: true,\n schema: [\n {\n type: \"object\",\n properties: {\n ignoreCase: {\n type: \"boolean\",\n description:\n \"if true, don't warn on ambiguously named event handlers like `onclick` or `onchange`\",\n default: false,\n },\n warnOnSpread: {\n type: \"boolean\",\n description:\n \"if true, warn when spreading event handlers onto JSX. Enable for Solid < v1.6.\",\n default: false,\n },\n },\n additionalProperties: false,\n },\n ],\n messages: {\n \"detected-attr\":\n '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}}.',\n naming:\n \"The {{name}} prop is ambiguous. If it is an event handler, change it to {{handlerName}}. If it is an attribute, change it to {{attrName}}.\",\n capitalization: \"The {{name}} prop should be renamed to {{fixedName}} for readability.\",\n nonstandard:\n \"The {{name}} prop should be renamed to {{fixedName}}, because it's not a standard event handler.\",\n \"make-handler\": \"Change the {{name}} prop to {{handlerName}}.\",\n \"make-attr\": \"Change the {{name}} prop to {{attrName}}.\",\n \"spread-handler\":\n \"The {{name}} prop should be added as a JSX attribute, not spread in. Solid doesn't add listeners when spreading into JSX.\",\n },\n },\n defaultOptions: [],\n create(context) {\n const sourceCode = getSourceCode(context);\n\n return {\n JSXAttribute(node) {\n const openingElement = node.parent as T.JSXOpeningElement;\n if (\n openingElement.name.type !== \"JSXIdentifier\" ||\n !isDOMElementName(openingElement.name.name)\n ) {\n return; // bail if this is not a DOM/SVG element or web component\n }\n\n if (node.name.type === \"JSXNamespacedName\") {\n return; // bail early on attr:, on:, oncapture:, etc. props\n }\n\n // string name of the name node\n const { name } = node.name;\n\n if (!/^on[a-zA-Z]/.test(name)) {\n return; // bail if Solid doesn't consider the prop name an event handler\n }\n\n let staticValue: ReturnType<typeof getStaticValue> = null;\n if (\n node.value?.type === \"JSXExpressionContainer\" &&\n node.value.expression.type !== \"JSXEmptyExpression\" &&\n node.value.expression.type !== \"ArrayExpression\" && // array syntax prevents inlining\n (staticValue = getStaticValue(node.value.expression, getScope(context, node))) !== null &&\n (typeof staticValue.value === \"string\" || typeof staticValue.value === \"number\")\n ) {\n // One of the first things Solid (actually babel-plugin-dom-expressions) does with an\n // attribute is determine if it can be inlined into a template string instead of\n // injected programmatically. It runs\n // `attribute.get(\"value\").get(\"expression\").evaluate().value` on attributes with\n // JSXExpressionContainers, and if the statically evaluated value is a string or number,\n // it inlines it. This runs even for attributes that follow the naming convention for\n // event handlers. By starting an attribute name with \"on\", the user has signalled that\n // they intend the attribute to be an event handler. If the attribute value would be\n // inlined, report that.\n // https://github.com/ryansolid/dom-expressions/blob/cb3be7558c731e2a442e9c7e07d25373c40cf2be/packages/babel-plugin-jsx-dom-expressions/src/dom/element.js#L347\n context.report({\n node,\n messageId: \"detected-attr\",\n data: {\n name,\n staticValue: staticValue.value,\n },\n });\n } else if (node.value === null || node.value?.type === \"Literal\") {\n // Check for same as above for literal values\n context.report({\n node,\n messageId: \"detected-attr\",\n data: {\n name,\n staticValue: node.value !== null ? node.value.value : true,\n },\n });\n } else if (!context.options[0]?.ignoreCase) {\n const lowercaseHandlerName = name.toLowerCase();\n if (isNonstandardEventName(lowercaseHandlerName)) {\n const fixedName = getStandardEventHandlerName(lowercaseHandlerName);\n context.report({\n node: node.name,\n messageId: \"nonstandard\",\n data: { name, fixedName },\n fix: (fixer) => fixer.replaceText(node.name, fixedName),\n });\n } else if (isCommonHandlerName(lowercaseHandlerName)) {\n const fixedName = getCommonEventHandlerName(lowercaseHandlerName);\n if (fixedName !== name) {\n // For common DOM event names, we know the user intended the prop to be an event handler.\n // Fix it to have an uppercase third letter and be properly camel-cased.\n context.report({\n node: node.name,\n messageId: \"capitalization\",\n data: { name, fixedName },\n fix: (fixer) => fixer.replaceText(node.name, fixedName),\n });\n }\n } else if (name[2] === name[2].toLowerCase()) {\n // this includes words like `only` and `ongoing` as well as unknown handlers like `onfoobar`.\n // Enforce using either /^on[A-Z]/ (event handler) or /^attr:on[a-z]/ (forced regular attribute)\n // to make user intent clear and code maximally readable\n const handlerName = `on${name[2].toUpperCase()}${name.slice(3)}`;\n const attrName = `attr:${name}`;\n context.report({\n node: node.name,\n messageId: \"naming\",\n data: { name, attrName, handlerName },\n suggest: [\n {\n messageId: \"make-handler\",\n data: { name, handlerName },\n fix: (fixer) => fixer.replaceText(node.name, handlerName),\n },\n {\n messageId: \"make-attr\",\n data: { name, attrName },\n fix: (fixer) => fixer.replaceText(node.name, attrName),\n },\n ],\n });\n }\n }\n },\n Property(node: T.Property) {\n if (\n context.options[0]?.warnOnSpread &&\n node.parent?.type === \"ObjectExpression\" &&\n node.parent.parent?.type === \"JSXSpreadAttribute\" &&\n node.parent.parent.parent?.type === \"JSXOpeningElement\"\n ) {\n const openingElement = node.parent.parent.parent;\n if (\n openingElement.name.type === \"JSXIdentifier\" &&\n isDOMElementName(openingElement.name.name)\n ) {\n if (node.key.type === \"Identifier\" && /^on/.test(node.key.name)) {\n const handlerName = node.key.name;\n // An event handler is being spread in (ex. <button {...{ onClick }} />), which doesn't\n // actually add an event listener, just a plain attribute.\n context.report({\n node,\n messageId: \"spread-handler\",\n data: {\n name: node.key.name,\n },\n *fix(fixer) {\n const commaAfter = sourceCode.getTokenAfter(node);\n yield fixer.remove(\n (node.parent as T.ObjectExpression).properties.length === 1\n ? node.parent!.parent!\n : node\n );\n if (commaAfter?.value === \",\") {\n yield fixer.remove(commaAfter);\n }\n yield fixer.insertTextAfter(\n node.parent!.parent!,\n ` ${handlerName}={${sourceCode.getText(node.value)}}`\n );\n },\n });\n }\n }\n }\n },\n };\n },\n});\n","import { TSESTree as T, TSESLint, ESLintUtils } from \"@typescript-eslint/utils\";\nimport { appendImports, insertImports, removeSpecifier } from \"../utils\";\nimport { getSourceCode } from \"../compat\";\n\nconst createRule = ESLintUtils.RuleCreator.withoutDocs;\n\n// Below: create maps of imports and types to designated import source.\n// We could mess with `Object.keys(require(\"solid-js\"))` to generate this, but requiring it from\n// node activates the \"node\" export condition, which doesn't necessarily match what users will\n// receive i.e. through bundlers. Instead, we're manually listing all of the public exports that\n// should be imported from \"solid-js\", etc.\n// ==============\n\ntype Source = \"solid-js\" | \"solid-js/web\" | \"solid-js/store\";\n\n// Set up map of imports to module\nconst primitiveMap = new Map<string, Source>();\nfor (const primitive of [\n \"createSignal\",\n \"createEffect\",\n \"createMemo\",\n \"createResource\",\n \"onMount\",\n \"onCleanup\",\n \"onError\",\n \"untrack\",\n \"batch\",\n \"on\",\n \"createRoot\",\n \"getOwner\",\n \"runWithOwner\",\n \"mergeProps\",\n \"splitProps\",\n \"useTransition\",\n \"observable\",\n \"from\",\n \"mapArray\",\n \"indexArray\",\n \"createContext\",\n \"useContext\",\n \"children\",\n \"lazy\",\n \"createUniqueId\",\n \"createDeferred\",\n \"createRenderEffect\",\n \"createComputed\",\n \"createReaction\",\n \"createSelector\",\n \"DEV\",\n \"For\",\n \"Show\",\n \"Switch\",\n \"Match\",\n \"Index\",\n \"ErrorBoundary\",\n \"Suspense\",\n \"SuspenseList\",\n]) {\n primitiveMap.set(primitive, \"solid-js\");\n}\nfor (const primitive of [\n \"Portal\",\n \"render\",\n \"hydrate\",\n \"renderToString\",\n \"renderToStream\",\n \"isServer\",\n \"renderToStringAsync\",\n \"generateHydrationScript\",\n \"HydrationScript\",\n \"Dynamic\",\n]) {\n primitiveMap.set(primitive, \"solid-js/web\");\n}\nfor (const primitive of [\n \"createStore\",\n \"produce\",\n \"reconcile\",\n \"unwrap\",\n \"createMutable\",\n \"modifyMutable\",\n]) {\n primitiveMap.set(primitive, \"solid-js/store\");\n}\n\n// Set up map of type imports to module\nconst typeMap = new Map<string, Source>();\nfor (const type of [\n \"Signal\",\n \"Accessor\",\n \"Setter\",\n \"Resource\",\n \"ResourceActions\",\n \"ResourceOptions\",\n \"ResourceReturn\",\n \"ResourceFetcher\",\n \"InitializedResourceReturn\",\n \"Component\",\n \"VoidProps\",\n \"VoidComponent\",\n \"ParentProps\",\n \"ParentComponent\",\n \"FlowProps\",\n \"FlowComponent\",\n \"ValidComponent\",\n \"ComponentProps\",\n \"Ref\",\n \"MergeProps\",\n \"SplitPrips\",\n \"Context\",\n \"JSX\",\n \"ResolvedChildren\",\n \"MatchProps\",\n]) {\n typeMap.set(type, \"solid-js\");\n}\nfor (const type of [/* \"JSX\", */ \"MountableElement\"]) {\n typeMap.set(type, \"solid-js/web\");\n}\nfor (const type of [\"StoreNode\", \"Store\", \"SetStoreFunction\"]) {\n typeMap.set(type, \"solid-js/store\");\n}\n\nconst sourceRegex = /^solid-js(?:\\/web|\\/store)?$/;\nconst isSource = (source: string): source is Source => sourceRegex.test(source);\n\nexport default createRule({\n meta: {\n type: \"suggestion\",\n docs: {\n description:\n 'Enforce consistent imports from \"solid-js\", \"solid-js/web\", and \"solid-js/store\".',\n url: \"https://github.com/solidjs-community/eslint-plugin-solid/blob/main/packages/eslint-plugin-solid/docs/imports.md\",\n },\n fixable: \"code\",\n schema: [],\n messages: {\n \"prefer-source\": 'Prefer importing {{name}} from \"{{source}}\".',\n },\n },\n defaultOptions: [],\n create(context) {\n return {\n ImportDeclaration(node) {\n const source = node.source.value;\n if (!isSource(source)) return;\n\n for (const specifier of node.specifiers) {\n if (specifier.type === \"ImportSpecifier\") {\n const isType = specifier.importKind === \"type\" || node.importKind === \"type\";\n const map = isType ? typeMap : primitiveMap;\n const correctSource = map.get(specifier.imported.name);\n if (correctSource != null && correctSource !== source) {\n context.report({\n node: specifier,\n messageId: \"prefer-source\",\n data: {\n name: specifier.imported.name,\n source: correctSource,\n },\n fix(fixer) {\n const sourceCode = getSourceCode(context);\n const program: T.Program = sourceCode.ast;\n const correctDeclaration = program.body.find(\n (node) =>\n node.type === \"ImportDeclaration\" && node.source.value === correctSource\n ) as T.ImportDeclaration | undefined;\n\n if (correctDeclaration) {\n return [\n removeSpecifier(fixer, sourceCode, specifier),\n appendImports(fixer, sourceCode, correctDeclaration, [\n sourceCode.getText(specifier),\n ]),\n ].filter(Boolean) as Array<TSESLint.RuleFix>;\n }\n\n const firstSolidDeclaration = program.body.find(\n (node) => node.type === \"ImportDeclaration\" && isSource(node.source.value)\n ) as T.ImportDeclaration | undefined;\n return [\n removeSpecifier(fixer, sourceCode, specifier),\n insertImports(\n fixer,\n sourceCode,\n correctSource,\n [sourceCode.getText(specifier)],\n firstSolidDeclaration,\n isType\n ),\n ];\n },\n });\n }\n }\n }\n },\n };\n },\n});\n","/**\n * FIXME: remove this comments and import when below issue is fixed.\n * This import is necessary for type generation due to a bug in the TypeScript compiler.\n * See: https://github.com/microsoft/TypeScript/issues/42873\n */\n// eslint-disable-next-line @typescript-eslint/no-unused-vars\nimport type { TSESLint } from \"@typescript-eslint/utils\";\n\nimport { TSESTree as T, ESLintUtils } from \"@typescript-eslint/utils\";\nimport { jsxGetAllProps } from \"../utils\";\n\nconst createRule = ESLintUtils.RuleCreator.withoutDocs;\n\n/*\n * This rule is adapted from eslint-plugin-react's jsx-no-duplicate-props rule under\n * the MIT license, with some enhancements. Thank you for your work!\n */\n\ntype MessageIds = \"noDuplicateProps\" | \"noDuplicateClass\" | \"noDuplicateChildren\";\ntype Options = [{ ignoreCase?: boolean }?];\n\nexport default createRule<Options, MessageIds>({\n meta: {\n type: \"problem\",\n docs: {\n description: \"Disallow passing the same prop twice in JSX.\",\n url: \"https://github.com/solidjs-community/eslint-plugin-solid/blob/main/packages/eslint-plugin-solid/docs/jsx-no-duplicate-props.md\",\n },\n schema: [\n {\n type: \"object\",\n properties: {\n ignoreCase: {\n type: \"boolean\",\n description: \"Consider two prop names differing only by case to be the same.\",\n default: false,\n },\n },\n },\n ],\n messages: {\n noDuplicateProps: \"Duplicate props are not allowed.\",\n noDuplicateClass:\n \"Duplicate `class` props are not allowed; while it might seem to work, it can break unexpectedly. Use `classList` instead.\",\n noDuplicateChildren: \"Using {{used}} at the same time is not allowed.\",\n },\n },\n defaultOptions: [],\n create(context) {\n return {\n JSXOpeningElement(node) {\n const ignoreCase = context.options[0]?.ignoreCase ?? false;\n const props = new Set();\n const checkPropName = (name: string, node: T.Node) => {\n if (ignoreCase || name.startsWith(\"on\")) {\n name = name\n .toLowerCase()\n .replace(/^on(?:capture)?:/, \"on\")\n .replace(/^(?:attr|prop):/, \"\");\n }\n if (props.has(name)) {\n context.report({\n node,\n messageId: name === \"class\" ? \"noDuplicateClass\" : \"noDuplicateProps\",\n });\n }\n props.add(name);\n };\n\n for (const [name, propNode] of jsxGetAllProps(node.attributes)) {\n checkPropName(name, propNode);\n }\n\n const hasChildrenProp = props.has(\"children\");\n const hasChildren = (node.parent as T.JSXElement | T.JSXFragment).children.length > 0;\n const hasInnerHTML = props.has(\"innerHTML\") || props.has(\"innerhtml\");\n const hasTextContent = props.has(\"textContent\") || props.has(\"textContent\");\n const used = [\n hasChildrenProp && \"`props.children`\",\n hasChildren && \"JSX children\",\n hasInnerHTML && \"`props.innerHTML`\",\n hasTextContent && \"`props.textContent`\",\n ].filter(Boolean);\n if (used.length > 1) {\n context.report({\n node,\n messageId: \"noDuplicateChildren\",\n data: {\n used: used.join(\", \"),\n },\n });\n }\n },\n };\n },\n});\n","/**\n * FIXME: remove this comments and import when below issue is fixed.\n * This import is necessary for type generation due to a bug in the TypeScript compiler.\n * See: https://github.com/microsoft/TypeScript/issues/42873\n */\n// eslint-disable-next-line @typescript-eslint/no-unused-vars\nimport type { TSESLint } from \"@typescript-eslint/utils\";\n\nimport { ESLintUtils, ASTUtils } from \"@typescript-eslint/utils\";\nimport { getScope } from \"../compat\";\n\nconst createRule = ESLintUtils.RuleCreator.withoutDocs;\nconst { getStaticValue }: { getStaticValue: any } = ASTUtils;\n\n// A javascript: URL can contain leading C0 control or \\u0020 SPACE,\n// and any newline or tab are filtered out as if they're not part of the URL.\n// https://url.spec.whatwg.org/#url-parsing\n// Tab or newline are defined as \\r\\n\\t:\n// https://infra.spec.whatwg.org/#ascii-tab-or-newline\n// A C0 control is a code point in the range \\u0000 NULL to \\u001F\n// INFORMATION SEPARATOR ONE, inclusive:\n// https://infra.spec.whatwg.org/#c0-control-or-space\nconst isJavaScriptProtocol =\n /^[\\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\n\n/**\n * This rule is adapted from eslint-plugin-react's jsx-no-script-url rule under the MIT license.\n * Thank you for your work!\n */\nexport default createRule({\n meta: {\n type: \"problem\",\n docs: {\n description: \"Disallow javascript: URLs.\",\n url: \"https://github.com/solidjs-community/eslint-plugin-solid/blob/main/packages/eslint-plugin-solid/docs/jsx-no-script-url.md\",\n },\n schema: [],\n messages: {\n noJSURL: \"For security, don't use javascript: URLs. Use event handlers instead if you can.\",\n },\n },\n defaultOptions: [],\n create(context) {\n return {\n JSXAttribute(node) {\n if (node.name.type === \"JSXIdentifier\" && node.value) {\n const link: { value: unknown } | null = getStaticValue(\n node.value.type === \"JSXExpressionContainer\" ? node.value.expression : node.value,\n getScope(context, node)\n );\n if (link && typeof link.value === \"string\" && isJavaScriptProtocol.test(link.value)) {\n context.report({\n node: node.value,\n messageId: \"noJSURL\",\n });\n }\n }\n },\n };\n },\n});\n","/**\n * FIXME: remove this comments and import when below issue is fixed.\n * This import is necessary for type generation due to a bug in the TypeScript compiler.\n * See: https://github.com/microsoft/TypeScript/issues/42873\n */\n// eslint-disable-next-line @typescript-eslint/no-unused-vars\nimport type { TSESLint } from \"@typescript-eslint/utils\";\n\nimport { TSESTree as T, ESLintUtils } from \"@typescript-eslint/utils\";\nimport { isDOMElementName, formatList, appendImports, insertImports } from \"../utils\";\nimport { getScope, getSourceCode } from \"../compat\";\n\nconst createRule = ESLintUtils.RuleCreator.withoutDocs;\n\n// Currently all of the control flow components are from 'solid-js'.\nconst AUTO_COMPONENTS = [\"Show\", \"For\", \"Index\", \"Switch\", \"Match\"];\nconst SOURCE_MODULE = \"solid-js\";\n\n/*\n * This rule is adapted from eslint-plugin-react's jsx-no-undef rule under\n * the MIT license. Thank you for your work!\n */\ntype MessageIds = \"undefined\" | \"customDirectiveUndefined\" | \"autoImport\";\ntype Options = [\n {\n allowGlobals?: boolean;\n autoImport?: boolean;\n typescriptEnabled?: boolean;\n }?\n];\nexport default createRule<Options, MessageIds>({\n meta: {\n type: \"problem\",\n docs: {\n description: \"Disallow references to undefined variables in JSX. Handles custom directives.\",\n url: \"https://github.com/solidjs-community/eslint-plugin-solid/blob/main/packages/eslint-plugin-solid/docs/jsx-no-undef.md\",\n },\n fixable: \"code\",\n schema: [\n {\n type: \"object\",\n properties: {\n allowGlobals: {\n type: \"boolean\",\n description:\n \"When true, the rule will consider the global scope when checking for defined components.\",\n default: false,\n },\n autoImport: {\n type: \"boolean\",\n description:\n 'Automatically import certain components from `\"solid-js\"` if they are undefined.',\n default: true,\n },\n typescriptEnabled: {\n type: \"boolean\",\n description: \"Adjusts behavior not to conflict with TypeScript's type checking.\",\n default: false,\n },\n },\n additionalProperties: false,\n },\n ],\n messages: {\n undefined: \"'{{identifier}}' is not defined.\",\n customDirectiveUndefined: \"Custom directive '{{identifier}}' is not defined.\",\n autoImport: \"{{imports}} should be imported from '{{source}}'.\",\n },\n },\n defaultOptions: [],\n create(context) {\n const allowGlobals = context.options[0]?.allowGlobals ?? false;\n const autoImport = context.options[0]?.autoImport !== false;\n const isTypeScriptEnabled = context.options[0]?.typescriptEnabled ?? false;\n\n const missingComponentsSet = new Set<string>();\n\n /**\n * Compare an identifier with the variables declared in the scope\n * @param {ASTNode} node - Identifier or JSXIdentifier node\n * @returns {void}\n */\n function checkIdentifierInJSX(\n node: T.Identifier | T.JSXIdentifier,\n {\n isComponent,\n isCustomDirective,\n }: { isComponent?: boolean; isCustomDirective?: boolean } = {}\n ) {\n let scope = getScope(context, node);\n const sourceCode = getSourceCode(context);\n const sourceType = sourceCode.ast.sourceType;\n const scopeUpperBound = !allowGlobals && sourceType === \"module\" ? \"module\" : \"global\";\n const variables = [...scope.variables];\n\n // Ignore 'this' keyword (also maked as JSXIdentifier when used in JSX)\n if (node.name === \"this\") {\n return;\n }\n\n while (scope.type !== scopeUpperBound && scope.type !== \"global\" && scope.upper) {\n scope = scope.upper;\n variables.push(...scope.variables);\n }\n if (scope.childScopes.length) {\n variables.push(...scope.childScopes[0].variables);\n // Temporary fix for babel-eslint\n if (scope.childScopes[0].childScopes.length) {\n variables.push(...scope.childScopes[0].childScopes[0].variables);\n }\n }\n\n if (variables.find((variable) => variable.name === node.name)) {\n return;\n }\n\n if (\n isComponent &&\n autoImport &&\n AUTO_COMPONENTS.includes(node.name) &&\n !missingComponentsSet.has(node.name)\n ) {\n // track which names are undefined\n missingComponentsSet.add(node