Convert src/compiler/compile/nodes/Binding.ts to JavaScript

pull/8569/head
Simon Holthausen 3 years ago
parent d8f01f4c1a
commit 0e6a27e895

@ -19,10 +19,10 @@ export default class AwaitBlock extends Node {
/** @type {import('./shared/Context.js').Context[]} */ /** @type {import('./shared/Context.js').Context[]} */
catch_contexts; catch_contexts;
/** @type {ESTreeNode | null} */ /** @type {import('estree').Node | null} */
then_node; then_node;
/** @type {ESTreeNode | null} */ /** @type {import('estree').Node | null} */
catch_node; catch_node;
/** @type {import('./PendingBlock.js').default} */ /** @type {import('./PendingBlock.js').default} */
@ -34,14 +34,14 @@ export default class AwaitBlock extends Node {
/** @type {import('./CatchBlock.js').default} */ /** @type {import('./CatchBlock.js').default} */
catch; catch;
/** @type {Map<string, ESTreeNode>} */ /** @type {Map<string, import('estree').Node>} */
context_rest_properties = new Map(); context_rest_properties = new Map();
/** /**
* @param {import('../Component.js').default} component * * @param {import('../Component.js').default} component
* @param {import('./shared/Node.js').default} parent * * @param {import('./shared/Node.js').default} parent
* @param {import('./shared/TemplateScope.js').default} scope * * @param {import('./shared/TemplateScope.js').default} scope
* @param {import('../../interfaces.js').TemplateNode} info undefined * @param {import('../../interfaces.js').TemplateNode} info
*/ */
constructor(component, parent, scope, info) { constructor(component, parent, scope, info) {
super(component, parent, scope, info); super(component, parent, scope, info);

@ -1,131 +1,133 @@
import Node from './shared/Node'; import Node from './shared/Node.js';
import get_object from '../utils/get_object'; import get_object from '../utils/get_object.js';
import Expression from './shared/Expression'; import Expression from './shared/Expression.js';
import Component from '../Component'; import { regex_dimensions, regex_box_size } from '../../utils/patterns.js';
import TemplateScope from './shared/TemplateScope'; import { clone } from '../../utils/clone.js';
import { regex_dimensions, regex_box_size } from '../../utils/patterns'; import compiler_errors from '../compiler_errors.js';
import { Node as ESTreeNode } from 'estree'; import compiler_warnings from '../compiler_warnings.js';
import { TemplateNode } from '../../interfaces';
import Element from './Element';
import InlineComponent from './InlineComponent';
import Window from './Window';
import Document from './Document';
import { clone } from '../../utils/clone';
import compiler_errors from '../compiler_errors';
import compiler_warnings from '../compiler_warnings';
// TODO this should live in a specific binding // TODO this should live in a specific binding
const read_only_media_attributes = new Set([ const read_only_media_attributes = new Set([
'duration', 'duration',
'buffered', 'buffered',
'seekable', 'seekable',
'played', 'played',
'seeking', 'seeking',
'ended', 'ended',
'videoHeight', 'videoHeight',
'videoWidth', 'videoWidth',
'naturalWidth', 'naturalWidth',
'naturalHeight', 'naturalHeight',
'readyState' 'readyState'
]); ]);
/** @extends Node */
export default class Binding extends Node { export default class Binding extends Node {
type: 'Binding';
name: string;
expression: Expression;
raw_expression: ESTreeNode; // TODO exists only for bind:this — is there a more elegant solution?
is_contextual: boolean;
is_readonly: boolean;
constructor(
component: Component,
parent: Element | InlineComponent | Window | Document,
scope: TemplateScope,
info: TemplateNode
) {
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((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((name) => { /** @type {'Binding'} */
const variable = component.var_lookup.get(name); type;
if (variable) {
variable.mutated = true; /** @type {string} */
} name;
});
} else { /** @type {import('./shared/Expression.js').default} */
const variable = component.var_lookup.get(name); expression;
if (!variable || variable.global) { /** @type {ESTreeNode} */
component.error(this.expression.node as any, compiler_errors.binding_undeclared(name)); raw_expression; // TODO exists only for bind:this — is there a more elegant solution?
return;
} /** @type {boolean} */
is_contextual;
variable[this.expression.node.type === 'MemberExpression' ? 'mutated' : 'reassigned'] = true;
/** @type {boolean} */
if (info.expression.type === 'Identifier' && !variable.writable) { is_readonly;
component.error(this.expression.node as any, compiler_errors.invalid_binding_writable);
return; /**
} * @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));
}
}
});
}
}
const type = parent.get_static_attribute_value('type'); /**
* @param {import('./shared/Node.js').default} node
* @returns {boolean}
*/
function isElement(node) {
return !!( /** @type {any} */(node)).is_media_node;
}
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);
}
validate_binding_rest_properties(scope: TemplateScope) {
this.expression.references.forEach((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(
rest_node as any,
compiler_warnings.invalid_rest_eachblock_binding(name)
);
}
}
});
}
}
function isElement(node: Node): node is Element {
return !!(node as any).is_media_node;
}

Loading…
Cancel
Save