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 <kifiranet@gmail.com>
Co-authored-by: Rich Harris <rich.harris@vercel.com>
pull/15959/head
7nik 4 months ago committed by GitHub
parent 7183886a73
commit c66a43940a
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -0,0 +1,5 @@
---
'svelte': minor
---
feat: warn on implicitly closed tags

@ -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
```

@ -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% ... />`

@ -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 = {

@ -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

@ -0,0 +1,6 @@
<main><div class="hello"></main>
<main>
<div class="hello">
<p>hello</p>
</main>

@ -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
}
}
]

@ -0,0 +1,9 @@
<div>
<p class="hello">
<span></span>
<p></p>
</div>
<div>
<p class="hello"><p></p>
</div>

@ -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
}
}
]
Loading…
Cancel
Save