use BindDirective metadata to mark a spread element instead of setting the SpreadElement as the expression

pull/16607/head
Jack Goodall 3 weeks ago
parent 92e9e79c6c
commit dfcc5f6829

@ -1,4 +1,4 @@
/** @import { Expression, SpreadElement } from 'estree' */ /** @import { Expression } from 'estree' */
/** @import { AST } from '#compiler' */ /** @import { AST } from '#compiler' */
/** @import { Parser } from '../index.js' */ /** @import { Parser } from '../index.js' */
import { is_void } from '../../../../utils.js'; import { is_void } from '../../../../utils.js';
@ -618,15 +618,6 @@ function read_attribute(parser) {
e.directive_missing_name({ start, end: start + colon_index + 1 }, name); e.directive_missing_name({ start, end: start + colon_index + 1 }, name);
} }
if (
type !== 'BindDirective' &&
value !== true &&
'metadata' in value &&
value.metadata.expression.has_spread
) {
e.directive_invalid_value(value.start);
}
if (type === 'StyleDirective') { if (type === 'StyleDirective') {
return { return {
start, start,
@ -643,7 +634,7 @@ function read_attribute(parser) {
const first_value = value === true ? undefined : Array.isArray(value) ? value[0] : value; const first_value = value === true ? undefined : Array.isArray(value) ? value[0] : value;
/** @type {Expression | SpreadElement | null} */ /** @type {Expression | null} */
let expression = null; let expression = null;
if (first_value) { if (first_value) {
@ -655,29 +646,26 @@ function read_attribute(parser) {
// TODO throw a parser error in a future version here if this `[ExpressionTag]` instead of `ExpressionTag`, // TODO throw a parser error in a future version here if this `[ExpressionTag]` instead of `ExpressionTag`,
// which means stringified value, which isn't allowed for some directives? // which means stringified value, which isn't allowed for some directives?
expression = first_value.expression; expression = first_value.expression;
if (type === 'BindDirective' && first_value.metadata.expression.has_spread) {
expression = {
type: 'SpreadElement',
start: first_value.start,
end: first_value.end,
argument: expression
};
}
} }
} }
/** @type {AST.Directive} */ const directive = /** @type {AST.Directive} */ ({
const directive = {
start, start,
end, end,
type, type,
name: directive_name, name: directive_name,
modifiers: [],
expression, expression,
metadata: { metadata: {
expression: create_expression_metadata() expression: create_expression_metadata()
} }
}; });
if (first_value?.metadata.expression.has_spread) {
if (directive.type !== 'BindDirective') {
e.directive_invalid_value(first_value.start);
}
directive.metadata.spread_binding = true;
}
// @ts-expect-error we do this separately from the declaration to avoid upsetting typescript // @ts-expect-error we do this separately from the declaration to avoid upsetting typescript
directive.modifiers = modifiers; directive.modifiers = modifiers;

@ -158,20 +158,11 @@ export function BindDirective(node, context) {
return; return;
} }
if (node.expression.type === 'SpreadElement') { if (node.metadata.spread_binding) {
if (node.name === 'group') { if (node.name === 'group') {
e.bind_group_invalid_expression(node); e.bind_group_invalid_expression(node);
} }
const argument = node.expression.argument;
if (
argument.type !== 'Identifier' &&
argument.type !== 'MemberExpression' &&
argument.type !== 'CallExpression'
) {
e.bind_invalid_expression(node);
}
mark_subtree_dynamic(context.path); mark_subtree_dynamic(context.path);
return; return;
@ -261,7 +252,8 @@ export function BindDirective(node, context) {
node.metadata = { node.metadata = {
binding_group_name: group_name, binding_group_name: group_name,
parent_each_blocks: each_blocks parent_each_blocks: each_blocks,
spread_binding: false
}; };
} }

@ -21,8 +21,7 @@ export function BindDirective(node, context) {
let get, set; let get, set;
// Handle SpreadElement by creating a variable declaration before visiting if (node.metadata.spread_binding) {
if (node.expression.type === 'SpreadElement') {
const { get: getter, set: setter } = init_spread_bindings(node.expression, context); const { get: getter, set: setter } = init_spread_bindings(node.expression, context);
get = getter; get = getter;
set = setter; set = setter;

@ -414,7 +414,7 @@ function setup_select_synchronization(value_binding, context) {
let bound = value_binding.expression; let bound = value_binding.expression;
if (bound.type === 'SequenceExpression') { if (bound.type === 'SequenceExpression' || value_binding.metadata.spread_binding) {
return; return;
} }

@ -197,11 +197,14 @@ export function build_component(node, component_name, context) {
push_prop(b.init(attribute.name, value)); push_prop(b.init(attribute.name, value));
} }
} else if (attribute.type === 'BindDirective') { } else if (attribute.type === 'BindDirective') {
if (attribute.expression.type === 'SpreadElement') { if (attribute.metadata.spread_binding) {
const { get, set } = init_spread_bindings(attribute.expression, context); const { get, set } = init_spread_bindings(attribute.expression, context);
if (attribute.name === 'this') { if (attribute.name === 'this') {
bind_this = attribute.expression; bind_this = {
type: 'SpreadElement',
argument: attribute.expression
};
} else { } else {
push_prop(b.get(attribute.name, [b.return(b.call(get))]), true); push_prop(b.get(attribute.name, [b.return(b.call(get))]), true);
push_prop(b.set(attribute.name, [b.stmt(b.call(set, b.id('$$value')))]), true); push_prop(b.set(attribute.name, [b.stmt(b.call(set, b.id('$$value')))]), true);

@ -212,7 +212,7 @@ export function parse_directive_name(name) {
export function build_bind_this(expression, value, context) { export function build_bind_this(expression, value, context) {
const { state, visit } = context; const { state, visit } = context;
if (expression.type === 'SpreadElement') { if (expression.type === 'SpreadElement') {
const { get, set } = init_spread_bindings(expression, context); const { get, set } = init_spread_bindings(expression.argument, context);
return b.call('$.bind_this', value, set, get); return b.call('$.bind_this', value, set, get);
} }
@ -297,10 +297,7 @@ export function build_bind_this(expression, value, context) {
* @param {MemberExpression} expression * @param {MemberExpression} expression
*/ */
export function validate_binding(state, binding, expression) { export function validate_binding(state, binding, expression) {
if ( if (binding.expression.type === 'SequenceExpression' || binding.metadata.spread_binding) {
binding.expression.type === 'SequenceExpression' ||
binding.expression.type === 'SpreadElement'
) {
return; return;
} }
// If we are referencing a $store.foo then we don't need to add validation // If we are referencing a $store.foo then we don't need to add validation

@ -94,7 +94,7 @@ export function build_inline_component(node, expression, context) {
const value = build_attribute_value(attribute.value, context, false, true); const value = build_attribute_value(attribute.value, context, false, true);
push_prop(b.prop('init', b.key(attribute.name), value)); push_prop(b.prop('init', b.key(attribute.name), value));
} else if (attribute.type === 'BindDirective' && attribute.name !== 'this') { } else if (attribute.type === 'BindDirective' && attribute.name !== 'this') {
if (attribute.expression.type === 'SpreadElement') { if (attribute.metadata.spread_binding) {
const { get, set } = init_spread_bindings(attribute.expression, context); const { get, set } = init_spread_bindings(attribute.expression, context);
push_prop(b.get(attribute.name, [b.return(b.call(get))])); push_prop(b.get(attribute.name, [b.return(b.call(get))]));

@ -119,8 +119,7 @@ export function build_element_attributes(node, context) {
let expression = /** @type {Expression} */ (context.visit(attribute.expression)); let expression = /** @type {Expression} */ (context.visit(attribute.expression));
// Handle SpreadElement for bind directives if (attribute.metadata.spread_binding) {
if (attribute.expression.type === 'SpreadElement') {
const { get } = init_spread_bindings(attribute.expression, context); const { get } = init_spread_bindings(attribute.expression, context);
expression = b.call(get); expression = b.call(get);
} else if (expression.type === 'SequenceExpression') { } else if (expression.type === 'SequenceExpression') {
@ -134,7 +133,7 @@ export function build_element_attributes(node, context) {
} else if ( } else if (
attribute.name === 'group' && attribute.name === 'group' &&
attribute.expression.type !== 'SequenceExpression' && attribute.expression.type !== 'SequenceExpression' &&
attribute.expression.type !== 'SpreadElement' !attribute.metadata.spread_binding
) { ) {
const value_attribute = /** @type {AST.Attribute | undefined} */ ( const value_attribute = /** @type {AST.Attribute | undefined} */ (
node.attributes.find((attr) => attr.type === 'Attribute' && attr.name === 'value') node.attributes.find((attr) => attr.type === 'Attribute' && attr.name === 'value')

@ -1,4 +1,4 @@
/** @import { Expression, SpreadElement } from 'estree' */ /** @import { Expression } from 'estree' */
/** @import { ComponentContext as ClientContext } from '../client/types.js' */ /** @import { ComponentContext as ClientContext } from '../client/types.js' */
/** @import { ComponentContext as ServerContext } from '../server/types.js' */ /** @import { ComponentContext as ServerContext } from '../server/types.js' */
import * as b from '#compiler/builders'; import * as b from '#compiler/builders';
@ -6,14 +6,14 @@ import { dev, source } from '../../../state.js';
/** /**
* Initializes spread bindings for a SpreadElement in a bind directive. * Initializes spread bindings for a SpreadElement in a bind directive.
* @param {SpreadElement} spread_expression * @param {Expression} spread_expression
* @param {ClientContext | ServerContext} context * @param {ClientContext | ServerContext} context
* @returns {{ get: Expression, set: Expression }} * @returns {{ get: Expression, set: Expression }}
*/ */
export function init_spread_bindings(spread_expression, { state, visit }) { export function init_spread_bindings(spread_expression, { state, visit }) {
const visited_expression = /** @type {Expression} */ (visit(spread_expression.argument)); const expression = /** @type {Expression} */ (visit(spread_expression));
const expression_text = dev const expression_text = dev
? b.literal(source.slice(spread_expression.argument.start, spread_expression.argument.end)) ? b.literal(source.slice(spread_expression.start, spread_expression.end))
: undefined; : undefined;
const id = state.scope.generate('$$spread_binding'); const id = state.scope.generate('$$spread_binding');
@ -22,7 +22,7 @@ export function init_spread_bindings(spread_expression, { state, visit }) {
state.init.push( state.init.push(
b.const( b.const(
b.array_pattern([get, set]), b.array_pattern([get, set]),
b.call('$.validate_spread_bindings', visited_expression, expression_text) b.call('$.validate_spread_bindings', expression, expression_text)
) )
); );

@ -7,7 +7,8 @@ import type {
MemberExpression, MemberExpression,
ObjectExpression, ObjectExpression,
Pattern, Pattern,
SequenceExpression SequenceExpression,
SpreadElement
} from 'estree'; } from 'estree';
interface BaseNode { interface BaseNode {
@ -50,7 +51,7 @@ export interface LegacyBinding extends BaseNode {
/** The 'x' in `bind:x` */ /** The 'x' in `bind:x` */
name: string; name: string;
/** The y in `bind:x={y}` */ /** The y in `bind:x={y}` */
expression: Identifier | MemberExpression | SequenceExpression; expression: Identifier | MemberExpression | SequenceExpression | SpreadElement;
} }
export interface LegacyBody extends BaseElement { export interface LegacyBody extends BaseElement {

@ -212,11 +212,12 @@ export namespace AST {
/** The 'x' in `bind:x` */ /** The 'x' in `bind:x` */
name: string; name: string;
/** The y in `bind:x={y}` */ /** The y in `bind:x={y}` */
expression: Identifier | MemberExpression | SequenceExpression | SpreadElement; expression: Identifier | MemberExpression | SequenceExpression;
/** @internal */ /** @internal */
metadata: { metadata: {
binding_group_name: Identifier; binding_group_name: Identifier;
parent_each_blocks: EachBlock[]; parent_each_blocks: EachBlock[];
spread_binding: boolean;
}; };
} }

@ -50,6 +50,23 @@ export function invalid_snippet_arguments() {
} }
} }
/**
* `%name%` must be a function or `undefined`
* @param {string} name
* @returns {never}
*/
export function invalid_spread_bindings(name) {
if (DEV) {
const error = new Error(`invalid_spread_bindings\n\`${name}\` must be a function or \`undefined\`\nhttps://svelte.dev/e/invalid_spread_bindings`);
error.name = 'Svelte error';
throw error;
} else {
throw new Error(`https://svelte.dev/e/invalid_spread_bindings`);
}
}
/** /**
* `%name%(...)` can only be used during component initialisation * `%name%(...)` can only be used during component initialisation
* @param {string} name * @param {string} name
@ -115,20 +132,3 @@ export function svelte_element_invalid_this_value() {
throw new Error(`https://svelte.dev/e/svelte_element_invalid_this_value`); throw new Error(`https://svelte.dev/e/svelte_element_invalid_this_value`);
} }
} }
/**
* `%name%%member%` must be a function or `undefined`
* @param {string} name
* @returns {never}
*/
export function invalid_spread_bindings(name) {
if (DEV) {
const error = new Error(`invalid_spread_bindings\n\`${name}\` must be a function or \`undefined\`\nhttps://svelte.dev/e/invalid_spread_bindings`);
error.name = 'Svelte error';
throw error;
} else {
throw new Error(`https://svelte.dev/e/invalid_spread_bindings`);
}
}

@ -795,7 +795,7 @@ declare module 'svelte/attachments' {
declare module 'svelte/compiler' { declare module 'svelte/compiler' {
import type { SourceMap } from 'magic-string'; import type { SourceMap } from 'magic-string';
import type { ArrayExpression, ArrowFunctionExpression, VariableDeclaration, VariableDeclarator, Expression, Identifier, MemberExpression, Node, ObjectExpression, Pattern, Program, ChainExpression, SimpleCallExpression, SequenceExpression, SpreadElement } from 'estree'; import type { ArrayExpression, ArrowFunctionExpression, VariableDeclaration, VariableDeclarator, Expression, Identifier, MemberExpression, Node, ObjectExpression, Pattern, Program, ChainExpression, SimpleCallExpression, SequenceExpression } from 'estree';
import type { Location } from 'locate-character'; import type { Location } from 'locate-character';
/** /**
* `compile` converts your `.svelte` source code into a JavaScript module that exports a component * `compile` converts your `.svelte` source code into a JavaScript module that exports a component
@ -1268,7 +1268,7 @@ declare module 'svelte/compiler' {
/** The 'x' in `bind:x` */ /** The 'x' in `bind:x` */
name: string; name: string;
/** The y in `bind:x={y}` */ /** The y in `bind:x={y}` */
expression: Identifier | MemberExpression | SequenceExpression | SpreadElement; expression: Identifier | MemberExpression | SequenceExpression;
} }
/** A `class:` directive */ /** A `class:` directive */

Loading…
Cancel
Save