Commit 23ee19ea authored by David Sveningsson's avatar David Sveningsson
Browse files

feat: new rule `input-attributes`

fixes #119
parent 25f6bd7b
Pipeline #320822076 passed with stages
in 9 minutes and 41 seconds
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`docs/rules/input-attributes.md inline validation: correct 1`] = `Array []`;
exports[`docs/rules/input-attributes.md inline validation: incorrect 1`] = `
Array [
Object {
"errorCount": 1,
"filePath": "inline",
"messages": Array [
Object {
"column": 20,
"context": Object {
"attribute": "step",
"type": "text",
},
"line": 1,
"message": "Attribute \\"step\\" is not allowed on <input type=\\"text\\">",
"offset": 19,
"ruleId": "input-attributes",
"ruleUrl": "https://html-validate.org/rules/input-attributes.html",
"selector": "input",
"severity": 2,
"size": 4,
},
],
"source": "<input type=\\"text\\" step=\\"5\\">",
"warningCount": 0,
},
]
`;
import HtmlValidate from "../../../src/htmlvalidate";
const markup: { [key: string]: string } = {};
markup["incorrect"] = `<input type="text" step="5">`;
markup["correct"] = `<input type="number" step="5">`;
describe("docs/rules/input-attributes.md", () => {
it("inline validation: incorrect", () => {
expect.assertions(1);
const htmlvalidate = new HtmlValidate({"rules":{"input-attributes":"error"}});
const report = htmlvalidate.validateString(markup["incorrect"]);
expect(report.results).toMatchSnapshot();
});
it("inline validation: correct", () => {
expect.assertions(1);
const htmlvalidate = new HtmlValidate({"rules":{"input-attributes":"error"}});
const report = htmlvalidate.validateString(markup["correct"]);
expect(report.results).toMatchSnapshot();
});
});
......@@ -57,3 +57,7 @@ For instance, when configured with `{"pattern": ["[a-z0-9-]+", "myprefix-.+"]}`
By default attributes on foreign elements (such as `<svg>` and `<math>`) are ignored as they follow their own specifications.
Disable this option if you want to validate attributes on foreign elements as well.
## Version history
- v%version% - Rule added.
---
docType: rule
name: input-attributes
category: content-model
summary: Validates usage of input attributes
---
# Validates usage of input attributes (`input-attributes`)
The `<input>` element uses the `type` attribute to set what type of input field it is.
Depending on what type of input field it is many other attributes is allowed or disallowed.
For instance, the `step` attribute can be used with numerical fields but not with textual input.
This rule validates the usage of these attributes, ensuring the attributes are used only in the proper context.
See [HTML5 specification][whatwg] for a table of attributes and types.
[whatwg]: https://html.spec.whatwg.org/multipage/input.html#concept-input-apply
## Rule details
Examples of **incorrect** code for this rule:
<validate name="incorrect" rules="input-attributes">
<input type="text" step="5">
</validate>
Examples of **correct** code for this rule:
<validate name="correct" rules="input-attributes">
<input type="number" step="5">
</validate>
## Version history
- v%version% - Rule added.
This source diff could not be displayed because it is too large. You can view the blob instead.
......@@ -24,6 +24,7 @@ const config: ConfigData = {
"element-required-content": "error",
"empty-heading": "error",
"empty-title": "error",
"input-attributes": "error",
"long-title": "error",
"meta-refresh": "error",
"multiple-labeled-controls": "error",
......
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`rule input-attributes should contain contextual documentation (invalid) 1`] = `
Object {
"description": "Attribute \`missing\` is not allowed on \`<input type=\\"text\\">\`
\`missing\` can only be used when \`type\` is:",
"url": "https://html-validate.org/rules/input-attributes.html",
}
`;
exports[`rule input-attributes should contain contextual documentation 1`] = `
Object {
"description": "Attribute \`alt\` is not allowed on \`<input type=\\"text\\">\`
\`alt\` can only be used when \`type\` is:
- \`image\`",
"url": "https://html-validate.org/rules/input-attributes.html",
}
`;
exports[`rule input-attributes should contain documentation 1`] = `
Object {
"description": "This attribute cannot be used with this input type.",
"url": "https://html-validate.org/rules/input-attributes.html",
}
`;
......@@ -26,6 +26,7 @@ import EmptyHeading from "./empty-heading";
import EmptyTitle from "./empty-title";
import HeadingLevel from "./heading-level";
import IdPattern from "./id-pattern";
import InputAttributes from "./input-attributes";
import InputMissingLabel from "./input-missing-label";
import LongTitle from "./long-title";
import MetaRefresh from "./meta-refresh";
......@@ -91,6 +92,7 @@ const bundledRules: Record<string, RuleConstructor<any, any>> = {
"empty-title": EmptyTitle,
"heading-level": HeadingLevel,
"id-pattern": IdPattern,
"input-attributes": InputAttributes,
"input-missing-label": InputMissingLabel,
"long-title": LongTitle,
"meta-refresh": MetaRefresh,
......
import HtmlValidate from "../htmlvalidate";
import "../matchers";
import { processAttribute } from "../transform/mocks/attribute";
describe("rule input-attributes", () => {
let htmlvalidate: HtmlValidate;
beforeAll(() => {
htmlvalidate = new HtmlValidate({
root: true,
rules: { "input-attributes": ["error", { style: "lowercase" }] },
});
});
it("should not report error for other elements", () => {
expect.assertions(1);
const report = htmlvalidate.validateString('<div type="text" step="5"></div>');
expect(report).toBeValid();
});
it("should not report error when attribute is correct", () => {
expect.assertions(1);
const report = htmlvalidate.validateString('<input type="number" step="5">');
expect(report).toBeValid();
});
it("should report error when incorrect attribute is used", () => {
expect.assertions(2);
const report = htmlvalidate.validateString('<input type="text" step="5">');
expect(report).toBeInvalid();
expect(report).toHaveError(
"input-attributes",
'Attribute "step" is not allowed on <input type="text">'
);
});
it("should handle when type is missing", () => {
expect.assertions(1);
const report = htmlvalidate.validateString('<input step="5">');
expect(report).toBeValid();
});
it("should handle when type is incomplete", () => {
expect.assertions(1);
const report = htmlvalidate.validateString('<input type step="5">');
expect(report).toBeValid();
});
it("should handle when type is dynamic", () => {
expect.assertions(1);
const report = htmlvalidate.validateString('<input dynamic-type="type" step="5">', {
processAttribute,
});
expect(report).toBeValid();
});
it("should contain documentation", () => {
expect.assertions(1);
htmlvalidate = new HtmlValidate({
root: true,
rules: { "input-attributes": "error" },
});
expect(htmlvalidate.getRuleDocumentation("input-attributes")).toMatchSnapshot();
});
it("should contain contextual documentation", () => {
expect.assertions(1);
htmlvalidate = new HtmlValidate({
root: true,
rules: { "input-attributes": "error" },
});
const context = {
attribute: "alt",
type: "text",
};
expect(htmlvalidate.getRuleDocumentation("input-attributes", null, context)).toMatchSnapshot();
});
it("should contain contextual documentation (invalid)", () => {
expect.assertions(1);
htmlvalidate = new HtmlValidate({
root: true,
rules: { "input-attributes": "error" },
});
const context = {
attribute: "missing",
type: "text",
};
expect(htmlvalidate.getRuleDocumentation("input-attributes", null, context)).toMatchSnapshot();
});
});
/* eslint-disable sonarjs/no-duplicate-string */
import { TagReadyEvent } from "../event";
import { Rule, RuleDocumentation, ruleDocumentationUrl } from "../rule";
interface RuleContext {
attribute: string;
type: string;
}
const restricted: Map<string, string[]> = new Map([
["accept", ["file"]],
["alt", ["image"]],
[
"autocomplete",
[
"hidden",
"text",
"search",
"url",
"tel",
"email",
"password",
"date",
"month",
"week",
"time",
"datetime-local",
"number",
"range",
"color",
],
],
["capture", ["file"]],
["checked", ["checkbox", "radio"]],
["dirname", ["text", "search"]],
["formaction", ["submit", "image"]],
["formenctype", ["submit", "image"]],
["formmethod", ["submit", "image"]],
["formnovalidate", ["submit", "image"]],
["formtarget", ["submit", "image"]],
["height", ["image"]],
[
"list",
[
"text",
"search",
"url",
"tel",
"email",
"date",
"month",
"week",
"time",
"datetime-local",
"number",
"range",
"color",
],
],
["max", ["date", "month", "week", "time", "datetime-local", "number", "range"]],
["maxlength", ["text", "search", "url", "tel", "email", "password"]],
["min", ["date", "month", "week", "time", "datetime-local", "number", "range"]],
["minlength", ["text", "search", "url", "tel", "email", "password"]],
["multiple", ["email", "file"]],
["pattern", ["text", "search", "url", "tel", "email", "password"]],
["placeholder", ["text", "search", "url", "tel", "email", "password", "number"]],
[
"readonly",
[
"text",
"search",
"url",
"tel",
"email",
"password",
"date",
"month",
"week",
"time",
"datetime-local",
"number",
],
],
[
"required",
[
"text",
"search",
"url",
"tel",
"email",
"password",
"date",
"month",
"week",
"time",
"datetime-local",
"number",
"checkbox",
"radio",
"file",
],
],
["size", ["text", "search", "url", "tel", "email", "password"]],
["src", ["image"]],
["step", ["date", "month", "week", "time", "datetime-local", "number", "range"]],
["width", ["image"]],
]);
function isInput(event: TagReadyEvent): boolean {
const { target } = event;
return target.is("input");
}
export default class InputAttributes extends Rule<RuleContext> {
public documentation(context?: RuleContext): RuleDocumentation {
if (context) {
const { attribute, type } = context;
const summary = `Attribute \`${attribute}\` is not allowed on \`<input type="${type}">\`\n`;
const details = `\`${attribute}\` can only be used when \`type\` is:`;
const list = restricted.get(attribute)?.map((it) => `- \`${it}\``) ?? [];
return {
description: [summary, details, ...list].join("\n"),
url: ruleDocumentationUrl(__filename),
};
} else {
return {
description: `This attribute cannot be used with this input type.`,
url: ruleDocumentationUrl(__filename),
};
}
}
public setup(): void {
this.on("tag:ready", isInput, (event: TagReadyEvent) => {
const { target } = event;
const type = target.getAttribute("type");
if (!type || type.isDynamic || !type.value) {
return;
}
const typeValue = type.value.toString();
for (const attr of target.attributes) {
const validTypes = restricted.get(attr.key);
if (!validTypes) {
continue;
}
if (validTypes.includes(typeValue)) {
continue;
}
const context: RuleContext = {
attribute: attr.key,
type: typeValue,
};
const message = `Attribute "${attr.key}" is not allowed on <input type="${typeValue}">`;
this.report(target, message, attr.keyLocation, context);
}
});
}
}
......@@ -3,10 +3,10 @@
<!-- should not allow attributes -->
<input type="text" autofocus="foobar">
<input type="text" capture="foobar">
<input type="text" checked="foobar">
<input type="file" capture="foobar">
<input type="checkbox" checked="foobar">
<input type="text" disabled="foobar">
<input type="text" multiple="foobar">
<input type="file" multiple="foobar">
<input type="text" readonly="foobar">
<input type="text" required="foobar">
<input type="text" inputmode="foobar">
......@@ -24,3 +24,424 @@
<!-- type="image" requires alt text -->
<input type="image">
<!-- attributes with restrictions based on type -->
<div id="attribute-restriction">
<input type="hidden" accept="image/jpeg">
<input type="text" accept="image/jpeg">
<input type="search" accept="image/jpeg">
<input type="url" accept="image/jpeg">
<input type="tel" accept="image/jpeg">
<input type="email" accept="image/jpeg">
<input type="password" accept="image/jpeg">
<input type="date" accept="image/jpeg">
<input type="month" accept="image/jpeg">
<input type="week" accept="image/jpeg">
<input type="time" accept="image/jpeg">
<input type="datetime-local" accept="image/jpeg">
<input type="number" accept="image/jpeg">
<input type="range" accept="image/jpeg">
<input type="color" accept="image/jpeg">
<input type="checkbox" accept="image/jpeg">
<input type="radio" accept="image/jpeg">
<input type="submit" accept="image/jpeg">
<input type="image" accept="image/jpeg" alt="lorem ipsum">
<input type="reset" accept="image/jpeg">
<input type="hidden" alt="lorem ipsum">
<input type="text" alt="lorem ipsum">
<input type="search" alt="lorem ipsum">
<input type="url" alt="lorem ipsum">
<input type="tel" alt="lorem ipsum">
<input type="email" alt="lorem ipsum">
<input type="password" alt="lorem ipsum">
<input type="date" alt="lorem ipsum">
<input type="month" alt="lorem ipsum">
<input type="week" alt="lorem ipsum">
<input type="time" alt="lorem ipsum">
<input type="datetime-local" alt="lorem ipsum">
<input type="number" alt="lorem ipsum">
<input type="range" alt="lorem ipsum">
<input type="color" alt="lorem ipsum">
<input type="checkbox" alt="lorem ipsum">
<input type="radio" alt="lorem ipsum">
<input type="file" alt="lorem ipsum">
<input type="submit" alt="lorem ipsum">
<input type="reset" alt="lorem ipsum">
<input type="checkbox" autocomplete="on">
<input type="radio" autocomplete="on">
<input type="file" autocomplete="on">
<input type="submit" autocomplete="on">
<input type="image" autocomplete="on" alt="lorem ipsum">
<input type="reset" autocomplete="on">
<input type="hidden" capture>
<input type="text" capture>
<input type="search" capture>
<input type="url" capture>
<input type="tel" capture>
<input type="email" capture>
<input type="password" capture>
<input type="date" capture>
<input type="month" capture>
<input type="week" capture>
<input type="time" capture>
<input type="datetime-local" capture>
<input type="number" capture>
<input type="range" capture>
<input type="color" capture>
<input type="checkbox" capture>
<input type="radio" capture>
<input type="submit" capture>
<input type="image" capture alt="lorem ipsum">
<input type="reset" capture>
<input type="hidden" checked>
<input type="text" checked>
<input type="search" checked>
<input type="url" checked>
<input type="tel" checked>
<input type="email" checked>
<input type="password" checked>
<input type="date" checked>
<input type="month" checked>
<input type="week" checked>
<input type="time" checked>
<input type="datetime-local" checked>
<input type="number" checked>
<input type="range" checked>
<input type="color" checked>
<input type="file" checked>
<input type="submit" checked>
<input type="image" checked alt="lorem ipsum">
<input type="reset" checked>
<input type="hidden" dirname="myname">
<input type="url" dirname="myname">
<input type="tel" dirname="myname">
<input type="email" dirname="myname">
<input type="password" dirname="myname">
<input type="date" dirname="myname">
<input type="month" dirname="myname">
<input type="week" dirname="myname">
<input type="time" dirname="myname">
<input type="datetime-local" dirname="myname">
<input type="number" dirname="myname">
<input type="range" dirname="myname">
<input type="color" dirname="myname">
<input type="checkbox" dirname="myname">
<input type="radio" dirname="myname">
<input type="file" dirname="myname">
<input type="submit" dirname="myname">
<input type="image" dirname="myname" alt="lorem ipsum">
<input type="reset" dirname="myname">
<input type="hidden" formaction="/myaction">
<input type="text" formaction="/myaction">
<input type="search" formaction="/myaction">
<input type="url" formaction="/myaction">
<input type="tel" formaction="/myaction">
<input type="email" formaction="/myaction">
<input type="password" formaction="/myaction">
<input type="date" formaction="/myaction">
<input type="month" formaction="/myaction">
<input type="week" formaction="/myaction">
<input type="time" formaction="/myaction">
<input type="datetime-local" formaction="/myaction">
<input type="number" formaction="/myaction">
<input type="range" formaction="/myaction">
<input type="color" formaction="/myaction">
<input type="checkbox" formaction="/myaction">
<input type="radio" formaction="/myaction">
<input type="file" formaction="/myaction">
<input type="reset" formaction="/myaction">
<input type="hidden" formenctype>
<input type="text" formenctype>
<input type="search" formenctype>
<input type="url" formenctype>
<input type="tel" formenctype>
<input type="email" formenctype>
<input type="password" formenctype>
<input type="date" formenctype>
<input type="month" formenctype>
<input type="week" formenctype>
<input type="time" formenctype>
<input type="datetime-local" formenctype>
<input type="number" formenctype>
<input type="range" formenctype>
<input type="color" formenctype>
<input type="checkbox" formenctype>
<input type="radio" formenctype>
<input type="file" formenctype>
<input type="reset" formenctype>
<input type="hidden" formmethod="post">
<input type="text" formmethod="post">
<input type="search" formmethod="post">
<input type="url" formmethod="post">
<input type="tel" formmethod="post">
<input type="email" formmethod="post">
<input type="password" formmethod="post">
<input type="date" formmethod="post">
<input type="month" formmethod="post">
<input type="week" formmethod="post">
<input type="time" formmethod="post">
<input type="datetime-local" formmethod="post">