fix: don't mistake `type` identifier expressions for TS `type` declarations in tags

Detect TypeScript `type` declarations by actually parsing instead of by
character-class blacklists, so that expressions like `{type === 'all' ? a : b}`
or `{type instanceof Foo}` aren't misclassified as malformed declarations.

Closes #18328
pull/18330/head
baseballyama 5 days ago
parent e027d8bf22
commit 4bf2a373e7

@ -0,0 +1,5 @@
---
'svelte': patch
---
fix: don't mistake expressions starting with `type` (e.g. `{type === 'all' ? a : b}`) for TypeScript `type` declarations in tags

@ -12,9 +12,9 @@ import { find_matching_bracket, match_bracket } from '../utils/bracket.js';
const regex_whitespace_with_closing_curly_brace = /\s*}/y;
const regex_supported_declaration = /(?:let|const)\b/y;
// All except `type` are reserved keywords and cannot be used as variable names.
// For type we check if it's not something like `type .x` / `type ()` / `type % 2` / ...
const regex_unsupported_declaration = /(?:(?:var|interface|enum)\b)|(?:type\s+[^?.(`<[&|%^}])/y;
const regex_unsupported_declaration = /(?:var|interface|enum)\b/y;
// `type` is a contextual keyword; this is just a shape hint, confirmed by parsing.
const regex_maybe_type_declaration = /type\s+[\p{ID_Start}_$]/uy;
const pointy_bois = { '<': '>' };
@ -77,6 +77,26 @@ function read_declaration(parser) {
e.declaration_tag_invalid_type({ start, end: start + unsupported.length });
}
const type_alias_match = parser.match_regex(regex_maybe_type_declaration);
if (type_alias_match) {
const initial_comment_count = parser.root.comments.length;
/** @type {{ type: string; start: number; end: number } | undefined} */
let statement;
try {
statement = /** @type {{ type: string; start: number; end: number }} */ (
/** @type {unknown} */ (parse_statement_at(parser, parser.template, start))
);
} catch {
e.declaration_tag_invalid_type({ start, end: start + type_alias_match.length });
}
if (statement.type !== 'ExpressionStatement') {
e.declaration_tag_invalid_type(statement);
}
// discard probe comments so `read_expression` doesn't re-add them
parser.root.comments.length = initial_comment_count;
return null;
}
if (!parser.match_regex(regex_supported_declaration)) {
return null;
}

@ -0,0 +1,92 @@
{
"css": null,
"js": [],
"start": 0,
"end": 36,
"type": "Root",
"fragment": {
"type": "Fragment",
"nodes": [
{
"type": "ExpressionTag",
"start": 0,
"end": 36,
"expression": {
"type": "BinaryExpression",
"start": 1,
"end": 35,
"loc": {
"start": {
"line": 1,
"column": 1
},
"end": {
"line": 1,
"column": 35
}
},
"left": {
"type": "Identifier",
"start": 1,
"end": 5,
"loc": {
"start": {
"line": 1,
"column": 1
},
"end": {
"line": 1,
"column": 5
}
},
"name": "type"
},
"operator": "instanceof",
"right": {
"type": "Identifier",
"start": 29,
"end": 35,
"loc": {
"start": {
"line": 1,
"column": 29
},
"end": {
"line": 1,
"column": 35
}
},
"name": "Object",
"leadingComments": [
{
"type": "Block",
"value": " probe ",
"start": 17,
"end": 28
}
]
}
}
}
]
},
"options": null,
"comments": [
{
"type": "Block",
"value": " probe ",
"start": 17,
"end": 28,
"loc": {
"start": {
"line": 1,
"column": 17
},
"end": {
"line": 1,
"column": 28
}
}
}
]
}

@ -0,0 +1,5 @@
import { test } from '../../test';
export default test({
html: `<ul><li>a</li><li>b</li><li>All types</li></ul><p data-cast="">cast</p><p data-generic-call="">generic-call</p><p data-generic-call-space="">generic-call-space</p>`
});

@ -0,0 +1,16 @@
<script lang="ts">
const assetTypes = ['a', 'b', 'all'] as const;
function type<T>(x: T): T {
return x;
}
</script>
<ul>
{#each assetTypes as type (type)}
<li>{type === 'all' ? 'All types' : type}</li>
{/each}
</ul>
<p data-cast>{'cast' as string}</p>
<p data-generic-call>{type<string>('generic-call')}</p>
<p data-generic-call-space>{type <string>('generic-call-space')}</p>

@ -3,11 +3,11 @@
"code": "declaration_tag_invalid_type",
"message": "Declaration tags must be `let` or `const` declarations",
"start": {
"line": 14,
"line": 25,
"column": 2
},
"end": {
"line": 14,
"line": 25,
"column": 8
}
}

@ -10,6 +10,17 @@
{type ()}
{type [1]}
{type `tag`}
{type === 'all' ? 'All types' : type}
{type !== 'all'}
{type == 'all'}
{type != 'all'}
{type + 1}
{type - 1}
{type * 2}
{type / 2}
{type > 1}
{type instanceof Foo}
{type in foo}
<!-- ... this one should trigger though -->
{type foo = boolean}
{/if}

@ -0,0 +1,14 @@
[
{
"code": "declaration_tag_invalid_type",
"message": "Declaration tags must be `let` or `const` declarations",
"start": {
"line": 2,
"column": 1
},
"end": {
"line": 2,
"column": 33
}
}
]

@ -0,0 +1,2 @@
<script lang="ts"></script>
{type Foo<T extends string> = T[]}

@ -0,0 +1,14 @@
[
{
"code": "declaration_tag_invalid_type",
"message": "Declaration tags must be `let` or `const` declarations",
"start": {
"line": 2,
"column": 1
},
"end": {
"line": 2,
"column": 16
}
}
]
Loading…
Cancel
Save