@angular-eslint/eslint-plugin-template 19.2.2-alpha.4 → 19.2.2-alpha.6
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/alt-text.js +1 -1
- package/dist/rules/attributes-order.js +1 -1
- package/dist/rules/button-has-type.js +1 -1
- package/dist/rules/click-events-have-key-events.js +1 -1
- package/dist/rules/conditional-complexity.js +1 -1
- package/dist/rules/elements-content.js +1 -1
- package/dist/rules/i18n.js +3 -3
- package/dist/rules/interactive-supports-focus.js +1 -1
- package/dist/rules/label-has-associated-control.js +1 -1
- package/dist/rules/mouse-events-have-key-events.js +1 -1
- package/dist/rules/no-autofocus.js +1 -1
- package/dist/rules/no-distracting-elements.js +1 -1
- package/dist/rules/no-duplicate-attributes.js +1 -1
- package/dist/rules/no-inline-styles.js +1 -1
- package/dist/rules/no-interpolation-in-attributes.js +1 -1
- package/dist/rules/no-positive-tabindex.js +1 -1
- package/dist/rules/prefer-contextual-for-variables.d.ts +17 -0
- package/dist/rules/prefer-contextual-for-variables.d.ts.map +1 -0
- package/dist/rules/prefer-contextual-for-variables.js +546 -0
- package/dist/rules/prefer-ngsrc.js +1 -1
- package/dist/rules/prefer-self-closing-tags.js +1 -1
- package/dist/rules/role-has-required-aria.js +1 -1
- package/dist/rules/table-scope.js +1 -1
- package/dist/rules/valid-aria.js +1 -1
- package/dist/utils/are-equivalent-asts.d.ts +3 -0
- package/dist/utils/are-equivalent-asts.d.ts.map +1 -0
- package/dist/utils/are-equivalent-asts.js +115 -0
- package/package.json +5 -5
package/README.md
CHANGED
|
@@ -61,6 +61,7 @@ Please see https://github.com/angular-eslint/angular-eslint for full usage instr
|
|
|
61
61
|
| [`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. | | | | |
|
|
62
62
|
| [`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: | |
|
|
63
63
|
| [`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: | |
|
|
64
|
+
| [`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: | | |
|
|
64
65
|
| [`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. | | | | |
|
|
65
66
|
| [`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 | | | | |
|
|
66
67
|
| [`role-has-required-aria`](https://github.com/angular-eslint/angular-eslint/blob/main/packages/eslint-plugin-template/docs/rules/role-has-required-aria.md) | [Accessibility] Ensures elements with ARIA roles have all required properties for that role. | | | :bulb: | :accessibility: |
|
package/dist/configs/all.json
CHANGED
|
@@ -24,6 +24,7 @@
|
|
|
24
24
|
"@angular-eslint/template/no-interpolation-in-attributes": "error",
|
|
25
25
|
"@angular-eslint/template/no-negated-async": "error",
|
|
26
26
|
"@angular-eslint/template/no-positive-tabindex": "error",
|
|
27
|
+
"@angular-eslint/template/prefer-contextual-for-variables": "error",
|
|
27
28
|
"@angular-eslint/template/prefer-control-flow": "error",
|
|
28
29
|
"@angular-eslint/template/prefer-ngsrc": "error",
|
|
29
30
|
"@angular-eslint/template/prefer-self-closing-tags": "error",
|
package/dist/index.d.ts
CHANGED
|
@@ -26,6 +26,7 @@ declare const _default: {
|
|
|
26
26
|
"@angular-eslint/template/no-interpolation-in-attributes": string;
|
|
27
27
|
"@angular-eslint/template/no-negated-async": string;
|
|
28
28
|
"@angular-eslint/template/no-positive-tabindex": string;
|
|
29
|
+
"@angular-eslint/template/prefer-contextual-for-variables": string;
|
|
29
30
|
"@angular-eslint/template/prefer-control-flow": string;
|
|
30
31
|
"@angular-eslint/template/prefer-ngsrc": string;
|
|
31
32
|
"@angular-eslint/template/prefer-self-closing-tags": string;
|
|
@@ -105,6 +106,7 @@ declare const _default: {
|
|
|
105
106
|
"no-interpolation-in-attributes": import("@typescript-eslint/utils/ts-eslint").RuleModule<"noInterpolationInAttributes", [], import("./utils/create-eslint-rule").RuleDocs, import("@typescript-eslint/utils/ts-eslint").RuleListener>;
|
|
106
107
|
"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>;
|
|
107
108
|
"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>;
|
|
109
|
+
"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>;
|
|
108
110
|
"prefer-control-flow": import("@typescript-eslint/utils/ts-eslint").RuleModule<"preferControlFlow", [], import("./utils/create-eslint-rule").RuleDocs, import("@typescript-eslint/utils/ts-eslint").RuleListener>;
|
|
109
111
|
"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>;
|
|
110
112
|
"prefer-static-string-properties": import("@typescript-eslint/utils/ts-eslint").RuleModule<"preferStaticStringProperties", [], 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":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAyFA,kBAyCE"}
|
package/dist/index.js
CHANGED
|
@@ -53,6 +53,7 @@ const no_interpolation_in_attributes_1 = __importStar(require("./rules/no-interp
|
|
|
53
53
|
const no_negated_async_1 = __importStar(require("./rules/no-negated-async"));
|
|
54
54
|
const no_positive_tabindex_1 = __importStar(require("./rules/no-positive-tabindex"));
|
|
55
55
|
const prefer_ngsrc_1 = __importStar(require("./rules/prefer-ngsrc"));
|
|
56
|
+
const prefer_contextual_for_variables_1 = __importStar(require("./rules/prefer-contextual-for-variables"));
|
|
56
57
|
const prefer_control_flow_1 = __importStar(require("./rules/prefer-control-flow"));
|
|
57
58
|
const prefer_self_closing_tags_1 = __importStar(require("./rules/prefer-self-closing-tags"));
|
|
58
59
|
const prefer_static_string_properties_1 = __importStar(require("./rules/prefer-static-string-properties"));
|
|
@@ -91,6 +92,7 @@ module.exports = {
|
|
|
91
92
|
[no_interpolation_in_attributes_1.RULE_NAME]: no_interpolation_in_attributes_1.default,
|
|
92
93
|
[no_negated_async_1.RULE_NAME]: no_negated_async_1.default,
|
|
93
94
|
[no_positive_tabindex_1.RULE_NAME]: no_positive_tabindex_1.default,
|
|
95
|
+
[prefer_contextual_for_variables_1.RULE_NAME]: prefer_contextual_for_variables_1.default,
|
|
94
96
|
[prefer_control_flow_1.RULE_NAME]: prefer_control_flow_1.default,
|
|
95
97
|
[prefer_self_closing_tags_1.RULE_NAME]: prefer_self_closing_tags_1.default,
|
|
96
98
|
[prefer_static_string_properties_1.RULE_NAME]: prefer_static_string_properties_1.default,
|
package/dist/rules/alt-text.js
CHANGED
|
@@ -21,7 +21,7 @@ exports.default = (0, create_eslint_rule_1.createESLintRule)({
|
|
|
21
21
|
create(context) {
|
|
22
22
|
const parserServices = (0, utils_1.getTemplateParserServices)(context);
|
|
23
23
|
return {
|
|
24
|
-
'Element
|
|
24
|
+
'Element[name=/^(img|area|object|input)$/]'(node) {
|
|
25
25
|
const isValid = isValidNode(node);
|
|
26
26
|
if (!isValid) {
|
|
27
27
|
const loc = parserServices.convertElementSourceSpanToLoc(context, node);
|
|
@@ -67,7 +67,7 @@ exports.default = (0, create_eslint_rule_1.createESLintRule)({
|
|
|
67
67
|
return adjustLocation(parserServices.convertNodeSourceSpanToLoc(attr.sourceSpan), 'location', attr);
|
|
68
68
|
}
|
|
69
69
|
return {
|
|
70
|
-
['Element
|
|
70
|
+
['Element, Template'](node) {
|
|
71
71
|
if (isImplicitTemplate(node)) {
|
|
72
72
|
return;
|
|
73
73
|
}
|
|
@@ -42,7 +42,7 @@ exports.default = (0, create_eslint_rule_1.createESLintRule)({
|
|
|
42
42
|
create(context, [{ ignoreWithDirectives }]) {
|
|
43
43
|
const parserServices = (0, utils_1.getTemplateParserServices)(context);
|
|
44
44
|
return {
|
|
45
|
-
[`Element
|
|
45
|
+
[`Element[name=button]`](element) {
|
|
46
46
|
if (!isTypeAttributePresentInElement(element)) {
|
|
47
47
|
if (!isIgnored(ignoreWithDirectives, element)) {
|
|
48
48
|
context.report({
|
|
@@ -53,7 +53,7 @@ exports.default = (0, create_eslint_rule_1.createESLintRule)({
|
|
|
53
53
|
data: { maxComplexity, totalComplexity },
|
|
54
54
|
});
|
|
55
55
|
},
|
|
56
|
-
Interpolation
|
|
56
|
+
Interpolation({ expressions }) {
|
|
57
57
|
for (const expression of expressions) {
|
|
58
58
|
const totalComplexity = getTotalComplexity(expression);
|
|
59
59
|
if (totalComplexity <= maxComplexity) {
|
|
@@ -44,7 +44,7 @@ exports.default = (0, create_eslint_rule_1.createESLintRule)({
|
|
|
44
44
|
create(context, [{ allowList }]) {
|
|
45
45
|
const parserServices = (0, utils_1.getTemplateParserServices)(context);
|
|
46
46
|
return {
|
|
47
|
-
'Element
|
|
47
|
+
'Element[name=/^(a|button|h1|h2|h3|h4|h5|h6)$/][children.length=0]'(node) {
|
|
48
48
|
if ((0, is_hidden_from_screen_reader_1.isHiddenFromScreenReader)(node))
|
|
49
49
|
return;
|
|
50
50
|
const { attributes, inputs, name: element, sourceSpan } = node;
|
package/dist/rules/i18n.js
CHANGED
|
@@ -287,7 +287,7 @@ exports.default = (0, create_eslint_rule_1.createESLintRule)({
|
|
|
287
287
|
checkId ||
|
|
288
288
|
requireDescription ||
|
|
289
289
|
requireMeaning) && {
|
|
290
|
-
[`:matches(Element
|
|
290
|
+
[`:matches(Element, Template[tagName="ng-template"])${allowMarkupInContent ? '[i18n]' : ''}`](node) {
|
|
291
291
|
handleElementOrTemplate(node);
|
|
292
292
|
},
|
|
293
293
|
}),
|
|
@@ -295,12 +295,12 @@ exports.default = (0, create_eslint_rule_1.createESLintRule)({
|
|
|
295
295
|
checkId ||
|
|
296
296
|
requireDescription ||
|
|
297
297
|
requireMeaning) && {
|
|
298
|
-
[`Element
|
|
298
|
+
[`Element > TextAttribute[value=${PL_PATTERN}]`](node) {
|
|
299
299
|
handleTextAttribute(node);
|
|
300
300
|
},
|
|
301
301
|
}),
|
|
302
302
|
...(checkText && {
|
|
303
|
-
[`BoundText, Icu
|
|
303
|
+
[`BoundText, Icu, Text[value=${PL_PATTERN}]`](node) {
|
|
304
304
|
handleBoundTextOrIcuOrText(node);
|
|
305
305
|
},
|
|
306
306
|
}),
|
|
@@ -38,7 +38,7 @@ exports.default = (0, create_eslint_rule_1.createESLintRule)({
|
|
|
38
38
|
defaultOptions: [{ allowList: DEFAULT_ALLOW_LIST }],
|
|
39
39
|
create(context, [{ allowList }]) {
|
|
40
40
|
return {
|
|
41
|
-
Element
|
|
41
|
+
Element(node) {
|
|
42
42
|
const elementType = node.name;
|
|
43
43
|
if (!(0, get_dom_elements_1.getDomElements)().has(elementType)) {
|
|
44
44
|
return;
|
|
@@ -39,7 +39,7 @@ exports.default = (0, create_eslint_rule_1.createESLintRule)({
|
|
|
39
39
|
];
|
|
40
40
|
return eventPairs.reduce((accumulator, [keyEvent, mouseEvent]) => ({
|
|
41
41
|
...accumulator,
|
|
42
|
-
[`Element
|
|
42
|
+
[`Element[name=${domElementsPattern}]:has(BoundEvent[name='${mouseEvent}']):not(:has(BoundEvent[name='${keyEvent}']))`]({ sourceSpan, }) {
|
|
43
43
|
const loc = parserServices.convertNodeSourceSpanToLoc(sourceSpan);
|
|
44
44
|
context.report({
|
|
45
45
|
loc,
|
|
@@ -24,7 +24,7 @@ exports.default = (0, create_eslint_rule_1.createESLintRule)({
|
|
|
24
24
|
const parserServices = (0, utils_1.getTemplateParserServices)(context);
|
|
25
25
|
const elementNamePattern = (0, to_pattern_1.toPattern)([...(0, get_dom_elements_1.getDomElements)()]);
|
|
26
26
|
return {
|
|
27
|
-
[`Element
|
|
27
|
+
[`Element[name=${elementNamePattern}] > :matches(BoundAttribute, TextAttribute)[name="autofocus"]`]({ sourceSpan, }) {
|
|
28
28
|
const loc = parserServices.convertNodeSourceSpanToLoc(sourceSpan);
|
|
29
29
|
context.report({
|
|
30
30
|
loc,
|
|
@@ -21,7 +21,7 @@ exports.default = (0, create_eslint_rule_1.createESLintRule)({
|
|
|
21
21
|
create(context) {
|
|
22
22
|
const parserServices = (0, utils_1.getTemplateParserServices)(context);
|
|
23
23
|
return {
|
|
24
|
-
'Element
|
|
24
|
+
'Element[name=/^(blink|marquee)$/]'({ name: element, sourceSpan, }) {
|
|
25
25
|
const loc = parserServices.convertNodeSourceSpanToLoc(sourceSpan);
|
|
26
26
|
context.report({
|
|
27
27
|
loc,
|
|
@@ -52,7 +52,7 @@ exports.default = (0, create_eslint_rule_1.createESLintRule)({
|
|
|
52
52
|
create(context, [{ allowTwoWayDataBinding, allowStylePrecedenceDuplicates, ignore }]) {
|
|
53
53
|
const parserServices = (0, utils_1.getTemplateParserServices)(context);
|
|
54
54
|
return {
|
|
55
|
-
Element
|
|
55
|
+
Element({ inputs, outputs, attributes }) {
|
|
56
56
|
// According to the Angular documentation (https://angular.dev/guide/templates/class-binding#styling-precedence)
|
|
57
57
|
// Angular merges both attributes which means their combined use can be seen as valid
|
|
58
58
|
const angularStylePrecedenceDuplicatesAllowed = ['class', 'style'];
|
|
@@ -39,7 +39,7 @@ exports.default = (0, create_eslint_rule_1.createESLintRule)({
|
|
|
39
39
|
create(context, [{ allowNgStyle, allowBindToStyle }]) {
|
|
40
40
|
const parserServices = (0, utils_1.getTemplateParserServices)(context);
|
|
41
41
|
return {
|
|
42
|
-
Element
|
|
42
|
+
Element(node) {
|
|
43
43
|
let isInvalid = false;
|
|
44
44
|
if (!allowNgStyle && !allowBindToStyle) {
|
|
45
45
|
isInvalid =
|
|
@@ -19,7 +19,7 @@ exports.default = (0, create_eslint_rule_1.createESLintRule)({
|
|
|
19
19
|
create(context) {
|
|
20
20
|
const sourceCode = context.sourceCode;
|
|
21
21
|
return {
|
|
22
|
-
['BoundAttribute Interpolation
|
|
22
|
+
['BoundAttribute Interpolation'](interpolation) {
|
|
23
23
|
const { sourceSpan: { start, end }, } = interpolation;
|
|
24
24
|
context.report({
|
|
25
25
|
loc: {
|
|
@@ -25,7 +25,7 @@ exports.default = (0, create_eslint_rule_1.createESLintRule)({
|
|
|
25
25
|
const parserServices = (0, utils_1.getTemplateParserServices)(context);
|
|
26
26
|
const elementNamePattern = (0, to_pattern_1.toPattern)([...(0, get_dom_elements_1.getDomElements)()]);
|
|
27
27
|
return {
|
|
28
|
-
[`Element
|
|
28
|
+
[`Element[name=${elementNamePattern}] > BoundAttribute[name="tabindex"][value.ast.value>0], TextAttribute[name="tabindex"][value>0]`]({ valueSpan, }) {
|
|
29
29
|
const loc = parserServices.convertNodeSourceSpanToLoc(valueSpan);
|
|
30
30
|
context.report({
|
|
31
31
|
loc,
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export type Options = [
|
|
2
|
+
{
|
|
3
|
+
readonly allowedAliases?: {
|
|
4
|
+
$count?: readonly string[];
|
|
5
|
+
$index?: readonly string[];
|
|
6
|
+
$first?: readonly string[];
|
|
7
|
+
$last?: readonly string[];
|
|
8
|
+
$even?: readonly string[];
|
|
9
|
+
$odd?: readonly string[];
|
|
10
|
+
};
|
|
11
|
+
}
|
|
12
|
+
];
|
|
13
|
+
export type MessageIds = 'preferContextualVariable' | 'preferCount' | 'preferFirst' | 'preferLast' | 'preferEven' | 'preferOdd';
|
|
14
|
+
export declare const RULE_NAME = "prefer-contextual-for-variables";
|
|
15
|
+
declare const _default: import("@typescript-eslint/utils/ts-eslint").RuleModule<MessageIds, Options, import("../utils/create-eslint-rule").RuleDocs, import("@typescript-eslint/utils/ts-eslint").RuleListener>;
|
|
16
|
+
export default _default;
|
|
17
|
+
//# sourceMappingURL=prefer-contextual-for-variables.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"prefer-contextual-for-variables.d.ts","sourceRoot":"","sources":["../../src/rules/prefer-contextual-for-variables.ts"],"names":[],"mappings":"AAmBA,MAAM,MAAM,OAAO,GAAG;IACpB;QACE,QAAQ,CAAC,cAAc,CAAC,EAAE;YACxB,MAAM,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;YAC3B,MAAM,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;YAC3B,MAAM,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;YAC3B,KAAK,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;YAC1B,KAAK,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;YAC1B,IAAI,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;SAC1B,CAAC;KACH;CACF,CAAC;AACF,MAAM,MAAM,UAAU,GAClB,0BAA0B,GAC1B,aAAa,GACb,aAAa,GACb,YAAY,GACZ,YAAY,GACZ,WAAW,CAAC;AAChB,eAAO,MAAM,SAAS,oCAAoC,CAAC;;AAc3D,wBAiaG"}
|
|
@@ -0,0 +1,546 @@
|
|
|
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-contextual-for-variables';
|
|
9
|
+
const DEFAULT_OPTIONS = {
|
|
10
|
+
allowedAliases: {
|
|
11
|
+
$count: [],
|
|
12
|
+
$index: [],
|
|
13
|
+
$first: [],
|
|
14
|
+
$last: [],
|
|
15
|
+
$even: [],
|
|
16
|
+
$odd: [],
|
|
17
|
+
},
|
|
18
|
+
};
|
|
19
|
+
const EQUALITY_OPERATORS = ['===', '=='];
|
|
20
|
+
const INEQUALITY_OPERATORS = ['!==', '!='];
|
|
21
|
+
const LOGICAL_OPERATORS = ['&&', '||'];
|
|
22
|
+
exports.default = (0, create_eslint_rule_1.createESLintRule)({
|
|
23
|
+
name: exports.RULE_NAME,
|
|
24
|
+
meta: {
|
|
25
|
+
type: 'suggestion',
|
|
26
|
+
docs: {
|
|
27
|
+
description: 'Ensures that contextual variables are used in @for blocks where possible instead of aliasing them.',
|
|
28
|
+
},
|
|
29
|
+
fixable: 'code',
|
|
30
|
+
schema: [
|
|
31
|
+
{
|
|
32
|
+
type: 'object',
|
|
33
|
+
properties: {
|
|
34
|
+
allowedAliases: {
|
|
35
|
+
type: 'object',
|
|
36
|
+
properties: {
|
|
37
|
+
$count: {
|
|
38
|
+
type: 'array',
|
|
39
|
+
items: { type: 'string' },
|
|
40
|
+
description: 'Aliases for $count that are allowed to be used.',
|
|
41
|
+
},
|
|
42
|
+
$index: {
|
|
43
|
+
type: 'array',
|
|
44
|
+
items: { type: 'string' },
|
|
45
|
+
description: 'Aliases for $index that are allowed to be used.',
|
|
46
|
+
},
|
|
47
|
+
$first: {
|
|
48
|
+
type: 'array',
|
|
49
|
+
items: { type: 'string' },
|
|
50
|
+
description: 'Aliases for $first that are allowed to be used.',
|
|
51
|
+
},
|
|
52
|
+
$last: {
|
|
53
|
+
type: 'array',
|
|
54
|
+
items: { type: 'string' },
|
|
55
|
+
description: 'Aliases for $last that are allowed to be used.',
|
|
56
|
+
},
|
|
57
|
+
$even: {
|
|
58
|
+
type: 'array',
|
|
59
|
+
items: { type: 'string' },
|
|
60
|
+
description: 'Aliases for $even that are allowed to be used.',
|
|
61
|
+
},
|
|
62
|
+
$odd: {
|
|
63
|
+
type: 'array',
|
|
64
|
+
items: { type: 'string' },
|
|
65
|
+
description: 'Aliases for $odd that are allowed to be used.',
|
|
66
|
+
},
|
|
67
|
+
},
|
|
68
|
+
},
|
|
69
|
+
},
|
|
70
|
+
additionalProperties: false,
|
|
71
|
+
},
|
|
72
|
+
],
|
|
73
|
+
messages: {
|
|
74
|
+
preferContextualVariable: "Use the '{{name}}' contextual variable instead of aliasing it.",
|
|
75
|
+
preferCount: "Use '$count' instead of '{{ expression }}'.",
|
|
76
|
+
preferFirst: "Use '$first' instead of '{{ expression }}'.",
|
|
77
|
+
preferLast: "Use '$last' instead of '{{ expression }}'.",
|
|
78
|
+
preferEven: "Use '$even' instead of '{{ expression }}'.",
|
|
79
|
+
preferOdd: "Use '$odd' instead of '{{ expression }}'.",
|
|
80
|
+
},
|
|
81
|
+
},
|
|
82
|
+
defaultOptions: [DEFAULT_OPTIONS],
|
|
83
|
+
create(context, [{ allowedAliases }]) {
|
|
84
|
+
const parserServices = (0, utils_1.getTemplateParserServices)(context);
|
|
85
|
+
const forLoops = [];
|
|
86
|
+
function reportSimplifications(messageId, forLoop) {
|
|
87
|
+
const simplifications = forLoop.simplifications?.[messageId];
|
|
88
|
+
if (!simplifications) {
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
const sourceCode = context.sourceCode;
|
|
92
|
+
for (const simplification of simplifications) {
|
|
93
|
+
context.report({
|
|
94
|
+
messageId,
|
|
95
|
+
loc: {
|
|
96
|
+
start: sourceCode.getLocFromIndex(simplification.range[0]),
|
|
97
|
+
end: sourceCode.getLocFromIndex(simplification.range[1]),
|
|
98
|
+
},
|
|
99
|
+
data: {
|
|
100
|
+
expression: context.sourceCode.text.slice(simplification.range[0], simplification.range[1]),
|
|
101
|
+
},
|
|
102
|
+
fix: (fixer) => fixer.replaceTextRange(simplification.range, simplification.replacement),
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
return {
|
|
107
|
+
ForLoopBlock(node) {
|
|
108
|
+
// We need to know if there are nested for loops before we
|
|
109
|
+
// can report any problems. When there are nested for loops,
|
|
110
|
+
// aliasing will be required to access the outer contextual
|
|
111
|
+
// variables from within the inner loop, so we won't report
|
|
112
|
+
// any problems when there are nested for loops.
|
|
113
|
+
const nested = forLoops.length > 0;
|
|
114
|
+
if (nested) {
|
|
115
|
+
forLoops[forLoops.length - 1].canAlias = true;
|
|
116
|
+
}
|
|
117
|
+
// All contextual variables are defined, but
|
|
118
|
+
// only aliased variables have a value span.
|
|
119
|
+
const variables = node.contextVariables.filter((x) => x.valueSpan);
|
|
120
|
+
forLoops.push({
|
|
121
|
+
canAlias: nested,
|
|
122
|
+
source: node.expression.ast,
|
|
123
|
+
variables,
|
|
124
|
+
references:
|
|
125
|
+
// Don't bother creating a map of variable
|
|
126
|
+
// references if there are no variables to track.
|
|
127
|
+
variables.length > 0
|
|
128
|
+
? new Map(variables.map((variable) => [variable.name, []]))
|
|
129
|
+
: undefined,
|
|
130
|
+
});
|
|
131
|
+
},
|
|
132
|
+
'ForLoopBlock:exit'() {
|
|
133
|
+
const forLoop = forLoops.pop();
|
|
134
|
+
if (!forLoop) {
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
if (!forLoop.canAlias) {
|
|
138
|
+
const problems = [];
|
|
139
|
+
for (const [index, variable] of forLoop.variables.entries()) {
|
|
140
|
+
const allowed = getAllowedAliases(allowedAliases, variable.value);
|
|
141
|
+
if (allowed === undefined || !allowed.includes(variable.name)) {
|
|
142
|
+
problems.push({
|
|
143
|
+
index,
|
|
144
|
+
variable,
|
|
145
|
+
loc: parserServices.convertNodeSourceSpanToLoc(variable.sourceSpan),
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
for (const problem of problems) {
|
|
150
|
+
context.report({
|
|
151
|
+
messageId: 'preferContextualVariable',
|
|
152
|
+
loc: problem.loc,
|
|
153
|
+
data: { name: problem.variable.value },
|
|
154
|
+
fix: function* (fixer) {
|
|
155
|
+
yield fixer.removeRange(getVariableRangeToRemove(problem, context.sourceCode, forLoop.variables.length));
|
|
156
|
+
// Replace any references to the alias
|
|
157
|
+
// with the contextual variable name.
|
|
158
|
+
const references = forLoop.references?.get(problem.variable.name);
|
|
159
|
+
if (references) {
|
|
160
|
+
for (const reference of references) {
|
|
161
|
+
yield fixer.replaceTextRange(reference, problem.variable.value);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
},
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
if (forLoop.simplifications) {
|
|
169
|
+
reportSimplifications('preferCount', forLoop);
|
|
170
|
+
reportSimplifications('preferFirst', forLoop);
|
|
171
|
+
reportSimplifications('preferLast', forLoop);
|
|
172
|
+
reportSimplifications('preferEven', forLoop);
|
|
173
|
+
reportSimplifications('preferOdd', forLoop);
|
|
174
|
+
}
|
|
175
|
+
},
|
|
176
|
+
PropertyRead(node) {
|
|
177
|
+
// Get the information for the inner-most for loop (which will be
|
|
178
|
+
// the last one in the array) so that we can record the usage of
|
|
179
|
+
// aliases and expressions using contextual variables that can be
|
|
180
|
+
// simplified. We only need the inner-most for loop because we
|
|
181
|
+
// don't remove aliases when there are nested for loops (meaning
|
|
182
|
+
// we don't need to record alias usage for the outer for loop), and
|
|
183
|
+
// any contextual variables will only reference the inner most loop.
|
|
184
|
+
const forLoop = forLoops.at(-1);
|
|
185
|
+
if (!forLoop) {
|
|
186
|
+
return;
|
|
187
|
+
}
|
|
188
|
+
// Record any references to aliased variables so
|
|
189
|
+
// that we can replace them if we remove the alias.
|
|
190
|
+
forLoop.references
|
|
191
|
+
?.get(node.name)
|
|
192
|
+
?.push([node.sourceSpan.start, node.sourceSpan.end]);
|
|
193
|
+
// If the `length` property is being read from the same
|
|
194
|
+
// value that was used as the source of the for loop, then
|
|
195
|
+
// we can simplify that to just use the `$count` variable.
|
|
196
|
+
if (node.name === 'length' &&
|
|
197
|
+
(0, are_equivalent_asts_1.areEquivalentASTs)(node.receiver, forLoop.source)) {
|
|
198
|
+
recordSimplification(node, forLoop, 'preferCount', '$count');
|
|
199
|
+
}
|
|
200
|
+
},
|
|
201
|
+
Binary(node) {
|
|
202
|
+
const forLoop = forLoops.at(-1);
|
|
203
|
+
if (!forLoop) {
|
|
204
|
+
return;
|
|
205
|
+
}
|
|
206
|
+
if (isIndex(node.left)) {
|
|
207
|
+
if (isZero(node.right)) {
|
|
208
|
+
if (EQUALITY_OPERATORS.includes(node.operation)) {
|
|
209
|
+
// `$index === 0` can be simplified to `$first`.
|
|
210
|
+
recordSimplification(node, forLoop, 'preferFirst', '$first');
|
|
211
|
+
}
|
|
212
|
+
else if (INEQUALITY_OPERATORS.includes(node.operation) ||
|
|
213
|
+
node.operation === '>') {
|
|
214
|
+
// `$index !== 0` or `$index > 0` can be simplified to `!$first`.
|
|
215
|
+
recordSimplification(node, forLoop, 'preferFirst', '!$first');
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
else if (isCountMinusOne(node.right)) {
|
|
219
|
+
if (EQUALITY_OPERATORS.includes(node.operation)) {
|
|
220
|
+
// `$index === ($count - 1)` can be simplified to `$last`.
|
|
221
|
+
recordSimplification(node, forLoop, 'preferLast', '$last');
|
|
222
|
+
}
|
|
223
|
+
else if (INEQUALITY_OPERATORS.includes(node.operation) ||
|
|
224
|
+
node.operation === '<') {
|
|
225
|
+
// `$index !== ($count - 1)` or `$index < ($count - 1)`
|
|
226
|
+
// can be simplified to `!$last`.
|
|
227
|
+
recordSimplification(node, forLoop, 'preferLast', '!$last');
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
else if (isZero(node.left)) {
|
|
232
|
+
if (isIndex(node.right)) {
|
|
233
|
+
if (EQUALITY_OPERATORS.includes(node.operation)) {
|
|
234
|
+
// `0 === $index` can be simplified to `$first`.
|
|
235
|
+
recordSimplification(node, forLoop, 'preferFirst', '$first');
|
|
236
|
+
}
|
|
237
|
+
else if (INEQUALITY_OPERATORS.includes(node.operation) ||
|
|
238
|
+
node.operation === '<') {
|
|
239
|
+
// `0 !== $index` or `0 < $index` can be simplified to `!$first`.
|
|
240
|
+
recordSimplification(node, forLoop, 'preferFirst', '!$first');
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
else if (isIndexModTwo(node.right)) {
|
|
244
|
+
if (EQUALITY_OPERATORS.includes(node.operation)) {
|
|
245
|
+
// `0 == ($index % 2)` can be simplified to `$even`.
|
|
246
|
+
recordSimplification(node, forLoop, 'preferEven', '$even');
|
|
247
|
+
}
|
|
248
|
+
else if (INEQUALITY_OPERATORS.includes(node.operation) ||
|
|
249
|
+
node.operation === '<') {
|
|
250
|
+
// `0 !== ($index % 2)` or `0 < ($index % 2)`
|
|
251
|
+
// can be simplified to `$odd`.
|
|
252
|
+
recordSimplification(node, forLoop, 'preferOdd', '$odd');
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
else if (isOne(node.left)) {
|
|
257
|
+
if (isIndexModTwo(node.right)) {
|
|
258
|
+
if (EQUALITY_OPERATORS.includes(node.operation)) {
|
|
259
|
+
// `1 === ($index % 2)` can be simplified to `$odd`.
|
|
260
|
+
recordSimplification(node, forLoop, 'preferOdd', '$odd');
|
|
261
|
+
}
|
|
262
|
+
else if (INEQUALITY_OPERATORS.includes(node.operation) ||
|
|
263
|
+
node.operation === '>') {
|
|
264
|
+
// `1 !== ($index % 2)` or `1 > ($index % 2)`
|
|
265
|
+
// can be simplified to `$even`.
|
|
266
|
+
recordSimplification(node, forLoop, 'preferEven', '$even');
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
else if (isCount(node.left)) {
|
|
271
|
+
if (isIndexPlusOne(node.right)) {
|
|
272
|
+
if (EQUALITY_OPERATORS.includes(node.operation)) {
|
|
273
|
+
// `$count === ($index + 1)` can be simplified to `$last`.
|
|
274
|
+
recordSimplification(node, forLoop, 'preferLast', '$last');
|
|
275
|
+
}
|
|
276
|
+
else if (INEQUALITY_OPERATORS.includes(node.operation) ||
|
|
277
|
+
node.operation === '>') {
|
|
278
|
+
// `$count !== ($index + 1)` or `$count > ($index + 1)`
|
|
279
|
+
// can be simplified to `!$last`.
|
|
280
|
+
recordSimplification(node, forLoop, 'preferLast', '!$last');
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
else if (isIndexPlusOne(node.left)) {
|
|
285
|
+
if (isCount(node.right)) {
|
|
286
|
+
if (EQUALITY_OPERATORS.includes(node.operation)) {
|
|
287
|
+
// `($index + 1) === $count` can be simplified to `$last`.
|
|
288
|
+
recordSimplification(node, forLoop, 'preferLast', '$last');
|
|
289
|
+
}
|
|
290
|
+
else if (INEQUALITY_OPERATORS.includes(node.operation) ||
|
|
291
|
+
node.operation === '<') {
|
|
292
|
+
// `($index + 1) !== $count` or `($index + 1) < $count`
|
|
293
|
+
// can be simplified to `!$last`.
|
|
294
|
+
recordSimplification(node, forLoop, 'preferLast', '!$last');
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
else if (isCountMinusOne(node.left)) {
|
|
299
|
+
if (isIndex(node.right)) {
|
|
300
|
+
if (EQUALITY_OPERATORS.includes(node.operation)) {
|
|
301
|
+
// `($count - 1) === $index` can be simplified to `$last`.
|
|
302
|
+
recordSimplification(node, forLoop, 'preferLast', '$last');
|
|
303
|
+
}
|
|
304
|
+
else if (INEQUALITY_OPERATORS.includes(node.operation) ||
|
|
305
|
+
node.operation === '>') {
|
|
306
|
+
// `($count - 1) !== $index` or `($count - 1) > $index`
|
|
307
|
+
// can be simplified to `!$last`.
|
|
308
|
+
recordSimplification(node, forLoop, 'preferLast', '!$last');
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
else if (isIndexModTwo(node.left)) {
|
|
313
|
+
if (isZero(node.right)) {
|
|
314
|
+
if (EQUALITY_OPERATORS.includes(node.operation)) {
|
|
315
|
+
// `($index % 2) === 0` can be simplified to `$even`.
|
|
316
|
+
recordSimplification(node, forLoop, 'preferEven', '$even');
|
|
317
|
+
}
|
|
318
|
+
else if (INEQUALITY_OPERATORS.includes(node.operation) ||
|
|
319
|
+
node.operation === '>') {
|
|
320
|
+
// `($index % 2) !== 0` or `($index % 2) > 0`
|
|
321
|
+
// can be simplified to `$odd`.
|
|
322
|
+
recordSimplification(node, forLoop, 'preferOdd', '$odd');
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
else if (isOne(node.right)) {
|
|
326
|
+
if (EQUALITY_OPERATORS.includes(node.operation)) {
|
|
327
|
+
// `($index % 2) === 1` can be simplified to `$odd`.
|
|
328
|
+
recordSimplification(node, forLoop, 'preferOdd', '$odd');
|
|
329
|
+
}
|
|
330
|
+
else if (INEQUALITY_OPERATORS.includes(node.operation) ||
|
|
331
|
+
node.operation === '<') {
|
|
332
|
+
// `($index % 2) !== 1` or `($index % 2) < 1`
|
|
333
|
+
// can be simplified to `$even`.
|
|
334
|
+
recordSimplification(node, forLoop, 'preferEven', '$even');
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
else if (LOGICAL_OPERATORS.includes(node.operation)) {
|
|
338
|
+
// `$index % 2` can be used to test if `$index` is odd, but it
|
|
339
|
+
// results in a number, so we can only simplify it when it is
|
|
340
|
+
// being used as a truthy value. Because it's on the left-hand
|
|
341
|
+
// side of a logical binary expression, we can simplify it.
|
|
342
|
+
recordSimplification(node.left, forLoop, 'preferOdd', '$odd');
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
if (isIndexModTwo(node.right) &&
|
|
346
|
+
LOGICAL_OPERATORS.includes(node.operation)) {
|
|
347
|
+
// As we did with the left-hand side above, when `$index % 2`
|
|
348
|
+
// is used as a truthy value on the right-hand side
|
|
349
|
+
// of a logical binary expression, we can simplify it.
|
|
350
|
+
recordSimplification(node.right, forLoop, 'preferOdd', '$odd');
|
|
351
|
+
}
|
|
352
|
+
},
|
|
353
|
+
PrefixNot(node) {
|
|
354
|
+
const forLoop = forLoops.at(-1);
|
|
355
|
+
if (!forLoop) {
|
|
356
|
+
return;
|
|
357
|
+
}
|
|
358
|
+
if (isOdd(node.expression) || isIndexModTwo(node.expression)) {
|
|
359
|
+
// `!$odd` or `!($index % 2)` can be simplified to `$even`.
|
|
360
|
+
recordSimplification(node, forLoop, 'preferEven', '$even');
|
|
361
|
+
}
|
|
362
|
+
else if (isEven(node.expression)) {
|
|
363
|
+
// `!$even` can be simplified to `$odd`.
|
|
364
|
+
recordSimplification(node, forLoop, 'preferOdd', '$odd');
|
|
365
|
+
}
|
|
366
|
+
},
|
|
367
|
+
Conditional(node) {
|
|
368
|
+
const forLoop = forLoops.at(-1);
|
|
369
|
+
if (!forLoop) {
|
|
370
|
+
return;
|
|
371
|
+
}
|
|
372
|
+
// If the condition is `$index % 2`, then it's being
|
|
373
|
+
// used as a truthy value and we can simplify it.
|
|
374
|
+
if (isIndexModTwo(node.condition)) {
|
|
375
|
+
recordSimplification(node.condition, forLoop, 'preferOdd', '$odd');
|
|
376
|
+
}
|
|
377
|
+
},
|
|
378
|
+
IfBlockBranch(node) {
|
|
379
|
+
const forLoop = forLoops.at(-1);
|
|
380
|
+
if (!forLoop) {
|
|
381
|
+
return;
|
|
382
|
+
}
|
|
383
|
+
// If the expression is `$index % 2`, then it's being
|
|
384
|
+
// used as a truthy value and we can simplify it.
|
|
385
|
+
if (node.expression) {
|
|
386
|
+
let expression = node.expression;
|
|
387
|
+
if (expression instanceof bundled_angular_compiler_1.ASTWithSource) {
|
|
388
|
+
expression = expression.ast;
|
|
389
|
+
}
|
|
390
|
+
if (isIndexModTwo(expression)) {
|
|
391
|
+
recordSimplification(expression, forLoop, 'preferOdd', '$odd');
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
},
|
|
395
|
+
};
|
|
396
|
+
},
|
|
397
|
+
});
|
|
398
|
+
function getAllowedAliases(allowedAliases, variableName) {
|
|
399
|
+
if (allowedAliases && variableName in allowedAliases) {
|
|
400
|
+
return allowedAliases[variableName];
|
|
401
|
+
}
|
|
402
|
+
return undefined;
|
|
403
|
+
}
|
|
404
|
+
function getVariableRangeToRemove(problem, sourceCode, variableCount) {
|
|
405
|
+
let start = problem.variable.sourceSpan.start.offset;
|
|
406
|
+
let end = problem.variable.sourceSpan.end.offset;
|
|
407
|
+
if (variableCount === 1) {
|
|
408
|
+
// There's only one variable defined, so we
|
|
409
|
+
// want to remove the `let` keyword as well.
|
|
410
|
+
const letIndex = getStartOfPreviousToken('let', start, sourceCode);
|
|
411
|
+
if (letIndex !== undefined) {
|
|
412
|
+
// We also want to remove the preceding semicolon.
|
|
413
|
+
start = getStartOfPreviousToken(';', letIndex, sourceCode) ?? letIndex;
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
else if (problem.index === 0) {
|
|
417
|
+
// There are multiple variables, but we're removing
|
|
418
|
+
// the first one. We need to keep the `let` keyword, but
|
|
419
|
+
// remove the trailing comma and any whitespace after it.
|
|
420
|
+
const commaIndex = getStartOfNextToken(',', end, sourceCode);
|
|
421
|
+
if (commaIndex !== undefined) {
|
|
422
|
+
// The range to remove is end-exclusive, so we
|
|
423
|
+
// need to add one to remove the comma.
|
|
424
|
+
end = getIndexOfNextNonWhitespace(commaIndex + 1, sourceCode);
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
else {
|
|
428
|
+
// There is a variable before this one, so we
|
|
429
|
+
// need to remove the preceding comma as well.
|
|
430
|
+
start = getStartOfPreviousToken(',', start, sourceCode) ?? start;
|
|
431
|
+
}
|
|
432
|
+
return [start, end];
|
|
433
|
+
}
|
|
434
|
+
function getStartOfPreviousToken(tokenToFind, startIndex, sourceCode) {
|
|
435
|
+
const text = sourceCode.text;
|
|
436
|
+
for (let i = startIndex - tokenToFind.length; i >= 0; i--) {
|
|
437
|
+
if (text.slice(i, i + tokenToFind.length) === tokenToFind) {
|
|
438
|
+
return i;
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
return undefined;
|
|
442
|
+
}
|
|
443
|
+
function getStartOfNextToken(tokenToFind, startIndex, sourceCode) {
|
|
444
|
+
const text = sourceCode.text;
|
|
445
|
+
for (let i = startIndex; i < text.length; i++) {
|
|
446
|
+
if (text.slice(i, i + tokenToFind.length) === tokenToFind) {
|
|
447
|
+
return i;
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
return undefined;
|
|
451
|
+
}
|
|
452
|
+
function getIndexOfNextNonWhitespace(startIndex, sourceCode) {
|
|
453
|
+
const text = sourceCode.text;
|
|
454
|
+
let index = startIndex;
|
|
455
|
+
while (index < text.length) {
|
|
456
|
+
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
457
|
+
if (!/\s/.test(text.at(index))) {
|
|
458
|
+
return index;
|
|
459
|
+
}
|
|
460
|
+
index++;
|
|
461
|
+
}
|
|
462
|
+
return text.length;
|
|
463
|
+
}
|
|
464
|
+
function recordSimplification(node, forLoop, type, replacement) {
|
|
465
|
+
// Most of the time we won't find anything to simplify because
|
|
466
|
+
// we would have simplified everything on the previous passes,
|
|
467
|
+
// so we delay-create this to reduce memory allocations.
|
|
468
|
+
if (!forLoop.simplifications) {
|
|
469
|
+
forLoop.simplifications = {};
|
|
470
|
+
}
|
|
471
|
+
let nodes = forLoop.simplifications[type];
|
|
472
|
+
if (!nodes) {
|
|
473
|
+
nodes = [];
|
|
474
|
+
forLoop.simplifications[type] = nodes;
|
|
475
|
+
}
|
|
476
|
+
nodes.push({
|
|
477
|
+
range: [node.sourceSpan.start, node.sourceSpan.end],
|
|
478
|
+
replacement,
|
|
479
|
+
});
|
|
480
|
+
}
|
|
481
|
+
function isIndex(node) {
|
|
482
|
+
return isContextualVariable(node, '$index');
|
|
483
|
+
}
|
|
484
|
+
function isIndexPlusOne(node) {
|
|
485
|
+
if (node instanceof bundled_angular_compiler_1.Binary) {
|
|
486
|
+
if (node.operation === '+') {
|
|
487
|
+
if (isIndex(node.left)) {
|
|
488
|
+
return isOne(node.right);
|
|
489
|
+
}
|
|
490
|
+
else {
|
|
491
|
+
return isIndex(node.right) && isOne(node.left);
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
return false;
|
|
496
|
+
}
|
|
497
|
+
function isIndexModTwo(node) {
|
|
498
|
+
return (node instanceof bundled_angular_compiler_1.Binary &&
|
|
499
|
+
node.operation === '%' &&
|
|
500
|
+
isIndex(node.left) &&
|
|
501
|
+
isTwo(node.right));
|
|
502
|
+
}
|
|
503
|
+
function isCount(node) {
|
|
504
|
+
return isContextualVariable(node, '$count');
|
|
505
|
+
}
|
|
506
|
+
function isCountMinusOne(node) {
|
|
507
|
+
if (node instanceof bundled_angular_compiler_1.Binary) {
|
|
508
|
+
if (node.operation === '-') {
|
|
509
|
+
if (isCount(node.left)) {
|
|
510
|
+
return isOne(node.right);
|
|
511
|
+
}
|
|
512
|
+
else {
|
|
513
|
+
return isCount(node.right) && isOne(node.left);
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
return false;
|
|
518
|
+
}
|
|
519
|
+
function isEven(node) {
|
|
520
|
+
return isContextualVariable(node, '$even');
|
|
521
|
+
}
|
|
522
|
+
function isOdd(node) {
|
|
523
|
+
return isContextualVariable(node, '$odd');
|
|
524
|
+
}
|
|
525
|
+
function isContextualVariable(node, name) {
|
|
526
|
+
return (node instanceof bundled_angular_compiler_1.PropertyRead &&
|
|
527
|
+
node.name === name &&
|
|
528
|
+
// The contextual variable must be accessed implicitly.
|
|
529
|
+
// That is, `this.$index` is not a contextual variable.
|
|
530
|
+
// Note that `ThisReceiver` extends `ImplicitReceiver`, so we
|
|
531
|
+
// need to check that the receiver is exactly an `ImplicitReceiver`
|
|
532
|
+
// and not just an instance of `ImplicitReceiver`.
|
|
533
|
+
node.receiver.constructor === bundled_angular_compiler_1.ImplicitReceiver);
|
|
534
|
+
}
|
|
535
|
+
function isZero(node) {
|
|
536
|
+
return isLiteralNumber(node, 0);
|
|
537
|
+
}
|
|
538
|
+
function isOne(node) {
|
|
539
|
+
return isLiteralNumber(node, 1);
|
|
540
|
+
}
|
|
541
|
+
function isTwo(node) {
|
|
542
|
+
return isLiteralNumber(node, 2);
|
|
543
|
+
}
|
|
544
|
+
function isLiteralNumber(node, value) {
|
|
545
|
+
return node instanceof bundled_angular_compiler_1.LiteralPrimitive && node.value === value;
|
|
546
|
+
}
|
|
@@ -22,7 +22,7 @@ exports.default = (0, create_eslint_rule_1.createESLintRule)({
|
|
|
22
22
|
create(context) {
|
|
23
23
|
const parserServices = (0, utils_1.getTemplateParserServices)(context);
|
|
24
24
|
return {
|
|
25
|
-
'Element
|
|
25
|
+
'Element[name=img]'(element) {
|
|
26
26
|
const ngSrcAttribute = hasNgSrcAttribute(element);
|
|
27
27
|
const srcAttribute = hasNormalSrcAttribute(element);
|
|
28
28
|
if (!srcAttribute ||
|
|
@@ -27,7 +27,7 @@ exports.default = (0, create_eslint_rule_1.createESLintRule)({
|
|
|
27
27
|
const parserServices = (0, utils_1.getTemplateParserServices)(context);
|
|
28
28
|
const elementNamePattern = (0, to_pattern_1.toPattern)([...(0, get_dom_elements_1.getDomElements)()]);
|
|
29
29
|
return {
|
|
30
|
-
[`Element
|
|
30
|
+
[`Element[name=${elementNamePattern}] > TextAttribute[name='role']`](node) {
|
|
31
31
|
const { value: role, sourceSpan } = node;
|
|
32
32
|
const { attributes, inputs, name: element } = node.parent;
|
|
33
33
|
const props = [...attributes, ...inputs];
|
|
@@ -24,7 +24,7 @@ exports.default = (0, create_eslint_rule_1.createESLintRule)({
|
|
|
24
24
|
const parserServices = (0, utils_1.getTemplateParserServices)(context);
|
|
25
25
|
const domElementsPattern = (0, to_pattern_1.toPattern)([...(0, get_dom_elements_1.getDomElements)()].filter((domElement) => domElement !== 'th'));
|
|
26
26
|
return {
|
|
27
|
-
[`Element
|
|
27
|
+
[`Element[name=${domElementsPattern}] > :matches(BoundAttribute, TextAttribute)[name='scope']`]({ sourceSpan, }) {
|
|
28
28
|
const loc = parserServices.convertNodeSourceSpanToLoc(sourceSpan);
|
|
29
29
|
context.report({
|
|
30
30
|
loc,
|
package/dist/rules/valid-aria.js
CHANGED
|
@@ -28,7 +28,7 @@ exports.default = (0, create_eslint_rule_1.createESLintRule)({
|
|
|
28
28
|
const parserServices = (0, utils_1.getTemplateParserServices)(context);
|
|
29
29
|
const elementNamePattern = (0, to_pattern_1.toPattern)([...(0, get_dom_elements_1.getDomElements)()]);
|
|
30
30
|
return {
|
|
31
|
-
[`Element
|
|
31
|
+
[`Element[name=${elementNamePattern}] > :matches(BoundAttribute, TextAttribute)[name=/^aria-.+/]`](node) {
|
|
32
32
|
const { name: attribute, sourceSpan } = node;
|
|
33
33
|
const ariaPropertyDefinition = aria_query_1.aria.get(attribute);
|
|
34
34
|
const loc = parserServices.convertNodeSourceSpanToLoc(sourceSpan);
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"are-equivalent-asts.d.ts","sourceRoot":"","sources":["../../src/utils/are-equivalent-asts.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,GAAG,EAuBJ,MAAM,0CAA0C,CAAC;AAElD,wBAAgB,iBAAiB,CAAC,CAAC,EAAE,GAAG,EAAE,CAAC,EAAE,GAAG,GAAG,OAAO,CA4JzD"}
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.areEquivalentASTs = areEquivalentASTs;
|
|
4
|
+
const bundled_angular_compiler_1 = require("@angular-eslint/bundled-angular-compiler");
|
|
5
|
+
function areEquivalentASTs(a, b) {
|
|
6
|
+
// An `ImplicitReceiver` is equivalent to a `ThisReceiver` because
|
|
7
|
+
// `this.foo` and `foo` mean the same thing. A `ThisReceiver` extends
|
|
8
|
+
// `ImplicitReceiver` so before we check if the two ASTs are the same
|
|
9
|
+
// type, we can check if they are both some sort of `ImplicitReceiver`.
|
|
10
|
+
if (a instanceof bundled_angular_compiler_1.ImplicitReceiver) {
|
|
11
|
+
return b instanceof bundled_angular_compiler_1.ImplicitReceiver;
|
|
12
|
+
}
|
|
13
|
+
// Bail out if the two ASTs are not the same type.
|
|
14
|
+
if (a.constructor !== b.constructor) {
|
|
15
|
+
return false;
|
|
16
|
+
}
|
|
17
|
+
// Check reads and calls first, because
|
|
18
|
+
// they are probably the most common type.
|
|
19
|
+
if (a instanceof bundled_angular_compiler_1.PropertyRead && b instanceof bundled_angular_compiler_1.PropertyRead) {
|
|
20
|
+
return a.name === b.name && areEquivalentASTs(a.receiver, b.receiver);
|
|
21
|
+
}
|
|
22
|
+
if (a instanceof bundled_angular_compiler_1.SafePropertyRead && b instanceof bundled_angular_compiler_1.SafePropertyRead) {
|
|
23
|
+
return a.name === b.name && areEquivalentASTs(a.receiver, b.receiver);
|
|
24
|
+
}
|
|
25
|
+
if (a instanceof bundled_angular_compiler_1.Call && b instanceof bundled_angular_compiler_1.Call) {
|
|
26
|
+
return (areEquivalentASTArrays(a.args, b.args) &&
|
|
27
|
+
areEquivalentASTs(a.receiver, b.receiver));
|
|
28
|
+
}
|
|
29
|
+
if (a instanceof bundled_angular_compiler_1.SafeCall && b instanceof bundled_angular_compiler_1.SafeCall) {
|
|
30
|
+
return (areEquivalentASTArrays(a.args, b.args) &&
|
|
31
|
+
areEquivalentASTs(a.receiver, b.receiver));
|
|
32
|
+
}
|
|
33
|
+
if (a instanceof bundled_angular_compiler_1.KeyedRead && b instanceof bundled_angular_compiler_1.KeyedRead) {
|
|
34
|
+
return (areEquivalentASTs(a.key, b.key) &&
|
|
35
|
+
areEquivalentASTs(a.receiver, b.receiver));
|
|
36
|
+
}
|
|
37
|
+
if (a instanceof bundled_angular_compiler_1.SafeKeyedRead && b instanceof bundled_angular_compiler_1.SafeKeyedRead) {
|
|
38
|
+
return (areEquivalentASTs(a.key, b.key) &&
|
|
39
|
+
areEquivalentASTs(a.receiver, b.receiver));
|
|
40
|
+
}
|
|
41
|
+
if (a instanceof bundled_angular_compiler_1.NonNullAssert && b instanceof bundled_angular_compiler_1.NonNullAssert) {
|
|
42
|
+
return areEquivalentASTs(a.expression, b.expression);
|
|
43
|
+
}
|
|
44
|
+
// Expressions used as conditions can come next.
|
|
45
|
+
if (a instanceof bundled_angular_compiler_1.PrefixNot && b instanceof bundled_angular_compiler_1.PrefixNot) {
|
|
46
|
+
return areEquivalentASTs(a.expression, b.expression);
|
|
47
|
+
}
|
|
48
|
+
// Unary extends Binary, so we need to check `Unary`
|
|
49
|
+
// first, otherwise we will treat it as a `Binary`.
|
|
50
|
+
if (a instanceof bundled_angular_compiler_1.Unary && b instanceof bundled_angular_compiler_1.Unary) {
|
|
51
|
+
return a.operator === b.operator && areEquivalentASTs(a.expr, b.expr);
|
|
52
|
+
}
|
|
53
|
+
if (a instanceof bundled_angular_compiler_1.Binary && b instanceof bundled_angular_compiler_1.Binary) {
|
|
54
|
+
return (a.operation === b.operation &&
|
|
55
|
+
areEquivalentASTs(a.left, b.left) &&
|
|
56
|
+
areEquivalentASTs(a.right, b.right));
|
|
57
|
+
}
|
|
58
|
+
if (a instanceof bundled_angular_compiler_1.Conditional && b instanceof bundled_angular_compiler_1.Conditional) {
|
|
59
|
+
return (areEquivalentASTs(a.condition, b.condition) &&
|
|
60
|
+
areEquivalentASTs(a.trueExp, b.trueExp) &&
|
|
61
|
+
areEquivalentASTs(a.falseExp, b.falseExp));
|
|
62
|
+
}
|
|
63
|
+
// Literals can be checked next.
|
|
64
|
+
if (a instanceof bundled_angular_compiler_1.LiteralPrimitive && b instanceof bundled_angular_compiler_1.LiteralPrimitive) {
|
|
65
|
+
return a.value === b.value;
|
|
66
|
+
}
|
|
67
|
+
if (a instanceof bundled_angular_compiler_1.LiteralArray && b instanceof bundled_angular_compiler_1.LiteralArray) {
|
|
68
|
+
return areEquivalentASTArrays(a.expressions, b.expressions);
|
|
69
|
+
}
|
|
70
|
+
if (a instanceof bundled_angular_compiler_1.LiteralMap && b instanceof bundled_angular_compiler_1.LiteralMap) {
|
|
71
|
+
return (a.keys.length === b.keys.length &&
|
|
72
|
+
// Only check that the keys are equivalent. We don't need to check
|
|
73
|
+
// the `quoted` property because a quoted key with the same value as
|
|
74
|
+
// an unquoted key is the same key. Likewise, the `isShorthandInitialized`
|
|
75
|
+
// property doesn't affect the name of the key.
|
|
76
|
+
a.keys.every((aKey, index) => aKey.key === b.keys[index].key) &&
|
|
77
|
+
areEquivalentASTArrays(a.values, b.values));
|
|
78
|
+
}
|
|
79
|
+
// Pipes and interpolations are next.
|
|
80
|
+
if (a instanceof bundled_angular_compiler_1.BindingPipe && b instanceof bundled_angular_compiler_1.BindingPipe) {
|
|
81
|
+
return (a.name === b.name &&
|
|
82
|
+
areEquivalentASTs(a.exp, b.exp) &&
|
|
83
|
+
areEquivalentASTArrays(a.args, b.args));
|
|
84
|
+
}
|
|
85
|
+
if (a instanceof bundled_angular_compiler_1.Interpolation && b instanceof bundled_angular_compiler_1.Interpolation) {
|
|
86
|
+
return (a.strings.length === b.strings.length &&
|
|
87
|
+
a.strings.every((aString, index) => aString === b.strings[index]) &&
|
|
88
|
+
areEquivalentASTArrays(a.expressions, b.expressions));
|
|
89
|
+
}
|
|
90
|
+
// Miscellaneous things and writes can be checked next.
|
|
91
|
+
if (a instanceof bundled_angular_compiler_1.ASTWithSource && b instanceof bundled_angular_compiler_1.ASTWithSource) {
|
|
92
|
+
return areEquivalentASTs(a.ast, b.ast);
|
|
93
|
+
}
|
|
94
|
+
if (a instanceof bundled_angular_compiler_1.Chain && b instanceof bundled_angular_compiler_1.Chain) {
|
|
95
|
+
return areEquivalentASTArrays(a.expressions, b.expressions);
|
|
96
|
+
}
|
|
97
|
+
if (a instanceof bundled_angular_compiler_1.PropertyWrite && b instanceof bundled_angular_compiler_1.PropertyWrite) {
|
|
98
|
+
return (a.name === b.name &&
|
|
99
|
+
areEquivalentASTs(a.receiver, b.receiver) &&
|
|
100
|
+
areEquivalentASTs(a.value, b.value));
|
|
101
|
+
}
|
|
102
|
+
if (a instanceof bundled_angular_compiler_1.KeyedWrite && b instanceof bundled_angular_compiler_1.KeyedWrite) {
|
|
103
|
+
return (areEquivalentASTs(a.key, b.key) &&
|
|
104
|
+
areEquivalentASTs(a.receiver, b.receiver) &&
|
|
105
|
+
areEquivalentASTs(a.value, b.value));
|
|
106
|
+
}
|
|
107
|
+
if (a instanceof bundled_angular_compiler_1.TypeofExpression && b instanceof bundled_angular_compiler_1.TypeofExpression) {
|
|
108
|
+
return areEquivalentASTs(a.expression, b.expression);
|
|
109
|
+
}
|
|
110
|
+
return false;
|
|
111
|
+
}
|
|
112
|
+
function areEquivalentASTArrays(a, b) {
|
|
113
|
+
return (a.length === b.length &&
|
|
114
|
+
a.every((aElement, index) => areEquivalentASTs(aElement, b[index])));
|
|
115
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@angular-eslint/eslint-plugin-template",
|
|
3
|
-
"version": "19.2.2-alpha.
|
|
3
|
+
"version": "19.2.2-alpha.6",
|
|
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/
|
|
24
|
-
"@angular-eslint/
|
|
23
|
+
"@angular-eslint/bundled-angular-compiler": "19.2.2-alpha.6",
|
|
24
|
+
"@angular-eslint/utils": "19.2.2-alpha.6"
|
|
25
25
|
},
|
|
26
26
|
"devDependencies": {
|
|
27
27
|
"@types/aria-query": "5.0.4",
|
|
28
|
-
"@angular-eslint/template-parser": "19.2.2-alpha.
|
|
29
|
-
"@angular-eslint/test-utils": "19.2.2-alpha.
|
|
28
|
+
"@angular-eslint/template-parser": "19.2.2-alpha.6",
|
|
29
|
+
"@angular-eslint/test-utils": "19.2.2-alpha.6"
|
|
30
30
|
},
|
|
31
31
|
"peerDependencies": {
|
|
32
32
|
"@typescript-eslint/types": "^7.11.0 || ^8.0.0",
|