...
 
Commits (6)
# html-validate changelog
# [2.12.0](https://gitlab.com/html-validate/html-validate/compare/v2.11.0...v2.12.0) (2020-01-27)
### Bug Fixes
- **rules:** dont report elements where the tag is already correct ([ee354a0](https://gitlab.com/html-validate/html-validate/commit/ee354a0070f4ac6657cf0a5ce84bddadb3d2dab7)), closes [#65](https://gitlab.com/html-validate/html-validate/issues/65)
### Features
- **rules:** new rule no-redundant-role ([a32b816](https://gitlab.com/html-validate/html-validate/commit/a32b81623ac4c8603923b4ff1a41c342a5dfe1d2)), closes [#65](https://gitlab.com/html-validate/html-validate/issues/65)
# [2.11.0](https://gitlab.com/html-validate/html-validate/compare/v2.10.0...v2.11.0) (2020-01-26)
### Bug Fixes
......
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`docs/rules/no-redundant-role.md inline validation: correct 1`] = `Array []`;
exports[`docs/rules/no-redundant-role.md inline validation: incorrect 1`] = `
Array [
Object {
"errorCount": 2,
"filePath": "inline",
"messages": Array [
Object {
"column": 13,
"context": Object {
"role": "main",
"tagname": "main",
},
"line": 1,
"message": "Redundant role \\"main\\" on <main>",
"offset": 12,
"ruleId": "no-redundant-role",
"selector": "main",
"severity": 2,
"size": 4,
},
Object {
"column": 15,
"context": Object {
"role": "listitem",
"tagname": "li",
},
"line": 3,
"message": "Redundant role \\"listitem\\" on <li>",
"offset": 40,
"ruleId": "no-redundant-role",
"selector": "main > ul > li",
"severity": 2,
"size": 8,
},
],
"source": "<main role=\\"main\\">
<ul>
<li role=\\"listitem\\">Lorem ipsum</li>
</ul>
</main>",
"warningCount": 0,
},
]
`;
import HtmlValidate from "../../../src/htmlvalidate";
const markup: { [key: string]: string } = {};
markup["incorrect"] = `<main role="main">
<ul>
<li role="listitem">Lorem ipsum</li>
</ul>
</main>`;
markup["correct"] = `<ul>
<li role="presentation">Lorem ipsum</li>
</ul>`;
describe("docs/rules/no-redundant-role.md", () => {
it("inline validation: incorrect", () => {
const htmlvalidate = new HtmlValidate({"rules":{"no-redundant-role":"error"}});
const report = htmlvalidate.validateString(markup["incorrect"]);
expect(report.results).toMatchSnapshot();
});
it("inline validation: correct", () => {
const htmlvalidate = new HtmlValidate({"rules":{"no-redundant-role":"error"}});
const report = htmlvalidate.validateString(markup["correct"]);
expect(report.results).toMatchSnapshot();
});
});
---
docType: rule
name: no-redundant-role
summary: Disallow usage of redundant roles
---
# Disallow usage of redundant roles (`no-redundant-role`)
Some HTML5 elements have implied [WAI-ARIA roles][wai-aria-roles] and this rule disallows setting the implied roles on those elements.
## Rule details
Examples of **incorrect** code for this rule:
<validate name="incorrect" rules="no-redundant-role">
<main role="main">
<ul>
<li role="listitem">Lorem ipsum</li>
</ul>
</main>
</validate>
Examples of **correct** code for this rule:
<validate name="correct" rules="no-redundant-role">
<ul>
<li role="presentation">Lorem ipsum</li>
</ul>
</validate>
{
"name": "html-validate",
"version": "2.11.0",
"version": "2.12.0",
"lockfileVersion": 1,
"requires": true,
"dependencies": {
......@@ -3828,9 +3828,9 @@
}
},
"@html-validate/eslint-config": {
"version": "1.0.7",
"resolved": "https://registry.npmjs.org/@html-validate/eslint-config/-/eslint-config-1.0.7.tgz",
"integrity": "sha512-dHJp0VQwiwD95SQ9/vmx092UeGuruSTbM0G7oqb0mzDjmzjRIXZBw+xWcIU5p1B1+vth9Rl+y5QduCdSrHEbrA==",
"version": "1.0.8",
"resolved": "https://registry.npmjs.org/@html-validate/eslint-config/-/eslint-config-1.0.8.tgz",
"integrity": "sha512-ZsatfWOlHXQaHC5SfAgRAP9+6Mu+nr06EDYQUnMuHj8WV5iO//fpDJQHl9tbda6NGYU0Q1h8K4PNsU3yl9/GqQ==",
"dev": true,
"requires": {
"@typescript-eslint/eslint-plugin": "^2.12.0",
......@@ -3848,9 +3848,9 @@
"dev": true
},
"@html-validate/semantic-release-config": {
"version": "1.0.6",
"resolved": "https://registry.npmjs.org/@html-validate/semantic-release-config/-/semantic-release-config-1.0.6.tgz",
"integrity": "sha512-9lXufkgYkQrT0PTgEuBCD6P2BhPhhtaxBoa9udvpvYeEzY0B6umLATxuxsSFkJk/TZE/XMLVLS4PkPnpbgmCuQ==",
"version": "1.0.7",
"resolved": "https://registry.npmjs.org/@html-validate/semantic-release-config/-/semantic-release-config-1.0.7.tgz",
"integrity": "sha512-Ux5j2MKS74VAEzfEGCQkPPkIUmzS7elkc8fU9aTEqd7RUI6lnv6t0AAllaN2PkcqvUZ528XF4woPKk6OCRCq+A==",
"dev": true,
"requires": {
"@semantic-release/changelog": "3.0.6",
......@@ -4957,12 +4957,6 @@
"mimic-fn": "^2.1.0"
}
},
"p-reduce": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/p-reduce/-/p-reduce-2.1.0.tgz",
"integrity": "sha512-2USApvnsutq8uoxZBGbbWM0JIYLiEMJ9RlaN7fAzVNb9OZN0SHjjTTfIcb667XynS5Y1VhwDJVDa72TnPzAYWw==",
"dev": true
},
"path-key": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz",
......@@ -5071,15 +5065,6 @@
"url-join": "^4.0.0"
},
"dependencies": {
"combined-stream": {
"version": "1.0.8",
"resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
"integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
"dev": true,
"requires": {
"delayed-stream": "~1.0.0"
}
},
"debug": {
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz",
......@@ -5802,18 +5787,63 @@
"dev": true
},
"@typescript-eslint/eslint-plugin": {
"version": "2.15.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-2.15.0.tgz",
"integrity": "sha512-XRJFznI5v4K1WvIrWmjFjBAdQWaUTz4xJEdqR7+wAFsv6Q9dP3mOlE6BMNT3pdlp9eF1+bC5m5LZTmLMqffCVw==",
"version": "2.17.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-2.17.0.tgz",
"integrity": "sha512-tg/OMOtPeXlvk0ES8mZzEZ4gd1ruSE03nsKcK+teJhxYv5CPCXK6Mb/OK6NpB4+CqGTHs4MVeoSZXNFqpT1PyQ==",
"dev": true,
"requires": {
"@typescript-eslint/experimental-utils": "2.15.0",
"@typescript-eslint/experimental-utils": "2.17.0",
"eslint-utils": "^1.4.3",
"functional-red-black-tree": "^1.0.1",
"regexpp": "^3.0.0",
"tsutils": "^3.17.1"
},
"dependencies": {
"@typescript-eslint/experimental-utils": {
"version": "2.17.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/experimental-utils/-/experimental-utils-2.17.0.tgz",
"integrity": "sha512-2bNf+mZ/3mj5/3CP56v+ldRK3vFy9jOvmCPs/Gr2DeSJh+asPZrhFniv4QmQsHWQFPJFWhFHgkGgJeRmK4m8iQ==",
"dev": true,
"requires": {
"@types/json-schema": "^7.0.3",
"@typescript-eslint/typescript-estree": "2.17.0",
"eslint-scope": "^5.0.0"
}
},
"@typescript-eslint/typescript-estree": {
"version": "2.17.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-2.17.0.tgz",
"integrity": "sha512-g0eVRULGnEEUakxRfJO0s0Hr1LLQqsI6OrkiCLpdHtdJJek+wyd8mb00vedqAoWldeDcOcP8plqw8/jx9Gr3Lw==",
"dev": true,
"requires": {
"debug": "^4.1.1",
"eslint-visitor-keys": "^1.1.0",
"glob": "^7.1.6",
"is-glob": "^4.0.1",
"lodash": "^4.17.15",
"semver": "^6.3.0",
"tsutils": "^3.17.1"
}
},
"debug": {
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz",
"integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==",
"dev": true,
"requires": {
"ms": "^2.1.1"
}
},
"eslint-scope": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.0.0.tgz",
"integrity": "sha512-oYrhJW7S0bxAFDvWqzvMPRm6pcgcnWc4QnofCAqRTRfQC0JcwenzGglTtsLyIuuWFfkqDG9vz67cnttSd53djw==",
"dev": true,
"requires": {
"esrecurse": "^4.1.0",
"estraverse": "^4.1.1"
}
},
"eslint-utils": {
"version": "1.4.3",
"resolved": "https://registry.npmjs.org/eslint-utils/-/eslint-utils-1.4.3.tgz",
......@@ -5829,11 +5859,37 @@
"integrity": "sha512-8y9YjtM1JBJU/A9Kc+SbaOV4y29sSWckBwMHa+FGtVj5gN/sbnKDf6xJUl+8g7FAij9LVaP8C24DUiH/f/2Z9A==",
"dev": true
},
"glob": {
"version": "7.1.6",
"resolved": "https://registry.npmjs.org/glob/-/glob-7.1.6.tgz",
"integrity": "sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==",
"dev": true,
"requires": {
"fs.realpath": "^1.0.0",
"inflight": "^1.0.4",
"inherits": "2",
"minimatch": "^3.0.4",
"once": "^1.3.0",
"path-is-absolute": "^1.0.0"
}
},
"ms": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
"integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==",
"dev": true
},
"regexpp": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/regexpp/-/regexpp-3.0.0.tgz",
"integrity": "sha512-Z+hNr7RAVWxznLPuA7DIh8UNX1j9CDrUQxskw9IrBE1Dxue2lyXT+shqEIeLUjrokxIP8CMy1WkjgG3rTsd5/g==",
"dev": true
},
"semver": {
"version": "6.3.0",
"resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz",
"integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==",
"dev": true
}
}
},
......@@ -5861,22 +5917,93 @@
}
},
"@typescript-eslint/parser": {
"version": "2.15.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-2.15.0.tgz",
"integrity": "sha512-6iSgQsqAYTaHw59t0tdjzZJluRAjswdGltzKEdLtcJOxR2UVTPHYvZRqkAVGCkaMVb6Fpa60NnuozNCvsSpA9g==",
"version": "2.17.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-2.17.0.tgz",
"integrity": "sha512-k1g3gRQ4fwfJoIfgUpz78AovicSWKFANmvTfkAHP24MgJHjWfZI6ya7tsQZt1sLczvP4G9BE5G5MgADHdmJB/w==",
"dev": true,
"requires": {
"@types/eslint-visitor-keys": "^1.0.0",
"@typescript-eslint/experimental-utils": "2.15.0",
"@typescript-eslint/typescript-estree": "2.15.0",
"@typescript-eslint/experimental-utils": "2.17.0",
"@typescript-eslint/typescript-estree": "2.17.0",
"eslint-visitor-keys": "^1.1.0"
},
"dependencies": {
"@typescript-eslint/experimental-utils": {
"version": "2.17.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/experimental-utils/-/experimental-utils-2.17.0.tgz",
"integrity": "sha512-2bNf+mZ/3mj5/3CP56v+ldRK3vFy9jOvmCPs/Gr2DeSJh+asPZrhFniv4QmQsHWQFPJFWhFHgkGgJeRmK4m8iQ==",
"dev": true,
"requires": {
"@types/json-schema": "^7.0.3",
"@typescript-eslint/typescript-estree": "2.17.0",
"eslint-scope": "^5.0.0"
}
},
"@typescript-eslint/typescript-estree": {
"version": "2.17.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-2.17.0.tgz",
"integrity": "sha512-g0eVRULGnEEUakxRfJO0s0Hr1LLQqsI6OrkiCLpdHtdJJek+wyd8mb00vedqAoWldeDcOcP8plqw8/jx9Gr3Lw==",
"dev": true,
"requires": {
"debug": "^4.1.1",
"eslint-visitor-keys": "^1.1.0",
"glob": "^7.1.6",
"is-glob": "^4.0.1",
"lodash": "^4.17.15",
"semver": "^6.3.0",
"tsutils": "^3.17.1"
}
},
"debug": {
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz",
"integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==",
"dev": true,
"requires": {
"ms": "^2.1.1"
}
},
"eslint-scope": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.0.0.tgz",
"integrity": "sha512-oYrhJW7S0bxAFDvWqzvMPRm6pcgcnWc4QnofCAqRTRfQC0JcwenzGglTtsLyIuuWFfkqDG9vz67cnttSd53djw==",
"dev": true,
"requires": {
"esrecurse": "^4.1.0",
"estraverse": "^4.1.1"
}
},
"eslint-visitor-keys": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-1.1.0.tgz",
"integrity": "sha512-8y9YjtM1JBJU/A9Kc+SbaOV4y29sSWckBwMHa+FGtVj5gN/sbnKDf6xJUl+8g7FAij9LVaP8C24DUiH/f/2Z9A==",
"dev": true
},
"glob": {
"version": "7.1.6",
"resolved": "https://registry.npmjs.org/glob/-/glob-7.1.6.tgz",
"integrity": "sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==",
"dev": true,
"requires": {
"fs.realpath": "^1.0.0",
"inflight": "^1.0.4",
"inherits": "2",
"minimatch": "^3.0.4",
"once": "^1.3.0",
"path-is-absolute": "^1.0.0"
}
},
"ms": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
"integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==",
"dev": true
},
"semver": {
"version": "6.3.0",
"resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz",
"integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==",
"dev": true
}
}
},
......@@ -28,6 +28,7 @@ module.exports = {
"no-implicit-close": "error",
"no-inline-style": "error",
"no-raw-characters": "error",
"no-redundant-role": "error",
"no-trailing-whitespace": "error",
"prefer-button": "error",
"prefer-native-element": "error",
......
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`rule no-redundant-role should contain contextual documentation 1`] = `
Object {
"description": "Using the \\"checkbox\\" role is redundant as it is already implied by the <input> element.",
"url": "https://html-validate.org/rules/no-redundant-role.html",
}
`;
exports[`rule no-redundant-role should contain documentation 1`] = `
Object {
"description": "Using this role is redundant as it is already implied by the element.",
"url": "https://html-validate.org/rules/no-redundant-role.html",
}
`;
import HtmlValidate from "../htmlvalidate";
import "../matchers";
import { processAttribute } from "../transform/mocks/attribute";
describe("rule no-redundant-role", () => {
let htmlvalidate: HtmlValidate;
beforeAll(() => {
htmlvalidate = new HtmlValidate({
rules: { "no-redundant-role": "error" },
});
});
it("should not report error when element has non-redundant role", () => {
const report = htmlvalidate.validateString('<li role="presentation"></li>');
expect(report).toBeValid();
});
it("should not report error element has no known roles", () => {
const report = htmlvalidate.validateString('<span role="main"></span>');
expect(report).toBeValid();
});
it("should not report error when role is boolean", () => {
const report = htmlvalidate.validateString("<div role></div>");
expect(report).toBeValid();
});
it("should not report error for dynamic attributes", () => {
const report = htmlvalidate.validateString(
'<input dynamic-role="main">',
null,
{
processAttribute,
}
);
expect(report).toBeValid();
});
it("should report error when element has redundant role", () => {
const htmlvalidate = new HtmlValidate({
rules: { "no-redundant-role": "error" },
});
const report = htmlvalidate.validateString('<li role="listitem"></li>');
expect(report).toBeInvalid();
});
it("should contain documentation", () => {
const htmlvalidate = new HtmlValidate({
rules: { "no-redundant-role": "error" },
});
expect(
htmlvalidate.getRuleDocumentation("no-redundant-role")
).toMatchSnapshot();
});
it("should contain contextual documentation", () => {
const htmlvalidate = new HtmlValidate({
rules: { "no-redundant-role": "error" },
});
const context = {
role: "checkbox",
tagname: "input",
};
expect(
htmlvalidate.getRuleDocumentation("no-redundant-role", null, context)
).toMatchSnapshot();
});
});
import { DynamicValue } from "../dom";
import { AttributeEvent } from "../event";
import { Rule, RuleDocumentation, ruleDocumentationUrl } from "../rule";
interface RuleContext {
tagname: string;
role: string;
}
const mapping: Record<string, string[]> = {
article: ["article"],
header: ["banner"],
button: ["button"],
td: ["cell"],
input: ["checkbox", "radio", "input"],
aside: ["complementary"],
footer: ["contentinfo"],
figure: ["figure"],
form: ["form"],
h1: ["heading"],
h2: ["heading"],
h3: ["heading"],
h4: ["heading"],
h5: ["heading"],
h6: ["heading"],
a: ["link"],
ul: ["list"],
select: ["listbox"],
li: ["listitem"],
main: ["main"],
nav: ["navigation"],
progress: ["progressbar"],
section: ["region"],
table: ["table"],
textarea: ["textbox"],
};
class NoRedundantRole extends Rule<RuleContext, void> {
public documentation(context: RuleContext): RuleDocumentation {
const doc: RuleDocumentation = {
description: `Using this role is redundant as it is already implied by the element.`,
url: ruleDocumentationUrl(__filename),
};
if (context) {
doc.description = `Using the "${context.role}" role is redundant as it is already implied by the <${context.tagname}> element.`;
}
return doc;
}
public setup(): void {
this.on("attr", (event: AttributeEvent) => {
const { target } = event;
/* ignore non-role attributes */
if (event.key.toLowerCase() !== "role") {
return;
}
/* ignore missing and dynamic values */
if (!event.value || event.value instanceof DynamicValue) {
return;
}
/* ignore elements without known redundant roles */
const redundant = mapping[target.tagName];
if (!redundant) {
return;
}
/* ignore elements with non-redundant roles */
if (!redundant.includes(event.value)) {
return;
}
/* report error */
const context: RuleContext = {
tagname: target.tagName,
role: event.value,
};
this.report(
event.target,
`Redundant role "${event.value}" on <${target.tagName}>`,
event.valueLocation,
context
);
});
}
}
module.exports = NoRedundantRole;
......@@ -33,6 +33,11 @@ describe("rule prefer-native-element", () => {
expect(report).toBeValid();
});
it("should not report error when element has redundant role", () => {
const report = htmlvalidate.validateString('<main role="main"></main>');
expect(report).toBeValid();
});
it("should report error when using role with native equivalent element", () => {
const report = htmlvalidate.validateString('<div role="main"></div>');
expect(report).toBeInvalid();
......
......@@ -78,8 +78,14 @@ class PreferNativeElement extends Rule<RuleContext, RuleOptions> {
return;
}
/* report error */
/* dont report when the element is already of the right type but has a
* redundant role, such as <main role="main"> */
const replacement = mapping[role];
if (event.target.is(replacement)) {
return;
}
/* report error */
const context: RuleContext = { role, replacement };
const location = this.getLocation(event);
this.report(
......