@angular-eslint/eslint-plugin-template 15.2.2-alpha.9 → 16.0.0-alpha.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/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 +4 -4
- 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,103 @@
|
|
|
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 is_child_node_of_1 = require("../utils/is-child-node-of");
|
|
7
|
+
exports.RULE_NAME = 'label-has-associated-control';
|
|
8
|
+
const DEFAULT_CONTROL_COMPONENTS = [
|
|
9
|
+
'input',
|
|
10
|
+
'meter',
|
|
11
|
+
'output',
|
|
12
|
+
'progress',
|
|
13
|
+
'select',
|
|
14
|
+
'textarea',
|
|
15
|
+
];
|
|
16
|
+
const DEFAULT_LABEL_COMPONENTS = [
|
|
17
|
+
{ inputs: ['for', 'htmlFor'], selector: 'label' },
|
|
18
|
+
];
|
|
19
|
+
const DEFAULT_OPTIONS = {
|
|
20
|
+
controlComponents: DEFAULT_CONTROL_COMPONENTS,
|
|
21
|
+
labelComponents: DEFAULT_LABEL_COMPONENTS,
|
|
22
|
+
};
|
|
23
|
+
exports.default = (0, create_eslint_rule_1.createESLintRule)({
|
|
24
|
+
name: exports.RULE_NAME,
|
|
25
|
+
meta: {
|
|
26
|
+
type: 'suggestion',
|
|
27
|
+
docs: {
|
|
28
|
+
description: '[Accessibility] Ensures that a label element/component is associated with a form element',
|
|
29
|
+
recommended: false,
|
|
30
|
+
},
|
|
31
|
+
schema: [
|
|
32
|
+
{
|
|
33
|
+
additionalProperties: false,
|
|
34
|
+
properties: {
|
|
35
|
+
controlComponents: {
|
|
36
|
+
items: { type: 'string' },
|
|
37
|
+
type: 'array',
|
|
38
|
+
uniqueItems: true,
|
|
39
|
+
},
|
|
40
|
+
labelComponents: {
|
|
41
|
+
items: {
|
|
42
|
+
additionalProperties: false,
|
|
43
|
+
properties: {
|
|
44
|
+
inputs: {
|
|
45
|
+
items: { type: 'string' },
|
|
46
|
+
type: 'array',
|
|
47
|
+
uniqueItems: true,
|
|
48
|
+
},
|
|
49
|
+
selector: { type: 'string' },
|
|
50
|
+
},
|
|
51
|
+
required: ['selector'],
|
|
52
|
+
type: 'object',
|
|
53
|
+
},
|
|
54
|
+
type: 'array',
|
|
55
|
+
uniqueItems: true,
|
|
56
|
+
},
|
|
57
|
+
},
|
|
58
|
+
type: 'object',
|
|
59
|
+
},
|
|
60
|
+
],
|
|
61
|
+
messages: {
|
|
62
|
+
labelHasAssociatedControl: 'A label component must be associated with a form element',
|
|
63
|
+
},
|
|
64
|
+
},
|
|
65
|
+
defaultOptions: [DEFAULT_OPTIONS],
|
|
66
|
+
create(context, [{ controlComponents, labelComponents }]) {
|
|
67
|
+
const parserServices = (0, utils_1.getTemplateParserServices)(context);
|
|
68
|
+
const allControlComponents = new Set([
|
|
69
|
+
...DEFAULT_CONTROL_COMPONENTS,
|
|
70
|
+
...(controlComponents !== null && controlComponents !== void 0 ? controlComponents : []),
|
|
71
|
+
]);
|
|
72
|
+
const allLabelComponents = [
|
|
73
|
+
...DEFAULT_LABEL_COMPONENTS,
|
|
74
|
+
...(labelComponents !== null && labelComponents !== void 0 ? labelComponents : []),
|
|
75
|
+
];
|
|
76
|
+
const labelSelectors = allLabelComponents.map(({ selector }) => selector);
|
|
77
|
+
const labelComponentsPattern = toPattern(labelSelectors);
|
|
78
|
+
return {
|
|
79
|
+
[`Element$1[name=${labelComponentsPattern}]`](node) {
|
|
80
|
+
var _a;
|
|
81
|
+
const element = allLabelComponents.find(({ selector }) => selector === node.name);
|
|
82
|
+
if (!element)
|
|
83
|
+
return;
|
|
84
|
+
const attributesInputs = new Set([...node.attributes, ...node.inputs].map(({ name }) => name));
|
|
85
|
+
const hasInput = (_a = element.inputs) === null || _a === void 0 ? void 0 : _a.some((input) => attributesInputs.has(input));
|
|
86
|
+
if (hasInput || hasControlComponentIn(allControlComponents, node)) {
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
const loc = parserServices.convertNodeSourceSpanToLoc(node.sourceSpan);
|
|
90
|
+
context.report({
|
|
91
|
+
loc,
|
|
92
|
+
messageId: 'labelHasAssociatedControl',
|
|
93
|
+
});
|
|
94
|
+
},
|
|
95
|
+
};
|
|
96
|
+
},
|
|
97
|
+
});
|
|
98
|
+
function hasControlComponentIn(controlComponents, element) {
|
|
99
|
+
return Boolean([...controlComponents].some((controlComponent) => (0, is_child_node_of_1.isChildNodeOf)(element, controlComponent)));
|
|
100
|
+
}
|
|
101
|
+
function toPattern(value) {
|
|
102
|
+
return RegExp(`^(${value.join('|')})$`);
|
|
103
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
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 to_pattern_1 = require("../utils/to-pattern");
|
|
8
|
+
exports.RULE_NAME = 'mouse-events-have-key-events';
|
|
9
|
+
const STYLE_GUIDE_LINK = 'https://www.w3.org/WAI/WCAG21/Understanding/keyboard';
|
|
10
|
+
exports.default = (0, create_eslint_rule_1.createESLintRule)({
|
|
11
|
+
name: exports.RULE_NAME,
|
|
12
|
+
meta: {
|
|
13
|
+
type: 'suggestion',
|
|
14
|
+
docs: {
|
|
15
|
+
description: `[Accessibility] Ensures that the mouse events \`${"mouseout" /* MouseEvents.MouseOut */}\` and \`${"mouseover" /* MouseEvents.MouseOver */}\` are accompanied by \`${"focus" /* KeyEvents.Focus */}\` and \`${"blur" /* KeyEvents.Blur */}\` events respectively. Coding for the keyboard is important for users with physical disabilities who cannot use a mouse, AT compatibility, and screenreader users. See more at ${STYLE_GUIDE_LINK}`,
|
|
16
|
+
recommended: false,
|
|
17
|
+
},
|
|
18
|
+
schema: [],
|
|
19
|
+
messages: {
|
|
20
|
+
mouseEventsHaveKeyEvents: `\`{{mouseEvent}}\` must be accompanied by \`{{keyEvent}}\` for accessibility (${STYLE_GUIDE_LINK})`,
|
|
21
|
+
},
|
|
22
|
+
},
|
|
23
|
+
defaultOptions: [],
|
|
24
|
+
create(context) {
|
|
25
|
+
const parserServices = (0, utils_1.getTemplateParserServices)(context);
|
|
26
|
+
const domElementsPattern = (0, to_pattern_1.toPattern)([...(0, get_dom_elements_1.getDomElements)()]);
|
|
27
|
+
const eventPairs = [
|
|
28
|
+
["blur" /* KeyEvents.Blur */, "mouseout" /* MouseEvents.MouseOut */],
|
|
29
|
+
["focus" /* KeyEvents.Focus */, "mouseover" /* MouseEvents.MouseOver */],
|
|
30
|
+
];
|
|
31
|
+
return eventPairs.reduce((accumulator, [keyEvent, mouseEvent]) => (Object.assign(Object.assign({}, accumulator), { [`Element$1[name=${domElementsPattern}]:has(BoundEvent[name='${mouseEvent}']):not(:has(BoundEvent[name='${keyEvent}']))`]({ sourceSpan, }) {
|
|
32
|
+
const loc = parserServices.convertNodeSourceSpanToLoc(sourceSpan);
|
|
33
|
+
context.report({
|
|
34
|
+
loc,
|
|
35
|
+
messageId: 'mouseEventsHaveKeyEvents',
|
|
36
|
+
data: { keyEvent, mouseEvent },
|
|
37
|
+
});
|
|
38
|
+
} })), {});
|
|
39
|
+
},
|
|
40
|
+
});
|
|
@@ -0,0 +1,59 @@
|
|
|
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 = 'no-any';
|
|
8
|
+
const ANY_TYPE_CAST_FUNCTION_NAME = '$any';
|
|
9
|
+
exports.default = (0, create_eslint_rule_1.createESLintRule)({
|
|
10
|
+
name: exports.RULE_NAME,
|
|
11
|
+
meta: {
|
|
12
|
+
type: 'suggestion',
|
|
13
|
+
docs: {
|
|
14
|
+
description: `The use of "${ANY_TYPE_CAST_FUNCTION_NAME}" nullifies the compile-time benefits of Angular's type system`,
|
|
15
|
+
recommended: false,
|
|
16
|
+
},
|
|
17
|
+
hasSuggestions: true,
|
|
18
|
+
schema: [],
|
|
19
|
+
messages: {
|
|
20
|
+
noAny: `Avoid using "${ANY_TYPE_CAST_FUNCTION_NAME}" in templates`,
|
|
21
|
+
suggestRemoveAny: `Remove ${ANY_TYPE_CAST_FUNCTION_NAME}`,
|
|
22
|
+
},
|
|
23
|
+
},
|
|
24
|
+
defaultOptions: [],
|
|
25
|
+
create(context) {
|
|
26
|
+
(0, utils_1.ensureTemplateParser)(context);
|
|
27
|
+
const sourceCode = context.getSourceCode();
|
|
28
|
+
return {
|
|
29
|
+
[`Call[receiver.name="${ANY_TYPE_CAST_FUNCTION_NAME}"]`]({ receiver, sourceSpan: { end, start }, }) {
|
|
30
|
+
if (!(receiver instanceof bundled_angular_compiler_1.PropertyRead)) {
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
if (!(
|
|
34
|
+
// this.$any() is also valid usage of the native Angular $any()
|
|
35
|
+
(receiver.receiver instanceof bundled_angular_compiler_1.ThisReceiver ||
|
|
36
|
+
receiver.receiver instanceof bundled_angular_compiler_1.ImplicitReceiver))) {
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
const nameSpan = receiver.nameSpan;
|
|
40
|
+
context.report({
|
|
41
|
+
messageId: 'noAny',
|
|
42
|
+
loc: {
|
|
43
|
+
start: sourceCode.getLocFromIndex(start),
|
|
44
|
+
end: sourceCode.getLocFromIndex(end),
|
|
45
|
+
},
|
|
46
|
+
suggest: [
|
|
47
|
+
{
|
|
48
|
+
messageId: 'suggestRemoveAny',
|
|
49
|
+
fix: (fixer) => [
|
|
50
|
+
fixer.removeRange([nameSpan.start, nameSpan.end + 1]),
|
|
51
|
+
fixer.removeRange([end - 1, end]),
|
|
52
|
+
],
|
|
53
|
+
},
|
|
54
|
+
],
|
|
55
|
+
});
|
|
56
|
+
},
|
|
57
|
+
};
|
|
58
|
+
},
|
|
59
|
+
});
|
|
@@ -0,0 +1,41 @@
|
|
|
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 to_pattern_1 = require("../utils/to-pattern");
|
|
8
|
+
exports.RULE_NAME = 'no-autofocus';
|
|
9
|
+
exports.default = (0, create_eslint_rule_1.createESLintRule)({
|
|
10
|
+
name: exports.RULE_NAME,
|
|
11
|
+
meta: {
|
|
12
|
+
type: 'suggestion',
|
|
13
|
+
docs: {
|
|
14
|
+
description: '[Accessibility] Ensures that the `autofocus` attribute is not used',
|
|
15
|
+
recommended: false,
|
|
16
|
+
},
|
|
17
|
+
fixable: 'code',
|
|
18
|
+
schema: [],
|
|
19
|
+
messages: {
|
|
20
|
+
noAutofocus: 'The `autofocus` attribute should not be used, as it reduces usability and accessibility for users',
|
|
21
|
+
},
|
|
22
|
+
},
|
|
23
|
+
defaultOptions: [],
|
|
24
|
+
create(context) {
|
|
25
|
+
const parserServices = (0, utils_1.getTemplateParserServices)(context);
|
|
26
|
+
const elementNamePattern = (0, to_pattern_1.toPattern)([...(0, get_dom_elements_1.getDomElements)()]);
|
|
27
|
+
return {
|
|
28
|
+
[`Element$1[name=${elementNamePattern}] > :matches(BoundAttribute, TextAttribute)[name="autofocus"]`]({ sourceSpan, }) {
|
|
29
|
+
const loc = parserServices.convertNodeSourceSpanToLoc(sourceSpan);
|
|
30
|
+
context.report({
|
|
31
|
+
loc,
|
|
32
|
+
messageId: 'noAutofocus',
|
|
33
|
+
fix: (fixer) => fixer.removeRange([
|
|
34
|
+
sourceSpan.start.offset - 1,
|
|
35
|
+
sourceSpan.end.offset,
|
|
36
|
+
]),
|
|
37
|
+
});
|
|
38
|
+
},
|
|
39
|
+
};
|
|
40
|
+
},
|
|
41
|
+
});
|
|
@@ -0,0 +1,68 @@
|
|
|
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 get_nearest_node_from_1 = require("../utils/get-nearest-node-from");
|
|
8
|
+
exports.RULE_NAME = 'no-call-expression';
|
|
9
|
+
exports.default = (0, create_eslint_rule_1.createESLintRule)({
|
|
10
|
+
name: exports.RULE_NAME,
|
|
11
|
+
meta: {
|
|
12
|
+
type: 'suggestion',
|
|
13
|
+
docs: {
|
|
14
|
+
description: 'Disallows calling expressions in templates, except for output handlers',
|
|
15
|
+
recommended: false,
|
|
16
|
+
},
|
|
17
|
+
schema: [
|
|
18
|
+
{
|
|
19
|
+
additionalProperties: false,
|
|
20
|
+
properties: {
|
|
21
|
+
allowList: {
|
|
22
|
+
items: { type: 'string' },
|
|
23
|
+
type: 'array',
|
|
24
|
+
uniqueItems: true,
|
|
25
|
+
},
|
|
26
|
+
},
|
|
27
|
+
type: 'object',
|
|
28
|
+
},
|
|
29
|
+
],
|
|
30
|
+
messages: {
|
|
31
|
+
noCallExpression: 'Avoid calling expressions in templates',
|
|
32
|
+
},
|
|
33
|
+
},
|
|
34
|
+
defaultOptions: [{ allowList: [] }],
|
|
35
|
+
create(context, [{ allowList }]) {
|
|
36
|
+
(0, utils_1.ensureTemplateParser)(context);
|
|
37
|
+
const sourceCode = context.getSourceCode();
|
|
38
|
+
return {
|
|
39
|
+
'Call[receiver.name!="$any"]'(node) {
|
|
40
|
+
const isChildOfBoundEvent = Boolean((0, get_nearest_node_from_1.getNearestNodeFrom)(node, isBoundEvent));
|
|
41
|
+
if (isChildOfBoundEvent)
|
|
42
|
+
return;
|
|
43
|
+
if (isCallNameInAllowList(node.receiver, allowList))
|
|
44
|
+
return;
|
|
45
|
+
const { sourceSpan: { start, end }, } = node;
|
|
46
|
+
context.report({
|
|
47
|
+
loc: {
|
|
48
|
+
start: sourceCode.getLocFromIndex(start),
|
|
49
|
+
end: sourceCode.getLocFromIndex(end),
|
|
50
|
+
},
|
|
51
|
+
messageId: 'noCallExpression',
|
|
52
|
+
});
|
|
53
|
+
},
|
|
54
|
+
};
|
|
55
|
+
},
|
|
56
|
+
});
|
|
57
|
+
function isBoundEvent(node) {
|
|
58
|
+
return node instanceof bundled_angular_compiler_1.TmplAstBoundEvent;
|
|
59
|
+
}
|
|
60
|
+
function isASTWithName(ast) {
|
|
61
|
+
return !!ast.name;
|
|
62
|
+
}
|
|
63
|
+
function isCallNameInAllowList(ast, allowList) {
|
|
64
|
+
return (allowList &&
|
|
65
|
+
allowList.length > 0 &&
|
|
66
|
+
isASTWithName(ast) &&
|
|
67
|
+
allowList.indexOf(ast.name) > -1);
|
|
68
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
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 = 'no-distracting-elements';
|
|
7
|
+
exports.default = (0, create_eslint_rule_1.createESLintRule)({
|
|
8
|
+
name: exports.RULE_NAME,
|
|
9
|
+
meta: {
|
|
10
|
+
type: 'suggestion',
|
|
11
|
+
docs: {
|
|
12
|
+
description: '[Accessibility] Enforces that no distracting elements are used',
|
|
13
|
+
recommended: false,
|
|
14
|
+
},
|
|
15
|
+
fixable: 'code',
|
|
16
|
+
schema: [],
|
|
17
|
+
messages: {
|
|
18
|
+
noDistractingElements: 'Do not use <{{element}}> elements as they can create visual accessibility issues and are deprecated',
|
|
19
|
+
},
|
|
20
|
+
},
|
|
21
|
+
defaultOptions: [],
|
|
22
|
+
create(context) {
|
|
23
|
+
const parserServices = (0, utils_1.getTemplateParserServices)(context);
|
|
24
|
+
return {
|
|
25
|
+
'Element$1[name=/^(blink|marquee)$/]'({ name: element, sourceSpan, }) {
|
|
26
|
+
const loc = parserServices.convertNodeSourceSpanToLoc(sourceSpan);
|
|
27
|
+
context.report({
|
|
28
|
+
loc,
|
|
29
|
+
messageId: 'noDistractingElements',
|
|
30
|
+
data: { element },
|
|
31
|
+
fix: (fixer) => fixer.removeRange([sourceSpan.start.offset, sourceSpan.end.offset]),
|
|
32
|
+
});
|
|
33
|
+
},
|
|
34
|
+
};
|
|
35
|
+
},
|
|
36
|
+
});
|
|
@@ -0,0 +1,97 @@
|
|
|
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_original_attribute_name_1 = require("../utils/get-original-attribute-name");
|
|
7
|
+
exports.RULE_NAME = 'no-duplicate-attributes';
|
|
8
|
+
const DEFAULT_OPTIONS = {
|
|
9
|
+
allowTwoWayDataBinding: true,
|
|
10
|
+
ignore: [],
|
|
11
|
+
};
|
|
12
|
+
exports.default = (0, create_eslint_rule_1.createESLintRule)({
|
|
13
|
+
name: exports.RULE_NAME,
|
|
14
|
+
meta: {
|
|
15
|
+
type: 'problem',
|
|
16
|
+
docs: {
|
|
17
|
+
description: 'Ensures that there are no duplicate input properties or output event listeners',
|
|
18
|
+
recommended: false,
|
|
19
|
+
},
|
|
20
|
+
hasSuggestions: true,
|
|
21
|
+
schema: [
|
|
22
|
+
{
|
|
23
|
+
type: 'object',
|
|
24
|
+
properties: {
|
|
25
|
+
allowTwoWayDataBinding: {
|
|
26
|
+
type: 'boolean',
|
|
27
|
+
default: DEFAULT_OPTIONS.allowTwoWayDataBinding,
|
|
28
|
+
description: `Whether or not two-way data binding is allowed as an exception to the rule.`,
|
|
29
|
+
},
|
|
30
|
+
ignore: {
|
|
31
|
+
type: 'array',
|
|
32
|
+
items: { type: 'string' },
|
|
33
|
+
uniqueItems: true,
|
|
34
|
+
default: DEFAULT_OPTIONS.ignore,
|
|
35
|
+
description: `Input or output properties for which duplicate presence is allowed as an exception to the rule.`,
|
|
36
|
+
},
|
|
37
|
+
},
|
|
38
|
+
additionalProperties: false,
|
|
39
|
+
},
|
|
40
|
+
],
|
|
41
|
+
messages: {
|
|
42
|
+
noDuplicateAttributes: 'Duplicate attribute `{{attributeName}}`',
|
|
43
|
+
suggestRemoveAttribute: 'Remove attribute `{{attributeName}}`',
|
|
44
|
+
},
|
|
45
|
+
},
|
|
46
|
+
defaultOptions: [DEFAULT_OPTIONS],
|
|
47
|
+
create(context, [{ allowTwoWayDataBinding, ignore }]) {
|
|
48
|
+
const parserServices = (0, utils_1.getTemplateParserServices)(context);
|
|
49
|
+
return {
|
|
50
|
+
Element$1({ inputs, outputs, attributes }) {
|
|
51
|
+
const duplicateInputsAndAttributes = findDuplicates([
|
|
52
|
+
...inputs,
|
|
53
|
+
...attributes,
|
|
54
|
+
]);
|
|
55
|
+
const filteredOutputs = allowTwoWayDataBinding
|
|
56
|
+
? outputs.filter((output) => {
|
|
57
|
+
return !inputs.some((input) => input.sourceSpan.start === output.sourceSpan.start &&
|
|
58
|
+
input.sourceSpan.end === output.sourceSpan.end);
|
|
59
|
+
})
|
|
60
|
+
: outputs;
|
|
61
|
+
const duplicateOutputs = findDuplicates(filteredOutputs);
|
|
62
|
+
const allDuplicates = [
|
|
63
|
+
...duplicateInputsAndAttributes,
|
|
64
|
+
...duplicateOutputs,
|
|
65
|
+
];
|
|
66
|
+
const filteredDuplicates = ignore && ignore.length > 0
|
|
67
|
+
? allDuplicates.filter((duplicate) => !ignore.includes((0, get_original_attribute_name_1.getOriginalAttributeName)(duplicate)))
|
|
68
|
+
: allDuplicates;
|
|
69
|
+
filteredDuplicates.forEach((duplicate) => {
|
|
70
|
+
const loc = parserServices.convertNodeSourceSpanToLoc(duplicate.sourceSpan);
|
|
71
|
+
const data = {
|
|
72
|
+
attributeName: (0, get_original_attribute_name_1.getOriginalAttributeName)(duplicate),
|
|
73
|
+
};
|
|
74
|
+
context.report({
|
|
75
|
+
loc,
|
|
76
|
+
messageId: 'noDuplicateAttributes',
|
|
77
|
+
data,
|
|
78
|
+
suggest: [
|
|
79
|
+
{
|
|
80
|
+
messageId: 'suggestRemoveAttribute',
|
|
81
|
+
fix: (fixer) => fixer.removeRange([loc.start.column, loc.end.column + 1]),
|
|
82
|
+
data,
|
|
83
|
+
},
|
|
84
|
+
],
|
|
85
|
+
});
|
|
86
|
+
});
|
|
87
|
+
},
|
|
88
|
+
};
|
|
89
|
+
},
|
|
90
|
+
});
|
|
91
|
+
function findDuplicates(elements) {
|
|
92
|
+
return elements.filter((element) => {
|
|
93
|
+
return elements.some((otherElement) => otherElement !== element &&
|
|
94
|
+
(0, get_original_attribute_name_1.getOriginalAttributeName)(otherElement) ===
|
|
95
|
+
(0, get_original_attribute_name_1.getOriginalAttributeName)(element));
|
|
96
|
+
});
|
|
97
|
+
}
|
|
@@ -0,0 +1,104 @@
|
|
|
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 DEFAULT_OPTIONS = {
|
|
7
|
+
allowNgStyle: false,
|
|
8
|
+
allowBindToStyle: false,
|
|
9
|
+
};
|
|
10
|
+
exports.RULE_NAME = 'no-inline-styles';
|
|
11
|
+
exports.default = (0, create_eslint_rule_1.createESLintRule)({
|
|
12
|
+
name: exports.RULE_NAME,
|
|
13
|
+
meta: {
|
|
14
|
+
type: 'suggestion',
|
|
15
|
+
docs: {
|
|
16
|
+
description: 'Disallows the use of inline styles in HTML templates',
|
|
17
|
+
recommended: false,
|
|
18
|
+
},
|
|
19
|
+
schema: [
|
|
20
|
+
{
|
|
21
|
+
type: 'object',
|
|
22
|
+
properties: {
|
|
23
|
+
allowNgStyle: {
|
|
24
|
+
type: 'boolean',
|
|
25
|
+
default: DEFAULT_OPTIONS.allowNgStyle,
|
|
26
|
+
},
|
|
27
|
+
allowBindToStyle: {
|
|
28
|
+
type: 'boolean',
|
|
29
|
+
default: DEFAULT_OPTIONS.allowBindToStyle,
|
|
30
|
+
},
|
|
31
|
+
},
|
|
32
|
+
additionalProperties: false,
|
|
33
|
+
},
|
|
34
|
+
],
|
|
35
|
+
messages: {
|
|
36
|
+
noInlineStyles: '<{{element}}/> element should not have inline styles via style attribute. Please use classes instead.',
|
|
37
|
+
},
|
|
38
|
+
},
|
|
39
|
+
defaultOptions: [DEFAULT_OPTIONS],
|
|
40
|
+
create(context, [{ allowNgStyle, allowBindToStyle }]) {
|
|
41
|
+
const parserServices = (0, utils_1.getTemplateParserServices)(context);
|
|
42
|
+
return {
|
|
43
|
+
Element$1(node) {
|
|
44
|
+
let isInvalid = false;
|
|
45
|
+
if (!allowNgStyle && !allowBindToStyle) {
|
|
46
|
+
isInvalid =
|
|
47
|
+
isNodeHasStyleAttribute(node) ||
|
|
48
|
+
isNodeHasNgStyleAttribute(node) ||
|
|
49
|
+
isNodeHasBindingToStyleAttribute(node);
|
|
50
|
+
}
|
|
51
|
+
else {
|
|
52
|
+
const ngStyle = allowNgStyle
|
|
53
|
+
? false
|
|
54
|
+
: isNodeHasNgStyleAttribute(node);
|
|
55
|
+
const bindToStyle = allowBindToStyle
|
|
56
|
+
? false
|
|
57
|
+
: isNodeHasBindingToStyleAttribute(node);
|
|
58
|
+
isInvalid = isNodeHasStyleAttribute(node) || ngStyle || bindToStyle;
|
|
59
|
+
}
|
|
60
|
+
if (isInvalid) {
|
|
61
|
+
const loc = parserServices.convertElementSourceSpanToLoc(context, node);
|
|
62
|
+
context.report({
|
|
63
|
+
loc,
|
|
64
|
+
messageId: 'noInlineStyles',
|
|
65
|
+
data: {
|
|
66
|
+
element: node.name,
|
|
67
|
+
},
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
},
|
|
71
|
+
};
|
|
72
|
+
},
|
|
73
|
+
});
|
|
74
|
+
/**
|
|
75
|
+
* Check that the any element for example `<img>` has a `style` attribute or `attr.style` binding.
|
|
76
|
+
*/
|
|
77
|
+
function isNodeHasStyleAttribute(node) {
|
|
78
|
+
return (node.attributes.some(({ name }) => isStyle(name)) ||
|
|
79
|
+
node.inputs.some(({ name }) => isStyle(name)));
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* Check that the any element for example `<img>` has a `ngStyle` attribute binding.
|
|
83
|
+
*/
|
|
84
|
+
function isNodeHasNgStyleAttribute(node) {
|
|
85
|
+
return node.inputs.some(({ name }) => isNgStyle(name));
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* Check that the any element for example `<img>` has a `[style.background-color]` attribute binding.
|
|
89
|
+
*/
|
|
90
|
+
function isNodeHasBindingToStyleAttribute(node) {
|
|
91
|
+
return node.inputs.some(({ keySpan }) => isStyleBound(keySpan));
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* Check element is style
|
|
95
|
+
*/
|
|
96
|
+
function isStyle(name) {
|
|
97
|
+
return name === 'style';
|
|
98
|
+
}
|
|
99
|
+
function isNgStyle(name) {
|
|
100
|
+
return name === 'ngStyle';
|
|
101
|
+
}
|
|
102
|
+
function isStyleBound(keySpan) {
|
|
103
|
+
return (keySpan === null || keySpan === void 0 ? void 0 : keySpan.details) ? keySpan.details.includes('style.') : false;
|
|
104
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.RULE_NAME = exports.MESSAGE_ID = void 0;
|
|
4
|
+
const create_eslint_rule_1 = require("../utils/create-eslint-rule");
|
|
5
|
+
exports.MESSAGE_ID = 'noInterpolationInAttributes';
|
|
6
|
+
exports.RULE_NAME = 'no-interpolation-in-attributes';
|
|
7
|
+
exports.default = (0, create_eslint_rule_1.createESLintRule)({
|
|
8
|
+
name: exports.RULE_NAME,
|
|
9
|
+
meta: {
|
|
10
|
+
type: 'suggestion',
|
|
11
|
+
docs: {
|
|
12
|
+
description: 'Ensures that property-binding is used instead of interpolation in attributes.',
|
|
13
|
+
recommended: false,
|
|
14
|
+
},
|
|
15
|
+
schema: [],
|
|
16
|
+
messages: {
|
|
17
|
+
[exports.MESSAGE_ID]: 'Use property binding [attribute]="value" instead of interpolation {{ value }} for an attribute.',
|
|
18
|
+
},
|
|
19
|
+
},
|
|
20
|
+
defaultOptions: [],
|
|
21
|
+
create(context) {
|
|
22
|
+
const sourceCode = context.getSourceCode();
|
|
23
|
+
return {
|
|
24
|
+
['BoundAttribute Interpolation'](interpolation) {
|
|
25
|
+
const { sourceSpan: { start, end }, } = interpolation;
|
|
26
|
+
context.report({
|
|
27
|
+
loc: {
|
|
28
|
+
start: sourceCode.getLocFromIndex(start),
|
|
29
|
+
end: sourceCode.getLocFromIndex(end),
|
|
30
|
+
},
|
|
31
|
+
messageId: exports.MESSAGE_ID,
|
|
32
|
+
});
|
|
33
|
+
},
|
|
34
|
+
};
|
|
35
|
+
},
|
|
36
|
+
});
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.RULE_DOCS_EXTENSION = 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 = 'no-negated-async';
|
|
7
|
+
exports.default = (0, create_eslint_rule_1.createESLintRule)({
|
|
8
|
+
name: exports.RULE_NAME,
|
|
9
|
+
meta: {
|
|
10
|
+
type: 'suggestion',
|
|
11
|
+
docs: {
|
|
12
|
+
description: 'Ensures that async pipe results are not negated',
|
|
13
|
+
recommended: 'error',
|
|
14
|
+
},
|
|
15
|
+
hasSuggestions: true,
|
|
16
|
+
schema: [],
|
|
17
|
+
messages: {
|
|
18
|
+
noNegatedAsync: 'Async pipe results should not be negated. Use `(observable | async) === false`, `(observable | async) === null`, or `(observable | async) === undefined` to check its value instead',
|
|
19
|
+
suggestFalseComparison: 'Compare with `false`',
|
|
20
|
+
suggestNullComparison: 'Compare with `null`',
|
|
21
|
+
suggestUndefinedComparison: 'Compare with `undefined`',
|
|
22
|
+
},
|
|
23
|
+
},
|
|
24
|
+
defaultOptions: [],
|
|
25
|
+
create(context) {
|
|
26
|
+
(0, utils_1.ensureTemplateParser)(context);
|
|
27
|
+
const sourceCode = context.getSourceCode();
|
|
28
|
+
return {
|
|
29
|
+
':not(PrefixNot) > PrefixNot > BindingPipe[name="async"]'({ parent: { sourceSpan: { end, start }, }, }) {
|
|
30
|
+
context.report({
|
|
31
|
+
messageId: 'noNegatedAsync',
|
|
32
|
+
loc: {
|
|
33
|
+
start: sourceCode.getLocFromIndex(start),
|
|
34
|
+
end: sourceCode.getLocFromIndex(end),
|
|
35
|
+
},
|
|
36
|
+
suggest: getSuggestionsSchema().map(({ messageId, textToInsert }) => ({
|
|
37
|
+
messageId,
|
|
38
|
+
fix: (fixer) => [
|
|
39
|
+
fixer.removeRange([start, start + 1]),
|
|
40
|
+
fixer.insertTextAfterRange([end, end], textToInsert),
|
|
41
|
+
],
|
|
42
|
+
})),
|
|
43
|
+
});
|
|
44
|
+
},
|
|
45
|
+
};
|
|
46
|
+
},
|
|
47
|
+
});
|
|
48
|
+
exports.RULE_DOCS_EXTENSION = {
|
|
49
|
+
rationale: `Angular's async pipes emit null initially, prior to the observable emitting any values, or the promise resolving. This can cause negations, like *ngIf="!(myConditional | async)" to thrash the layout and cause expensive side-effects like firing off XHR requests for a component which should not be shown.`,
|
|
50
|
+
};
|
|
51
|
+
function getSuggestionsSchema() {
|
|
52
|
+
return [
|
|
53
|
+
{ messageId: 'suggestFalseComparison', textToInsert: ' === false' },
|
|
54
|
+
{ messageId: 'suggestNullComparison', textToInsert: ' === null' },
|
|
55
|
+
{
|
|
56
|
+
messageId: 'suggestUndefinedComparison',
|
|
57
|
+
textToInsert: ' === undefined',
|
|
58
|
+
},
|
|
59
|
+
];
|
|
60
|
+
}
|