Skip to content

Commit 6d26c59

Browse files
authored
feat(eslint-plugin-template): [self-closing-tags] add rule (#1322)
1 parent e7c762a commit 6d26c59

File tree

6 files changed

+420
-0
lines changed

6 files changed

+420
-0
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,263 @@
1+
<!--
2+
3+
DO NOT EDIT.
4+
5+
This markdown file was autogenerated using a mixture of the following files as the source of truth for its data:
6+
- ../../src/rules/prefer-self-closing-tags.ts
7+
- ../../tests/rules/prefer-self-closing-tags/cases.ts
8+
9+
In order to update this file, it is therefore those files which need to be updated, as well as potentially the generator script:
10+
- ../../../../tools/scripts/generate-rule-docs.ts
11+
12+
-->
13+
14+
<br>
15+
16+
# `@angular-eslint/template/prefer-self-closing-tags`
17+
18+
Ensures that self-closing tags are used for elements with a closing tag but no content.
19+
20+
- Type: layout
21+
- 🔧 Supports autofix (`--fix`)
22+
23+
<br>
24+
25+
## Rule Options
26+
27+
The rule does not have any configuration options.
28+
29+
<br>
30+
31+
## Usage Examples
32+
33+
> The following examples are generated automatically from the actual unit tests within the plugin, so you can be assured that their behavior is accurate based on the current commit.
34+
35+
<br>
36+
37+
<details>
38+
<summary>❌ - Toggle examples of <strong>incorrect</strong> code for this rule</summary>
39+
40+
<br>
41+
42+
#### Default Config
43+
44+
```json
45+
{
46+
"rules": {
47+
"@angular-eslint/template/prefer-self-closing-tags": [
48+
"error"
49+
]
50+
}
51+
}
52+
```
53+
54+
<br>
55+
56+
#### ❌ Invalid Code
57+
58+
```html
59+
<my-component></my-component>
60+
~~~~~~~~~~~~~~~
61+
```
62+
63+
<br>
64+
65+
---
66+
67+
<br>
68+
69+
#### Default Config
70+
71+
```json
72+
{
73+
"rules": {
74+
"@angular-eslint/template/prefer-self-closing-tags": [
75+
"error"
76+
]
77+
}
78+
}
79+
```
80+
81+
<br>
82+
83+
#### ❌ Invalid Code
84+
85+
```html
86+
<my-component type="text" [name]="foo"></my-component>
87+
~~~~~~~~~~~~~~~
88+
```
89+
90+
<br>
91+
92+
---
93+
94+
<br>
95+
96+
#### Default Config
97+
98+
```json
99+
{
100+
"rules": {
101+
"@angular-eslint/template/prefer-self-closing-tags": [
102+
"error"
103+
]
104+
}
105+
}
106+
```
107+
108+
<br>
109+
110+
#### ❌ Invalid Code
111+
112+
```html
113+
<my-component
114+
type="text"
115+
[name]="foo"
116+
[items]="items">
117+
</my-component>
118+
~~~~~~~~~~~~~~~
119+
```
120+
121+
</details>
122+
123+
<br>
124+
125+
---
126+
127+
<br>
128+
129+
<details>
130+
<summary>✅ - Toggle examples of <strong>correct</strong> code for this rule</summary>
131+
132+
<br>
133+
134+
#### Default Config
135+
136+
```json
137+
{
138+
"rules": {
139+
"@angular-eslint/template/prefer-self-closing-tags": [
140+
"error"
141+
]
142+
}
143+
}
144+
```
145+
146+
<br>
147+
148+
#### ✅ Valid Code
149+
150+
```html
151+
<my-component type="text" [name]="foo">With some content</my-component>
152+
```
153+
154+
<br>
155+
156+
---
157+
158+
<br>
159+
160+
#### Default Config
161+
162+
```json
163+
{
164+
"rules": {
165+
"@angular-eslint/template/prefer-self-closing-tags": [
166+
"error"
167+
]
168+
}
169+
}
170+
```
171+
172+
<br>
173+
174+
#### ✅ Valid Code
175+
176+
```html
177+
<my-component />
178+
```
179+
180+
<br>
181+
182+
---
183+
184+
<br>
185+
186+
#### Default Config
187+
188+
```json
189+
{
190+
"rules": {
191+
"@angular-eslint/template/prefer-self-closing-tags": [
192+
"error"
193+
]
194+
}
195+
}
196+
```
197+
198+
<br>
199+
200+
#### ✅ Valid Code
201+
202+
```html
203+
<my-component
204+
type="text"
205+
[name]="foo"
206+
[items]="items" />
207+
```
208+
209+
<br>
210+
211+
---
212+
213+
<br>
214+
215+
#### Default Config
216+
217+
```json
218+
{
219+
"rules": {
220+
"@angular-eslint/template/prefer-self-closing-tags": [
221+
"error"
222+
]
223+
}
224+
}
225+
```
226+
227+
<br>
228+
229+
#### ✅ Valid Code
230+
231+
```html
232+
<img />
233+
```
234+
235+
<br>
236+
237+
---
238+
239+
<br>
240+
241+
#### Default Config
242+
243+
```json
244+
{
245+
"rules": {
246+
"@angular-eslint/template/prefer-self-closing-tags": [
247+
"error"
248+
]
249+
}
250+
}
251+
```
252+
253+
<br>
254+
255+
#### ✅ Valid Code
256+
257+
```html
258+
<div></div>
259+
```
260+
261+
</details>
262+
263+
<br>

packages/eslint-plugin-template/src/configs/all.json

+1
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
"@angular-eslint/template/no-interpolation-in-attributes": "error",
2525
"@angular-eslint/template/no-negated-async": "error",
2626
"@angular-eslint/template/no-positive-tabindex": "error",
27+
"@angular-eslint/template/prefer-self-closing-tags": "error",
2728
"@angular-eslint/template/role-has-required-aria": "error",
2829
"@angular-eslint/template/table-scope": "error",
2930
"@angular-eslint/template/use-track-by-function": "error",

packages/eslint-plugin-template/src/index.ts

+4
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,9 @@ import noNegatedAsync, {
6161
import noPositiveTabindex, {
6262
RULE_NAME as noPositiveTabindexRuleName,
6363
} from './rules/no-positive-tabindex';
64+
import preferSelfClosingTags, {
65+
RULE_NAME as preferSelfClosingTagsRuleName,
66+
} from './rules/prefer-self-closing-tags';
6467
import roleHasRequiredAria, {
6568
RULE_NAME as roleHasRequiredAriaRuleName,
6669
} from './rules/role-has-required-aria';
@@ -103,6 +106,7 @@ export = {
103106
[noInterpolationInAttributesRuleName]: noInterpolationInAttributes,
104107
[noNegatedAsyncRuleName]: noNegatedAsync,
105108
[noPositiveTabindexRuleName]: noPositiveTabindex,
109+
[preferSelfClosingTagsRuleName]: preferSelfClosingTags,
106110
[roleHasRequiredAriaRuleName]: roleHasRequiredAria,
107111
[tableScopeRuleName]: tableScope,
108112
[useTrackByFunctionRuleName]: useTrackByFunction,
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
import type {
2+
TmplAstElement,
3+
TmplAstText,
4+
} from '@angular-eslint/bundled-angular-compiler';
5+
import { getTemplateParserServices } from '@angular-eslint/utils';
6+
import { createESLintRule } from '../utils/create-eslint-rule';
7+
import { getDomElements } from '../utils/get-dom-elements';
8+
9+
export const MESSAGE_ID = 'preferSelfClosingTags';
10+
export const RULE_NAME = 'prefer-self-closing-tags';
11+
12+
export default createESLintRule<[], typeof MESSAGE_ID>({
13+
name: RULE_NAME,
14+
meta: {
15+
type: 'layout',
16+
docs: {
17+
description:
18+
'Ensures that self-closing tags are used for elements with a closing tag but no content.',
19+
recommended: false,
20+
},
21+
fixable: 'code',
22+
schema: [],
23+
messages: {
24+
[MESSAGE_ID]:
25+
'Use self-closing tags for elements with a closing tag but no content.',
26+
},
27+
},
28+
defaultOptions: [],
29+
create(context) {
30+
const parserServices = getTemplateParserServices(context);
31+
32+
return {
33+
Element$1({
34+
children,
35+
name,
36+
startSourceSpan,
37+
endSourceSpan,
38+
}: TmplAstElement) {
39+
// Ignore native elements.
40+
if (getDomElements().has(name)) {
41+
return;
42+
}
43+
44+
const noContent =
45+
!children.length ||
46+
children.every((node) => {
47+
const text = (node as TmplAstText).value;
48+
49+
// If the node has no value, or only whitespace,
50+
// we can consider it empty.
51+
return (
52+
typeof text === 'string' && text.replace(/\n/g, '').trim() === ''
53+
);
54+
});
55+
const noCloseTag =
56+
!endSourceSpan ||
57+
(startSourceSpan.start.offset === endSourceSpan.start.offset &&
58+
startSourceSpan.end.offset === endSourceSpan.end.offset);
59+
60+
if (!noContent || noCloseTag) {
61+
return;
62+
}
63+
64+
context.report({
65+
loc: parserServices.convertNodeSourceSpanToLoc(endSourceSpan),
66+
messageId: MESSAGE_ID,
67+
fix: (fixer) =>
68+
fixer.replaceTextRange(
69+
[startSourceSpan.end.offset - 1, endSourceSpan.end.offset],
70+
' />',
71+
),
72+
});
73+
},
74+
};
75+
},
76+
});

0 commit comments

Comments
 (0)