Skip to content

Commit

Permalink
feat: better require-closing-tags support (#188)
Browse files Browse the repository at this point in the history
* 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 <yeonjuan93@naver.com>

* 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 <yeonjuan93@naver.com>
  • Loading branch information
seleb and yeonjuan authored Jun 8, 2024
1 parent c9095f0 commit 9e968b9
Show file tree
Hide file tree
Showing 4 changed files with 121 additions and 37 deletions.
3 changes: 2 additions & 1 deletion .cspell.json
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
"noembed",
"roletype",
"nextid",
"screenreader"
"screenreader",
"mspace"
]
}
31 changes: 18 additions & 13 deletions docs/rules/require-closing-tags.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,15 +30,15 @@ Examples of **correct** code for this rule:

### Options

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

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

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

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

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

#### selfClosing : "never"

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

#### "allowSelfClosingCustom": false
#### selfClosingCustomPatterns: ["-"]

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

<!-- prettier-ignore -->
```html,incorrect
<custom-tag />
<custom-tag> </custom-tag>
```

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

<!-- prettier-ignore -->
```html,correct
<custom-tag> </custom-tag>
<custom-tag>children</custom-tag>
<custom-tag />
```

#### "allowSelfClosingCustom": true
#### selfClosingCustomPatterns: []

Examples of **correct** code for the `{ "allowSelfClosingCustom": true }` option:
Examples of **incorrect** code for the `{ "allowSelfClosingCustom": [] }` option:

<!-- prettier-ignore -->
```html,correct
<!-- both allowed -->
```html,incorrect
<custom-tag />
```

Examples of **correct** code for the `{ "allowSelfClosingCustom": [] }` option:

<!-- prettier-ignore -->
```html,correct
<custom-tag> </custom-tag>
<custom-tag />
```

## Further Reading
Expand Down
61 changes: 45 additions & 16 deletions packages/eslint-plugin/lib/rules/require-closing-tags.js
Original file line number Diff line number Diff line change
Expand Up @@ -34,8 +34,11 @@ module.exports = {
selfClosing: {
enum: ["always", "never"],
},
allowSelfClosingCustom: {
type: "boolean",
selfClosingCustomPatterns: {
type: "array",
items: {
type: "string",
},
},
},
additionalProperties: false,
Expand All @@ -49,14 +52,21 @@ module.exports = {
},

create(context) {
const shouldSelfClose =
/** @type {string[]} */
const foreignContext = [];
const shouldSelfCloseVoid =
context.options && context.options.length
? context.options[0].selfClosing === "always"
: false;
const allowSelfClosingCustom =
context.options && context.options.length
? context.options[0].allowSelfClosingCustom === true
: false;
/** @type {string[]} */
const selfClosingCustomPatternsOption =
(context.options &&
context.options.length &&
context.options[0].selfClosingCustomPatterns) ||
[];
const selfClosingCustomPatterns = selfClosingCustomPatternsOption.map(
(i) => new RegExp(i)
);

/**
* @param {TagNode} node
Expand Down Expand Up @@ -91,7 +101,10 @@ module.exports = {
if (!fixable) {
return null;
}
return fixer.replaceText(node.openEnd, " />");
const fixes = [];
fixes.push(fixer.replaceText(node.openEnd, " />"));
if (node.close) fixes.push(fixer.remove(node.close));
return fixes;
},
});
}
Expand All @@ -115,17 +128,33 @@ module.exports = {
return {
Tag(node) {
const isVoidElement = VOID_ELEMENTS_SET.has(node.name);
if (
node.selfClosing &&
allowSelfClosingCustom &&
node.name.indexOf("-") !== -1
) {
checkVoidElement(node, true, false);
} else if (node.selfClosing || isVoidElement) {
checkVoidElement(node, shouldSelfClose, isVoidElement);
const isSelfClosingCustomElement = !!selfClosingCustomPatterns.some(
(i) => node.name.match(i)
);
const isForeign = foreignContext.length > 0;
const shouldSelfCloseCustom =
isSelfClosingCustomElement && !node.children.length;
const shouldSelfCloseForeign = node.selfClosing;
const shouldSelfClose =
(isVoidElement && shouldSelfCloseVoid) ||
(isSelfClosingCustomElement && shouldSelfCloseCustom) ||
(isForeign && shouldSelfCloseForeign);
const canSelfClose =
isVoidElement || isSelfClosingCustomElement || isForeign;
if (node.selfClosing || canSelfClose) {
checkVoidElement(node, shouldSelfClose, canSelfClose);
} else if (node.openEnd.value !== "/>") {
checkClosingTag(node);
}
if (["svg", "math"].includes(node.name)) foreignContext.push(node.name);
},
/**
* @param {TagNode} node
*/
"Tag:exit"(node) {
if (node.name === foreignContext[foreignContext.length - 1]) {
foreignContext.pop();
}
},
};
},
Expand Down
63 changes: 56 additions & 7 deletions packages/eslint-plugin/tests/rules/require-closing-tags.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,39 +23,47 @@ ruleTester.run("require-closing-tags", rule, {
code: `<custom-tag> </custom-tag>`,
options: [
{
allowSelfClosingCustom: false,
selfClosingCustomPatterns: [],
},
],
},
{
code: `<custom-tag/>`,
options: [
{
allowSelfClosingCustom: true,
selfClosingCustomPatterns: ["-"],
},
],
},
{
code: `<custom-tag />`,
options: [
{
allowSelfClosingCustom: true,
selfClosingCustomPatterns: ["-"],
},
],
},
{
code: `<custom-tag> </custom-tag>`,
options: [
{
allowSelfClosingCustom: true,
selfClosingCustomPatterns: ["-"],
},
],
},
{
code: `<custom-tag id="foo" />`,
options: [
{
allowSelfClosingCustom: true,
selfClosingCustomPatterns: ["-"],
},
],
},
{
code: `<custom-tag>children</custom-tag>`,
options: [
{
selfClosingCustomPatterns: ["-"],
},
],
},
Expand All @@ -75,6 +83,20 @@ ruleTester.run("require-closing-tags", rule, {
<circle />
</svg>
</body>
`,
options: [
{
selfClosing: "always",
},
],
},
{
code: `
<body>
<math>
1<mspace width="100px" />2
</math>
</body>
`,
options: [
{
Expand Down Expand Up @@ -142,7 +164,7 @@ ruleTester.run("require-closing-tags", rule, {
code: `<custom-tag />`,
options: [
{
allowSelfClosingCustom: false,
selfClosingCustomPatterns: [],
},
],
errors: [
Expand All @@ -155,7 +177,34 @@ ruleTester.run("require-closing-tags", rule, {
code: `<custom-tag id="foo" />`,
options: [
{
allowSelfClosingCustom: false,
selfClosingCustomPatterns: [],
},
],
errors: [
{
messageId: "unexpected",
},
],
},
{
code: `<custom-tag></custom-tag>`,
options: [
{
selfClosingCustomPatterns: ["-"],
},
],
output: "<custom-tag />",
errors: [
{
messageId: "missingSelf",
},
],
},
{
code: `<div />`,
options: [
{
selfClosing: "always",
},
],
output: null,
Expand Down

0 comments on commit 9e968b9

Please sign in to comment.