From c66a43940aee4c24f4c003a50b433e64d316e40a Mon Sep 17 00:00:00 2001 From: 7nik Date: Tue, 20 May 2025 17:50:54 +0300 Subject: [PATCH] feat: warn on implicitly closed tags (#15932) * warn on implicitly closed tags * lint * changeset * separate tests for parent/sibling case * account for attributes in opening tag * tweak * update message --------- Co-authored-by: 7nik Co-authored-by: Rich Harris --- .changeset/blue-experts-tell.md | 5 ++++ .../.generated/compile-warnings.md | 19 ++++++++++++++ .../messages/compile-warnings/template.md | 17 ++++++++++++ .../compiler/phases/1-parse/state/element.js | 13 +++++++++- packages/svelte/src/compiler/warnings.js | 11 ++++++++ .../implicitly-closed-by-parent/input.svelte | 6 +++++ .../implicitly-closed-by-parent/warnings.json | 26 +++++++++++++++++++ .../implicitly-closed-by-sibling/input.svelte | 9 +++++++ .../warnings.json | 26 +++++++++++++++++++ 9 files changed, 131 insertions(+), 1 deletion(-) create mode 100644 .changeset/blue-experts-tell.md create mode 100644 packages/svelte/tests/validator/samples/implicitly-closed-by-parent/input.svelte create mode 100644 packages/svelte/tests/validator/samples/implicitly-closed-by-parent/warnings.json create mode 100644 packages/svelte/tests/validator/samples/implicitly-closed-by-sibling/input.svelte create mode 100644 packages/svelte/tests/validator/samples/implicitly-closed-by-sibling/warnings.json diff --git a/.changeset/blue-experts-tell.md b/.changeset/blue-experts-tell.md new file mode 100644 index 0000000000..a12fb126d1 --- /dev/null +++ b/.changeset/blue-experts-tell.md @@ -0,0 +1,5 @@ +--- +'svelte': minor +--- + +feat: warn on implicitly closed tags diff --git a/documentation/docs/98-reference/.generated/compile-warnings.md b/documentation/docs/98-reference/.generated/compile-warnings.md index 7069f90206..b579d38602 100644 --- a/documentation/docs/98-reference/.generated/compile-warnings.md +++ b/documentation/docs/98-reference/.generated/compile-warnings.md @@ -632,6 +632,25 @@ In some situations a selector may target an element that is not 'visible' to the ``` +### element_implicitly_closed + +``` +This element is implicitly closed by the following `%tag%`, which can cause an unexpected DOM structure. Add an explicit `%closing%` to avoid surprises. +``` + +In HTML, some elements are implicitly closed by another element. For example, you cannot nest a `

` inside another `

`: + +```html + +

hello

+ + +

+

hello

+``` + +Similarly, a parent element's closing tag will implicitly close all child elements, even if the ` `<%name%>` will be treated as an HTML element unless it begins with a capital letter +## element_implicitly_closed + +> This element is implicitly closed by the following `%tag%`, which can cause an unexpected DOM structure. Add an explicit `%closing%` to avoid surprises. + +In HTML, some elements are implicitly closed by another element. For example, you cannot nest a `

` inside another `

`: + +```html + +

hello

+ + +

+

hello

+``` + +Similarly, a parent element's closing tag will implicitly close all child elements, even if the ` Self-closing HTML tags for non-void elements are ambiguous — use `<%name% ...>` rather than `<%name% ... />` diff --git a/packages/svelte/src/compiler/phases/1-parse/state/element.js b/packages/svelte/src/compiler/phases/1-parse/state/element.js index d20369b0d9..6b6c9160d8 100644 --- a/packages/svelte/src/compiler/phases/1-parse/state/element.js +++ b/packages/svelte/src/compiler/phases/1-parse/state/element.js @@ -93,7 +93,16 @@ export default function element(parser) { } } - if (parent.type !== 'RegularElement' && !parser.loose) { + if (parent.type === 'RegularElement') { + if (!parser.last_auto_closed_tag || parser.last_auto_closed_tag.tag !== name) { + const end = parent.fragment.nodes[0]?.start ?? start; + w.element_implicitly_closed( + { start: parent.start, end }, + ``, + `` + ); + } + } else if (!parser.loose) { if (parser.last_auto_closed_tag && parser.last_auto_closed_tag.tag === name) { e.element_invalid_closing_tag_autoclosed(start, name, parser.last_auto_closed_tag.reason); } else { @@ -186,6 +195,8 @@ export default function element(parser) { parser.allow_whitespace(); if (parent.type === 'RegularElement' && closing_tag_omitted(parent.name, name)) { + const end = parent.fragment.nodes[0]?.start ?? start; + w.element_implicitly_closed({ start: parent.start, end }, `<${name}>`, ``); parent.end = start; parser.pop(); parser.last_auto_closed_tag = { diff --git a/packages/svelte/src/compiler/warnings.js b/packages/svelte/src/compiler/warnings.js index c281433213..2a8316308c 100644 --- a/packages/svelte/src/compiler/warnings.js +++ b/packages/svelte/src/compiler/warnings.js @@ -114,6 +114,7 @@ export const codes = [ 'bind_invalid_each_rest', 'block_empty', 'component_name_lowercase', + 'element_implicitly_closed', 'element_invalid_self_closing_tag', 'event_directive_deprecated', 'node_invalid_placement_ssr', @@ -746,6 +747,16 @@ export function component_name_lowercase(node, name) { w(node, 'component_name_lowercase', `\`<${name}>\` will be treated as an HTML element unless it begins with a capital letter\nhttps://svelte.dev/e/component_name_lowercase`); } +/** + * This element is implicitly closed by the following `%tag%`, which can cause an unexpected DOM structure. Add an explicit `%closing%` to avoid surprises. + * @param {null | NodeLike} node + * @param {string} tag + * @param {string} closing + */ +export function element_implicitly_closed(node, tag, closing) { + w(node, 'element_implicitly_closed', `This element is implicitly closed by the following \`${tag}\`, which can cause an unexpected DOM structure. Add an explicit \`${closing}\` to avoid surprises.\nhttps://svelte.dev/e/element_implicitly_closed`); +} + /** * Self-closing HTML tags for non-void elements are ambiguous — use `<%name% ...>` rather than `<%name% ... />` * @param {null | NodeLike} node diff --git a/packages/svelte/tests/validator/samples/implicitly-closed-by-parent/input.svelte b/packages/svelte/tests/validator/samples/implicitly-closed-by-parent/input.svelte new file mode 100644 index 0000000000..f67eba18b8 --- /dev/null +++ b/packages/svelte/tests/validator/samples/implicitly-closed-by-parent/input.svelte @@ -0,0 +1,6 @@ +
+ +
+
+

hello

+
diff --git a/packages/svelte/tests/validator/samples/implicitly-closed-by-parent/warnings.json b/packages/svelte/tests/validator/samples/implicitly-closed-by-parent/warnings.json new file mode 100644 index 0000000000..1316a2b65b --- /dev/null +++ b/packages/svelte/tests/validator/samples/implicitly-closed-by-parent/warnings.json @@ -0,0 +1,26 @@ +[ + { + "code": "element_implicitly_closed", + "message": "This element is implicitly closed by the following ``, which can cause an unexpected DOM structure. Add an explicit `` to avoid surprises.", + "start": { + "line": 1, + "column": 6 + }, + "end": { + "line": 1, + "column": 25 + } + }, + { + "code": "element_implicitly_closed", + "message": "This element is implicitly closed by the following ``, which can cause an unexpected DOM structure. Add an explicit `` to avoid surprises.", + "start": { + "line": 4, + "column": 1 + }, + "end": { + "line": 4, + "column": 20 + } + } +] diff --git a/packages/svelte/tests/validator/samples/implicitly-closed-by-sibling/input.svelte b/packages/svelte/tests/validator/samples/implicitly-closed-by-sibling/input.svelte new file mode 100644 index 0000000000..7721f2f380 --- /dev/null +++ b/packages/svelte/tests/validator/samples/implicitly-closed-by-sibling/input.svelte @@ -0,0 +1,9 @@ +
+

+ +

+
+ +
+

+
diff --git a/packages/svelte/tests/validator/samples/implicitly-closed-by-sibling/warnings.json b/packages/svelte/tests/validator/samples/implicitly-closed-by-sibling/warnings.json new file mode 100644 index 0000000000..6ea36c5a50 --- /dev/null +++ b/packages/svelte/tests/validator/samples/implicitly-closed-by-sibling/warnings.json @@ -0,0 +1,26 @@ +[ + { + "code": "element_implicitly_closed", + "message": "This element is implicitly closed by the following `

`, which can cause an unexpected DOM structure. Add an explicit `

` to avoid surprises.", + "start": { + "line": 2, + "column": 1 + }, + "end": { + "line": 2, + "column": 18 + } + }, + { + "code": "element_implicitly_closed", + "message": "This element is implicitly closed by the following `

`, which can cause an unexpected DOM structure. Add an explicit `

` to avoid surprises.", + "start": { + "line": 8, + "column": 1 + }, + "end": { + "line": 8, + "column": 18 + } + } +]