pull/15820/head
Rich Harris 4 months ago
parent 1f52cf00d9
commit 3bfcaccc85

@ -48,6 +48,7 @@ import { Literal } from './visitors/Literal.js';
import { MemberExpression } from './visitors/MemberExpression.js'; import { MemberExpression } from './visitors/MemberExpression.js';
import { NewExpression } from './visitors/NewExpression.js'; import { NewExpression } from './visitors/NewExpression.js';
import { OnDirective } from './visitors/OnDirective.js'; import { OnDirective } from './visitors/OnDirective.js';
import { PropertyDefinition } from './visitors/PropertyDefinition.js';
import { RegularElement } from './visitors/RegularElement.js'; import { RegularElement } from './visitors/RegularElement.js';
import { RenderTag } from './visitors/RenderTag.js'; import { RenderTag } from './visitors/RenderTag.js';
import { SlotElement } from './visitors/SlotElement.js'; import { SlotElement } from './visitors/SlotElement.js';
@ -164,6 +165,7 @@ const visitors = {
MemberExpression, MemberExpression,
NewExpression, NewExpression,
OnDirective, OnDirective,
PropertyDefinition,
RegularElement, RegularElement,
RenderTag, RenderTag,
SlotElement, SlotElement,
@ -266,7 +268,7 @@ export function analyze_module(ast, options) {
scope, scope,
scopes, scopes,
analysis: /** @type {ComponentAnalysis} */ (analysis), analysis: /** @type {ComponentAnalysis} */ (analysis),
state_fields: null, state_fields: new Map(),
// TODO the following are not needed for modules, but we have to pass them in order to avoid type error, // TODO the following are not needed for modules, but we have to pass them in order to avoid type error,
// and reducing the type would result in a lot of tedious type casts elsewhere - find a good solution one day // and reducing the type would result in a lot of tedious type casts elsewhere - find a good solution one day
ast_type: /** @type {any} */ (null), ast_type: /** @type {any} */ (null),
@ -626,7 +628,7 @@ export function analyze_component(root, source, options) {
has_props_rune: false, has_props_rune: false,
component_slots: new Set(), component_slots: new Set(),
expression: null, expression: null,
state_fields: null, state_fields: new Map(),
function_depth: scope.function_depth, function_depth: scope.function_depth,
reactive_statement: null reactive_statement: null
}; };
@ -693,7 +695,7 @@ export function analyze_component(root, source, options) {
reactive_statement: null, reactive_statement: null,
component_slots: new Set(), component_slots: new Set(),
expression: null, expression: null,
state_fields: null, state_fields: new Map(),
function_depth: scope.function_depth function_depth: scope.function_depth
}; };

@ -20,7 +20,7 @@ export interface AnalysisState {
expression: ExpressionMetadata | null; expression: ExpressionMetadata | null;
/** Used to analyze class state */ /** Used to analyze class state */
state_fields: Record<string, StateField> | null; state_fields: Map<string, StateField>;
function_depth: number; function_depth: number;

@ -30,14 +30,11 @@ export function ClassBody(node, context) {
} }
} }
/** @type {Record<string, StateField>} */ /** @type {Map<string, StateField>} */
const state_fields = {}; const state_fields = new Map();
context.state.analysis.classes.set(node, state_fields); context.state.analysis.classes.set(node, state_fields);
/** @type {string[]} */
const seen = [];
/** @type {MethodDefinition | null} */ /** @type {MethodDefinition | null} */
let constructor = null; let constructor = null;
@ -53,24 +50,22 @@ export function ClassBody(node, context) {
const rune = get_rune(value, context.state.scope); const rune = get_rune(value, context.state.scope);
if (rune && is_state_creation_rune(rune)) { if (rune && is_state_creation_rune(rune)) {
if (seen.includes(name)) { if (state_fields.has(name)) {
e.state_field_duplicate(node, name); e.state_field_duplicate(node, name);
} }
state_fields[name] = { state_fields.set(name, {
node, node,
type: rune, type: rune,
// @ts-expect-error for public state this is filled out in a moment // @ts-expect-error for public state this is filled out in a moment
key: key.type === 'PrivateIdentifier' ? key : null, key: key.type === 'PrivateIdentifier' ? key : null,
value: /** @type {CallExpression} */ (value) value: /** @type {CallExpression} */ (value)
}; });
seen.push(name);
} }
} }
for (const child of node.body) { for (const child of node.body) {
if (child.type === 'PropertyDefinition' && !child.computed) { if (child.type === 'PropertyDefinition' && !child.computed && !child.static) {
handle(child, child.key, child.value); handle(child, child.key, child.value);
} }
@ -94,13 +89,11 @@ export function ClassBody(node, context) {
} }
} }
for (const name in state_fields) { for (const [name, field] of state_fields) {
if (name[0] === '#') { if (name[0] === '#') {
continue; continue;
} }
const field = state_fields[name];
let deconflicted = name.replace(regex_invalid_identifier_chars, '_'); let deconflicted = name.replace(regex_invalid_identifier_chars, '_');
while (private_ids.includes(deconflicted)) { while (private_ids.includes(deconflicted)) {
deconflicted = '_' + deconflicted; deconflicted = '_' + deconflicted;

@ -0,0 +1,21 @@
/** @import { PropertyDefinition } from 'estree' */
/** @import { Context } from '../types' */
import * as e from '../../../errors.js';
import { get_name } from '../../nodes.js';
/**
* @param {PropertyDefinition} node
* @param {Context} context
*/
export function PropertyDefinition(node, context) {
const name = get_name(node.key);
const field = name && context.state.state_fields.get(name);
if (field && node !== field.node && node.value) {
if (/** @type {number} */ (node.start) < /** @type {number} */ (field.node.start)) {
e.state_field_invalid_assignment(node);
}
}
context.next();
}

@ -42,11 +42,7 @@ export function validate_assignment(node, argument, context) {
? null ? null
: get_name(argument.property); : get_name(argument.property);
const field = const field = name !== null && context.state.state_fields?.get(name);
name !== null &&
context.state.state_fields &&
Object.hasOwn(context.state.state_fields, name) &&
context.state.state_fields[name];
// check we're not assigning to a state field before its declaration in the constructor // check we're not assigning to a state field before its declaration in the constructor
if (field && field.node.type === 'AssignmentExpression' && node !== field.node) { if (field && field.node.type === 'AssignmentExpression' && node !== field.node) {

@ -163,7 +163,7 @@ export function client_component(analysis, options) {
}, },
events: new Set(), events: new Set(),
preserve_whitespace: options.preserveWhitespace, preserve_whitespace: options.preserveWhitespace,
state_fields: {}, state_fields: new Map(),
transform: {}, transform: {},
in_constructor: false, in_constructor: false,
instance_level_snippets: [], instance_level_snippets: [],
@ -670,7 +670,7 @@ export function client_module(analysis, options) {
options, options,
scope: analysis.module.scope, scope: analysis.module.scope,
scopes: analysis.module.scopes, scopes: analysis.module.scopes,
state_fields: {}, state_fields: new Map(),
transform: {}, transform: {},
in_constructor: false in_constructor: false
}; };

@ -54,12 +54,11 @@ const callees = {
function build_assignment(operator, left, right, context) { function build_assignment(operator, left, right, context) {
if (context.state.analysis.runes && left.type === 'MemberExpression') { if (context.state.analysis.runes && left.type === 'MemberExpression') {
const name = get_name(left.property); const name = get_name(left.property);
const field = name && context.state.state_fields.get(name);
if (name !== null) { if (field) {
// special case — state declaration in class constructor // special case — state declaration in class constructor
const ancestor = context.path.at(-4); if (field.node.type === 'AssignmentExpression' && left === field.node.left) {
if (ancestor?.type === 'MethodDefinition' && ancestor.kind === 'constructor') {
const rune = get_rune(right, context.state.scope); const rune = get_rune(right, context.state.scope);
if (rune) { if (rune) {
@ -70,7 +69,7 @@ function build_assignment(operator, left, right, context) {
return b.assignment( return b.assignment(
operator, operator,
b.member(b.this, context.state.state_fields[name].key), b.member(b.this, field.key),
/** @type {Expression} */ (context.visit(right, child_state)) /** @type {Expression} */ (context.visit(right, child_state))
); );
} }
@ -78,20 +77,16 @@ function build_assignment(operator, left, right, context) {
// special case — assignment to private state field // special case — assignment to private state field
if (left.property.type === 'PrivateIdentifier') { if (left.property.type === 'PrivateIdentifier') {
const field = context.state.state_fields[name]; let value = /** @type {Expression} */ (
context.visit(build_assignment_value(operator, left, right))
if (field) { );
let value = /** @type {Expression} */ (
context.visit(build_assignment_value(operator, left, right))
);
const needs_proxy = const needs_proxy =
field.type === '$state' && field.type === '$state' &&
is_non_coercive_operator(operator) && is_non_coercive_operator(operator) &&
should_proxy(value, context.state.scope); should_proxy(value, context.state.scope);
return b.call('$.set', left, value, needs_proxy && b.true); return b.call('$.set', left, value, needs_proxy && b.true);
}
} }
} }
} }

@ -1,4 +1,5 @@
/** @import { CallExpression, ClassBody, MethodDefinition, PropertyDefinition, StaticBlock } from 'estree' */ /** @import { CallExpression, ClassBody, MethodDefinition, PropertyDefinition, StaticBlock } from 'estree' */
/** @import { StateField } from '#compiler' */
/** @import { Context } from '../types' */ /** @import { Context } from '../types' */
import * as b from '#compiler/builders'; import * as b from '#compiler/builders';
import { get_name } from '../../../nodes.js'; import { get_name } from '../../../nodes.js';
@ -21,13 +22,11 @@ export function ClassBody(node, context) {
const child_state = { ...context.state, state_fields }; const child_state = { ...context.state, state_fields };
for (const name in state_fields) { for (const [name, field] of state_fields) {
if (name[0] === '#') { if (name[0] === '#') {
continue; continue;
} }
const field = state_fields[name];
// insert backing fields for stuff declared in the constructor // insert backing fields for stuff declared in the constructor
if (field.node.type === 'AssignmentExpression') { if (field.node.type === 'AssignmentExpression') {
const member = b.member(b.this, field.key); const member = b.member(b.this, field.key);
@ -61,13 +60,13 @@ export function ClassBody(node, context) {
} }
const name = get_name(definition.key); const name = get_name(definition.key);
if (name === null || !Object.hasOwn(state_fields, name)) { const field = name && /** @type {StateField} */ (state_fields.get(name));
if (!field) {
body.push(/** @type {PropertyDefinition} */ (context.visit(definition, child_state))); body.push(/** @type {PropertyDefinition} */ (context.visit(definition, child_state)));
continue; continue;
} }
const field = state_fields[name];
if (name[0] === '#') { if (name[0] === '#') {
body.push(/** @type {PropertyDefinition} */ (context.visit(definition, child_state))); body.push(/** @type {PropertyDefinition} */ (context.visit(definition, child_state)));
} else if (field.node === definition) { } else if (field.node === definition) {

@ -9,7 +9,7 @@ import * as b from '#compiler/builders';
export function MemberExpression(node, context) { export function MemberExpression(node, context) {
// rewrite `this.#foo` as `this.#foo.v` inside a constructor // rewrite `this.#foo` as `this.#foo.v` inside a constructor
if (node.property.type === 'PrivateIdentifier') { if (node.property.type === 'PrivateIdentifier') {
const field = context.state.state_fields['#' + node.property.name]; const field = context.state.state_fields.get('#' + node.property.name);
if (field) { if (field) {
return context.state.in_constructor && return context.state.in_constructor &&

@ -15,7 +15,7 @@ export function UpdateExpression(node, context) {
argument.type === 'MemberExpression' && argument.type === 'MemberExpression' &&
argument.object.type === 'ThisExpression' && argument.object.type === 'ThisExpression' &&
argument.property.type === 'PrivateIdentifier' && argument.property.type === 'PrivateIdentifier' &&
Object.hasOwn(context.state.state_fields, '#' + argument.property.name) context.state.state_fields.has('#' + argument.property.name)
) { ) {
let fn = '$.update'; let fn = '$.update';
if (node.prefix) fn += '_pre'; if (node.prefix) fn += '_pre';

@ -97,7 +97,7 @@ export function server_component(analysis, options) {
template: /** @type {any} */ (null), template: /** @type {any} */ (null),
namespace: options.namespace, namespace: options.namespace,
preserve_whitespace: options.preserveWhitespace, preserve_whitespace: options.preserveWhitespace,
state_fields: {}, state_fields: new Map(),
skip_hydration_boundaries: false skip_hydration_boundaries: false
}; };
@ -393,7 +393,7 @@ export function server_module(analysis, options) {
// to be present for `javascript_visitors_legacy` and so is included in module // to be present for `javascript_visitors_legacy` and so is included in module
// transform state as well as component transform state // transform state as well as component transform state
legacy_reactive_statements: new Map(), legacy_reactive_statements: new Map(),
state_fields: {} state_fields: new Map()
}; };
const module = /** @type {Program} */ ( const module = /** @type {Program} */ (

@ -26,26 +26,23 @@ export function AssignmentExpression(node, context) {
function build_assignment(operator, left, right, context) { function build_assignment(operator, left, right, context) {
if (context.state.analysis.runes && left.type === 'MemberExpression') { if (context.state.analysis.runes && left.type === 'MemberExpression') {
const name = get_name(left.property); const name = get_name(left.property);
const field = name && context.state.state_fields.get(name);
if (name !== null) { // special case — state declaration in class constructor
// special case — state declaration in class constructor if (field && field.node.type === 'AssignmentExpression' && left === field.node.left) {
const ancestor = context.path.at(-4); const rune = get_rune(right, context.state.scope);
if (ancestor?.type === 'MethodDefinition' && ancestor.kind === 'constructor') { if (rune) {
const rune = get_rune(right, context.state.scope); const key =
left.property.type === 'PrivateIdentifier' || rune === '$state' || rune === '$state.raw'
? left.property
: field.key;
if (rune) { return b.assignment(
const key = operator,
left.property.type === 'PrivateIdentifier' || rune === '$state' || rune === '$state.raw' b.member(b.this, key, key.type === 'Literal'),
? left.property /** @type {Expression} */ (context.visit(right))
: context.state.state_fields[name].key; );
return b.assignment(
operator,
b.member(b.this, key, key.type === 'Literal'),
/** @type {Expression} */ (context.visit(right))
);
}
} }
} }
} }

@ -21,15 +21,14 @@ export function ClassBody(node, context) {
const child_state = { ...context.state, state_fields }; const child_state = { ...context.state, state_fields };
for (const name in state_fields) { for (const [name, field] of state_fields) {
if (name[0] === '#') { if (name[0] === '#') {
continue; continue;
} }
const field = state_fields[name];
// insert backing fields for stuff declared in the constructor // insert backing fields for stuff declared in the constructor
if ( if (
field &&
field.node.type === 'AssignmentExpression' && field.node.type === 'AssignmentExpression' &&
(field.type === '$derived' || field.type === '$derived.by') (field.type === '$derived' || field.type === '$derived.by')
) { ) {
@ -52,13 +51,13 @@ export function ClassBody(node, context) {
} }
const name = get_name(definition.key); const name = get_name(definition.key);
if (name === null || !Object.hasOwn(state_fields, name)) { const field = name && state_fields.get(name);
if (!field) {
body.push(/** @type {PropertyDefinition} */ (context.visit(definition, child_state))); body.push(/** @type {PropertyDefinition} */ (context.visit(definition, child_state)));
continue; continue;
} }
const field = state_fields[name];
if (name[0] === '#' || field.type === '$state' || field.type === '$state.raw') { if (name[0] === '#' || field.type === '$state' || field.type === '$state.raw') {
body.push(/** @type {PropertyDefinition} */ (context.visit(definition, child_state))); body.push(/** @type {PropertyDefinition} */ (context.visit(definition, child_state)));
} else if (field.node === definition) { } else if (field.node === definition) {

@ -8,5 +8,5 @@ export interface TransformState {
readonly scope: Scope; readonly scope: Scope;
readonly scopes: Map<AST.SvelteNode, Scope>; readonly scopes: Map<AST.SvelteNode, Scope>;
readonly state_fields: Record<string, StateField>; readonly state_fields: Map<string, StateField>;
} }

@ -38,7 +38,7 @@ export interface Analysis {
immutable: boolean; immutable: boolean;
tracing: boolean; tracing: boolean;
classes: Map<ClassBody, Record<string, StateField>>; classes: Map<ClassBody, Map<string, StateField>>;
// TODO figure out if we can move this to ComponentAnalysis // TODO figure out if we can move this to ComponentAnalysis
accessors: boolean; accessors: boolean;

@ -0,0 +1,14 @@
[
{
"code": "state_field_invalid_assignment",
"message": "Cannot assign to a state field before its declaration",
"start": {
"line": 2,
"column": 1
},
"end": {
"line": 2,
"column": 12
}
}
]

@ -0,0 +1,7 @@
export class Counter {
count = -1;
constructor() {
this.count = $state(0);
}
}
Loading…
Cancel
Save