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 `` was a typo and you meant to create a _new_ element. To avoid ambiguity, it's always a good idea to have an explicit closing tag.
+
### element_invalid_self_closing_tag
```
diff --git a/packages/svelte/messages/compile-warnings/template.md b/packages/svelte/messages/compile-warnings/template.md
index c1675e5995..d61a61d950 100644
--- a/packages/svelte/messages/compile-warnings/template.md
+++ b/packages/svelte/messages/compile-warnings/template.md
@@ -30,6 +30,23 @@
> `<%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 `` was a typo and you meant to create a _new_ element. To avoid ambiguity, it's always a good idea to have an explicit closing tag.
+
## element_invalid_self_closing_tag
> Self-closing HTML tags for non-void elements are ambiguous — use `<%name% ...>%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 },
+ `${name}>`,
+ `${parent.name}>`
+ );
+ }
+ } 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.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% ...>%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
+ }
+ }
+]