hoist-unmodified-var
Ben McCann 8 months ago
parent 0bd0c4e77e
commit b0900386f1

@ -1,5 +1,5 @@
import * as b from '../../../utils/builders.js';
import { extract_paths, is_simple_expression } from '../../../utils/ast.js';
import { extract_paths, is_simple_expression, unwrap_ts_expression } from '../../../utils/ast.js';
import { error } from '../../../errors.js';
import {
PROPS_IS_LAZY_INITIAL,
@ -7,6 +7,7 @@ import {
PROPS_IS_RUNES,
PROPS_IS_UPDATED
} from '../../../../constants.js';
import { get_rune } from '../../scope';
/**
* @template {import('./types').ClientTransformState} State
@ -535,18 +536,19 @@ export function should_proxy_or_freeze(node) {
/**
* @param {import('#compiler').Binding | undefined} binding
* @param {import('#compiler').SvelteNode[]} path
* @param {string} name
* @returns {boolean}
*/
export function is_hoistable_declaration(binding, path) {
const is_top_level = path.at(-1)?.type === 'Program';
export function is_hoistable_declaration(binding, name) {
// TODO: allow object expressions that are not passed to functions or components as props
// and expressions as long as they do not reference non-hoistable variables
return (
is_top_level &&
!!binding &&
binding.kind === 'normal' &&
binding.scope.is_top_level &&
!binding.mutated &&
!binding.reassigned &&
binding?.initial?.type === 'Literal'
binding.initial?.type === 'Literal' &&
!binding.scope.declared_in_outer_scope(name)
);
}

@ -1,7 +1,12 @@
import { is_hoistable_function } from '../../utils.js';
import * as b from '../../../../utils/builders.js';
import { extract_paths } from '../../../../utils/ast.js';
import { create_state_declarators, get_prop_source, serialize_get_binding } from '../utils.js';
import {
create_state_declarators,
get_prop_source,
is_hoistable_declaration,
serialize_get_binding
} from '../utils.js';
/** @type {import('../types.js').ComponentVisitors} */
export const javascript_visitors_legacy = {
@ -18,6 +23,19 @@ export const javascript_visitors_legacy = {
if (!has_state && !has_props) {
const init = declarator.init;
if (init != null && declarator.id.type === 'Identifier') {
const binding = state.scope
.owner(declarator.id.name)
?.declarations.get(declarator.id.name);
if (is_hoistable_declaration(binding, declarator.id.name)) {
state.scope.root.unique;
state.hoisted.push(b.declaration('const', declarator.id, init));
continue;
}
}
if (init != null && is_hoistable_function(init)) {
const hoistable_function = visit(init);
state.hoisted.push(

@ -156,21 +156,22 @@ export const javascript_visitors_runes = {
return { ...node, body };
},
VariableDeclaration(node, { path, state, visit }) {
VariableDeclaration(node, { state, visit }) {
const declarations = [];
for (const declarator of node.declarations) {
const init = unwrap_ts_expression(declarator.init);
const rune = get_rune(init, state.scope);
if (!rune && init != null && declarator.id.type === 'Identifier') {
if (init != null && declarator.id.type === 'Identifier') {
const binding = state.scope.owner(declarator.id.name)?.declarations.get(declarator.id.name);
if (is_hoistable_declaration(binding, path)) {
if (is_hoistable_declaration(binding, declarator.id.name)) {
state.scope.root.unique;
state.hoisted.push(b.declaration('const', declarator.id, init));
continue;
}
}
const rune = get_rune(init, state.scope);
if (!rune || rune === '$effect.active' || rune === '$effect.root' || rune === '$inspect') {
if (init != null && is_hoistable_function(init)) {
const hoistable_function = visit(init);

@ -501,8 +501,6 @@ function serialize_element_attribute_update_assignment(element, node_id, attribu
}
let grouped_value = value;
/** @type {import('estree').Expression | null} */
let constant_expression = grouped_value;
if (name === 'autofocus') {
state.init.push(b.stmt(b.call('$.auto_focus', node_id, value)));
@ -511,7 +509,6 @@ function serialize_element_attribute_update_assignment(element, node_id, attribu
if (name === 'class') {
grouped_value = b.call('$.to_class', value);
constant_expression = null;
}
/**
@ -567,18 +564,19 @@ function serialize_element_attribute_update_assignment(element, node_id, attribu
}
};
let is_in_hoistable = false;
let hoistable = false;
if (Array.isArray(attribute.value)) {
for (let value of attribute.value) {
if (value.type === 'ExpressionTag' && value.expression.type === 'Identifier') {
const binding = context.state.scope
.owner(value.expression.name)
?.declarations.get(value.expression.name);
is_in_hoistable ||= is_hoistable_declaration(binding, context.path);
hoistable ||= is_hoistable_declaration(binding, value.expression.name);
}
}
}
if (attribute.metadata.dynamic && !is_in_hoistable) {
if (attribute.metadata.dynamic && !hoistable) {
const id = state.scope.generate(`${node_id.name}_${name}`);
serialize_update_assignment(
state,
@ -590,12 +588,12 @@ function serialize_element_attribute_update_assignment(element, node_id, attribu
);
return true;
} else {
if (!is_in_hoistable || DOMBooleanAttributes.includes(name)) {
state.init.push(assign(grouped_value).grouped);
} else if (constant_expression) {
if (hoistable) {
push_template_quasi(context.state, ` ${name}="`);
push_template_expression(context.state, grouped_value);
push_template_quasi(context.state, `"`);
} else {
state.init.push(assign(grouped_value).grouped);
}
return false;
}

@ -70,6 +70,76 @@ export const ElementBindings = [
'indeterminate'
];
export const GlobalBindings = new Set([
'Map',
'Set',
'Array',
'Uint8Array',
'Uint8ClampedArray',
'Int16Array',
'Uint16Array',
'Int32Array',
'Uint32Array',
'BigInt64Array',
'BigUint64Array',
'Float32Array',
'Float64Array',
'Int8Array',
'Function',
'Error',
'AggregateError',
'EvalError',
'RangeError',
'ReferenceError',
'SyntaxError',
'TypeError',
'URIError',
'InternalError',
'Number',
'Math',
'BigInt',
'String',
'RegEx',
'Date',
'Boolean',
'Symbol',
'Object',
'ArrayBuffer',
'SharedArrayBuffer',
'DataView',
'Atomics',
'JSON',
'WeakRef',
'FinalizationRegistry',
'Iterator',
'AsyncIterator',
'Promise',
'GeneratorFunction',
'AsyncGeneratorFunction',
'Generator',
'AsyncGenerator',
'AsyncFunction',
'Reflect',
'Proxy',
'Intl',
'eval',
'isFinite',
'isNaN',
'parseFloat',
'parseInt',
'decodeURI',
'decodeURIComponent',
'encodeURI',
'encodeURIComponent',
'console',
'undefined',
'NaN',
'Infinity',
'globalThis',
'window',
'document'
]);
export const Runes = /** @type {const} */ ([
'$state',
'$state.frozen',

@ -4,12 +4,19 @@ import { is_element_node } from './nodes.js';
import * as b from '../utils/builders.js';
import { error } from '../errors.js';
import { extract_identifiers, extract_identifiers_from_expression } from '../utils/ast.js';
import { JsKeywords, Runes } from './constants.js';
import { GlobalBindings, JsKeywords, Runes } from './constants.js';
export class Scope {
/** @type {ScopeRoot} */
root;
/**
* Whether this is a top-level script scope.
* I.e. `<script>`, `<script context=module>`, or `.svelte.js`.
* @type {boolean}
*/
is_top_level;
/**
* The immediate parent scope
* @type {Scope | null}
@ -49,16 +56,17 @@ export class Scope {
function_depth = 0;
/**
*
* @param {ScopeRoot} root
* @param {Scope | null} parent
* @param {boolean} porous
* @param {boolean} is_top_level
*/
constructor(root, parent, porous) {
constructor(root, parent, porous, is_top_level) {
this.root = root;
this.#parent = parent;
this.#porous = porous;
this.function_depth = parent ? parent.function_depth + (porous ? 0 : 1) : 0;
this.is_top_level = is_top_level;
}
/**
@ -119,8 +127,23 @@ export class Scope {
return binding;
}
/**
* @param {string} name
* @returns {boolean}
*/
declared_in_outer_scope(name) {
let outer = this.#parent;
while (outer !== null) {
if (outer.references.has(name) || outer.declarations.has(name)) {
return true;
}
outer = outer.#parent;
}
return GlobalBindings.has(name) || JsKeywords.includes(name);
}
child(porous = false) {
return new Scope(this.root, this, porous);
return new Scope(this.root, this, porous, false);
}
/**
@ -238,7 +261,7 @@ export function create_scopes(ast, root, allow_reactive_declarations, parent) {
* @type {Map<import('#compiler').SvelteNode, Scope>}
*/
const scopes = new Map();
const scope = new Scope(root, parent, false);
const scope = new Scope(root, parent, false, ast.type === 'Program');
scopes.set(ast, scope);
/** @type {State} */

Loading…
Cancel
Save