Skip to content

feat: warn on implicitly closed tags #15932

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 7 commits into from
May 20, 2025
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
5 changes: 5 additions & 0 deletions .changeset/blue-experts-tell.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'svelte': minor
---

feat: warn on implicitly closed tags
19 changes: 19 additions & 0 deletions documentation/docs/98-reference/.generated/compile-warnings.md
Original file line number Diff line number Diff line change
Expand Up @@ -632,6 +632,25 @@ In some situations a selector may target an element that is not 'visible' to the
</style>
```

### 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 `<p>` inside another `<p>`:

```html
<!-- this HTML... -->
<p><p>hello</p>

<!-- results in this DOM structure -->
<p></p>
<p>hello</p>
```

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

```
Expand Down
17 changes: 17 additions & 0 deletions packages/svelte/messages/compile-warnings/template.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 `<p>` inside another `<p>`:

```html
<!-- this HTML... -->
<p><p>hello</p>

<!-- results in this DOM structure -->
<p></p>
<p>hello</p>
```

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% ... />`
Expand Down
13 changes: 12 additions & 1 deletion packages/svelte/src/compiler/phases/1-parse/state/element.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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 = {
Expand Down
11 changes: 11 additions & 0 deletions packages/svelte/src/compiler/warnings.js
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
<main><div class="hello"></main>

<main>
<div class="hello">
<p>hello</p>
</main>
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
[
{
"code": "element_implicitly_closed",
"message": "This element is implicitly closed by the following `</main>`, which can cause an unexpected DOM structure. Add an explicit `</div>` 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 `</main>`, which can cause an unexpected DOM structure. Add an explicit `</div>` to avoid surprises.",
"start": {
"line": 4,
"column": 1
},
"end": {
"line": 4,
"column": 20
}
}
]
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
<div>
<p class="hello">
<span></span>
<p></p>
</div>

<div>
<p class="hello"><p></p>
</div>
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
[
{
"code": "element_implicitly_closed",
"message": "This element is implicitly closed by the following `<p>`, which can cause an unexpected DOM structure. Add an explicit `</p>` 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 `<p>`, which can cause an unexpected DOM structure. Add an explicit `</p>` to avoid surprises.",
"start": {
"line": 8,
"column": 1
},
"end": {
"line": 8,
"column": 18
}
}
]