Skip to content

Commit 9e968b9

Browse files
selebyeonjuan
andauthored
feat: better require-closing-tags support (#188)
* add test case covering non-void self-closing tag when self-closing is enforced * don't close non-void elements * add test case covering math self close exception * clarify names * re-implement foreign context check * foreign context doesn't include the opening node itself * rewrite custom element checks to enforce the rule when possible * update custom tag tests * add custom tag test for children * replace custom tag check with regex + custom pattern option * add tests for custom pattern option * add test for preferred self closing custom tag with no children * Update require-closing-tags.md * add `mspace` to spellcheck https://developer.mozilla.org/en-US/docs/Web/MathML/Element/mspace * prettier * `customPattern` string -> `customPatterns` string array * format * lint * Update docs/rules/require-closing-tags.md Co-authored-by: YeonJuan <[email protected]> * update test to expect removal of closing tag when fixing * remove closing tag in fixed output * update fixes * update tests to reflect new independent options spec * replace `allowSelfClosingCustom` + `customPatterns` with independent `selfClosingCustomPatterns` option and update foreign context handling to compensate * Update require-closing-tags.md * format * update default to disallow self-closing custom tags * update docs * format --------- Co-authored-by: YeonJuan <[email protected]>
1 parent c9095f0 commit 9e968b9

File tree

4 files changed

+121
-37
lines changed

4 files changed

+121
-37
lines changed

.cspell.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@
3131
"noembed",
3232
"roletype",
3333
"nextid",
34-
"screenreader"
34+
"screenreader",
35+
"mspace"
3536
]
3637
}

docs/rules/require-closing-tags.md

Lines changed: 18 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -30,15 +30,15 @@ Examples of **correct** code for this rule:
3030

3131
### Options
3232

33-
This rule has an object option for [Void Elements](https://html.spec.whatwg.org/multipage/syntax.html#void-elements).
33+
This rule has an object option for [Void Elements](https://html.spec.whatwg.org/multipage/syntax.html#void-elements) and custom element patterns.
3434

3535
- `"selfClosing": "never"`: (default) disallow using self closing tag on [Void Elements](https://html.spec.whatwg.org/multipage/syntax.html#void-elements).
3636

3737
- `"selfClosing": "always"`: enforce using self closing tag on [Void Elements](https://html.spec.whatwg.org/multipage/syntax.html#void-elements).
3838

39-
- `"allowSelfClosingCustom": false`: (default) disallow self-closing for the custom tags.
39+
- `"selfClosingCustomPatterns": []`: (default) disallow self-closing for custom tags.
4040

41-
- `"allowSelfClosingCustom": true`: allow self-closing for the custom tags.
41+
- `"selfClosingCustomPatterns": ["-"]`: enforce self-closing for tags matching any of an array of strings representing regular expression pattern (e.g. tags including `-` in the name).
4242

4343
#### selfClosing : "never"
4444

@@ -76,32 +76,37 @@ Examples of **correct** code for the `{ "selfClosing": "always" }` option:
7676
<base />
7777
```
7878

79-
#### "allowSelfClosingCustom": false
79+
#### selfClosingCustomPatterns: ["-"]
8080

81-
Examples of **incorrect** code for the `{ "allowSelfClosingCustom": false }` option:
81+
Examples of **incorrect** code for the `{ "selfClosingCustomPatterns": ["-"] }` option:
8282

8383
<!-- prettier-ignore -->
8484
```html,incorrect
85-
<custom-tag />
85+
<custom-tag> </custom-tag>
8686
```
8787

88-
Examples of **correct** code for the `{ "allowSelfClosingCustom": false }` option:
88+
Examples of **correct** code for the `{ "selfClosingCustomPatterns": ["-"] }` option:
8989

9090
<!-- prettier-ignore -->
9191
```html,correct
92-
<custom-tag> </custom-tag>
92+
<custom-tag>children</custom-tag>
93+
<custom-tag />
9394
```
9495

95-
#### "allowSelfClosingCustom": true
96+
#### selfClosingCustomPatterns: []
9697

97-
Examples of **correct** code for the `{ "allowSelfClosingCustom": true }` option:
98+
Examples of **incorrect** code for the `{ "allowSelfClosingCustom": [] }` option:
9899

99100
<!-- prettier-ignore -->
100-
```html,correct
101-
<!-- both allowed -->
101+
```html,incorrect
102+
<custom-tag />
103+
```
104+
105+
Examples of **correct** code for the `{ "allowSelfClosingCustom": [] }` option:
102106

107+
<!-- prettier-ignore -->
108+
```html,correct
103109
<custom-tag> </custom-tag>
104-
<custom-tag />
105110
```
106111

107112
## Further Reading

packages/eslint-plugin/lib/rules/require-closing-tags.js

Lines changed: 45 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -34,8 +34,11 @@ module.exports = {
3434
selfClosing: {
3535
enum: ["always", "never"],
3636
},
37-
allowSelfClosingCustom: {
38-
type: "boolean",
37+
selfClosingCustomPatterns: {
38+
type: "array",
39+
items: {
40+
type: "string",
41+
},
3942
},
4043
},
4144
additionalProperties: false,
@@ -49,14 +52,21 @@ module.exports = {
4952
},
5053

5154
create(context) {
52-
const shouldSelfClose =
55+
/** @type {string[]} */
56+
const foreignContext = [];
57+
const shouldSelfCloseVoid =
5358
context.options && context.options.length
5459
? context.options[0].selfClosing === "always"
5560
: false;
56-
const allowSelfClosingCustom =
57-
context.options && context.options.length
58-
? context.options[0].allowSelfClosingCustom === true
59-
: false;
61+
/** @type {string[]} */
62+
const selfClosingCustomPatternsOption =
63+
(context.options &&
64+
context.options.length &&
65+
context.options[0].selfClosingCustomPatterns) ||
66+
[];
67+
const selfClosingCustomPatterns = selfClosingCustomPatternsOption.map(
68+
(i) => new RegExp(i)
69+
);
6070

6171
/**
6272
* @param {TagNode} node
@@ -91,7 +101,10 @@ module.exports = {
91101
if (!fixable) {
92102
return null;
93103
}
94-
return fixer.replaceText(node.openEnd, " />");
104+
const fixes = [];
105+
fixes.push(fixer.replaceText(node.openEnd, " />"));
106+
if (node.close) fixes.push(fixer.remove(node.close));
107+
return fixes;
95108
},
96109
});
97110
}
@@ -115,17 +128,33 @@ module.exports = {
115128
return {
116129
Tag(node) {
117130
const isVoidElement = VOID_ELEMENTS_SET.has(node.name);
118-
if (
119-
node.selfClosing &&
120-
allowSelfClosingCustom &&
121-
node.name.indexOf("-") !== -1
122-
) {
123-
checkVoidElement(node, true, false);
124-
} else if (node.selfClosing || isVoidElement) {
125-
checkVoidElement(node, shouldSelfClose, isVoidElement);
131+
const isSelfClosingCustomElement = !!selfClosingCustomPatterns.some(
132+
(i) => node.name.match(i)
133+
);
134+
const isForeign = foreignContext.length > 0;
135+
const shouldSelfCloseCustom =
136+
isSelfClosingCustomElement && !node.children.length;
137+
const shouldSelfCloseForeign = node.selfClosing;
138+
const shouldSelfClose =
139+
(isVoidElement && shouldSelfCloseVoid) ||
140+
(isSelfClosingCustomElement && shouldSelfCloseCustom) ||
141+
(isForeign && shouldSelfCloseForeign);
142+
const canSelfClose =
143+
isVoidElement || isSelfClosingCustomElement || isForeign;
144+
if (node.selfClosing || canSelfClose) {
145+
checkVoidElement(node, shouldSelfClose, canSelfClose);
126146
} else if (node.openEnd.value !== "/>") {
127147
checkClosingTag(node);
128148
}
149+
if (["svg", "math"].includes(node.name)) foreignContext.push(node.name);
150+
},
151+
/**
152+
* @param {TagNode} node
153+
*/
154+
"Tag:exit"(node) {
155+
if (node.name === foreignContext[foreignContext.length - 1]) {
156+
foreignContext.pop();
157+
}
129158
},
130159
};
131160
},

packages/eslint-plugin/tests/rules/require-closing-tags.test.js

Lines changed: 56 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -23,39 +23,47 @@ ruleTester.run("require-closing-tags", rule, {
2323
code: `<custom-tag> </custom-tag>`,
2424
options: [
2525
{
26-
allowSelfClosingCustom: false,
26+
selfClosingCustomPatterns: [],
2727
},
2828
],
2929
},
3030
{
3131
code: `<custom-tag/>`,
3232
options: [
3333
{
34-
allowSelfClosingCustom: true,
34+
selfClosingCustomPatterns: ["-"],
3535
},
3636
],
3737
},
3838
{
3939
code: `<custom-tag />`,
4040
options: [
4141
{
42-
allowSelfClosingCustom: true,
42+
selfClosingCustomPatterns: ["-"],
4343
},
4444
],
4545
},
4646
{
4747
code: `<custom-tag> </custom-tag>`,
4848
options: [
4949
{
50-
allowSelfClosingCustom: true,
50+
selfClosingCustomPatterns: ["-"],
5151
},
5252
],
5353
},
5454
{
5555
code: `<custom-tag id="foo" />`,
5656
options: [
5757
{
58-
allowSelfClosingCustom: true,
58+
selfClosingCustomPatterns: ["-"],
59+
},
60+
],
61+
},
62+
{
63+
code: `<custom-tag>children</custom-tag>`,
64+
options: [
65+
{
66+
selfClosingCustomPatterns: ["-"],
5967
},
6068
],
6169
},
@@ -75,6 +83,20 @@ ruleTester.run("require-closing-tags", rule, {
7583
<circle />
7684
</svg>
7785
</body>
86+
`,
87+
options: [
88+
{
89+
selfClosing: "always",
90+
},
91+
],
92+
},
93+
{
94+
code: `
95+
<body>
96+
<math>
97+
1<mspace width="100px" />2
98+
</math>
99+
</body>
78100
`,
79101
options: [
80102
{
@@ -142,7 +164,7 @@ ruleTester.run("require-closing-tags", rule, {
142164
code: `<custom-tag />`,
143165
options: [
144166
{
145-
allowSelfClosingCustom: false,
167+
selfClosingCustomPatterns: [],
146168
},
147169
],
148170
errors: [
@@ -155,7 +177,34 @@ ruleTester.run("require-closing-tags", rule, {
155177
code: `<custom-tag id="foo" />`,
156178
options: [
157179
{
158-
allowSelfClosingCustom: false,
180+
selfClosingCustomPatterns: [],
181+
},
182+
],
183+
errors: [
184+
{
185+
messageId: "unexpected",
186+
},
187+
],
188+
},
189+
{
190+
code: `<custom-tag></custom-tag>`,
191+
options: [
192+
{
193+
selfClosingCustomPatterns: ["-"],
194+
},
195+
],
196+
output: "<custom-tag />",
197+
errors: [
198+
{
199+
messageId: "missingSelf",
200+
},
201+
],
202+
},
203+
{
204+
code: `<div />`,
205+
options: [
206+
{
207+
selfClosing: "always",
159208
},
160209
],
161210
output: null,

0 commit comments

Comments
 (0)