|
import { walk } from 'estree-walker'; |
|
import is_reference from 'is-reference'; |
|
import flatten_reference from '../../utils/flatten_reference.js'; |
|
import { create_scopes, extract_names } from '../../utils/scope.js'; |
|
import { sanitize } from '../../../utils/names.js'; |
|
import get_object from '../../utils/get_object.js'; |
|
import is_dynamic from '../../render_dom/wrappers/shared/is_dynamic.js'; |
|
import { b } from 'code-red'; |
|
import { invalidate } from '../../render_dom/invalidate.js'; |
|
import { is_reserved_keyword } from '../../utils/reserved_keywords.js'; |
|
import replace_object from '../../utils/replace_object.js'; |
|
import is_contextual from './is_contextual.js'; |
|
import { clone } from '../../../utils/clone.js'; |
|
import compiler_errors from '../../compiler_errors.js'; |
|
|
|
const regex_contains_term_function_expression = /FunctionExpression/; |
|
|
|
export default class Expression { |
|
|
|
type = 'Expression'; |
|
|
|
|
|
component; |
|
|
|
|
|
owner; |
|
|
|
|
|
node; |
|
|
|
|
|
references = new Set(); |
|
|
|
|
|
|
|
|
|
|
|
dependencies = new Set(); |
|
|
|
|
|
|
|
|
|
|
|
contextual_dependencies = new Set(); |
|
|
|
|
|
template_scope; |
|
|
|
|
|
scope; |
|
|
|
|
|
scope_map; |
|
|
|
|
|
declarations = []; |
|
|
|
|
|
uses_context = false; |
|
|
|
|
|
manipulated; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
constructor(component, owner, template_scope, info, lazy) { |
|
|
|
Object.defineProperties(this, { |
|
component: { |
|
value: component |
|
} |
|
}); |
|
this.node = info; |
|
this.template_scope = template_scope; |
|
this.owner = owner; |
|
const { dependencies, contextual_dependencies, references } = this; |
|
let { map, scope } = create_scopes(info); |
|
this.scope = scope; |
|
this.scope_map = map; |
|
const expression = this; |
|
let function_expression; |
|
|
|
|
|
walk(info, { |
|
|
|
|
|
|
|
|
|
|
|
enter(node, parent, key) { |
|
|
|
if (key === 'key' && (parent).shorthand) return; |
|
|
|
if (node.type === 'MetaProperty') return this.skip(); |
|
if (map.has(node)) { |
|
scope = map.get(node); |
|
} |
|
if (!function_expression && regex_contains_term_function_expression.test(node.type)) { |
|
function_expression = node; |
|
} |
|
if (is_reference(node, parent)) { |
|
const { name, nodes } = flatten_reference(node); |
|
references.add(name); |
|
if (scope.has(name)) return; |
|
if (name[0] === '$') { |
|
const store_name = name.slice(1); |
|
if (template_scope.names.has(store_name) || scope.has(store_name)) { |
|
return component.error(node, compiler_errors.contextual_store); |
|
} |
|
} |
|
if (template_scope.is_let(name)) { |
|
if (!lazy) { |
|
contextual_dependencies.add(name); |
|
dependencies.add(name); |
|
} |
|
} else if (template_scope.names.has(name)) { |
|
expression.uses_context = true; |
|
contextual_dependencies.add(name); |
|
const owner = template_scope.get_owner(name); |
|
const is_index = owner.type === 'EachBlock' && owner.key && name === owner.index; |
|
if (!lazy || is_index) { |
|
template_scope.dependencies_for_name |
|
.get(name) |
|
.forEach((name) => dependencies.add(name)); |
|
} |
|
} else { |
|
if (!lazy) { |
|
const variable = component.var_lookup.get(name); |
|
if (!variable || !variable.imported || variable.mutated || variable.reassigned) { |
|
dependencies.add(name); |
|
} |
|
} |
|
component.add_reference(node, name); |
|
component.warn_if_undefined(name, nodes[0], template_scope, owner); |
|
} |
|
this.skip(); |
|
} |
|
|
|
let names; |
|
let deep = false; |
|
if (function_expression) { |
|
if (node.type === 'AssignmentExpression') { |
|
deep = node.left.type === 'MemberExpression'; |
|
names = extract_names(deep ? get_object(node.left) : node.left); |
|
} else if (node.type === 'UpdateExpression') { |
|
deep = node.argument.type === 'MemberExpression'; |
|
names = extract_names(get_object(node.argument)); |
|
} |
|
} |
|
if (names) { |
|
names.forEach((name) => { |
|
if (template_scope.names.has(name)) { |
|
if (template_scope.is_const(name)) { |
|
component.error(node, compiler_errors.invalid_const_update(name)); |
|
} |
|
template_scope.dependencies_for_name.get(name).forEach((name) => { |
|
const variable = component.var_lookup.get(name); |
|
if (variable) variable[deep ? 'mutated' : 'reassigned'] = true; |
|
}); |
|
const each_block = template_scope.get_owner(name); |
|
(each_block).has_binding = true; |
|
} else { |
|
component.add_reference(node, name); |
|
const variable = component.var_lookup.get(name); |
|
if (variable) { |
|
variable[deep ? 'mutated' : 'reassigned'] = true; |
|
} |
|
|
|
const declaration = scope.find_owner(name)?.declarations.get(name); |
|
if (declaration) { |
|
if ( |
|
(declaration).kind === |
|
'const' && |
|
!deep |
|
) { |
|
component.error(node, { |
|
code: 'assignment-to-const', |
|
message: 'You are assigning to a const' |
|
}); |
|
} |
|
} else if (variable && variable.writable === false && !deep) { |
|
component.error(node, { |
|
code: 'assignment-to-const', |
|
message: 'You are assigning to a const' |
|
}); |
|
} |
|
} |
|
}); |
|
} |
|
}, |
|
|
|
|
|
leave(node) { |
|
if (map.has(node)) { |
|
scope = scope.parent; |
|
} |
|
if (node === function_expression) { |
|
function_expression = null; |
|
} |
|
} |
|
}); |
|
} |
|
dynamic_dependencies() { |
|
return Array.from(this.dependencies).filter((name) => { |
|
if (this.template_scope.is_let(name)) return true; |
|
if (is_reserved_keyword(name)) return true; |
|
const variable = this.component.var_lookup.get(name); |
|
return is_dynamic(variable); |
|
}); |
|
} |
|
dynamic_contextual_dependencies() { |
|
return Array.from(this.contextual_dependencies).filter((name) => { |
|
return Array.from(this.template_scope.dependencies_for_name.get(name)).some( |
|
(variable_name) => { |
|
const variable = this.component.var_lookup.get(variable_name); |
|
return is_dynamic(variable); |
|
} |
|
); |
|
}); |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
manipulate(block, ctx) { |
|
|
|
|
|
if (this.manipulated) return this.manipulated; |
|
const { component, declarations, scope_map: map, template_scope, owner } = this; |
|
let scope = this.scope; |
|
|
|
|
|
let function_expression; |
|
|
|
|
|
let dependencies; |
|
|
|
|
|
let contextual_dependencies; |
|
const node = walk(this.node, { |
|
|
|
enter(node, parent) { |
|
if (node.type === 'Property' && node.shorthand) { |
|
node.value = clone(node.value); |
|
node.shorthand = false; |
|
} |
|
if (map.has(node)) { |
|
scope = map.get(node); |
|
} |
|
if (node.type === 'Identifier' && is_reference(node, parent)) { |
|
const { name } = flatten_reference(node); |
|
if (scope.has(name)) return; |
|
if (function_expression) { |
|
if (template_scope.names.has(name)) { |
|
contextual_dependencies.add(name); |
|
template_scope.dependencies_for_name.get(name).forEach((dependency) => { |
|
dependencies.add(dependency); |
|
}); |
|
} else { |
|
dependencies.add(name); |
|
component.add_reference(node, name); |
|
} |
|
} else if (is_contextual(component, template_scope, name)) { |
|
const reference = block.renderer.reference(node, ctx); |
|
this.replace(reference); |
|
} |
|
this.skip(); |
|
} |
|
if (!function_expression) { |
|
if (node.type === 'AssignmentExpression') { |
|
|
|
} |
|
if (node.type === 'FunctionExpression' || node.type === 'ArrowFunctionExpression') { |
|
function_expression = node; |
|
dependencies = new Set(); |
|
contextual_dependencies = new Set(); |
|
} |
|
} |
|
}, |
|
|
|
|
|
leave(node, parent) { |
|
if (map.has(node)) scope = scope.parent; |
|
if (node === function_expression) { |
|
const id = component.get_unique_name(sanitize(get_function_name(node, owner))); |
|
const declaration = b`const ${id} = ${node}`; |
|
const extract_functions = () => { |
|
const deps = Array.from(contextual_dependencies); |
|
const function_expression = (node); |
|
const has_args = function_expression.params.length > 0; |
|
function_expression.params = [ |
|
...deps.map( |
|
(name) => ({ type: 'Identifier', name }) |
|
), |
|
...function_expression.params |
|
]; |
|
const context_args = deps.map((name) => block.renderer.reference(name, ctx)); |
|
component.partly_hoisted.push(declaration); |
|
block.renderer.add_to_context(id.name); |
|
const callee = block.renderer.reference(id); |
|
this.replace(id); |
|
const func_declaration = has_args |
|
? b`function ${id}(...args) { |
|
return ${callee}(${context_args}, ...args); |
|
}` |
|
: b`function ${id}() { |
|
return ${callee}(${context_args}); |
|
}`; |
|
return { deps, func_declaration }; |
|
}; |
|
if (owner.type === 'ConstTag') { |
|
|
|
if (contextual_dependencies.size === 0) { |
|
let child_scope = scope; |
|
walk(node, { |
|
|
|
enter(node, parent) { |
|
if (map.has(node)) child_scope = map.get(node); |
|
if (node.type === 'Identifier' && is_reference(node, parent)) { |
|
if (child_scope.has(node.name)) return; |
|
this.replace(block.renderer.reference(node, ctx)); |
|
} |
|
}, |
|
|
|
|
|
leave(node) { |
|
if (map.has(node)) child_scope = child_scope.parent; |
|
} |
|
}); |
|
} else { |
|
const { func_declaration } = extract_functions(); |
|
this.replace(func_declaration[0]); |
|
} |
|
} else if (dependencies.size === 0 && contextual_dependencies.size === 0) { |
|
|
|
component.fully_hoisted.push(declaration); |
|
this.replace(id); |
|
component.add_var(node, { |
|
name: id.name, |
|
internal: true, |
|
hoistable: true, |
|
referenced: true |
|
}); |
|
} else if (contextual_dependencies.size === 0) { |
|
|
|
component.partly_hoisted.push(declaration); |
|
block.renderer.add_to_context(id.name); |
|
this.replace(block.renderer.reference(id)); |
|
} else { |
|
|
|
const { deps, func_declaration } = extract_functions(); |
|
if (owner.type === 'Attribute' && owner.parent.name === 'slot') { |
|
|
|
const dep_scopes = new Set(deps.map((name) => template_scope.get_owner(name))); |
|
|
|
|
|
|
|
let node = owner.parent; |
|
while (node && !dep_scopes.has(node)) { |
|
node = node.parent; |
|
} |
|
const func_expression = func_declaration[0]; |
|
|
|
if (node.type === 'SlotTemplate') { |
|
|
|
this.replace(func_expression); |
|
} else { |
|
|
|
const func_id = component.get_unique_name(id.name + '_func'); |
|
block.renderer.add_to_context(func_id.name, true); |
|
|
|
walk(func_expression, { |
|
|
|
enter(node) { |
|
if (node.type === 'Identifier' && node.name === '#ctx') { |
|
node.name = 'child_ctx'; |
|
} |
|
} |
|
}); |
|
|
|
|
|
( |
|
template_scope.get_owner(deps[0]) |
|
).contexts.push({ |
|
type: 'DestructuredVariable', |
|
key: func_id, |
|
modifier: () => func_expression, |
|
default_modifier: (node) => node |
|
}); |
|
this.replace(block.renderer.reference(func_id)); |
|
} |
|
} else { |
|
declarations.push(func_declaration); |
|
} |
|
} |
|
function_expression = null; |
|
dependencies = null; |
|
contextual_dependencies = null; |
|
if (parent && parent.type === 'Property') { |
|
parent.method = false; |
|
} |
|
} |
|
if (node.type === 'AssignmentExpression' || node.type === 'UpdateExpression') { |
|
const assignee = node.type === 'AssignmentExpression' ? node.left : node.argument; |
|
const object_name = get_object(assignee).name; |
|
if (scope.has(object_name)) return; |
|
|
|
|
|
|
|
|
|
const names = new Set(extract_names( (assignee))); |
|
|
|
|
|
const traced = new Set(); |
|
names.forEach((name) => { |
|
const dependencies = template_scope.dependencies_for_name.get(name); |
|
if (dependencies) { |
|
dependencies.forEach((name) => traced.add(name)); |
|
} else { |
|
traced.add(name); |
|
} |
|
}); |
|
const context = block.bindings.get(object_name); |
|
if (context) { |
|
|
|
|
|
|
|
const { snippet, object, property } = context; |
|
|
|
|
|
const replaced = replace_object(assignee, snippet); |
|
if (node.type === 'AssignmentExpression') { |
|
node.left = replaced; |
|
} else { |
|
node.argument = replaced; |
|
} |
|
contextual_dependencies.add(object.name); |
|
contextual_dependencies.add(property.name); |
|
} |
|
this.replace(invalidate(block.renderer, scope, node, traced)); |
|
} |
|
} |
|
}); |
|
|
|
if (declarations.length > 0) { |
|
block.maintain_context = true; |
|
declarations.forEach((declaration) => { |
|
block.chunks.init.push(declaration); |
|
}); |
|
} |
|
return (this.manipulated = (node)); |
|
} |
|
} |
|
|
|
|
|
|
|
|
|
|
|
function get_function_name(_node, parent) { |
|
if (parent.type === 'EventHandler') { |
|
return `${parent.name}_handler`; |
|
} |
|
if (parent.type === 'Action') { |
|
return `${parent.name}_function`; |
|
} |
|
return 'func'; |
|
} |
|
|