Convert src/compiler/compile/nodes/shared/Expression.ts to JavaScript

pull/8569/head
Simon Holthausen 3 years ago
parent f78ca010f6
commit 86884827ea

@ -1,474 +1,496 @@
import Component from '../../Component';
import { walk } from 'estree-walker'; import { walk } from 'estree-walker';
import is_reference from 'is-reference'; import is_reference from 'is-reference';
import flatten_reference from '../../utils/flatten_reference'; import flatten_reference from '../../utils/flatten_reference.js';
import { create_scopes, Scope, extract_names } from '../../utils/scope'; import { create_scopes, extract_names } from '../../utils/scope.js';
import { sanitize } from '../../../utils/names'; import { sanitize } from '../../../utils/names.js';
import TemplateScope from './TemplateScope'; import get_object from '../../utils/get_object.js';
import get_object from '../../utils/get_object'; import is_dynamic from '../../render_dom/wrappers/shared/is_dynamic.js';
import Block from '../../render_dom/Block';
import is_dynamic from '../../render_dom/wrappers/shared/is_dynamic';
import { b } from 'code-red'; import { b } from 'code-red';
import { invalidate } from '../../render_dom/invalidate'; import { invalidate } from '../../render_dom/invalidate.js';
import { Node, FunctionExpression, Identifier } from 'estree'; import { is_reserved_keyword } from '../../utils/reserved_keywords.js';
import { INode } from '../interfaces'; import replace_object from '../../utils/replace_object.js';
import { is_reserved_keyword } from '../../utils/reserved_keywords'; import is_contextual from './is_contextual.js';
import replace_object from '../../utils/replace_object'; import { clone } from '../../../utils/clone.js';
import is_contextual from './is_contextual'; import compiler_errors from '../../compiler_errors.js';
import EachBlock from '../EachBlock';
import { clone } from '../../../utils/clone';
import compiler_errors from '../../compiler_errors';
type Owner = INode;
const regex_contains_term_function_expression = /FunctionExpression/; const regex_contains_term_function_expression = /FunctionExpression/;
export default class Expression { export default class Expression {
type: 'Expression' = 'Expression' as const;
component: Component; /** @type {'Expression'} */
owner: Owner; type = ('Expression');
node: Node;
references: Set<string> = new Set(); /** @type {import('../../Component.js').default} */
dependencies: Set<string> = new Set(); component;
contextual_dependencies: Set<string> = new Set();
/** @type {Owner} */
template_scope: TemplateScope; owner;
scope: Scope;
scope_map: WeakMap<Node, Scope>; /** @type {import('estree').Node} */
node;
declarations: Array<Node | Node[]> = [];
uses_context = false; /** @type {Set<string>} */
references = new Set();
manipulated: Node;
/** @type {Set<string>} */
constructor( dependencies = new Set();
component: Component,
owner: Owner, /** @type {Set<string>} */
template_scope: TemplateScope, contextual_dependencies = new Set();
info: Node,
lazy?: boolean /** @type {import('./TemplateScope.js').default} */
) { template_scope;
// TODO revert to direct property access in prod?
Object.defineProperties(this, { /** @type {import('../../utils/scope.js').Scope} */
component: { scope;
value: component
} /** @type {WeakMap<import('estree').Node, import('../../utils/scope.js').Scope>} */
}); scope_map;
this.node = info; /** @type {Array<import('estree').Node | import('estree').Node[]>} */
this.template_scope = template_scope; declarations = [];
this.owner = owner; /** */
uses_context = false;
const { dependencies, contextual_dependencies, references } = this;
/** @type {import('estree').Node} */
let { map, scope } = create_scopes(info); manipulated;
this.scope = scope;
this.scope_map = map; /**
* @param {import('../../Component.js').default} component *
const expression = this; * @param {Owner} owner *
let function_expression; * @param {import('./TemplateScope.js').default} template_scope *
* @param {import('estree').Node} info *
// discover dependencies, but don't change the code yet * @param {boolean} [lazy] undefined
walk(info, { */
enter(node: any, parent: any, key: string) { constructor(component, owner, template_scope, info, lazy) {
// don't manipulate shorthand props twice // TODO revert to direct property access in prod?
if (key === 'key' && parent.shorthand) return; Object.defineProperties(this, {
// don't manipulate `import.meta`, `new.target` component: {
if (node.type === 'MetaProperty') return this.skip(); value: component
}
if (map.has(node)) { });
scope = map.get(node); this.node = info;
} this.template_scope = template_scope;
this.owner = owner;
if (!function_expression && regex_contains_term_function_expression.test(node.type)) { const { dependencies, contextual_dependencies, references } = this;
function_expression = node; let { map, scope } = create_scopes(info);
} this.scope = scope;
this.scope_map = map;
if (is_reference(node, parent)) { const expression = this;
const { name, nodes } = flatten_reference(node); let function_expression;
references.add(name); // discover dependencies, but don't change the code yet
walk(info, {
if (scope.has(name)) return;
/**
if (name[0] === '$') { * @param {any} node
const store_name = name.slice(1); * @param {any} parent
if (template_scope.names.has(store_name) || scope.has(store_name)) { * @param {string} key
return component.error(node, compiler_errors.contextual_store); */
} enter(node, parent, key) {
} // don't manipulate shorthand props twice
if (key === 'key' && parent.shorthand)
if (template_scope.is_let(name)) { return;
if (!lazy) { // don't manipulate `import.meta`, `new.target`
contextual_dependencies.add(name); if (node.type === 'MetaProperty')
dependencies.add(name); return this.skip();
} if (map.has(node)) {
} else if (template_scope.names.has(name)) { scope = map.get(node);
expression.uses_context = true; }
if (!function_expression && regex_contains_term_function_expression.test(node.type)) {
contextual_dependencies.add(name); function_expression = node;
}
const owner = template_scope.get_owner(name); if (is_reference(node, parent)) {
const is_index = owner.type === 'EachBlock' && owner.key && name === owner.index; const { name, nodes } = flatten_reference(node);
references.add(name);
if (!lazy || is_index) { if (scope.has(name))
template_scope.dependencies_for_name return;
.get(name) if (name[0] === '$') {
.forEach((name) => dependencies.add(name)); const store_name = name.slice(1);
} if (template_scope.names.has(store_name) || scope.has(store_name)) {
} else { return component.error(node, compiler_errors.contextual_store);
if (!lazy) { }
dependencies.add(name); }
} if (template_scope.is_let(name)) {
if (!lazy) {
component.add_reference(node, name); contextual_dependencies.add(name);
component.warn_if_undefined(name, nodes[0], template_scope); dependencies.add(name);
} }
}
this.skip(); else if (template_scope.names.has(name)) {
} expression.uses_context = true;
contextual_dependencies.add(name);
// track any assignments from template expressions as mutable const owner = template_scope.get_owner(name);
let names; const is_index = owner.type === 'EachBlock' && owner.key && name === owner.index;
let deep = false; if (!lazy || is_index) {
template_scope.dependencies_for_name
if (function_expression) { .get(name)
if (node.type === 'AssignmentExpression') { .forEach(/** @param {any} name */ (name) => dependencies.add(name));
deep = node.left.type === 'MemberExpression'; }
names = extract_names(deep ? get_object(node.left) : node.left); }
} else if (node.type === 'UpdateExpression') { else {
deep = node.argument.type === 'MemberExpression'; if (!lazy) {
names = extract_names(get_object(node.argument)); dependencies.add(name);
} }
} component.add_reference(node, name);
component.warn_if_undefined(name, nodes[0], template_scope);
if (names) { }
names.forEach((name) => { this.skip();
if (template_scope.names.has(name)) { }
if (template_scope.is_const(name)) { // track any assignments from template expressions as mutable
component.error(node, compiler_errors.invalid_const_update(name)); let names;
} let deep = false;
if (function_expression) {
template_scope.dependencies_for_name.get(name).forEach((name) => { if (node.type === 'AssignmentExpression') {
const variable = component.var_lookup.get(name); deep = node.left.type === 'MemberExpression';
if (variable) variable[deep ? 'mutated' : 'reassigned'] = true; names = extract_names(deep ? get_object(node.left) : node.left);
}); }
const each_block = template_scope.get_owner(name); else if (node.type === 'UpdateExpression') {
(each_block as EachBlock).has_binding = true; deep = node.argument.type === 'MemberExpression';
} else { names = extract_names(get_object(node.argument));
component.add_reference(node, name); }
}
const variable = component.var_lookup.get(name); if (names) {
names.forEach(/** @param {any} name */ (name) => {
if (variable) { if (template_scope.names.has(name)) {
variable[deep ? 'mutated' : 'reassigned'] = true; if (template_scope.is_const(name)) {
} component.error(node, compiler_errors.invalid_const_update(name));
}
const declaration: any = scope.find_owner(name)?.declarations.get(name); template_scope.dependencies_for_name.get(name).forEach(/** @param {any} name */ (name) => {
const variable = component.var_lookup.get(name);
if (declaration) { if (variable)
if (declaration.kind === 'const' && !deep) { variable[deep ? 'mutated' : 'reassigned'] = true;
component.error(node, { });
code: 'assignment-to-const', const each_block = template_scope.get_owner(name);
message: 'You are assigning to a const' ( /** @type {import('../EachBlock.js').default} */(each_block)).has_binding = true;
}); }
} else {
} else if (variable && variable.writable === false && !deep) { component.add_reference(node, name);
component.error(node, { const variable = component.var_lookup.get(name);
code: 'assignment-to-const', if (variable) {
message: 'You are assigning to a const' variable[deep ? 'mutated' : 'reassigned'] = true;
}); }
}
} /** @type {any} */
}); const declaration = scope.find_owner(name)?.declarations.get(name);
} if (declaration) {
}, if (declaration.kind === 'const' && !deep) {
component.error(node, {
leave(node: Node) { code: 'assignment-to-const',
if (map.has(node)) { message: 'You are assigning to a const'
scope = scope.parent; });
} }
}
if (node === function_expression) { else if (variable && variable.writable === false && !deep) {
function_expression = null; component.error(node, {
} code: 'assignment-to-const',
} message: 'You are assigning to a const'
}); });
} }
}
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;
/** @param {import('estree').Node} node */
const variable = this.component.var_lookup.get(name); leave(node) {
return is_dynamic(variable); if (map.has(node)) {
}); scope = scope.parent;
} }
if (node === function_expression) {
dynamic_contextual_dependencies() { function_expression = null;
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); dynamic_dependencies() {
} return Array.from(this.dependencies).filter(/** @param {any} name */ (name) => {
); if (this.template_scope.is_let(name))
}); return true;
} if (is_reserved_keyword(name))
return true;
// TODO move this into a render-dom wrapper? const variable = this.component.var_lookup.get(name);
manipulate(block?: Block, ctx?: string | void) { return is_dynamic(variable);
// TODO ideally we wouldn't end up calling this method });
// multiple times }
if (this.manipulated) return this.manipulated; dynamic_contextual_dependencies() {
return Array.from(this.contextual_dependencies).filter(/** @param {any} name */ (name) => {
const { component, declarations, scope_map: map, template_scope, owner } = this; return Array.from(this.template_scope.dependencies_for_name.get(name)).some(
let scope = this.scope;
/** @param {any} variable_name */
let function_expression; (variable_name) => {
const variable = this.component.var_lookup.get(variable_name);
let dependencies: Set<string>; return is_dynamic(variable);
let contextual_dependencies: Set<string>; });
});
const node = walk(this.node, { }
enter(node: any, parent: any) { // TODO move this into a render-dom wrapper?
if (node.type === 'Property' && node.shorthand) {
node.value = clone(node.value); /**
node.shorthand = false; * @param {import('../../render_dom/Block.js').default} [block]
} * @param {string | void} [ctx]
*/
if (map.has(node)) { manipulate(block, ctx) {
scope = map.get(node); // TODO ideally we wouldn't end up calling this method
} // multiple times
if (this.manipulated)
if (node.type === 'Identifier' && is_reference(node, parent)) { return this.manipulated;
const { name } = flatten_reference(node); const { component, declarations, scope_map: map, template_scope, owner } = this;
let scope = this.scope;
if (scope.has(name)) return; let function_expression;
if (function_expression) { /** @type {Set<string>} */
if (template_scope.names.has(name)) { let dependencies;
contextual_dependencies.add(name);
/** @type {Set<string>} */
template_scope.dependencies_for_name.get(name).forEach((dependency) => { let contextual_dependencies;
dependencies.add(dependency); const node = walk(this.node, {
});
} else { /**
dependencies.add(name); * @param {any} node
component.add_reference(node, name); // TODO is this redundant/misplaced? * @param {any} parent
} */
} else if (is_contextual(component, template_scope, name)) { enter(node, parent) {
const reference = block.renderer.reference(node, ctx); if (node.type === 'Property' && node.shorthand) {
this.replace(reference); node.value = clone(node.value);
} node.shorthand = false;
}
this.skip(); if (map.has(node)) {
} scope = map.get(node);
}
if (!function_expression) { if (node.type === 'Identifier' && is_reference(node, parent)) {
if (node.type === 'AssignmentExpression') { const { name } = flatten_reference(node);
// TODO should this be a warning/error? `<p>{foo = 1}</p>` if (scope.has(name))
} return;
if (function_expression) {
if (node.type === 'FunctionExpression' || node.type === 'ArrowFunctionExpression') { if (template_scope.names.has(name)) {
function_expression = node; contextual_dependencies.add(name);
dependencies = new Set(); template_scope.dependencies_for_name.get(name).forEach(/** @param {any} dependency */ (dependency) => {
contextual_dependencies = new Set(); dependencies.add(dependency);
} });
} }
}, else {
dependencies.add(name);
leave(node: Node, parent: Node) { component.add_reference(node, name); // TODO is this redundant/misplaced?
if (map.has(node)) scope = scope.parent; }
}
if (node === function_expression) { else if (is_contextual(component, template_scope, name)) {
const id = component.get_unique_name(sanitize(get_function_name(node, owner))); const reference = block.renderer.reference(node, ctx);
this.replace(reference);
const declaration = b`const ${id} = ${node}`; }
const extract_functions = () => { this.skip();
const deps = Array.from(contextual_dependencies); }
const function_expression = node as FunctionExpression; if (!function_expression) {
if (node.type === 'AssignmentExpression') {
const has_args = function_expression.params.length > 0; // TODO should this be a warning/error? `<p>{foo = 1}</p>`
function_expression.params = [ }
...deps.map((name) => ({ type: 'Identifier', name } as Identifier)), if (node.type === 'FunctionExpression' || node.type === 'ArrowFunctionExpression') {
...function_expression.params function_expression = node;
]; dependencies = new Set();
contextual_dependencies = new Set();
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); * @param {import('estree').Node} node
* @param {import('estree').Node} parent
this.replace(id as any); */
leave(node, parent) {
const func_declaration = has_args if (map.has(node))
? b`function ${id}(...args) { 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 = /** @type {import('estree').FunctionExpression} */ (node);
const has_args = function_expression.params.length > 0;
function_expression.params = [
...deps.map(/** @param {any} name */ (name) => ( /** @type {import('estree').Identifier} */({ type: 'Identifier', name }))),
...function_expression.params
];
const context_args = deps.map(/** @param {any} name */ (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(/** @type {any} */ (id));
const func_declaration = has_args
? b `function ${id}(...args) {
return ${callee}(${context_args}, ...args); return ${callee}(${context_args}, ...args);
}` }`
: b`function ${id}() { : b `function ${id}() {
return ${callee}(${context_args}); return ${callee}(${context_args});
}`; }`;
return { deps, func_declaration }; return { deps, func_declaration };
}; };
if (owner.type === 'ConstTag') {
if (owner.type === 'ConstTag') { // we need a combo block/init recipe
// we need a combo block/init recipe if (contextual_dependencies.size === 0) {
if (contextual_dependencies.size === 0) { let child_scope = scope;
let child_scope = scope; walk(node, {
walk(node, {
enter(node: Node, parent: any) { /**
if (map.has(node)) child_scope = map.get(node); * @param {import('estree').Node} node
if (node.type === 'Identifier' && is_reference(node, parent)) { * @param {any} parent
if (child_scope.has(node.name)) return; */
this.replace(block.renderer.reference(node, ctx)); enter(node, parent) {
} if (map.has(node))
}, child_scope = map.get(node);
leave(node: Node) { if (node.type === 'Identifier' && is_reference(node, parent)) {
if (map.has(node)) child_scope = child_scope.parent; if (child_scope.has(node.name))
} return;
}); this.replace(block.renderer.reference(node, ctx));
} else { }
const { func_declaration } = extract_functions(); },
this.replace(func_declaration[0]);
} /** @param {import('estree').Node} node */
} else if (dependencies.size === 0 && contextual_dependencies.size === 0) { leave(node) {
// we can hoist this out of the component completely if (map.has(node))
component.fully_hoisted.push(declaration); child_scope = child_scope.parent;
}
this.replace(id as any); });
}
component.add_var(node, { else {
name: id.name, const { func_declaration } = extract_functions();
internal: true, this.replace(func_declaration[0]);
hoistable: true, }
referenced: true }
}); else if (dependencies.size === 0 && contextual_dependencies.size === 0) {
} else if (contextual_dependencies.size === 0) { // we can hoist this out of the component completely
// function can be hoisted inside the component init component.fully_hoisted.push(declaration);
component.partly_hoisted.push(declaration); this.replace(/** @type {any} */ (id));
component.add_var(node, {
block.renderer.add_to_context(id.name); name: id.name,
this.replace(block.renderer.reference(id)); internal: true,
} else { hoistable: true,
// we need a combo block/init recipe referenced: true
const { deps, func_declaration } = extract_functions(); });
}
if (owner.type === 'Attribute' && owner.parent.name === 'slot') { else if (contextual_dependencies.size === 0) {
const dep_scopes = new Set<INode>(deps.map((name) => template_scope.get_owner(name))); // function can be hoisted inside the component init
// find the nearest scopes component.partly_hoisted.push(declaration);
let node: INode = owner.parent; block.renderer.add_to_context(id.name);
while (node && !dep_scopes.has(node)) { this.replace(block.renderer.reference(id));
node = node.parent; }
} else {
// we need a combo block/init recipe
const func_expression = func_declaration[0]; const { deps, func_declaration } = extract_functions();
if (owner.type === 'Attribute' && owner.parent.name === 'slot') {
if (node.type === 'InlineComponent' || node.type === 'SlotTemplate') { const dep_scopes = new Set(deps.map(/** @param {any} name */ (name) => template_scope.get_owner(name)));
// <Comp let:data /> // find the nearest scopes
this.replace(func_expression);
} else { /** @type {import('../interfaces.js').INode} */
// {#each}, {#await} let node = owner.parent;
const func_id = component.get_unique_name(id.name + '_func'); while (node && !dep_scopes.has(node)) {
block.renderer.add_to_context(func_id.name, true); node = node.parent;
// rename #ctx -> child_ctx; }
walk(func_expression, { const func_expression = func_declaration[0];
enter(node: Node) { if (node.type === 'InlineComponent' || node.type === 'SlotTemplate') {
if (node.type === 'Identifier' && node.name === '#ctx') { // <Comp let:data />
node.name = 'child_ctx'; this.replace(func_expression);
} }
} else {
}); // {#each}, {#await}
// add to get_xxx_context const func_id = component.get_unique_name(id.name + '_func');
// child_ctx[x] = function () { ... } block.renderer.add_to_context(func_id.name, true);
(template_scope.get_owner(deps[0]) as EachBlock).contexts.push({ // rename #ctx -> child_ctx;
type: 'DestructuredVariable', walk(func_expression, {
key: func_id,
modifier: () => func_expression, /** @param {import('estree').Node} node */
default_modifier: (node) => node enter(node) {
}); if (node.type === 'Identifier' && node.name === '#ctx') {
this.replace(block.renderer.reference(func_id)); node.name = 'child_ctx';
} }
} else { }
declarations.push(func_declaration); });
} // add to get_xxx_context
} // child_ctx[x] = function () { ... }
( /** @type {import('../EachBlock.js').default} */(template_scope.get_owner(deps[0]))).contexts.push({
function_expression = null; type: 'DestructuredVariable',
dependencies = null; key: func_id,
contextual_dependencies = null; modifier: () => func_expression,
default_modifier: /** @param {any} node */ (node) => node
if (parent && parent.type === 'Property') { });
parent.method = false; this.replace(block.renderer.reference(func_id));
} }
} }
else {
if (node.type === 'AssignmentExpression' || node.type === 'UpdateExpression') { declarations.push(func_declaration);
const assignee = node.type === 'AssignmentExpression' ? node.left : node.argument; }
}
const object_name = get_object(assignee).name; function_expression = null;
dependencies = null;
if (scope.has(object_name)) return; contextual_dependencies = null;
if (parent && parent.type === 'Property') {
// normally (`a = 1`, `b.c = 2`), there'll be a single name parent.method = false;
// (a or b). In destructuring cases (`[d, e] = [e, d]`) there }
// may be more, in which case we need to tack the extra ones }
// onto the initial function call if (node.type === 'AssignmentExpression' || node.type === 'UpdateExpression') {
const names = new Set(extract_names(assignee as Node)); const assignee = node.type === 'AssignmentExpression' ? node.left : node.argument;
const object_name = get_object(assignee).name;
const traced: Set<string> = new Set(); if (scope.has(object_name))
names.forEach((name) => { return;
const dependencies = template_scope.dependencies_for_name.get(name); // normally (`a = 1`, `b.c = 2`), there'll be a single name
if (dependencies) { // (a or b). In destructuring cases (`[d, e] = [e, d]`) there
dependencies.forEach((name) => traced.add(name)); // may be more, in which case we need to tack the extra ones
} else { // onto the initial function call
traced.add(name); const names = new Set(extract_names(/** @type {import('estree').Node} */ (assignee)));
}
}); /** @type {Set<string>} */
const traced = new Set();
const context = block.bindings.get(object_name); names.forEach(/** @param {any} name */ (name) => {
const dependencies = template_scope.dependencies_for_name.get(name);
if (context) { if (dependencies) {
// for `{#each array as item}` dependencies.forEach(/** @param {any} name */ (name) => traced.add(name));
// replace `item = 1` to `each_array[each_index] = 1`, this allow us to mutate the array }
// rather than mutating the local `item` variable else {
const { snippet, object, property } = context; traced.add(name);
const replaced: any = replace_object(assignee, snippet); }
if (node.type === 'AssignmentExpression') { });
node.left = replaced; const context = block.bindings.get(object_name);
} else { if (context) {
node.argument = replaced; // for `{#each array as item}`
} // replace `item = 1` to `each_array[each_index] = 1`, this allow us to mutate the array
contextual_dependencies.add(object.name); // rather than mutating the local `item` variable
contextual_dependencies.add(property.name); const { snippet, object, property } = context;
}
/** @type {any} */
this.replace(invalidate(block.renderer, scope, node, traced)); const replaced = replace_object(assignee, snippet);
} if (node.type === 'AssignmentExpression') {
} node.left = replaced;
}); }
else {
if (declarations.length > 0) { node.argument = replaced;
block.maintain_context = true; }
declarations.forEach((declaration) => { contextual_dependencies.add(object.name);
block.chunks.init.push(declaration); contextual_dependencies.add(property.name);
}); }
} this.replace(invalidate(block.renderer, scope, node, traced));
}
return (this.manipulated = node as Node); }
} });
if (declarations.length > 0) {
block.maintain_context = true;
declarations.forEach(/** @param {any} declaration */ (declaration) => {
block.chunks.init.push(declaration);
});
}
return (this.manipulated = /** @type {import('estree').Node} */ (node));
}
} }
/**
* @param {any} _node
* @param {any} parent
*/
function get_function_name(_node, parent) { function get_function_name(_node, parent) {
if (parent.type === 'EventHandler') { if (parent.type === 'EventHandler') {
return `${parent.name}_handler`; return `${parent.name}_handler`;
} }
if (parent.type === 'Action') {
return `${parent.name}_function`;
}
return 'func';
}
if (parent.type === 'Action') {
return `${parent.name}_function`;
}
return 'func'; /** @typedef {INode} Owner */
}

Loading…
Cancel
Save