eslint-plugin-solid
Version:
Solid-specific linting rules for ESLint.
1,303 lines (1,293 loc) • 133 kB
JavaScript
var __create = Object.create;
var __defProp = Object.defineProperty;
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
var __getOwnPropNames = Object.getOwnPropertyNames;
var __getProtoOf = Object.getPrototypeOf;
var __hasOwnProp = Object.prototype.hasOwnProperty;
var __esm = (fn, res) => function __init() {
return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
};
var __commonJS = (cb, mod) => function __require() {
return mod || (0, cb[__getOwnPropNames(cb)[0]])((mod = { exports: {} }).exports, mod), mod.exports;
};
var __copyProps = (to, from, except, desc) => {
if (from && typeof from === "object" || typeof from === "function") {
for (let key of __getOwnPropNames(from))
if (!__hasOwnProp.call(to, key) && key !== except)
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
}
return to;
};
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
// If the importer is in node compatibility mode or this is not an ESM
// file that has been converted to a CommonJS file using a Babel-
// compatible transform (i.e. "__esModule" has not been set), then set
// "default" to the CommonJS "module.exports" for node compatibility.
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
mod
));
// src/compat.ts
import { ASTUtils } from "@typescript-eslint/utils";
function getSourceCode(context) {
if (typeof context.getSourceCode === "function") {
return context.getSourceCode();
}
return context.sourceCode;
}
function getScope(context, node) {
const sourceCode = getSourceCode(context);
if (typeof sourceCode.getScope === "function") {
return sourceCode.getScope(node);
}
if (typeof context.getScope === "function") {
return context.getScope();
}
return context.sourceCode.getScope(node);
}
function findVariable(context, node) {
return ASTUtils.findVariable(getScope(context, node), node);
}
function markVariableAsUsed(context, name2, node) {
if (typeof context.markVariableAsUsed === "function") {
context.markVariableAsUsed(name2);
} else {
getSourceCode(context).markVariableAsUsed(name2, node);
}
}
var init_compat = __esm({
"src/compat.ts"() {
"use strict";
}
});
// src/utils.ts
function findParent(node, predicate) {
return node.parent ? find(node.parent, predicate) : null;
}
function trace(node, context) {
if (node.type === "Identifier") {
const variable = findVariable(context, node);
if (!variable) return node;
const def = variable.defs[0];
switch (def?.type) {
case "FunctionName":
case "ClassName":
case "ImportBinding":
return def.node;
case "Variable":
if ((def.node.parent.kind === "const" || variable.references.every((ref) => ref.init || ref.isReadOnly())) && def.node.id.type === "Identifier" && def.node.init) {
return trace(def.node.init, context);
}
}
}
return node;
}
function ignoreTransparentWrappers(node, up = false) {
if (node.type === "TSAsExpression" || node.type === "TSNonNullExpression" || node.type === "TSSatisfiesExpression") {
const next = up ? node.parent : node.expression;
if (next) {
return ignoreTransparentWrappers(next, up);
}
}
return node;
}
function findInScope(node, scope, predicate) {
const found = find(node, (node2) => node2 === scope || predicate(node2));
return found === scope && !predicate(node) ? null : found;
}
function appendImports(fixer, sourceCode, importNode, identifiers) {
const identifiersString = identifiers.join(", ");
const reversedSpecifiers = importNode.specifiers.slice().reverse();
const lastSpecifier = reversedSpecifiers.find((s) => s.type === "ImportSpecifier");
if (lastSpecifier) {
return fixer.insertTextAfter(lastSpecifier, `, ${identifiersString}`);
}
const otherSpecifier = importNode.specifiers.find(
(s) => s.type === "ImportDefaultSpecifier" || s.type === "ImportNamespaceSpecifier"
);
if (otherSpecifier) {
return fixer.insertTextAfter(otherSpecifier, `, { ${identifiersString} }`);
}
if (importNode.specifiers.length === 0) {
const [importToken, maybeBrace] = sourceCode.getFirstTokens(importNode, { count: 2 });
if (maybeBrace?.value === "{") {
return fixer.insertTextAfter(maybeBrace, ` ${identifiersString} `);
} else {
return importToken ? fixer.insertTextAfter(importToken, ` { ${identifiersString} } from`) : null;
}
}
return null;
}
function insertImports(fixer, sourceCode, source, identifiers, aboveImport, isType = false) {
const identifiersString = identifiers.join(", ");
const programNode = sourceCode.ast;
const firstImport = aboveImport ?? programNode.body.find((n) => n.type === "ImportDeclaration");
if (firstImport) {
return fixer.insertTextBeforeRange(
(getCommentBefore(firstImport, sourceCode) ?? firstImport).range,
`import ${isType ? "type " : ""}{ ${identifiersString} } from "${source}";
`
);
}
return fixer.insertTextBeforeRange(
[0, 0],
`import ${isType ? "type " : ""}{ ${identifiersString} } from "${source}";
`
);
}
function removeSpecifier(fixer, sourceCode, specifier, pure = true) {
const declaration = specifier.parent;
if (declaration.specifiers.length === 1 && pure) {
return fixer.remove(declaration);
}
const maybeComma = sourceCode.getTokenAfter(specifier);
if (maybeComma?.value === ",") {
return fixer.removeRange([specifier.range[0], maybeComma.range[1]]);
}
return fixer.remove(specifier);
}
function jsxPropName(prop) {
if (prop.name.type === "JSXNamespacedName") {
return `${prop.name.namespace.name}:${prop.name.name.name}`;
}
return prop.name.name;
}
function* jsxGetAllProps(props) {
for (const attr of props) {
if (attr.type === "JSXSpreadAttribute" && attr.argument.type === "ObjectExpression") {
for (const property of attr.argument.properties) {
if (property.type === "Property") {
if (property.key.type === "Identifier") {
yield [property.key.name, property.key];
} else if (property.key.type === "Literal") {
yield [String(property.key.value), property.key];
}
}
}
} else if (attr.type === "JSXAttribute") {
yield [jsxPropName(attr), attr.name];
}
}
}
function jsxHasProp(props, prop) {
for (const [p] of jsxGetAllProps(props)) {
if (p === prop) return true;
}
return false;
}
function jsxGetProp(props, prop) {
return props.find(
(attribute) => attribute.type !== "JSXSpreadAttribute" && prop === jsxPropName(attribute)
);
}
var domElementRegex, isDOMElementName, propsRegex, isPropsByName, formatList, find, FUNCTION_TYPES, isFunctionNode, PROGRAM_OR_FUNCTION_TYPES, isProgramOrFunctionNode, isJSXElementOrFragment, getFunctionName, getCommentBefore, trackImports;
var init_utils = __esm({
"src/utils.ts"() {
"use strict";
init_compat();
domElementRegex = /^[a-z]/;
isDOMElementName = (name2) => domElementRegex.test(name2);
propsRegex = /[pP]rops/;
isPropsByName = (name2) => propsRegex.test(name2);
formatList = (strings) => {
if (strings.length === 0) {
return "";
} else if (strings.length === 1) {
return `'${strings[0]}'`;
} else if (strings.length === 2) {
return `'${strings[0]}' and '${strings[1]}'`;
} else {
const last = strings.length - 1;
return `${strings.slice(0, last).map((s) => `'${s}'`).join(", ")}, and '${strings[last]}'`;
}
};
find = (node, predicate) => {
let n = node;
while (n) {
const result = predicate(n);
if (result) {
return n;
}
n = n.parent;
}
return null;
};
FUNCTION_TYPES = ["FunctionExpression", "ArrowFunctionExpression", "FunctionDeclaration"];
isFunctionNode = (node) => !!node && FUNCTION_TYPES.includes(node.type);
PROGRAM_OR_FUNCTION_TYPES = ["Program"].concat(FUNCTION_TYPES);
isProgramOrFunctionNode = (node) => !!node && PROGRAM_OR_FUNCTION_TYPES.includes(node.type);
isJSXElementOrFragment = (node) => node?.type === "JSXElement" || node?.type === "JSXFragment";
getFunctionName = (node) => {
if ((node.type === "FunctionDeclaration" || node.type === "FunctionExpression") && node.id != null) {
return node.id.name;
}
if (node.parent?.type === "VariableDeclarator" && node.parent.id.type === "Identifier") {
return node.parent.id.name;
}
return null;
};
getCommentBefore = (node, sourceCode) => sourceCode.getCommentsBefore(node).find((comment) => comment.loc.end.line >= node.loc.start.line - 1);
trackImports = (fromModule = /^solid-js(?:\/?|\b)/) => {
const importMap = /* @__PURE__ */ new Map();
const handleImportDeclaration = (node) => {
if (fromModule.test(node.source.value)) {
for (const specifier of node.specifiers) {
if (specifier.type === "ImportSpecifier") {
importMap.set(specifier.imported.name, specifier.local.name);
}
}
}
};
const matchImport = (imports, str) => {
const importArr = Array.isArray(imports) ? imports : [imports];
return importArr.find((i) => importMap.get(i) === str);
};
return { matchImport, handleImportDeclaration };
};
}
});
// src/rules/components-return-once.ts
import { ESLintUtils } from "@typescript-eslint/utils";
var createRule, isNothing, getLineLength, components_return_once_default;
var init_components_return_once = __esm({
"src/rules/components-return-once.ts"() {
"use strict";
init_utils();
init_compat();
createRule = ESLintUtils.RuleCreator.withoutDocs;
isNothing = (node) => {
if (!node) {
return true;
}
switch (node.type) {
case "Literal":
return [null, void 0, false, ""].includes(node.value);
case "JSXFragment":
return !node.children || node.children.every(isNothing);
default:
return false;
}
};
getLineLength = (loc) => loc.end.line - loc.start.line + 1;
components_return_once_default = createRule({
meta: {
type: "problem",
docs: {
description: "Disallow early returns in components. Solid components only run once, and so conditionals should be inside JSX.",
url: "https://github.com/solidjs-community/eslint-plugin-solid/blob/main/packages/eslint-plugin-solid/docs/components-return-once.md"
},
fixable: "code",
schema: [],
messages: {
noEarlyReturn: "Solid components run once, so an early return breaks reactivity. Move the condition inside a JSX element, such as a fragment or <Show />.",
noConditionalReturn: "Solid components run once, so a conditional return breaks reactivity. Move the condition inside a JSX element, such as a fragment or <Show />."
}
},
defaultOptions: [],
create(context) {
const functionStack = [];
const putIntoJSX = (node) => {
const text = getSourceCode(context).getText(node);
return node.type === "JSXElement" || node.type === "JSXFragment" ? text : `{${text}}`;
};
const currentFunction = () => functionStack[functionStack.length - 1];
const onFunctionEnter = (node) => {
let lastReturn;
if (node.body.type === "BlockStatement") {
const last = node.body.body.findLast((node2) => !node2.type.endsWith("Declaration"));
if (last && last.type === "ReturnStatement") {
lastReturn = last;
}
}
functionStack.push({ isComponent: false, lastReturn, earlyReturns: [] });
};
const onFunctionExit = (node) => {
if (
// "render props" aren't components
getFunctionName(node)?.match(/^[a-z]/) || node.parent?.type === "JSXExpressionContainer" || // ignore createMemo(() => conditional JSX), report HOC(() => conditional JSX)
node.parent?.type === "CallExpression" && node.parent.arguments.some((n) => n === node) && !node.parent.callee.name?.match(/^[A-Z]/)
) {
currentFunction().isComponent = false;
}
if (currentFunction().isComponent) {
currentFunction().earlyReturns.forEach((earlyReturn) => {
context.report({
node: earlyReturn,
messageId: "noEarlyReturn"
});
});
const argument = currentFunction().lastReturn?.argument;
if (argument?.type === "ConditionalExpression") {
const sourceCode = getSourceCode(context);
context.report({
node: argument.parent,
messageId: "noConditionalReturn",
fix: (fixer) => {
const { test, consequent, alternate } = argument;
const conditions = [{ test, consequent }];
let fallback = alternate;
while (fallback.type === "ConditionalExpression") {
conditions.push({ test: fallback.test, consequent: fallback.consequent });
fallback = fallback.alternate;
}
if (conditions.length >= 2) {
const fallbackStr = !isNothing(fallback) ? ` fallback={${sourceCode.getText(fallback)}}` : "";
return fixer.replaceText(
argument,
`<Switch${fallbackStr}>
${conditions.map(
({ test: test2, consequent: consequent2 }) => `<Match when={${sourceCode.getText(test2)}}>${putIntoJSX(
consequent2
)}</Match>`
).join("\n")}
</Switch>`
);
}
if (isNothing(consequent)) {
return fixer.replaceText(
argument,
`<Show when={!(${sourceCode.getText(test)})}>${putIntoJSX(alternate)}</Show>`
);
}
if (isNothing(fallback) || getLineLength(consequent.loc) >= getLineLength(alternate.loc) * 1.5) {
const fallbackStr = !isNothing(fallback) ? ` fallback={${sourceCode.getText(fallback)}}` : "";
return fixer.replaceText(
argument,
`<Show when={${sourceCode.getText(test)}}${fallbackStr}>${putIntoJSX(
consequent
)}</Show>`
);
}
return fixer.replaceText(argument, `<>${putIntoJSX(argument)}</>`);
}
});
} else if (argument?.type === "LogicalExpression") {
if (argument.operator === "&&") {
const sourceCode = getSourceCode(context);
context.report({
node: argument,
messageId: "noConditionalReturn",
fix: (fixer) => {
const { left: test, right: consequent } = argument;
return fixer.replaceText(
argument,
`<Show when={${sourceCode.getText(test)}}>${putIntoJSX(consequent)}</Show>`
);
}
});
} else {
context.report({
node: argument,
messageId: "noConditionalReturn"
});
}
}
}
functionStack.pop();
};
return {
FunctionDeclaration: onFunctionEnter,
FunctionExpression: onFunctionEnter,
ArrowFunctionExpression: onFunctionEnter,
"FunctionDeclaration:exit": onFunctionExit,
"FunctionExpression:exit": onFunctionExit,
"ArrowFunctionExpression:exit": onFunctionExit,
JSXElement() {
if (functionStack.length) {
currentFunction().isComponent = true;
}
},
JSXFragment() {
if (functionStack.length) {
currentFunction().isComponent = true;
}
},
ReturnStatement(node) {
if (functionStack.length && node !== currentFunction().lastReturn) {
currentFunction().earlyReturns.push(node);
}
}
};
}
});
}
});
// src/rules/event-handlers.ts
import { ESLintUtils as ESLintUtils2, ASTUtils as ASTUtils2 } from "@typescript-eslint/utils";
var createRule2, getStaticValue, COMMON_EVENTS, COMMON_EVENTS_MAP, NONSTANDARD_EVENTS_MAP, isCommonHandlerName, getCommonEventHandlerName, isNonstandardEventName, getStandardEventHandlerName, event_handlers_default;
var init_event_handlers = __esm({
"src/rules/event-handlers.ts"() {
"use strict";
init_utils();
init_compat();
createRule2 = ESLintUtils2.RuleCreator.withoutDocs;
({ getStaticValue } = ASTUtils2);
COMMON_EVENTS = [
"onAnimationEnd",
"onAnimationIteration",
"onAnimationStart",
"onBeforeInput",
"onBlur",
"onChange",
"onClick",
"onContextMenu",
"onCopy",
"onCut",
"onDblClick",
"onDrag",
"onDragEnd",
"onDragEnter",
"onDragExit",
"onDragLeave",
"onDragOver",
"onDragStart",
"onDrop",
"onError",
"onFocus",
"onFocusIn",
"onFocusOut",
"onGotPointerCapture",
"onInput",
"onInvalid",
"onKeyDown",
"onKeyPress",
"onKeyUp",
"onLoad",
"onLostPointerCapture",
"onMouseDown",
"onMouseEnter",
"onMouseLeave",
"onMouseMove",
"onMouseOut",
"onMouseOver",
"onMouseUp",
"onPaste",
"onPointerCancel",
"onPointerDown",
"onPointerEnter",
"onPointerLeave",
"onPointerMove",
"onPointerOut",
"onPointerOver",
"onPointerUp",
"onReset",
"onScroll",
"onSelect",
"onSubmit",
"onToggle",
"onTouchCancel",
"onTouchEnd",
"onTouchMove",
"onTouchStart",
"onTransitionEnd",
"onWheel"
];
COMMON_EVENTS_MAP = new Map(
function* () {
for (const event of COMMON_EVENTS) {
yield [event.toLowerCase(), event];
}
}()
);
NONSTANDARD_EVENTS_MAP = {
ondoubleclick: "onDblClick"
};
isCommonHandlerName = (lowercaseHandlerName) => COMMON_EVENTS_MAP.has(lowercaseHandlerName);
getCommonEventHandlerName = (lowercaseHandlerName) => COMMON_EVENTS_MAP.get(lowercaseHandlerName);
isNonstandardEventName = (lowercaseEventName) => Boolean(NONSTANDARD_EVENTS_MAP[lowercaseEventName]);
getStandardEventHandlerName = (lowercaseEventName) => NONSTANDARD_EVENTS_MAP[lowercaseEventName];
event_handlers_default = createRule2({
meta: {
type: "problem",
docs: {
description: "Enforce naming DOM element event handlers consistently and prevent Solid's analysis from misunderstanding whether a prop should be an event handler.",
url: "https://github.com/solidjs-community/eslint-plugin-solid/blob/main/packages/eslint-plugin-solid/docs/event-handlers.md"
},
fixable: "code",
hasSuggestions: true,
schema: [
{
type: "object",
properties: {
ignoreCase: {
type: "boolean",
description: "if true, don't warn on ambiguously named event handlers like `onclick` or `onchange`",
default: false
},
warnOnSpread: {
type: "boolean",
description: "if true, warn when spreading event handlers onto JSX. Enable for Solid < v1.6.",
default: false
}
},
additionalProperties: false
}
],
messages: {
"detected-attr": '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}}.',
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}}.",
capitalization: "The {{name}} prop should be renamed to {{fixedName}} for readability.",
nonstandard: "The {{name}} prop should be renamed to {{fixedName}}, because it's not a standard event handler.",
"make-handler": "Change the {{name}} prop to {{handlerName}}.",
"make-attr": "Change the {{name}} prop to {{attrName}}.",
"spread-handler": "The {{name}} prop should be added as a JSX attribute, not spread in. Solid doesn't add listeners when spreading into JSX."
}
},
defaultOptions: [],
create(context) {
const sourceCode = getSourceCode(context);
return {
JSXAttribute(node) {
const openingElement = node.parent;
if (openingElement.name.type !== "JSXIdentifier" || !isDOMElementName(openingElement.name.name)) {
return;
}
if (node.name.type === "JSXNamespacedName") {
return;
}
const { name: name2 } = node.name;
if (!/^on[a-zA-Z]/.test(name2)) {
return;
}
let staticValue = null;
if (node.value?.type === "JSXExpressionContainer" && node.value.expression.type !== "JSXEmptyExpression" && node.value.expression.type !== "ArrayExpression" && // array syntax prevents inlining
(staticValue = getStaticValue(node.value.expression, getScope(context, node))) !== null && (typeof staticValue.value === "string" || typeof staticValue.value === "number")) {
context.report({
node,
messageId: "detected-attr",
data: {
name: name2,
staticValue: staticValue.value
}
});
} else if (node.value === null || node.value?.type === "Literal") {
context.report({
node,
messageId: "detected-attr",
data: {
name: name2,
staticValue: node.value !== null ? node.value.value : true
}
});
} else if (!context.options[0]?.ignoreCase) {
const lowercaseHandlerName = name2.toLowerCase();
if (isNonstandardEventName(lowercaseHandlerName)) {
const fixedName = getStandardEventHandlerName(lowercaseHandlerName);
context.report({
node: node.name,
messageId: "nonstandard",
data: { name: name2, fixedName },
fix: (fixer) => fixer.replaceText(node.name, fixedName)
});
} else if (isCommonHandlerName(lowercaseHandlerName)) {
const fixedName = getCommonEventHandlerName(lowercaseHandlerName);
if (fixedName !== name2) {
context.report({
node: node.name,
messageId: "capitalization",
data: { name: name2, fixedName },
fix: (fixer) => fixer.replaceText(node.name, fixedName)
});
}
} else if (name2[2] === name2[2].toLowerCase()) {
const handlerName = `on${name2[2].toUpperCase()}${name2.slice(3)}`;
const attrName = `attr:${name2}`;
context.report({
node: node.name,
messageId: "naming",
data: { name: name2, attrName, handlerName },
suggest: [
{
messageId: "make-handler",
data: { name: name2, handlerName },
fix: (fixer) => fixer.replaceText(node.name, handlerName)
},
{
messageId: "make-attr",
data: { name: name2, attrName },
fix: (fixer) => fixer.replaceText(node.name, attrName)
}
]
});
}
}
},
Property(node) {
if (context.options[0]?.warnOnSpread && node.parent?.type === "ObjectExpression" && node.parent.parent?.type === "JSXSpreadAttribute" && node.parent.parent.parent?.type === "JSXOpeningElement") {
const openingElement = node.parent.parent.parent;
if (openingElement.name.type === "JSXIdentifier" && isDOMElementName(openingElement.name.name)) {
if (node.key.type === "Identifier" && /^on/.test(node.key.name)) {
const handlerName = node.key.name;
context.report({
node,
messageId: "spread-handler",
data: {
name: node.key.name
},
*fix(fixer) {
const commaAfter = sourceCode.getTokenAfter(node);
yield fixer.remove(
node.parent.properties.length === 1 ? node.parent.parent : node
);
if (commaAfter?.value === ",") {
yield fixer.remove(commaAfter);
}
yield fixer.insertTextAfter(
node.parent.parent,
` ${handlerName}={${sourceCode.getText(node.value)}}`
);
}
});
}
}
}
}
};
}
});
}
});
// src/rules/imports.ts
import { ESLintUtils as ESLintUtils3 } from "@typescript-eslint/utils";
var createRule3, primitiveMap, typeMap, sourceRegex, isSource, imports_default;
var init_imports = __esm({
"src/rules/imports.ts"() {
"use strict";
init_utils();
init_compat();
createRule3 = ESLintUtils3.RuleCreator.withoutDocs;
primitiveMap = /* @__PURE__ */ new Map();
for (const primitive of [
"createSignal",
"createEffect",
"createMemo",
"createResource",
"onMount",
"onCleanup",
"onError",
"untrack",
"batch",
"on",
"createRoot",
"getOwner",
"runWithOwner",
"mergeProps",
"splitProps",
"useTransition",
"observable",
"from",
"mapArray",
"indexArray",
"createContext",
"useContext",
"children",
"lazy",
"createUniqueId",
"createDeferred",
"createRenderEffect",
"createComputed",
"createReaction",
"createSelector",
"DEV",
"For",
"Show",
"Switch",
"Match",
"Index",
"ErrorBoundary",
"Suspense",
"SuspenseList"
]) {
primitiveMap.set(primitive, "solid-js");
}
for (const primitive of [
"Portal",
"render",
"hydrate",
"renderToString",
"renderToStream",
"isServer",
"renderToStringAsync",
"generateHydrationScript",
"HydrationScript",
"Dynamic"
]) {
primitiveMap.set(primitive, "solid-js/web");
}
for (const primitive of [
"createStore",
"produce",
"reconcile",
"unwrap",
"createMutable",
"modifyMutable"
]) {
primitiveMap.set(primitive, "solid-js/store");
}
typeMap = /* @__PURE__ */ new Map();
for (const type of [
"Signal",
"Accessor",
"Setter",
"Resource",
"ResourceActions",
"ResourceOptions",
"ResourceReturn",
"ResourceFetcher",
"InitializedResourceReturn",
"Component",
"VoidProps",
"VoidComponent",
"ParentProps",
"ParentComponent",
"FlowProps",
"FlowComponent",
"ValidComponent",
"ComponentProps",
"Ref",
"MergeProps",
"SplitPrips",
"Context",
"JSX",
"ResolvedChildren",
"MatchProps"
]) {
typeMap.set(type, "solid-js");
}
for (const type of [
/* "JSX", */
"MountableElement"
]) {
typeMap.set(type, "solid-js/web");
}
for (const type of ["StoreNode", "Store", "SetStoreFunction"]) {
typeMap.set(type, "solid-js/store");
}
sourceRegex = /^solid-js(?:\/web|\/store)?$/;
isSource = (source) => sourceRegex.test(source);
imports_default = createRule3({
meta: {
type: "suggestion",
docs: {
description: 'Enforce consistent imports from "solid-js", "solid-js/web", and "solid-js/store".',
url: "https://github.com/solidjs-community/eslint-plugin-solid/blob/main/packages/eslint-plugin-solid/docs/imports.md"
},
fixable: "code",
schema: [],
messages: {
"prefer-source": 'Prefer importing {{name}} from "{{source}}".'
}
},
defaultOptions: [],
create(context) {
return {
ImportDeclaration(node) {
const source = node.source.value;
if (!isSource(source)) return;
for (const specifier of node.specifiers) {
if (specifier.type === "ImportSpecifier") {
const isType = specifier.importKind === "type" || node.importKind === "type";
const map = isType ? typeMap : primitiveMap;
const correctSource = map.get(specifier.imported.name);
if (correctSource != null && correctSource !== source) {
context.report({
node: specifier,
messageId: "prefer-source",
data: {
name: specifier.imported.name,
source: correctSource
},
fix(fixer) {
const sourceCode = getSourceCode(context);
const program = sourceCode.ast;
const correctDeclaration = program.body.find(
(node2) => node2.type === "ImportDeclaration" && node2.source.value === correctSource
);
if (correctDeclaration) {
return [
removeSpecifier(fixer, sourceCode, specifier),
appendImports(fixer, sourceCode, correctDeclaration, [
sourceCode.getText(specifier)
])
].filter(Boolean);
}
const firstSolidDeclaration = program.body.find(
(node2) => node2.type === "ImportDeclaration" && isSource(node2.source.value)
);
return [
removeSpecifier(fixer, sourceCode, specifier),
insertImports(
fixer,
sourceCode,
correctSource,
[sourceCode.getText(specifier)],
firstSolidDeclaration,
isType
)
];
}
});
}
}
}
}
};
}
});
}
});
// src/rules/jsx-no-duplicate-props.ts
import { ESLintUtils as ESLintUtils4 } from "@typescript-eslint/utils";
var createRule4, jsx_no_duplicate_props_default;
var init_jsx_no_duplicate_props = __esm({
"src/rules/jsx-no-duplicate-props.ts"() {
"use strict";
init_utils();
createRule4 = ESLintUtils4.RuleCreator.withoutDocs;
jsx_no_duplicate_props_default = createRule4({
meta: {
type: "problem",
docs: {
description: "Disallow passing the same prop twice in JSX.",
url: "https://github.com/solidjs-community/eslint-plugin-solid/blob/main/packages/eslint-plugin-solid/docs/jsx-no-duplicate-props.md"
},
schema: [
{
type: "object",
properties: {
ignoreCase: {
type: "boolean",
description: "Consider two prop names differing only by case to be the same.",
default: false
}
}
}
],
messages: {
noDuplicateProps: "Duplicate props are not allowed.",
noDuplicateClass: "Duplicate `class` props are not allowed; while it might seem to work, it can break unexpectedly. Use `classList` instead.",
noDuplicateChildren: "Using {{used}} at the same time is not allowed."
}
},
defaultOptions: [],
create(context) {
return {
JSXOpeningElement(node) {
const ignoreCase = context.options[0]?.ignoreCase ?? false;
const props = /* @__PURE__ */ new Set();
const checkPropName = (name2, node2) => {
if (ignoreCase || name2.startsWith("on")) {
name2 = name2.toLowerCase().replace(/^on(?:capture)?:/, "on").replace(/^(?:attr|prop):/, "");
}
if (props.has(name2)) {
context.report({
node: node2,
messageId: name2 === "class" ? "noDuplicateClass" : "noDuplicateProps"
});
}
props.add(name2);
};
for (const [name2, propNode] of jsxGetAllProps(node.attributes)) {
checkPropName(name2, propNode);
}
const hasChildrenProp = props.has("children");
const hasChildren = node.parent.children.length > 0;
const hasInnerHTML = props.has("innerHTML") || props.has("innerhtml");
const hasTextContent = props.has("textContent") || props.has("textContent");
const used = [
hasChildrenProp && "`props.children`",
hasChildren && "JSX children",
hasInnerHTML && "`props.innerHTML`",
hasTextContent && "`props.textContent`"
].filter(Boolean);
if (used.length > 1) {
context.report({
node,
messageId: "noDuplicateChildren",
data: {
used: used.join(", ")
}
});
}
}
};
}
});
}
});
// src/rules/jsx-no-script-url.ts
import { ESLintUtils as ESLintUtils5, ASTUtils as ASTUtils3 } from "@typescript-eslint/utils";
var createRule5, getStaticValue2, isJavaScriptProtocol, jsx_no_script_url_default;
var init_jsx_no_script_url = __esm({
"src/rules/jsx-no-script-url.ts"() {
"use strict";
init_compat();
createRule5 = ESLintUtils5.RuleCreator.withoutDocs;
({ getStaticValue: getStaticValue2 } = ASTUtils3);
isJavaScriptProtocol = /^[\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;
jsx_no_script_url_default = createRule5({
meta: {
type: "problem",
docs: {
description: "Disallow javascript: URLs.",
url: "https://github.com/solidjs-community/eslint-plugin-solid/blob/main/packages/eslint-plugin-solid/docs/jsx-no-script-url.md"
},
schema: [],
messages: {
noJSURL: "For security, don't use javascript: URLs. Use event handlers instead if you can."
}
},
defaultOptions: [],
create(context) {
return {
JSXAttribute(node) {
if (node.name.type === "JSXIdentifier" && node.value) {
const link = getStaticValue2(
node.value.type === "JSXExpressionContainer" ? node.value.expression : node.value,
getScope(context, node)
);
if (link && typeof link.value === "string" && isJavaScriptProtocol.test(link.value)) {
context.report({
node: node.value,
messageId: "noJSURL"
});
}
}
}
};
}
});
}
});
// src/rules/jsx-no-undef.ts
import { ESLintUtils as ESLintUtils6 } from "@typescript-eslint/utils";
var createRule6, AUTO_COMPONENTS, SOURCE_MODULE, jsx_no_undef_default;
var init_jsx_no_undef = __esm({
"src/rules/jsx-no-undef.ts"() {
"use strict";
init_utils();
init_compat();
createRule6 = ESLintUtils6.RuleCreator.withoutDocs;
AUTO_COMPONENTS = ["Show", "For", "Index", "Switch", "Match"];
SOURCE_MODULE = "solid-js";
jsx_no_undef_default = createRule6({
meta: {
type: "problem",
docs: {
description: "Disallow references to undefined variables in JSX. Handles custom directives.",
url: "https://github.com/solidjs-community/eslint-plugin-solid/blob/main/packages/eslint-plugin-solid/docs/jsx-no-undef.md"
},
fixable: "code",
schema: [
{
type: "object",
properties: {
allowGlobals: {
type: "boolean",
description: "When true, the rule will consider the global scope when checking for defined components.",
default: false
},
autoImport: {
type: "boolean",
description: 'Automatically import certain components from `"solid-js"` if they are undefined.',
default: true
},
typescriptEnabled: {
type: "boolean",
description: "Adjusts behavior not to conflict with TypeScript's type checking.",
default: false
}
},
additionalProperties: false
}
],
messages: {
undefined: "'{{identifier}}' is not defined.",
customDirectiveUndefined: "Custom directive '{{identifier}}' is not defined.",
autoImport: "{{imports}} should be imported from '{{source}}'."
}
},
defaultOptions: [],
create(context) {
const allowGlobals = context.options[0]?.allowGlobals ?? false;
const autoImport = context.options[0]?.autoImport !== false;
const isTypeScriptEnabled = context.options[0]?.typescriptEnabled ?? false;
const missingComponentsSet = /* @__PURE__ */ new Set();
function checkIdentifierInJSX(node, {
isComponent: isComponent2,
isCustomDirective
} = {}) {
let scope = getScope(context, node);
const sourceCode = getSourceCode(context);
const sourceType = sourceCode.ast.sourceType;
const scopeUpperBound = !allowGlobals && sourceType === "module" ? "module" : "global";
const variables = [...scope.variables];
if (node.name === "this") {
return;
}
while (scope.type !== scopeUpperBound && scope.type !== "global" && scope.upper) {
scope = scope.upper;
variables.push(...scope.variables);
}
if (scope.childScopes.length) {
variables.push(...scope.childScopes[0].variables);
if (scope.childScopes[0].childScopes.length) {
variables.push(...scope.childScopes[0].childScopes[0].variables);
}
}
if (variables.find((variable) => variable.name === node.name)) {
return;
}
if (isComponent2 && autoImport && AUTO_COMPONENTS.includes(node.name) && !missingComponentsSet.has(node.name)) {
missingComponentsSet.add(node.name);
} else if (isCustomDirective) {
context.report({
node,
messageId: "customDirectiveUndefined",
data: {
identifier: node.name
}
});
} else if (!isTypeScriptEnabled) {
context.report({
node,
messageId: "undefined",
data: {
identifier: node.name
}
});
}
}
return {
JSXOpeningElement(node) {
let n;
switch (node.name.type) {
case "JSXIdentifier":
if (!isDOMElementName(node.name.name)) {
checkIdentifierInJSX(node.name, { isComponent: true });
}
break;
case "JSXMemberExpression":
n = node.name;
do {
n = n.object;
} while (n && n.type !== "JSXIdentifier");
if (n) {
checkIdentifierInJSX(n);
}
break;
default:
break;
}
},
"JSXAttribute > JSXNamespacedName": (node) => {
if (node.namespace?.type === "JSXIdentifier" && node.namespace.name === "use" && node.name?.type === "JSXIdentifier") {
checkIdentifierInJSX(node.name, { isCustomDirective: true });
}
},
"Program:exit": (programNode) => {
const missingComponents = Array.from(missingComponentsSet.values());
if (autoImport && missingComponents.length) {
const importNode = programNode.body.find(
(n) => n.type === "ImportDeclaration" && n.importKind !== "type" && n.source.type === "Literal" && n.source.value === SOURCE_MODULE
);
if (importNode) {
context.report({
node: importNode,
messageId: "autoImport",
data: {
imports: formatList(missingComponents),
// "Show, For, and Switch"
source: SOURCE_MODULE
},
fix: (fixer) => {
return appendImports(fixer, getSourceCode(context), importNode, missingComponents);
}
});
} else {
context.report({
node: programNode,
messageId: "autoImport",
data: {
imports: formatList(missingComponents),
source: SOURCE_MODULE
},
fix: (fixer) => {
return insertImports(fixer, getSourceCode(context), "solid-js", missingComponents);
}
});
}
}
}
};
}
});
}
});
// src/rules/jsx-uses-vars.ts
import { ESLintUtils as ESLintUtils7 } from "@typescript-eslint/utils";
var createRule7, jsx_uses_vars_default;
var init_jsx_uses_vars = __esm({
"src/rules/jsx-uses-vars.ts"() {
"use strict";
init_compat();
createRule7 = ESLintUtils7.RuleCreator.withoutDocs;
jsx_uses_vars_default = createRule7({
meta: {
type: "problem",
docs: {
// eslint-disable-next-line eslint-plugin/require-meta-docs-description
description: "Prevent variables used in JSX from being marked as unused.",
url: "https://github.com/solidjs-community/eslint-plugin-solid/blob/main/packages/eslint-plugin-solid/docs/jsx-uses-vars.md"
},
schema: [],
// eslint-disable-next-line eslint-plugin/prefer-message-ids
messages: {}
},
defaultOptions: [],
create(context) {
return {
JSXOpeningElement(node) {
let parent;
switch (node.name.type) {
case "JSXNamespacedName":
return;
case "JSXIdentifier":
markVariableAsUsed(context, node.name.name, node.name);
break;
case "JSXMemberExpression":
parent = node.name.object;
while (parent?.type === "JSXMemberExpression") {
parent = parent.object;
}
if (parent.type === "JSXIdentifier") {
markVariableAsUsed(context, parent.name, parent);
}
break;
}
},
"JSXAttribute > JSXNamespacedName": (node) => {
if (node.namespace?.type === "JSXIdentifier" && node.namespace.name === "use" && node.name?.type === "JSXIdentifier") {
markVariableAsUsed(context, node.name.name, node.name);
}
}
};
}
});
}
});
// src/rules/no-destructure.ts
import { ESLintUtils as ESLintUtils8, ASTUtils as ASTUtils4 } from "@typescript-eslint/utils";
var createRule8, getStringIfConstant, getName, getPropertyInfo, no_destructure_default;
var init_no_destructure = __esm({
"src/rules/no-destructure.ts"() {
"use strict";
init_compat();
createRule8 = ESLintUtils8.RuleCreator.withoutDocs;
({ getStringIfConstant } = ASTUtils4);
getName = (node) => {
switch (node.type) {
case "Literal":
return typeof node.value === "string" ? node.value : null;
case "Identifier":
return node.name;
case "AssignmentPattern":
return getName(node.left);
default:
return getStringIfConstant(node);
}
};
getPropertyInfo = (prop) => {
const valueName = getName(prop.value);
if (valueName !== null) {
return {
real: prop.key,
var: valueName,
computed: prop.computed,
init: prop.value.type === "AssignmentPattern" ? prop.value.right : void 0
};
} else {
return null;
}
};
no_destructure_default = createRule8({
meta: {
type: "problem",
docs: {
description: "Disallow destructuring props. In Solid, props must be used with property accesses (`props.foo`) to preserve reactivity. This rule only tracks destructuring in the parameter list.",
url: "https://github.com/solidjs-community/eslint-plugin-solid/blob/main/packages/eslint-plugin-solid/docs/no-destructure.md"
},
fixable: "code",
schema: [],
messages: {
noDestructure: "Destructuring component props breaks Solid's reactivity; use property access instead."
// noWriteToProps: "Component props are readonly, writing to props is not supported.",
}
},
defaultOptions: [],
create(context) {
const functionStack = [];
const currentFunction = () => functionStack[functionStack.length - 1];
const onFunctionEnter = () => {
functionStack.push({ hasJSX: false });
};
const onFunctionExit = (node) => {
if (node.params.length === 1) {
const props = node.params[0];
if (props.type === "ObjectPattern" && currentFunction().hasJSX && node.parent?.type !== "JSXExpressionContainer") {
context.report({
node: props,
messageId: "noDestructure",
fix: (fixer) => fixDestructure(node, props, fixer)
});
}
}
functionStack.pop();
};
function* fixDestructure(func, props, fixer) {
const propsName = "props";
const properties = props.properties;
const propertyInfo = [];
let rest = null;
for (const property of properties) {
if (property.type === "RestElement") {
rest = property;
} else {
const info = getPropertyInfo(property);
if (info === null) {
continue;
}
propertyInfo.push(info);
}
}
const hasDefaults = propertyInfo.some((info) => info.init);
const origProps = !(hasDefaults || rest) ? propsName : "_" + propsName;
if (props.typeAnnotation) {
const range = [props.range[0], props.typeAnnotation.range[0]];
yield fixer.replaceTextRange(range, origProps);
} else {
yield fixer.replaceText(props, origProps);
}
const sourceCode = getSourceCode(context);
const defaultsObjectString = () => propertyInfo.filter((info) => info.init).map(
(info) => `${info.computed ? "[" : ""}${sourceCode.getText(info.real)}${info.computed ? "]" : ""}: ${sourceCode.getText(info.init)}`
).join(", ");
const splitPropsArray = () => `[${propertyInfo.map(
(info) => info.real.type === "Identifier" ? JSON.stringify(info.real.name) : sourceCode.getText(info.real)
).join(", ")}]`;
let lineToInsert = "";
if (hasDefaults && rest) {
lineToInsert = ` const [${propsName}, ${rest.argument.type === "Identifier" && rest.argument.name || "rest"}] = splitProps(mergeProps({ ${defaultsObjectString()} }, ${origProps}), ${splitPropsArray()});`;
} else if (hasDefaults) {
lineToInsert = ` const ${propsName} = mergeProps({ ${defaultsObjectString()} }, ${origProps});
`;
} else if (rest) {
lineToInsert = ` const [${propsName}, ${rest.argument.type === "Identifier" && rest.argument.name || "rest"}] = splitProps(${origProps}, ${splitPropsArray()});
`;
}
if (lineToInsert) {
const body = func.body;
if (body.type === "BlockStatement") {
if (body.body.length > 0) {
yield fixer.insertTextBefore(body.body[0], lineToInsert);
}
} el