Skip to content

Commit

Permalink
feat(eslint-plugin-template): [self-closing-tags] add rule (#1322)
Browse files Browse the repository at this point in the history
  • Loading branch information
aleesaan committed Jun 3, 2023
1 parent e7c762a commit 6d26c59
Show file tree
Hide file tree
Showing 6 changed files with 420 additions and 0 deletions.
263 changes: 263 additions & 0 deletions packages/eslint-plugin-template/docs/rules/prefer-self-closing-tags.md
@@ -0,0 +1,263 @@
<!--
DO NOT EDIT.
This markdown file was autogenerated using a mixture of the following files as the source of truth for its data:
- ../../src/rules/prefer-self-closing-tags.ts
- ../../tests/rules/prefer-self-closing-tags/cases.ts
In order to update this file, it is therefore those files which need to be updated, as well as potentially the generator script:
- ../../../../tools/scripts/generate-rule-docs.ts
-->

<br>

# `@angular-eslint/template/prefer-self-closing-tags`

Ensures that self-closing tags are used for elements with a closing tag but no content.

- Type: layout
- 🔧 Supports autofix (`--fix`)

<br>

## Rule Options

The rule does not have any configuration options.

<br>

## Usage Examples

> 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.
<br>

<details>
<summary>❌ - Toggle examples of <strong>incorrect</strong> code for this rule</summary>

<br>

#### Default Config

```json
{
"rules": {
"@angular-eslint/template/prefer-self-closing-tags": [
"error"
]
}
}
```

<br>

#### ❌ Invalid Code

```html
<my-component></my-component>
~~~~~~~~~~~~~~~
```

<br>

---

<br>

#### Default Config

```json
{
"rules": {
"@angular-eslint/template/prefer-self-closing-tags": [
"error"
]
}
}
```

<br>

#### ❌ Invalid Code

```html
<my-component type="text" [name]="foo"></my-component>
~~~~~~~~~~~~~~~
```

<br>

---

<br>

#### Default Config

```json
{
"rules": {
"@angular-eslint/template/prefer-self-closing-tags": [
"error"
]
}
}
```

<br>

#### ❌ Invalid Code

```html
<my-component
type="text"
[name]="foo"
[items]="items">
</my-component>
~~~~~~~~~~~~~~~
```

</details>

<br>

---

<br>

<details>
<summary>✅ - Toggle examples of <strong>correct</strong> code for this rule</summary>

<br>

#### Default Config

```json
{
"rules": {
"@angular-eslint/template/prefer-self-closing-tags": [
"error"
]
}
}
```

<br>

#### ✅ Valid Code

```html
<my-component type="text" [name]="foo">With some content</my-component>
```

<br>

---

<br>

#### Default Config

```json
{
"rules": {
"@angular-eslint/template/prefer-self-closing-tags": [
"error"
]
}
}
```

<br>

#### ✅ Valid Code

```html
<my-component />
```

<br>

---

<br>

#### Default Config

```json
{
"rules": {
"@angular-eslint/template/prefer-self-closing-tags": [
"error"
]
}
}
```

<br>

#### ✅ Valid Code

```html
<my-component
type="text"
[name]="foo"
[items]="items" />
```

<br>

---

<br>

#### Default Config

```json
{
"rules": {
"@angular-eslint/template/prefer-self-closing-tags": [
"error"
]
}
}
```

<br>

#### ✅ Valid Code

```html
<img />
```

<br>

---

<br>

#### Default Config

```json
{
"rules": {
"@angular-eslint/template/prefer-self-closing-tags": [
"error"
]
}
}
```

<br>

#### ✅ Valid Code

```html
<div></div>
```

</details>

<br>
1 change: 1 addition & 0 deletions packages/eslint-plugin-template/src/configs/all.json
Expand Up @@ -24,6 +24,7 @@
"@angular-eslint/template/no-interpolation-in-attributes": "error",
"@angular-eslint/template/no-negated-async": "error",
"@angular-eslint/template/no-positive-tabindex": "error",
"@angular-eslint/template/prefer-self-closing-tags": "error",
"@angular-eslint/template/role-has-required-aria": "error",
"@angular-eslint/template/table-scope": "error",
"@angular-eslint/template/use-track-by-function": "error",
Expand Down
4 changes: 4 additions & 0 deletions packages/eslint-plugin-template/src/index.ts
Expand Up @@ -61,6 +61,9 @@ import noNegatedAsync, {
import noPositiveTabindex, {
RULE_NAME as noPositiveTabindexRuleName,
} from './rules/no-positive-tabindex';
import preferSelfClosingTags, {
RULE_NAME as preferSelfClosingTagsRuleName,
} from './rules/prefer-self-closing-tags';
import roleHasRequiredAria, {
RULE_NAME as roleHasRequiredAriaRuleName,
} from './rules/role-has-required-aria';
Expand Down Expand Up @@ -103,6 +106,7 @@ export = {
[noInterpolationInAttributesRuleName]: noInterpolationInAttributes,
[noNegatedAsyncRuleName]: noNegatedAsync,
[noPositiveTabindexRuleName]: noPositiveTabindex,
[preferSelfClosingTagsRuleName]: preferSelfClosingTags,
[roleHasRequiredAriaRuleName]: roleHasRequiredAria,
[tableScopeRuleName]: tableScope,
[useTrackByFunctionRuleName]: useTrackByFunction,
Expand Down
@@ -0,0 +1,76 @@
import type {
TmplAstElement,
TmplAstText,
} from '@angular-eslint/bundled-angular-compiler';
import { getTemplateParserServices } from '@angular-eslint/utils';
import { createESLintRule } from '../utils/create-eslint-rule';
import { getDomElements } from '../utils/get-dom-elements';

export const MESSAGE_ID = 'preferSelfClosingTags';
export const RULE_NAME = 'prefer-self-closing-tags';

export default createESLintRule<[], typeof MESSAGE_ID>({
name: RULE_NAME,
meta: {
type: 'layout',
docs: {
description:
'Ensures that self-closing tags are used for elements with a closing tag but no content.',
recommended: false,
},
fixable: 'code',
schema: [],
messages: {
[MESSAGE_ID]:
'Use self-closing tags for elements with a closing tag but no content.',
},
},
defaultOptions: [],
create(context) {
const parserServices = getTemplateParserServices(context);

return {
Element$1({
children,
name,
startSourceSpan,
endSourceSpan,
}: TmplAstElement) {
// Ignore native elements.
if (getDomElements().has(name)) {
return;
}

const noContent =
!children.length ||
children.every((node) => {
const text = (node as TmplAstText).value;

// If the node has no value, or only whitespace,
// we can consider it empty.
return (
typeof text === 'string' && text.replace(/\n/g, '').trim() === ''
);
});
const noCloseTag =
!endSourceSpan ||
(startSourceSpan.start.offset === endSourceSpan.start.offset &&
startSourceSpan.end.offset === endSourceSpan.end.offset);

if (!noContent || noCloseTag) {
return;
}

context.report({
loc: parserServices.convertNodeSourceSpanToLoc(endSourceSpan),
messageId: MESSAGE_ID,
fix: (fixer) =>
fixer.replaceTextRange(
[startSourceSpan.end.offset - 1, endSourceSpan.end.offset],
' />',
),
});
},
};
},
});

0 comments on commit 6d26c59

Please sign in to comment.