@angular-eslint/eslint-plugin-template 19.4.1-alpha.1 → 19.4.1-alpha.11
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/all.json +1 -0
- 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-empty.d.ts +6 -0
- package/dist/rules/prefer-at-empty.d.ts.map +1 -0
- package/dist/rules/prefer-at-empty.js +492 -0
- package/dist/rules/prefer-template-literal.d.ts.map +1 -1
- package/dist/rules/prefer-template-literal.js +56 -18
- package/package.json +5 -5
package/README.md
CHANGED
|
@@ -62,6 +62,7 @@ Please see https://github.com/angular-eslint/angular-eslint for full usage instr
|
|
|
62
62
|
| [`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. | | | | |
|
|
63
63
|
| [`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: | |
|
|
64
64
|
| [`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: | |
|
|
65
|
+
| [`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: | | |
|
|
65
66
|
| [`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: | | |
|
|
66
67
|
| [`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. | | | | |
|
|
67
68
|
| [`prefer-ngsrc`](https://github.com/angular-eslint/angular-eslint/blob/main/packages/eslint-plugin-template/docs/rules/prefer-ngsrc.md) | Ensures ngSrc is used instead of src for img elements | | | | |
|
package/dist/configs/all.json
CHANGED
|
@@ -25,6 +25,7 @@
|
|
|
25
25
|
"@angular-eslint/template/no-negated-async": "error",
|
|
26
26
|
"@angular-eslint/template/no-nested-tags": "error",
|
|
27
27
|
"@angular-eslint/template/no-positive-tabindex": "error",
|
|
28
|
+
"@angular-eslint/template/prefer-at-empty": "error",
|
|
28
29
|
"@angular-eslint/template/prefer-contextual-for-variables": "error",
|
|
29
30
|
"@angular-eslint/template/prefer-control-flow": "error",
|
|
30
31
|
"@angular-eslint/template/prefer-ngsrc": "error",
|
package/dist/index.d.ts
CHANGED
|
@@ -27,6 +27,7 @@ declare const _default: {
|
|
|
27
27
|
"@angular-eslint/template/no-negated-async": string;
|
|
28
28
|
"@angular-eslint/template/no-nested-tags": string;
|
|
29
29
|
"@angular-eslint/template/no-positive-tabindex": string;
|
|
30
|
+
"@angular-eslint/template/prefer-at-empty": string;
|
|
30
31
|
"@angular-eslint/template/prefer-contextual-for-variables": string;
|
|
31
32
|
"@angular-eslint/template/prefer-control-flow": string;
|
|
32
33
|
"@angular-eslint/template/prefer-ngsrc": string;
|
|
@@ -109,6 +110,7 @@ declare const _default: {
|
|
|
109
110
|
"no-interpolation-in-attributes": import("@typescript-eslint/utils/ts-eslint").RuleModule<"noInterpolationInAttributes", import("./rules/no-interpolation-in-attributes").Options, import("./utils/create-eslint-rule").RuleDocs, import("@typescript-eslint/utils/ts-eslint").RuleListener>;
|
|
110
111
|
"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>;
|
|
111
112
|
"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>;
|
|
113
|
+
"prefer-at-empty": import("@typescript-eslint/utils/ts-eslint").RuleModule<"preferAtEmpty", [], import("./utils/create-eslint-rule").RuleDocs, import("@typescript-eslint/utils/ts-eslint").RuleListener>;
|
|
112
114
|
"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>;
|
|
113
115
|
"prefer-control-flow": import("@typescript-eslint/utils/ts-eslint").RuleModule<"preferControlFlow", [], import("./utils/create-eslint-rule").RuleDocs, import("@typescript-eslint/utils/ts-eslint").RuleListener>;
|
|
114
116
|
"prefer-self-closing-tags": import("@typescript-eslint/utils/ts-eslint").RuleModule<"preferSelfClosingTags", [], 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":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAkGA,kBA4CE"}
|
package/dist/index.js
CHANGED
|
@@ -64,6 +64,7 @@ const no_negated_async_1 = __importStar(require("./rules/no-negated-async"));
|
|
|
64
64
|
const no_nested_tags_1 = __importStar(require("./rules/no-nested-tags"));
|
|
65
65
|
const no_positive_tabindex_1 = __importStar(require("./rules/no-positive-tabindex"));
|
|
66
66
|
const prefer_ngsrc_1 = __importStar(require("./rules/prefer-ngsrc"));
|
|
67
|
+
const prefer_at_empty_1 = __importStar(require("./rules/prefer-at-empty"));
|
|
67
68
|
const prefer_contextual_for_variables_1 = __importStar(require("./rules/prefer-contextual-for-variables"));
|
|
68
69
|
const prefer_control_flow_1 = __importStar(require("./rules/prefer-control-flow"));
|
|
69
70
|
const prefer_self_closing_tags_1 = __importStar(require("./rules/prefer-self-closing-tags"));
|
|
@@ -105,6 +106,7 @@ module.exports = {
|
|
|
105
106
|
[no_interpolation_in_attributes_1.RULE_NAME]: no_interpolation_in_attributes_1.default,
|
|
106
107
|
[no_negated_async_1.RULE_NAME]: no_negated_async_1.default,
|
|
107
108
|
[no_positive_tabindex_1.RULE_NAME]: no_positive_tabindex_1.default,
|
|
109
|
+
[prefer_at_empty_1.RULE_NAME]: prefer_at_empty_1.default,
|
|
108
110
|
[prefer_contextual_for_variables_1.RULE_NAME]: prefer_contextual_for_variables_1.default,
|
|
109
111
|
[prefer_control_flow_1.RULE_NAME]: prefer_control_flow_1.default,
|
|
110
112
|
[prefer_self_closing_tags_1.RULE_NAME]: prefer_self_closing_tags_1.default,
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export type Options = [];
|
|
2
|
+
export type MessageIds = 'preferAtEmpty';
|
|
3
|
+
export declare const RULE_NAME = "prefer-at-empty";
|
|
4
|
+
declare const _default: import("@typescript-eslint/utils/ts-eslint").RuleModule<"preferAtEmpty", [], import("../utils/create-eslint-rule").RuleDocs, import("@typescript-eslint/utils/ts-eslint").RuleListener>;
|
|
5
|
+
export default _default;
|
|
6
|
+
//# sourceMappingURL=prefer-at-empty.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"prefer-at-empty.d.ts","sourceRoot":"","sources":["../../src/rules/prefer-at-empty.ts"],"names":[],"mappings":"AAkBA,MAAM,MAAM,OAAO,GAAG,EAAE,CAAC;AACzB,MAAM,MAAM,UAAU,GAAG,eAAe,CAAC;AACzC,eAAO,MAAM,SAAS,oBAAoB,CAAC;;AAE3C,wBAgaG"}
|
|
@@ -0,0 +1,492 @@
|
|
|
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
|
+
exports.RULE_NAME = 'prefer-at-empty';
|
|
9
|
+
exports.default = (0, create_eslint_rule_1.createESLintRule)({
|
|
10
|
+
name: exports.RULE_NAME,
|
|
11
|
+
meta: {
|
|
12
|
+
type: 'suggestion',
|
|
13
|
+
fixable: 'code',
|
|
14
|
+
docs: {
|
|
15
|
+
description: 'Prefer using `@empty` with `@for` loops instead of a separate `@if` or `@else` block to reduce code and make it easier to read.',
|
|
16
|
+
},
|
|
17
|
+
schema: [],
|
|
18
|
+
messages: {
|
|
19
|
+
preferAtEmpty: 'Prefer using `@for (...) {...} @empty {...}`.',
|
|
20
|
+
},
|
|
21
|
+
},
|
|
22
|
+
defaultOptions: [],
|
|
23
|
+
create(context) {
|
|
24
|
+
const parserServices = (0, utils_1.getTemplateParserServices)(context);
|
|
25
|
+
const previousNodeStack = [undefined];
|
|
26
|
+
function getOnlyForBlock(node) {
|
|
27
|
+
let forBlock;
|
|
28
|
+
// Find the only `@for` block in the children,
|
|
29
|
+
// ignoring any text nodes that are only whitespace.
|
|
30
|
+
for (const child of node.children) {
|
|
31
|
+
if (child instanceof bundled_angular_compiler_1.TmplAstForLoopBlock) {
|
|
32
|
+
if (forBlock) {
|
|
33
|
+
return undefined;
|
|
34
|
+
}
|
|
35
|
+
forBlock = child;
|
|
36
|
+
}
|
|
37
|
+
else if (child instanceof bundled_angular_compiler_1.TmplAstText) {
|
|
38
|
+
// The `value` property contains the HTML-decoded
|
|
39
|
+
// value, so we need to look at the raw source code
|
|
40
|
+
// to see if the content is only whitespace.
|
|
41
|
+
if (context.sourceCode.text
|
|
42
|
+
.slice(child.sourceSpan.start.offset, child.sourceSpan.end.offset)
|
|
43
|
+
.trim() !== '') {
|
|
44
|
+
return undefined;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
else {
|
|
48
|
+
return undefined;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
return forBlock;
|
|
52
|
+
}
|
|
53
|
+
function checkFor(forInfo, previous) {
|
|
54
|
+
// If the `@for` block is immediately preceded by an "if empty"
|
|
55
|
+
// block for the same collection, then that `@if` block can
|
|
56
|
+
// be moved into the `@empty` block.
|
|
57
|
+
if (previous?.kind === 'if-empty') {
|
|
58
|
+
if ((0, are_equivalent_asts_1.areEquivalentASTs)(forInfo.collection, previous.collection)) {
|
|
59
|
+
const branch = previous.node.branches[0];
|
|
60
|
+
const branchEnd = branch.endSourceSpan;
|
|
61
|
+
context.report({
|
|
62
|
+
loc: parserServices.convertNodeSourceSpanToLoc(previous.node.nameSpan),
|
|
63
|
+
messageId: 'preferAtEmpty',
|
|
64
|
+
fix: branchEnd
|
|
65
|
+
? function* (fixer) {
|
|
66
|
+
// Remove the entire `@if` block.
|
|
67
|
+
yield fixer.removeRange(toRange(previous.node.sourceSpan));
|
|
68
|
+
if (forInfo.node.empty) {
|
|
69
|
+
// There is already an `@empty` block. The contents of the
|
|
70
|
+
// `@if` block and the contents of the `@empty` block would
|
|
71
|
+
// both be shown in the collection is empty, so we need to
|
|
72
|
+
// combine the two blocks. The `@if` block would be rendered
|
|
73
|
+
// first, so it needs to be inserted before the existing
|
|
74
|
+
// contents of the `@empty` block.
|
|
75
|
+
yield fixer.insertTextAfterRange([
|
|
76
|
+
forInfo.node.empty.nameSpan.end.offset,
|
|
77
|
+
forInfo.node.empty.startSourceSpan.end.offset,
|
|
78
|
+
], context.sourceCode.text.slice(branch.startSourceSpan.end.offset,
|
|
79
|
+
// The end offset is after the closing `}`, so we
|
|
80
|
+
// need to subtract one to ensure it's not included.
|
|
81
|
+
branchEnd.end.offset - 1));
|
|
82
|
+
}
|
|
83
|
+
else {
|
|
84
|
+
// Take the contents of the `@if` block and move
|
|
85
|
+
// 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)}`);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
: undefined,
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
function checkIfEmpty(ifInfo, previous) {
|
|
95
|
+
if (!previous) {
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
// If the `@if` block is immediately preceded by a `@for`
|
|
99
|
+
// block for the same collection, then that `@if` block
|
|
100
|
+
// can be moved into the `@empty` block.
|
|
101
|
+
switch (previous.kind) {
|
|
102
|
+
case 'for':
|
|
103
|
+
if ((0, are_equivalent_asts_1.areEquivalentASTs)(ifInfo.collection, previous.collection)) {
|
|
104
|
+
// The `@if` block can be moved into the `@for` block,
|
|
105
|
+
// so report the problem on the `@if` block.
|
|
106
|
+
context.report({
|
|
107
|
+
loc: parserServices.convertNodeSourceSpanToLoc(ifInfo.node.nameSpan),
|
|
108
|
+
messageId: 'preferAtEmpty',
|
|
109
|
+
fix: function* (fixer) {
|
|
110
|
+
if (previous.node.empty?.endSourceSpan) {
|
|
111
|
+
// There is already an `@empty` block. The contents of
|
|
112
|
+
// the `@empty` block and the `@if` block would both be
|
|
113
|
+
// rendered. The `@empty` block would appear first, so
|
|
114
|
+
// we need to move the contents of the `@if` block after
|
|
115
|
+
// the existing contents of the `@empty` block. This can
|
|
116
|
+
// easily be achieved by removing the closing brace of the
|
|
117
|
+
// `@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
|
+
}
|
|
121
|
+
else {
|
|
122
|
+
// There is not already an `@empty` block, so
|
|
123
|
+
// we can create one by replacing the entire
|
|
124
|
+
// `@if (...) {` segment with `@empty {`.
|
|
125
|
+
yield fixer.replaceTextRange(toRange(ifInfo.node.startSourceSpan), '@empty {');
|
|
126
|
+
}
|
|
127
|
+
},
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
break;
|
|
131
|
+
case 'if-not-empty':
|
|
132
|
+
if ((0, are_equivalent_asts_1.areEquivalentASTs)(ifInfo.collection, previous.collection)) {
|
|
133
|
+
const forBlock = getOnlyForBlock(previous.node.branches[0]);
|
|
134
|
+
if (forBlock &&
|
|
135
|
+
(0, are_equivalent_asts_1.areEquivalentASTs)(ifInfo.collection, forBlock.expression.ast)) {
|
|
136
|
+
const previousIfBlockEnd = previous.node.endSourceSpan;
|
|
137
|
+
// The previous `@if` block can be removed and the current `@if`
|
|
138
|
+
// block moved into the `@for` block's `@empty` block, so report
|
|
139
|
+
// the problem on the previous `@if` block.
|
|
140
|
+
context.report({
|
|
141
|
+
loc: parserServices.convertNodeSourceSpanToLoc(previous.node.nameSpan),
|
|
142
|
+
messageId: 'preferAtEmpty',
|
|
143
|
+
fix: previousIfBlockEnd
|
|
144
|
+
? (fixer) => [
|
|
145
|
+
// Remove the previous `@if` statement.
|
|
146
|
+
fixer.removeRange(toRange(previous.node.startSourceSpan)),
|
|
147
|
+
// Remove the closing brace from the previous `@if` block.
|
|
148
|
+
fixer.removeRange(toRange(previousIfBlockEnd)),
|
|
149
|
+
// Take the contents of the current `@if` block and move
|
|
150
|
+
// 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,
|
|
152
|
+
// The end offset includes the closing brace.
|
|
153
|
+
ifInfo.node.sourceSpan.end.offset)}`),
|
|
154
|
+
// Remove the entirety of the current `@if` block.
|
|
155
|
+
fixer.removeRange(toRange(ifInfo.node.sourceSpan)),
|
|
156
|
+
]
|
|
157
|
+
: undefined,
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
function checkIfEmptyElse(info) {
|
|
164
|
+
// Look for an `@for` block in the `@else` branch.
|
|
165
|
+
const forBlock = getOnlyForBlock(info.node.branches[1]);
|
|
166
|
+
if (forBlock &&
|
|
167
|
+
(0, are_equivalent_asts_1.areEquivalentASTs)(info.collection, forBlock.expression.ast)) {
|
|
168
|
+
const ifBranchEnd = info.node.branches[0].endSourceSpan;
|
|
169
|
+
// The contents of the `@if` block can be moved into an
|
|
170
|
+
// `@empty` block, so report the problem on the `@if` block.
|
|
171
|
+
context.report({
|
|
172
|
+
loc: parserServices.convertNodeSourceSpanToLoc(info.node.nameSpan),
|
|
173
|
+
messageId: 'preferAtEmpty',
|
|
174
|
+
fix: ifBranchEnd
|
|
175
|
+
? function* (fixer) {
|
|
176
|
+
// Remove the entire `@if` branch through to the
|
|
177
|
+
// start of the body of the `@else` block.
|
|
178
|
+
yield fixer.removeRange([
|
|
179
|
+
info.node.sourceSpan.start.offset,
|
|
180
|
+
info.node.branches[1].startSourceSpan.end.offset,
|
|
181
|
+
]);
|
|
182
|
+
// Take the contents of the `@if` branch and move
|
|
183
|
+
// it into an `@empty` block after the `@for` block.
|
|
184
|
+
const empty = context.sourceCode.text.slice(info.node.startSourceSpan.end.offset, ifBranchEnd.start.offset);
|
|
185
|
+
if (forBlock.empty?.endSourceSpan) {
|
|
186
|
+
// There is already an `@empty` block, but because the `@for`
|
|
187
|
+
// block was inside an `@else` block, the `@empty` block
|
|
188
|
+
// would never have be rendered, so we can replace its contents.
|
|
189
|
+
yield fixer.replaceTextRange([
|
|
190
|
+
forBlock.empty.startSourceSpan.end.offset,
|
|
191
|
+
forBlock.empty.endSourceSpan.start.offset,
|
|
192
|
+
], empty);
|
|
193
|
+
}
|
|
194
|
+
else {
|
|
195
|
+
// There isn't an existing `@empty` block, so we can create
|
|
196
|
+
// one. We don't need to include a closing brace, because
|
|
197
|
+
// we can reuse the one from the end of the @`if` block.
|
|
198
|
+
yield fixer.insertTextAfterRange(toRange(forBlock.sourceSpan), ` @empty {${empty}`);
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
: undefined,
|
|
202
|
+
});
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
function checkIfNotEmpty(ifNotInfo, previous) {
|
|
206
|
+
if (previous?.kind === 'if-empty') {
|
|
207
|
+
if ((0, are_equivalent_asts_1.areEquivalentASTs)(ifNotInfo.collection, previous.collection)) {
|
|
208
|
+
const forBlock = getOnlyForBlock(ifNotInfo.node.branches[0]);
|
|
209
|
+
if (forBlock &&
|
|
210
|
+
(0, are_equivalent_asts_1.areEquivalentASTs)(ifNotInfo.collection, forBlock.expression.ast)) {
|
|
211
|
+
// The `@if` block can be removed and the contents of
|
|
212
|
+
// the `@else` block moved into an `@empty` block,
|
|
213
|
+
// so report the problem on the `@if` block.
|
|
214
|
+
context.report({
|
|
215
|
+
loc: parserServices.convertNodeSourceSpanToLoc(ifNotInfo.node.nameSpan),
|
|
216
|
+
messageId: 'preferAtEmpty',
|
|
217
|
+
fix: (fixer) => [
|
|
218
|
+
// Remove the entire previous `@if` block.
|
|
219
|
+
fixer.removeRange(toRange(previous.node.sourceSpan)),
|
|
220
|
+
// Remove the current `@if` statement.
|
|
221
|
+
fixer.removeRange(toRange(ifNotInfo.node.startSourceSpan)),
|
|
222
|
+
// Take the contents of the previous `@if` block and move
|
|
223
|
+
// it into the `@empty` block after the `@for` block.
|
|
224
|
+
fixer.insertTextAfterRange(toRange(forBlock.sourceSpan), ` @empty {${context.sourceCode.text.slice(previous.node.startSourceSpan.end.offset,
|
|
225
|
+
// The end offset is after the closing `}`, so we
|
|
226
|
+
// need to subtract one to ensure it gets removed.
|
|
227
|
+
previous.node.sourceSpan.end.offset - 1)}`),
|
|
228
|
+
],
|
|
229
|
+
});
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
function checkIfNotEmptyElse(info) {
|
|
235
|
+
const forBlock = getOnlyForBlock(info.node.branches[0]);
|
|
236
|
+
if (forBlock &&
|
|
237
|
+
(0, are_equivalent_asts_1.areEquivalentASTs)(info.collection, forBlock.expression.ast)) {
|
|
238
|
+
const ifBranchEnd = info.node.branches[0].endSourceSpan;
|
|
239
|
+
const ifEnd = info.node.endSourceSpan;
|
|
240
|
+
// The `@if` block can be removed and the contents of
|
|
241
|
+
// the `@else` block moved into an `@empty` block,
|
|
242
|
+
// so report the problem on the `@if` block.
|
|
243
|
+
context.report({
|
|
244
|
+
loc: parserServices.convertNodeSourceSpanToLoc(info.node.nameSpan),
|
|
245
|
+
messageId: 'preferAtEmpty',
|
|
246
|
+
fix: ifBranchEnd && ifEnd
|
|
247
|
+
? function* (fixer) {
|
|
248
|
+
if (forBlock.empty) {
|
|
249
|
+
// Because the `@for` block was inside an `@if`
|
|
250
|
+
// block, the `@empty` block would never be rendered,
|
|
251
|
+
// so we can remove it. We could try to replace it,
|
|
252
|
+
// but it's easier to remove it and create a new one.
|
|
253
|
+
yield fixer.removeRange(toRange(forBlock.empty.sourceSpan));
|
|
254
|
+
}
|
|
255
|
+
// Remove the entire `@if (...) {` segment.
|
|
256
|
+
yield fixer.removeRange(toRange(info.node.startSourceSpan));
|
|
257
|
+
const elseBranch = info.node.branches[1];
|
|
258
|
+
if (elseBranch.expression) {
|
|
259
|
+
// The second branch is an `@else if` branch. We
|
|
260
|
+
// need to turn it into its own `@if` block. Replace
|
|
261
|
+
// the `@else if` text with the start of the `@empty`
|
|
262
|
+
// block and the start of the `@if` block, then put
|
|
263
|
+
// a closing brace after the original `@if` block
|
|
264
|
+
// to close the `@empty` block.
|
|
265
|
+
yield fixer.replaceTextRange([
|
|
266
|
+
ifBranchEnd.end.offset - 1,
|
|
267
|
+
elseBranch.nameSpan.end.offset,
|
|
268
|
+
], '@empty { @if ');
|
|
269
|
+
yield fixer.insertTextAfterRange(toRange(ifEnd), '}');
|
|
270
|
+
}
|
|
271
|
+
else {
|
|
272
|
+
// The second branch is just an `@else` branch, so we
|
|
273
|
+
// can replace from end of the `@if` branch through to
|
|
274
|
+
// the end of the `@else` statement with `@empty {`.
|
|
275
|
+
// The children of the `@else` branch, and the closing
|
|
276
|
+
// `}`, will become part of the `@empty` block.
|
|
277
|
+
yield fixer.replaceTextRange([
|
|
278
|
+
// The end offset is after the closing `}`, so we
|
|
279
|
+
// need to subtract one to ensure it gets removed.
|
|
280
|
+
ifBranchEnd.end.offset - 1,
|
|
281
|
+
elseBranch.startSourceSpan.end.offset,
|
|
282
|
+
], '@empty {');
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
: undefined,
|
|
286
|
+
});
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
return {
|
|
290
|
+
// We need to visit `@for` and `@if` blocks, but we
|
|
291
|
+
// also need to know if there are any nodes immediately
|
|
292
|
+
// before them, so we need to visit all nodes.
|
|
293
|
+
'*'(node) {
|
|
294
|
+
const current = getNodeInfo(node);
|
|
295
|
+
if (current !== undefined) {
|
|
296
|
+
switch (current.kind) {
|
|
297
|
+
case 'for':
|
|
298
|
+
checkFor(current, previousNodeStack.at(-1));
|
|
299
|
+
break;
|
|
300
|
+
case 'if-empty':
|
|
301
|
+
checkIfEmpty(current, previousNodeStack.at(-1));
|
|
302
|
+
break;
|
|
303
|
+
case 'if-empty-else':
|
|
304
|
+
checkIfEmptyElse(current);
|
|
305
|
+
break;
|
|
306
|
+
case 'if-not-empty':
|
|
307
|
+
checkIfNotEmpty(current, previousNodeStack.at(-1));
|
|
308
|
+
break;
|
|
309
|
+
case 'if-not-empty-else':
|
|
310
|
+
checkIfNotEmptyElse(current);
|
|
311
|
+
break;
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
// Record this current node as the previous node so that
|
|
315
|
+
// we can get the info when we look at the next sibling.
|
|
316
|
+
previousNodeStack[previousNodeStack.length - 1] = current;
|
|
317
|
+
// We are about to visit the children of this node,
|
|
318
|
+
// so push a new "previous node info" onto the stack.
|
|
319
|
+
// The previous node of the first child is undefined.
|
|
320
|
+
previousNodeStack.push(undefined);
|
|
321
|
+
},
|
|
322
|
+
'*:exit'() {
|
|
323
|
+
// We've finished visiting the children of this node,
|
|
324
|
+
// so pop the "previous node info" off the stack.
|
|
325
|
+
previousNodeStack.pop();
|
|
326
|
+
},
|
|
327
|
+
};
|
|
328
|
+
},
|
|
329
|
+
});
|
|
330
|
+
function getNodeInfo(node) {
|
|
331
|
+
if (node instanceof bundled_angular_compiler_1.TmplAstForLoopBlock) {
|
|
332
|
+
return {
|
|
333
|
+
node,
|
|
334
|
+
kind: 'for',
|
|
335
|
+
collection: node.expression.ast,
|
|
336
|
+
};
|
|
337
|
+
}
|
|
338
|
+
if (node instanceof bundled_angular_compiler_1.TmplAstIfBlock) {
|
|
339
|
+
if (node.branches.length === 0) {
|
|
340
|
+
return undefined;
|
|
341
|
+
}
|
|
342
|
+
if (!node.branches[0].expression) {
|
|
343
|
+
return undefined;
|
|
344
|
+
}
|
|
345
|
+
let collection = getNotEmptyTestCollection(node.branches[0].expression);
|
|
346
|
+
if (collection) {
|
|
347
|
+
// The block is either:
|
|
348
|
+
//
|
|
349
|
+
// @if (collection.length > 0) {
|
|
350
|
+
// }
|
|
351
|
+
//
|
|
352
|
+
// or:
|
|
353
|
+
//
|
|
354
|
+
// @if (collection.length > 0) {
|
|
355
|
+
// } @else {
|
|
356
|
+
// }
|
|
357
|
+
//
|
|
358
|
+
// or:
|
|
359
|
+
//
|
|
360
|
+
// @if (collection.length > 0) {
|
|
361
|
+
// } @else if (condition){
|
|
362
|
+
// }
|
|
363
|
+
//
|
|
364
|
+
// or:
|
|
365
|
+
//
|
|
366
|
+
// @if (collection.length > 0) {
|
|
367
|
+
// } @else if (condition) {
|
|
368
|
+
// } @else {
|
|
369
|
+
// }
|
|
370
|
+
//
|
|
371
|
+
// In any case, we treat this as one of the "if not empty"
|
|
372
|
+
// nodes, because if there is an `@for` block in the `@if`
|
|
373
|
+
// branch, then whatever is in the `@else if` or @else`
|
|
374
|
+
// branches, could be moved into the `@empty` block.
|
|
375
|
+
return {
|
|
376
|
+
node,
|
|
377
|
+
kind: node.branches.length === 1 ? 'if-not-empty' : 'if-not-empty-else',
|
|
378
|
+
collection,
|
|
379
|
+
};
|
|
380
|
+
}
|
|
381
|
+
collection = getEmptyTestCollection(node.branches[0].expression);
|
|
382
|
+
if (collection) {
|
|
383
|
+
// Unlike the "if not empty" cases, there are only two cases
|
|
384
|
+
// that could be considered an "if empty" case:
|
|
385
|
+
//
|
|
386
|
+
// @if (collection.length === 0) {
|
|
387
|
+
// }
|
|
388
|
+
//
|
|
389
|
+
// or:
|
|
390
|
+
//
|
|
391
|
+
// @if (collection.length > 0) {
|
|
392
|
+
// } @else {
|
|
393
|
+
// }
|
|
394
|
+
//
|
|
395
|
+
// If there is an `@else if`, then whatever is in the `@if`
|
|
396
|
+
// branch could not safely be moved into an `@empty` block
|
|
397
|
+
// because of the condition in the `@else if` branch.
|
|
398
|
+
if (node.branches.length === 1) {
|
|
399
|
+
return {
|
|
400
|
+
node,
|
|
401
|
+
kind: 'if-empty',
|
|
402
|
+
collection,
|
|
403
|
+
};
|
|
404
|
+
}
|
|
405
|
+
else if (node.branches.length === 2 && !node.branches[1].expression) {
|
|
406
|
+
return {
|
|
407
|
+
node,
|
|
408
|
+
kind: 'if-empty-else',
|
|
409
|
+
collection,
|
|
410
|
+
};
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
return undefined;
|
|
415
|
+
}
|
|
416
|
+
function getNotEmptyTestCollection(node) {
|
|
417
|
+
if (node instanceof bundled_angular_compiler_1.ASTWithSource) {
|
|
418
|
+
node = node.ast;
|
|
419
|
+
}
|
|
420
|
+
if (isLengthRead(node)) {
|
|
421
|
+
// @if (collection.length)
|
|
422
|
+
return node.receiver;
|
|
423
|
+
}
|
|
424
|
+
if (node instanceof bundled_angular_compiler_1.Binary) {
|
|
425
|
+
if (isLengthRead(node.left)) {
|
|
426
|
+
if (node.operation === '!==' ||
|
|
427
|
+
node.operation === '>' ||
|
|
428
|
+
node.operation === '!=') {
|
|
429
|
+
if (isZero(node.right)) {
|
|
430
|
+
// @if (collection.length !== 0)
|
|
431
|
+
// @if (collection.length > 0)
|
|
432
|
+
// @if (collection.length != 0)
|
|
433
|
+
return node.left.receiver;
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
else if (isZero(node.left)) {
|
|
438
|
+
if (node.operation === '!==' ||
|
|
439
|
+
node.operation === '<' ||
|
|
440
|
+
node.operation === '!=') {
|
|
441
|
+
if (isLengthRead(node.right)) {
|
|
442
|
+
// @if (0 !== collection.length)
|
|
443
|
+
// @if (0 < collection.length)
|
|
444
|
+
// @if (0 != collection.length)
|
|
445
|
+
return node.right.receiver;
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
return undefined;
|
|
451
|
+
}
|
|
452
|
+
function getEmptyTestCollection(node) {
|
|
453
|
+
if (node instanceof bundled_angular_compiler_1.ASTWithSource) {
|
|
454
|
+
node = node.ast;
|
|
455
|
+
}
|
|
456
|
+
if (node instanceof bundled_angular_compiler_1.PrefixNot) {
|
|
457
|
+
if (isLengthRead(node.expression)) {
|
|
458
|
+
// @if (!collection.length)
|
|
459
|
+
return node.expression.receiver;
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
else if (node instanceof bundled_angular_compiler_1.Binary) {
|
|
463
|
+
if (isLengthRead(node.left)) {
|
|
464
|
+
if (node.operation === '===' || node.operation === '==') {
|
|
465
|
+
if (isZero(node.right)) {
|
|
466
|
+
// @if (collection.length === 0)
|
|
467
|
+
// @if (collection.length == 0)
|
|
468
|
+
return node.left.receiver;
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
else if (isZero(node.left)) {
|
|
473
|
+
if (node.operation === '===' || node.operation === '==') {
|
|
474
|
+
if (isLengthRead(node.right)) {
|
|
475
|
+
// @if (0 === collection.length)
|
|
476
|
+
// @if (0 == collection.length)
|
|
477
|
+
return node.right.receiver;
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
return undefined;
|
|
483
|
+
}
|
|
484
|
+
function isLengthRead(node) {
|
|
485
|
+
return node instanceof bundled_angular_compiler_1.PropertyRead && node.name === 'length';
|
|
486
|
+
}
|
|
487
|
+
function isZero(node) {
|
|
488
|
+
return node instanceof bundled_angular_compiler_1.LiteralPrimitive && node.value === 0;
|
|
489
|
+
}
|
|
490
|
+
function toRange(span) {
|
|
491
|
+
return [span.start.offset, span.end.offset];
|
|
492
|
+
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"prefer-template-literal.d.ts","sourceRoot":"","sources":["../../src/rules/prefer-template-literal.ts"],"names":[],"mappings":"AAeA,QAAA,MAAM,SAAS,0BAA0B,CAAC;AAE1C,MAAM,MAAM,OAAO,GAAG,EAAE,CAAC;AACzB,MAAM,MAAM,UAAU,GAAG,OAAO,SAAS,CAAC;AAC1C,eAAO,MAAM,SAAS,4BAA4B,CAAC;;AAEnD,
|
|
1
|
+
{"version":3,"file":"prefer-template-literal.d.ts","sourceRoot":"","sources":["../../src/rules/prefer-template-literal.ts"],"names":[],"mappings":"AAeA,QAAA,MAAM,SAAS,0BAA0B,CAAC;AAE1C,MAAM,MAAM,OAAO,GAAG,EAAE,CAAC;AACzB,MAAM,MAAM,UAAU,GAAG,OAAO,SAAS,CAAC;AAC1C,eAAO,MAAM,SAAS,4BAA4B,CAAC;;AAEnD,wBAiJG"}
|
|
@@ -45,6 +45,11 @@ exports.default = (0, create_eslint_rule_1.createESLintRule)({
|
|
|
45
45
|
}
|
|
46
46
|
return '`';
|
|
47
47
|
}
|
|
48
|
+
function hasParentheses(node) {
|
|
49
|
+
const { start, end } = node.sourceSpan;
|
|
50
|
+
const text = sourceCode.text.slice(start - 1, end + 1);
|
|
51
|
+
return text.startsWith('(') && text.endsWith(')');
|
|
52
|
+
}
|
|
48
53
|
context.report({
|
|
49
54
|
loc: {
|
|
50
55
|
start: sourceCode.getLocFromIndex(start),
|
|
@@ -58,13 +63,50 @@ exports.default = (0, create_eslint_rule_1.createESLintRule)({
|
|
|
58
63
|
const quote = getQuote();
|
|
59
64
|
return fixer.replaceTextRange([start, end], `${quote}${(0, literal_primitive_1.getLiteralPrimitiveStringValue)(left, quote)}${(0, literal_primitive_1.getLiteralPrimitiveStringValue)(right, quote)}${quote}`);
|
|
60
65
|
}
|
|
61
|
-
const fixes =
|
|
66
|
+
const fixes = Array();
|
|
67
|
+
const leftHasParentheses = hasParentheses(left);
|
|
68
|
+
const rightHasParentheses = hasParentheses(right);
|
|
69
|
+
// Remove the left first parenthesis if it exists
|
|
70
|
+
if (leftHasParentheses) {
|
|
71
|
+
fixes.push(fixer.removeRange([
|
|
72
|
+
left.sourceSpan.start - 1,
|
|
73
|
+
left.sourceSpan.start,
|
|
74
|
+
]));
|
|
75
|
+
}
|
|
62
76
|
// Fix the left side
|
|
63
77
|
fixes.push(...getLeftSideFixes(fixer, left));
|
|
78
|
+
// Remove the left last parenthesis if it exists
|
|
79
|
+
if (leftHasParentheses) {
|
|
80
|
+
fixes.push(fixer.removeRange([
|
|
81
|
+
left.sourceSpan.end,
|
|
82
|
+
left.sourceSpan.end + 1,
|
|
83
|
+
]));
|
|
84
|
+
}
|
|
64
85
|
// Remove the `+` sign
|
|
65
|
-
fixes.push(fixer.removeRange([
|
|
86
|
+
fixes.push(fixer.removeRange([
|
|
87
|
+
leftHasParentheses
|
|
88
|
+
? left.sourceSpan.end + 1
|
|
89
|
+
: left.sourceSpan.end,
|
|
90
|
+
rightHasParentheses
|
|
91
|
+
? right.sourceSpan.start - 1
|
|
92
|
+
: right.sourceSpan.start,
|
|
93
|
+
]));
|
|
94
|
+
// Remove the right first parenthesis if it exists
|
|
95
|
+
if (rightHasParentheses) {
|
|
96
|
+
fixes.push(fixer.removeRange([
|
|
97
|
+
right.sourceSpan.start - 1,
|
|
98
|
+
right.sourceSpan.start,
|
|
99
|
+
]));
|
|
100
|
+
}
|
|
66
101
|
// Fix the right side
|
|
67
102
|
fixes.push(...getRightSideFixes(fixer, right));
|
|
103
|
+
// Remove the right last parenthesis if it exists
|
|
104
|
+
if (rightHasParentheses) {
|
|
105
|
+
fixes.push(fixer.removeRange([
|
|
106
|
+
right.sourceSpan.end,
|
|
107
|
+
right.sourceSpan.end + 1,
|
|
108
|
+
]));
|
|
109
|
+
}
|
|
68
110
|
return fixes;
|
|
69
111
|
},
|
|
70
112
|
});
|
|
@@ -78,19 +120,17 @@ function getLeftSideFixes(fixer, left) {
|
|
|
78
120
|
// Remove the end ` sign from the left side
|
|
79
121
|
return [fixer.removeRange([end - 1, end])];
|
|
80
122
|
}
|
|
81
|
-
|
|
123
|
+
if ((0, literal_primitive_1.isLiteralPrimitive)(left)) {
|
|
82
124
|
// Transform left side to template literal
|
|
83
125
|
return [
|
|
84
126
|
fixer.replaceTextRange([start, end], `\`${(0, literal_primitive_1.getLiteralPrimitiveStringValue)(left, '`')}`),
|
|
85
127
|
];
|
|
86
128
|
}
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
];
|
|
93
|
-
}
|
|
129
|
+
// Transform left side to template literal
|
|
130
|
+
return [
|
|
131
|
+
fixer.insertTextBeforeRange([start, end], '`${'),
|
|
132
|
+
fixer.insertTextAfterRange([start, end], '}'),
|
|
133
|
+
];
|
|
94
134
|
}
|
|
95
135
|
function getRightSideFixes(fixer, right) {
|
|
96
136
|
const { start, end } = right.sourceSpan;
|
|
@@ -98,17 +138,15 @@ function getRightSideFixes(fixer, right) {
|
|
|
98
138
|
// Remove the start ` sign from the right side
|
|
99
139
|
return [fixer.removeRange([start, start + 1])];
|
|
100
140
|
}
|
|
101
|
-
|
|
141
|
+
if ((0, literal_primitive_1.isLiteralPrimitive)(right)) {
|
|
102
142
|
// Transform right side to template literal if it's a string
|
|
103
143
|
return [
|
|
104
144
|
fixer.replaceTextRange([start, end], `${(0, literal_primitive_1.getLiteralPrimitiveStringValue)(right, '`')}\``),
|
|
105
145
|
];
|
|
106
146
|
}
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
];
|
|
113
|
-
}
|
|
147
|
+
// Transform right side to template literal
|
|
148
|
+
return [
|
|
149
|
+
fixer.insertTextBeforeRange([start, end], '${'),
|
|
150
|
+
fixer.insertTextAfterRange([start, end], '}`'),
|
|
151
|
+
];
|
|
114
152
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@angular-eslint/eslint-plugin-template",
|
|
3
|
-
"version": "19.4.1-alpha.
|
|
3
|
+
"version": "19.4.1-alpha.11",
|
|
4
4
|
"description": "ESLint plugin for Angular Templates",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -20,13 +20,13 @@
|
|
|
20
20
|
"dependencies": {
|
|
21
21
|
"aria-query": "5.3.2",
|
|
22
22
|
"axobject-query": "4.1.0",
|
|
23
|
-
"@angular-eslint/bundled-angular-compiler": "19.4.1-alpha.
|
|
24
|
-
"@angular-eslint/utils": "19.4.1-alpha.
|
|
23
|
+
"@angular-eslint/bundled-angular-compiler": "19.4.1-alpha.11",
|
|
24
|
+
"@angular-eslint/utils": "19.4.1-alpha.11"
|
|
25
25
|
},
|
|
26
26
|
"devDependencies": {
|
|
27
27
|
"@types/aria-query": "5.0.4",
|
|
28
|
-
"@angular-eslint/template-parser": "19.4.1-alpha.
|
|
29
|
-
"@angular-eslint/test-utils": "19.4.1-alpha.
|
|
28
|
+
"@angular-eslint/template-parser": "19.4.1-alpha.11",
|
|
29
|
+
"@angular-eslint/test-utils": "19.4.1-alpha.11"
|
|
30
30
|
},
|
|
31
31
|
"peerDependencies": {
|
|
32
32
|
"@typescript-eslint/types": "^7.11.0 || ^8.0.0",
|