fix: allow ts casts in bindings (#10181)

fixes #10179

---------

Co-authored-by: Simon Holthausen <simon.holthausen@vercel.com>
pull/10233/head
gtmnayan 2 years ago committed by GitHub
parent 2861ad66e0
commit 0071e0252a
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -0,0 +1,5 @@
---
"svelte": patch
---
fix: allow ts casts in bindings

@ -338,9 +338,11 @@ export const validation = {
BindDirective(node, context) { BindDirective(node, context) {
validate_no_const_assignment(node, node.expression, context.state.scope, true); validate_no_const_assignment(node, node.expression, context.state.scope, true);
let left = node.expression; const assignee = unwrap_ts_expression(node.expression);
let left = assignee;
while (left.type === 'MemberExpression') { while (left.type === 'MemberExpression') {
left = /** @type {import('estree').MemberExpression} */ (left.object); left = unwrap_ts_expression(/** @type {import('estree').MemberExpression} */ (left.object));
} }
if (left.type !== 'Identifier') { if (left.type !== 'Identifier') {
@ -348,7 +350,7 @@ export const validation = {
} }
if ( if (
node.expression.type === 'Identifier' && assignee.type === 'Identifier' &&
node.name !== 'this' // bind:this also works for regular variables node.name !== 'this' // bind:this also works for regular variables
) { ) {
const binding = context.state.scope.get(left.name); const binding = context.state.scope.get(left.name);

@ -1,5 +1,5 @@
import * as b from '../../../utils/builders.js'; import * as b from '../../../utils/builders.js';
import { extract_paths, is_simple_expression } from '../../../utils/ast.js'; import { extract_paths, is_simple_expression, unwrap_ts_expression } from '../../../utils/ast.js';
import { error } from '../../../errors.js'; import { error } from '../../../errors.js';
import { import {
PROPS_IS_LAZY_INITIAL, PROPS_IS_LAZY_INITIAL,
@ -223,10 +223,11 @@ function is_expression_async(expression) {
export function serialize_set_binding(node, context, fallback, options) { export function serialize_set_binding(node, context, fallback, options) {
const { state, visit } = context; const { state, visit } = context;
const assignee = unwrap_ts_expression(node.left);
if ( if (
node.left.type === 'ArrayPattern' || assignee.type === 'ArrayPattern' ||
node.left.type === 'ObjectPattern' || assignee.type === 'ObjectPattern' ||
node.left.type === 'RestElement' assignee.type === 'RestElement'
) { ) {
// Turn assignment into an IIFE, so that `$.set` calls etc don't produce invalid code // Turn assignment into an IIFE, so that `$.set` calls etc don't produce invalid code
const tmp_id = context.state.scope.generate('tmp'); const tmp_id = context.state.scope.generate('tmp');
@ -237,7 +238,7 @@ export function serialize_set_binding(node, context, fallback, options) {
/** @type {import('estree').Expression[]} */ /** @type {import('estree').Expression[]} */
const assignments = []; const assignments = [];
const paths = extract_paths(node.left); const paths = extract_paths(assignee);
for (const path of paths) { for (const path of paths) {
const value = path.expression?.(b.id(tmp_id)); const value = path.expression?.(b.id(tmp_id));
@ -275,11 +276,11 @@ export function serialize_set_binding(node, context, fallback, options) {
} }
} }
if (node.left.type !== 'Identifier' && node.left.type !== 'MemberExpression') { if (assignee.type !== 'Identifier' && assignee.type !== 'MemberExpression') {
error(node, 'INTERNAL', `Unexpected assignment type ${node.left.type}`); error(node, 'INTERNAL', `Unexpected assignment type ${assignee.type}`);
} }
let left = node.left; let left = assignee;
// Handle class private/public state assignment cases // Handle class private/public state assignment cases
while (left.type === 'MemberExpression') { while (left.type === 'MemberExpression') {
@ -342,7 +343,7 @@ export function serialize_set_binding(node, context, fallback, options) {
} }
} }
// @ts-expect-error // @ts-expect-error
left = left.object; left = unwrap_ts_expression(left.object);
} }
if (left.type !== 'Identifier') { if (left.type !== 'Identifier') {

@ -3,7 +3,8 @@ import {
extract_paths, extract_paths,
is_event_attribute, is_event_attribute,
is_text_attribute, is_text_attribute,
object object,
unwrap_ts_expression
} from '../../../../utils/ast.js'; } from '../../../../utils/ast.js';
import { binding_properties } from '../../../bindings.js'; import { binding_properties } from '../../../bindings.js';
import { import {
@ -2579,24 +2580,9 @@ export const template_visitors = {
}, },
BindDirective(node, context) { BindDirective(node, context) {
const { state, path, visit } = context; const { state, path, visit } = context;
const expression = unwrap_ts_expression(node.expression);
/** @type {import('estree').Expression[]} */ const getter = b.thunk(/** @type {import('estree').Expression} */ (visit(expression)));
const properties = []; const assignment = b.assignment('=', expression, b.id('$$value'));
let expression = node.expression;
while (expression.type === 'MemberExpression') {
properties.unshift(
expression.computed
? /** @type {import('estree').Expression} */ (expression.property)
: b.literal(/** @type {import('estree').Identifier} */ (expression.property).name)
);
expression = /** @type {import('estree').Identifier | import('estree').MemberExpression} */ (
expression.object
);
}
const getter = b.thunk(/** @type {import('estree').Expression} */ (visit(node.expression)));
const assignment = b.assignment('=', node.expression, b.id('$$value'));
const setter = b.arrow( const setter = b.arrow(
[b.id('$$value')], [b.id('$$value')],
serialize_set_binding( serialize_set_binding(
@ -2716,7 +2702,7 @@ export const template_visitors = {
setter, setter,
/** @type {import('estree').Expression} */ ( /** @type {import('estree').Expression} */ (
// if expression is not an identifier, we know it can't be a signal // if expression is not an identifier, we know it can't be a signal
node.expression.type === 'Identifier' ? node.expression : undefined expression.type === 'Identifier' ? expression : undefined
) )
); );
break; break;
@ -2765,7 +2751,7 @@ export const template_visitors = {
group_getter = b.thunk( group_getter = b.thunk(
b.block([ b.block([
b.stmt(serialize_attribute_value(value, context)[1]), b.stmt(serialize_attribute_value(value, context)[1]),
b.return(/** @type {import('estree').Expression} */ (visit(node.expression))) b.return(/** @type {import('estree').Expression} */ (visit(expression)))
]) ])
); );
} }

@ -1,4 +1,3 @@
import { error } from '../errors.js';
import * as b from '../utils/builders.js'; import * as b from '../utils/builders.js';
/** /**
@ -7,10 +6,13 @@ import * as b from '../utils/builders.js';
* @returns {import('estree').Identifier | null} * @returns {import('estree').Identifier | null}
*/ */
export function object(expression) { export function object(expression) {
expression = unwrap_ts_expression(expression);
while (expression.type === 'MemberExpression') { while (expression.type === 'MemberExpression') {
expression = /** @type {import('estree').MemberExpression | import('estree').Identifier} */ ( expression = /** @type {import('estree').MemberExpression | import('estree').Identifier} */ (
expression.object expression.object
); );
expression = unwrap_ts_expression(expression);
} }
if (expression.type !== 'Identifier') { if (expression.type !== 'Identifier') {
@ -270,6 +272,9 @@ function _extract_paths(assignments = [], param, expression, update_expression)
* The Acorn TS plugin defines `foo!` as a `TSNonNullExpression` node, and * The Acorn TS plugin defines `foo!` as a `TSNonNullExpression` node, and
* `foo as Bar` as a `TSAsExpression` node. This function unwraps those. * `foo as Bar` as a `TSAsExpression` node. This function unwraps those.
* *
* We can't just remove the typescript AST nodes in the parser stage because subsequent
* parsing would fail, since AST start/end nodes would point at the wrong positions.
*
* @template {import('#compiler').SvelteNode | undefined | null} T * @template {import('#compiler').SvelteNode | undefined | null} T
* @param {T} node * @param {T} node
* @returns {T} * @returns {T}
@ -279,8 +284,14 @@ export function unwrap_ts_expression(node) {
return node; return node;
} }
// @ts-expect-error these types don't exist on the base estree types if (
if (node.type === 'TSNonNullExpression' || node.type === 'TSAsExpression') { // @ts-expect-error these types don't exist on the base estree types
node.type === 'TSNonNullExpression' ||
// @ts-expect-error these types don't exist on the base estree types
node.type === 'TSAsExpression' ||
// @ts-expect-error these types don't exist on the base estree types
node.type === 'TSSatisfiesExpression'
) {
// @ts-expect-error // @ts-expect-error
return node.expression; return node.expression;
} }

@ -1,5 +1,6 @@
import { test } from '../../test'; import { test } from '../../test';
export default test({ export default test({
html: '1 2' html: '1 2 <div></div> <input type="number"> <input type="number">',
ssrHtml: '1 2 <div></div> <input type="number" value="1"> <input type="number" value="2">'
}); });

@ -1,6 +1,14 @@
<script lang="ts"> <script lang="ts">
let count = $state(1) as number; let count = $state(1) as number;
let double = $derived(count as number * 2) as number; let double = $derived(count as number * 2) as number;
let element = null;
let with_state = $state({ foo: 1 });
let without_state = { foo: 2 };
</script> </script>
{count as number} {double as number} {count as number} {double as number}
<div bind:this={element as HTMLElement}></div>
<input type="number" bind:value={(with_state as { foo: number }).foo} />
<input type="number" bind:value={(without_state as { foo: number }).foo as number} />

@ -1,5 +1,5 @@
import { test } from '../../test'; import { test } from '../../test';
export default test({ export default test({
html: '1 2' html: '1 2 <input type="number">'
}); });

@ -1,6 +1,9 @@
<script lang="ts"> <script lang="ts">
let count = $state(1)!; let count = $state(1)!;
let double = $derived(count! * 2)!; let double = $derived(count! * 2)!;
let binding = $state(null);
</script> </script>
{count!} {double!} {count!} {double!}
<input type="number" bind:value={binding!} />

Loading…
Cancel
Save