@angular-eslint/eslint-plugin-template 15.2.2-alpha.25 → 15.2.2-alpha.26
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/dist/configs/README.md +10 -0
- package/dist/configs/accessibility.json +17 -0
- package/dist/configs/all.json +10 -10
- package/dist/configs/recommended.json +2 -1
- package/dist/index.js +95 -1
- package/dist/processors.js +210 -0
- package/dist/rules/alt-text.js +131 -0
- package/dist/rules/attributes-order.js +233 -0
- package/dist/rules/banana-in-box.js +47 -0
- package/dist/rules/button-has-type.js +76 -0
- package/dist/rules/click-events-have-key-events.js +57 -0
- package/dist/rules/conditional-complexity.js +107 -0
- package/dist/rules/cyclomatic-complexity.js +50 -0
- package/dist/rules/elements-content.js +70 -0
- package/dist/rules/eqeqeq.js +99 -0
- package/dist/rules/i18n.js +368 -0
- package/dist/rules/interactive-supports-focus.js +64 -0
- package/dist/rules/label-has-associated-control.js +103 -0
- package/dist/rules/mouse-events-have-key-events.js +40 -0
- package/dist/rules/no-any.js +59 -0
- package/dist/rules/no-autofocus.js +41 -0
- package/dist/rules/no-call-expression.js +68 -0
- package/dist/rules/no-distracting-elements.js +36 -0
- package/dist/rules/no-duplicate-attributes.js +97 -0
- package/dist/rules/no-inline-styles.js +104 -0
- package/dist/rules/no-interpolation-in-attributes.js +36 -0
- package/dist/rules/no-negated-async.js +60 -0
- package/dist/rules/no-positive-tabindex.js +43 -0
- package/dist/rules/role-has-required-aria.js +73 -0
- package/dist/rules/table-scope.js +41 -0
- package/dist/rules/use-track-by-function.js +56 -0
- package/dist/rules/valid-aria.js +126 -0
- package/dist/utils/attributes-comparator.js +19 -0
- package/dist/utils/constants.js +11 -0
- package/dist/utils/create-eslint-rule.js +26 -0
- package/dist/utils/get-attribute-value.js +39 -0
- package/dist/utils/get-dom-elements.js +9 -0
- package/dist/utils/get-nearest-node-from.js +17 -0
- package/dist/utils/get-original-attribute-name.js +29 -0
- package/dist/utils/is-child-node-of.js +12 -0
- package/dist/utils/is-content-editable.js +14 -0
- package/dist/utils/is-disabled-element.js +20 -0
- package/dist/utils/is-hidden-from-screen-reader.js +97 -0
- package/dist/utils/is-interactive-element/get-interactive-element-ax-object-schemas.js +27 -0
- package/dist/utils/is-interactive-element/get-interactive-element-role-schemas.js +38 -0
- package/dist/utils/is-interactive-element/get-non-interactive-element-role-schemas.js +44 -0
- package/dist/utils/is-interactive-element/index.js +45 -0
- package/dist/utils/is-presentation-role.js +14 -0
- package/dist/utils/is-semantic-role-element.js +36 -0
- package/dist/utils/to-pattern.js +7 -0
- package/package.json +6 -6
- package/dist/configs/base.json +0 -4
- package/dist/eslint-plugin-template/src/index.d.ts +0 -133
- package/dist/eslint-plugin-template/src/processors.d.ts +0 -37
- package/dist/eslint-plugin-template/src/rules/accessibility-alt-text.d.ts +0 -4
- package/dist/eslint-plugin-template/src/rules/accessibility-elements-content.d.ts +0 -9
- package/dist/eslint-plugin-template/src/rules/accessibility-interactive-supports-focus.d.ts +0 -4
- package/dist/eslint-plugin-template/src/rules/accessibility-label-for.d.ts +0 -11
- package/dist/eslint-plugin-template/src/rules/accessibility-label-has-associated-control.d.ts +0 -14
- package/dist/eslint-plugin-template/src/rules/accessibility-role-has-required-aria.d.ts +0 -4
- package/dist/eslint-plugin-template/src/rules/accessibility-table-scope.d.ts +0 -4
- package/dist/eslint-plugin-template/src/rules/accessibility-valid-aria.d.ts +0 -4
- package/dist/eslint-plugin-template/src/rules/attributes-order.d.ts +0 -18
- package/dist/eslint-plugin-template/src/rules/banana-in-box.d.ts +0 -4
- package/dist/eslint-plugin-template/src/rules/button-has-type.d.ts +0 -5
- package/dist/eslint-plugin-template/src/rules/click-events-have-key-events.d.ts +0 -4
- package/dist/eslint-plugin-template/src/rules/conditional-complexity.d.ts +0 -7
- package/dist/eslint-plugin-template/src/rules/cyclomatic-complexity.d.ts +0 -7
- package/dist/eslint-plugin-template/src/rules/eqeqeq.d.ts +0 -8
- package/dist/eslint-plugin-template/src/rules/i18n.d.ts +0 -18
- package/dist/eslint-plugin-template/src/rules/mouse-events-have-key-events.d.ts +0 -5
- package/dist/eslint-plugin-template/src/rules/no-any.d.ts +0 -4
- package/dist/eslint-plugin-template/src/rules/no-autofocus.d.ts +0 -4
- package/dist/eslint-plugin-template/src/rules/no-call-expression.d.ts +0 -9
- package/dist/eslint-plugin-template/src/rules/no-distracting-elements.d.ts +0 -4
- package/dist/eslint-plugin-template/src/rules/no-duplicate-attributes.d.ts +0 -10
- package/dist/eslint-plugin-template/src/rules/no-inline-styles.d.ts +0 -10
- package/dist/eslint-plugin-template/src/rules/no-interpolation-in-attributes.d.ts +0 -4
- package/dist/eslint-plugin-template/src/rules/no-negated-async.d.ts +0 -7
- package/dist/eslint-plugin-template/src/rules/no-positive-tabindex.d.ts +0 -4
- package/dist/eslint-plugin-template/src/rules/use-track-by-function.d.ts +0 -4
- package/dist/eslint-plugin-template/src/utils/attributes-comparator.d.ts +0 -3
- package/dist/eslint-plugin-template/src/utils/constants.d.ts +0 -8
- package/dist/eslint-plugin-template/src/utils/create-eslint-rule.d.ts +0 -2
- package/dist/eslint-plugin-template/src/utils/get-attribute-value.d.ts +0 -12
- package/dist/eslint-plugin-template/src/utils/get-dom-elements.d.ts +0 -1
- package/dist/eslint-plugin-template/src/utils/get-nearest-node-from.d.ts +0 -6
- package/dist/eslint-plugin-template/src/utils/get-original-attribute-name.d.ts +0 -13
- package/dist/eslint-plugin-template/src/utils/is-child-node-of.d.ts +0 -2
- package/dist/eslint-plugin-template/src/utils/is-content-editable.d.ts +0 -2
- package/dist/eslint-plugin-template/src/utils/is-disabled-element.d.ts +0 -2
- package/dist/eslint-plugin-template/src/utils/is-hidden-from-screen-reader.d.ts +0 -9
- package/dist/eslint-plugin-template/src/utils/is-interactive-element/get-interactive-element-ax-object-schemas.d.ts +0 -9
- package/dist/eslint-plugin-template/src/utils/is-interactive-element/get-interactive-element-role-schemas.d.ts +0 -2
- package/dist/eslint-plugin-template/src/utils/is-interactive-element/get-non-interactive-element-role-schemas.d.ts +0 -3
- package/dist/eslint-plugin-template/src/utils/is-interactive-element/index.d.ts +0 -9
- package/dist/eslint-plugin-template/src/utils/is-presentation-role.d.ts +0 -2
- package/dist/eslint-plugin-template/src/utils/is-semantic-role-element.d.ts +0 -2
- package/dist/eslint-plugin-template/src/utils/to-pattern.d.ts +0 -1
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.RULE_NAME = void 0;
|
|
4
|
+
const utils_1 = require("@angular-eslint/utils");
|
|
5
|
+
const create_eslint_rule_1 = require("../utils/create-eslint-rule");
|
|
6
|
+
const get_attribute_value_1 = require("../utils/get-attribute-value");
|
|
7
|
+
exports.RULE_NAME = 'alt-text';
|
|
8
|
+
exports.default = (0, create_eslint_rule_1.createESLintRule)({
|
|
9
|
+
name: exports.RULE_NAME,
|
|
10
|
+
meta: {
|
|
11
|
+
type: 'suggestion',
|
|
12
|
+
docs: {
|
|
13
|
+
description: '[Accessibility] Enforces alternate text for elements which require the alt, aria-label, aria-labelledby attributes.',
|
|
14
|
+
recommended: false,
|
|
15
|
+
},
|
|
16
|
+
schema: [],
|
|
17
|
+
messages: {
|
|
18
|
+
altText: '<{{element}}/> element must have a text alternative.',
|
|
19
|
+
},
|
|
20
|
+
},
|
|
21
|
+
defaultOptions: [],
|
|
22
|
+
create(context) {
|
|
23
|
+
const parserServices = (0, utils_1.getTemplateParserServices)(context);
|
|
24
|
+
return {
|
|
25
|
+
'Element$1[name=/^(img|area|object|input)$/]'(node) {
|
|
26
|
+
const isValid = isValidNode(node);
|
|
27
|
+
if (!isValid) {
|
|
28
|
+
const loc = parserServices.convertElementSourceSpanToLoc(context, node);
|
|
29
|
+
context.report({
|
|
30
|
+
loc,
|
|
31
|
+
messageId: 'altText',
|
|
32
|
+
data: {
|
|
33
|
+
element: node.name,
|
|
34
|
+
},
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
},
|
|
38
|
+
};
|
|
39
|
+
},
|
|
40
|
+
});
|
|
41
|
+
function isValidNode(node) {
|
|
42
|
+
if (node.name === 'img') {
|
|
43
|
+
return isValidImgNode(node);
|
|
44
|
+
}
|
|
45
|
+
else if (node.name === 'object') {
|
|
46
|
+
return isValidObjectNode(node);
|
|
47
|
+
}
|
|
48
|
+
else if (node.name === 'area') {
|
|
49
|
+
return isValidAreaNode(node);
|
|
50
|
+
}
|
|
51
|
+
else {
|
|
52
|
+
return isValidInputNode(node);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* In this case, we check that the `<img>` element has an `alt` attribute or `attr.alt` binding.
|
|
57
|
+
*/
|
|
58
|
+
function isValidImgNode(node) {
|
|
59
|
+
return (node.attributes.some(({ name }) => isAlt(name)) ||
|
|
60
|
+
node.inputs.some(({ name }) => isAlt(name)));
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* In this case, we check that the `<object>` element has a `title` or `aria-label` attribute.
|
|
64
|
+
* Otherwise, we check for the presence of `attr.title` or `attr.aria-label` bindings.
|
|
65
|
+
*/
|
|
66
|
+
function isValidObjectNode(node) {
|
|
67
|
+
let hasTitleAttribute = false, hasAriaLabelAttribute = false;
|
|
68
|
+
for (const attribute of node.attributes) {
|
|
69
|
+
hasTitleAttribute = attribute.name === 'title';
|
|
70
|
+
hasAriaLabelAttribute = isAriaLabel(attribute.name);
|
|
71
|
+
}
|
|
72
|
+
// Note that we return "early" before looping through `element.inputs`.
|
|
73
|
+
// Because if the element has an attribute, then we don't need to iterate
|
|
74
|
+
// over the inputs unnecessarily.
|
|
75
|
+
if (hasTitleAttribute || hasAriaLabelAttribute) {
|
|
76
|
+
return true;
|
|
77
|
+
}
|
|
78
|
+
let hasTitleBinding = false, hasAriaLabelBinding = false;
|
|
79
|
+
for (const input of node.inputs) {
|
|
80
|
+
hasTitleBinding = input.name === 'title';
|
|
81
|
+
hasAriaLabelBinding = isAriaLabel(input.name);
|
|
82
|
+
}
|
|
83
|
+
if (hasTitleBinding || hasAriaLabelBinding) {
|
|
84
|
+
return true;
|
|
85
|
+
}
|
|
86
|
+
return (node.children.length > 0 &&
|
|
87
|
+
!!node.children[0].value);
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* In this case, we check that the `<area>` element has an `alt` or `aria-label` attribute.
|
|
91
|
+
* Otherwise, we check for the presence of `attr.alt` or `attr.aria-label` bindings.
|
|
92
|
+
*/
|
|
93
|
+
function isValidAreaNode(node) {
|
|
94
|
+
let hasAltAttribute = false, hasAriaLabelAttribute = false;
|
|
95
|
+
for (const attribute of node.attributes) {
|
|
96
|
+
hasAltAttribute = isAlt(attribute.name);
|
|
97
|
+
hasAriaLabelAttribute = isAriaLabel(attribute.name);
|
|
98
|
+
}
|
|
99
|
+
// Note that we return "early" before looping through `element.inputs`.
|
|
100
|
+
// Because if the element has an attribute, then we don't need to iterate
|
|
101
|
+
// over the inputs unnecessarily.
|
|
102
|
+
if (hasAltAttribute || hasAriaLabelAttribute) {
|
|
103
|
+
return true;
|
|
104
|
+
}
|
|
105
|
+
let hasAltBinding = false, hasAriaLabelBinding = false;
|
|
106
|
+
for (const input of node.inputs) {
|
|
107
|
+
hasAltBinding = isAlt(input.name);
|
|
108
|
+
hasAriaLabelBinding = isAriaLabel(input.name);
|
|
109
|
+
}
|
|
110
|
+
return hasAltBinding || hasAriaLabelBinding;
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* In this case, we check that the `<input>` element has an `alt` or `aria-label` attribute.
|
|
114
|
+
* Otherwise, we check for the presence of `attr.alt` or `attr.aria-label` bindings.
|
|
115
|
+
*/
|
|
116
|
+
function isValidInputNode(node) {
|
|
117
|
+
const type = (0, get_attribute_value_1.getAttributeValue)(node, 'type');
|
|
118
|
+
// We are only interested in the `<input type="image">` elements.
|
|
119
|
+
if (type !== 'image') {
|
|
120
|
+
return true;
|
|
121
|
+
}
|
|
122
|
+
else {
|
|
123
|
+
return isValidAreaNode(node);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
function isAriaLabel(name) {
|
|
127
|
+
return name === 'aria-label' || name === 'aria-labelledby';
|
|
128
|
+
}
|
|
129
|
+
function isAlt(name) {
|
|
130
|
+
return name === 'alt';
|
|
131
|
+
}
|
|
@@ -0,0 +1,233 @@
|
|
|
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
|
+
exports.RULE_NAME = 'attributes-order';
|
|
8
|
+
const DEFAULT_ORDER = [
|
|
9
|
+
"STRUCTURAL_DIRECTIVE" /* OrderType.StructuralDirective */,
|
|
10
|
+
"TEMPLATE_REFERENCE" /* OrderType.TemplateReferenceVariable */,
|
|
11
|
+
"ATTRIBUTE_BINDING" /* OrderType.AttributeBinding */,
|
|
12
|
+
"INPUT_BINDING" /* OrderType.InputBinding */,
|
|
13
|
+
"TWO_WAY_BINDING" /* OrderType.TwoWayBinding */,
|
|
14
|
+
"OUTPUT_BINDING" /* OrderType.OutputBinding */,
|
|
15
|
+
];
|
|
16
|
+
const DEFAULT_OPTIONS = {
|
|
17
|
+
alphabetical: false,
|
|
18
|
+
order: [...DEFAULT_ORDER],
|
|
19
|
+
};
|
|
20
|
+
exports.default = (0, create_eslint_rule_1.createESLintRule)({
|
|
21
|
+
name: exports.RULE_NAME,
|
|
22
|
+
meta: {
|
|
23
|
+
type: 'layout',
|
|
24
|
+
docs: {
|
|
25
|
+
description: 'Ensures that HTML attributes and Angular bindings are sorted based on an expected order',
|
|
26
|
+
recommended: false,
|
|
27
|
+
},
|
|
28
|
+
fixable: 'code',
|
|
29
|
+
schema: [
|
|
30
|
+
{
|
|
31
|
+
type: 'object',
|
|
32
|
+
properties: {
|
|
33
|
+
alphabetical: {
|
|
34
|
+
type: 'boolean',
|
|
35
|
+
default: DEFAULT_OPTIONS.alphabetical,
|
|
36
|
+
},
|
|
37
|
+
order: {
|
|
38
|
+
type: 'array',
|
|
39
|
+
items: {
|
|
40
|
+
enum: DEFAULT_OPTIONS.order,
|
|
41
|
+
},
|
|
42
|
+
default: DEFAULT_OPTIONS.order,
|
|
43
|
+
minItems: DEFAULT_OPTIONS.order.length,
|
|
44
|
+
uniqueItems: true,
|
|
45
|
+
},
|
|
46
|
+
},
|
|
47
|
+
additionalProperties: false,
|
|
48
|
+
},
|
|
49
|
+
],
|
|
50
|
+
messages: {
|
|
51
|
+
attributesOrder: `The element's attributes/bindings did not match the expected order: expected {{expected}} instead of {{actual}}`,
|
|
52
|
+
},
|
|
53
|
+
},
|
|
54
|
+
defaultOptions: [DEFAULT_OPTIONS],
|
|
55
|
+
create(context, [{ alphabetical, order }]) {
|
|
56
|
+
const parserServices = (0, utils_1.getTemplateParserServices)(context);
|
|
57
|
+
function getLocation(attr) {
|
|
58
|
+
const loc = parserServices.convertNodeSourceSpanToLoc(attr.sourceSpan);
|
|
59
|
+
switch (attr.orderType) {
|
|
60
|
+
case "STRUCTURAL_DIRECTIVE" /* OrderType.StructuralDirective */:
|
|
61
|
+
return {
|
|
62
|
+
start: {
|
|
63
|
+
line: loc.start.line,
|
|
64
|
+
column: loc.start.column - 1,
|
|
65
|
+
},
|
|
66
|
+
end: {
|
|
67
|
+
line: loc.end.line,
|
|
68
|
+
column: loc.end.column + 1,
|
|
69
|
+
},
|
|
70
|
+
};
|
|
71
|
+
default:
|
|
72
|
+
return loc;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
return {
|
|
76
|
+
Element$1(node) {
|
|
77
|
+
var _a;
|
|
78
|
+
const { attributes, inputs, outputs, references } = node;
|
|
79
|
+
const { extractedBananaBoxes, extractedInputs, extractedOutputs } = normalizeInputsOutputs(inputs.map(toInputBindingOrderType), outputs.map(toOutputBindingOrderType));
|
|
80
|
+
const allAttributes = [
|
|
81
|
+
...extractTemplateAttrs(node),
|
|
82
|
+
...attributes.map(toAttributeBindingOrderType),
|
|
83
|
+
...references.map(toTemplateReferenceVariableOrderType),
|
|
84
|
+
...extractedBananaBoxes,
|
|
85
|
+
...extractedInputs,
|
|
86
|
+
...extractedOutputs,
|
|
87
|
+
];
|
|
88
|
+
if (allAttributes.length < 2) {
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
const sortedAttributes = [...allAttributes].sort(byLocation);
|
|
92
|
+
const expectedAttributes = [...allAttributes].sort(byOrder(order, alphabetical));
|
|
93
|
+
let errorRange;
|
|
94
|
+
for (let i = 0; i < sortedAttributes.length; i++) {
|
|
95
|
+
if (sortedAttributes[i] !== expectedAttributes[i]) {
|
|
96
|
+
errorRange = [(_a = errorRange === null || errorRange === void 0 ? void 0 : errorRange[0]) !== null && _a !== void 0 ? _a : i, i];
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
if (errorRange) {
|
|
100
|
+
const [startIndex, endIndex] = errorRange;
|
|
101
|
+
const sourceCode = context.getSourceCode();
|
|
102
|
+
const { start } = getLocation(sortedAttributes[startIndex]);
|
|
103
|
+
const { end } = getLocation(sortedAttributes[endIndex]);
|
|
104
|
+
const loc = { start, end };
|
|
105
|
+
const range = [
|
|
106
|
+
getStartPos(sortedAttributes[startIndex]),
|
|
107
|
+
getEndPos(sortedAttributes[endIndex]),
|
|
108
|
+
];
|
|
109
|
+
let replacementText = '';
|
|
110
|
+
let lastPos = range[0];
|
|
111
|
+
for (let i = startIndex; i <= endIndex; i++) {
|
|
112
|
+
const oldAttr = sortedAttributes[i];
|
|
113
|
+
const oldStart = getStartPos(oldAttr);
|
|
114
|
+
const oldEnd = getEndPos(oldAttr);
|
|
115
|
+
const newAttr = expectedAttributes[i];
|
|
116
|
+
const newStart = getStartPos(newAttr);
|
|
117
|
+
const newEnd = getEndPos(newAttr);
|
|
118
|
+
replacementText += sourceCode.text.slice(lastPos, oldStart);
|
|
119
|
+
replacementText += sourceCode.text.slice(newStart, newEnd);
|
|
120
|
+
lastPos = oldEnd;
|
|
121
|
+
}
|
|
122
|
+
context.report({
|
|
123
|
+
loc,
|
|
124
|
+
messageId: 'attributesOrder',
|
|
125
|
+
data: {
|
|
126
|
+
expected: expectedAttributes
|
|
127
|
+
.slice(startIndex, endIndex + 1)
|
|
128
|
+
.map((a) => `\`${getMessageName(a)}\``)
|
|
129
|
+
.join(', '),
|
|
130
|
+
actual: sortedAttributes
|
|
131
|
+
.slice(startIndex, endIndex + 1)
|
|
132
|
+
.map((a) => `\`${getMessageName(a)}\``)
|
|
133
|
+
.join(', '),
|
|
134
|
+
},
|
|
135
|
+
fix: (fixer) => fixer.replaceTextRange(range, replacementText),
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
},
|
|
139
|
+
};
|
|
140
|
+
},
|
|
141
|
+
});
|
|
142
|
+
function byLocation(one, other) {
|
|
143
|
+
return one.sourceSpan.start.line === other.sourceSpan.start.line
|
|
144
|
+
? one.sourceSpan.start.col - other.sourceSpan.start.col
|
|
145
|
+
: one.sourceSpan.start.line - other.sourceSpan.start.line;
|
|
146
|
+
}
|
|
147
|
+
function byOrder(order, alphabetical) {
|
|
148
|
+
return function (one, other) {
|
|
149
|
+
const orderComparison = getOrderIndex(one, order) - getOrderIndex(other, order);
|
|
150
|
+
if (alphabetical && orderComparison === 0) {
|
|
151
|
+
return one.name > other.name ? 1 : -1;
|
|
152
|
+
}
|
|
153
|
+
return orderComparison;
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
function getOrderIndex(attr, order) {
|
|
157
|
+
return order.indexOf(attr.orderType);
|
|
158
|
+
}
|
|
159
|
+
function toAttributeBindingOrderType(attribute) {
|
|
160
|
+
return Object.assign(Object.assign({}, attribute), { orderType: "ATTRIBUTE_BINDING" /* OrderType.AttributeBinding */ });
|
|
161
|
+
}
|
|
162
|
+
function toInputBindingOrderType(input) {
|
|
163
|
+
return Object.assign(Object.assign({}, input), { orderType: "INPUT_BINDING" /* OrderType.InputBinding */ });
|
|
164
|
+
}
|
|
165
|
+
function toStructuralDirectiveOrderType(attributeOrInput) {
|
|
166
|
+
return Object.assign(Object.assign({}, attributeOrInput), { orderType: "STRUCTURAL_DIRECTIVE" /* OrderType.StructuralDirective */ });
|
|
167
|
+
}
|
|
168
|
+
function toOutputBindingOrderType(output) {
|
|
169
|
+
return Object.assign(Object.assign({}, output), { orderType: "OUTPUT_BINDING" /* OrderType.OutputBinding */ });
|
|
170
|
+
}
|
|
171
|
+
function toTwoWayBindingOrderType(output) {
|
|
172
|
+
return Object.assign(Object.assign({}, output), { orderType: "TWO_WAY_BINDING" /* OrderType.TwoWayBinding */ });
|
|
173
|
+
}
|
|
174
|
+
function toTemplateReferenceVariableOrderType(reference) {
|
|
175
|
+
return Object.assign(Object.assign({}, reference), { orderType: "TEMPLATE_REFERENCE" /* OrderType.TemplateReferenceVariable */ });
|
|
176
|
+
}
|
|
177
|
+
function extractTemplateAttrs(node) {
|
|
178
|
+
return isTmplAstTemplate(node.parent)
|
|
179
|
+
? node.parent.templateAttrs.map(toStructuralDirectiveOrderType)
|
|
180
|
+
: [];
|
|
181
|
+
}
|
|
182
|
+
function normalizeInputsOutputs(inputs, outputs) {
|
|
183
|
+
const extractedInputs = inputs
|
|
184
|
+
.filter((input) => !outputs.some((output) => isOnSameLocation(input, output)))
|
|
185
|
+
.map(toInputBindingOrderType);
|
|
186
|
+
const { extractedBananaBoxes, extractedOutputs } = outputs.reduce(({ extractedBananaBoxes, extractedOutputs }, output) => {
|
|
187
|
+
const boundInput = inputs.find((input) => isOnSameLocation(input, output));
|
|
188
|
+
return {
|
|
189
|
+
extractedBananaBoxes: extractedBananaBoxes.concat(boundInput ? toTwoWayBindingOrderType(boundInput) : []),
|
|
190
|
+
extractedOutputs: extractedOutputs.concat(boundInput ? [] : toOutputBindingOrderType(output)),
|
|
191
|
+
};
|
|
192
|
+
}, { extractedBananaBoxes: [], extractedOutputs: [] });
|
|
193
|
+
return { extractedBananaBoxes, extractedInputs, extractedOutputs };
|
|
194
|
+
}
|
|
195
|
+
function isTmplAstTemplate(node) {
|
|
196
|
+
return node instanceof bundled_angular_compiler_1.TmplAstTemplate;
|
|
197
|
+
}
|
|
198
|
+
function isOnSameLocation(input, output) {
|
|
199
|
+
return (input.sourceSpan.start === output.sourceSpan.start &&
|
|
200
|
+
input.sourceSpan.end === output.sourceSpan.end);
|
|
201
|
+
}
|
|
202
|
+
function getMessageName(expected) {
|
|
203
|
+
switch (expected.orderType) {
|
|
204
|
+
case "STRUCTURAL_DIRECTIVE" /* OrderType.StructuralDirective */:
|
|
205
|
+
return `*${expected.name}`;
|
|
206
|
+
case "TEMPLATE_REFERENCE" /* OrderType.TemplateReferenceVariable */:
|
|
207
|
+
return `#${expected.name}`;
|
|
208
|
+
case "INPUT_BINDING" /* OrderType.InputBinding */:
|
|
209
|
+
return `[${expected.name}]`;
|
|
210
|
+
case "OUTPUT_BINDING" /* OrderType.OutputBinding */:
|
|
211
|
+
return `(${expected.name})`;
|
|
212
|
+
case "TWO_WAY_BINDING" /* OrderType.TwoWayBinding */:
|
|
213
|
+
return `[(${expected.name})]`;
|
|
214
|
+
default:
|
|
215
|
+
return expected.name;
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
function getStartPos(expected) {
|
|
219
|
+
switch (expected.orderType) {
|
|
220
|
+
case "STRUCTURAL_DIRECTIVE" /* OrderType.StructuralDirective */:
|
|
221
|
+
return expected.sourceSpan.start.offset - 1;
|
|
222
|
+
default:
|
|
223
|
+
return expected.sourceSpan.start.offset;
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
function getEndPos(expected) {
|
|
227
|
+
switch (expected.orderType) {
|
|
228
|
+
case "STRUCTURAL_DIRECTIVE" /* OrderType.StructuralDirective */:
|
|
229
|
+
return expected.sourceSpan.end.offset + 1;
|
|
230
|
+
default:
|
|
231
|
+
return expected.sourceSpan.end.offset;
|
|
232
|
+
}
|
|
233
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.RULE_NAME = void 0;
|
|
4
|
+
const utils_1 = require("@angular-eslint/utils");
|
|
5
|
+
const create_eslint_rule_1 = require("../utils/create-eslint-rule");
|
|
6
|
+
exports.RULE_NAME = 'banana-in-box';
|
|
7
|
+
const INVALID_PATTERN = /\[(.*)\]/;
|
|
8
|
+
const VALID_CLOSE_BOX = ')]';
|
|
9
|
+
const VALID_OPEN_BOX = '[(';
|
|
10
|
+
exports.default = (0, create_eslint_rule_1.createESLintRule)({
|
|
11
|
+
name: exports.RULE_NAME,
|
|
12
|
+
meta: {
|
|
13
|
+
type: 'suggestion',
|
|
14
|
+
docs: {
|
|
15
|
+
description: 'Ensures that the two-way data binding syntax is correct',
|
|
16
|
+
recommended: 'error',
|
|
17
|
+
},
|
|
18
|
+
fixable: 'code',
|
|
19
|
+
schema: [],
|
|
20
|
+
messages: {
|
|
21
|
+
bananaInBox: 'Invalid binding syntax. Use [(expr)] instead',
|
|
22
|
+
},
|
|
23
|
+
},
|
|
24
|
+
defaultOptions: [],
|
|
25
|
+
create(context) {
|
|
26
|
+
const parserServices = (0, utils_1.getTemplateParserServices)(context);
|
|
27
|
+
const sourceCode = context.getSourceCode();
|
|
28
|
+
return {
|
|
29
|
+
BoundEvent({ name, sourceSpan }) {
|
|
30
|
+
const matches = name.match(INVALID_PATTERN);
|
|
31
|
+
if (!matches)
|
|
32
|
+
return;
|
|
33
|
+
const loc = parserServices.convertNodeSourceSpanToLoc(sourceSpan);
|
|
34
|
+
context.report({
|
|
35
|
+
messageId: 'bananaInBox',
|
|
36
|
+
loc,
|
|
37
|
+
fix: (fixer) => {
|
|
38
|
+
const [, textInTheBox] = matches;
|
|
39
|
+
const textToReplace = `${VALID_OPEN_BOX}${textInTheBox}${VALID_CLOSE_BOX}`;
|
|
40
|
+
const startIndex = sourceCode.getIndexFromLoc(loc.start);
|
|
41
|
+
return fixer.replaceTextRange([startIndex, startIndex + name.length + 2], textToReplace);
|
|
42
|
+
},
|
|
43
|
+
});
|
|
44
|
+
},
|
|
45
|
+
};
|
|
46
|
+
},
|
|
47
|
+
});
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.INVALID_TYPE_DATA_KEY = 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
|
+
exports.RULE_NAME = 'button-has-type';
|
|
8
|
+
exports.INVALID_TYPE_DATA_KEY = 'type';
|
|
9
|
+
// https://www.w3.org/TR/html401/interact/forms.html#adef-type-BUTTON
|
|
10
|
+
const VALID_BUTTON_TYPES = ['button', 'submit', 'reset'];
|
|
11
|
+
const TYPE_ATTRIBUTE_NAME = 'type';
|
|
12
|
+
exports.default = (0, create_eslint_rule_1.createESLintRule)({
|
|
13
|
+
name: exports.RULE_NAME,
|
|
14
|
+
meta: {
|
|
15
|
+
type: 'suggestion',
|
|
16
|
+
docs: {
|
|
17
|
+
description: 'Ensures that a button has a valid type specified',
|
|
18
|
+
recommended: false,
|
|
19
|
+
},
|
|
20
|
+
schema: [],
|
|
21
|
+
messages: {
|
|
22
|
+
missingType: 'Type for <button> is missing',
|
|
23
|
+
invalidType: `"{{${exports.INVALID_TYPE_DATA_KEY}}}" can not be used as a type for <button>`,
|
|
24
|
+
},
|
|
25
|
+
},
|
|
26
|
+
defaultOptions: [],
|
|
27
|
+
create(context) {
|
|
28
|
+
const parserServices = (0, utils_1.getTemplateParserServices)(context);
|
|
29
|
+
return {
|
|
30
|
+
[`Element$1[name=button]`](element) {
|
|
31
|
+
if (!isTypeAttributePresentInElement(element)) {
|
|
32
|
+
context.report({
|
|
33
|
+
loc: parserServices.convertNodeSourceSpanToLoc(element.sourceSpan),
|
|
34
|
+
messageId: 'missingType',
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
const invalidTypeInfo = getInvalidButtonTypeIfPresent(element);
|
|
38
|
+
if (invalidTypeInfo != null) {
|
|
39
|
+
context.report({
|
|
40
|
+
loc: parserServices.convertNodeSourceSpanToLoc(invalidTypeInfo.sourceSpan),
|
|
41
|
+
messageId: 'invalidType',
|
|
42
|
+
data: {
|
|
43
|
+
[exports.INVALID_TYPE_DATA_KEY]: invalidTypeInfo.value,
|
|
44
|
+
},
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
},
|
|
48
|
+
};
|
|
49
|
+
},
|
|
50
|
+
});
|
|
51
|
+
function isTypeAttributePresentInElement({ inputs, attributes, }) {
|
|
52
|
+
return [...inputs, ...attributes].some(({ name }) => name === TYPE_ATTRIBUTE_NAME);
|
|
53
|
+
}
|
|
54
|
+
function getInvalidButtonTypeIfPresent(element) {
|
|
55
|
+
const invalidTextAttribute = element.attributes.find(({ name, value }) => name === TYPE_ATTRIBUTE_NAME && !VALID_BUTTON_TYPES.includes(value));
|
|
56
|
+
if (invalidTextAttribute) {
|
|
57
|
+
return {
|
|
58
|
+
sourceSpan: invalidTextAttribute.sourceSpan,
|
|
59
|
+
value: invalidTextAttribute.value,
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
for (const { name, value, sourceSpan } of element.inputs) {
|
|
63
|
+
// Intentionally ignore the property binding by looking for literal primitives only
|
|
64
|
+
// in order to avoid type-checking for the property, e.g. check that it's 'submit', 'button', etc.
|
|
65
|
+
if (name === TYPE_ATTRIBUTE_NAME &&
|
|
66
|
+
value instanceof bundled_angular_compiler_1.ASTWithSource &&
|
|
67
|
+
value.ast instanceof bundled_angular_compiler_1.LiteralPrimitive &&
|
|
68
|
+
!VALID_BUTTON_TYPES.includes(value.ast.value)) {
|
|
69
|
+
return {
|
|
70
|
+
value: value.ast.value,
|
|
71
|
+
sourceSpan,
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
return null;
|
|
76
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.RULE_NAME = void 0;
|
|
4
|
+
const utils_1 = require("@angular-eslint/utils");
|
|
5
|
+
const create_eslint_rule_1 = require("../utils/create-eslint-rule");
|
|
6
|
+
const get_dom_elements_1 = require("../utils/get-dom-elements");
|
|
7
|
+
const is_hidden_from_screen_reader_1 = require("../utils/is-hidden-from-screen-reader");
|
|
8
|
+
const is_interactive_element_1 = require("../utils/is-interactive-element");
|
|
9
|
+
const is_presentation_role_1 = require("../utils/is-presentation-role");
|
|
10
|
+
exports.RULE_NAME = 'click-events-have-key-events';
|
|
11
|
+
exports.default = (0, create_eslint_rule_1.createESLintRule)({
|
|
12
|
+
name: exports.RULE_NAME,
|
|
13
|
+
meta: {
|
|
14
|
+
type: 'suggestion',
|
|
15
|
+
docs: {
|
|
16
|
+
description: '[Accessibility] Ensures that the click event is accompanied with at least one key event keyup, keydown or keypress.',
|
|
17
|
+
recommended: false,
|
|
18
|
+
},
|
|
19
|
+
schema: [],
|
|
20
|
+
messages: {
|
|
21
|
+
clickEventsHaveKeyEvents: 'click must be accompanied by either keyup, keydown or keypress event for accessibility.',
|
|
22
|
+
},
|
|
23
|
+
},
|
|
24
|
+
defaultOptions: [],
|
|
25
|
+
create(context) {
|
|
26
|
+
return {
|
|
27
|
+
Element$1(node) {
|
|
28
|
+
if (!(0, get_dom_elements_1.getDomElements)().has(node.name)) {
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
if ((0, is_presentation_role_1.isPresentationRole)(node) ||
|
|
32
|
+
(0, is_hidden_from_screen_reader_1.isHiddenFromScreenReader)(node) ||
|
|
33
|
+
(0, is_interactive_element_1.isInteractiveElement)(node)) {
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
let hasClick = false, hasKeyEvent = false;
|
|
37
|
+
for (const output of node.outputs) {
|
|
38
|
+
hasClick = hasClick || output.name === 'click';
|
|
39
|
+
hasKeyEvent =
|
|
40
|
+
hasKeyEvent ||
|
|
41
|
+
output.name.startsWith('keyup') ||
|
|
42
|
+
output.name.startsWith('keydown') ||
|
|
43
|
+
output.name.startsWith('keypress');
|
|
44
|
+
}
|
|
45
|
+
if (!hasClick || hasKeyEvent) {
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
const parserServices = (0, utils_1.getTemplateParserServices)(context);
|
|
49
|
+
const loc = parserServices.convertNodeSourceSpanToLoc(node.sourceSpan);
|
|
50
|
+
context.report({
|
|
51
|
+
loc,
|
|
52
|
+
messageId: 'clickEventsHaveKeyEvents',
|
|
53
|
+
});
|
|
54
|
+
},
|
|
55
|
+
};
|
|
56
|
+
},
|
|
57
|
+
});
|
|
@@ -0,0 +1,107 @@
|
|
|
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
|
+
exports.RULE_NAME = 'conditional-complexity';
|
|
8
|
+
const DEFAULT_MAX_COMPLEXITY = 5;
|
|
9
|
+
exports.default = (0, create_eslint_rule_1.createESLintRule)({
|
|
10
|
+
name: exports.RULE_NAME,
|
|
11
|
+
meta: {
|
|
12
|
+
type: 'suggestion',
|
|
13
|
+
docs: {
|
|
14
|
+
description: 'The conditional complexity should not exceed a rational limit',
|
|
15
|
+
recommended: false,
|
|
16
|
+
},
|
|
17
|
+
schema: [
|
|
18
|
+
{
|
|
19
|
+
type: 'object',
|
|
20
|
+
properties: {
|
|
21
|
+
maxComplexity: {
|
|
22
|
+
minimum: 1,
|
|
23
|
+
type: 'number',
|
|
24
|
+
},
|
|
25
|
+
},
|
|
26
|
+
additionalProperties: false,
|
|
27
|
+
},
|
|
28
|
+
],
|
|
29
|
+
messages: {
|
|
30
|
+
conditionalComplexity: 'The conditional complexity {{totalComplexity}} exceeds the defined limit {{maxComplexity}}',
|
|
31
|
+
},
|
|
32
|
+
},
|
|
33
|
+
defaultOptions: [{ maxComplexity: DEFAULT_MAX_COMPLEXITY }],
|
|
34
|
+
create(context, [{ maxComplexity }]) {
|
|
35
|
+
(0, utils_1.ensureTemplateParser)(context);
|
|
36
|
+
const sourceCode = context.getSourceCode();
|
|
37
|
+
return {
|
|
38
|
+
BoundAttribute(node) {
|
|
39
|
+
if (!node.value.source || node.value.ast instanceof bundled_angular_compiler_1.Interpolation) {
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
const possibleBinary = extractPossibleBinaryOrConditionalFrom(getParser().parseBinding(node.value.source, '', 0).ast);
|
|
43
|
+
const totalComplexity = getTotalComplexity(possibleBinary);
|
|
44
|
+
if (totalComplexity <= maxComplexity) {
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
const { sourceSpan: { start, end }, } = node.value;
|
|
48
|
+
context.report({
|
|
49
|
+
loc: {
|
|
50
|
+
start: sourceCode.getLocFromIndex(start),
|
|
51
|
+
end: sourceCode.getLocFromIndex(end),
|
|
52
|
+
},
|
|
53
|
+
messageId: 'conditionalComplexity',
|
|
54
|
+
data: { maxComplexity, totalComplexity },
|
|
55
|
+
});
|
|
56
|
+
},
|
|
57
|
+
Interpolation({ expressions }) {
|
|
58
|
+
for (const expression of expressions) {
|
|
59
|
+
const totalComplexity = getTotalComplexity(expression);
|
|
60
|
+
if (totalComplexity <= maxComplexity) {
|
|
61
|
+
continue;
|
|
62
|
+
}
|
|
63
|
+
const { sourceSpan: { start, end }, } = expression;
|
|
64
|
+
context.report({
|
|
65
|
+
loc: {
|
|
66
|
+
start: sourceCode.getLocFromIndex(start),
|
|
67
|
+
end: sourceCode.getLocFromIndex(end),
|
|
68
|
+
},
|
|
69
|
+
messageId: 'conditionalComplexity',
|
|
70
|
+
data: { maxComplexity, totalComplexity },
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
},
|
|
74
|
+
};
|
|
75
|
+
},
|
|
76
|
+
});
|
|
77
|
+
function extractPossibleBinaryOrConditionalFrom(node) {
|
|
78
|
+
return node instanceof bundled_angular_compiler_1.BindingPipe ? node.exp : node;
|
|
79
|
+
}
|
|
80
|
+
let parser = null;
|
|
81
|
+
// Instantiate the `Parser` class lazily only when this rule is applied.
|
|
82
|
+
function getParser() {
|
|
83
|
+
return parser || (parser = new bundled_angular_compiler_1.Parser(new bundled_angular_compiler_1.Lexer()));
|
|
84
|
+
}
|
|
85
|
+
function getTotalComplexity(ast) {
|
|
86
|
+
const possibleBinaryOrConditional = extractPossibleBinaryOrConditionalFrom(ast);
|
|
87
|
+
if (!(possibleBinaryOrConditional instanceof bundled_angular_compiler_1.Binary ||
|
|
88
|
+
possibleBinaryOrConditional instanceof bundled_angular_compiler_1.Conditional)) {
|
|
89
|
+
return 0;
|
|
90
|
+
}
|
|
91
|
+
let total = 1;
|
|
92
|
+
if (possibleBinaryOrConditional instanceof bundled_angular_compiler_1.Binary) {
|
|
93
|
+
if (possibleBinaryOrConditional.left instanceof bundled_angular_compiler_1.Binary) {
|
|
94
|
+
total += getTotalComplexity(possibleBinaryOrConditional.left);
|
|
95
|
+
}
|
|
96
|
+
if (possibleBinaryOrConditional.right instanceof bundled_angular_compiler_1.Binary) {
|
|
97
|
+
total += getTotalComplexity(possibleBinaryOrConditional.right);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
if (possibleBinaryOrConditional instanceof bundled_angular_compiler_1.Conditional) {
|
|
101
|
+
total +=
|
|
102
|
+
getTotalComplexity(possibleBinaryOrConditional.condition) +
|
|
103
|
+
getTotalComplexity(possibleBinaryOrConditional.trueExp) +
|
|
104
|
+
getTotalComplexity(possibleBinaryOrConditional.falseExp);
|
|
105
|
+
}
|
|
106
|
+
return total;
|
|
107
|
+
}
|