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 eab8ca82..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
@@ -49,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: [],
@@ -69,17 +68,6 @@ export function parseHtmlNode(p5Node: IP5TagNode, parent: HtmlNode | undefined,
return htmlNode;
}
-/**
- * Returns if this node is self-closed.
- * @param p5Node
- * @param context
- */
-function isSelfClosed(p5Node: IP5TagNode, context: ParseHtmlContext) {
- const isEmpty = p5Node.childNodes == null || p5Node.childNodes.length === 0;
- const isSelfClosed = getSourceLocation(p5Node)!.startTag.endOffset === getSourceLocation(p5Node)!.endOffset;
- return isEmpty && 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 1046bc6c..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,7 +20,6 @@ export interface IHtmlNodeBase {
attributes: HtmlNodeAttr[];
parent?: HtmlNode;
children: HtmlNode[];
- 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 04ded8a2..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,18 +34,18 @@ const rule: RuleModule = {
priority: "low"
},
visitHtmlNode(htmlNode, context) {
- if (!htmlNode.selfClosed && htmlNode.location.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-nullable-attribute-binding.ts b/packages/lit-analyzer/src/test/rules/no-nullable-attribute-binding.ts
index 3e788653..b0fed9a4 100644
--- a/packages/lit-analyzer/src/test/rules/no-nullable-attribute-binding.ts
+++ b/packages/lit-analyzer/src/test/rules/no-nullable-attribute-binding.ts
@@ -11,7 +11,7 @@ tsTest("Cannot assign 'undefined' in attribute binding", t => {
tsTest("Can assign 'undefined' in property binding", t => {
const { diagnostics } = getDiagnostics([
makeElement({ slots: ["foo: number | undefined"] }),
- 'html`
` tag will be closed automatically if immediately followed by a lot of other elements, +// including `
Unclosed Content
`", { rules: { "no-unclosed-tag": true } }); + hasDiagnostic(t, diagnostics, "no-unclosed-tag"); +}); + +// Regeression test for https://github.com/runem/lit-analyzer/issues/283 +tsTest("Report 'p' tag that is implicitly closed via tag omission containing a space", t => { + // Note, the browser will parse this case into: `` which can be + // unexpected, but technically means the first `
` tag is not explicitly closed. + const { diagnostics } = getDiagnostics("html`
`", { rules: { "no-unclosed-tag": true } }); + hasDiagnostic(t, diagnostics, "no-unclosed-tag"); +}); + +// 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 } }); + hasDiagnostic(t, diagnostics, "no-unclosed-tag"); +}); + +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); +});