diff --git a/src/compiler/compile/nodes/Binding.js b/src/compiler/compile/nodes/Binding.js index 79c6c9e7b4..7c2bc26b82 100644 --- a/src/compiler/compile/nodes/Binding.js +++ b/src/compiler/compile/nodes/Binding.js @@ -7,117 +7,126 @@ import compiler_errors from '../compiler_errors.js'; import compiler_warnings from '../compiler_warnings.js'; // TODO this should live in a specific binding const read_only_media_attributes = new Set([ - 'duration', - 'buffered', - 'seekable', - 'played', - 'seeking', - 'ended', - 'videoHeight', - 'videoWidth', - 'naturalWidth', - 'naturalHeight', - 'readyState' + 'duration', + 'buffered', + 'seekable', + 'played', + 'seeking', + 'ended', + 'videoHeight', + 'videoWidth', + 'naturalWidth', + 'naturalHeight', + 'readyState' ]); /** @extends Node */ export default class Binding extends Node { + /** @type {'Binding'} */ + type; - /** @type {'Binding'} */ - type; + /** @type {string} */ + name; - /** @type {string} */ - name; + /** @type {import('./shared/Expression.js').default} */ + expression; - /** @type {import('./shared/Expression.js').default} */ - expression; + /** @type {ESTreeNode} */ + raw_expression; // TODO exists only for bind:this — is there a more elegant solution? - /** @type {ESTreeNode} */ - raw_expression; // TODO exists only for bind:this — is there a more elegant solution? + /** @type {boolean} */ + is_contextual; - /** @type {boolean} */ - is_contextual; + /** @type {boolean} */ + is_readonly; - /** @type {boolean} */ - is_readonly; + /** + * @param {import('../Component.js').default} component * + * @param {import('./Element.js').default | import('./InlineComponent.js').default | import('./Window.js').default | import('./Document.js').default} parent * + * @param {import('./shared/TemplateScope.js').default} scope * + * @param {import('../../interfaces.js').TemplateNode} info undefined + */ + constructor(component, parent, scope, info) { + super(component, parent, scope, info); + if (info.expression.type !== 'Identifier' && info.expression.type !== 'MemberExpression') { + component.error(info, compiler_errors.invalid_directive_value); + return; + } + this.name = info.name; + this.expression = new Expression(component, this, scope, info.expression); + this.raw_expression = clone(info.expression); + const { name } = get_object(this.expression.node); + this.is_contextual = Array.from(this.expression.references).some( + /** @param {any} name */ (name) => scope.names.has(name) + ); + if (this.is_contextual) this.validate_binding_rest_properties(scope); + // make sure we track this as a mutable ref + if (scope.is_let(name)) { + component.error(this, compiler_errors.invalid_binding_let); + return; + } else if (scope.names.has(name)) { + if (scope.is_await(name)) { + component.error(this, compiler_errors.invalid_binding_await); + return; + } + if (scope.is_const(name)) { + component.error(this, compiler_errors.invalid_binding_const); + } + scope.dependencies_for_name.get(name).forEach( + /** @param {any} name */ (name) => { + const variable = component.var_lookup.get(name); + if (variable) { + variable.mutated = true; + } + } + ); + } else { + const variable = component.var_lookup.get(name); + if (!variable || variable.global) { + component.error( + /** @type {any} */ (this.expression.node), + compiler_errors.binding_undeclared(name) + ); + return; + } + variable[this.expression.node.type === 'MemberExpression' ? 'mutated' : 'reassigned'] = true; + if (info.expression.type === 'Identifier' && !variable.writable) { + component.error( + /** @type {any} */ (this.expression.node), + compiler_errors.invalid_binding_writable + ); + return; + } + } + const type = parent.get_static_attribute_value('type'); + this.is_readonly = + regex_dimensions.test(this.name) || + regex_box_size.test(this.name) || + (isElement(parent) && + ((parent.is_media_node() && read_only_media_attributes.has(this.name)) || + (parent.name === 'input' && type === 'file'))) /* TODO others? */; + } + is_readonly_media_attribute() { + return read_only_media_attributes.has(this.name); + } - /** - * @param {import('../Component.js').default} component * - * @param {import('./Element.js').default | import('./InlineComponent.js').default | import('./Window.js').default | import('./Document.js').default} parent * - * @param {import('./shared/TemplateScope.js').default} scope * - * @param {import('../../interfaces.js').TemplateNode} info undefined - */ - constructor(component, parent, scope, info) { - super(component, parent, scope, info); - if (info.expression.type !== 'Identifier' && info.expression.type !== 'MemberExpression') { - component.error(info, compiler_errors.invalid_directive_value); - return; - } - this.name = info.name; - this.expression = new Expression(component, this, scope, info.expression); - this.raw_expression = clone(info.expression); - const { name } = get_object(this.expression.node); - this.is_contextual = Array.from(this.expression.references).some(/** @param {any} name */ (name) => scope.names.has(name)); - if (this.is_contextual) - this.validate_binding_rest_properties(scope); - // make sure we track this as a mutable ref - if (scope.is_let(name)) { - component.error(this, compiler_errors.invalid_binding_let); - return; - } - else if (scope.names.has(name)) { - if (scope.is_await(name)) { - component.error(this, compiler_errors.invalid_binding_await); - return; - } - if (scope.is_const(name)) { - component.error(this, compiler_errors.invalid_binding_const); - } - scope.dependencies_for_name.get(name).forEach(/** @param {any} name */ (name) => { - const variable = component.var_lookup.get(name); - if (variable) { - variable.mutated = true; - } - }); - } - else { - const variable = component.var_lookup.get(name); - if (!variable || variable.global) { - component.error(/** @type {any} */ (this.expression.node), compiler_errors.binding_undeclared(name)); - return; - } - variable[this.expression.node.type === 'MemberExpression' ? 'mutated' : 'reassigned'] = true; - if (info.expression.type === 'Identifier' && !variable.writable) { - component.error(/** @type {any} */ (this.expression.node), compiler_errors.invalid_binding_writable); - return; - } - } - const type = parent.get_static_attribute_value('type'); - this.is_readonly = - regex_dimensions.test(this.name) || - regex_box_size.test(this.name) || - (isElement(parent) && - ((parent.is_media_node() && read_only_media_attributes.has(this.name)) || - (parent.name === 'input' && type === 'file'))) /* TODO others? */; - } - is_readonly_media_attribute() { - return read_only_media_attributes.has(this.name); - } - - /** @param {import('./shared/TemplateScope.js').default} scope */ - validate_binding_rest_properties(scope) { - this.expression.references.forEach(/** @param {any} name */ (name) => { - const each_block = scope.get_owner(name); - if (each_block && each_block.type === 'EachBlock') { - const rest_node = each_block.context_rest_properties.get(name); - if (rest_node) { - this.component.warn( - - /** @type {any} */ (rest_node), compiler_warnings.invalid_rest_eachblock_binding(name)); - } - } - }); - } + /** @param {import('./shared/TemplateScope.js').default} scope */ + validate_binding_rest_properties(scope) { + this.expression.references.forEach( + /** @param {any} name */ (name) => { + const each_block = scope.get_owner(name); + if (each_block && each_block.type === 'EachBlock') { + const rest_node = each_block.context_rest_properties.get(name); + if (rest_node) { + this.component.warn( + /** @type {any} */ (rest_node), + compiler_warnings.invalid_rest_eachblock_binding(name) + ); + } + } + } + ); + } } /** @@ -125,9 +134,5 @@ export default class Binding extends Node { * @returns {boolean} */ function isElement(node) { - return !!( /** @type {any} */(node)).is_media_node; + return !!/** @type {any} */ (node).is_media_node; } - - - -