forked from solidjs-community/eslint-plugin-solid
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathself-closing-comp.ts
150 lines (139 loc) · 5.37 KB
/
self-closing-comp.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
/**
* 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/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}>`);
},
});
}
}
},
};
},
});