@html-eslint/eslint-plugin 0.31.1 → 0.33.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.
Files changed (135) hide show
  1. package/lib/rules/attrs-newline.js +3 -5
  2. package/lib/rules/element-newline.js +11 -9
  3. package/lib/rules/id-naming-convention.js +5 -5
  4. package/lib/rules/indent/indent.js +121 -69
  5. package/lib/rules/index.js +20 -0
  6. package/lib/rules/lowercase.js +7 -8
  7. package/lib/rules/max-element-depth.js +96 -0
  8. package/lib/rules/no-abstract-roles.js +4 -4
  9. package/lib/rules/no-accesskey-attrs.js +4 -4
  10. package/lib/rules/no-duplicate-attrs.js +4 -4
  11. package/lib/rules/no-duplicate-id.js +7 -7
  12. package/lib/rules/no-extra-spacing-attrs.js +14 -14
  13. package/lib/rules/no-extra-spacing-text.js +9 -10
  14. package/lib/rules/no-heading-inside-button.js +54 -0
  15. package/lib/rules/no-inline-styles.js +0 -1
  16. package/lib/rules/no-invalid-role.js +287 -0
  17. package/lib/rules/no-multiple-empty-lines.js +5 -5
  18. package/lib/rules/no-multiple-h1.js +2 -2
  19. package/lib/rules/no-nested-interactive.js +120 -0
  20. package/lib/rules/no-positive-tabindex.js +4 -4
  21. package/lib/rules/no-restricted-attr-values.js +6 -6
  22. package/lib/rules/no-restricted-attrs.js +6 -6
  23. package/lib/rules/no-script-style-type.js +4 -4
  24. package/lib/rules/no-skip-heading-levels.js +2 -2
  25. package/lib/rules/no-trailing-spaces.js +3 -3
  26. package/lib/rules/prefer-https.js +106 -0
  27. package/lib/rules/quotes.js +8 -8
  28. package/lib/rules/require-attrs.js +14 -6
  29. package/lib/rules/require-closing-tags.js +4 -4
  30. package/lib/rules/require-explicit-size.js +122 -0
  31. package/lib/rules/require-form-method.js +81 -0
  32. package/lib/rules/require-img-alt.js +2 -3
  33. package/lib/rules/require-input-label.js +77 -0
  34. package/lib/rules/require-meta-charset.js +3 -3
  35. package/lib/rules/require-meta-description.js +3 -3
  36. package/lib/rules/require-meta-viewport.js +3 -3
  37. package/lib/rules/require-open-graph-protocol.js +3 -3
  38. package/lib/rules/require-title.js +5 -5
  39. package/lib/rules/sort-attrs.js +12 -12
  40. package/lib/rules/utils/node.js +72 -32
  41. package/lib/rules/utils/settings.js +2 -2
  42. package/lib/rules/utils/visitors.js +1 -1
  43. package/lib/types/ast.d.ts +204 -0
  44. package/lib/types/index.d.ts +3 -0
  45. package/lib/types/rule.d.ts +83 -0
  46. package/lib/types/settings.ts +13 -0
  47. package/package.json +4 -4
  48. package/types/rules/attrs-newline.d.ts +2 -3
  49. package/types/rules/attrs-newline.d.ts.map +1 -1
  50. package/types/rules/element-newline.d.ts +8 -9
  51. package/types/rules/element-newline.d.ts.map +1 -1
  52. package/types/rules/id-naming-convention.d.ts +4 -4
  53. package/types/rules/id-naming-convention.d.ts.map +1 -1
  54. package/types/rules/indent/indent.d.ts +12 -5
  55. package/types/rules/indent/indent.d.ts.map +1 -1
  56. package/types/rules/index.d.ts +8 -0
  57. package/types/rules/lowercase.d.ts +4 -5
  58. package/types/rules/lowercase.d.ts.map +1 -1
  59. package/types/rules/max-element-depth.d.ts +10 -0
  60. package/types/rules/max-element-depth.d.ts.map +1 -0
  61. package/types/rules/no-abstract-roles.d.ts +4 -4
  62. package/types/rules/no-abstract-roles.d.ts.map +1 -1
  63. package/types/rules/no-accesskey-attrs.d.ts +4 -4
  64. package/types/rules/no-accesskey-attrs.d.ts.map +1 -1
  65. package/types/rules/no-duplicate-attrs.d.ts +4 -4
  66. package/types/rules/no-duplicate-attrs.d.ts.map +1 -1
  67. package/types/rules/no-duplicate-id.d.ts +5 -5
  68. package/types/rules/no-duplicate-id.d.ts.map +1 -1
  69. package/types/rules/no-extra-spacing-attrs.d.ts +11 -11
  70. package/types/rules/no-extra-spacing-attrs.d.ts.map +1 -1
  71. package/types/rules/no-extra-spacing-text.d.ts +7 -8
  72. package/types/rules/no-extra-spacing-text.d.ts.map +1 -1
  73. package/types/rules/no-heading-inside-button.d.ts +7 -0
  74. package/types/rules/no-heading-inside-button.d.ts.map +1 -0
  75. package/types/rules/no-inline-styles.d.ts +1 -2
  76. package/types/rules/no-inline-styles.d.ts.map +1 -1
  77. package/types/rules/no-invalid-role.d.ts +7 -0
  78. package/types/rules/no-invalid-role.d.ts.map +1 -0
  79. package/types/rules/no-multiple-empty-lines.d.ts +5 -5
  80. package/types/rules/no-multiple-empty-lines.d.ts.map +1 -1
  81. package/types/rules/no-multiple-h1.d.ts +2 -2
  82. package/types/rules/no-multiple-h1.d.ts.map +1 -1
  83. package/types/rules/no-nested-interactive.d.ts +8 -0
  84. package/types/rules/no-nested-interactive.d.ts.map +1 -0
  85. package/types/rules/no-positive-tabindex.d.ts +4 -4
  86. package/types/rules/no-positive-tabindex.d.ts.map +1 -1
  87. package/types/rules/no-restricted-attr-values.d.ts +5 -5
  88. package/types/rules/no-restricted-attr-values.d.ts.map +1 -1
  89. package/types/rules/no-restricted-attrs.d.ts +5 -5
  90. package/types/rules/no-restricted-attrs.d.ts.map +1 -1
  91. package/types/rules/no-script-style-type.d.ts +4 -4
  92. package/types/rules/no-script-style-type.d.ts.map +1 -1
  93. package/types/rules/no-skip-heading-levels.d.ts +2 -2
  94. package/types/rules/no-skip-heading-levels.d.ts.map +1 -1
  95. package/types/rules/no-trailing-spaces.d.ts +3 -3
  96. package/types/rules/no-trailing-spaces.d.ts.map +1 -1
  97. package/types/rules/prefer-https.d.ts +11 -0
  98. package/types/rules/prefer-https.d.ts.map +1 -0
  99. package/types/rules/quotes.d.ts +6 -6
  100. package/types/rules/quotes.d.ts.map +1 -1
  101. package/types/rules/require-attrs.d.ts +9 -4
  102. package/types/rules/require-attrs.d.ts.map +1 -1
  103. package/types/rules/require-closing-tags.d.ts +2 -2
  104. package/types/rules/require-closing-tags.d.ts.map +1 -1
  105. package/types/rules/require-explicit-size.d.ts +9 -0
  106. package/types/rules/require-explicit-size.d.ts.map +1 -0
  107. package/types/rules/require-form-method.d.ts +7 -0
  108. package/types/rules/require-form-method.d.ts.map +1 -0
  109. package/types/rules/require-img-alt.d.ts +2 -2
  110. package/types/rules/require-img-alt.d.ts.map +1 -1
  111. package/types/rules/require-input-label.d.ts +8 -0
  112. package/types/rules/require-input-label.d.ts.map +1 -0
  113. package/types/rules/require-meta-charset.d.ts +3 -3
  114. package/types/rules/require-meta-charset.d.ts.map +1 -1
  115. package/types/rules/require-meta-description.d.ts +3 -3
  116. package/types/rules/require-meta-description.d.ts.map +1 -1
  117. package/types/rules/require-meta-viewport.d.ts +3 -3
  118. package/types/rules/require-meta-viewport.d.ts.map +1 -1
  119. package/types/rules/require-open-graph-protocol.d.ts +3 -3
  120. package/types/rules/require-open-graph-protocol.d.ts.map +1 -1
  121. package/types/rules/require-title.d.ts +4 -4
  122. package/types/rules/require-title.d.ts.map +1 -1
  123. package/types/rules/sort-attrs.d.ts +4 -4
  124. package/types/rules/sort-attrs.d.ts.map +1 -1
  125. package/types/rules/utils/node.d.ts +56 -37
  126. package/types/rules/utils/node.d.ts.map +1 -1
  127. package/types/rules/utils/settings.d.ts +2 -2
  128. package/types/rules/utils/settings.d.ts.map +1 -1
  129. package/types/rules/utils/source-code.d.ts +1 -1
  130. package/types/rules/utils/source-code.d.ts.map +1 -1
  131. package/types/rules/utils/visitors.d.ts +1 -1
  132. package/types/rules/utils/visitors.d.ts.map +1 -1
  133. package/types/types/settings.d.ts +13 -0
  134. package/types/types/settings.d.ts.map +1 -0
  135. package/lib/types.d.ts +0 -289
@@ -1,8 +1,8 @@
1
1
  /**
2
2
  * @typedef { import("../types").RuleModule } RuleModule
3
- * @typedef { import("../types").StyleTagNode } StyleTagNode
4
- * @typedef { import("../types").TagNode } TagNode
5
- * @typedef { import("../types").ScriptTagNode } ScriptTagNode
3
+ * @typedef { import("../types").StyleTag } StyleTag
4
+ * @typedef { import("../types").Tag } Tag
5
+ * @typedef { import("../types").ScriptTag } ScriptTag
6
6
  */
7
7
 
8
8
  const { RULE_CATEGORY } = require("../constants");
@@ -37,7 +37,7 @@ module.exports = {
37
37
  create(context) {
38
38
  /**
39
39
  *
40
- * @param {ScriptTagNode | TagNode | StyleTagNode} node
40
+ * @param {ScriptTag | Tag | StyleTag} node
41
41
  * @param {string} unnecessaryValue
42
42
  */
43
43
  function check(node, unnecessaryValue) {
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * @typedef { import("../types").RuleModule } RuleModule
3
- * @typedef { import("../types").TagNode } TagNode
3
+ * @typedef { import("../types").Tag } Tag
4
4
  */
5
5
 
6
6
  const { RULE_CATEGORY } = require("../constants");
@@ -32,7 +32,7 @@ module.exports = {
32
32
 
33
33
  create(context) {
34
34
  /**
35
- * @type {{node: TagNode; level: number}[]}
35
+ * @type {{node: Tag; level: number}[]}
36
36
  */
37
37
  const headings = [];
38
38
 
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * @typedef { import("../types").RuleModule } RuleModule
3
- * @typedef { import("es-html-parser").CommentContentNode } CommentContentNode
4
- * @typedef { import("es-html-parser").TextNode } TextNode
3
+ * @typedef { import("../types").CommentContent } CommentContent
4
+ * @typedef { import("../types").Text } Text
5
5
  */
6
6
 
7
7
  const { parse } = require("@html-eslint/template-parser");
@@ -45,7 +45,7 @@ module.exports = {
45
45
  * @param {string} source
46
46
  * @param {string[]} lines
47
47
  * @param {number} rangeOffset
48
- * @param {((CommentContentNode | TextNode)['templates'][number])[]} tokens
48
+ * @param {((CommentContent | Text)['templates'][number])[]} tokens
49
49
  */
50
50
  function check(source, lines, rangeOffset, tokens) {
51
51
  let rangeIndex = rangeOffset;
@@ -0,0 +1,106 @@
1
+ /**
2
+ * @typedef { import("../types").RuleModule } RuleModule
3
+ * @typedef { import("../types").Tag } Tag
4
+ * @typedef { import("../types").ScriptTag } ScriptTag
5
+ * @typedef { import("../types").Attribute } Attribute
6
+ * @typedef { import("../types").AttributeValue } AttributeValue
7
+ */
8
+
9
+ const { RULE_CATEGORY } = require("../constants");
10
+ const { findAttr, isScript } = require("./utils/node");
11
+ const { createVisitors } = require("./utils/visitors");
12
+
13
+ const MESSAGE_IDS = {
14
+ UNEXPECTED: "unexpected",
15
+ };
16
+
17
+ /**
18
+ * @param {string} url
19
+ */
20
+ function getProtocol(url) {
21
+ try {
22
+ return new URL(url).protocol;
23
+ } catch (e) {
24
+ return null;
25
+ }
26
+ }
27
+
28
+ /**
29
+ * @param {Tag | ScriptTag} node
30
+ * @returns {AttributeValue | undefined}
31
+ */
32
+ function getResourceAttributeValue(node) {
33
+ /**
34
+ * @type {Attribute | undefined}
35
+ */
36
+ let attribute;
37
+ if (isScript(node)) {
38
+ attribute = findAttr(node, "src");
39
+ } else {
40
+ switch (node.name.toLowerCase()) {
41
+ case "img":
42
+ case "iframe":
43
+ case "audio":
44
+ case "video":
45
+ case "source":
46
+ case "embed": {
47
+ attribute = findAttr(node, "src");
48
+ break;
49
+ }
50
+ case "link": {
51
+ attribute = findAttr(node, "href");
52
+ break;
53
+ }
54
+ case "object": {
55
+ attribute = findAttr(node, "data");
56
+ break;
57
+ }
58
+ }
59
+ }
60
+ if (attribute) {
61
+ return attribute.value;
62
+ }
63
+ return undefined;
64
+ }
65
+
66
+ /**
67
+ * @type {RuleModule}
68
+ */
69
+ module.exports = {
70
+ meta: {
71
+ type: "code",
72
+ docs: {
73
+ description: "Prefer to use HTTPS for embedded resources",
74
+ recommended: false,
75
+ category: RULE_CATEGORY.BEST_PRACTICE,
76
+ },
77
+ fixable: false,
78
+ schema: [],
79
+ messages: {
80
+ [MESSAGE_IDS.UNEXPECTED]: "Unexpected use 'HTTP' protocol.",
81
+ },
82
+ },
83
+
84
+ create(context) {
85
+ /**
86
+ * @param {Tag | ScriptTag} node
87
+ */
88
+ function check(node) {
89
+ const attributeValue = getResourceAttributeValue(node);
90
+ if (attributeValue && !attributeValue.templates.length) {
91
+ const protocol = getProtocol(attributeValue.value);
92
+ if (protocol === "http:") {
93
+ context.report({
94
+ node: attributeValue,
95
+ messageId: MESSAGE_IDS.UNEXPECTED,
96
+ });
97
+ }
98
+ }
99
+ }
100
+
101
+ return createVisitors(context, {
102
+ ScriptTag: check,
103
+ Tag: check,
104
+ });
105
+ },
106
+ };
@@ -1,10 +1,10 @@
1
1
  /**
2
+ * @typedef { import("eslint").AST.Range } Range
2
3
  * @typedef { import("../types").RuleModule } RuleModule
3
- * @typedef { import("../types").Range } Range
4
- * @typedef { import("../types").AttributeNode } AttributeNode
5
- * @typedef { import("../types").TagNode } TagNode
6
- * @typedef { import("../types").ScriptTagNode } ScriptTagNode
7
- * @typedef { import("../types").StyleTagNode } StyleTagNode
4
+ * @typedef { import("../types").Attribute } Attribute
5
+ * @typedef { import("../types").Tag } Tag
6
+ * @typedef { import("../types").ScriptTag } ScriptTag
7
+ * @typedef { import("../types").StyleTag } StyleTag
8
8
  */
9
9
 
10
10
  const { RULE_CATEGORY } = require("../constants");
@@ -68,7 +68,7 @@ module.exports = {
68
68
 
69
69
  /**
70
70
  *
71
- * @param {AttributeNode} attr
71
+ * @param {Attribute} attr
72
72
  * @returns {[string, string]}
73
73
  */
74
74
  function getQuotes(attr) {
@@ -77,7 +77,7 @@ module.exports = {
77
77
  }
78
78
 
79
79
  /**
80
- * @param {AttributeNode} attr
80
+ * @param {Attribute} attr
81
81
  * @returns {void}
82
82
  */
83
83
  function checkQuotes(attr) {
@@ -133,7 +133,7 @@ module.exports = {
133
133
  }
134
134
  }
135
135
  /**
136
- * @param {TagNode | ScriptTagNode | StyleTagNode} node
136
+ * @param {Tag | ScriptTag | StyleTag} node
137
137
  */
138
138
  function check(node) {
139
139
  node.attributes.forEach((attr) => checkQuotes(attr));
@@ -1,8 +1,13 @@
1
1
  /**
2
2
  * @typedef { import("../types").RuleModule } RuleModule
3
- * @typedef { import("../types").TagNode } TagNode
4
- * @typedef { import("../types").ScriptTagNode } ScriptTagNode
5
- * @typedef { import("../types").StyleTagNode } StyleTagNode
3
+ * @typedef { import("../types").Tag } Tag
4
+ * @typedef { import("../types").ScriptTag } ScriptTag
5
+ * @typedef { import("../types").StyleTag } StyleTag
6
+ *
7
+ * @typedef {Object} Option
8
+ * @property {string} tag
9
+ * @property {string} attr
10
+ * @property {string | undefined} value
6
11
  */
7
12
 
8
13
  const { NODE_TYPES } = require("@html-eslint/parser");
@@ -48,6 +53,9 @@ module.exports = {
48
53
  },
49
54
 
50
55
  create(context) {
56
+ /**
57
+ * @type {Option[]}
58
+ */
51
59
  const options = context.options || [];
52
60
  /**
53
61
  * @type {Map<string, { tag: string, attr: string, value?: string}[]>}
@@ -67,7 +75,7 @@ module.exports = {
67
75
  });
68
76
 
69
77
  /**
70
- * @param {StyleTagNode | ScriptTagNode | TagNode} node
78
+ * @param {StyleTag | ScriptTag | Tag} node
71
79
  * @param {string} tagName
72
80
  */
73
81
  function check(node, tagName) {
@@ -105,7 +113,7 @@ module.exports = {
105
113
  }
106
114
 
107
115
  /**
108
- * @param {StyleTagNode | ScriptTagNode} node
116
+ * @param {StyleTag | ScriptTag} node
109
117
  */
110
118
  function checkStyleOrScript(node) {
111
119
  const tagName = node.type === NODE_TYPES.StyleTag ? "style" : "script";
@@ -116,7 +124,7 @@ module.exports = {
116
124
  }
117
125
 
118
126
  /**
119
- * @param {TagNode} node
127
+ * @param {Tag} node
120
128
  */
121
129
  function checkTag(node) {
122
130
  const tagName = node.name.toLowerCase();
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * @typedef { import("../types").RuleModule } RuleModule
3
- * @typedef { import("../types").TagNode } TagNode
3
+ * @typedef { import("../types").Tag } Tag
4
4
  */
5
5
 
6
6
  const { RULE_CATEGORY, VOID_ELEMENTS } = require("../constants");
@@ -70,7 +70,7 @@ module.exports = {
70
70
  );
71
71
 
72
72
  /**
73
- * @param {TagNode} node
73
+ * @param {Tag} node
74
74
  */
75
75
  function checkClosingTag(node) {
76
76
  if (!node.close) {
@@ -85,7 +85,7 @@ module.exports = {
85
85
  }
86
86
 
87
87
  /**
88
- * @param {TagNode} node
88
+ * @param {Tag} node
89
89
  * @param {boolean} shouldSelfClose
90
90
  * @param {boolean} fixable
91
91
  */
@@ -150,7 +150,7 @@ module.exports = {
150
150
  if (["svg", "math"].includes(node.name)) foreignContext.push(node.name);
151
151
  },
152
152
  /**
153
- * @param {TagNode} node
153
+ * @param {Tag} node
154
154
  */
155
155
  "Tag:exit"(node) {
156
156
  if (node.name === foreignContext[foreignContext.length - 1]) {
@@ -0,0 +1,122 @@
1
+ /**
2
+ * @typedef { import("../types").RuleModule } RuleModule
3
+ * @typedef { import("../types").Tag } Tag
4
+ * @typedef { import("../types").AnyNode } AnyNode
5
+ */
6
+
7
+ const { RULE_CATEGORY } = require("../constants");
8
+ const { findAttr } = require("./utils/node");
9
+ const { createVisitors } = require("./utils/visitors");
10
+
11
+ const MESSAGE_IDS = {
12
+ MISSING_HEIGHT: "missingHeight",
13
+ MISSING_WIDTH: "missingWidth",
14
+ };
15
+
16
+ const TARGET_ELEMENTS = ["img", "iframe"];
17
+
18
+ /**
19
+ * @type {RuleModule}
20
+ */
21
+ module.exports = {
22
+ meta: {
23
+ type: "code",
24
+
25
+ docs: {
26
+ description:
27
+ "Enforces that some elements (img, iframe) have explicitly defined width and height attributes.",
28
+ category: RULE_CATEGORY.BEST_PRACTICE,
29
+ recommended: false,
30
+ },
31
+
32
+ fixable: null,
33
+ schema: [
34
+ {
35
+ type: "object",
36
+ properties: {
37
+ allowClass: {
38
+ type: "array",
39
+ items: {
40
+ type: "string",
41
+ },
42
+ },
43
+ allowId: {
44
+ type: "array",
45
+ items: {
46
+ type: "string",
47
+ },
48
+ },
49
+ },
50
+ additionalProperties: false,
51
+ },
52
+ ],
53
+ messages: {
54
+ [MESSAGE_IDS.MISSING_HEIGHT]: "Missing `width` attribute for <{{name}}>",
55
+ [MESSAGE_IDS.MISSING_WIDTH]: "Missing `height` attribute for <{{name}}>",
56
+ },
57
+ },
58
+
59
+ create(context) {
60
+ const allowClass =
61
+ (context.options &&
62
+ context.options[0] &&
63
+ context.options[0].allowClass) ||
64
+ [];
65
+ const allowId =
66
+ (context.options && context.options[0] && context.options[0].allowId) ||
67
+ [];
68
+
69
+ const allowClassSet = new Set(allowClass);
70
+ const allowIdSet = new Set(allowId);
71
+ return createVisitors(context, {
72
+ Tag(node) {
73
+ if (
74
+ !TARGET_ELEMENTS.some(
75
+ (element) => element === node.name.toLowerCase()
76
+ )
77
+ ) {
78
+ return;
79
+ }
80
+ const classAttr = findAttr(node, "class");
81
+ if (
82
+ classAttr &&
83
+ classAttr.value &&
84
+ classAttr.value.value.split(" ").some((cls) => allowClassSet.has(cls))
85
+ ) {
86
+ return;
87
+ }
88
+ const idAttr = findAttr(node, "id");
89
+ if (
90
+ idAttr &&
91
+ idAttr.value &&
92
+ idAttr.value.value.split(" ").some((id) => allowIdSet.has(id))
93
+ ) {
94
+ return;
95
+ }
96
+
97
+ const width = findAttr(node, "width");
98
+ const height = findAttr(node, "height");
99
+
100
+ if (!height || !height.value) {
101
+ context.report({
102
+ node: node.openStart,
103
+ messageId: MESSAGE_IDS.MISSING_HEIGHT,
104
+ data: {
105
+ name: node.name,
106
+ },
107
+ });
108
+ }
109
+
110
+ if (!width || !width.value) {
111
+ context.report({
112
+ node: node.openStart,
113
+ messageId: MESSAGE_IDS.MISSING_WIDTH,
114
+ data: {
115
+ name: node.name,
116
+ },
117
+ });
118
+ }
119
+ },
120
+ });
121
+ },
122
+ };
@@ -0,0 +1,81 @@
1
+ /**
2
+ * @typedef { import("../types").RuleModule } RuleModule
3
+ */
4
+
5
+ const { RULE_CATEGORY } = require("../constants");
6
+ const { findAttr } = require("./utils/node");
7
+ const { createVisitors } = require("./utils/visitors");
8
+
9
+ const MESSAGE_IDS = {
10
+ MISSING: "missing",
11
+ MISSING_VALUE: "missingValue",
12
+ UNEXPECTED: "unexpected",
13
+ };
14
+
15
+ const ALLOWED_METHODS = new Set(["GET", "POST", "DIALOG"]);
16
+
17
+ /**
18
+ * @type {RuleModule}
19
+ */
20
+ module.exports = {
21
+ meta: {
22
+ type: "code",
23
+
24
+ docs: {
25
+ description: "Require `method` attribute in `<form>`",
26
+ category: RULE_CATEGORY.ACCESSIBILITY,
27
+ recommended: false,
28
+ },
29
+
30
+ fixable: false,
31
+ schema: [],
32
+ messages: {
33
+ [MESSAGE_IDS.MISSING]: "The 'method' attribute is missing on the <form>.",
34
+ [MESSAGE_IDS.MISSING_VALUE]:
35
+ "The 'method' attribute value is missing on the <form>",
36
+ [MESSAGE_IDS.UNEXPECTED]:
37
+ "The 'method' attribute has an invalid value on the <form>.",
38
+ },
39
+ },
40
+
41
+ create(context) {
42
+ return createVisitors(context, {
43
+ Tag(node) {
44
+ if (node.name.toLowerCase() !== "form") {
45
+ return;
46
+ }
47
+ const method = findAttr(node, "method");
48
+
49
+ if (!method) {
50
+ context.report({
51
+ node: node.openStart,
52
+ messageId: MESSAGE_IDS.MISSING,
53
+ });
54
+ return;
55
+ }
56
+
57
+ if (!method.value) {
58
+ context.report({
59
+ node: node.openStart,
60
+ messageId: MESSAGE_IDS.MISSING_VALUE,
61
+ });
62
+ return;
63
+ }
64
+
65
+ if (
66
+ method.value.templates &&
67
+ method.value.templates.some((template) => template.isTemplate)
68
+ ) {
69
+ return;
70
+ }
71
+
72
+ if (!ALLOWED_METHODS.has(method.value.value.toUpperCase())) {
73
+ context.report({
74
+ node: node.openStart,
75
+ messageId: MESSAGE_IDS.UNEXPECTED,
76
+ });
77
+ }
78
+ },
79
+ });
80
+ },
81
+ };
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * @typedef { import("../types").RuleModule } RuleModule
3
- * @typedef { import("../types").TagNode } TagNode
3
+ * @typedef { import("../types").Tag } Tag
4
4
  */
5
5
 
6
6
  const { RULE_CATEGORY } = require("../constants");
@@ -72,8 +72,7 @@ module.exports = {
72
72
  };
73
73
 
74
74
  /**
75
- *
76
- * @param {TagNode} node
75
+ * @param {Tag} node
77
76
  * @param {string[]} substitute
78
77
  * @returns
79
78
  */
@@ -0,0 +1,77 @@
1
+ /**
2
+ * @typedef { import("../types").RuleModule } RuleModule
3
+ * @typedef { import("../types").Tag } Tag
4
+ */
5
+
6
+ const { RULE_CATEGORY } = require("../constants");
7
+ const { createVisitors } = require("./utils/visitors");
8
+ const { findParent, isTag } = require("./utils/node");
9
+
10
+ const MESSAGE_IDS = {
11
+ MISSING: "missingLabel",
12
+ };
13
+
14
+ const INPUT_TAGS = new Set(["input", "textarea", "select"]);
15
+
16
+ const LABEL_ATTRIBUTES = new Set(["id", "aria-labelledby", "aria-label"]);
17
+
18
+ /**
19
+ * @type {RuleModule}
20
+ */
21
+ module.exports = {
22
+ meta: {
23
+ type: "code",
24
+
25
+ docs: {
26
+ description:
27
+ "Enforces use of label for form elements(`input`, `textarea`, `select`)",
28
+ category: RULE_CATEGORY.ACCESSIBILITY,
29
+ recommended: false,
30
+ },
31
+ fixable: null,
32
+ schema: [],
33
+ messages: {
34
+ [MESSAGE_IDS.MISSING]: "Missing an associated label",
35
+ },
36
+ },
37
+ create(context) {
38
+ return createVisitors(context, {
39
+ Tag(node) {
40
+ if (!INPUT_TAGS.has(node.name.toLowerCase())) {
41
+ return;
42
+ }
43
+
44
+ for (const attr of node.attributes) {
45
+ if (
46
+ LABEL_ATTRIBUTES.has(attr.key.value.toLowerCase()) &&
47
+ attr.value &&
48
+ attr.value.value
49
+ ) {
50
+ return;
51
+ }
52
+
53
+ if (
54
+ attr.key.value.toLowerCase() === "type" &&
55
+ attr.value &&
56
+ attr.value.value === "hidden"
57
+ ) {
58
+ return;
59
+ }
60
+ }
61
+
62
+ const label = findParent(node, (parent) => {
63
+ return isTag(parent) && parent.name.toLowerCase() === "label";
64
+ });
65
+
66
+ if (label) {
67
+ return;
68
+ }
69
+
70
+ context.report({
71
+ node,
72
+ messageId: "missingLabel",
73
+ });
74
+ },
75
+ });
76
+ },
77
+ };
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * @typedef { import("../types").RuleModule } RuleModule
3
- * @typedef { import("es-html-parser").TagNode } TagNode
4
- * @typedef { import("es-html-parser").AnyNode } AnyNode
3
+ * @typedef { import("../types").Tag } Tag
4
+ * @typedef { import("../types").AnyNode } AnyNode
5
5
  */
6
6
 
7
7
  const { RULE_CATEGORY } = require("../constants");
@@ -15,7 +15,7 @@ const MESSAGE_IDS = {
15
15
 
16
16
  /**
17
17
  * @param {AnyNode} node
18
- * @returns {node is TagNode}
18
+ * @returns {node is Tag}
19
19
  */
20
20
  function isMetaCharset(node) {
21
21
  return isTag(node) && node.name === "meta" && !!findAttr(node, "charset");
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * @typedef { import("../types").RuleModule } RuleModule
3
- * @typedef { import("es-html-parser").TagNode } TagNode
4
- * @typedef { import("es-html-parser").AnyNode } AnyNode
3
+ * @typedef { import("../types").Tag } Tag
4
+ * @typedef { import("../types").AnyNode } AnyNode
5
5
  */
6
6
  const { RULE_CATEGORY } = require("../constants");
7
7
  const { filter } = require("./utils/array");
@@ -14,7 +14,7 @@ const MESSAGE_IDS = {
14
14
 
15
15
  /**
16
16
  * @param {AnyNode} node
17
- * @returns {node is TagNode}
17
+ * @returns {node is Tag}
18
18
  */
19
19
  function isMetaTagNode(node) {
20
20
  return isTag(node) && node.name === "meta";
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * @typedef { import("../types").RuleModule } RuleModule
3
- * @typedef { import("es-html-parser").TagNode } TagNode
4
- * @typedef { import("es-html-parser").AnyNode } AnyNode
3
+ * @typedef { import("../types").Tag } Tag
4
+ * @typedef { import("../types").AnyNode } AnyNode
5
5
  */
6
6
 
7
7
  const { RULE_CATEGORY } = require("../constants");
@@ -15,7 +15,7 @@ const MESSAGE_IDS = {
15
15
 
16
16
  /**
17
17
  * @param {AnyNode} node
18
- * @returns {node is TagNode}
18
+ * @returns {node is Tag}
19
19
  */
20
20
  function isMetaViewport(node) {
21
21
  if (isTag(node) && node.name === "meta") {
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * @typedef { import("../types").RuleModule } RuleModule
3
- * @typedef { import("es-html-parser").TagNode } TagNode
4
- * @typedef { import("es-html-parser").AnyNode } AnyNode
3
+ * @typedef { import("../types").Tag } Tag
4
+ * @typedef { import("../types").AnyNode } AnyNode
5
5
  */
6
6
 
7
7
  const { RULE_CATEGORY } = require("../constants");
@@ -71,7 +71,7 @@ module.exports = {
71
71
 
72
72
  /**
73
73
  * @param {AnyNode} node
74
- * @returns {node is TagNode}
74
+ * @returns {node is Tag}
75
75
  */
76
76
  function isOgpMeta(node) {
77
77
  const isMeta = isTag(node) && node.name === "meta";