eslint-plugin-solid
Version:
Solid-specific linting rules for ESLint.
151 lines (139 loc) • 5.53 kB
text/typescript
/**
* 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";
import { getSourceCode } from "../compat";
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/packages/eslint-plugin-solid/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 = getSourceCode(context);
const tagName = sourceCode.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}>`);
},
});
}
}
},
};
},
});