@angular-eslint/eslint-plugin-template 20.3.1-alpha.8 → 20.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -0
- package/dist/configs/accessibility.json +15 -15
- package/dist/configs/all.json +40 -39
- package/dist/configs/process-inline-templates.json +7 -7
- package/dist/configs/recommended.json +7 -7
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -0
- package/dist/rules/prefer-at-else.d.ts +6 -0
- package/dist/rules/prefer-at-else.d.ts.map +1 -0
- package/dist/rules/prefer-at-else.js +192 -0
- package/dist/rules/prefer-at-empty.d.ts.map +1 -1
- package/dist/rules/prefer-at-empty.js +28 -35
- package/dist/utils/ast-types.d.ts +4 -0
- package/dist/utils/ast-types.d.ts.map +1 -0
- package/dist/utils/ast-types.js +11 -0
- package/dist/utils/to-range.d.ts +4 -0
- package/dist/utils/to-range.d.ts.map +1 -0
- package/dist/utils/to-range.js +10 -0
- package/package.json +5 -5
package/README.md
CHANGED
|
@@ -63,6 +63,7 @@ Please see https://github.com/angular-eslint/angular-eslint for full usage instr
|
|
|
63
63
|
| [`no-interpolation-in-attributes`](https://github.com/angular-eslint/angular-eslint/blob/main/packages/eslint-plugin-template/docs/rules/no-interpolation-in-attributes.md) | Ensures that property-binding is used instead of interpolation in attributes. | | :wrench: | | |
|
|
64
64
|
| [`no-negated-async`](https://github.com/angular-eslint/angular-eslint/blob/main/packages/eslint-plugin-template/docs/rules/no-negated-async.md) | Ensures that async pipe results, as well as values used with the async pipe, are not negated | :white_check_mark: | | :bulb: | |
|
|
65
65
|
| [`no-positive-tabindex`](https://github.com/angular-eslint/angular-eslint/blob/main/packages/eslint-plugin-template/docs/rules/no-positive-tabindex.md) | Ensures that the `tabindex` attribute is not positive | | | :bulb: | |
|
|
66
|
+
| [`prefer-at-else`](https://github.com/angular-eslint/angular-eslint/blob/main/packages/eslint-plugin-template/docs/rules/prefer-at-else.md) | Prefer using `@else` instead of a second `@if` with the opposite condition to reduce code and make it easier to read. | | :wrench: | | |
|
|
66
67
|
| [`prefer-at-empty`](https://github.com/angular-eslint/angular-eslint/blob/main/packages/eslint-plugin-template/docs/rules/prefer-at-empty.md) | Prefer using `@empty` with `@for` loops instead of a separate `@if` or `@else` block to reduce code and make it easier to read. | | :wrench: | | |
|
|
67
68
|
| [`prefer-contextual-for-variables`](https://github.com/angular-eslint/angular-eslint/blob/main/packages/eslint-plugin-template/docs/rules/prefer-contextual-for-variables.md) | Ensures that contextual variables are used in @for blocks where possible instead of aliasing them. | | :wrench: | | |
|
|
68
69
|
| [`prefer-control-flow`](https://github.com/angular-eslint/angular-eslint/blob/main/packages/eslint-plugin-template/docs/rules/prefer-control-flow.md) | Ensures that the built-in control flow is used. | | | | |
|
|
@@ -1,17 +1,17 @@
|
|
|
1
1
|
{
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
2
|
+
"parser": "@angular-eslint/template-parser",
|
|
3
|
+
"plugins": ["@angular-eslint/template"],
|
|
4
|
+
"rules": {
|
|
5
|
+
"@angular-eslint/template/alt-text": "error",
|
|
6
|
+
"@angular-eslint/template/click-events-have-key-events": "error",
|
|
7
|
+
"@angular-eslint/template/elements-content": "error",
|
|
8
|
+
"@angular-eslint/template/interactive-supports-focus": "error",
|
|
9
|
+
"@angular-eslint/template/label-has-associated-control": "error",
|
|
10
|
+
"@angular-eslint/template/mouse-events-have-key-events": "error",
|
|
11
|
+
"@angular-eslint/template/no-autofocus": "error",
|
|
12
|
+
"@angular-eslint/template/no-distracting-elements": "error",
|
|
13
|
+
"@angular-eslint/template/role-has-required-aria": "error",
|
|
14
|
+
"@angular-eslint/template/table-scope": "error",
|
|
15
|
+
"@angular-eslint/template/valid-aria": "error"
|
|
16
|
+
}
|
|
17
17
|
}
|
package/dist/configs/all.json
CHANGED
|
@@ -1,41 +1,42 @@
|
|
|
1
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
|
-
|
|
2
|
+
"parser": "@angular-eslint/template-parser",
|
|
3
|
+
"plugins": ["@angular-eslint/template"],
|
|
4
|
+
"rules": {
|
|
5
|
+
"@angular-eslint/template/alt-text": "error",
|
|
6
|
+
"@angular-eslint/template/attributes-order": "error",
|
|
7
|
+
"@angular-eslint/template/banana-in-box": "error",
|
|
8
|
+
"@angular-eslint/template/button-has-type": "error",
|
|
9
|
+
"@angular-eslint/template/click-events-have-key-events": "error",
|
|
10
|
+
"@angular-eslint/template/conditional-complexity": "error",
|
|
11
|
+
"@angular-eslint/template/cyclomatic-complexity": "error",
|
|
12
|
+
"@angular-eslint/template/elements-content": "error",
|
|
13
|
+
"@angular-eslint/template/eqeqeq": "error",
|
|
14
|
+
"@angular-eslint/template/i18n": "error",
|
|
15
|
+
"@angular-eslint/template/interactive-supports-focus": "error",
|
|
16
|
+
"@angular-eslint/template/label-has-associated-control": "error",
|
|
17
|
+
"@angular-eslint/template/mouse-events-have-key-events": "error",
|
|
18
|
+
"@angular-eslint/template/no-any": "error",
|
|
19
|
+
"@angular-eslint/template/no-autofocus": "error",
|
|
20
|
+
"@angular-eslint/template/no-call-expression": "error",
|
|
21
|
+
"@angular-eslint/template/no-distracting-elements": "error",
|
|
22
|
+
"@angular-eslint/template/no-duplicate-attributes": "error",
|
|
23
|
+
"@angular-eslint/template/no-empty-control-flow": "error",
|
|
24
|
+
"@angular-eslint/template/no-inline-styles": "error",
|
|
25
|
+
"@angular-eslint/template/no-interpolation-in-attributes": "error",
|
|
26
|
+
"@angular-eslint/template/no-negated-async": "error",
|
|
27
|
+
"@angular-eslint/template/no-nested-tags": "error",
|
|
28
|
+
"@angular-eslint/template/no-positive-tabindex": "error",
|
|
29
|
+
"@angular-eslint/template/prefer-at-else": "error",
|
|
30
|
+
"@angular-eslint/template/prefer-at-empty": "error",
|
|
31
|
+
"@angular-eslint/template/prefer-contextual-for-variables": "error",
|
|
32
|
+
"@angular-eslint/template/prefer-control-flow": "error",
|
|
33
|
+
"@angular-eslint/template/prefer-ngsrc": "error",
|
|
34
|
+
"@angular-eslint/template/prefer-self-closing-tags": "error",
|
|
35
|
+
"@angular-eslint/template/prefer-static-string-properties": "error",
|
|
36
|
+
"@angular-eslint/template/prefer-template-literal": "error",
|
|
37
|
+
"@angular-eslint/template/role-has-required-aria": "error",
|
|
38
|
+
"@angular-eslint/template/table-scope": "error",
|
|
39
|
+
"@angular-eslint/template/use-track-by-function": "error",
|
|
40
|
+
"@angular-eslint/template/valid-aria": "error"
|
|
41
|
+
}
|
|
41
42
|
}
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
{
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
2
|
+
"parser": "@typescript-eslint/parser",
|
|
3
|
+
"parserOptions": {
|
|
4
|
+
"ecmaVersion": 2020,
|
|
5
|
+
"sourceType": "module"
|
|
6
|
+
},
|
|
7
|
+
"plugins": ["@angular-eslint/template"],
|
|
8
|
+
"processor": "@angular-eslint/template/extract-inline-html"
|
|
9
9
|
}
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
{
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
2
|
+
"parser": "@angular-eslint/template-parser",
|
|
3
|
+
"plugins": ["@angular-eslint/template"],
|
|
4
|
+
"rules": {
|
|
5
|
+
"@angular-eslint/template/banana-in-box": "error",
|
|
6
|
+
"@angular-eslint/template/eqeqeq": "error",
|
|
7
|
+
"@angular-eslint/template/no-negated-async": "error"
|
|
8
|
+
}
|
|
9
9
|
}
|
package/dist/index.d.ts
CHANGED
|
@@ -28,6 +28,7 @@ declare const _default: {
|
|
|
28
28
|
"@angular-eslint/template/no-negated-async": string;
|
|
29
29
|
"@angular-eslint/template/no-nested-tags": string;
|
|
30
30
|
"@angular-eslint/template/no-positive-tabindex": string;
|
|
31
|
+
"@angular-eslint/template/prefer-at-else": string;
|
|
31
32
|
"@angular-eslint/template/prefer-at-empty": string;
|
|
32
33
|
"@angular-eslint/template/prefer-contextual-for-variables": string;
|
|
33
34
|
"@angular-eslint/template/prefer-control-flow": string;
|
|
@@ -112,6 +113,7 @@ declare const _default: {
|
|
|
112
113
|
"no-negated-async": import("@typescript-eslint/utils/ts-eslint").RuleModule<import("./rules/no-negated-async").MessageIds, [], import("./utils/create-eslint-rule").RuleDocs, import("@typescript-eslint/utils/ts-eslint").RuleListener>;
|
|
113
114
|
"no-nested-tags": import("@typescript-eslint/utils/ts-eslint").RuleModule<"noNestedTags", [], import("./utils/create-eslint-rule").RuleDocs, import("@typescript-eslint/utils/ts-eslint").RuleListener>;
|
|
114
115
|
"no-positive-tabindex": import("@typescript-eslint/utils/ts-eslint").RuleModule<import("./rules/no-positive-tabindex").MessageIds, [], import("./utils/create-eslint-rule").RuleDocs, import("@typescript-eslint/utils/ts-eslint").RuleListener>;
|
|
116
|
+
"prefer-at-else": import("@typescript-eslint/utils/ts-eslint").RuleModule<"preferAtElse", [], import("./utils/create-eslint-rule").RuleDocs, import("@typescript-eslint/utils/ts-eslint").RuleListener>;
|
|
115
117
|
"prefer-at-empty": import("@typescript-eslint/utils/ts-eslint").RuleModule<"preferAtEmpty", [], import("./utils/create-eslint-rule").RuleDocs, import("@typescript-eslint/utils/ts-eslint").RuleListener>;
|
|
116
118
|
"prefer-contextual-for-variables": import("@typescript-eslint/utils/ts-eslint").RuleModule<import("./rules/prefer-contextual-for-variables").MessageIds, import("./rules/prefer-contextual-for-variables").Options, import("./utils/create-eslint-rule").RuleDocs, import("@typescript-eslint/utils/ts-eslint").RuleListener>;
|
|
117
119
|
"prefer-control-flow": import("@typescript-eslint/utils/ts-eslint").RuleModule<"preferControlFlow", [], import("./utils/create-eslint-rule").RuleDocs, import("@typescript-eslint/utils/ts-eslint").RuleListener>;
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAwGA,kBA8CE"}
|
package/dist/index.js
CHANGED
|
@@ -65,6 +65,7 @@ const no_negated_async_1 = __importStar(require("./rules/no-negated-async"));
|
|
|
65
65
|
const no_nested_tags_1 = __importStar(require("./rules/no-nested-tags"));
|
|
66
66
|
const no_positive_tabindex_1 = __importStar(require("./rules/no-positive-tabindex"));
|
|
67
67
|
const prefer_ngsrc_1 = __importStar(require("./rules/prefer-ngsrc"));
|
|
68
|
+
const prefer_at_else_1 = __importStar(require("./rules/prefer-at-else"));
|
|
68
69
|
const prefer_at_empty_1 = __importStar(require("./rules/prefer-at-empty"));
|
|
69
70
|
const prefer_contextual_for_variables_1 = __importStar(require("./rules/prefer-contextual-for-variables"));
|
|
70
71
|
const prefer_control_flow_1 = __importStar(require("./rules/prefer-control-flow"));
|
|
@@ -108,6 +109,7 @@ module.exports = {
|
|
|
108
109
|
[no_negated_async_1.RULE_NAME]: no_negated_async_1.default,
|
|
109
110
|
[no_nested_tags_1.RULE_NAME]: no_nested_tags_1.default,
|
|
110
111
|
[no_positive_tabindex_1.RULE_NAME]: no_positive_tabindex_1.default,
|
|
112
|
+
[prefer_at_else_1.RULE_NAME]: prefer_at_else_1.default,
|
|
111
113
|
[prefer_at_empty_1.RULE_NAME]: prefer_at_empty_1.default,
|
|
112
114
|
[prefer_contextual_for_variables_1.RULE_NAME]: prefer_contextual_for_variables_1.default,
|
|
113
115
|
[prefer_control_flow_1.RULE_NAME]: prefer_control_flow_1.default,
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export type Options = [];
|
|
2
|
+
export type MessageIds = 'preferAtElse';
|
|
3
|
+
export declare const RULE_NAME = "prefer-at-else";
|
|
4
|
+
declare const _default: import("@typescript-eslint/utils/ts-eslint").RuleModule<"preferAtElse", [], import("../utils/create-eslint-rule").RuleDocs, import("@typescript-eslint/utils/ts-eslint").RuleListener>;
|
|
5
|
+
export default _default;
|
|
6
|
+
//# sourceMappingURL=prefer-at-else.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"prefer-at-else.d.ts","sourceRoot":"","sources":["../../src/rules/prefer-at-else.ts"],"names":[],"mappings":"AAeA,MAAM,MAAM,OAAO,GAAG,EAAE,CAAC;AACzB,MAAM,MAAM,UAAU,GAAG,cAAc,CAAC;AACxC,eAAO,MAAM,SAAS,mBAAmB,CAAC;;AAe1C,wBA+HG"}
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.RULE_NAME = void 0;
|
|
4
|
+
const bundled_angular_compiler_1 = require("@angular-eslint/bundled-angular-compiler");
|
|
5
|
+
const utils_1 = require("@angular-eslint/utils");
|
|
6
|
+
const create_eslint_rule_1 = require("../utils/create-eslint-rule");
|
|
7
|
+
const are_equivalent_asts_1 = require("../utils/are-equivalent-asts");
|
|
8
|
+
const ast_types_1 = require("../utils/ast-types");
|
|
9
|
+
const to_range_1 = require("../utils/to-range");
|
|
10
|
+
exports.RULE_NAME = 'prefer-at-else';
|
|
11
|
+
const OPPOSITE_OPERATORS = new Map([
|
|
12
|
+
['', '!'],
|
|
13
|
+
['!', ''],
|
|
14
|
+
['<', '>='],
|
|
15
|
+
['>', '<='],
|
|
16
|
+
['<=', '>'],
|
|
17
|
+
['>=', '<'],
|
|
18
|
+
['==', '!='],
|
|
19
|
+
['!=', '=='],
|
|
20
|
+
['===', '!=='],
|
|
21
|
+
['!==', '==='],
|
|
22
|
+
]);
|
|
23
|
+
exports.default = (0, create_eslint_rule_1.createESLintRule)({
|
|
24
|
+
name: exports.RULE_NAME,
|
|
25
|
+
meta: {
|
|
26
|
+
type: 'suggestion',
|
|
27
|
+
fixable: 'code',
|
|
28
|
+
docs: {
|
|
29
|
+
description: 'Prefer using `@else` instead of a second `@if` with the opposite condition to reduce code and make it easier to read.',
|
|
30
|
+
},
|
|
31
|
+
schema: [],
|
|
32
|
+
messages: {
|
|
33
|
+
preferAtElse: 'Prefer using `@else` instead of a second `@if` clause.',
|
|
34
|
+
},
|
|
35
|
+
},
|
|
36
|
+
defaultOptions: [],
|
|
37
|
+
create(context) {
|
|
38
|
+
const parserServices = (0, utils_1.getTemplateParserServices)(context);
|
|
39
|
+
const previousNodeStack = [undefined];
|
|
40
|
+
function getFix(previous, current) {
|
|
41
|
+
const previousIf = previous.node.branches[0];
|
|
42
|
+
const currentIf = current.node.branches[0];
|
|
43
|
+
const currentElse = current.node.branches.at(1);
|
|
44
|
+
const previousElse = previous.node.branches.at(1);
|
|
45
|
+
// If the current `@if` block uses an alias, then
|
|
46
|
+
// we won't fix it because the alias won't exist
|
|
47
|
+
// in the `@else` block of the previous `@if` block.
|
|
48
|
+
if (currentIf.expressionAlias) {
|
|
49
|
+
return null;
|
|
50
|
+
}
|
|
51
|
+
return function* fix(fixer) {
|
|
52
|
+
if (!previousElse) {
|
|
53
|
+
// The previous `@if` block has no `@else` block,
|
|
54
|
+
// so we can turn the current `@if` block into one.
|
|
55
|
+
yield fixer.replaceTextRange([
|
|
56
|
+
currentIf.sourceSpan.start.offset,
|
|
57
|
+
currentIf.startSourceSpan.end.offset,
|
|
58
|
+
], '@else {');
|
|
59
|
+
}
|
|
60
|
+
else {
|
|
61
|
+
// The previous `@if` block already has an `@else` block.
|
|
62
|
+
// Since the current `@if` block is the opposite of the previous
|
|
63
|
+
// `@if` block, the previous `@else` block and the current `@if`
|
|
64
|
+
// block would both be rendered. We can achieve the same result
|
|
65
|
+
// with a single block by putting the contents of the current
|
|
66
|
+
// `@if` block at the end of the previous `@else` block.
|
|
67
|
+
const ifContents = context.sourceCode.text.slice(currentIf.startSourceSpan.end.offset, currentIf.sourceSpan.end.offset - 1);
|
|
68
|
+
yield fixer.insertTextAfterRange((0, to_range_1.toZeroLengthRange)(previousElse.sourceSpan.end.offset - 1), ifContents);
|
|
69
|
+
yield fixer.removeRange((0, to_range_1.toRange)(currentIf.sourceSpan));
|
|
70
|
+
}
|
|
71
|
+
if (currentElse && currentIf.endSourceSpan) {
|
|
72
|
+
// The current node has an `@else` block. Since the current
|
|
73
|
+
// `@if` block is the opposite of the previous `@if` block,
|
|
74
|
+
// the `@else` block would be rendered when the previous
|
|
75
|
+
// `@if` is also rendered. We can achieve the same result
|
|
76
|
+
// by putting the contents of the current `@else` block
|
|
77
|
+
// at the end of the previous `@if` block.
|
|
78
|
+
const elseContents = context.sourceCode.text.slice(currentElse.startSourceSpan.end.offset, currentElse.sourceSpan.end.offset - 1);
|
|
79
|
+
yield fixer.insertTextAfterRange((0, to_range_1.toZeroLengthRange)(previousIf.sourceSpan.end.offset - 1), elseContents);
|
|
80
|
+
yield fixer.removeRange([
|
|
81
|
+
currentIf.endSourceSpan.end.offset,
|
|
82
|
+
currentElse.sourceSpan.end.offset,
|
|
83
|
+
]);
|
|
84
|
+
}
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
return {
|
|
88
|
+
// We need to visit `@if` blocks, but we also
|
|
89
|
+
// need to know if there are any nodes immediately
|
|
90
|
+
// before them, so we need to visit all nodes.
|
|
91
|
+
'*'(node) {
|
|
92
|
+
const current = getIfNodeInfo(node);
|
|
93
|
+
if (current) {
|
|
94
|
+
const previous = previousNodeStack.at(-1);
|
|
95
|
+
if (previous && canCombine(previous, current)) {
|
|
96
|
+
context.report({
|
|
97
|
+
loc: parserServices.convertNodeSourceSpanToLoc(current.node.nameSpan),
|
|
98
|
+
messageId: 'preferAtElse',
|
|
99
|
+
fix: getFix(previous, current),
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
// Record this current node as the previous node so that
|
|
104
|
+
// we can get the info when we look at the next sibling.
|
|
105
|
+
previousNodeStack[previousNodeStack.length - 1] = current;
|
|
106
|
+
// We are about to visit the children of this node,
|
|
107
|
+
// so push a new "previous node info" onto the stack.
|
|
108
|
+
// The previous node of the first child is undefined.
|
|
109
|
+
previousNodeStack.push(undefined);
|
|
110
|
+
},
|
|
111
|
+
'*:exit'() {
|
|
112
|
+
// We've finished visiting the children of this node,
|
|
113
|
+
// so pop the "previous node info" off the stack.
|
|
114
|
+
previousNodeStack.pop();
|
|
115
|
+
},
|
|
116
|
+
};
|
|
117
|
+
},
|
|
118
|
+
});
|
|
119
|
+
function getIfNodeInfo(node) {
|
|
120
|
+
// We only care about `@if` blocks with one or two branches.
|
|
121
|
+
// Any more branches and it would have to contain an
|
|
122
|
+
// `@else if` branch, which we cannot handle.
|
|
123
|
+
if (node instanceof bundled_angular_compiler_1.TmplAstIfBlock &&
|
|
124
|
+
node.branches.length >= 1 &&
|
|
125
|
+
node.branches[0].expression instanceof bundled_angular_compiler_1.ASTWithSource &&
|
|
126
|
+
node.branches.length <= 2) {
|
|
127
|
+
// When there are two branches, the second
|
|
128
|
+
// branch cannot have an expression, otherwise it
|
|
129
|
+
// would be an `@else if` block, which we cannot
|
|
130
|
+
// combine with a previous or next `@if` block.
|
|
131
|
+
if (node.branches.length == 1 || !node.branches[1].expression) {
|
|
132
|
+
const ast = node.branches[0].expression.ast;
|
|
133
|
+
if (ast instanceof bundled_angular_compiler_1.Binary) {
|
|
134
|
+
return { node, lhs: ast.left, rhs: ast.right, operator: ast.operation };
|
|
135
|
+
}
|
|
136
|
+
if (ast instanceof bundled_angular_compiler_1.PrefixNot) {
|
|
137
|
+
return { node, lhs: ast.expression, rhs: undefined, operator: '!' };
|
|
138
|
+
}
|
|
139
|
+
return { node, lhs: ast, rhs: undefined, operator: '' };
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
return undefined;
|
|
143
|
+
}
|
|
144
|
+
function canCombine(previous, current) {
|
|
145
|
+
if (OPPOSITE_OPERATORS.get(previous.operator) === current.operator) {
|
|
146
|
+
if ((0, are_equivalent_asts_1.areEquivalentASTs)(previous.lhs, current.lhs)) {
|
|
147
|
+
if (previous.rhs === undefined && current.rhs === undefined) {
|
|
148
|
+
return true;
|
|
149
|
+
}
|
|
150
|
+
if (previous.rhs &&
|
|
151
|
+
current.rhs &&
|
|
152
|
+
(0, are_equivalent_asts_1.areEquivalentASTs)(previous.rhs, current.rhs)) {
|
|
153
|
+
return true;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
// Arrays cannot have a length less than zero, so there is
|
|
158
|
+
// a special case we can look for. If the previous node
|
|
159
|
+
// was an "is empty" and the current node is "is not empty"
|
|
160
|
+
// (or vice versa), then we can consider them opposites.
|
|
161
|
+
if ((isEmptyLength(previous) && isNonEmptyLength(current)) ||
|
|
162
|
+
(isNonEmptyLength(previous) && isEmptyLength(current))) {
|
|
163
|
+
return true;
|
|
164
|
+
}
|
|
165
|
+
return false;
|
|
166
|
+
}
|
|
167
|
+
function isEmptyLength(node) {
|
|
168
|
+
if (node.rhs !== undefined) {
|
|
169
|
+
if (node.operator === '==' || node.operator === '===') {
|
|
170
|
+
if ((0, ast_types_1.isLengthRead)(node.lhs) && (0, ast_types_1.isZero)(node.rhs)) {
|
|
171
|
+
return true;
|
|
172
|
+
}
|
|
173
|
+
if ((0, ast_types_1.isZero)(node.lhs) && (0, ast_types_1.isLengthRead)(node.rhs)) {
|
|
174
|
+
return true;
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
return false;
|
|
179
|
+
}
|
|
180
|
+
function isNonEmptyLength(node) {
|
|
181
|
+
if (node.rhs !== undefined) {
|
|
182
|
+
// We don't need to check for the inequality operators because
|
|
183
|
+
// they would be handled by the standard "are opposite" check.
|
|
184
|
+
if ((0, ast_types_1.isLengthRead)(node.lhs) && node.operator === '>' && (0, ast_types_1.isZero)(node.rhs)) {
|
|
185
|
+
return true;
|
|
186
|
+
}
|
|
187
|
+
if ((0, ast_types_1.isZero)(node.lhs) && node.operator === '<' && (0, ast_types_1.isLengthRead)(node.rhs)) {
|
|
188
|
+
return true;
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
return false;
|
|
192
|
+
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"prefer-at-empty.d.ts","sourceRoot":"","sources":["../../src/rules/prefer-at-empty.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"prefer-at-empty.d.ts","sourceRoot":"","sources":["../../src/rules/prefer-at-empty.ts"],"names":[],"mappings":"AAiBA,MAAM,MAAM,OAAO,GAAG,EAAE,CAAC;AACzB,MAAM,MAAM,UAAU,GAAG,eAAe,CAAC;AACzC,eAAO,MAAM,SAAS,oBAAoB,CAAC;;AAE3C,wBAwaG"}
|
|
@@ -5,6 +5,8 @@ const bundled_angular_compiler_1 = require("@angular-eslint/bundled-angular-comp
|
|
|
5
5
|
const utils_1 = require("@angular-eslint/utils");
|
|
6
6
|
const create_eslint_rule_1 = require("../utils/create-eslint-rule");
|
|
7
7
|
const are_equivalent_asts_1 = require("../utils/are-equivalent-asts");
|
|
8
|
+
const ast_types_1 = require("../utils/ast-types");
|
|
9
|
+
const to_range_1 = require("../utils/to-range");
|
|
8
10
|
exports.RULE_NAME = 'prefer-at-empty';
|
|
9
11
|
exports.default = (0, create_eslint_rule_1.createESLintRule)({
|
|
10
12
|
name: exports.RULE_NAME,
|
|
@@ -64,7 +66,7 @@ exports.default = (0, create_eslint_rule_1.createESLintRule)({
|
|
|
64
66
|
fix: branchEnd
|
|
65
67
|
? function* (fixer) {
|
|
66
68
|
// Remove the entire `@if` block.
|
|
67
|
-
yield fixer.removeRange(toRange(previous.node.sourceSpan));
|
|
69
|
+
yield fixer.removeRange((0, to_range_1.toRange)(previous.node.sourceSpan));
|
|
68
70
|
if (forInfo.node.empty) {
|
|
69
71
|
// There is already an `@empty` block. The contents of the
|
|
70
72
|
// `@if` block and the contents of the `@empty` block would
|
|
@@ -83,7 +85,7 @@ exports.default = (0, create_eslint_rule_1.createESLintRule)({
|
|
|
83
85
|
else {
|
|
84
86
|
// Take the contents of the `@if` block and move
|
|
85
87
|
// it into an `@empty` block after the `@for` block.
|
|
86
|
-
yield fixer.insertTextAfterRange(toRange(forInfo.node.sourceSpan), ` @empty {${context.sourceCode.text.slice(branch.startSourceSpan.end.offset, branchEnd.end.offset)}`);
|
|
88
|
+
yield fixer.insertTextAfterRange((0, to_range_1.toRange)(forInfo.node.sourceSpan), ` @empty {${context.sourceCode.text.slice(branch.startSourceSpan.end.offset, branchEnd.end.offset)}`);
|
|
87
89
|
}
|
|
88
90
|
}
|
|
89
91
|
: undefined,
|
|
@@ -115,14 +117,14 @@ exports.default = (0, create_eslint_rule_1.createESLintRule)({
|
|
|
115
117
|
// the existing contents of the `@empty` block. This can
|
|
116
118
|
// easily be achieved by removing the closing brace of the
|
|
117
119
|
// `@empty` block and removing the `@if` statement.
|
|
118
|
-
yield fixer.removeRange(toRange(previous.node.empty.endSourceSpan));
|
|
119
|
-
yield fixer.removeRange(toRange(ifInfo.node.startSourceSpan));
|
|
120
|
+
yield fixer.removeRange((0, to_range_1.toRange)(previous.node.empty.endSourceSpan));
|
|
121
|
+
yield fixer.removeRange((0, to_range_1.toRange)(ifInfo.node.startSourceSpan));
|
|
120
122
|
}
|
|
121
123
|
else {
|
|
122
124
|
// There is not already an `@empty` block, so
|
|
123
125
|
// we can create one by replacing the entire
|
|
124
126
|
// `@if (...) {` segment with `@empty {`.
|
|
125
|
-
yield fixer.replaceTextRange(toRange(ifInfo.node.startSourceSpan), '@empty {');
|
|
127
|
+
yield fixer.replaceTextRange((0, to_range_1.toRange)(ifInfo.node.startSourceSpan), '@empty {');
|
|
126
128
|
}
|
|
127
129
|
},
|
|
128
130
|
});
|
|
@@ -143,16 +145,16 @@ exports.default = (0, create_eslint_rule_1.createESLintRule)({
|
|
|
143
145
|
fix: previousIfBlockEnd
|
|
144
146
|
? (fixer) => [
|
|
145
147
|
// Remove the previous `@if` statement.
|
|
146
|
-
fixer.removeRange(toRange(previous.node.startSourceSpan)),
|
|
148
|
+
fixer.removeRange((0, to_range_1.toRange)(previous.node.startSourceSpan)),
|
|
147
149
|
// Remove the closing brace from the previous `@if` block.
|
|
148
|
-
fixer.removeRange(toRange(previousIfBlockEnd)),
|
|
150
|
+
fixer.removeRange((0, to_range_1.toRange)(previousIfBlockEnd)),
|
|
149
151
|
// Take the contents of the current `@if` block and move
|
|
150
152
|
// it into the `@empty` block of the previous `@for` block.
|
|
151
|
-
fixer.insertTextAfterRange(toRange(forBlock.sourceSpan), ` @empty {${context.sourceCode.text.slice(ifInfo.node.startSourceSpan.end.offset,
|
|
153
|
+
fixer.insertTextAfterRange((0, to_range_1.toRange)(forBlock.sourceSpan), ` @empty {${context.sourceCode.text.slice(ifInfo.node.startSourceSpan.end.offset,
|
|
152
154
|
// The end offset includes the closing brace.
|
|
153
155
|
ifInfo.node.sourceSpan.end.offset)}`),
|
|
154
156
|
// Remove the entirety of the current `@if` block.
|
|
155
|
-
fixer.removeRange(toRange(ifInfo.node.sourceSpan)),
|
|
157
|
+
fixer.removeRange((0, to_range_1.toRange)(ifInfo.node.sourceSpan)),
|
|
156
158
|
]
|
|
157
159
|
: undefined,
|
|
158
160
|
});
|
|
@@ -202,7 +204,7 @@ exports.default = (0, create_eslint_rule_1.createESLintRule)({
|
|
|
202
204
|
// There isn't an existing `@empty` block, so we can create
|
|
203
205
|
// one. We don't need to include a closing brace, because
|
|
204
206
|
// we can reuse the one from the end of the @`if` block.
|
|
205
|
-
yield fixer.insertTextAfterRange(toRange(forBlock.sourceSpan), ` @empty {${empty}`);
|
|
207
|
+
yield fixer.insertTextAfterRange((0, to_range_1.toRange)(forBlock.sourceSpan), ` @empty {${empty}`);
|
|
206
208
|
}
|
|
207
209
|
}
|
|
208
210
|
: undefined,
|
|
@@ -223,12 +225,12 @@ exports.default = (0, create_eslint_rule_1.createESLintRule)({
|
|
|
223
225
|
messageId: 'preferAtEmpty',
|
|
224
226
|
fix: (fixer) => [
|
|
225
227
|
// Remove the entire previous `@if` block.
|
|
226
|
-
fixer.removeRange(toRange(previous.node.sourceSpan)),
|
|
228
|
+
fixer.removeRange((0, to_range_1.toRange)(previous.node.sourceSpan)),
|
|
227
229
|
// Remove the current `@if` statement.
|
|
228
|
-
fixer.removeRange(toRange(ifNotInfo.node.startSourceSpan)),
|
|
230
|
+
fixer.removeRange((0, to_range_1.toRange)(ifNotInfo.node.startSourceSpan)),
|
|
229
231
|
// Take the contents of the previous `@if` block and move
|
|
230
232
|
// it into the `@empty` block after the `@for` block.
|
|
231
|
-
fixer.insertTextAfterRange(toRange(forBlock.sourceSpan), ` @empty {${context.sourceCode.text.slice(previous.node.startSourceSpan.end.offset,
|
|
233
|
+
fixer.insertTextAfterRange((0, to_range_1.toRange)(forBlock.sourceSpan), ` @empty {${context.sourceCode.text.slice(previous.node.startSourceSpan.end.offset,
|
|
232
234
|
// The end offset is after the closing `}`, so we
|
|
233
235
|
// need to subtract one to ensure it gets removed.
|
|
234
236
|
previous.node.sourceSpan.end.offset - 1)}`),
|
|
@@ -257,10 +259,10 @@ exports.default = (0, create_eslint_rule_1.createESLintRule)({
|
|
|
257
259
|
// block, the `@empty` block would never be rendered,
|
|
258
260
|
// so we can remove it. We could try to replace it,
|
|
259
261
|
// but it's easier to remove it and create a new one.
|
|
260
|
-
yield fixer.removeRange(toRange(forBlock.empty.sourceSpan));
|
|
262
|
+
yield fixer.removeRange((0, to_range_1.toRange)(forBlock.empty.sourceSpan));
|
|
261
263
|
}
|
|
262
264
|
// Remove the entire `@if (...) {` segment.
|
|
263
|
-
yield fixer.removeRange(toRange(info.node.startSourceSpan));
|
|
265
|
+
yield fixer.removeRange((0, to_range_1.toRange)(info.node.startSourceSpan));
|
|
264
266
|
const elseBranch = info.node.branches[1];
|
|
265
267
|
if (elseBranch.expression) {
|
|
266
268
|
// The second branch is an `@else if` branch. We
|
|
@@ -273,7 +275,7 @@ exports.default = (0, create_eslint_rule_1.createESLintRule)({
|
|
|
273
275
|
ifBranchEnd.end.offset - 1,
|
|
274
276
|
elseBranch.nameSpan.end.offset,
|
|
275
277
|
], '@empty { @if ');
|
|
276
|
-
yield fixer.insertTextAfterRange(toRange(ifEnd), '}');
|
|
278
|
+
yield fixer.insertTextAfterRange((0, to_range_1.toRange)(ifEnd), '}');
|
|
277
279
|
}
|
|
278
280
|
else {
|
|
279
281
|
// The second branch is just an `@else` branch, so we
|
|
@@ -424,16 +426,16 @@ function getNotEmptyTestCollection(node) {
|
|
|
424
426
|
if (node instanceof bundled_angular_compiler_1.ASTWithSource) {
|
|
425
427
|
node = node.ast;
|
|
426
428
|
}
|
|
427
|
-
if (isLengthRead(node)) {
|
|
429
|
+
if ((0, ast_types_1.isLengthRead)(node)) {
|
|
428
430
|
// @if (collection.length)
|
|
429
431
|
return node.receiver;
|
|
430
432
|
}
|
|
431
433
|
if (node instanceof bundled_angular_compiler_1.Binary) {
|
|
432
|
-
if (isLengthRead(node.left)) {
|
|
434
|
+
if ((0, ast_types_1.isLengthRead)(node.left)) {
|
|
433
435
|
if (node.operation === '!==' ||
|
|
434
436
|
node.operation === '>' ||
|
|
435
437
|
node.operation === '!=') {
|
|
436
|
-
if (isZero(node.right)) {
|
|
438
|
+
if ((0, ast_types_1.isZero)(node.right)) {
|
|
437
439
|
// @if (collection.length !== 0)
|
|
438
440
|
// @if (collection.length > 0)
|
|
439
441
|
// @if (collection.length != 0)
|
|
@@ -441,11 +443,11 @@ function getNotEmptyTestCollection(node) {
|
|
|
441
443
|
}
|
|
442
444
|
}
|
|
443
445
|
}
|
|
444
|
-
else if (isZero(node.left)) {
|
|
446
|
+
else if ((0, ast_types_1.isZero)(node.left)) {
|
|
445
447
|
if (node.operation === '!==' ||
|
|
446
448
|
node.operation === '<' ||
|
|
447
449
|
node.operation === '!=') {
|
|
448
|
-
if (isLengthRead(node.right)) {
|
|
450
|
+
if ((0, ast_types_1.isLengthRead)(node.right)) {
|
|
449
451
|
// @if (0 !== collection.length)
|
|
450
452
|
// @if (0 < collection.length)
|
|
451
453
|
// @if (0 != collection.length)
|
|
@@ -461,24 +463,24 @@ function getEmptyTestCollection(node) {
|
|
|
461
463
|
node = node.ast;
|
|
462
464
|
}
|
|
463
465
|
if (node instanceof bundled_angular_compiler_1.PrefixNot) {
|
|
464
|
-
if (isLengthRead(node.expression)) {
|
|
466
|
+
if ((0, ast_types_1.isLengthRead)(node.expression)) {
|
|
465
467
|
// @if (!collection.length)
|
|
466
468
|
return node.expression.receiver;
|
|
467
469
|
}
|
|
468
470
|
}
|
|
469
471
|
else if (node instanceof bundled_angular_compiler_1.Binary) {
|
|
470
|
-
if (isLengthRead(node.left)) {
|
|
472
|
+
if ((0, ast_types_1.isLengthRead)(node.left)) {
|
|
471
473
|
if (node.operation === '===' || node.operation === '==') {
|
|
472
|
-
if (isZero(node.right)) {
|
|
474
|
+
if ((0, ast_types_1.isZero)(node.right)) {
|
|
473
475
|
// @if (collection.length === 0)
|
|
474
476
|
// @if (collection.length == 0)
|
|
475
477
|
return node.left.receiver;
|
|
476
478
|
}
|
|
477
479
|
}
|
|
478
480
|
}
|
|
479
|
-
else if (isZero(node.left)) {
|
|
481
|
+
else if ((0, ast_types_1.isZero)(node.left)) {
|
|
480
482
|
if (node.operation === '===' || node.operation === '==') {
|
|
481
|
-
if (isLengthRead(node.right)) {
|
|
483
|
+
if ((0, ast_types_1.isLengthRead)(node.right)) {
|
|
482
484
|
// @if (0 === collection.length)
|
|
483
485
|
// @if (0 == collection.length)
|
|
484
486
|
return node.right.receiver;
|
|
@@ -488,12 +490,3 @@ function getEmptyTestCollection(node) {
|
|
|
488
490
|
}
|
|
489
491
|
return undefined;
|
|
490
492
|
}
|
|
491
|
-
function isLengthRead(node) {
|
|
492
|
-
return node instanceof bundled_angular_compiler_1.PropertyRead && node.name === 'length';
|
|
493
|
-
}
|
|
494
|
-
function isZero(node) {
|
|
495
|
-
return node instanceof bundled_angular_compiler_1.LiteralPrimitive && node.value === 0;
|
|
496
|
-
}
|
|
497
|
-
function toRange(span) {
|
|
498
|
-
return [span.start.offset, span.end.offset];
|
|
499
|
-
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ast-types.d.ts","sourceRoot":"","sources":["../../src/utils/ast-types.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,GAAG,EAEH,YAAY,EACb,MAAM,0CAA0C,CAAC;AAElD,wBAAgB,YAAY,CAAC,IAAI,EAAE,GAAG,GAAG,IAAI,IAAI,YAAY,CAE5D;AAED,wBAAgB,MAAM,CAAC,IAAI,EAAE,GAAG,GAAG,OAAO,CAEzC"}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.isLengthRead = isLengthRead;
|
|
4
|
+
exports.isZero = isZero;
|
|
5
|
+
const bundled_angular_compiler_1 = require("@angular-eslint/bundled-angular-compiler");
|
|
6
|
+
function isLengthRead(node) {
|
|
7
|
+
return node instanceof bundled_angular_compiler_1.PropertyRead && node.name === 'length';
|
|
8
|
+
}
|
|
9
|
+
function isZero(node) {
|
|
10
|
+
return node instanceof bundled_angular_compiler_1.LiteralPrimitive && node.value === 0;
|
|
11
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"to-range.d.ts","sourceRoot":"","sources":["../../src/utils/to-range.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,eAAe,EAAE,MAAM,0CAA0C,CAAC;AAE3E,wBAAgB,OAAO,CAAC,IAAI,EAAE,eAAe,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAE/D;AAED,wBAAgB,iBAAiB,CAAC,MAAM,EAAE,MAAM,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAElE"}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.toRange = toRange;
|
|
4
|
+
exports.toZeroLengthRange = toZeroLengthRange;
|
|
5
|
+
function toRange(span) {
|
|
6
|
+
return [span.start.offset, span.end.offset];
|
|
7
|
+
}
|
|
8
|
+
function toZeroLengthRange(offset) {
|
|
9
|
+
return [offset, offset];
|
|
10
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@angular-eslint/eslint-plugin-template",
|
|
3
|
-
"version": "20.
|
|
3
|
+
"version": "20.4.0",
|
|
4
4
|
"description": "ESLint plugin for Angular Templates",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -20,19 +20,19 @@
|
|
|
20
20
|
"dependencies": {
|
|
21
21
|
"aria-query": "5.3.2",
|
|
22
22
|
"axobject-query": "4.1.0",
|
|
23
|
-
"@angular-eslint/bundled-angular-compiler": "20.
|
|
24
|
-
"@angular-eslint/utils": "20.
|
|
23
|
+
"@angular-eslint/bundled-angular-compiler": "20.4.0",
|
|
24
|
+
"@angular-eslint/utils": "20.4.0"
|
|
25
25
|
},
|
|
26
26
|
"devDependencies": {
|
|
27
27
|
"@types/aria-query": "5.0.4",
|
|
28
|
-
"@angular-eslint/test-utils": "20.
|
|
28
|
+
"@angular-eslint/test-utils": "20.4.0"
|
|
29
29
|
},
|
|
30
30
|
"peerDependencies": {
|
|
31
31
|
"@typescript-eslint/types": "^7.11.0 || ^8.0.0",
|
|
32
32
|
"@typescript-eslint/utils": "^7.11.0 || ^8.0.0",
|
|
33
33
|
"eslint": "^8.57.0 || ^9.0.0",
|
|
34
34
|
"typescript": "*",
|
|
35
|
-
"@angular-eslint/template-parser": "20.
|
|
35
|
+
"@angular-eslint/template-parser": "20.4.0"
|
|
36
36
|
},
|
|
37
37
|
"gitHead": "e2006e5e9c99e5a943d1a999e0efa5247d29ec24"
|
|
38
38
|
}
|