add more globals, template literals, try functions and (some) member expressions

pull/15781/head
ComputerGuy 1 year ago
parent ed6c208405
commit ed7f90ca26

@ -685,7 +685,7 @@ function build_element_special_value_attribute(element, node_id, attribute, cont
: value
);
const evaluated = context.state.scope.evaluate(value);
const evaluated = context.state.scope.evaluate(value, new Set(), context.state.scopes);
const assignment = b.assignment('=', b.member(node_id, '__value'), value);
const inner_assignment = b.assignment(

@ -64,12 +64,9 @@ export function build_template_chunk(
node.expression.name !== 'undefined' ||
state.scope.get('undefined')
) {
let value = memoize(
/** @type {Expression} */ (visit(node.expression, state)),
node.metadata.expression
);
let value = /** @type {Expression} */ (visit(node.expression, state));
const evaluated = state.scope.evaluate(value);
const evaluated = state.scope.evaluate(value, new Set(), state.scopes);
has_state ||= node.metadata.expression.has_state && !evaluated.is_known;
@ -98,6 +95,7 @@ export function build_template_chunk(
if (evaluated.is_known) {
quasi.value.cooked += evaluated.value + '';
} else {
value = memoize(value, node.metadata.expression);
if (!evaluated.is_defined) {
// add `?? ''` where necessary
value = b.logical('??', value, b.literal(''));

@ -45,7 +45,7 @@ export function process_children(nodes, { visit, state }) {
quasi.value.cooked +=
node.type === 'Comment' ? `<!--${node.data}-->` : escape_html(node.data);
} else {
const evaluated = state.scope.evaluate(node.expression);
const evaluated = state.scope.evaluate(node.expression, new Set(), state.scopes);
if (evaluated.is_known) {
quasi.value.cooked += escape_html((evaluated.value ?? '') + '');

@ -1,4 +1,4 @@
/** @import { ArrowFunctionExpression, BinaryOperator, ClassDeclaration, Expression, FunctionDeclaration, FunctionExpression, Identifier, ImportDeclaration, MemberExpression, LogicalOperator, Node, Pattern, UnaryOperator, VariableDeclarator } from 'estree' */
/** @import { ArrowFunctionExpression, BinaryOperator, ClassDeclaration, Expression, FunctionDeclaration, FunctionExpression, Identifier, ImportDeclaration, MemberExpression, LogicalOperator, Node, Pattern, UnaryOperator, VariableDeclarator, Super, Function, Program, PrivateIdentifier } from 'estree' */
/** @import { Context, Visitor } from 'zimmerframe' */
/** @import { AST, BindingKind, DeclarationKind } from '#compiler' */
import is_reference from 'is-reference';
@ -20,7 +20,124 @@ export const UNKNOWN = Symbol('unknown');
/** Includes `BigInt` */
export const NUMBER = Symbol('number');
export const STRING = Symbol('string');
/** Used for when you need to add `true` and `false` to the values, but can't do it for whatever reason */
const BOOLEAN = Symbol('boolean');
const TYPES = [UNKNOWN, NUMBER, STRING];
const global_calls = Object.freeze({
String: ['fromCharCode', 'fromCodepoint'],
Math: [
'min',
'max',
'random',
'floor',
'f16round',
'round',
'abs',
'acos',
'asin',
'atan',
'atan2',
'ceil',
'cos',
'sin',
'tan',
'exp',
'log',
'pow',
'sqrt',
'clz32',
'imul',
'sign',
'log10',
'log2',
'log1p',
'expm1',
'cosh',
'sinh',
'tanh',
'acosh',
'asinh',
'atanh',
'trunc',
'fround',
'cbrt'
],
Number: ['isInteger', 'isFinite', 'isNaN', 'isSafeInteger', 'parseFloat', 'parseInt']
});
const math_constants = Object.freeze([
'PI',
'E',
'LN10',
'LN2',
'LOG10E',
'LOG2E',
'SQRT_2',
'SQRT1_2'
]);
/**
* @param {Expression | Super} callee
* @param {Scope} scope
* @returns {null | [keyof typeof global_calls, (typeof global_calls)[keyof typeof global_calls][number]] | [string]}
*/
function is_global_call(callee, scope) {
if (
callee.type === 'MemberExpression' &&
callee.property?.type !== 'PrivateIdentifier' &&
!callee.computed
) {
const root = object(callee);
if (root?.type !== 'Identifier') {
return null;
}
if (scope.get(root.name)) {
return null;
}
if (callee.object === root && root.name === 'globalThis') {
return is_global_call(callee.property, scope);
}
/** @type {keyof typeof global_calls | undefined} */
let root_name;
if (
callee.object.type === 'MemberExpression' &&
callee.object.property.type === 'Identifier' &&
!callee.object.computed
) {
if (
callee.object.object.type === 'Identifier' &&
callee.object.object.name === 'globalThis'
) {
root_name = /** @type {keyof typeof global_calls} */ (callee.object.property?.name);
} else if (callee.object.object === root) {
root_name = /** @type {keyof typeof global_calls} */ (root.name);
}
} else {
//@ts-ignore
root_name = /** @type {Identifier} */ (callee.object).name;
}
console.log(root_name);
if (root_name === undefined) return null;
if (!(root_name in global_calls)) {
return null;
}
const valid_members = global_calls[root_name];
const property = /** @type {Identifier} */ (callee.property).name;
console.log(property, valid_members, valid_members.includes(property));
if (!valid_members.includes(property)) {
return null;
}
return [root_name, property];
} else if (callee.type === 'Identifier') {
return callee.name === 'Number' || callee.name === 'BigInt' || callee.name === 'String'
? [callee.name]
: null;
}
return null;
}
export class Binding {
/** @type {Scope} */
scope;
@ -148,8 +265,9 @@ class Evaluation {
* @param {Scope} scope
* @param {Expression} expression
* @param {Set<any>} values
* @param {Map<AST.SvelteNode, Scope>} scopes
*/
constructor(scope, expression, values = new Set()) {
constructor(scope, expression, values, scopes) {
this.values = values;
switch (expression.type) {
@ -181,7 +299,11 @@ class Evaluation {
}
if (!binding.updated && binding.initial !== null && !is_prop) {
binding.scope.evaluate(/** @type {Expression} */ (binding.initial), this.values);
binding.scope.evaluate(
/** @type {Expression} */ (binding.initial),
this.values,
scopes
);
break;
}
} else if (expression.name === 'undefined') {
@ -197,8 +319,8 @@ class Evaluation {
}
case 'BinaryExpression': {
const a = scope.evaluate(/** @type {Expression} */ (expression.left)); // `left` cannot be `PrivateIdentifier` unless operator is `in`
const b = scope.evaluate(expression.right);
const a = scope.evaluate(/** @type {Expression} */ (expression.left), new Set(), scopes); // `left` cannot be `PrivateIdentifier` unless operator is `in`
const b = scope.evaluate(expression.right, new Set(), scopes);
if (a.is_known && b.is_known) {
this.values.add(binary[expression.operator](a.value, b.value));
@ -252,9 +374,9 @@ class Evaluation {
}
case 'ConditionalExpression': {
const test = scope.evaluate(expression.test);
const consequent = scope.evaluate(expression.consequent);
const alternate = scope.evaluate(expression.alternate);
const test = scope.evaluate(expression.test, new Set(), scopes);
const consequent = scope.evaluate(expression.consequent, new Set(), scopes);
const alternate = scope.evaluate(expression.alternate, new Set(), scopes);
if (test.is_known) {
for (const value of (test.value ? consequent : alternate).values) {
@ -273,8 +395,8 @@ class Evaluation {
}
case 'LogicalExpression': {
const a = scope.evaluate(expression.left);
const b = scope.evaluate(expression.right);
const a = scope.evaluate(expression.left, new Set(), scopes);
const b = scope.evaluate(expression.right, new Set(), scopes);
if (a.is_known) {
if (b.is_known) {
@ -308,7 +430,7 @@ class Evaluation {
}
case 'UnaryExpression': {
const argument = scope.evaluate(expression.argument);
const argument = scope.evaluate(expression.argument, new Set(), scopes);
if (argument.is_known) {
this.values.add(unary[expression.operator](argument.value));
@ -353,7 +475,7 @@ class Evaluation {
case '$state.raw':
case '$derived':
if (arg) {
scope.evaluate(arg, this.values);
scope.evaluate(arg, this.values, scopes);
} else {
this.values.add(undefined);
}
@ -370,7 +492,7 @@ class Evaluation {
case '$derived.by':
if (arg?.type === 'ArrowFunctionExpression' && arg.body.type !== 'BlockStatement') {
scope.evaluate(arg.body, this.values);
scope.evaluate(arg.body, this.values, scopes);
break;
}
@ -385,11 +507,12 @@ class Evaluation {
break;
}
if (expression.callee.type === 'Identifier' && scope.get(expression.callee.name) === null) {
switch (expression.callee.name) {
if (is_global_call(expression.callee, scope)?.length === 1) {
const [call] = /** @type {[string]} */ (is_global_call(expression.callee, scope));
switch (call) {
case 'Number': {
const arg = /** @type {Expression | undefined} */ (expression.arguments[0]);
const e = arg && scope.evaluate(arg);
const e = arg && scope.evaluate(arg, new Set(), scopes);
this.values.add(e ? (e.is_known ? Number(e.value) : NUMBER) : 0);
break;
@ -397,19 +520,203 @@ class Evaluation {
case 'String': {
const arg = /** @type {Expression | undefined} */ (expression.arguments[0]);
const e = arg && scope.evaluate(arg);
const e = arg && scope.evaluate(arg, new Set(), scopes);
this.values.add(e ? (e.is_known ? String(e.value) : STRING) : 0);
this.values.add(e ? (e.is_known ? String(e.value) : STRING) : '');
break;
}
case 'BigInt': {
const arg = /** @type {Expression | undefined} */ (expression.arguments[0]);
const e = arg && scope.evaluate(arg, new Set(), scopes);
this.values.add(e ? (e.is_known ? BigInt(e.value) : NUMBER) : 0n);
break;
}
default:
this.values.add(UNKNOWN);
}
break;
} else if (
expression.callee.type === 'MemberExpression' &&
is_global_call(expression.callee, scope)?.length === 2
) {
const [object, property] = /** @type {[keyof typeof global_calls, string]} */ (
is_global_call(expression.callee, scope)
);
switch (object) {
case 'Math': {
const args = /** @type {Expression[]} */ (expression.arguments).map((arg) =>
scope.evaluate(arg, new Set(), scopes)
);
const all_are_known = args.every((evaluated) => evaluated.is_known);
const vals = all_are_known
? //@ts-expect-error
/** @type {() => unknown} */ (Math[property])(...args.map((arg) => arg.value))
: NUMBER;
this.values.add(vals);
break;
}
case 'Number': {
const args = /** @type {Expression[]} */ (expression.arguments).map((arg) =>
scope.evaluate(arg, new Set(), scopes)
);
const all_are_known = args.every((evaluated) => evaluated.is_known);
const vals = all_are_known
? Number[
/** @type {'isSafeInteger' | 'isFinite' | 'isNaN' | 'parseFloat' | 'parseInt' | 'isInteger'} */ (
property
)
//@ts-expect-error types are a monster here
](...args.map((arg) => arg.value))
: property.startsWith('is')
? BOOLEAN
: NUMBER;
if (vals === BOOLEAN) {
this.values.add(true);
this.values.add(false);
} else {
this.values.add(vals);
}
break;
}
case 'String': {
const args = /** @type {Expression[]} */ (expression.arguments).map((arg) =>
scope.evaluate(arg, new Set(), scopes)
);
const all_are_known = args.every((evaluated) => evaluated.is_known);
this.values.add(
all_are_known
? String[/** @type {'fromCharCode' | 'fromCodePoint'} */ (property)](
...args.map((arg) => arg.value)
)
: STRING
);
break;
}
}
break;
} else if (expression.callee.type === 'Identifier' && !expression.arguments.length) {
const binding = scope.get(expression.callee.name);
if (binding) {
if (
binding.kind === 'normal' &&
!binding.reassigned &&
(binding.declaration_kind === 'function' ||
binding.declaration_kind === 'const' ||
binding.declaration_kind === 'let' ||
binding.declaration_kind === 'var')
) {
const fn =
/** @type {FunctionExpression | FunctionDeclaration | ArrowFunctionExpression} */ (
binding.initial
);
if (fn && fn.async === false && !fn?.generator) {
const fn_analysis = evaluate_function(fn, binding, scopes, new Set());
if (fn_analysis.pure && fn_analysis.never_throws) {
for (let value of fn_analysis.values) {
this.values.add(value);
}
break;
}
}
}
}
}
this.values.add(UNKNOWN);
break;
}
case 'TemplateLiteral': {
const expressions = expression.expressions.map((expr) =>
scope.evaluate(expr, new Set(), scopes)
);
const all_are_known = expressions.every((evaluated) => evaluated.is_known);
if (all_are_known) {
let res = '';
let quasi_index = 0;
let expr_index = 0;
let last_quasi = false;
for (let i = 0; i < expressions.length + expression.quasis.length; i++) {
if (last_quasi) {
const expression = expressions[expr_index++];
res += expression.value;
} else {
res += expression.quasis[quasi_index++].value.raw;
}
last_quasi = !last_quasi;
}
this.values.add(res);
} else {
this.values.add(UNKNOWN);
this.values.add(STRING);
}
break;
}
case 'MemberExpression': {
if (
expression.object.type !== 'Identifier' &&
expression.object.type !== 'MemberExpression'
) {
this.values.add(UNKNOWN);
break;
}
const { object, property, computed } = expression;
if (
object.type === 'MemberExpression' &&
object.object.type === 'Identifier' &&
object.object.name === 'globalThis' &&
!scope.get('globalThis')
) {
if (object.computed && object.property.type !== 'PrivateIdentifier') {
const key = scope.evaluate(object.property, new Set(), scopes);
if (key.is_known && key.value === 'Math') {
if (computed && property.type !== 'PrivateIdentifier') {
const math_prop = scope.evaluate(property, new Set(), scopes);
if (math_prop.is_known && math_constants.includes(math_prop.value)) {
this.values.add(Math[/** @type {keyof typeof Math} */ (math_prop.value)]);
break;
}
} else if (property.type === 'Identifier') {
if (math_constants.includes(property.name)) {
this.values.add(Math[/** @type {keyof typeof Math} */ (property.name)]);
break;
}
}
}
} else if (object.property.type === 'Identifier' && object.property.name === 'Math') {
if (computed && property.type !== 'PrivateIdentifier') {
const math_prop = scope.evaluate(property, new Set(), scopes);
if (math_prop.is_known && math_constants.includes(math_prop.value)) {
this.values.add(Math[/** @type {keyof typeof Math} */ (math_prop.value)]);
break;
}
} else if (property.type === 'Identifier') {
if (math_constants.includes(property.name)) {
this.values.add(Math[/** @type {keyof typeof Math} */ (property.name)]);
break;
}
}
} else {
this.values.add(UNKNOWN);
break;
}
} else if (object.type === 'Identifier' && object.name === 'Math' && !scope.get('Math')) {
if (computed && property.type !== 'PrivateIdentifier') {
const math_prop = scope.evaluate(property, new Set(), scopes);
if (math_prop.is_known && math_constants.includes(math_prop.value)) {
this.values.add(Math[/** @type {keyof typeof Math} */ (math_prop.value)]);
break;
}
} else if (property.type === 'Identifier') {
if (math_constants.includes(property.name)) {
this.values.add(Math[/** @type {keyof typeof Math} */ (property.name)]);
break;
}
}
}
this.values.add(UNKNOWN);
break;
}
@ -440,6 +747,223 @@ class Evaluation {
}
}
// TODO add more
const global_classes = [
'String',
'BigInt',
'Object',
'Set',
'Array',
'Proxy',
'Map',
'Boolean',
'WeakMap',
'WeakRef',
'WeakSet',
'Number',
'RegExp',
'Error',
'Date'
];
// TODO ditto
const known_globals = [
...global_classes,
'Symbol',
'console',
'Math',
'isNaN',
'isFinite',
'setTimeout',
'setInterval',
'NaN',
'undefined'
];
/**
* @param {FunctionExpression | ArrowFunctionExpression | FunctionDeclaration} fn
* @param {Binding} binding
* @param {Map<AST.SvelteNode, Scope>} scopes
* @param {Set<Binding>} [stack]
*/
function evaluate_function(fn, binding, scopes, stack = new Set()) {
const analysis = {
pure: true,
is_known: false,
is_defined: false,
values: new Set(),
/** @type {any} */
value: undefined,
never_throws: true
};
/** @type {Set<Scope>} */
const walked_scopes = new Set(/** @type {Scope[]} */ ([binding.scope]));
const fn_binding = binding;
walk(
/** @type {AST.SvelteNode} */ (fn),
{ scope: binding.scope },
{
MemberExpression(node, context) {
if (
!context.state.scope.evaluate(node, new Set(), scopes).is_known &&
!is_global_call(node, context.state.scope)
) {
analysis.pure = false;
analysis.never_throws = false; // getters/proxies could throw
}
context.next();
},
Identifier(node, context) {
if (is_reference(node, /** @type {Node} */ (context.path.at(-1)))) {
const binding = context.state.scope.get(node.name);
if (binding !== fn_binding) {
if (binding === null) {
if (known_globals.includes(node.name)) {
context.next();
return;
}
analysis.pure = false;
} else if (!walked_scopes.has(binding?.scope)) {
const evaluated = context.state.scope.evaluate(node, new Set(), scopes);
if (!evaluated.is_known) {
analysis.pure = false;
}
}
}
}
context.next();
},
CallExpression(node, context) {
if (node.callee.type === 'Identifier' && !node.arguments.length) {
const binding = context.state.scope.get(node.callee.name);
if (
binding &&
fn_binding !== binding &&
binding.kind === 'normal' &&
!binding.reassigned &&
(binding.declaration_kind === 'function' ||
binding.declaration_kind === 'const' ||
binding.declaration_kind === 'let' ||
binding.declaration_kind === 'var')
) {
if (!stack.has(binding)) {
const fn_analysis = evaluate_function(
/** @type {FunctionExpression | ArrowFunctionExpression | FunctionDeclaration} */ (
binding.initial
),
binding,
scopes,
new Set([...stack])
);
analysis.pure &&= fn_analysis.pure;
analysis.never_throws &&= fn_analysis.never_throws;
}
} else if (!(node.callee.name in global_calls)) {
analysis.pure = false;
}
} else if (!is_global_call(node.callee, context.state.scope)) {
analysis.pure = false;
console.log(node.callee);
}
context.next();
},
NewExpression(node, context) {
const { callee } = node;
if (callee.type === 'Identifier') {
if (context.state.scope.get(callee.name) || !global_classes.includes(callee.name)) {
analysis.pure = false;
}
}
context.next();
},
AssignmentExpression(node, context) {
const { left } = node;
const assigned_to = unwrap_pattern(/** @type {Pattern} */ (context.visit(left)));
if (assigned_to.some((assigned) => assigned.type === 'MemberExpression')) {
analysis.pure = false;
analysis.never_throws = false;
} else if (
assigned_to.some((assigned) => {
const binding = context.state.scope.get(/** @type {Identifier} */ (assigned).name);
if (binding) {
return !walked_scopes.has(binding.scope);
} else {
return true;
}
})
) {
analysis.pure = false;
}
context.next();
},
UpdateExpression(node, context) {
analysis.pure = false;
analysis.never_throws = false;
context.next();
},
ReturnStatement(node, context) {
if (
context.path.findLast(
(parent) =>
parent.type === 'FunctionDeclaration' ||
parent.type === 'FunctionExpression' ||
parent.type === 'ArrowFunctionExpression'
) === fn
) {
const return_values = node.argument
? context.state.scope.evaluate(
/** @type {Expression} */ (context.visit(node.argument)),
new Set(),
scopes
)
: undefined;
if (return_values === undefined) {
analysis.values.add(undefined);
} else {
for (let value of return_values.values) {
analysis.values.add(value);
}
}
}
},
ThrowStatement(node, context) {
if (
context.path.findLast(
(parent) =>
parent.type === 'FunctionDeclaration' ||
parent.type === 'FunctionExpression' ||
parent.type === 'ArrowFunctionExpression'
) === fn
) {
analysis.never_throws = false;
}
context.next();
},
_(node, context) {
const scope = scopes.get(node);
if (scope && node.type !== 'FunctionDeclaration') {
walked_scopes.add(scope);
context.next({ scope });
} else if (node.type !== 'FunctionDeclaration' || node === fn) {
context.next(context.state);
}
}
}
);
for (const value of analysis.values) {
analysis.value = value; // saves having special logic for `size === 1`
if (value == null || value === UNKNOWN) {
analysis.is_defined = false;
}
}
if (analysis.values.size <= 1 && !TYPES.includes(analysis.value)) {
analysis.is_known = true;
}
console.log(analysis);
return analysis;
}
export class Scope {
/** @type {ScopeRoot} */
root;
@ -625,10 +1149,11 @@ export class Scope {
* Only call this once scope has been fully generated in a first pass,
* else this evaluates on incomplete data and may yield wrong results.
* @param {Expression} expression
* @param {Set<any>} [values]
* @param {Set<any>} values
* @param {Map<AST.SvelteNode, Scope>} scopes
*/
evaluate(expression, values) {
return new Evaluation(this, expression, values);
evaluate(expression, values, scopes) {
return new Evaluation(this, expression, values, scopes);
}
}

Loading…
Cancel
Save