FRE-600: Fix code review blockers

- Consolidated duplicate UndoManagers to single instance
- Fixed connection promise to only resolve on 'connected' status
- Fixed WebSocketProvider import (WebsocketProvider)
- Added proper doc.destroy() cleanup
- Renamed isPresenceInitialized property to avoid conflict

Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
2026-04-25 00:08:01 -04:00
parent 65b552bb08
commit 7c684a42cc
48450 changed files with 5679671 additions and 383 deletions

View File

@@ -0,0 +1,54 @@
/**
* FIXME: remove this comments and import when below issue is fixed.
* This import is necessary for type generation due to a bug in the TypeScript compiler.
* See: https://github.com/microsoft/TypeScript/issues/42873
*/
// eslint-disable-next-line @typescript-eslint/no-unused-vars
import type { TSESLint } from "@typescript-eslint/utils";
import { plugin } from "../plugin";
const recommended = {
plugins: {
solid: plugin,
},
languageOptions: {
sourceType: "module",
parserOptions: {
ecmaFeatures: {
jsx: true,
},
},
},
rules: {
// identifier usage is important
"solid/jsx-no-duplicate-props": 2,
"solid/jsx-no-undef": 2,
"solid/jsx-uses-vars": 2,
"solid/no-unknown-namespaces": 2,
// security problems
"solid/no-innerhtml": 2,
"solid/jsx-no-script-url": 2,
// reactivity
"solid/components-return-once": 1,
"solid/no-destructure": 2,
"solid/prefer-for": 2,
"solid/reactivity": 1,
"solid/event-handlers": 1,
// these rules are mostly style suggestions
"solid/imports": 1,
"solid/style-prop": 1,
"solid/no-react-deps": 1,
"solid/no-react-specific-props": 1,
"solid/self-closing-comp": 1,
"solid/no-array-handlers": 0,
// handled by Solid compiler, opt-in style suggestion
"solid/prefer-show": 0,
// only necessary for resource-constrained environments
"solid/no-proxy-apis": 0,
// deprecated
"solid/prefer-classlist": 0,
},
};
export = recommended;

View File

@@ -0,0 +1,26 @@
/**
* FIXME: remove this comments and import when below issue is fixed.
* This import is necessary for type generation due to a bug in the TypeScript compiler.
* See: https://github.com/microsoft/TypeScript/issues/42873
*/
// eslint-disable-next-line @typescript-eslint/no-unused-vars
import type { TSESLint } from "@typescript-eslint/utils";
import recommended from "./recommended";
const typescript = {
// no files; either apply to all files, or let users spread in this config
// and specify matching patterns. This is eslint-plugin-react's take.
plugins: recommended.plugins,
// no languageOptions; ESLint's default parser can't parse TypeScript,
// and parsers are configured in languageOptions, so let the user handle
// this rather than cause potential conflicts
rules: {
...recommended.rules,
"solid/jsx-no-undef": [2, { typescriptEnabled: true }],
// namespaces taken care of by TS
"solid/no-unknown-namespaces": 0,
},
};
export = typescript;

1
node_modules/eslint-plugin-solid/src/deps.d.ts generated vendored Normal file
View File

@@ -0,0 +1 @@
declare module "kebab-case";

40
node_modules/eslint-plugin-solid/src/index.ts generated vendored Normal file
View File

@@ -0,0 +1,40 @@
/**
* FIXME: remove this comments and import when below issue is fixed.
* This import is necessary for type generation due to a bug in the TypeScript compiler.
* See: https://github.com/microsoft/TypeScript/issues/42873
*/
// eslint-disable-next-line @typescript-eslint/no-unused-vars
import type { TSESLint } from "@typescript-eslint/utils";
import { plugin } from "./plugin";
import recommendedConfig from "./configs/recommended";
import typescriptConfig from "./configs/typescript";
const pluginLegacy = {
rules: plugin.rules,
configs: {
recommended: {
plugins: ["solid"],
env: {
browser: true,
es6: true,
},
parserOptions: recommendedConfig.languageOptions.parserOptions,
rules: recommendedConfig.rules,
},
typescript: {
plugins: ["solid"],
env: {
browser: true,
es6: true,
},
parserOptions: {
sourceType: "module",
},
rules: typescriptConfig.rules,
},
},
};
// Must be `export = ` for eslint to load everything
export = pluginLegacy;

55
node_modules/eslint-plugin-solid/src/plugin.ts generated vendored Normal file
View File

@@ -0,0 +1,55 @@
/**
* FIXME: remove this comments and import when below issue is fixed.
* This import is necessary for type generation due to a bug in the TypeScript compiler.
* See: https://github.com/microsoft/TypeScript/issues/42873
*/
// eslint-disable-next-line @typescript-eslint/no-unused-vars
import type { TSESLint } from "@typescript-eslint/utils";
import componentsReturnOnce from "./rules/components-return-once";
import eventHandlers from "./rules/event-handlers";
import imports from "./rules/imports";
import jsxNoDuplicateProps from "./rules/jsx-no-duplicate-props";
import jsxNoScriptUrl from "./rules/jsx-no-script-url";
import jsxNoUndef from "./rules/jsx-no-undef";
import jsxUsesVars from "./rules/jsx-uses-vars";
import noDestructure from "./rules/no-destructure";
import noInnerHTML from "./rules/no-innerhtml";
import noProxyApis from "./rules/no-proxy-apis";
import noReactDeps from "./rules/no-react-deps";
import noReactSpecificProps from "./rules/no-react-specific-props";
import noUnknownNamespaces from "./rules/no-unknown-namespaces";
import preferClasslist from "./rules/prefer-classlist";
import preferFor from "./rules/prefer-for";
import preferShow from "./rules/prefer-show";
import reactivity from "./rules/reactivity";
import selfClosingComp from "./rules/self-closing-comp";
import styleProp from "./rules/style-prop";
import noArrayHandlers from "./rules/no-array-handlers";
// import validateJsxNesting from "./rules/validate-jsx-nesting";
const allRules = {
"components-return-once": componentsReturnOnce,
"event-handlers": eventHandlers,
imports,
"jsx-no-duplicate-props": jsxNoDuplicateProps,
"jsx-no-undef": jsxNoUndef,
"jsx-no-script-url": jsxNoScriptUrl,
"jsx-uses-vars": jsxUsesVars,
"no-destructure": noDestructure,
"no-innerhtml": noInnerHTML,
"no-proxy-apis": noProxyApis,
"no-react-deps": noReactDeps,
"no-react-specific-props": noReactSpecificProps,
"no-unknown-namespaces": noUnknownNamespaces,
"prefer-classlist": preferClasslist,
"prefer-for": preferFor,
"prefer-show": preferShow,
reactivity,
"self-closing-comp": selfClosingComp,
"style-prop": styleProp,
"no-array-handlers": noArrayHandlers,
// "validate-jsx-nesting": validateJsxNesting
};
export const plugin = { rules: allRules };

View File

@@ -0,0 +1,206 @@
/**
* FIXME: remove this comments and import when below issue is fixed.
* This import is necessary for type generation due to a bug in the TypeScript compiler.
* See: https://github.com/microsoft/TypeScript/issues/42873
*/
// eslint-disable-next-line @typescript-eslint/no-unused-vars
import type { TSESLint } from "@typescript-eslint/utils";
import { TSESTree as T, ESLintUtils } from "@typescript-eslint/utils";
import { getFunctionName, type FunctionNode } from "../utils";
const createRule = ESLintUtils.RuleCreator.withoutDocs;
const isNothing = (node?: T.Node): boolean => {
if (!node) {
return true;
}
switch (node.type) {
case "Literal":
return ([null, undefined, false, ""] as Array<unknown>).includes(node.value);
case "JSXFragment":
return !node.children || node.children.every(isNothing);
default:
return false;
}
};
const getLineLength = (loc: T.SourceLocation) => loc.end.line - loc.start.line + 1;
export 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/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: Array<{
/** switched to true by :exit if the current function is detected to be a component */
isComponent: boolean;
lastReturn: T.ReturnStatement | undefined;
earlyReturns: Array<T.ReturnStatement>;
}> = [];
const putIntoJSX = (node: T.Node): string => {
const text = context.getSourceCode().getText(node);
return node.type === "JSXElement" || node.type === "JSXFragment" ? text : `{${text}}`;
};
const currentFunction = () => functionStack[functionStack.length - 1];
const onFunctionEnter = (node: FunctionNode) => {
let lastReturn: T.ReturnStatement | undefined;
if (node.body.type === "BlockStatement") {
const { length } = node.body.body;
const last = length && node.body.body[length - 1];
if (last && last.type === "ReturnStatement") {
lastReturn = last;
}
}
functionStack.push({ isComponent: false, lastReturn, earlyReturns: [] });
};
const onFunctionExit = (node: FunctionNode) => {
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 as T.Identifier).name?.match(/^[A-Z]/))
) {
currentFunction().isComponent = false;
}
if (currentFunction().isComponent) {
// Warn on each early return
currentFunction().earlyReturns.forEach((earlyReturn) => {
context.report({
node: earlyReturn,
messageId: "noEarlyReturn",
});
});
const argument = currentFunction().lastReturn?.argument;
if (argument?.type === "ConditionalExpression") {
const sourceCode = context.getSourceCode();
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) {
// we have a nested ternary, use <Switch><Match /></Switch>
const fallbackStr = !isNothing(fallback)
? ` fallback={${sourceCode.getText(fallback)}}`
: "";
return fixer.replaceText(
argument,
`<Switch${fallbackStr}>\n${conditions
.map(
({ test, consequent }) =>
`<Match when={${sourceCode.getText(test)}}>${putIntoJSX(
consequent
)}</Match>`
)
.join("\n")}\n</Switch>`
);
}
if (isNothing(consequent)) {
// we have a single ternary and the consequent is nothing. Negate the condition and use a <Show>.
return fixer.replaceText(
argument,
`<Show when={!(${sourceCode.getText(test)})}>${putIntoJSX(alternate)}</Show>`
);
}
if (
isNothing(fallback) ||
getLineLength(consequent.loc) >= getLineLength(alternate.loc) * 1.5
) {
// we have a standard ternary, and the alternate is a bit shorter in LOC than the consequent, which
// should be enough to tell that it's logically a fallback instead of an equal branch.
const fallbackStr = !isNothing(fallback)
? ` fallback={${sourceCode.getText(fallback)}}`
: "";
return fixer.replaceText(
argument,
`<Show when={${sourceCode.getText(test)}}${fallbackStr}>${putIntoJSX(
consequent
)}</Show>`
);
}
// we have a standard ternary, but no signal from the user as to which branch is the "fallback" and
// which is the children. Move the whole conditional inside a JSX fragment.
return fixer.replaceText(argument, `<>${putIntoJSX(argument)}</>`);
},
});
} else if (argument?.type === "LogicalExpression")
if (argument.operator === "&&") {
const sourceCode = context.getSourceCode();
// we have a `return condition && expression`--put that in a <Show />
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 {
// we have some other kind of conditional, warn
context.report({
node: argument,
messageId: "noConditionalReturn",
});
}
}
// Pop on exit
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);
}
},
};
},
});

View File

@@ -0,0 +1,309 @@
/**
* FIXME: remove this comments and import when below issue is fixed.
* This import is necessary for type generation due to a bug in the TypeScript compiler.
* See: https://github.com/microsoft/TypeScript/issues/42873
*/
// eslint-disable-next-line @typescript-eslint/no-unused-vars
import type { TSESLint } from "@typescript-eslint/utils";
import { TSESTree as T, ESLintUtils, ASTUtils } from "@typescript-eslint/utils";
import { isDOMElementName } from "../utils";
const createRule = ESLintUtils.RuleCreator.withoutDocs;
const { getStaticValue } = ASTUtils;
const 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",
] as const;
type CommonEvent = (typeof COMMON_EVENTS)[number];
const COMMON_EVENTS_MAP = new Map<string, CommonEvent>(
(function* () {
for (const event of COMMON_EVENTS) {
yield [event.toLowerCase(), event] as const;
}
})()
);
const NONSTANDARD_EVENTS_MAP = {
ondoubleclick: "onDblClick",
};
const isCommonHandlerName = (
lowercaseHandlerName: string
): lowercaseHandlerName is Lowercase<CommonEvent> => COMMON_EVENTS_MAP.has(lowercaseHandlerName);
const getCommonEventHandlerName = (lowercaseHandlerName: Lowercase<CommonEvent>): CommonEvent =>
COMMON_EVENTS_MAP.get(lowercaseHandlerName)!;
const isNonstandardEventName = (
lowercaseEventName: string
): lowercaseEventName is keyof typeof NONSTANDARD_EVENTS_MAP =>
Boolean((NONSTANDARD_EVENTS_MAP as Record<string, string>)[lowercaseEventName]);
const getStandardEventHandlerName = (lowercaseEventName: keyof typeof NONSTANDARD_EVENTS_MAP) =>
NONSTANDARD_EVENTS_MAP[lowercaseEventName];
type MessageIds =
| "naming"
| "capitalization"
| "nonstandard"
| "make-handler"
| "make-attr"
| "detected-attr"
| "spread-handler";
type Options = [{ ignoreCase?: boolean; warnOnSpread?: boolean }?];
export default createRule<Options, MessageIds>({
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/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 = context.getSourceCode();
return {
JSXAttribute(node) {
const openingElement = node.parent as T.JSXOpeningElement;
if (
openingElement.name.type !== "JSXIdentifier" ||
!isDOMElementName(openingElement.name.name)
) {
return; // bail if this is not a DOM/SVG element or web component
}
if (node.name.type === "JSXNamespacedName") {
return; // bail early on attr:, on:, oncapture:, etc. props
}
// string name of the name node
const { name } = node.name;
if (!/^on[a-zA-Z]/.test(name)) {
return; // bail if Solid doesn't consider the prop name an event handler
}
let staticValue: ReturnType<typeof getStaticValue> = 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, context.getScope())) !== null &&
(typeof staticValue.value === "string" || typeof staticValue.value === "number")
) {
// One of the first things Solid (actually babel-plugin-dom-expressions) does with an
// attribute is determine if it can be inlined into a template string instead of
// injected programmatically. It runs
// `attribute.get("value").get("expression").evaluate().value` on attributes with
// JSXExpressionContainers, and if the statically evaluated value is a string or number,
// it inlines it. This runs even for attributes that follow the naming convention for
// event handlers. By starting an attribute name with "on", the user has signalled that
// they intend the attribute to be an event handler. If the attribute value would be
// inlined, report that.
// https://github.com/ryansolid/dom-expressions/blob/cb3be7558c731e2a442e9c7e07d25373c40cf2be/packages/babel-plugin-jsx-dom-expressions/src/dom/element.js#L347
context.report({
node,
messageId: "detected-attr",
data: {
name,
staticValue: staticValue.value,
},
});
} else if (node.value === null || node.value?.type === "Literal") {
// Check for same as above for literal values
context.report({
node,
messageId: "detected-attr",
data: {
name,
staticValue: node.value !== null ? node.value.value : true,
},
});
} else if (!context.options[0]?.ignoreCase) {
const lowercaseHandlerName = name.toLowerCase();
if (isNonstandardEventName(lowercaseHandlerName)) {
const fixedName = getStandardEventHandlerName(lowercaseHandlerName);
context.report({
node: node.name,
messageId: "nonstandard",
data: { name, fixedName },
fix: (fixer) => fixer.replaceText(node.name, fixedName),
});
} else if (isCommonHandlerName(lowercaseHandlerName)) {
const fixedName = getCommonEventHandlerName(lowercaseHandlerName);
if (fixedName !== name) {
// For common DOM event names, we know the user intended the prop to be an event handler.
// Fix it to have an uppercase third letter and be properly camel-cased.
context.report({
node: node.name,
messageId: "capitalization",
data: { name, fixedName },
fix: (fixer) => fixer.replaceText(node.name, fixedName),
});
}
} else if (name[2] === name[2].toLowerCase()) {
// this includes words like `only` and `ongoing` as well as unknown handlers like `onfoobar`.
// Enforce using either /^on[A-Z]/ (event handler) or /^attr:on[a-z]/ (forced regular attribute)
// to make user intent clear and code maximally readable
const handlerName = `on${name[2].toUpperCase()}${name.slice(3)}`;
const attrName = `attr:${name}`;
context.report({
node: node.name,
messageId: "naming",
data: { name, attrName, handlerName },
suggest: [
{
messageId: "make-handler",
data: { name, handlerName },
fix: (fixer) => fixer.replaceText(node.name, handlerName),
},
{
messageId: "make-attr",
data: { name, attrName },
fix: (fixer) => fixer.replaceText(node.name, attrName),
},
],
});
}
}
},
Property(node: T.Property) {
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;
// An event handler is being spread in (ex. <button {...{ onClick }} />), which doesn't
// actually add an event listener, just a plain attribute.
context.report({
node,
messageId: "spread-handler",
data: {
name: node.key.name,
},
*fix(fixer) {
const commaAfter = sourceCode.getTokenAfter(node);
yield fixer.remove(
(node.parent as T.ObjectExpression).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)}}`
);
},
});
}
}
}
},
};
},
});

199
node_modules/eslint-plugin-solid/src/rules/imports.ts generated vendored Normal file
View File

@@ -0,0 +1,199 @@
import { TSESTree as T, TSESLint, ESLintUtils } from "@typescript-eslint/utils";
import { appendImports, insertImports, removeSpecifier } from "../utils";
const createRule = ESLintUtils.RuleCreator.withoutDocs;
// Below: create maps of imports and types to designated import source.
// We could mess with `Object.keys(require("solid-js"))` to generate this, but requiring it from
// node activates the "node" export condition, which doesn't necessarily match what users will
// receive i.e. through bundlers. Instead, we're manually listing all of the public exports that
// should be imported from "solid-js", etc.
// ==============
type Source = "solid-js" | "solid-js/web" | "solid-js/store";
// Set up map of imports to module
const primitiveMap = new Map<string, Source>();
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");
}
// Set up map of type imports to module
const typeMap = new Map<string, Source>();
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");
}
const sourceRegex = /^solid-js(?:\/web|\/store)?$/;
const isSource = (source: string): source is Source => sourceRegex.test(source);
export default createRule({
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/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 = context.getSourceCode();
const program: T.Program = sourceCode.ast;
const correctDeclaration = program.body.find(
(node) =>
node.type === "ImportDeclaration" && node.source.value === correctSource
) as T.ImportDeclaration | undefined;
if (correctDeclaration) {
return [
removeSpecifier(fixer, sourceCode, specifier),
appendImports(fixer, sourceCode, correctDeclaration, [
sourceCode.getText(specifier),
]),
].filter(Boolean) as Array<TSESLint.RuleFix>;
}
const firstSolidDeclaration = program.body.find(
(node) => node.type === "ImportDeclaration" && isSource(node.source.value)
) as T.ImportDeclaration | undefined;
return [
removeSpecifier(fixer, sourceCode, specifier),
insertImports(
fixer,
sourceCode,
correctSource,
[sourceCode.getText(specifier)],
firstSolidDeclaration,
isType
),
];
},
});
}
}
}
},
};
},
});

View File

@@ -0,0 +1,96 @@
/**
* FIXME: remove this comments and import when below issue is fixed.
* This import is necessary for type generation due to a bug in the TypeScript compiler.
* See: https://github.com/microsoft/TypeScript/issues/42873
*/
// eslint-disable-next-line @typescript-eslint/no-unused-vars
import type { TSESLint } from "@typescript-eslint/utils";
import { TSESTree as T, ESLintUtils } from "@typescript-eslint/utils";
import { jsxGetAllProps } from "../utils";
const createRule = ESLintUtils.RuleCreator.withoutDocs;
/*
* This rule is adapted from eslint-plugin-react's jsx-no-duplicate-props rule under
* the MIT license, with some enhancements. Thank you for your work!
*/
type MessageIds = "noDuplicateProps" | "noDuplicateClass" | "noDuplicateChildren";
type Options = [{ ignoreCase?: boolean }?];
export default createRule<Options, MessageIds>({
meta: {
type: "problem",
docs: {
description: "Disallow passing the same prop twice in JSX.",
url: "https://github.com/solidjs-community/eslint-plugin-solid/blob/main/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 = new Set();
const checkPropName = (name: string, node: T.Node) => {
if (ignoreCase || name.startsWith("on")) {
name = name
.toLowerCase()
.replace(/^on(?:capture)?:/, "on")
.replace(/^(?:attr|prop):/, "");
}
if (props.has(name)) {
context.report({
node,
messageId: name === "class" ? "noDuplicateClass" : "noDuplicateProps",
});
}
props.add(name);
};
for (const [name, propNode] of jsxGetAllProps(node.attributes)) {
checkPropName(name, propNode);
}
const hasChildrenProp = props.has("children");
const hasChildren = (node.parent as T.JSXElement | T.JSXFragment).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(", "),
},
});
}
},
};
},
});

View File

@@ -0,0 +1,60 @@
/**
* FIXME: remove this comments and import when below issue is fixed.
* This import is necessary for type generation due to a bug in the TypeScript compiler.
* See: https://github.com/microsoft/TypeScript/issues/42873
*/
// eslint-disable-next-line @typescript-eslint/no-unused-vars
import type { TSESLint } from "@typescript-eslint/utils";
import { ESLintUtils, ASTUtils } from "@typescript-eslint/utils";
const createRule = ESLintUtils.RuleCreator.withoutDocs;
const { getStaticValue }: { getStaticValue: any } = ASTUtils;
// A javascript: URL can contain leading C0 control or \u0020 SPACE,
// and any newline or tab are filtered out as if they're not part of the URL.
// https://url.spec.whatwg.org/#url-parsing
// Tab or newline are defined as \r\n\t:
// https://infra.spec.whatwg.org/#ascii-tab-or-newline
// A C0 control is a code point in the range \u0000 NULL to \u001F
// INFORMATION SEPARATOR ONE, inclusive:
// https://infra.spec.whatwg.org/#c0-control-or-space
const 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; // eslint-disable-line no-control-regex
/**
* This rule is adapted from eslint-plugin-react's jsx-no-script-url rule under the MIT license.
* Thank you for your work!
*/
export default createRule({
meta: {
type: "problem",
docs: {
description: "Disallow javascript: URLs.",
url: "https://github.com/solidjs-community/eslint-plugin-solid/blob/main/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: { value: unknown } | null = getStaticValue(
node.value.type === "JSXExpressionContainer" ? node.value.expression : node.value,
context.getScope()
);
if (link && typeof link.value === "string" && isJavaScriptProtocol.test(link.value)) {
context.report({
node: node.value,
messageId: "noJSURL",
});
}
}
},
};
},
});

View File

@@ -0,0 +1,216 @@
/**
* FIXME: remove this comments and import when below issue is fixed.
* This import is necessary for type generation due to a bug in the TypeScript compiler.
* See: https://github.com/microsoft/TypeScript/issues/42873
*/
// eslint-disable-next-line @typescript-eslint/no-unused-vars
import type { TSESLint } from "@typescript-eslint/utils";
import { TSESTree as T, ESLintUtils } from "@typescript-eslint/utils";
import { isDOMElementName, formatList, appendImports, insertImports } from "../utils";
const createRule = ESLintUtils.RuleCreator.withoutDocs;
// Currently all of the control flow components are from 'solid-js'.
const AUTO_COMPONENTS = ["Show", "For", "Index", "Switch", "Match"];
const SOURCE_MODULE = "solid-js";
/*
* This rule is adapted from eslint-plugin-react's jsx-no-undef rule under
* the MIT license. Thank you for your work!
*/
type MessageIds = "undefined" | "customDirectiveUndefined" | "autoImport";
type Options = [
{
allowGlobals?: boolean;
autoImport?: boolean;
typescriptEnabled?: boolean;
}?
];
export default createRule<Options, MessageIds>({
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/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 = new Set<string>();
/**
* Compare an identifier with the variables declared in the scope
* @param {ASTNode} node - Identifier or JSXIdentifier node
* @returns {void}
*/
function checkIdentifierInJSX(
node: T.Identifier | T.JSXIdentifier,
{
isComponent,
isCustomDirective,
}: { isComponent?: boolean; isCustomDirective?: boolean } = {}
) {
let scope = context.getScope();
const sourceCode = context.getSourceCode();
const sourceType = sourceCode.ast.sourceType;
const scopeUpperBound = !allowGlobals && sourceType === "module" ? "module" : "global";
const variables = [...scope.variables];
// Ignore 'this' keyword (also maked as JSXIdentifier when used in JSX)
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);
// Temporary fix for babel-eslint
if (scope.childScopes[0].childScopes.length) {
variables.push(...scope.childScopes[0].childScopes[0].variables);
}
}
if (variables.find((variable) => variable.name === node.name)) {
return;
}
if (
isComponent &&
autoImport &&
AUTO_COMPONENTS.includes(node.name) &&
!missingComponentsSet.has(node.name)
) {
// track which names are undefined
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: T.Node | undefined;
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 as any).object;
} while (n && n.type !== "JSXIdentifier");
if (n) {
checkIdentifierInJSX(n);
}
break;
default:
break;
}
},
"JSXAttribute > JSXNamespacedName": (node: T.JSXNamespacedName) => {
// <Element use:X /> applies the `X` custom directive to the element, where `X` must be an identifier in scope.
if (
node.namespace?.type === "JSXIdentifier" &&
node.namespace.name === "use" &&
node.name?.type === "JSXIdentifier"
) {
checkIdentifierInJSX(node.name, { isCustomDirective: true });
}
},
"Program:exit": (programNode: T.Program) => {
// add in any auto import components used in the program
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
) as T.ImportDeclaration | undefined;
if (importNode) {
context.report({
node: importNode,
messageId: "autoImport",
data: {
imports: formatList(missingComponents), // "Show, For, and Switch"
source: SOURCE_MODULE,
},
fix: (fixer) => {
return appendImports(fixer, context.getSourceCode(), importNode, missingComponents);
},
});
} else {
context.report({
node: programNode,
messageId: "autoImport",
data: {
imports: formatList(missingComponents),
source: SOURCE_MODULE,
},
fix: (fixer) => {
// insert `import { missing, identifiers } from "solid-js"` at top of module
return insertImports(fixer, context.getSourceCode(), "solid-js", missingComponents);
},
});
}
}
},
};
},
});

View File

@@ -0,0 +1,65 @@
/**
* FIXME: remove this comments and import when below issue is fixed.
* This import is necessary for type generation due to a bug in the TypeScript compiler.
* See: https://github.com/microsoft/TypeScript/issues/42873
*/
// eslint-disable-next-line @typescript-eslint/no-unused-vars
import type { TSESLint } from "@typescript-eslint/utils";
import { TSESTree as T, ESLintUtils } from "@typescript-eslint/utils";
const createRule = ESLintUtils.RuleCreator.withoutDocs;
/*
* This rule is lifted almost verbatim from eslint-plugin-react's
* jsx-uses-vars rule under the MIT license. Thank you for your work!
* Solid's custom directives are also handled.
*/
export default createRule({
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/docs/jsx-uses-vars.md",
},
schema: [],
// eslint-disable-next-line eslint-plugin/prefer-message-ids
messages: {},
},
defaultOptions: [],
create(context) {
return {
JSXOpeningElement(node) {
let parent: T.JSXTagNameExpression;
switch (node.name.type) {
case "JSXNamespacedName": // <Foo:Bar>
return;
case "JSXIdentifier": // <Foo>
context.markVariableAsUsed(node.name.name);
break;
case "JSXMemberExpression": // <Foo...Bar>
parent = node.name.object;
while (parent?.type === "JSXMemberExpression") {
parent = parent.object;
}
if (parent.type === "JSXIdentifier") {
context.markVariableAsUsed(parent.name);
}
break;
}
},
"JSXAttribute > JSXNamespacedName": (node: T.JSXNamespacedName) => {
// <Element use:X /> applies the `X` custom directive to the element, where `X` must be an identifier in scope.
if (
node.namespace?.type === "JSXIdentifier" &&
node.namespace.name === "use" &&
node.name?.type === "JSXIdentifier"
) {
context.markVariableAsUsed(node.name.name);
}
},
};
},
});

View File

@@ -0,0 +1,57 @@
/**
* FIXME: remove this comments and import when below issue is fixed.
* This import is necessary for type generation due to a bug in the TypeScript compiler.
* See: https://github.com/microsoft/TypeScript/issues/42873
*/
// eslint-disable-next-line @typescript-eslint/no-unused-vars
import type { TSESLint } from "@typescript-eslint/utils";
import { TSESTree as T, ESLintUtils } from "@typescript-eslint/utils";
import { isDOMElementName, trace } from "../utils";
const createRule = ESLintUtils.RuleCreator.withoutDocs;
export default createRule({
meta: {
type: "problem",
docs: {
description: "Disallow usage of type-unsafe event handlers.",
url: "https://github.com/solidjs-community/eslint-plugin-solid/blob/main/docs/no-array-handlers.md",
},
schema: [],
messages: {
noArrayHandlers: "Passing an array as an event handler is potentially type-unsafe.",
},
},
defaultOptions: [],
create(context) {
return {
JSXAttribute(node) {
const openingElement = node.parent as T.JSXOpeningElement;
if (
openingElement.name.type !== "JSXIdentifier" ||
!isDOMElementName(openingElement.name.name)
) {
return; // bail if this is not a DOM/SVG element or web component
}
const isNamespacedHandler =
node.name.type === "JSXNamespacedName" && node.name.namespace.name === "on";
const isNormalEventHandler =
node.name.type === "JSXIdentifier" && /^on[a-zA-Z]/.test(node.name.name);
if (
(isNamespacedHandler || isNormalEventHandler) &&
node.value?.type === "JSXExpressionContainer" &&
trace(node.value.expression, context.getScope()).type === "ArrayExpression"
) {
// Warn if passed an array
context.report({
node,
messageId: "noArrayHandlers",
});
}
},
};
},
});

View File

@@ -0,0 +1,230 @@
import { TSESTree as T, TSESLint, ESLintUtils, ASTUtils } from "@typescript-eslint/utils";
import type { FunctionNode } from "../utils";
const createRule = ESLintUtils.RuleCreator.withoutDocs;
const { getStringIfConstant } = ASTUtils;
const getName = (node: T.Node): string | null => {
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);
}
};
interface PropertyInfo {
real: T.Literal | T.Identifier | T.Expression;
var: string;
computed: boolean;
init: T.Expression | undefined;
}
// Given ({ 'hello-world': helloWorld = 5 }), returns { real: Literal('hello-world'), var: 'helloWorld', computed: false, init: Literal(5) }
const getPropertyInfo = (prop: T.Property): PropertyInfo | null => {
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 : undefined,
};
} else {
return null;
}
};
export default createRule({
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/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: Array<{
/** switched to true by :exit if JSX is detected in the current function */
hasJSX: boolean;
}> = [];
const currentFunction = () => functionStack[functionStack.length - 1];
const onFunctionEnter = () => {
functionStack.push({ hasJSX: false });
};
const onFunctionExit = (node: FunctionNode) => {
if (node.params.length === 1) {
const props = node.params[0];
if (
props.type === "ObjectPattern" &&
currentFunction().hasJSX &&
node.parent?.type !== "JSXExpressionContainer" // "render props" aren't components
) {
// Props are destructured in the function params, not the body. We actually don't
// need to handle the case where props are destructured in the body, because that
// will be a violation of "solid/reactivity".
context.report({
node: props,
messageId: "noDestructure",
fix: (fixer) => fixDestructure(node, props, fixer),
});
}
}
// Pop on exit
functionStack.pop();
};
function* fixDestructure(
func: FunctionNode,
props: T.ObjectPattern,
fixer: TSESLint.RuleFixer
): Generator<TSESLint.RuleFix> {
const propsName = "props";
const properties = props.properties;
const propertyInfo: Array<PropertyInfo> = [];
let rest: T.RestElement | null = 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);
// Replace destructured props with a `props` identifier (`_props` in case of rest params/defaults)
const origProps = !(hasDefaults || rest) ? propsName : "_" + propsName;
if (props.typeAnnotation) {
// in `{ prop1, prop2 }: Props`, leave `: Props` alone
const range = [props.range[0], props.typeAnnotation.range[0]] as const;
yield fixer.replaceTextRange(range, origProps);
} else {
yield fixer.replaceText(props, origProps);
}
const sourceCode = context.getSourceCode();
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) {
// Insert a line that assigns _props
lineToInsert = ` const [${propsName}, ${
(rest.argument.type === "Identifier" && rest.argument.name) || "rest"
}] = splitProps(mergeProps({ ${defaultsObjectString()} }, ${origProps}), ${splitPropsArray()});`;
} else if (hasDefaults) {
// Insert a line that assigns _props merged with defaults to props
lineToInsert = ` const ${propsName} = mergeProps({ ${defaultsObjectString()} }, ${origProps});\n`;
} else if (rest) {
// Insert a line that keeps named props and extracts the rest into a new reactive rest object
lineToInsert = ` const [${propsName}, ${
(rest.argument.type === "Identifier" && rest.argument.name) || "rest"
}] = splitProps(${origProps}, ${splitPropsArray()});\n`;
}
if (lineToInsert) {
const body = func.body;
if (body.type === "BlockStatement") {
if (body.body.length > 0) {
// Inject lines handling defaults/rest params before the first statement in the block.
yield fixer.insertTextBefore(body.body[0], lineToInsert);
}
// with an empty block statement body, no need to inject code
} else {
// The function is an arrow function that implicitly returns an expression, possibly with wrapping parentheses.
// These must be removed to convert the function body to a block statement for code injection.
const maybeOpenParen = sourceCode.getTokenBefore(body);
if (maybeOpenParen?.value === "(") {
yield fixer.remove(maybeOpenParen);
}
const maybeCloseParen = sourceCode.getTokenAfter(body);
if (maybeCloseParen?.value === ")") {
yield fixer.remove(maybeCloseParen);
}
// Inject lines handling defaults/rest params
yield fixer.insertTextBefore(body, `{\n${lineToInsert} return (`);
yield fixer.insertTextAfter(body, `);\n}`);
}
}
const scope = sourceCode.scopeManager?.acquire(func);
if (scope) {
// iterate through destructured variables, associated with real node
for (const [info, variable] of propertyInfo.map(
(info) => [info, scope.set.get(info.var)] as const
)) {
if (variable) {
// replace all usages of the variable with props accesses
for (const reference of variable.references) {
if (reference.isReadOnly()) {
const access =
info.real.type === "Identifier" && !info.computed
? `.${info.real.name}`
: `[${sourceCode.getText(info.real)}]`;
yield fixer.replaceText(reference.identifier, `${propsName}${access}`);
}
}
}
}
}
}
return {
FunctionDeclaration: onFunctionEnter,
FunctionExpression: onFunctionEnter,
ArrowFunctionExpression: onFunctionEnter,
"FunctionDeclaration:exit": onFunctionExit,
"FunctionExpression:exit": onFunctionExit,
"ArrowFunctionExpression:exit": onFunctionExit,
JSXElement() {
if (functionStack.length) {
currentFunction().hasJSX = true;
}
},
JSXFragment() {
if (functionStack.length) {
currentFunction().hasJSX = true;
}
},
};
},
});

View File

@@ -0,0 +1,143 @@
/**
* FIXME: remove this comments and import when below issue is fixed.
* This import is necessary for type generation due to a bug in the TypeScript compiler.
* See: https://github.com/microsoft/TypeScript/issues/42873
*/
// eslint-disable-next-line @typescript-eslint/no-unused-vars
import type { TSESLint } from "@typescript-eslint/utils";
import { ESLintUtils, ASTUtils } from "@typescript-eslint/utils";
import isHtml from "is-html";
import { jsxPropName } from "../utils";
const createRule = ESLintUtils.RuleCreator.withoutDocs;
const { getStringIfConstant } = ASTUtils;
type MessageIds = "dangerous" | "conflict" | "notHtml" | "useInnerText" | "dangerouslySetInnerHTML";
type Options = [{ allowStatic?: boolean }?];
export default createRule<Options, MessageIds>({
meta: {
type: "problem",
docs: {
description:
"Disallow usage of the innerHTML attribute, which can often lead to security vulnerabilities.",
url: "https://github.com/solidjs-community/eslint-plugin-solid/blob/main/docs/no-innerhtml.md",
},
fixable: "code",
hasSuggestions: true,
schema: [
{
type: "object",
properties: {
allowStatic: {
description:
"if the innerHTML value is guaranteed to be a static HTML string (i.e. no user input), allow it",
type: "boolean",
default: true,
},
},
additionalProperties: false,
},
],
messages: {
dangerous:
"The innerHTML attribute is dangerous; passing unsanitized input can lead to security vulnerabilities.",
conflict:
"The innerHTML attribute should not be used on an element with child elements; they will be overwritten.",
notHtml: "The string passed to innerHTML does not appear to be valid HTML.",
useInnerText: "For text content, using innerText is clearer and safer.",
dangerouslySetInnerHTML:
"The dangerouslySetInnerHTML prop is not supported; use innerHTML instead.",
},
},
defaultOptions: [{ allowStatic: true }],
create(context) {
const allowStatic = Boolean(context.options[0]?.allowStatic ?? true);
return {
JSXAttribute(node) {
if (jsxPropName(node) === "dangerouslySetInnerHTML") {
if (
node.value?.type === "JSXExpressionContainer" &&
node.value.expression.type === "ObjectExpression" &&
node.value.expression.properties.length === 1
) {
const htmlProp = node.value.expression.properties[0];
if (
htmlProp.type === "Property" &&
htmlProp.key.type === "Identifier" &&
htmlProp.key.name === "__html"
) {
context.report({
node,
messageId: "dangerouslySetInnerHTML",
fix: (fixer) => {
const propRange = node.range;
const valueRange = htmlProp.value.range;
return [
fixer.replaceTextRange([propRange[0], valueRange[0]], "innerHTML={"),
fixer.replaceTextRange([valueRange[1], propRange[1]], "}"),
];
},
});
} else {
context.report({
node,
messageId: "dangerouslySetInnerHTML",
});
}
} else {
context.report({
node,
messageId: "dangerouslySetInnerHTML",
});
}
return;
} else if (jsxPropName(node) !== "innerHTML") {
return;
}
if (allowStatic) {
const innerHtmlNode =
node.value?.type === "JSXExpressionContainer" ? node.value.expression : node.value;
const innerHtml = innerHtmlNode && getStringIfConstant(innerHtmlNode);
if (typeof innerHtml === "string") {
if (isHtml(innerHtml)) {
// go up to enclosing JSXElement and check if it has children
if (
node.parent?.parent?.type === "JSXElement" &&
node.parent.parent.children?.length
) {
context.report({
node: node.parent.parent, // report error on JSXElement instead of JSXAttribute
messageId: "conflict",
});
}
} else {
context.report({
node,
messageId: "notHtml",
suggest: [
{
fix: (fixer) => fixer.replaceText(node.name, "innerText"),
messageId: "useInnerText",
},
],
});
}
} else {
context.report({
node,
messageId: "dangerous",
});
}
} else {
context.report({
node,
messageId: "dangerous",
});
}
},
};
},
});

View File

@@ -0,0 +1,96 @@
/**
* FIXME: remove this comments and import when below issue is fixed.
* This import is necessary for type generation due to a bug in the TypeScript compiler.
* See: https://github.com/microsoft/TypeScript/issues/42873
*/
// eslint-disable-next-line @typescript-eslint/no-unused-vars
import type { TSESLint } from "@typescript-eslint/utils";
import { TSESTree as T, ESLintUtils } from "@typescript-eslint/utils";
import { isFunctionNode, trackImports, isPropsByName, trace } from "../utils";
const createRule = ESLintUtils.RuleCreator.withoutDocs;
export default createRule({
meta: {
type: "problem",
docs: {
description:
"Disallow usage of APIs that use ES6 Proxies, only to target environments that don't support them.",
url: "https://github.com/solidjs-community/eslint-plugin-solid/blob/main/docs/no-proxy-apis.md",
},
schema: [],
messages: {
noStore: "Solid Store APIs use Proxies, which are incompatible with your target environment.",
spreadCall:
"Using a function call in JSX spread makes Solid use Proxies, which are incompatible with your target environment.",
spreadMember:
"Using a property access in JSX spread makes Solid use Proxies, which are incompatible with your target environment.",
proxyLiteral: "Proxies are incompatible with your target environment.",
mergeProps:
"If you pass a function to `mergeProps`, it will create a Proxy, which are incompatible with your target environment.",
},
},
defaultOptions: [],
create(context) {
const { matchImport, handleImportDeclaration } = trackImports();
return {
ImportDeclaration(node) {
handleImportDeclaration(node); // track import aliases
const source = node.source.value;
if (source === "solid-js/store") {
context.report({
node,
messageId: "noStore",
});
}
},
"JSXSpreadAttribute MemberExpression"(node: T.MemberExpression) {
context.report({ node, messageId: "spreadMember" });
},
"JSXSpreadAttribute CallExpression"(node: T.CallExpression) {
context.report({ node, messageId: "spreadCall" });
},
CallExpression(node) {
if (node.callee.type === "Identifier") {
if (matchImport("mergeProps", node.callee.name)) {
node.arguments
.filter((arg) => {
if (arg.type === "SpreadElement") return true;
const traced = trace(arg, context.getScope());
return (
(traced.type === "Identifier" && !isPropsByName(traced.name)) ||
isFunctionNode(traced)
);
})
.forEach((badArg) => {
context.report({
node: badArg,
messageId: "mergeProps",
});
});
}
} else if (node.callee.type === "MemberExpression") {
if (
node.callee.object.type === "Identifier" &&
node.callee.object.name === "Proxy" &&
node.callee.property.type === "Identifier" &&
node.callee.property.name === "revocable"
) {
context.report({
node,
messageId: "proxyLiteral",
});
}
}
},
NewExpression(node) {
if (node.callee.type === "Identifier" && node.callee.name === "Proxy") {
context.report({ node, messageId: "proxyLiteral" });
}
},
};
},
});

View File

@@ -0,0 +1,63 @@
/**
* FIXME: remove this comments and import when below issue is fixed.
* This import is necessary for type generation due to a bug in the TypeScript compiler.
* See: https://github.com/microsoft/TypeScript/issues/42873
*/
// eslint-disable-next-line @typescript-eslint/no-unused-vars
import type { TSESLint } from "@typescript-eslint/utils";
import { ESLintUtils } from "@typescript-eslint/utils";
import { isFunctionNode, trace, trackImports } from "../utils";
const createRule = ESLintUtils.RuleCreator.withoutDocs;
export default createRule({
meta: {
type: "problem",
docs: {
description: "Disallow usage of dependency arrays in `createEffect` and `createMemo`.",
url: "https://github.com/solidjs-community/eslint-plugin-solid/blob/main/docs/no-react-deps.md",
},
fixable: "code",
schema: [],
messages: {
noUselessDep:
"In Solid, `{{name}}` doesn't accept a dependency array because it automatically tracks its dependencies. If you really need to override the list of dependencies, use `on`.",
},
},
defaultOptions: [],
create(context) {
/** Tracks imports from 'solid-js', handling aliases. */
const { matchImport, handleImportDeclaration } = trackImports();
return {
ImportDeclaration: handleImportDeclaration,
CallExpression(node) {
if (
node.callee.type === "Identifier" &&
matchImport(["createEffect", "createMemo"], node.callee.name) &&
node.arguments.length === 2 &&
node.arguments.every((arg) => arg.type !== "SpreadElement")
) {
// grab both arguments, tracing any variables to their actual values if possible
const [arg0, arg1] = node.arguments.map((arg) => trace(arg, context.getScope()));
if (isFunctionNode(arg0) && arg0.params.length === 0 && arg1.type === "ArrayExpression") {
// A second argument that looks like a dependency array was passed to
// createEffect/createMemo, and the inline function doesn't accept a parameter, so it
// can't just be an initial value.
context.report({
node: node.arguments[1], // if this is a variable, highlight the usage, not the initialization
messageId: "noUselessDep",
data: {
name: node.callee.name,
},
// remove dep array if it's given inline, otherwise don't fix
fix: arg1 === node.arguments[1] ? (fixer) => fixer.remove(arg1) : undefined,
});
}
}
},
};
},
});

View File

@@ -0,0 +1,60 @@
import { TSESLint, ESLintUtils } from "@typescript-eslint/utils";
import { isDOMElementName, jsxGetProp, jsxHasProp } from "../utils";
const createRule = ESLintUtils.RuleCreator.withoutDocs;
const reactSpecificProps = [
{ from: "className", to: "class" },
{ from: "htmlFor", to: "for" },
];
export default createRule({
meta: {
type: "problem",
docs: {
description:
"Disallow usage of React-specific `className`/`htmlFor` props, which were deprecated in v1.4.0.",
url: "https://github.com/solidjs-community/eslint-plugin-solid/blob/main/docs/no-react-specific-props.md",
},
fixable: "code",
schema: [],
messages: {
prefer: "Prefer the `{{ to }}` prop over the deprecated `{{ from }}` prop.",
noUselessKey: "Elements in a <For> or <Index> list do not need a key prop.",
},
},
defaultOptions: [],
create(context) {
return {
JSXOpeningElement(node) {
for (const { from, to } of reactSpecificProps) {
const classNameAttribute = jsxGetProp(node.attributes, from);
if (classNameAttribute) {
// only auto-fix if there is no class prop defined
const fix = !jsxHasProp(node.attributes, to)
? (fixer: TSESLint.RuleFixer) => fixer.replaceText(classNameAttribute.name, to)
: undefined;
context.report({
node: classNameAttribute,
messageId: "prefer",
data: { from, to },
fix,
});
}
}
if (node.name.type === "JSXIdentifier" && isDOMElementName(node.name.name)) {
const keyProp = jsxGetProp(node.attributes, "key");
if (keyProp) {
// no DOM element has a 'key' prop, so we can assert that this is a holdover from React.
context.report({
node: keyProp,
messageId: "noUselessKey",
fix: (fixer) => fixer.remove(keyProp),
});
}
}
},
};
},
});

View File

@@ -0,0 +1,108 @@
/**
* FIXME: remove this comments and import when below issue is fixed.
* This import is necessary for type generation due to a bug in the TypeScript compiler.
* See: https://github.com/microsoft/TypeScript/issues/42873
*/
// eslint-disable-next-line @typescript-eslint/no-unused-vars
import type { TSESLint } from "@typescript-eslint/utils";
import { ESLintUtils, TSESTree as T } from "@typescript-eslint/utils";
import { isDOMElementName } from "../utils";
const createRule = ESLintUtils.RuleCreator.withoutDocs;
const knownNamespaces = ["on", "oncapture", "use", "prop", "attr"];
const styleNamespaces = ["style", "class"];
const otherNamespaces = ["xmlns", "xlink"];
type MessageIds = "unknown" | "style" | "component" | "component-suggest";
type Options = [{ allowedNamespaces: Array<string> }?];
export default createRule<Options, MessageIds>({
meta: {
type: "problem",
docs: {
description:
"Enforce using only Solid-specific namespaced attribute names (i.e. `'on:'` in `<div on:click={...} />`).",
url: "https://github.com/solidjs-community/eslint-plugin-solid/blob/main/docs/no-unknown-namespaces.md",
},
hasSuggestions: true,
schema: [
{
type: "object",
properties: {
allowedNamespaces: {
description: "an array of additional namespace names to allow",
type: "array",
items: {
type: "string",
},
default: [],
minItems: 1,
uniqueItems: true,
},
},
additionalProperties: false,
},
],
messages: {
unknown: `'{{namespace}}:' is not one of Solid's special prefixes for JSX attributes (${knownNamespaces
.map((n) => `'${n}:'`)
.join(", ")}).`,
style:
"Using the '{{namespace}}:' special prefix is potentially confusing, prefer the '{{namespace}}' prop instead.",
component: "Namespaced props have no effect on components.",
"component-suggest": "Replace {{namespace}}:{{name}} with {{name}}.",
},
},
defaultOptions: [],
create(context) {
const explicitlyAllowedNamespaces = context.options?.[0]?.allowedNamespaces;
return {
"JSXAttribute > JSXNamespacedName": (node: T.JSXNamespacedName) => {
const openingElement = node.parent!.parent as T.JSXOpeningElement;
if (
openingElement.name.type === "JSXIdentifier" &&
!isDOMElementName(openingElement.name.name)
) {
// no namespaces on Solid component elements
context.report({
node,
messageId: "component",
suggest: [
{
messageId: "component-suggest",
data: { namespace: node.namespace.name, name: node.name.name },
fix: (fixer) => fixer.replaceText(node, node.name.name),
},
],
});
return;
}
const namespace = node.namespace?.name;
if (
!(
knownNamespaces.includes(namespace) ||
otherNamespaces.includes(namespace) ||
explicitlyAllowedNamespaces?.includes(namespace)
)
) {
if (styleNamespaces.includes(namespace)) {
context.report({
node,
messageId: "style",
data: { namespace },
});
} else {
context.report({
node,
messageId: "unknown",
data: { namespace },
});
}
}
},
};
},
});

View File

@@ -0,0 +1,93 @@
/**
* FIXME: remove this comments and import when below issue is fixed.
* This import is necessary for type generation due to a bug in the TypeScript compiler.
* See: https://github.com/microsoft/TypeScript/issues/42873
*/
// eslint-disable-next-line @typescript-eslint/no-unused-vars
import type { TSESLint } from "@typescript-eslint/utils";
import { ESLintUtils, TSESTree as T } from "@typescript-eslint/utils";
import { jsxHasProp, jsxPropName } from "../utils";
const createRule = ESLintUtils.RuleCreator.withoutDocs;
type MessageIds = "preferClasslist";
type Options = [{ classnames?: Array<string> }?];
export default createRule<Options, MessageIds>({
meta: {
type: "problem",
docs: {
description:
"Enforce using the classlist prop over importing a classnames helper. The classlist prop accepts an object `{ [class: string]: boolean }` just like classnames.",
url: "https://github.com/solidjs-community/eslint-plugin-solid/blob/main/docs/prefer-classlist.md",
},
fixable: "code",
schema: [
{
type: "object",
properties: {
classnames: {
type: "array",
description: "An array of names to treat as `classnames` functions",
default: ["cn", "clsx", "classnames"],
items: {
type: "string",
},
minItems: 1,
uniqueItems: true,
},
},
additionalProperties: false,
},
],
messages: {
preferClasslist:
"The classlist prop should be used instead of {{ classnames }} to efficiently set classes based on an object.",
},
deprecated: true,
},
defaultOptions: [],
create(context) {
const classnames = context.options[0]?.classnames ?? ["cn", "clsx", "classnames"];
return {
JSXAttribute(node) {
if (
["class", "className"].indexOf(jsxPropName(node)) === -1 ||
jsxHasProp(
(node.parent as T.JSXOpeningElement | undefined)?.attributes ?? [],
"classlist"
)
) {
return;
}
if (node.value?.type === "JSXExpressionContainer") {
const expr = node.value.expression;
if (
expr.type === "CallExpression" &&
expr.callee.type === "Identifier" &&
classnames.indexOf(expr.callee.name) !== -1 &&
expr.arguments.length === 1 &&
expr.arguments[0].type === "ObjectExpression"
) {
context.report({
node,
messageId: "preferClasslist",
data: {
classnames: expr.callee.name,
},
fix: (fixer) => {
const attrRange = node.range;
const objectRange = expr.arguments[0].range;
return [
fixer.replaceTextRange([attrRange[0], objectRange[0]], "classlist={"),
fixer.replaceTextRange([objectRange[1], attrRange[1]], "}"),
];
},
});
}
}
},
};
},
});

View File

@@ -0,0 +1,93 @@
/**
* FIXME: remove this comments and import when below issue is fixed.
* This import is necessary for type generation due to a bug in the TypeScript compiler.
* See: https://github.com/microsoft/TypeScript/issues/42873
*/
// eslint-disable-next-line @typescript-eslint/no-unused-vars
import type { TSESLint } from "@typescript-eslint/utils";
import { TSESTree as T, ESLintUtils, ASTUtils } from "@typescript-eslint/utils";
import { isFunctionNode, isJSXElementOrFragment } from "../utils";
const createRule = ESLintUtils.RuleCreator.withoutDocs;
const { getPropertyName } = ASTUtils;
export default createRule({
meta: {
type: "problem",
docs: {
description:
"Enforce using Solid's `<For />` component for mapping an array to JSX elements.",
url: "https://github.com/solidjs-community/eslint-plugin-solid/blob/main/docs/prefer-for.md",
},
fixable: "code",
schema: [],
messages: {
preferFor:
"Use Solid's `<For />` component for efficiently rendering lists. Array#map causes DOM elements to be recreated.",
preferForOrIndex:
"Use Solid's `<For />` component or `<Index />` component for rendering lists. Array#map causes DOM elements to be recreated.",
},
},
defaultOptions: [],
create(context) {
const reportPreferFor = (node: T.CallExpression) => {
const jsxExpressionContainerNode = node.parent as T.JSXExpressionContainer;
const arrayNode = (node.callee as T.MemberExpression).object;
const mapFnNode = node.arguments[0];
context.report({
node,
messageId: "preferFor",
fix: (fixer) => {
const beforeArray: [number, number] = [
jsxExpressionContainerNode.range[0],
arrayNode.range[0],
];
const betweenArrayAndMapFn: [number, number] = [arrayNode.range[1], mapFnNode.range[0]];
const afterMapFn: [number, number] = [
mapFnNode.range[1],
jsxExpressionContainerNode.range[1],
];
// We can insert the <For /> component
return [
fixer.replaceTextRange(beforeArray, "<For each={"),
fixer.replaceTextRange(betweenArrayAndMapFn, "}>{"),
fixer.replaceTextRange(afterMapFn, "}</For>"),
];
},
});
};
return {
CallExpression(node) {
const callOrChain = node.parent?.type === "ChainExpression" ? node.parent : node;
if (
callOrChain.parent?.type === "JSXExpressionContainer" &&
isJSXElementOrFragment(callOrChain.parent.parent)
) {
// check for Array.prototype.map in JSX
if (
node.callee.type === "MemberExpression" &&
getPropertyName(node.callee) === "map" &&
node.arguments.length === 1 && // passing thisArg to Array.prototype.map is rare, deopt in that case
isFunctionNode(node.arguments[0])
) {
const mapFnNode = node.arguments[0];
if (mapFnNode.params.length === 1 && mapFnNode.params[0].type !== "RestElement") {
// The map fn doesn't take an index param, so it can't possibly be an index-keyed list. Use <For />.
// The returned JSX, if it's coming from React, will have an unnecessary `key` prop to be removed in
// the useless-keys rule.
reportPreferFor(node);
} else {
// Too many possible solutions to make a suggestion or fix
context.report({
node,
messageId: "preferForOrIndex",
});
}
}
}
},
};
},
});

View File

@@ -0,0 +1,101 @@
/**
* FIXME: remove this comments and import when below issue is fixed.
* This import is necessary for type generation due to a bug in the TypeScript compiler.
* See: https://github.com/microsoft/TypeScript/issues/42873
*/
// eslint-disable-next-line @typescript-eslint/no-unused-vars
import type { TSESLint } from "@typescript-eslint/utils";
import { TSESTree as T, ESLintUtils } from "@typescript-eslint/utils";
import { isJSXElementOrFragment } from "../utils";
const createRule = ESLintUtils.RuleCreator.withoutDocs;
const EXPENSIVE_TYPES = ["JSXElement", "JSXFragment", "Identifier"];
export default createRule({
meta: {
type: "problem",
docs: {
description:
"Enforce using Solid's `<Show />` component for conditionally showing content. Solid's compiler covers this case, so it's a stylistic rule only.",
url: "https://github.com/solidjs-community/eslint-plugin-solid/blob/main/docs/prefer-show.md",
},
fixable: "code",
schema: [],
messages: {
preferShowAnd: "Use Solid's `<Show />` component for conditionally showing content.",
preferShowTernary:
"Use Solid's `<Show />` component for conditionally showing content with a fallback.",
},
},
defaultOptions: [],
create(context) {
const sourceCode = context.getSourceCode();
const putIntoJSX = (node: T.Node): string => {
const text = sourceCode.getText(node);
return isJSXElementOrFragment(node) ? text : `{${text}}`;
};
const logicalExpressionHandler = (node: T.LogicalExpression) => {
if (node.operator === "&&" && EXPENSIVE_TYPES.includes(node.right.type)) {
context.report({
node,
messageId: "preferShowAnd",
fix: (fixer) =>
fixer.replaceText(
node.parent?.type === "JSXExpressionContainer" &&
isJSXElementOrFragment(node.parent.parent)
? node.parent
: node,
`<Show when={${sourceCode.getText(node.left)}}>${putIntoJSX(node.right)}</Show>`
),
});
}
};
const conditionalExpressionHandler = (node: T.ConditionalExpression) => {
if (
EXPENSIVE_TYPES.includes(node.consequent.type) ||
EXPENSIVE_TYPES.includes(node.alternate.type)
) {
context.report({
node,
messageId: "preferShowTernary",
fix: (fixer) =>
fixer.replaceText(
node.parent?.type === "JSXExpressionContainer" &&
isJSXElementOrFragment(node.parent.parent)
? node.parent
: node,
`<Show when={${sourceCode.getText(node.test)}} fallback={${sourceCode.getText(
node.alternate
)}}>${putIntoJSX(node.consequent)}</Show>`
),
});
}
};
return {
JSXExpressionContainer(node) {
if (!isJSXElementOrFragment(node.parent)) {
return;
}
if (node.expression.type === "LogicalExpression") {
logicalExpressionHandler(node.expression);
} else if (
node.expression.type === "ArrowFunctionExpression" &&
node.expression.body.type === "LogicalExpression"
) {
logicalExpressionHandler(node.expression.body);
} else if (node.expression.type === "ConditionalExpression") {
conditionalExpressionHandler(node.expression);
} else if (
node.expression.type === "ArrowFunctionExpression" &&
node.expression.body.type === "ConditionalExpression"
) {
conditionalExpressionHandler(node.expression.body);
}
},
};
},
});

1240
node_modules/eslint-plugin-solid/src/rules/reactivity.ts generated vendored Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,149 @@
/**
* FIXME: remove this comments and import when below issue is fixed.
* This import is necessary for type generation due to a bug in the TypeScript compiler.
* See: https://github.com/microsoft/TypeScript/issues/42873
*/
// eslint-disable-next-line @typescript-eslint/no-unused-vars
import type { TSESLint } from "@typescript-eslint/utils";
import { TSESTree as T, ESLintUtils } from "@typescript-eslint/utils";
import { isDOMElementName } from "../utils";
const createRule = ESLintUtils.RuleCreator.withoutDocs;
function isComponent(node: T.JSXOpeningElement) {
return (
(node.name.type === "JSXIdentifier" && !isDOMElementName(node.name.name)) ||
node.name.type === "JSXMemberExpression"
);
}
const voidDOMElementRegex =
/^(?:area|base|br|col|embed|hr|img|input|link|meta|param|source|track|wbr)$/;
function isVoidDOMElementName(name: string) {
return voidDOMElementRegex.test(name);
}
function childrenIsEmpty(node: T.JSXOpeningElement) {
return (node.parent as T.JSXElement).children.length === 0;
}
function childrenIsMultilineSpaces(node: T.JSXOpeningElement) {
const childrens = (node.parent as T.JSXElement).children;
return (
childrens.length === 1 &&
childrens[0].type === "JSXText" &&
childrens[0].value.indexOf("\n") !== -1 &&
childrens[0].value.replace(/(?!\xA0)\s/g, "") === ""
);
}
type MessageIds = "selfClose" | "dontSelfClose";
type Options = [{ component?: "all" | "none"; html?: "all" | "void" | "none" }?];
/**
* This rule is adapted from eslint-plugin-react's self-closing-comp rule under the MIT license,
* with some enhancements. Thank you for your work!
*/
export default createRule<Options, MessageIds>({
meta: {
type: "layout",
docs: {
description: "Disallow extra closing tags for components without children.",
url: "https://github.com/solidjs-community/eslint-plugin-solid/blob/main/docs/self-closing-comp.md",
},
fixable: "code",
schema: [
{
type: "object",
properties: {
component: {
type: "string",
description: "which Solid components should be self-closing when possible",
enum: ["all", "none"],
default: "all",
},
html: {
type: "string",
description: "which native elements should be self-closing when possible",
enum: ["all", "void", "none"],
default: "all",
},
},
additionalProperties: false,
},
],
messages: {
selfClose: "Empty components are self-closing.",
dontSelfClose: "This element should not be self-closing.",
},
},
defaultOptions: [],
create(context) {
function shouldBeSelfClosedWhenPossible(node: T.JSXOpeningElement): boolean {
if (isComponent(node)) {
const whichComponents = context.options[0]?.component ?? "all";
return whichComponents === "all";
} else if (node.name.type === "JSXIdentifier" && isDOMElementName(node.name.name)) {
const whichComponents = context.options[0]?.html ?? "all";
switch (whichComponents) {
case "all":
return true;
case "void":
return isVoidDOMElementName(node.name.name);
case "none":
return false;
}
}
return true; // shouldn't encounter
}
return {
JSXOpeningElement(node) {
const canSelfClose = childrenIsEmpty(node) || childrenIsMultilineSpaces(node);
if (canSelfClose) {
const shouldSelfClose = shouldBeSelfClosedWhenPossible(node);
if (shouldSelfClose && !node.selfClosing) {
context.report({
node,
messageId: "selfClose",
fix(fixer) {
// Represents the last character of the JSXOpeningElement, the '>' character
const openingElementEnding = node.range[1] - 1;
// Represents the last character of the JSXClosingElement, the '>' character
const closingElementEnding = (node.parent as T.JSXElement).closingElement!.range[1];
// Replace />.*<\/.*>/ with '/>'
const range = [openingElementEnding, closingElementEnding] as const;
return fixer.replaceTextRange(range, " />");
},
});
} else if (!shouldSelfClose && node.selfClosing) {
context.report({
node,
messageId: "dontSelfClose",
fix(fixer) {
const sourceCode = context.getSourceCode();
const tagName = context.getSourceCode().getText(node.name);
// Represents the last character of the JSXOpeningElement, the '>' character
const selfCloseEnding = node.range[1];
// Replace ' />' or '/>' with '></${tagName}>'
const lastTokens = sourceCode.getLastTokens(node, { count: 3 }); // JSXIdentifier, '/', '>'
const isSpaceBeforeSelfClose = sourceCode.isSpaceBetween?.(
lastTokens[0],
lastTokens[1]
);
const range = [
isSpaceBeforeSelfClose ? selfCloseEnding - 3 : selfCloseEnding - 2,
selfCloseEnding,
] as const;
return fixer.replaceTextRange(range, `></${tagName}>`);
},
});
}
}
},
};
},
});

View File

@@ -0,0 +1,137 @@
/**
* FIXME: remove this comments and import when below issue is fixed.
* This import is necessary for type generation due to a bug in the TypeScript compiler.
* See: https://github.com/microsoft/TypeScript/issues/42873
*/
// eslint-disable-next-line @typescript-eslint/no-unused-vars
import type { TSESLint } from "@typescript-eslint/utils";
import { TSESTree as T, ESLintUtils, ASTUtils } from "@typescript-eslint/utils";
import kebabCase from "kebab-case";
import { all as allCssProperties } from "known-css-properties";
import parse from "style-to-object";
import { jsxPropName } from "../utils";
const createRule = ESLintUtils.RuleCreator.withoutDocs;
const { getPropertyName, getStaticValue } = ASTUtils;
const lengthPercentageRegex = /\b(?:width|height|margin|padding|border-width|font-size)\b/i;
type MessageIds = "kebabStyleProp" | "invalidStyleProp" | "numericStyleValue" | "stringStyle";
type Options = [{ styleProps?: Array<string>; allowString?: boolean }?];
export default createRule<Options, MessageIds>({
meta: {
type: "problem",
docs: {
description:
"Require CSS properties in the `style` prop to be valid and kebab-cased (ex. 'font-size'), not camel-cased (ex. 'fontSize') like in React, " +
"and that property values with dimensions are strings, not numbers with implicit 'px' units.",
url: "https://github.com/solidjs-community/eslint-plugin-solid/blob/main/docs/style-prop.md",
},
fixable: "code",
schema: [
{
type: "object",
properties: {
styleProps: {
description: "an array of prop names to treat as a CSS style object",
default: ["style"],
type: "array",
items: {
type: "string",
},
minItems: 1,
uniqueItems: true,
},
allowString: {
description:
"if allowString is set to true, this rule will not convert a style string literal into a style object (not recommended for performance)",
type: "boolean",
default: false,
},
},
additionalProperties: false,
},
],
messages: {
kebabStyleProp: "Use {{ kebabName }} instead of {{ name }}.",
invalidStyleProp: "{{ name }} is not a valid CSS property.",
numericStyleValue:
'This CSS property value should be a string with a unit; Solid does not automatically append a "px" unit.',
stringStyle: "Use an object for the style prop instead of a string.",
},
},
defaultOptions: [],
create(context) {
const allCssPropertiesSet: Set<string> = new Set(allCssProperties);
const allowString = Boolean(context.options[0]?.allowString);
const styleProps = context.options[0]?.styleProps || ["style"];
return {
JSXAttribute(node) {
if (styleProps.indexOf(jsxPropName(node)) === -1) {
return;
}
const style =
node.value?.type === "JSXExpressionContainer" ? node.value.expression : node.value;
if (!style) {
return;
} else if (style.type === "Literal" && typeof style.value === "string" && !allowString) {
// Convert style="font-size: 10px" to style={{ "font-size": "10px" }}
let objectStyles: Record<string, string> | undefined;
try {
objectStyles = parse(style.value) ?? undefined;
} catch (e) {} // eslint-disable-line no-empty
context.report({
node: style,
messageId: "stringStyle",
// replace full prop value, wrap in JSXExpressionContainer, more fixes may be applied below
fix:
objectStyles &&
((fixer) => fixer.replaceText(node.value!, `{${JSON.stringify(objectStyles)}}`)),
});
} else if (style.type === "TemplateLiteral" && !allowString) {
context.report({
node: style,
messageId: "stringStyle",
});
} else if (style.type === "ObjectExpression") {
const properties = style.properties.filter(
(prop) => prop.type === "Property"
) as Array<T.Property>;
properties.forEach((prop) => {
const name: string | null = getPropertyName(prop, context.getScope());
if (name && !name.startsWith("--") && !allCssPropertiesSet.has(name)) {
const kebabName: string = kebabCase(name);
if (allCssPropertiesSet.has(kebabName)) {
// if it's not valid simply because it's camelCased instead of kebab-cased, provide a fix
context.report({
node: prop.key,
messageId: "kebabStyleProp",
data: { name, kebabName },
fix: (fixer) => fixer.replaceText(prop.key, `"${kebabName}"`), // wrap kebab name in quotes to be a valid object key
});
} else {
context.report({
node: prop.key,
messageId: "invalidStyleProp",
data: { name },
});
}
} else if (!name || (!name.startsWith("--") && lengthPercentageRegex.test(name))) {
// catches numeric values (ex. { "font-size": 12 }) for common <length-percentage> peroperties
// and suggests quoting or appending 'px'
const value: unknown = getStaticValue(prop.value)?.value;
if (typeof value === "number" && value !== 0) {
context.report({ node: prop.value, messageId: "numericStyleValue" });
}
}
});
}
},
};
},
});

View File

@@ -0,0 +1 @@
export {};

286
node_modules/eslint-plugin-solid/src/utils.ts generated vendored Normal file
View File

@@ -0,0 +1,286 @@
import { TSESTree as T, TSESLint, ASTUtils } from "@typescript-eslint/utils";
const { findVariable } = ASTUtils;
const domElementRegex = /^[a-z]/;
export const isDOMElementName = (name: string): boolean => domElementRegex.test(name);
const propsRegex = /[pP]rops/;
export const isPropsByName = (name: string): boolean => propsRegex.test(name);
export const formatList = (strings: Array<string>): string => {
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]}'`;
}
};
export const find = (node: T.Node, predicate: (node: T.Node) => boolean): T.Node | null => {
let n: T.Node | undefined = node;
while (n) {
const result = predicate(n);
if (result) {
return n;
}
n = n.parent;
}
return null;
};
export function findParent<Guard extends T.Node>(
node: T.Node,
predicate: (node: T.Node) => node is Guard
): Guard | null;
export function findParent(node: T.Node, predicate: (node: T.Node) => boolean): T.Node | null;
export function findParent(node: T.Node, predicate: (node: T.Node) => boolean): T.Node | null {
return node.parent ? find(node.parent, predicate) : null;
}
// Try to resolve a variable to its definition
export function trace(node: T.Node, initialScope: TSESLint.Scope.Scope): T.Node {
if (node.type === "Identifier") {
const variable = findVariable(initialScope, node);
if (!variable) return node;
const def = variable.defs[0];
// def is `undefined` for Identifier `undefined`
switch (def?.type) {
case "FunctionName":
case "ClassName":
case "ImportBinding":
return def.node;
case "Variable":
if (
((def.node.parent as T.VariableDeclaration).kind === "const" ||
variable.references.every((ref) => ref.init || ref.isReadOnly())) &&
def.node.id.type === "Identifier" &&
def.node.init
) {
return trace(def.node.init, initialScope);
}
}
}
return node;
}
/** Get the relevant node when wrapped by a node that doesn't change the behavior */
export function ignoreTransparentWrappers(node: T.Node, up = false): T.Node {
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;
}
export type FunctionNode = T.FunctionExpression | T.ArrowFunctionExpression | T.FunctionDeclaration;
const FUNCTION_TYPES = ["FunctionExpression", "ArrowFunctionExpression", "FunctionDeclaration"];
export const isFunctionNode = (node: T.Node | null | undefined): node is FunctionNode =>
!!node && FUNCTION_TYPES.includes(node.type);
export type ProgramOrFunctionNode = FunctionNode | T.Program;
const PROGRAM_OR_FUNCTION_TYPES = ["Program"].concat(FUNCTION_TYPES);
export const isProgramOrFunctionNode = (
node: T.Node | null | undefined
): node is ProgramOrFunctionNode => !!node && PROGRAM_OR_FUNCTION_TYPES.includes(node.type);
export const isJSXElementOrFragment = (
node: T.Node | null | undefined
): node is T.JSXElement | T.JSXFragment =>
node?.type === "JSXElement" || node?.type === "JSXFragment";
export const getFunctionName = (node: FunctionNode): string | null => {
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;
};
export function findInScope(
node: T.Node,
scope: ProgramOrFunctionNode,
predicate: (node: T.Node) => boolean
): T.Node | null {
const found = find(node, (node) => node === scope || predicate(node));
return found === scope && !predicate(node) ? null : found;
}
// The next two functions were adapted from "eslint-plugin-import" under the MIT license.
// Checks whether `node` has a comment (that ends) on the previous line or on
// the same line as `node` (starts).
export const getCommentBefore = (
node: T.Node,
sourceCode: TSESLint.SourceCode
): T.Comment | undefined =>
sourceCode
.getCommentsBefore(node)
.find((comment) => comment.loc!.end.line >= node.loc!.start.line - 1);
// Checks whether `node` has a comment (that starts) on the same line as `node`
// (ends).
export const getCommentAfter = (
node: T.Node,
sourceCode: TSESLint.SourceCode
): T.Comment | undefined =>
sourceCode
.getCommentsAfter(node)
.find((comment) => comment.loc!.start.line === node.loc!.end.line);
export const trackImports = (fromModule = /^solid-js(?:\/?|\b)/) => {
const importMap = new Map<string, string>();
const handleImportDeclaration = (node: T.ImportDeclaration) => {
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: string | Array<string>, str: string): string | undefined => {
const importArr = Array.isArray(imports) ? imports : [imports];
return importArr.find((i) => importMap.get(i) === str);
};
return { matchImport, handleImportDeclaration };
};
export function appendImports(
fixer: TSESLint.RuleFixer,
sourceCode: TSESLint.SourceCode,
importNode: T.ImportDeclaration,
identifiers: Array<string>
): TSESLint.RuleFix | null {
const identifiersString = identifiers.join(", ");
const reversedSpecifiers = importNode.specifiers.slice().reverse();
const lastSpecifier = reversedSpecifiers.find((s) => s.type === "ImportSpecifier");
if (lastSpecifier) {
// import A, { B } from 'source' => import A, { B, C, D } from 'source'
// import { B } from 'source' => import { B, C, D } from 'source'
return fixer.insertTextAfter(lastSpecifier, `, ${identifiersString}`);
}
const otherSpecifier = importNode.specifiers.find(
(s) => s.type === "ImportDefaultSpecifier" || s.type === "ImportNamespaceSpecifier"
);
if (otherSpecifier) {
// import A from 'source' => import A, { B, C, D } from 'source'
return fixer.insertTextAfter(otherSpecifier, `, { ${identifiersString} }`);
}
if (importNode.specifiers.length === 0) {
const [importToken, maybeBrace] = sourceCode.getFirstTokens(importNode, { count: 2 });
if (maybeBrace?.value === "{") {
// import {} from 'source' => import { B, C, D } from 'source'
return fixer.insertTextAfter(maybeBrace, ` ${identifiersString} `);
} else {
// import 'source' => import { B, C, D } from 'source'
return importToken
? fixer.insertTextAfter(importToken, ` { ${identifiersString} } from`)
: null;
}
}
return null;
}
export function insertImports(
fixer: TSESLint.RuleFixer,
sourceCode: TSESLint.SourceCode,
source: string,
identifiers: Array<string>,
aboveImport?: T.ImportDeclaration,
isType = false
): TSESLint.RuleFix {
const identifiersString = identifiers.join(", ");
const programNode: T.Program = sourceCode.ast;
// insert `import { missing, identifiers } from "source"` above given node or at top of module
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}";\n`
);
}
return fixer.insertTextBeforeRange(
[0, 0],
`import ${isType ? "type " : ""}{ ${identifiersString} } from "${source}";\n`
);
}
export function removeSpecifier(
fixer: TSESLint.RuleFixer,
sourceCode: TSESLint.SourceCode,
specifier: T.ImportSpecifier,
pure = true
) {
const declaration = specifier.parent as T.ImportDeclaration;
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);
}
export function jsxPropName(prop: T.JSXAttribute) {
if (prop.name.type === "JSXNamespacedName") {
return `${prop.name.namespace.name}:${prop.name.name.name}`;
}
return prop.name.name;
}
type Props = T.JSXOpeningElement["attributes"];
/** Iterate through both attributes and spread object props, yielding the name and the node. */
export function* jsxGetAllProps(props: Props): Generator<[string, T.Node]> {
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];
}
}
}
/** Returns whether an element has a prop, checking spread object props. */
export function jsxHasProp(props: Props, prop: string) {
for (const [p] of jsxGetAllProps(props)) {
if (p === prop) return true;
}
return false;
}
/** Get a JSXAttribute, excluding spread props. */
export function jsxGetProp(props: Props, prop: string) {
return props.find(
(attribute) => attribute.type !== "JSXSpreadAttribute" && prop === jsxPropName(attribute)
) as T.JSXAttribute | undefined;
}