Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 13 additions & 8 deletions javascript/packages/linter/src/rules/html-no-self-closing.ts
Original file line number Diff line number Diff line change
@@ -1,25 +1,30 @@
import { ParserRule } from "../types.js"
import { BaseRuleVisitor, getTagName, isVoidElement } from "./rule-utils.js"
import { BaseRuleVisitor, isVoidElement } from "./rule-utils.js"
import { getTagName } from "@herb-tools/core"

import type { LintContext, LintOffense } from "../types.js"
import type { HTMLOpenTagNode, ParseResult } from "@herb-tools/core"
import type { HTMLOpenTagNode, HTMLElementNode, ParseResult } from "@herb-tools/core"

class NoSelfClosingVisitor extends BaseRuleVisitor {
visitHTMLElementNode(node: HTMLElementNode): void {
if (getTagName(node) === "svg") {
this.visit(node.open_tag)
} else {
this.visitChildNodes(node)
}
}

visitHTMLOpenTagNode(node: HTMLOpenTagNode): void {
if (node.tag_closing?.value === "/>") {
const tagName = getTagName(node)

const shouldBeVoid = tagName ? isVoidElement(tagName) : false
const instead = shouldBeVoid ? `Use \`<${tagName}>\` instead.` : `Use \`<${tagName}></${tagName}>\` instead.`
const instead = isVoidElement(tagName) ? `<${tagName}>` : `<${tagName}></${tagName}>`

this.addOffense(
`Self-closing syntax \`<${tagName} />\` is not allowed in HTML. ${instead}`,
`Use \`${instead}\` instead of self-closing \`<${tagName} />\` for HTML compatibility.`,
node.location,
"error"
)
}

super.visitHTMLOpenTagNode(node)
}
}

Expand Down

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

70 changes: 54 additions & 16 deletions javascript/packages/linter/test/rules/html-no-self-closing.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,20 +32,22 @@ describe("html-no-self-closing", () => {
<span />
<section />
<custom-element />
<svg />
`;
const linter = new Linter(Herb, [HTMLNoSelfClosingRule]);
const lintResult = linter.lint(html);

expect(lintResult.errors).toBe(4);
expect(lintResult.offenses).toHaveLength(4);
expect(lintResult.errors).toBe(5);
expect(lintResult.offenses).toHaveLength(5);

expect(lintResult.offenses[0].rule).toBe("html-no-self-closing");
expect(lintResult.offenses[0].severity).toBe("error");

expect(lintResult.offenses[0].message).toBe('Self-closing syntax `<div />` is not allowed in HTML. Use `<div></div>` instead.')
expect(lintResult.offenses[1].message).toBe('Self-closing syntax `<span />` is not allowed in HTML. Use `<span></span>` instead.')
expect(lintResult.offenses[2].message).toBe('Self-closing syntax `<section />` is not allowed in HTML. Use `<section></section>` instead.')
expect(lintResult.offenses[3].message).toBe('Self-closing syntax `<custom-element />` is not allowed in HTML. Use `<custom-element></custom-element>` instead.')
expect(lintResult.offenses[0].message).toBe('Use `<div></div>` instead of self-closing `<div />` for HTML compatibility.')
expect(lintResult.offenses[1].message).toBe('Use `<span></span>` instead of self-closing `<span />` for HTML compatibility.')
expect(lintResult.offenses[2].message).toBe('Use `<section></section>` instead of self-closing `<section />` for HTML compatibility.')
expect(lintResult.offenses[3].message).toBe('Use `<custom-element></custom-element>` instead of self-closing `<custom-element />` for HTML compatibility.')
expect(lintResult.offenses[4].message).toBe('Use `<svg></svg>` instead of self-closing `<svg />` for HTML compatibility.')
});

test("fails for self-closing void elements", () => {
Expand All @@ -64,10 +66,10 @@ describe("html-no-self-closing", () => {
expect(lintResult.offenses[0].rule).toBe("html-no-self-closing");
expect(lintResult.offenses[0].severity).toBe("error");

expect(lintResult.offenses[0].message).toBe('Self-closing syntax `<img />` is not allowed in HTML. Use `<img>` instead.')
expect(lintResult.offenses[1].message).toBe('Self-closing syntax `<input />` is not allowed in HTML. Use `<input>` instead.')
expect(lintResult.offenses[2].message).toBe('Self-closing syntax `<br />` is not allowed in HTML. Use `<br>` instead.')
expect(lintResult.offenses[3].message).toBe('Self-closing syntax `<hr />` is not allowed in HTML. Use `<hr>` instead.')
expect(lintResult.offenses[0].message).toBe('Use `<img>` instead of self-closing `<img />` for HTML compatibility.')
expect(lintResult.offenses[1].message).toBe('Use `<input>` instead of self-closing `<input />` for HTML compatibility.')
expect(lintResult.offenses[2].message).toBe('Use `<br>` instead of self-closing `<br />` for HTML compatibility.')
expect(lintResult.offenses[3].message).toBe('Use `<hr>` instead of self-closing `<hr />` for HTML compatibility.')
});

test("passes for mixed correct and incorrect tags", () => {
Expand All @@ -83,8 +85,8 @@ describe("html-no-self-closing", () => {
expect(lintResult.errors).toBe(2);
expect(lintResult.offenses).toHaveLength(2);

expect(lintResult.offenses[0].message).toBe('Self-closing syntax `<span />` is not allowed in HTML. Use `<span></span>` instead.')
expect(lintResult.offenses[1].message).toBe('Self-closing syntax `<input />` is not allowed in HTML. Use `<input>` instead.')
expect(lintResult.offenses[0].message).toBe('Use `<span></span>` instead of self-closing `<span />` for HTML compatibility.')
expect(lintResult.offenses[1].message).toBe('Use `<input>` instead of self-closing `<input />` for HTML compatibility.')
});

test("passes for nested non-self-closing tags", () => {
Expand Down Expand Up @@ -114,8 +116,8 @@ describe("html-no-self-closing", () => {
expect(lintResult.errors).toBe(2);
expect(lintResult.offenses).toHaveLength(2);

expect(lintResult.offenses[0].message).toBe('Self-closing syntax `<span />` is not allowed in HTML. Use `<span></span>` instead.')
expect(lintResult.offenses[1].message).toBe('Self-closing syntax `<section />` is not allowed in HTML. Use `<section></section>` instead.')
expect(lintResult.offenses[0].message).toBe('Use `<span></span>` instead of self-closing `<span />` for HTML compatibility.')
expect(lintResult.offenses[1].message).toBe('Use `<section></section>` instead of self-closing `<section />` for HTML compatibility.')
});

test("passes for custom elements without self-closing", () => {
Expand All @@ -141,8 +143,8 @@ describe("html-no-self-closing", () => {
expect(lintResult.errors).toBe(2);
expect(lintResult.offenses).toHaveLength(2);

expect(lintResult.offenses[0].message).toBe('Self-closing syntax `<custom-element />` is not allowed in HTML. Use `<custom-element></custom-element>` instead.')
expect(lintResult.offenses[1].message).toBe('Self-closing syntax `<another-custom />` is not allowed in HTML. Use `<another-custom></another-custom>` instead.')
expect(lintResult.offenses[0].message).toBe('Use `<custom-element></custom-element>` instead of self-closing `<custom-element />` for HTML compatibility.')
expect(lintResult.offenses[1].message).toBe('Use `<another-custom></another-custom>` instead of self-closing `<another-custom />` for HTML compatibility.')
});

test("passes for void elements without self-closing", () => {
Expand All @@ -158,4 +160,40 @@ describe("html-no-self-closing", () => {
expect(lintResult.errors).toBe(0);
expect(lintResult.offenses).toHaveLength(0);
});

test("passes for self-closing elements inside SVG", () => {
const html = `
<div class="flex items-center text-xs text-gray-500 mt-1">
<svg class="w-3 h-3 mr-1 fill-gray-400" viewBox="0 0 24 24">
<path d="M12 2C8.13 2 5 5.13 5 9c0 5.25 7 13 7 13s7-7.75 7-13c0-3.87-3.13-7-7-7zm0 9.5c-1.38 0-2.5-1.12-2.5-2.5s1.12-2.5 2.5-2.5 2.5 1.12 2.5 2.5-1.12 2.5-2.5 2.5z" />
<circle cx="12" cy="12" r="10" />
<rect x="0" y="0" width="24" height="24" />
</svg>
</div>
`;
const linter = new Linter(Herb, [HTMLNoSelfClosingRule]);
const lintResult = linter.lint(html);

expect(lintResult.errors).toBe(0);
expect(lintResult.offenses).toHaveLength(0);
});

test("fails for self-closing elements outside SVG but passes inside SVG", () => {
const html = `
<div />
<svg class="w-3 h-3" viewBox="0 0 24 24">
<path d="M12 2C8.13 2 5 5.13 5 9c0 5.25 7 13 7 13s7-7.75 7-13c0-3.87-3.13-7-7-7zm0 9.5c-1.38 0-2.5-1.12-2.5-2.5s1.12-2.5 2.5-2.5 2.5 1.12 2.5 2.5-1.12 2.5-2.5 2.5z" />
<circle cx="12" cy="12" r="10" />
</svg>
<span />
`;
const linter = new Linter(Herb, [HTMLNoSelfClosingRule]);
const lintResult = linter.lint(html);

expect(lintResult.errors).toBe(2);
expect(lintResult.offenses).toHaveLength(2);

expect(lintResult.offenses[0].message).toBe('Use `<div></div>` instead of self-closing `<div />` for HTML compatibility.');
expect(lintResult.offenses[1].message).toBe('Use `<span></span>` instead of self-closing `<span />` for HTML compatibility.');
});
});
Loading