Commit d9c869b3 authored by David Sveningsson's avatar David Sveningsson

feat(rules): new rule `no-self-closing`

refs #58
parent f30de03e
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`docs/rules/no-self-closing.md inline validation: correct 1`] = `Array []`;
exports[`docs/rules/no-self-closing.md inline validation: foreign 1`] = `
Array [
Object {
"errorCount": 1,
"filePath": "inline",
"messages": Array [
Object {
"column": 5,
"context": "svg",
"line": 1,
"message": "<svg> must not be self-closed",
"offset": 4,
"ruleId": "no-self-closing",
"selector": "svg",
"severity": 2,
"size": 2,
},
],
"source": "<svg/>",
"warningCount": 0,
},
]
`;
exports[`docs/rules/no-self-closing.md inline validation: incorrect 1`] = `
Array [
Object {
"errorCount": 1,
"filePath": "inline",
"messages": Array [
Object {
"column": 5,
"context": "div",
"line": 1,
"message": "<div> must not be self-closed",
"offset": 4,
"ruleId": "no-self-closing",
"selector": "div",
"severity": 2,
"size": 2,
},
],
"source": "<div/>",
"warningCount": 0,
},
]
`;
exports[`docs/rules/no-self-closing.md inline validation: xml 1`] = `
Array [
Object {
"errorCount": 1,
"filePath": "inline",
"messages": Array [
Object {
"column": 12,
"context": "xi:include",
"line": 1,
"message": "<xi:include> must not be self-closed",
"offset": 11,
"ruleId": "no-self-closing",
"selector": "xi:include",
"severity": 2,
"size": 2,
},
],
"source": "<xi:include/>",
"warningCount": 0,
},
]
`;
import HtmlValidate from "../../../src/htmlvalidate";
const markup: { [key: string]: string } = {};
markup["incorrect"] = `<div/>`;
markup["correct"] = `<div></div>
<!-- foreign elements are ignored -->
<svg/>
<!-- elements with XML namespace are ignored -->
<xi:include/>`;
markup["foreign"] = `<svg/>`;
markup["xml"] = `<xi:include/>`;
describe("docs/rules/no-self-closing.md", () => {
it("inline validation: incorrect", () => {
const htmlvalidate = new HtmlValidate({"rules":{"no-self-closing":"error"}});
const report = htmlvalidate.validateString(markup["incorrect"]);
expect(report.results).toMatchSnapshot();
});
it("inline validation: correct", () => {
const htmlvalidate = new HtmlValidate({"rules":{"no-self-closing":"error"}});
const report = htmlvalidate.validateString(markup["correct"]);
expect(report.results).toMatchSnapshot();
});
it("inline validation: foreign", () => {
const htmlvalidate = new HtmlValidate({"rules":{"no-self-closing":["error",{"ignoreForeign":false}]}});
const report = htmlvalidate.validateString(markup["foreign"]);
expect(report.results).toMatchSnapshot();
});
it("inline validation: xml", () => {
const htmlvalidate = new HtmlValidate({"rules":{"no-self-closing":["error",{"ignoreXML":false}]}});
const report = htmlvalidate.validateString(markup["xml"]);
expect(report.results).toMatchSnapshot();
});
});
---
docType: rule
name: no-self-closing
category: style
summary: Disallow self-closing elements
---
# Disallow self-closing elements (`no-self-closing`)
Require regular end tags for elements even if the element has no content, e.g. require `<div></div>` instead of `<div/>`.
This rule has no effect on void elements, see the related rule {@link void-style}.
## Rule details
Examples of **incorrect** code for this rule:
<validate name="incorrect" rules="no-self-closing">
<div/>
</validate>
Examples of **correct** code for this rule:
<validate name="correct" rules="no-self-closing">
<div></div>
<!-- foreign elements are ignored -->
<svg/>
<!-- elements with XML namespace are ignored -->
<xi:include/>
</validate>
## Options
This rule takes an optional object:
```json
{
"ignoreForeign": true,
"ignoreXML": true
}
```
### `ignoreForeign`
By default foreign elements are ignored by this rule.
By setting `ignoreForeign` to `false` foreign elements must not be self-closed either.
<validate name="foreign" rules="no-self-closing" no-self-closing='{"ignoreForeign": false}'>
<svg/>
</validate>
### `ignoreXML`
By default elements in XML namespaces are ignored by this rule.
By setting `ignoreXML` to `false` elements in XML namespaces must not be self-closed either.
<validate name="xml" rules="no-self-closing" no-self-closing='{"ignoreXML": false}'>
<xi:include/>
</validate>
......@@ -13,7 +13,7 @@ Void elements are implicitly closed (`<img>`) but may optionally be XML-style se
This rules enforces usage of one of the two styles.
Default is to omit self-closing tag.
Non-void elements are ignored by this rule.
This rule has no effect on non-void elements, see the related rule {@link no-self-closing}.
## Rule details
......
This diff is collapsed.
......@@ -164,6 +164,9 @@ describe("HTML elements", () => {
* yield any errors */
"prefer-button": "off",
/* void is being deprecated */
void: "off",
/* none of the WCAG rules should trigger in these tests, they are tested
* separately and adds too much noise here */
"wcag/h32": "off",
......
......@@ -10,7 +10,7 @@ Object {
"rules": Object {
"deprecated": "error",
"element-permitted-content": "off",
"void": "error",
"no-self-closing": "error",
},
"transform": Object {},
}
......@@ -24,22 +24,22 @@ Array [
"messages": Array [
Object {
"column": 3,
"context": undefined,
"context": "i",
"line": 1,
"message": "End tag for <i> must not be omitted",
"message": "<i> must not be self-closed",
"offset": 2,
"ruleId": "void",
"ruleId": "no-self-closing",
"selector": "i",
"severity": 2,
"size": 2,
},
Object {
"column": 3,
"context": undefined,
"context": "i",
"line": 8,
"message": "End tag for <i> must not be omitted",
"offset": 190,
"ruleId": "void",
"message": "<i> must not be self-closed",
"offset": 212,
"ruleId": "no-self-closing",
"selector": "i:nth-child(4)",
"severity": 2,
"size": 2,
......@@ -47,11 +47,11 @@ Array [
],
"source": "<i/>Before disable, should trigger
<!-- [html-validate-disable void] -->
<!-- [html-validate-disable no-self-closing] -->
<i/>After disable, should not trigger
<i/>After disable, should not trigger
<!-- [html-validate-enable void] -->
<!-- [html-validate-enable no-self-closing] -->
<i/>Before after, should trigger
",
"warningCount": 0,
......@@ -69,7 +69,7 @@ Object {
"rules": Object {
"deprecated": "error",
"element-permitted-content": "off",
"void": "error",
"no-self-closing": "error",
},
"transform": Object {},
}
......@@ -83,44 +83,44 @@ Array [
"messages": Array [
Object {
"column": 3,
"context": undefined,
"context": "i",
"line": 1,
"message": "End tag for <i> must not be omitted",
"message": "<i> must not be self-closed",
"offset": 2,
"ruleId": "void",
"ruleId": "no-self-closing",
"selector": "i",
"severity": 2,
"size": 2,
},
Object {
"column": 4,
"context": undefined,
"context": "i",
"line": 3,
"message": "End tag for <i> must not be omitted",
"message": "<i> must not be self-closed",
"offset": 43,
"ruleId": "void",
"ruleId": "no-self-closing",
"selector": "div > i",
"severity": 2,
"size": 2,
},
Object {
"column": 3,
"context": undefined,
"context": "i",
"line": 11,
"message": "End tag for <i> must not be omitted",
"offset": 324,
"ruleId": "void",
"message": "<i> must not be self-closed",
"offset": 335,
"ruleId": "no-self-closing",
"selector": "i:nth-child(3)",
"severity": 2,
"size": 2,
},
Object {
"column": 3,
"context": undefined,
"context": "i",
"line": 16,
"message": "End tag for <i> must not be omitted",
"offset": 417,
"ruleId": "void",
"message": "<i> must not be self-closed",
"offset": 439,
"ruleId": "no-self-closing",
"selector": "i:nth-child(5)",
"severity": 2,
"size": 2,
......@@ -129,7 +129,7 @@ Array [
"source": "<i/>Outside block, should trigger
<div>
<i/>Inside block, before directive, should trigger
<!-- [html-validate-disable-block void] -->
<!-- [html-validate-disable-block no-self-closing] -->
<i/>Inside block, after directive, shout not trigger
<div>
<i/>Inside block, after directive, shout not trigger
......@@ -139,11 +139,11 @@ Array [
<i/>Outside block, should trigger
<div>
<!-- [html-validate-disable-block void] -->
<!-- [html-validate-disable-block no-self-closing] -->
</div>
<i/>Outside block, should trigger
<!-- [html-validate-disable-block void] -->
<!-- [html-validate-disable-block no-self-closing] -->
Should handle when root element is parent but no children
",
"warningCount": 0,
......@@ -161,7 +161,7 @@ Object {
"rules": Object {
"deprecated": "error",
"element-permitted-content": "off",
"void": "error",
"no-self-closing": "error",
},
"transform": Object {},
}
......@@ -186,11 +186,11 @@ Array [
},
Object {
"column": 7,
"context": undefined,
"context": "blink",
"line": 1,
"message": "End tag for <blink> must not be omitted",
"message": "<blink> must not be self-closed",
"offset": 6,
"ruleId": "void",
"ruleId": "no-self-closing",
"selector": "blink",
"severity": 2,
"size": 2,
......@@ -198,7 +198,7 @@ Array [
],
"source": "<blink/>
<div>
<!-- [html-validate-disable-block void, deprecated] -->
<!-- [html-validate-disable-block no-self-closing, deprecated] -->
<blink/>
</div>
",
......@@ -217,7 +217,7 @@ Object {
"rules": Object {
"deprecated": "error",
"element-permitted-content": "off",
"void": "error",
"no-self-closing": "error",
},
"transform": Object {},
}
......@@ -231,22 +231,22 @@ Array [
"messages": Array [
Object {
"column": 3,
"context": undefined,
"context": "i",
"line": 1,
"message": "End tag for <i> must not be omitted",
"message": "<i> must not be self-closed",
"offset": 2,
"ruleId": "void",
"ruleId": "no-self-closing",
"selector": "i",
"severity": 2,
"size": 2,
},
Object {
"column": 3,
"context": undefined,
"context": "i",
"line": 5,
"message": "End tag for <i> must not be omitted",
"offset": 125,
"ruleId": "void",
"message": "<i> must not be self-closed",
"offset": 136,
"ruleId": "no-self-closing",
"selector": "i:nth-child(3)",
"severity": 2,
"size": 2,
......@@ -254,7 +254,7 @@ Array [
],
"source": "<i/>Before disable, should trigger
<!-- [html-validate-disable-next void] -->
<!-- [html-validate-disable-next no-self-closing] -->
<i/>First after disable, should not trigger
<i/>Second after disable, should trigger
",
......@@ -277,7 +277,7 @@ Object {
"rules": Object {
"deprecated": "error",
"element-permitted-content": "error",
"void": "error",
"no-self-closing": "error",
},
"transform": Object {},
}
......@@ -336,7 +336,7 @@ Object {
"rules": Object {
"deprecated": "error",
"element-permitted-content": "error",
"void": "error",
"no-self-closing": "error",
},
"transform": Object {},
}
......@@ -378,7 +378,7 @@ Object {
"rules": Object {
"deprecated": "error",
"element-permitted-content": "error",
"void": "error",
"no-self-closing": "error",
},
"transform": Object {},
}
......@@ -420,7 +420,7 @@ Object {
"rules": Object {
"deprecated": "error",
"element-permitted-content": "error",
"void": "off",
"no-self-closing": "error",
},
"transform": Object {},
}
......@@ -438,7 +438,7 @@ Object {
"rules": Object {
"deprecated": "error",
"element-permitted-content": "error",
"void": "warn",
"no-self-closing": "error",
},
"transform": Object {},
}
......
......@@ -199,7 +199,11 @@ describe("ConfigLoader", () => {
/* extract only relevant rules from configuration to avoid bloat when new
* rules are added to recommended config */
function filter(src: Config): ConfigData {
const whitelisted = ["void", "deprecated", "element-permitted-content"];
const whitelisted = [
"no-self-closing",
"deprecated",
"element-permitted-content",
];
const data = src.get();
data.rules = Object.keys(data.rules)
.filter(key => whitelisted.includes(key))
......
......@@ -29,6 +29,7 @@ module.exports = {
"no-inline-style": "error",
"no-raw-characters": "error",
"no-redundant-role": "error",
"no-self-closing": "error",
"no-trailing-whitespace": "error",
"prefer-button": "error",
"prefer-native-element": "error",
......
......@@ -77,6 +77,7 @@ describe("Engine", () => {
extends: ["html-validate:recommended"],
rules: {
deprecated: "off",
void: "off",
"void-content": "off",
},
});
......@@ -186,12 +187,12 @@ describe("Engine", () => {
it('"enable" should enable rule', () => {
const source: Source[] = [
inline(
"<!-- [html-validate-disable void] --><i/><!-- [html-validate-enable void] --><i/>"
"<!-- [html-validate-disable no-self-closing] --><i/><!-- [html-validate-enable no-self-closing] --><i/>"
),
];
const report = engine.lint(source);
expect(report).toBeInvalid();
expect(report).toHaveErrors([{ ruleId: "void", column: 80 }]);
expect(report).toHaveErrors([{ ruleId: "no-self-closing", column: 102 }]);
});
it('"enable" set severity to error if off', () => {
......@@ -220,15 +221,15 @@ describe("Engine", () => {
it('"disable-block" should disable rule for all subsequent occurrences until block closes', () => {
const source: Source[] = [
inline(
"<i/><div><i/><!-- [html-validate-disable-block void] --><i/><i/></div><i/>"
"<i/><div><i/><!-- [html-validate-disable-block no-self-closing] --><i/><i/></div><i/>"
),
];
const report = engine.lint(source);
expect(report).toBeInvalid();
expect(report).toHaveErrors([
{ ruleId: "void", column: 3 },
{ ruleId: "void", column: 12 },
{ ruleId: "void", column: 73 },
{ ruleId: "no-self-closing", column: 3 },
{ ruleId: "no-self-closing", column: 12 },
{ ruleId: "no-self-closing", column: 84 },
]);
});
......@@ -248,7 +249,9 @@ describe("Engine", () => {
it('"disable-block" should handle empty block', () => {
const source: Source[] = [
inline("<div><!-- [html-validate-disable-block void] --></div>"),
inline(
"<div><!-- [html-validate-disable-block no-self-closing] --></div>"
),
];
const report = engine.lint(source);
expect(report).toBeValid();
......@@ -256,7 +259,7 @@ describe("Engine", () => {
it('"disable-block" should handle root element', () => {
const source: Source[] = [
inline("<!-- [html-validate-disable-block void] --><i/>"),
inline("<!-- [html-validate-disable-block no-self-closing] --><i/>"),
];
const report = engine.lint(source);
expect(report).toBeValid();
......@@ -264,7 +267,7 @@ describe("Engine", () => {
it('"disable-block" should handle empty root element', () => {
const source: Source[] = [
inline("<!-- [html-validate-disable-block void] -->"),
inline("<!-- [html-validate-disable-block no-self-closing] -->"),
];
const report = engine.lint(source);
expect(report).toBeValid();
......
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`rule no-self-closing should contain contextual documentation 1`] = `
Object {
"description": "Self-closing elements are disallowed. Use regular end tag <div></div> instead of self-closing <div/>.",
"url": "https://html-validate.org/rules/no-self-closing.html",
}
`;
exports[`rule no-self-closing should contain documentation 1`] = `
Object {
"description": "Self-closing elements are disallowed. Use regular end tag <element></element> instead of self-closing <element/>.",
"url": "https://html-validate.org/rules/no-self-closing.html",
}
`;
import HtmlValidate from "../htmlvalidate";
import "../matchers";
describe("rule no-self-closing", () => {
let htmlvalidate: HtmlValidate;
describe("default configuration", () => {
beforeAll(() => {
htmlvalidate = new HtmlValidate({
rules: { "no-self-closing": "error" },
});
});
it("should not report error when element has end tag", () => {
const report = htmlvalidate.validateString("<div></div>");
expect(report).toBeValid();
});
it("should not report error for foreign elements", () => {
const report = htmlvalidate.validateString("<svg/>");
expect(report).toBeValid();
});
it("should not report error for xml namespaces", () => {
const report = htmlvalidate.validateString("<xi:include/>");
expect(report).toBeValid();
});
it("should not report error for void", () => {
const report = htmlvalidate.validateString("<input/>");
expect(report).toBeValid();
});
it("should not report error when void element has end tag", () => {
const report = htmlvalidate.validateString("<input></input>");
expect(report).toBeValid();
});
it("should report error when element is self-closed", () => {
const report = htmlvalidate.validateString("<div/>");
expect(report).toBeInvalid();
expect(report).toHaveError(
"no-self-closing",
"<div> must not be self-closed"
);
});
it("should report error for unknown elements", () => {
const report = htmlvalidate.validateString("<custom-element/>");
expect(report).toHaveError(
"no-self-closing",
"<custom-element> must not be self-closed"
);
});
});
describe("ignoreForeign false", () => {
beforeAll(() => {
htmlvalidate = new HtmlValidate({
rules: { "no-self-closing": ["error", { ignoreForeign: false }] },
});
});
it("should report error for foreign elements", () => {
const report = htmlvalidate.validateString("<svg/>");
expect(report).toBeInvalid();
expect(report).toHaveError(
"no-self-closing",
"<svg> must not be self-closed"
);
});
});
describe("ignoreXML false", () => {
beforeAll(() => {
htmlvalidate = new HtmlValidate({
rules: { "no-self-closing": ["error", { ignoreXML: false }] },
});
});
it("should report error for elements in xml namespace", () => {
const report = htmlvalidate.validateString("<xi:include/>");
expect(report).toBeInvalid();
expect(report).toHaveError(
"no-self-closing",
"<xi:include> must not be self-closed"
);
});
});
it("should contain documentation", () => {
htmlvalidate = new HtmlValidate({
rules: { "no-self-closing": "error" },
});
expect(
htmlvalidate.getRuleDocumentation("no-self-closing")
).toMatchSnapshot();
});
it("should contain contextual documentation", () => {
htmlvalidate = new HtmlValidate({
rules: { "no-self-closing": "error" },
});
const context = "div";
expect(
htmlvalidate.getRuleDocumentation("no-self-closing", null, context)
).toMatchSnapshot();
});
});
import { HtmlElement, NodeClosed } from "../dom";
import { TagCloseEvent } from "../event";
import { Rule, RuleDocumentation, ruleDocumentationUrl } from "../rule";
interface RuleOptions {
ignoreForeign: boolean;
ignoreXML: boolean;
}
const xmlns = /^(.+):.+$/;
const defaults: RuleOptions = {
ignoreForeign: true,
ignoreXML: true,
};
class NoSelfClosing extends Rule<string, RuleOptions> {
public constructor(options: RuleOptions) {
super(Object.assign({}, defaults, options));
}
public documentation(tagName: string): RuleDocumentation {
tagName = tagName || "element";
return {
description: `Self-closing elements are disallowed. Use regular end tag <${tagName}></${tagName}> instead of self-closing <${tagName}/>.`,
url: ruleDocumentationUrl(__filename),
};
}
public setup(): void {
this.on("tag:close", (event: TagCloseEvent) => {
const active = event.previous; // The current active element (that is, the current element on the stack)
if (!isRelevant(active, this.options)) {
return;
}
this.validateElement(active);
});
}
private validateElement(node: HtmlElement): void {
if (node.closed !== NodeClosed.VoidSelfClosed) {
return;
}
this.report(
node,
`<${node.tagName}> must not be self-closed`,
null,