@html-eslint/eslint-plugin 0.9.0-alpha.0 → 0.11.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/lib/configs/recommended.js +1 -0
- package/lib/constants/node-types.js +0 -5
- package/lib/constants/obsolete-tags.js +1 -1
- package/lib/constants/rule-category.js +0 -5
- package/lib/constants/void-elements.js +1 -1
- package/lib/rules/element-newline.js +29 -9
- package/lib/rules/id-naming-convention.js +16 -9
- package/lib/rules/indent.js +19 -7
- package/lib/rules/index.js +14 -2
- package/lib/rules/no-abstract-roles.js +62 -0
- package/lib/rules/no-accesskey-attrs.js +45 -0
- package/lib/rules/no-aria-hidden-body.js +46 -0
- package/lib/rules/no-duplicate-attrs.js +54 -0
- package/lib/rules/no-duplicate-id.js +7 -0
- package/lib/rules/no-extra-spacing-attrs.js +27 -5
- package/lib/rules/no-inline-styles.js +7 -0
- package/lib/rules/no-multiple-empty-lines.js +90 -0
- package/lib/rules/no-multiple-h1.js +7 -0
- package/lib/rules/no-non-scalable-viewport.js +7 -0
- package/lib/rules/no-obsolete-tags.js +7 -0
- package/lib/rules/no-positive-tabindex.js +7 -0
- package/lib/rules/no-skip-heading-levels.js +7 -0
- package/lib/rules/no-target-blank.js +7 -0
- package/lib/rules/quotes.js +30 -2
- package/lib/rules/require-button-type.js +58 -0
- package/lib/rules/require-closing-tags.js +11 -4
- package/lib/rules/require-doctype.js +7 -0
- package/lib/rules/require-frame-title.js +7 -0
- package/lib/rules/require-img-alt.js +13 -0
- package/lib/rules/require-lang.js +10 -4
- package/lib/rules/require-li-container.js +13 -1
- package/lib/rules/require-meta-charset.js +12 -1
- package/lib/rules/require-meta-description.js +15 -4
- package/lib/rules/require-meta-viewport.js +12 -8
- package/lib/rules/require-title.js +9 -3
- package/lib/rules/utils/node-utils.js +27 -5
- package/lib/types.d.ts +104 -33
- package/package.json +8 -4
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* @typedef {import("../types").
|
|
2
|
+
* @typedef {import("../types").ElementNode} ElementNode
|
|
3
|
+
* @typedef {import("../types").AnyNode} AnyNode
|
|
4
|
+
* @typedef {import("../types").Context} Context
|
|
3
5
|
*/
|
|
4
6
|
|
|
5
7
|
const { RULE_CATEGORY, NODE_TYPES } = require("../constants");
|
|
@@ -29,9 +31,12 @@ module.exports = {
|
|
|
29
31
|
},
|
|
30
32
|
},
|
|
31
33
|
|
|
34
|
+
/**
|
|
35
|
+
* @param {Context} context
|
|
36
|
+
*/
|
|
32
37
|
create(context) {
|
|
33
|
-
function checkSiblings(
|
|
34
|
-
|
|
38
|
+
function checkSiblings(siblings) {
|
|
39
|
+
siblings
|
|
35
40
|
.filter((node) => node.type !== NODE_TYPES.TEXT && node.range[0])
|
|
36
41
|
.forEach((current, index, arr) => {
|
|
37
42
|
const after = arr[index + 1];
|
|
@@ -50,6 +55,10 @@ module.exports = {
|
|
|
50
55
|
});
|
|
51
56
|
}
|
|
52
57
|
|
|
58
|
+
/**
|
|
59
|
+
*
|
|
60
|
+
* @param {ElementNode['childNodes'][number]} node
|
|
61
|
+
*/
|
|
53
62
|
function checkChild(node) {
|
|
54
63
|
const children = (node.childNodes || []).filter(
|
|
55
64
|
(n) => !!n.range[0] && n.type !== NODE_TYPES.TEXT
|
|
@@ -57,31 +66,40 @@ module.exports = {
|
|
|
57
66
|
const first = children[0];
|
|
58
67
|
const last = children[children.length - 1];
|
|
59
68
|
if (first) {
|
|
60
|
-
if (isOnTheSameLine(node.startTag, first)) {
|
|
69
|
+
if (node.startTag && isOnTheSameLine(node.startTag, first)) {
|
|
61
70
|
context.report({
|
|
62
71
|
node: node.startTag,
|
|
63
72
|
messageId: MESSAGE_IDS.EXPECT_NEW_LINE_AFTER,
|
|
64
73
|
data: { tag: `<${node.tagName}>` },
|
|
65
74
|
fix(fixer) {
|
|
66
|
-
|
|
75
|
+
if (node.startTag) {
|
|
76
|
+
return fixer.insertTextAfter(node.startTag, "\n");
|
|
77
|
+
}
|
|
78
|
+
return null;
|
|
67
79
|
},
|
|
68
80
|
});
|
|
69
81
|
}
|
|
70
82
|
}
|
|
71
83
|
if (last) {
|
|
72
|
-
if (isOnTheSameLine(node.endTag, last)) {
|
|
84
|
+
if (node.endTag && isOnTheSameLine(node.endTag, last)) {
|
|
73
85
|
context.report({
|
|
74
86
|
node: node.endTag,
|
|
75
87
|
messageId: MESSAGE_IDS.EXPECT_NEW_LINE_BEFORE,
|
|
76
88
|
data: { tag: `</${node.tagName}>` },
|
|
77
89
|
fix(fixer) {
|
|
78
|
-
|
|
90
|
+
if (node.endTag) {
|
|
91
|
+
return fixer.insertTextBefore(node.endTag, "\n");
|
|
92
|
+
}
|
|
93
|
+
return null;
|
|
79
94
|
},
|
|
80
95
|
});
|
|
81
96
|
}
|
|
82
97
|
}
|
|
83
98
|
}
|
|
84
99
|
return {
|
|
100
|
+
/**
|
|
101
|
+
* @param {ElementNode} node
|
|
102
|
+
*/
|
|
85
103
|
"*"(node) {
|
|
86
104
|
if (node.type !== NODE_TYPES.TEXT) {
|
|
87
105
|
checkSiblings(node.childNodes || []);
|
|
@@ -94,13 +112,15 @@ module.exports = {
|
|
|
94
112
|
|
|
95
113
|
/**
|
|
96
114
|
* Checks whether two nodes are on the same line or not.
|
|
97
|
-
* @param {
|
|
98
|
-
* @param {
|
|
115
|
+
* @param {AnyNode} nodeBefore A node before
|
|
116
|
+
* @param {AnyNode} nodeAfter A node after
|
|
99
117
|
* @returns {boolean} `true` if two nodes are on the same line, otherwise `false`.
|
|
100
118
|
*/
|
|
101
119
|
function isOnTheSameLine(nodeBefore, nodeAfter) {
|
|
102
120
|
if (nodeBefore && nodeAfter) {
|
|
121
|
+
// @ts-ignore
|
|
103
122
|
if (nodeBefore.endTag) {
|
|
123
|
+
// @ts-ignore
|
|
104
124
|
return nodeBefore.endTag.loc.end.line === nodeAfter.loc.start.line;
|
|
105
125
|
}
|
|
106
126
|
return nodeBefore.loc.start.line === nodeAfter.loc.start.line;
|
|
@@ -1,3 +1,7 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @typedef {import("../types").Rule} Rule
|
|
3
|
+
*/
|
|
4
|
+
|
|
1
5
|
const { RULE_CATEGORY } = require("../constants");
|
|
2
6
|
const { NamingUtils, NodeUtils } = require("./utils");
|
|
3
7
|
|
|
@@ -5,7 +9,7 @@ const MESSAGE_IDS = {
|
|
|
5
9
|
WRONG: "wrong",
|
|
6
10
|
};
|
|
7
11
|
|
|
8
|
-
const
|
|
12
|
+
const CONVENTIONS = {
|
|
9
13
|
CAMEL_CASE: "camelCase",
|
|
10
14
|
SNAKE_CASE: "snake_case",
|
|
11
15
|
PASCAL_CASE: "PascalCase",
|
|
@@ -13,12 +17,15 @@ const CONEVNTIONS = {
|
|
|
13
17
|
};
|
|
14
18
|
|
|
15
19
|
const CONVENTION_CHECKERS = {
|
|
16
|
-
[
|
|
17
|
-
[
|
|
18
|
-
[
|
|
19
|
-
[
|
|
20
|
+
[CONVENTIONS.CAMEL_CASE]: NamingUtils.isCamelCase,
|
|
21
|
+
[CONVENTIONS.SNAKE_CASE]: NamingUtils.isSnakeCase,
|
|
22
|
+
[CONVENTIONS.PASCAL_CASE]: NamingUtils.isPascalCase,
|
|
23
|
+
[CONVENTIONS.KEBAB_CASE]: NamingUtils.isKebabCase,
|
|
20
24
|
};
|
|
21
25
|
|
|
26
|
+
/**
|
|
27
|
+
* @type {Rule}
|
|
28
|
+
*/
|
|
22
29
|
module.exports = {
|
|
23
30
|
meta: {
|
|
24
31
|
type: "code",
|
|
@@ -32,7 +39,7 @@ module.exports = {
|
|
|
32
39
|
fixable: null,
|
|
33
40
|
schema: [
|
|
34
41
|
{
|
|
35
|
-
enum: Object.values(
|
|
42
|
+
enum: Object.values(CONVENTIONS),
|
|
36
43
|
},
|
|
37
44
|
],
|
|
38
45
|
messages: {
|
|
@@ -45,14 +52,14 @@ module.exports = {
|
|
|
45
52
|
const convention =
|
|
46
53
|
context.options && context.options.length
|
|
47
54
|
? context.options[0]
|
|
48
|
-
:
|
|
55
|
+
: CONVENTIONS.SNAKE_CASE;
|
|
49
56
|
|
|
50
|
-
const
|
|
57
|
+
const checkNaming = CONVENTION_CHECKERS[convention];
|
|
51
58
|
|
|
52
59
|
return {
|
|
53
60
|
"*"(node) {
|
|
54
61
|
const idAttr = NodeUtils.findAttr(node, "id");
|
|
55
|
-
if (idAttr && idAttr.value && !
|
|
62
|
+
if (idAttr && idAttr.value && !checkNaming(idAttr.value)) {
|
|
56
63
|
context.report({
|
|
57
64
|
node: idAttr,
|
|
58
65
|
data: {
|
package/lib/rules/indent.js
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
|
-
// @ts-check
|
|
2
1
|
/**
|
|
3
|
-
* @typedef {import("../types").
|
|
2
|
+
* @typedef {import("../types").Rule} Rule
|
|
3
|
+
* @typedef {import("../types").ElementNode} ElementNode
|
|
4
4
|
* @typedef {import("../types").AttrNode} AttrNode
|
|
5
|
+
* @typedef {import("../types").TagNode} TagNode
|
|
6
|
+
* @typedef {import("../types").AnyNode} AnyNode
|
|
5
7
|
* @typedef {import("../types").BaseNode} BaseNode
|
|
6
8
|
* @typedef {Object} IndentType
|
|
7
9
|
* @property {"tab"} TAB
|
|
@@ -12,6 +14,7 @@
|
|
|
12
14
|
*/
|
|
13
15
|
|
|
14
16
|
const { RULE_CATEGORY, NODE_TYPES } = require("../constants");
|
|
17
|
+
const { NodeUtils } = require("./utils");
|
|
15
18
|
|
|
16
19
|
/** @type {MessageId} */
|
|
17
20
|
const MESSAGE_ID = {
|
|
@@ -31,6 +34,9 @@ const IGNORING_NODES = [
|
|
|
31
34
|
NODE_TYPES.XMP,
|
|
32
35
|
];
|
|
33
36
|
|
|
37
|
+
/**
|
|
38
|
+
* @type {Rule}
|
|
39
|
+
*/
|
|
34
40
|
module.exports = {
|
|
35
41
|
meta: {
|
|
36
42
|
type: "code",
|
|
@@ -60,7 +66,6 @@ module.exports = {
|
|
|
60
66
|
"Expected indentation of {{expected}} but found {{actual}}.",
|
|
61
67
|
},
|
|
62
68
|
},
|
|
63
|
-
|
|
64
69
|
create(context) {
|
|
65
70
|
const sourceCode = context.getSourceCode();
|
|
66
71
|
const indentLevel = new IndentLevel();
|
|
@@ -79,7 +84,7 @@ module.exports = {
|
|
|
79
84
|
|
|
80
85
|
/**
|
|
81
86
|
* @param {BaseNode} node
|
|
82
|
-
* @param {
|
|
87
|
+
* @param {BaseNode} [nodeToReport]
|
|
83
88
|
*/
|
|
84
89
|
function checkIndent(node, nodeToReport) {
|
|
85
90
|
const codeBefore = getLineCodeBefore(node);
|
|
@@ -133,7 +138,7 @@ module.exports = {
|
|
|
133
138
|
}
|
|
134
139
|
|
|
135
140
|
/**
|
|
136
|
-
* @param {
|
|
141
|
+
* @param {AnyNode} startTag
|
|
137
142
|
*/
|
|
138
143
|
function checkEndOfStartTag(startTag) {
|
|
139
144
|
const start = startTag.range[1] - 1;
|
|
@@ -160,7 +165,7 @@ module.exports = {
|
|
|
160
165
|
let nodesToIgnoreChildren = [];
|
|
161
166
|
return {
|
|
162
167
|
/**
|
|
163
|
-
* @param {
|
|
168
|
+
* @param {ElementNode} node
|
|
164
169
|
*/
|
|
165
170
|
"*"(node) {
|
|
166
171
|
if (IGNORING_NODES.includes(node.type)) {
|
|
@@ -186,7 +191,11 @@ module.exports = {
|
|
|
186
191
|
}
|
|
187
192
|
});
|
|
188
193
|
|
|
189
|
-
if (
|
|
194
|
+
if (
|
|
195
|
+
(NodeUtils.isTextNode(node) || NodeUtils.isCommentNode(node)) &&
|
|
196
|
+
node.lineNodes &&
|
|
197
|
+
node.lineNodes.length
|
|
198
|
+
) {
|
|
190
199
|
if (!node.startTag) {
|
|
191
200
|
indentLevel.down();
|
|
192
201
|
}
|
|
@@ -219,6 +228,9 @@ module.exports = {
|
|
|
219
228
|
};
|
|
220
229
|
|
|
221
230
|
function getIndentTypeAndSize(options) {
|
|
231
|
+
/**
|
|
232
|
+
* @type {IndentType['SPACE'] | IndentType['TAB']}
|
|
233
|
+
*/
|
|
222
234
|
let indentType = INDENT_TYPES.SPACE;
|
|
223
235
|
let indentSize = 4;
|
|
224
236
|
if (options.length) {
|
package/lib/rules/index.js
CHANGED
|
@@ -12,7 +12,7 @@ const indent = require("./indent");
|
|
|
12
12
|
const requireLiContainer = require("./require-li-container");
|
|
13
13
|
const quotes = require("./quotes");
|
|
14
14
|
const idNamingConvention = require("./id-naming-convention");
|
|
15
|
-
const
|
|
15
|
+
const noObsoleteTags = require("./no-obsolete-tags");
|
|
16
16
|
const requireClosingTags = require("./require-closing-tags");
|
|
17
17
|
const requireMetaDescription = require("./require-meta-description");
|
|
18
18
|
const requireFrameTitle = require("./require-frame-title");
|
|
@@ -21,6 +21,12 @@ const noPositiveTabindex = require("./no-positive-tabindex");
|
|
|
21
21
|
const requireMetaViewport = require("./require-meta-viewport");
|
|
22
22
|
const requireMetaCharset = require("./require-meta-charset");
|
|
23
23
|
const noTargetBlank = require("./no-target-blank");
|
|
24
|
+
const noDuplicateAttrs = require("./no-duplicate-attrs");
|
|
25
|
+
const noAbstractRoles = require("./no-abstract-roles");
|
|
26
|
+
const requireButtonType = require("./require-button-type");
|
|
27
|
+
const noAriaHiddenBody = require("./no-aria-hidden-body");
|
|
28
|
+
const noMultipleEmptyLines = require("./no-multiple-empty-lines");
|
|
29
|
+
const noAccesskeyAttrs = require("./no-accesskey-attrs");
|
|
24
30
|
|
|
25
31
|
module.exports = {
|
|
26
32
|
"require-lang": requireLang,
|
|
@@ -37,7 +43,7 @@ module.exports = {
|
|
|
37
43
|
indent: indent,
|
|
38
44
|
quotes: quotes,
|
|
39
45
|
"id-naming-convention": idNamingConvention,
|
|
40
|
-
"no-obsolete-tags":
|
|
46
|
+
"no-obsolete-tags": noObsoleteTags,
|
|
41
47
|
"require-closing-tags": requireClosingTags,
|
|
42
48
|
"require-meta-description": requireMetaDescription,
|
|
43
49
|
"require-frame-title": requireFrameTitle,
|
|
@@ -46,4 +52,10 @@ module.exports = {
|
|
|
46
52
|
"require-meta-viewport": requireMetaViewport,
|
|
47
53
|
"require-meta-charset": requireMetaCharset,
|
|
48
54
|
"no-target-blank": noTargetBlank,
|
|
55
|
+
"no-duplicate-attrs": noDuplicateAttrs,
|
|
56
|
+
"no-abstract-roles": noAbstractRoles,
|
|
57
|
+
"require-button-type": requireButtonType,
|
|
58
|
+
"no-aria-hidden-body": noAriaHiddenBody,
|
|
59
|
+
"no-multiple-empty-lines": noMultipleEmptyLines,
|
|
60
|
+
"no-accesskey-attrs": noAccesskeyAttrs,
|
|
49
61
|
};
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @typedef {import("../types").Rule} Rule
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
const { RULE_CATEGORY } = require("../constants");
|
|
6
|
+
const { NodeUtils } = require("./utils");
|
|
7
|
+
|
|
8
|
+
const MESSAGE_IDS = {
|
|
9
|
+
UNEXPECTED: "unexpected",
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
const ABSTRACT_ROLE_SET = new Set([
|
|
13
|
+
"command",
|
|
14
|
+
"composite",
|
|
15
|
+
"input",
|
|
16
|
+
"landmark",
|
|
17
|
+
"range",
|
|
18
|
+
"roletype",
|
|
19
|
+
"section",
|
|
20
|
+
"sectionhead",
|
|
21
|
+
"select",
|
|
22
|
+
"structure",
|
|
23
|
+
"widget",
|
|
24
|
+
"window",
|
|
25
|
+
]);
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* @type {Rule}
|
|
29
|
+
*/
|
|
30
|
+
module.exports = {
|
|
31
|
+
meta: {
|
|
32
|
+
type: "code",
|
|
33
|
+
|
|
34
|
+
docs: {
|
|
35
|
+
description: "Disallow to use of abstract roles",
|
|
36
|
+
category: RULE_CATEGORY.ACCESSIBILITY,
|
|
37
|
+
recommended: false,
|
|
38
|
+
},
|
|
39
|
+
|
|
40
|
+
fixable: null,
|
|
41
|
+
schema: [],
|
|
42
|
+
messages: {
|
|
43
|
+
[MESSAGE_IDS.UNEXPECTED]: "Unexpected use of an abstract role.",
|
|
44
|
+
},
|
|
45
|
+
},
|
|
46
|
+
|
|
47
|
+
create(context) {
|
|
48
|
+
return {
|
|
49
|
+
"*"(node) {
|
|
50
|
+
const roleAttr = NodeUtils.findAttr(node, "role");
|
|
51
|
+
if (roleAttr) {
|
|
52
|
+
if (ABSTRACT_ROLE_SET.has(roleAttr.value)) {
|
|
53
|
+
context.report({
|
|
54
|
+
messageId: MESSAGE_IDS.UNEXPECTED,
|
|
55
|
+
node: roleAttr,
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
},
|
|
60
|
+
};
|
|
61
|
+
},
|
|
62
|
+
};
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @typedef {import("../types").Rule} Rule
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
const { RULE_CATEGORY } = require("../constants");
|
|
6
|
+
const { NodeUtils } = require("./utils");
|
|
7
|
+
|
|
8
|
+
const MESSAGE_IDS = {
|
|
9
|
+
UNEXPECTED: "unexpected",
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* @type {Rule}
|
|
14
|
+
*/
|
|
15
|
+
module.exports = {
|
|
16
|
+
meta: {
|
|
17
|
+
type: "code",
|
|
18
|
+
|
|
19
|
+
docs: {
|
|
20
|
+
description: "Disallow to use of accesskey attribute",
|
|
21
|
+
category: RULE_CATEGORY.ACCESSIBILITY,
|
|
22
|
+
recommended: false,
|
|
23
|
+
},
|
|
24
|
+
|
|
25
|
+
fixable: null,
|
|
26
|
+
schema: [],
|
|
27
|
+
messages: {
|
|
28
|
+
[MESSAGE_IDS.UNEXPECTED]: "Unexpected use of accesskey attribute.",
|
|
29
|
+
},
|
|
30
|
+
},
|
|
31
|
+
|
|
32
|
+
create(context) {
|
|
33
|
+
return {
|
|
34
|
+
"*"(node) {
|
|
35
|
+
const accessKeyAttr = NodeUtils.findAttr(node, "accesskey");
|
|
36
|
+
if (accessKeyAttr) {
|
|
37
|
+
context.report({
|
|
38
|
+
node: accessKeyAttr,
|
|
39
|
+
messageId: MESSAGE_IDS.UNEXPECTED,
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
},
|
|
43
|
+
};
|
|
44
|
+
},
|
|
45
|
+
};
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @typedef {import("../types").Rule} Rule
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
const { RULE_CATEGORY } = require("../constants");
|
|
6
|
+
const { NodeUtils } = require("./utils");
|
|
7
|
+
|
|
8
|
+
const MESSAGE_IDS = {
|
|
9
|
+
UNEXPECTED: "unexpected",
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* @type {Rule}
|
|
14
|
+
*/
|
|
15
|
+
module.exports = {
|
|
16
|
+
meta: {
|
|
17
|
+
type: "code",
|
|
18
|
+
|
|
19
|
+
docs: {
|
|
20
|
+
description:
|
|
21
|
+
"Disallow to use aria-hidden attributes on the `body` element.",
|
|
22
|
+
category: RULE_CATEGORY.ACCESSIBILITY,
|
|
23
|
+
recommended: false,
|
|
24
|
+
},
|
|
25
|
+
|
|
26
|
+
fixable: null,
|
|
27
|
+
schema: [],
|
|
28
|
+
messages: {
|
|
29
|
+
[MESSAGE_IDS.UNEXPECTED]: "Unexpected aria-hidden on body tag.",
|
|
30
|
+
},
|
|
31
|
+
},
|
|
32
|
+
|
|
33
|
+
create(context) {
|
|
34
|
+
return {
|
|
35
|
+
Body(node) {
|
|
36
|
+
const ariaHiddenAttr = NodeUtils.findAttr(node, "aria-hidden");
|
|
37
|
+
if (ariaHiddenAttr && ariaHiddenAttr.value !== "false") {
|
|
38
|
+
context.report({
|
|
39
|
+
node: ariaHiddenAttr,
|
|
40
|
+
messageId: MESSAGE_IDS.UNEXPECTED,
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
},
|
|
44
|
+
};
|
|
45
|
+
},
|
|
46
|
+
};
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @typedef {import("../types").Rule} Rule
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
const { RULE_CATEGORY } = require("../constants");
|
|
6
|
+
|
|
7
|
+
const MESSAGE_IDS = {
|
|
8
|
+
DUPLICATE_ATTRS: "duplicateAttrs",
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* @type {Rule}
|
|
13
|
+
*/
|
|
14
|
+
module.exports = {
|
|
15
|
+
meta: {
|
|
16
|
+
type: "code",
|
|
17
|
+
|
|
18
|
+
docs: {
|
|
19
|
+
description: "Disallow to use duplicate attributes",
|
|
20
|
+
category: RULE_CATEGORY.BEST_PRACTICE,
|
|
21
|
+
recommended: true,
|
|
22
|
+
},
|
|
23
|
+
|
|
24
|
+
fixable: null,
|
|
25
|
+
schema: [],
|
|
26
|
+
messages: {
|
|
27
|
+
[MESSAGE_IDS.DUPLICATE_ATTRS]:
|
|
28
|
+
"The attribute '{{attrName}}' is duplicated.",
|
|
29
|
+
},
|
|
30
|
+
},
|
|
31
|
+
|
|
32
|
+
create(context) {
|
|
33
|
+
return {
|
|
34
|
+
"*"(node) {
|
|
35
|
+
if (Array.isArray(node.attrs)) {
|
|
36
|
+
const attrsSet = new Set();
|
|
37
|
+
node.attrs.forEach((attr) => {
|
|
38
|
+
if (attrsSet.has(attr.name)) {
|
|
39
|
+
context.report({
|
|
40
|
+
node: node.startTag,
|
|
41
|
+
data: {
|
|
42
|
+
attrName: attr.name,
|
|
43
|
+
},
|
|
44
|
+
messageId: MESSAGE_IDS.DUPLICATE_ATTRS,
|
|
45
|
+
});
|
|
46
|
+
} else {
|
|
47
|
+
attrsSet.add(attr.name);
|
|
48
|
+
}
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
},
|
|
52
|
+
};
|
|
53
|
+
},
|
|
54
|
+
};
|
|
@@ -1,3 +1,7 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @typedef {import("../types").Rule} Rule
|
|
3
|
+
*/
|
|
4
|
+
|
|
1
5
|
const { RULE_CATEGORY } = require("../constants");
|
|
2
6
|
const { NodeUtils } = require("./utils");
|
|
3
7
|
|
|
@@ -5,6 +9,9 @@ const MESSAGE_IDS = {
|
|
|
5
9
|
DUPLICATE_ID: "duplicateId",
|
|
6
10
|
};
|
|
7
11
|
|
|
12
|
+
/**
|
|
13
|
+
* @type {Rule}
|
|
14
|
+
*/
|
|
8
15
|
module.exports = {
|
|
9
16
|
meta: {
|
|
10
17
|
type: "code",
|
|
@@ -1,11 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @typedef {import("../types").Rule} Rule
|
|
3
|
+
* @typedef {import("../types").AttrNode} AttrNode
|
|
4
|
+
* @typedef {import("../types").TagNode} TagNode
|
|
5
|
+
* @typedef {import("../types").ElementNode} ElementNode
|
|
6
|
+
*/
|
|
7
|
+
|
|
1
8
|
const { RULE_CATEGORY } = require("../constants");
|
|
2
9
|
|
|
3
10
|
const MESSAGE_IDS = {
|
|
4
|
-
EXTRA_BETWEEN: "
|
|
5
|
-
EXTRA_AFTER: "
|
|
6
|
-
EXTRA_BEFORE: "
|
|
11
|
+
EXTRA_BETWEEN: "unexpectedBetween",
|
|
12
|
+
EXTRA_AFTER: "unexpectedAfter",
|
|
13
|
+
EXTRA_BEFORE: "unexpectedBefore",
|
|
7
14
|
};
|
|
8
15
|
|
|
16
|
+
/**
|
|
17
|
+
* @type {Rule}
|
|
18
|
+
*/
|
|
9
19
|
module.exports = {
|
|
10
20
|
meta: {
|
|
11
21
|
type: "code",
|
|
@@ -25,6 +35,9 @@ module.exports = {
|
|
|
25
35
|
},
|
|
26
36
|
},
|
|
27
37
|
create(context) {
|
|
38
|
+
/**
|
|
39
|
+
* @param {AttrNode[]} attrs
|
|
40
|
+
*/
|
|
28
41
|
function checkExtraSpacesBetweenAttrs(attrs) {
|
|
29
42
|
attrs.forEach((current, index, attrs) => {
|
|
30
43
|
if (index >= attrs.length - 1) {
|
|
@@ -50,9 +63,13 @@ module.exports = {
|
|
|
50
63
|
}
|
|
51
64
|
});
|
|
52
65
|
}
|
|
66
|
+
/**
|
|
67
|
+
* @param {TagNode} startTag
|
|
68
|
+
* @param {AttrNode} lastAttr
|
|
69
|
+
*/
|
|
53
70
|
function checkExtraSpaceAfter(startTag, lastAttr) {
|
|
54
71
|
if (startTag.loc.end.line !== lastAttr.loc.end.line) {
|
|
55
|
-
// skip the attribute on the
|
|
72
|
+
// skip the attribute on the different line with the start tag
|
|
56
73
|
return;
|
|
57
74
|
}
|
|
58
75
|
const spacesBetween = startTag.loc.end.column - lastAttr.loc.end.column;
|
|
@@ -72,9 +89,14 @@ module.exports = {
|
|
|
72
89
|
});
|
|
73
90
|
}
|
|
74
91
|
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* @param {ElementNode} node
|
|
95
|
+
* @param {AttrNode} firstAttr
|
|
96
|
+
*/
|
|
75
97
|
function checkExtraSpaceBefore(node, firstAttr) {
|
|
76
98
|
if (node.loc.start.line !== firstAttr.loc.start.line) {
|
|
77
|
-
// skip the attribute on the
|
|
99
|
+
// skip the attribute on the different line with the start tag
|
|
78
100
|
return;
|
|
79
101
|
}
|
|
80
102
|
const nodeLength = node.tagName.length;
|
|
@@ -1,3 +1,7 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @typedef {import("../types").Rule} Rule
|
|
3
|
+
*/
|
|
4
|
+
|
|
1
5
|
const { RULE_CATEGORY } = require("../constants");
|
|
2
6
|
const { NodeUtils } = require("./utils");
|
|
3
7
|
|
|
@@ -5,6 +9,9 @@ const MESSAGE_IDS = {
|
|
|
5
9
|
INLINE_STYLE: "unexpectedInlineStyle",
|
|
6
10
|
};
|
|
7
11
|
|
|
12
|
+
/**
|
|
13
|
+
* @type {Rule}
|
|
14
|
+
*/
|
|
8
15
|
module.exports = {
|
|
9
16
|
meta: {
|
|
10
17
|
type: "code",
|