refactor: tidy up compile/nodes

pull/8034/head
Simon He 4 years ago
parent b05b684b95
commit a5afc9d55b

@ -101,11 +101,10 @@ export default class Binding extends Node {
validate_binding_rest_properties(scope: TemplateScope) { validate_binding_rest_properties(scope: TemplateScope) {
this.expression.references.forEach(name => { this.expression.references.forEach(name => {
const each_block = scope.get_owner(name); const each_block = scope.get_owner(name);
if (each_block && each_block.type === 'EachBlock') { if (!each_block || each_block.type !== 'EachBlock') return;
const rest_node = each_block.context_rest_properties.get(name); const rest_node = each_block.context_rest_properties.get(name);
if (rest_node) { if (rest_node) {
this.component.warn(rest_node as any, compiler_warnings.invalid_rest_eachblock_binding(name)); this.component.warn(rest_node as any, compiler_warnings.invalid_rest_eachblock_binding(name));
}
} }
}); });
} }

@ -23,7 +23,7 @@ export default class ConstTag extends Node {
context_rest_properties: Map<string, ESTreeNode> = new Map(); context_rest_properties: Map<string, ESTreeNode> = new Map();
assignees: Set<string> = new Set(); assignees: Set<string> = new Set();
dependencies: Set<string> = new Set(); dependencies: Set<string> = new Set();
constructor(component: Component, parent: INodeAllowConstTag, scope: TemplateScope, info: ConstTagType) { constructor(component: Component, parent: INodeAllowConstTag, scope: TemplateScope, info: ConstTagType) {
super(component, parent, scope, info); super(component, parent, scope, info);
@ -37,22 +37,22 @@ export default class ConstTag extends Node {
const { assignees, dependencies } = this; const { assignees, dependencies } = this;
extract_identifiers(info.expression.left).forEach(({ name }) => { extract_identifiers(info.expression.left).forEach(({ name }) => {
assignees.add(name); assignees.add(name);
const owner = this.scope.get_owner(name); const owner = this.scope.get_owner(name);
if (owner === parent) { if (owner === parent) {
component.error(info, compiler_errors.invalid_const_declaration(name)); component.error(info, compiler_errors.invalid_const_declaration(name));
} }
}); });
walk(info.expression.right, { walk(info.expression.right, {
enter(node, parent) { enter(node, parent) {
if (is_reference(node as NodeWithPropertyDefinition, parent as NodeWithPropertyDefinition)) { if (is_reference(node as NodeWithPropertyDefinition, parent as NodeWithPropertyDefinition)) {
const identifier = get_object(node as any); const identifier = get_object(node as any);
const { name } = identifier; const { name } = identifier;
dependencies.add(name); dependencies.add(name);
} }
} }
}); });
} }
parse_expression() { parse_expression() {
@ -66,7 +66,7 @@ export default class ConstTag extends Node {
this.expression = new Expression(this.component, this, this.scope, this.node.expression.right); this.expression = new Expression(this.component, this, this.scope, this.node.expression.right);
this.contexts.forEach(context => { this.contexts.forEach(context => {
const owner = this.scope.get_owner(context.key.name); const owner = this.scope.get_owner(context.key.name);
if (owner && owner.type === 'ConstTag' && owner.parent === this.parent) { if (owner?.type === 'ConstTag' && owner?.parent === this.parent) {
this.component.error(this.node, compiler_errors.invalid_const_declaration(context.key.name)); this.component.error(this.node, compiler_errors.invalid_const_declaration(context.key.name));
} }
this.scope.add(context.key.name, this.expression.dependencies, this); this.scope.add(context.key.name, this.expression.dependencies, this);

@ -13,8 +13,6 @@ export default class DebugTag extends Node {
constructor(component: Component, parent: INode, scope: TemplateScope, info: TemplateNode) { constructor(component: Component, parent: INode, scope: TemplateScope, info: TemplateNode) {
super(component, parent, scope, info); super(component, parent, scope, info);
this.expressions = info.identifiers.map((node: EsTreeNode) => { this.expressions = info.identifiers.map((node: EsTreeNode) => new Expression(component, parent, scope, node));
return new Expression(component, parent, scope, node);
});
} }
} }

@ -261,24 +261,22 @@ export default class Element extends Node {
} }
} }
if (this.name === 'textarea') { if (this.name === 'textarea' && info.children.length) {
if (info.children.length > 0) { const value_attribute = info.attributes.find(node => node.name === 'value');
const value_attribute = info.attributes.find(node => node.name === 'value'); if (value_attribute) {
if (value_attribute) { component.error(value_attribute, compiler_errors.textarea_duplicate_value);
component.error(value_attribute, compiler_errors.textarea_duplicate_value); return;
return; }
}
// this is an egregious hack, but it's the easiest way to get <textarea> // this is an egregious hack, but it's the easiest way to get <textarea>
// children treated the same way as a value attribute // children treated the same way as a value attribute
info.attributes.push({ info.attributes.push({
type: 'Attribute', type: 'Attribute',
name: 'value', name: 'value',
value: info.children value: info.children
}); });
info.children = []; info.children = [];
}
} }
if (this.name === 'option') { if (this.name === 'option') {
@ -348,12 +346,12 @@ export default class Element extends Node {
} }
case 'Transition': case 'Transition':
{ {
const transition = new Transition(component, this, scope, node); const transition = new Transition(component, this, scope, node);
if (node.intro) this.intro = transition; if (node.intro) this.intro = transition;
if (node.outro) this.outro = transition; if (node.outro) this.outro = transition;
break; break;
} }
case 'Animation': case 'Animation':
this.animation = new Animation(component, this, scope, node); this.animation = new Animation(component, this, scope, node);
@ -688,20 +686,10 @@ export default class Element extends Node {
if (this.name === 'label') { if (this.name === 'label') {
const has_input_child = (children: INode[]) => { const has_input_child = (children: INode[]) => {
if (children.some(child => (child instanceof Element && (a11y_labelable.has(child.name) || child.name === 'slot')))) { return children.some(child => (child instanceof Element && (a11y_labelable.has(child.name) || child.name === 'slot'))) ||
return true; children.some(child =>
} ('children' in child) && child.children.length && has_input_child(child.children)
);
for (const child of children) {
if (!('children' in child) || child.children.length === 0) {
continue;
}
if (has_input_child(child.children)) {
return true;
}
}
return false;
}; };
if (!attribute_map.has('for') && !has_input_child(this.children)) { if (!attribute_map.has('for') && !has_input_child(this.children)) {
@ -714,11 +702,9 @@ export default class Element extends Node {
return; return;
} }
let has_caption; const has_caption = this.children
const track = this.children.find((i: Element) => i.name === 'track'); .find((i: Element) => i.name === 'track')
if (track) { ?.attributes.find(a => a.name === 'kind' && a.get_static_value() === 'captions');
has_caption = track.attributes.find(a => a.name === 'kind' && a.get_static_value() === 'captions');
}
if (!has_caption) { if (!has_caption) {
component.warn(this, compiler_warnings.a11y_media_has_caption); component.warn(this, compiler_warnings.a11y_media_has_caption);
@ -1011,15 +997,14 @@ export default class Element extends Node {
const attribute = this.attributes.find(a => a.name === attribute_name); const attribute = this.attributes.find(a => a.name === attribute_name);
if (attribute && !attribute.is_true) { if (attribute && !attribute.is_true) {
attribute.chunks.forEach((chunk, index) => { attribute.chunks.forEach((chunk, index) => {
if (chunk.type === 'Text') { if (chunk.type !== 'Text') return;
let data = chunk.data.replace(regex_any_repeated_whitespaces, ' '); let data = chunk.data.replace(regex_any_repeated_whitespaces, ' ');
if (index === 0) { if (index === 0) {
data = data.trimLeft(); data = data.trimLeft();
} else if (index === attribute.chunks.length - 1) { } else if (index === attribute.chunks.length - 1) {
data = data.trimRight(); data = data.trimRight();
}
chunk.data = data;
} }
chunk.data = data;
}); });
} }
}); });

@ -52,12 +52,8 @@ export default class EventHandler extends Node {
} }
get reassigned(): boolean { get reassigned(): boolean {
if (!this.expression) { if (!this.expression ||
return false; regex_contains_term_function_expression.test(this.expression.node.type)) {
}
const node = this.expression.node;
if (regex_contains_term_function_expression.test(node.type)) {
return false; return false;
} }

@ -24,7 +24,7 @@ export default class Head extends Node {
return (child.type !== 'Text' || regex_non_whitespace_character.test(child.data)); return (child.type !== 'Text' || regex_non_whitespace_character.test(child.data));
})); }));
if (this.children.length > 0) { if (this.children.length) {
this.id = `svelte-${hash(this.component.source.slice(this.start, this.end))}`; this.id = `svelte-${hash(this.component.source.slice(this.start, this.end))}`;
} }
} }

@ -52,7 +52,7 @@ export default class InlineComponent extends Node {
this.css_custom_properties.push(new Attribute(component, this, scope, node)); this.css_custom_properties.push(new Attribute(component, this, scope, node));
break; break;
} }
// fallthrough // fallthrough
case 'Spread': case 'Spread':
this.attributes.push(new Attribute(component, this, scope, node)); this.attributes.push(new Attribute(component, this, scope, node));
break; break;
@ -172,9 +172,7 @@ function not_whitespace_text(node) {
function get_namespace(parent: Node, explicit_namespace: string) { function get_namespace(parent: Node, explicit_namespace: string) {
const parent_element = parent.find_nearest(/^Element/); const parent_element = parent.find_nearest(/^Element/);
if (!parent_element) { return !parent_element
return explicit_namespace; ? explicit_namespace
} : parent_element.namespace;
return parent_element.namespace;
} }

@ -42,19 +42,20 @@ export default class SlotTemplate extends Node {
break; break;
} }
case 'Attribute': { case 'Attribute': {
if (node.name === 'slot') { if (node.name !== 'slot') {
this.slot_attribute = new Attribute(component, this, scope, node); throw new Error(`Invalid attribute '${node.name}' in <svelte:fragment>`);
if (!this.slot_attribute.is_static) {
return component.error(node, compiler_errors.invalid_slot_attribute);
}
const value = this.slot_attribute.get_static_value();
if (typeof value === 'boolean') {
return component.error(node, compiler_errors.invalid_slot_attribute_value_missing);
}
this.slot_template_name = value as string;
break;
} }
throw new Error(`Invalid attribute '${node.name}' in <svelte:fragment>`);
this.slot_attribute = new Attribute(component, this, scope, node);
if (!this.slot_attribute.is_static) {
return component.error(node, compiler_errors.invalid_slot_attribute);
}
const value = this.slot_attribute.get_static_value();
if (typeof value === 'boolean') {
return component.error(node, compiler_errors.invalid_slot_attribute_value_missing);
}
this.slot_template_name = value as string;
break;
} }
default: default:
throw new Error(`Not implemented: ${node.type}`); throw new Error(`Not implemented: ${node.type}`);

@ -40,16 +40,15 @@ export default class Text extends Node {
if (parent_element.type === 'InlineComponent') return parent_element.children.length === 1 && this === parent_element.children[0]; if (parent_element.type === 'InlineComponent') return parent_element.children.length === 1 && this === parent_element.children[0];
// svg namespace exclusions // svg namespace exclusions
if (regex_ends_with_svg.test(parent_element.namespace)) { if (regex_ends_with_svg.test(parent_element.namespace) && this.prev?.type === 'Element' && this.prev?.name === 'tspan') {
if (this.prev && this.prev.type === 'Element' && this.prev.name === 'tspan') return false; return false;
} }
return parent_element.namespace || elements_without_text.has(parent_element.name); return parent_element.namespace || elements_without_text.has(parent_element.name);
} }
keep_space(): boolean { keep_space(): boolean {
if (this.component.component_options.preserveWhitespace) return true; return this.component.component_options.preserveWhitespace || this.within_pre();
return this.within_pre();
} }
within_pre(): boolean { within_pre(): boolean {

@ -14,7 +14,7 @@ export default class Title extends Node {
super(component, parent, scope, info); super(component, parent, scope, info);
this.children = map_children(component, parent, scope, info.children); this.children = map_children(component, parent, scope, info.children);
if (info.attributes.length > 0) { if (info.attributes.length) {
component.error(info.attributes[0], compiler_errors.illegal_attribute_title); component.error(info.attributes[0], compiler_errors.illegal_attribute_title);
return; return;
} }

@ -47,11 +47,10 @@ export default class Window extends Node {
fuzzymatch(node.name, valid_bindings) fuzzymatch(node.name, valid_bindings)
); );
if (match) { return component.error(node, compiler_errors.invalid_binding_on(node.name, '<svelte:window>',
return component.error(node, compiler_errors.invalid_binding_on(node.name, '<svelte:window>', ` (did you mean '${match}'?)`)); match
} else { ? ` (did you mean '${match}'?)`
return component.error(node, compiler_errors.invalid_binding_on(node.name, '<svelte:window>', ` — valid bindings are ${list(valid_bindings)}`)); : ` — valid bindings are ${list(valid_bindings)}`));
}
} }
this.bindings.push(new Binding(component, this, scope, node)); this.bindings.push(new Binding(component, this, scope, node));

@ -19,7 +19,7 @@ export default class AbstractBlock extends Node {
const child = this.children[0]; const child = this.children[0];
if (!child || (child.type === 'Text' && !regex_non_whitespace_characters.test(child.data))) { if (child?.type === 'Text' && !regex_non_whitespace_characters.test(child.data)) {
this.component.warn(this, compiler_warnings.empty_block); this.component.warn(this, compiler_warnings.empty_block);
} }
} }

@ -48,7 +48,7 @@ export function unpack_destructuring({
context_rest_properties.set((node.argument as Identifier).name, node); context_rest_properties.set((node.argument as Identifier).name, node);
} else if (node.type === 'ArrayPattern') { } else if (node.type === 'ArrayPattern') {
node.elements.forEach((element, i) => { node.elements.forEach((element, i) => {
if (element && element.type === 'RestElement') { if (element?.type === 'RestElement') {
unpack_destructuring({ unpack_destructuring({
contexts, contexts,
node: element, node: element,
@ -59,7 +59,7 @@ export function unpack_destructuring({
context_rest_properties context_rest_properties
}); });
context_rest_properties.set((element.argument as Identifier).name, element); context_rest_properties.set((element.argument as Identifier).name, element);
} else if (element && element.type === 'AssignmentPattern') { } else if (element?.type === 'AssignmentPattern') {
const n = contexts.length; const n = contexts.length;
mark_referenced(element.right, scope, component); mark_referenced(element.right, scope, component);
@ -195,11 +195,10 @@ function mark_referenced(
) { ) {
walk(node, { walk(node, {
enter(node: any, parent: any) { enter(node: any, parent: any) {
if (is_reference(node, parent)) { if (!is_reference(node, parent)) return;
const { name } = flatten_reference(node); const { name } = flatten_reference(node);
if (!scope.is_let(name) && !scope.names.has(name)) { if (!scope.is_let(name) && !scope.names.has(name)) {
component.add_reference(node, name); component.add_reference(node, name);
}
} }
} }
}); });

@ -128,51 +128,49 @@ export default class Expression {
deep = node.left.type === 'MemberExpression'; deep = node.left.type === 'MemberExpression';
names = extract_names(deep ? get_object(node.left) : node.left); names = extract_names(deep ? get_object(node.left) : node.left);
} else if (node.type === 'UpdateExpression') { } else if (node.type === 'UpdateExpression') {
deep = node.argument.type === 'MemberExpression'; deep = node.argument.type === 'MemberExpression';
names = extract_names(get_object(node.argument)); names = extract_names(get_object(node.argument));
} }
} }
if (names) { names?.forEach(name => {
names.forEach(name => { if (template_scope.names.has(name)) {
if (template_scope.names.has(name)) { if (template_scope.is_const(name)) {
if (template_scope.is_const(name)) { component.error(node, compiler_errors.invalid_const_update(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 as EachBlock).has_binding = true;
} else {
component.add_reference(node, name);
template_scope.dependencies_for_name.get(name).forEach(name => {
const variable = component.var_lookup.get(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 as EachBlock).has_binding = true;
} else {
component.add_reference(node, name);
if (variable) { const variable = component.var_lookup.get(name);
variable[deep ? 'mutated' : 'reassigned'] = true;
}
const declaration: any = scope.find_owner(name)?.declarations.get(name); if (variable) {
variable[deep ? 'mutated' : 'reassigned'] = true;
}
if (declaration) { const declaration: any = scope.find_owner(name)?.declarations.get(name);
if (declaration.kind === 'const' && !deep) {
component.error(node, { if (declaration) {
code: 'assignment-to-const', if (declaration.kind === 'const' && !deep) {
message: 'You are assigning to a const'
});
}
} else if (variable && variable.writable === false && !deep) {
component.error(node, { component.error(node, {
code: 'assignment-to-const', code: 'assignment-to-const',
message: 'You are assigning to a 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: Node) { leave(node: Node) {
@ -189,8 +187,8 @@ export default class Expression {
dynamic_dependencies() { dynamic_dependencies() {
return Array.from(this.dependencies).filter(name => { return Array.from(this.dependencies).filter(name => {
if (this.template_scope.is_let(name)) return true; if (this.template_scope.is_let(name) ||
if (is_reserved_keyword(name)) return true; is_reserved_keyword(name)) return true;
const variable = this.component.var_lookup.get(name); const variable = this.component.var_lookup.get(name);
return is_dynamic(variable); return is_dynamic(variable);
@ -428,11 +426,9 @@ export default class Expression {
} }
}); });
if (declarations.length > 0) { if (declarations.length) {
block.maintain_context = true; block.maintain_context = true;
declarations.forEach(declaration => { block.chunks.init.push(...declarations);
block.chunks.init.push(declaration);
});
} }
return (this.manipulated = node as Node); return (this.manipulated = node as Node);

@ -36,10 +36,9 @@ export default class Node {
} }
cannot_use_innerhtml() { cannot_use_innerhtml() {
if (this.can_use_innerhtml !== false) { if (this.can_use_innerhtml === false) return;
this.can_use_innerhtml = false; this.can_use_innerhtml = false;
if (this.parent) this.parent.cannot_use_innerhtml(); if (this.parent) this.parent.cannot_use_innerhtml();
}
} }
find_nearest(selector: RegExp) { find_nearest(selector: RegExp) {

@ -28,8 +28,7 @@ export default class TemplateScope {
} }
child() { child() {
const child = new TemplateScope(this); return new TemplateScope(this);
return child;
} }
is_top_level(name: string) { is_top_level(name: string) {

@ -7,85 +7,85 @@ import check_graph_for_cycles from '../../utils/check_graph_for_cycles';
import compiler_errors from '../../compiler_errors'; import compiler_errors from '../../compiler_errors';
export default function get_const_tags(children: TemplateNode[], component: Component, node: INodeAllowConstTag, parent: INode): [ConstTag[], Array<Exclude<INode, ConstTag>>] { export default function get_const_tags(children: TemplateNode[], component: Component, node: INodeAllowConstTag, parent: INode): [ConstTag[], Array<Exclude<INode, ConstTag>>] {
const const_tags: ConstTagType[] = []; const const_tags: ConstTagType[] = [];
const others: Array<Exclude<TemplateNode, ConstTagType>> = []; const others: Array<Exclude<TemplateNode, ConstTagType>> = [];
for (const child of children) { for (const child of children) {
if (child.type === 'ConstTag') { if (child.type === 'ConstTag') {
const_tags.push(child as ConstTagType); const_tags.push(child as ConstTagType);
} else { } else {
others.push(child); others.push(child);
} }
} }
const consts_nodes = const_tags.map(tag => new ConstTag(component, node, node.scope, tag)); const consts_nodes = const_tags.map(tag => new ConstTag(component, node, node.scope, tag));
const sorted_consts_nodes = sort_consts_nodes(consts_nodes, component); const sorted_consts_nodes = sort_consts_nodes(consts_nodes, component);
sorted_consts_nodes.forEach(node => node.parse_expression()); sorted_consts_nodes.forEach(node => node.parse_expression());
const children_nodes = map_children(component, parent, node.scope, others); const children_nodes = map_children(component, parent, node.scope, others);
return [sorted_consts_nodes, children_nodes as Array<Exclude<INode, ConstTag>>]; return [sorted_consts_nodes, children_nodes as Array<Exclude<INode, ConstTag>>];
} }
function sort_consts_nodes(consts_nodes: ConstTag[], component: Component) { function sort_consts_nodes(consts_nodes: ConstTag[], component: Component) {
type ConstNode = { type ConstNode = {
assignees: Set<string>; assignees: Set<string>;
dependencies: Set<string>; dependencies: Set<string>;
node: ConstTag; node: ConstTag;
}; };
const sorted_consts_nodes: ConstNode[] = []; const sorted_consts_nodes: ConstNode[] = [];
const unsorted_consts_nodes: ConstNode[] = consts_nodes.map(node => { const unsorted_consts_nodes: ConstNode[] = consts_nodes.map(node => {
return { return {
assignees: node.assignees, assignees: node.assignees,
dependencies: node.dependencies, dependencies: node.dependencies,
node node
}; };
}); });
const lookup = new Map(); const lookup = new Map();
unsorted_consts_nodes.forEach(node => { unsorted_consts_nodes.forEach(node => {
node.assignees.forEach(name => { node.assignees.forEach(name => {
if (!lookup.has(name)) { if (!lookup.has(name)) {
lookup.set(name, []); lookup.set(name, []);
} }
lookup.get(name).push(node); lookup.get(name).push(node);
}); });
}); });
const cycle = check_graph_for_cycles(unsorted_consts_nodes.reduce((acc, node) => { const cycle = check_graph_for_cycles(unsorted_consts_nodes.reduce((acc, node) => {
node.assignees.forEach(v => { node.assignees.forEach(v => {
node.dependencies.forEach(w => { node.dependencies.forEach(w => {
if (!node.assignees.has(w)) { if (!node.assignees.has(w)) {
acc.push([v, w]); acc.push([v, w]);
} }
}); });
}); });
return acc; return acc;
}, [])); }, []));
if (cycle && cycle.length) { if (cycle?.length) {
const nodeList = lookup.get(cycle[0]); const nodeList = lookup.get(cycle[0]);
const node = nodeList[0]; const node = nodeList[0];
component.error(node.node, compiler_errors.cyclical_const_tags(cycle)); component.error(node.node, compiler_errors.cyclical_const_tags(cycle));
} }
const add_node = (node: ConstNode) => { const add_node = (node: ConstNode) => {
if (sorted_consts_nodes.includes(node)) return; if (sorted_consts_nodes.includes(node)) return;
node.dependencies.forEach(name => { node.dependencies.forEach(name => {
if (node.assignees.has(name)) return; if (node.assignees.has(name)) return;
const earlier_nodes = lookup.get(name); const earlier_nodes = lookup.get(name);
if (earlier_nodes) { if (earlier_nodes) {
earlier_nodes.forEach(add_node); earlier_nodes.forEach(add_node);
} }
}); });
sorted_consts_nodes.push(node); sorted_consts_nodes.push(node);
}; };
unsorted_consts_nodes.forEach(add_node); unsorted_consts_nodes.forEach(add_node);
return sorted_consts_nodes.map(node => node.node); return sorted_consts_nodes.map(node => node.node);
} }

@ -3,10 +3,11 @@ import TemplateScope from './TemplateScope';
import { is_reserved_keyword } from '../../utils/reserved_keywords'; import { is_reserved_keyword } from '../../utils/reserved_keywords';
export default function is_contextual(component: Component, scope: TemplateScope, name: string) { export default function is_contextual(component: Component, scope: TemplateScope, name: string) {
if (is_reserved_keyword(name)) return true; if (is_reserved_keyword(name) ||
// if it's a name below root scope, it's contextual
!scope.is_top_level(name)
) return true;
// if it's a name below root scope, it's contextual
if (!scope.is_top_level(name)) return true;
const variable = component.var_lookup.get(name); const variable = component.var_lookup.get(name);

Loading…
Cancel
Save