diff --git a/packages/lit-analyzer/src/lib/analyze/parse/document/text-document/html-document/parse-html-node/parse-html-node.ts b/packages/lit-analyzer/src/lib/analyze/parse/document/text-document/html-document/parse-html-node/parse-html-node.ts index affe476d..f005f42f 100644 --- a/packages/lit-analyzer/src/lib/analyze/parse/document/text-document/html-document/parse-html-node/parse-html-node.ts +++ b/packages/lit-analyzer/src/lib/analyze/parse/document/text-document/html-document/parse-html-node/parse-html-node.ts @@ -5,9 +5,6 @@ import { getSourceLocation, IP5TagNode, P5Node } from "../parse-html-p5/parse-ht import { parseHtmlNodeAttrs } from "./parse-html-attribute.js"; import { ParseHtmlContext } from "./parse-html-context.js"; -// List taken from https://html.spec.whatwg.org/multipage/syntax.html#void-elements -const VOID_ELEMENTS = new Set(["area", "base", "br", "col", "embed", "hr", "img", "input", "link", "meta", "source", "track", "wbr"]); - /** * Parses multiple p5Nodes into multiple html nodes. * @param p5Nodes @@ -52,7 +49,6 @@ export function parseHtmlNode(p5Node: IP5TagNode, parent: HtmlNode | undefined, const htmlNodeBase: IHtmlNodeBase = { tagName: p5Node.tagName.toLowerCase(), - selfClosed: isSelfClosed(p5Node, context), attributes: [], location: makeHtmlNodeLocation(p5Node, context), children: [], @@ -72,29 +68,6 @@ export function parseHtmlNode(p5Node: IP5TagNode, parent: HtmlNode | undefined, return htmlNode; } -/** - * Returns if this node was explicitly self-closed or void. - * - * Note: Self-closing tags do not actually exist in HTML - * https://developer.mozilla.org/en-US/docs/Glossary/Void_element#self-closing_tags - * - * Therefore this function returns `true` if `p5Node` is - * a void element, or was explicitly self-closed using XML/JSX - * syntax. If the node is implicitly closed, for example a - * `p` element followed by a `div`, then `false` is returned. - * - * @param p5Node - * @param context - */ -function isSelfClosed(p5Node: IP5TagNode, context: ParseHtmlContext) { - if (VOID_ELEMENTS.has(p5Node.tagName.toLowerCase())) { - return true; - } - const loc = getSourceLocation(p5Node)!; - const isSelfClosed = context.html[loc.startTag.endOffset - 2] === "/"; - return isSelfClosed; -} - /** * Creates source code location from a p5Node. * @param p5Node diff --git a/packages/lit-analyzer/src/lib/analyze/types/html-node/html-node-types.ts b/packages/lit-analyzer/src/lib/analyze/types/html-node/html-node-types.ts index 636c046c..daca7910 100644 --- a/packages/lit-analyzer/src/lib/analyze/types/html-node/html-node-types.ts +++ b/packages/lit-analyzer/src/lib/analyze/types/html-node/html-node-types.ts @@ -20,11 +20,6 @@ export interface IHtmlNodeBase { attributes: HtmlNodeAttr[]; parent?: HtmlNode; children: HtmlNode[]; - /** - * Is true when an HTML node is either a void element, or was - * explicitly closed with self-closing XML syntax. - */ - selfClosed: boolean; document: HtmlDocument; } diff --git a/packages/lit-analyzer/src/lib/rules/no-unclosed-tag.ts b/packages/lit-analyzer/src/lib/rules/no-unclosed-tag.ts index 7ddd6b9a..bcfddd64 100644 --- a/packages/lit-analyzer/src/lib/rules/no-unclosed-tag.ts +++ b/packages/lit-analyzer/src/lib/rules/no-unclosed-tag.ts @@ -2,6 +2,29 @@ import { RuleModule } from "../analyze/types/rule/rule-module.js"; import { isCustomElementTagName } from "../analyze/util/is-valid-name.js"; import { rangeFromHtmlNode } from "../analyze/util/range-util.js"; +// List taken from https://html.spec.whatwg.org/multipage/syntax.html#void-elements +// and parse5 list of void elements: https://github.com/inikulin/parse5/blob/86f09edd5a6840ab2269680b0eef2945e78c38fd/packages/parse5/lib/serializer/index.ts#L7-L26 +const VOID_ELEMENTS = new Set([ + "area", + "base", + "basefont", + "bgsound", + "br", + "col", + "embed", + "frame", + "hr", + "img", + "input", + "keygen", + "link", + "meta", + "param", + "source", + "track", + "wbr" +]); + /** * This rule validates that all tags are closed properly. */ @@ -11,22 +34,18 @@ const rule: RuleModule = { priority: "low" }, visitHtmlNode(htmlNode, context) { - const { - selfClosed, - location: { endTag } - } = htmlNode; - if (!selfClosed && endTag == null) { - // Report specifically that a custom element cannot be self closing - // if the user is trying to close a custom element. - const isCustomElement = isCustomElementTagName(htmlNode.tagName); - - context.report({ - location: rangeFromHtmlNode(htmlNode), - message: `This tag isn't closed.${isCustomElement ? " Custom elements cannot be self closing." : ""}` - }); + if (VOID_ELEMENTS.has(htmlNode.tagName.toLowerCase()) || htmlNode.location.endTag != null) { + return; } - return; + // Report specifically that a custom element cannot be self closing + // if the user is trying to close a custom element. + const isCustomElement = isCustomElementTagName(htmlNode.tagName); + + context.report({ + location: rangeFromHtmlNode(htmlNode), + message: `This tag isn't closed.${isCustomElement ? " Custom elements cannot be self closing." : ""}` + }); } }; diff --git a/packages/lit-analyzer/src/test/rules/no-unclosed-tag.ts b/packages/lit-analyzer/src/test/rules/no-unclosed-tag.ts index cea6496d..3ce43a9e 100644 --- a/packages/lit-analyzer/src/test/rules/no-unclosed-tag.ts +++ b/packages/lit-analyzer/src/test/rules/no-unclosed-tag.ts @@ -7,20 +7,20 @@ tsTest("Report unclosed tags", t => { hasDiagnostic(t, diagnostics, "no-unclosed-tag"); }); -tsTest("Don't report self closed tags", t => { - const { diagnostics } = getDiagnostics("html``", { rules: { "no-unclosed-tag": true } }); +tsTest("Don't report void elements", t => { + const { diagnostics } = getDiagnostics("html``", { rules: { "no-unclosed-tag": true } }); hasNoDiagnostics(t, diagnostics); }); -tsTest("Don't report void tags", t => { - const { diagnostics } = getDiagnostics("html``", { rules: { "no-unclosed-tag": true } }); +tsTest("Don't report void elements with self closing syntax", t => { + const { diagnostics } = getDiagnostics("html``", { rules: { "no-unclosed-tag": true } }); hasNoDiagnostics(t, diagnostics); }); // The `

` tag will be closed automatically if immediately followed by a lot of other elements, // including `

`. // Ref: https://html.spec.whatwg.org/multipage/grouping-content.html#the-p-element -tsTest("Report unclosed 'p' tag that is implicitly closed via tag omission", t => { +tsTest("Report unclosed 'p' tag that was implicitly closed via tag omission", t => { const { diagnostics } = getDiagnostics("html`

`", { rules: { "no-unclosed-tag": true } }); hasDiagnostic(t, diagnostics, "no-unclosed-tag"); }); @@ -30,14 +30,18 @@ tsTest("Report unclosed 'p' tag that is implicitly closed via tag omission conta hasDiagnostic(t, diagnostics, "no-unclosed-tag"); }); -// Self-closing tags do not exist in HTML, but we can use them to check -// that the user intended that the tag be closed. -tsTest("Don't report self closing 'p' tag", t => { +// Self-closing tags do not exist in HTML. They are only valid in SVG and MathML. +tsTest("Report non-void element using self closing syntax", t => { const { diagnostics } = getDiagnostics("html`

`", { rules: { "no-unclosed-tag": true } }); - hasNoDiagnostics(t, diagnostics); + hasDiagnostic(t, diagnostics, "no-unclosed-tag"); }); -tsTest("Don't report self closing 'p' tag containing text content", t => { +tsTest("Report self closing 'p' tag containing text content", t => { const { diagnostics } = getDiagnostics("html`

Unclosed Content

`", { rules: { "no-unclosed-tag": true } }); + hasDiagnostic(t, diagnostics, "no-unclosed-tag"); +}); + +tsTest("Don't report explicit closing 'p' tag containing text content", t => { + const { diagnostics } = getDiagnostics("html`

Unclosed Content

`", { rules: { "no-unclosed-tag": true } }); hasNoDiagnostics(t, diagnostics); });