-
-
Notifications
You must be signed in to change notification settings - Fork 51
feat: add new rule no-ineffective-attrs #399
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
4e1c700
7650814
9caa3a4
7a32928
c248ed2
3775d70
1a6c81a
88eed22
168f17b
b392efc
547bc1a
286ba84
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -59,6 +59,9 @@ | |
| "contenteditable", | ||
| "nbsb", | ||
| "endl", | ||
| "cout" | ||
| "cout", | ||
| "nodownload", | ||
| "nofullscreen", | ||
| "controlslist" | ||
| ] | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,78 @@ | ||
| # no-ineffective-attrs | ||
|
|
||
| ## How to use | ||
|
|
||
| ```js,.eslintrc.js | ||
| module.exports = { | ||
| rules: { | ||
| "@html-eslint/no-ineffective-attrs": "error", | ||
| }, | ||
| }; | ||
| ``` | ||
|
|
||
| ## Rule Details | ||
|
|
||
| This rule disallows HTML attributes that have no effect in their context. Such attributes may indicate errors or unnecessary code that should be removed. | ||
|
|
||
| Examples of **incorrect** code for this rule: | ||
|
|
||
| ```html | ||
| <!-- multiple has no effect on text inputs --> | ||
| <input type="text" multiple /> | ||
|
|
||
| <!-- multiple has no effect on checkbox inputs --> | ||
| <input type="checkbox" multiple /> | ||
|
|
||
| <!-- accept only works with file inputs --> | ||
| <input type="text" accept=".jpg,.png" /> | ||
|
|
||
| <!-- readonly has no effect on checkbox inputs --> | ||
| <input type="checkbox" readonly /> | ||
|
|
||
| <!-- readonly has no effect on file inputs --> | ||
| <input type="file" readonly /> | ||
|
|
||
| <!-- defer has no effect on inline scripts --> | ||
| <script defer> | ||
| console.log("hello"); | ||
| </script> | ||
|
|
||
| <!-- async has no effect on inline scripts --> | ||
| <script async> | ||
| console.log("hello"); | ||
| </script> | ||
|
|
||
| <!-- download needs href to work --> | ||
| <a download="file.pdf">Download</a> | ||
|
|
||
| <!-- controlslist needs controls to work --> | ||
| <audio controlslist="nodownload"></audio> | ||
| <video controlslist="nofullscreen"></video> | ||
| ``` | ||
|
|
||
| Examples of **correct** code for this rule: | ||
|
|
||
| ```html | ||
| <!-- multiple works with email and file inputs --> | ||
| <input type="email" multiple /> | ||
| <input type="file" multiple /> | ||
| <select multiple></select> | ||
|
|
||
| <!-- accept works with file inputs --> | ||
| <input type="file" accept=".jpg,.png" /> | ||
|
|
||
| <!-- readonly works with text and password inputs --> | ||
| <input type="text" readonly /> | ||
| <input type="password" readonly /> | ||
|
|
||
| <!-- defer and async work with external scripts --> | ||
| <script defer src="script.js"></script> | ||
| <script async src="script.js"></script> | ||
|
|
||
| <!-- download works when href is present --> | ||
| <a href="file.pdf" download="file.pdf">Download</a> | ||
|
|
||
| <!-- controlslist works when controls is present --> | ||
| <audio controls controlslist="nodownload"></audio> | ||
| <video controls controlslist="nofullscreen"></video> | ||
| ``` |
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,184 @@ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| /** | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * @import {RuleModule} from "../types"; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * @import {Tag, ScriptTag} from "@html-eslint/types" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * @typedef {{ attr: string; when: (node: Tag | ScriptTag) => boolean; message: string; }} AttributeChecker | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| */ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const { RULE_CATEGORY } = require("../constants"); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const { hasAttr, hasTemplate, findAttr } = require("./utils/node"); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const { createVisitors } = require("./utils/visitors"); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| /** | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * @param {Tag | ScriptTag} node | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * @param {string} attrName | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * @returns {string | undefined} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| */ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| function getAttrValue(node, attrName) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const attr = node.attributes.find( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| (a) => a.type === "Attribute" && a.key.value === attrName | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (!attr || !attr.value) return undefined; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return attr.value.value; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| /** | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * @param {Tag | ScriptTag} node | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * @param {string} attrName | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * @returns {boolean} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| */ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| function isTemplateValueAttr(node, attrName) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const attr = findAttr(node, attrName); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (!attr || !attr.value) return false; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return hasTemplate(attr.value); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| /** | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * @type {Record<string, AttributeChecker[]>} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| */ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const checkersByTag = { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| input: [ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| attr: "multiple", | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| when: (node) => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const type = getAttrValue(node, "type") || "text"; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return [ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "text", | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "password", | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "radio", | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "checkbox", | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "image", | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "hidden", | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "reset", | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "button", | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ].includes(type); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+44
to
+53
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return [ | |
| "text", | |
| "password", | |
| "radio", | |
| "checkbox", | |
| "image", | |
| "hidden", | |
| "reset", | |
| "button", | |
| ].includes(type); | |
| return INPUT_TYPES_WITHOUT_MULTIPLE.includes(type); |
Copilot
AI
Aug 9, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The array of input types that don't support 'readonly' should be extracted as a constant for better maintainability and consistency with other type checking logic.
| return ["checkbox", "radio", "file", "range", "color"].includes(type); | |
| return INPUT_TYPES_WITHOUT_READONLY.includes(type); |
Copilot
AI
Aug 9, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The logic in the ScriptTag visitor is nearly identical to the Tag visitor (lines 126-146). Consider extracting the common attribute checking logic into a shared helper function to reduce code duplication.
| const tagCheckers = checkersByTag[node.name]; | |
| if (!tagCheckers) return; | |
| for (const check of tagCheckers) { | |
| for (const attr of node.attributes) { | |
| if (attr.type !== "Attribute") continue; | |
| if (attr.key.value !== check.attr) continue; | |
| if (check.when(node)) { | |
| context.report({ | |
| loc: attr.loc, | |
| messageId: "ineffective", | |
| data: { | |
| message: check.message, | |
| }, | |
| }); | |
| } | |
| } | |
| } | |
| }, | |
| /** | |
| * @param {ScriptTag} node | |
| */ | |
| ScriptTag(node) { | |
| const scriptCheckers = checkersByTag.script; | |
| if (!scriptCheckers) return; | |
| for (const check of scriptCheckers) { | |
| for (const attr of node.attributes) { | |
| if (attr.type !== "Attribute") continue; | |
| if (attr.key.value !== check.attr) continue; | |
| if (check.when(node)) { | |
| context.report({ | |
| loc: attr.loc, | |
| messageId: "ineffective", | |
| data: { | |
| message: check.message, | |
| }, | |
| }); | |
| } | |
| } | |
| } | |
| checkIneffectiveAttrs(node, checkersByTag[node.name], context); | |
| }, | |
| /** | |
| * @param {ScriptTag} node | |
| */ | |
| ScriptTag(node) { | |
| checkIneffectiveAttrs(node, checkersByTag.script, context); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The magic string "text" is used as a default input type. Consider extracting this as a named constant to improve maintainability and reduce duplication (it appears on lines 32, 49, and 58).