remove unused stuff

pull/15820/head
Rich Harris 4 months ago
parent 737bfb5740
commit c536ec6f2b

@ -1,95 +0,0 @@
/** @import { Context } from '../../types.js' */
/** @import { MethodDefinition, PropertyDefinition, Expression, StaticBlock, SpreadElement } from 'estree' */
/** @import { StateCreationRuneName } from '../../../../../../utils.js' */
/** @import { AssignmentBuilder, ClassTransformer, StateFieldBuilder } from '../../../shared/types.js' */
import * as b from '#compiler/builders';
import { create_class_transformer } from '../../../shared/class_transformer.js';
import { should_proxy } from '../../utils.js';
/**
* @param {Array<MethodDefinition | PropertyDefinition | StaticBlock>} body
* @returns {ClassTransformer<Context>}
*/
export function create_client_class_transformer(body) {
/** @type {StateFieldBuilder<Context>} */
function build_state_field({ is_private, field, node, context }) {
let original_id = node.type === 'AssignmentExpression' ? node.left.property : node.key;
let value;
if (node.type === 'AssignmentExpression') {
// if there's no call expression, this is state that's created in the constructor.
// it's guaranteed to be the very first assignment to this field, so we initialize
// the field but don't assign to it.
value = null;
} else if (node.value.arguments.length > 0) {
value = build_init_value(field.kind, node.value.arguments[0], context);
} else {
// if no arguments, we know it's state as `$derived()` is a compile error
value = b.call('$.state');
}
if (is_private) {
return [b.prop_def(field.id, value)];
}
const member = b.member(b.this, field.id);
const val = b.id('value');
return [
// #foo;
b.prop_def(field.id, value),
// get foo() { return this.#foo; }
b.method('get', original_id, [], [b.return(b.call('$.get', member))]),
// set foo(value) { this.#foo = value; }
b.method(
'set',
original_id,
[val],
[b.stmt(b.call('$.set', member, val, field.kind === '$state' && b.true))]
)
];
}
/** @type {AssignmentBuilder<Context>} */
function build_assignment({ field, node, context }) {
return {
...node,
left: {
...node.left,
// ...swap out the assignment to go directly against the private field
property: field.id,
// this could be a transformation from `this.[1]` to `this.#_` (the private field we generated)
// -- private fields are never computed
computed: false
},
// ...and swap out the assignment's value for the state field init
right: build_init_value(field.kind, node.right.arguments[0], context)
};
}
return create_class_transformer(body, build_state_field, build_assignment);
}
/**
* @param {StateCreationRuneName} kind
* @param {Expression | SpreadElement} arg
* @param {Context} context
*/
function build_init_value(kind, arg, context) {
const init = arg
? /** @type {Expression} **/ (context.visit(arg, { ...context.state, in_constructor: false }))
: b.void0;
switch (kind) {
case '$state':
return b.call(
'$.state',
should_proxy(init, context.state.scope) ? b.call('$.proxy', init) : init
);
case '$state.raw':
return b.call('$.state', init);
case '$derived':
return b.call('$.derived', b.thunk(init));
case '$derived.by':
return b.call('$.derived', init);
}
}

@ -1,103 +0,0 @@
/** @import { Expression, MethodDefinition, StaticBlock, PropertyDefinition, SpreadElement } from 'estree' */
/** @import { Context } from '../../types.js' */
/** @import { AssignmentBuilder, StateFieldBuilder } from '../../../shared/types.js' */
/** @import { ClassTransformer } from '../../../shared/types.js' */
/** @import { StateCreationRuneName } from '../../../../../../utils.js' */
import * as b from '#compiler/builders';
import { dev } from '../../../../../state.js';
import { create_class_transformer } from '../../../shared/class_transformer.js';
/**
* @param {Array<MethodDefinition | PropertyDefinition | StaticBlock>} body
* @returns {ClassTransformer<Context>}
*/
export function create_server_class_transformer(body) {
/** @type {StateFieldBuilder<Context>} */
function build_state_field({ is_private, field, node, context }) {
let original_id = node.type === 'AssignmentExpression' ? node.left.property : node.key;
let value;
if (node.type === 'AssignmentExpression') {
// This means it's a state assignment in the constructor (this.foo = $state('bar'))
// which means the state field needs to have no default value so that the initial
// value can be assigned in the constructor.
value = null;
} else if (field.kind !== '$derived' && field.kind !== '$derived.by') {
return [/** @type {PropertyDefinition} */ (context.visit(node))];
} else {
const init = /** @type {Expression} **/ (context.visit(node.value.arguments[0]));
value =
field.kind === '$derived.by' ? b.call('$.once', init) : b.call('$.once', b.thunk(init));
}
if (is_private) {
return [b.prop_def(field.id, value)];
}
// #foo;
const member = b.member(b.this, field.id);
/** @type {Array<MethodDefinition | PropertyDefinition>} */
const defs = [
// #foo;
b.prop_def(field.id, value)
];
// get foo() { return this.#foo; }
if (field.kind === '$state' || field.kind === '$state.raw') {
defs.push(b.method('get', original_id, [], [b.return(member)]));
} else {
defs.push(b.method('get', original_id, [], [b.return(b.call(member))]));
}
// TODO make this work on server
if (dev) {
defs.push(
b.method(
'set',
original_id,
[b.id('_')],
[b.throw_error(`Cannot update a derived property ('${name}')`)]
)
);
}
return defs;
}
/** @type {AssignmentBuilder<Context>} */
function build_assignment({ field, node, context }) {
return {
...node,
left: {
...node.left,
// ...swap out the assignment to go directly against the private field
property: field.id,
computed: false
},
// ...and swap out the assignment's value for the state field init
right: build_init_value(field.kind, node.right.arguments[0], context)
};
}
return create_class_transformer(body, build_state_field, build_assignment);
}
/**
*
* @param {StateCreationRuneName} kind
* @param {Expression | SpreadElement} arg
* @param {Context} context
*/
function build_init_value(kind, arg, context) {
const init = arg ? /** @type {Expression} **/ (context.visit(arg)) : b.void0;
switch (kind) {
case '$state':
case '$state.raw':
return init;
case '$derived':
return b.call('$.once', b.thunk(init));
case '$derived.by':
return b.call('$.once', init);
}
}

@ -1,392 +0,0 @@
/** @import { AssignmentExpression, Identifier, Literal, MethodDefinition, PrivateIdentifier, PropertyDefinition, StaticBlock } from 'estree' */
/** @import { StateField } from '../types.js' */
/** @import { Context as ClientContext } from '../client/types.js' */
/** @import { Context as ServerContext } from '../server/types.js' */
/** @import { StateCreationRuneName } from '../../../../utils.js' */
/** @import { AssignmentBuilder, ClassTransformer, StateFieldBuilder, StatefulAssignment, StatefulPropertyDefinition } from './types.js' */
/** @import { Scope } from '../../scope.js' */
import * as b from '#compiler/builders';
import { once } from '../../../../internal/server/index.js';
import { is_state_creation_rune, STATE_CREATION_RUNES } from '../../../../utils.js';
import { regex_invalid_identifier_chars } from '../../patterns.js';
import { get_rune } from '../../scope.js';
/**
* @template {ClientContext | ServerContext} TContext
* @param {Array<PropertyDefinition | MethodDefinition | StaticBlock>} body
* @param {StateFieldBuilder<TContext>} build_state_field
* @param {AssignmentBuilder<TContext>} build_assignment
* @returns {ClassTransformer<TContext>}
*/
export function create_class_transformer(body, build_state_field, build_assignment) {
/**
* Public, stateful fields.
* @type {Map<string, StateField>}
*/
const public_fields = new Map();
/**
* Private, stateful fields. These are namespaced separately because
* public and private fields can have the same name in the AST -- ex.
* `count` and `#count` are both named `count` -- and because it's useful
* in a couple of cases to be able to check for only one or the other.
* @type {Map<string, StateField>}
*/
const private_fields = new Map();
/**
* Accumulates nodes for the new class body.
* @type {Array<PropertyDefinition | MethodDefinition>}
*/
const new_body = [];
/**
* Private identifiers in use by this analysis.
* Factoid: Unlike public class fields, private fields _must_ be declared in the class body
* before use. So the following is actually a JavaScript syntax error, which means we can
* be 100% certain we know all private fields after parsing the class body:
*
* ```ts
* class Example {
* constructor() {
* this.public = 'foo'; // not a problem!
* this.#private = 'bar'; // JavaScript parser error
* }
* }
* ```
* @type {Set<string>}
*/
const private_ids = new Set();
/**
* A registry of functions to call to complete body modifications.
* Replacements may insert more than one node to the body. The original
* body should not be modified -- instead, replacers should push new
* nodes to new_body.
*
* @type {Array<() => void>}
*/
const replacers = [];
/**
* Get a state field by name.
*
* @param {string} name
* @param {boolean} is_private
* @param {ReadonlyArray<StateCreationRuneName>} [kinds]
*/
function get_field(name, is_private, kinds = STATE_CREATION_RUNES) {
const value = (is_private ? private_fields : public_fields).get(name);
if (value && kinds.includes(value.kind)) {
return value;
}
}
/**
* Create a child context that makes sense for passing to the child analyzers.
* @param {TContext} context
* @returns {TContext}
*/
function create_child_context(context) {
const state = {
...context.state,
class_transformer
};
// @ts-expect-error - I can't find a way to make TypeScript happy with these
const visit = (node, state_override) => context.visit(node, { ...state, ...state_override });
// @ts-expect-error - I can't find a way to make TypeScript happy with these
const next = (state_override) => context.next({ ...state, ...state_override });
return {
...context,
state,
visit,
next
};
}
/**
* Generate a new body for the class. Ensure there is a visitor for AssignmentExpression that
* calls `generate_assignment` to capture any stateful fields declared in the constructor.
* @param {TContext} context
*/
function generate_body(context) {
const child_context = create_child_context(context);
for (const node of body) {
const was_registered = register_body_definition(node, child_context);
if (!was_registered) {
new_body.push(
/** @type {PropertyDefinition | MethodDefinition} */ (
// @ts-expect-error generics silliness
child_context.visit(node, child_context.state)
)
);
}
}
for (const replacer of replacers) {
replacer();
}
return new_body;
}
/**
* Given an assignment expression, check to see if that assignment expression declares
* a stateful field. If it does, register that field and then return the processed
* assignment expression. If an assignment expression is returned from this function,
* it should be considered _fully processed_ and should replace the existing assignment
* expression node.
* @param {AssignmentExpression} node
* @param {TContext} context
* @returns {AssignmentExpression | null} The node, if `register_assignment` handled its transformation.
*/
function generate_assignment(node, context) {
const child_context = create_child_context(context);
if (
!(
node.operator === '=' &&
node.left.type === 'MemberExpression' &&
node.left.object.type === 'ThisExpression' &&
(node.left.property.type === 'Identifier' ||
node.left.property.type === 'PrivateIdentifier' ||
node.left.property.type === 'Literal')
)
) {
return null;
}
const name = get_name(node.left.property);
if (!name) {
return null;
}
const parsed = parse_stateful_assignment(node, child_context.state.scope);
if (!parsed) {
return null;
}
const { stateful_assignment, rune } = parsed;
const is_private = stateful_assignment.left.property.type === 'PrivateIdentifier';
let field;
if (is_private) {
field = {
kind: rune,
id: /** @type {PrivateIdentifier} */ (stateful_assignment.left.property)
};
private_fields.set(name, field);
} else {
field = {
kind: rune,
// it's safe to do this upfront now because we're guaranteed to already know about all private
// identifiers (they had to have been declared at the class root, before we visited the constructor)
id: deconflict(name)
};
public_fields.set(name, field);
}
const replacer = () => {
const nodes = build_state_field({
is_private,
field,
node: stateful_assignment,
context: child_context
});
if (!nodes) {
return;
}
new_body.push(...nodes);
};
replacers.push(replacer);
return build_assignment({
field,
node: stateful_assignment,
context: child_context
});
}
/**
* Register a class body definition.
*
* @param {PropertyDefinition | MethodDefinition | StaticBlock} node
* @param {TContext} child_context
* @returns {boolean} if this node is stateful and was registered
*/
function register_body_definition(node, child_context) {
if (node.type === 'MethodDefinition' && node.kind === 'constructor') {
// life is easier to reason about if we've visited the constructor
// and registered its public state field before we start building
// anything else
replacers.unshift(() => {
new_body.push(
/** @type {MethodDefinition} */ (
// @ts-expect-error generics silliness
child_context.visit(node, child_context.state)
)
);
});
return true;
}
if (
!(
(node.type === 'PropertyDefinition' || node.type === 'MethodDefinition') &&
(node.key.type === 'Identifier' ||
node.key.type === 'PrivateIdentifier' ||
node.key.type === 'Literal')
)
) {
return false;
}
/*
* We don't know if the node is stateful yet, but we still need to register some details.
* For example: If the node is a private identifier, we could accidentally conflict with it later
* if we create a private field for public state (as would happen in this example:)
*
* ```ts
* class Foo {
* #count = 0;
* count = $state(0); // would become #count if we didn't know about the private field above
* }
*/
const name = get_name(node.key);
if (!name) {
return false;
}
const is_private = node.key.type === 'PrivateIdentifier';
if (is_private) {
private_ids.add(name);
}
const parsed = prop_def_is_stateful(node, child_context.state.scope);
if (!parsed) {
// this isn't a stateful field definition, but if could become one in the constructor -- so we register
// it, but conditionally -- so that if it's added as a field in the constructor (which causes us to create)
// a field definition for it), we don't end up with a duplicate definition (this one, plus the one we create)
replacers.push(() => {
if (!get_field(name, is_private)) {
new_body.push(
/** @type {PropertyDefinition | MethodDefinition} */ (
// @ts-expect-error generics silliness
child_context.visit(node, child_context.state)
)
);
}
});
return true;
}
const { stateful_prop_def, rune } = parsed;
let field;
if (is_private) {
field = {
kind: rune,
id: /** @type {PrivateIdentifier} */ (stateful_prop_def.key)
};
private_fields.set(name, field);
} else {
// We can't set the ID until we've identified all of the private state fields,
// otherwise we might conflict with them. After registering all property definitions,
// call `finalize_property_definitions` to populate the IDs. So long as we don't
// access the ID before the end of this loop, we're fine!
const id = once(() => deconflict(name));
field = {
kind: rune,
get id() {
return id();
}
};
public_fields.set(name, field);
}
const replacer = () => {
const nodes = build_state_field({
is_private,
field,
node: stateful_prop_def,
context: child_context
});
if (!nodes) {
return;
}
new_body.push(...nodes);
};
replacers.push(replacer);
return true;
}
/**
* @param {string} name
* @returns {PrivateIdentifier}
*/
function deconflict(name) {
let deconflicted = name;
while (private_ids.has(deconflicted)) {
deconflicted = '_' + deconflicted;
}
private_ids.add(deconflicted);
return b.private_id(deconflicted);
}
/**
* @param {Identifier | PrivateIdentifier | Literal} node
*/
function get_name(node) {
if (node.type === 'Literal') {
let name = node.value?.toString().replace(regex_invalid_identifier_chars, '_');
// the above could generate conflicts because it has to generate a valid identifier
// so stuff like `0` and `1` or `state%` and `state^` will result in the same string
// so we have to de-conflict. We can only check `public_fields` because private state
// can't have literal keys
while (name && public_fields.has(name)) {
name = '_' + name;
}
return name;
} else {
return node.name;
}
}
const class_transformer = {
get_field,
generate_body,
generate_assignment
};
return class_transformer;
}
/**
* `get_rune` is really annoying because it really guarantees this already
* we just need this to tell the type system about it
* @param {AssignmentExpression} node
* @param {Scope} scope
* @returns {{ stateful_assignment: StatefulAssignment, rune: StateCreationRuneName } | null}
*/
function parse_stateful_assignment(node, scope) {
const rune = get_rune(node.right, scope);
if (!rune || !is_state_creation_rune(rune)) {
return null;
}
return { stateful_assignment: /** @type {StatefulAssignment} */ (node), rune };
}
/**
* @param {PropertyDefinition | MethodDefinition} node
* @param {Scope} scope
* @returns {{ stateful_prop_def: StatefulPropertyDefinition, rune: StateCreationRuneName } | null}
*/
function prop_def_is_stateful(node, scope) {
const rune = get_rune(node.value, scope);
if (!rune || !is_state_creation_rune(rune)) {
return null;
}
return { stateful_prop_def: /** @type {StatefulPropertyDefinition} */ (node), rune };
}

@ -1,82 +0,0 @@
import type {
AssignmentExpression,
CallExpression,
Identifier,
MemberExpression,
PropertyDefinition,
MethodDefinition,
PrivateIdentifier,
ThisExpression,
Literal
} from 'estree';
import type { StateField } from '../types';
import type { Context as ServerContext } from '../server/types';
import type { Context as ClientContext } from '../client/types';
import type { StateCreationRuneName } from '../../../../utils';
export type StatefulAssignment = AssignmentExpression & {
left: MemberExpression & {
object: ThisExpression;
property: Identifier | PrivateIdentifier | Literal;
};
right: CallExpression;
};
export type StatefulPropertyDefinition = PropertyDefinition & {
key: Identifier | PrivateIdentifier | Literal;
value: CallExpression;
};
export type StateFieldBuilderParams<TContext extends ServerContext | ClientContext> = {
is_private: boolean;
field: StateField;
node: StatefulAssignment | StatefulPropertyDefinition;
context: TContext;
};
export type StateFieldBuilder<TContext extends ServerContext | ClientContext> = (
params: StateFieldBuilderParams<TContext>
) => Array<PropertyDefinition | MethodDefinition>;
export type AssignmentBuilderParams<TContext extends ServerContext | ClientContext> = {
node: StatefulAssignment;
field: StateField;
context: TContext;
};
export type AssignmentBuilder<TContext extends ServerContext | ClientContext> = (
params: AssignmentBuilderParams<TContext>
) => AssignmentExpression;
export type ClassTransformer<TContext extends ServerContext | ClientContext> = {
/**
* @param name - The name of the field.
* @param is_private - Whether the field is private (whether its name starts with '#').
* @param kinds - What kinds of state creation runes you're looking for, eg. only '$derived.by'.
* @returns The field if it exists and matches the given criteria, or null.
*/
get_field: (
name: string,
is_private: boolean,
kinds?: Array<StateCreationRuneName>
) => StateField | undefined;
/**
* Given the body of a class, generate a new body with stateful fields.
* This assumes that {@link register_assignment} is registered to be called
* for all `AssignmentExpression` nodes in the class body.
* @param context - The context associated with the `ClassBody`.
* @returns The new body.
*/
generate_body: (context: TContext) => Array<PropertyDefinition | MethodDefinition>;
/**
* Register an assignment expression. This checks to see if the assignment is creating
* a state field on the class. If it is, it registers that state field and modifies the
* assignment expression.
*/
generate_assignment: (
node: AssignmentExpression,
context: TContext
) => AssignmentExpression | null;
};
Loading…
Cancel
Save