Skip to content

feat: State declarations in class constructors #15820

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 5 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 32 additions & 1 deletion documentation/docs/98-reference/.generated/compile-errors.md
Original file line number Diff line number Diff line change
Expand Up @@ -208,6 +208,37 @@ Cannot assign to %thing%
Cannot bind to %thing%
```

### constructor_state_reassignment

```
Cannot redeclare stateful field `%name%` in the constructor. The field was originally declared here: `%original_location%`
```

To create stateful class fields in the constructor, the rune assignment must be the _first_ assignment to the class field.
Assignments thereafter must not use the rune.

```ts
constructor() {
this.count = $state(0);
this.count = $state(1); // invalid, assigning to the same property with `$state` again
}

constructor() {
this.count = $state(0);
this.count = $state.raw(1); // invalid, assigning to the same property with a different rune
}

constructor() {
this.count = 0;
this.count = $state(1); // invalid, this property was created as a regular property, not state
}

constructor() {
this.count = $state(0);
this.count = 1; // valid, this is setting the state that has already been declared
}
```

### css_empty_declaration

```
Expand Down Expand Up @@ -855,7 +886,7 @@ Cannot export state from a module if it is reassigned. Either export a function
### state_invalid_placement

```
`%rune%(...)` can only be used as a variable declaration initializer or a class field
`%rune%(...)` can only be used as a variable declaration initializer, a class field declaration, or the first assignment to a class field at the top level of the constructor.
```

### store_invalid_scoped_subscription
Expand Down
31 changes: 30 additions & 1 deletion packages/svelte/messages/compile-errors/script.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,35 @@

> Cannot bind to %thing%

## constructor_state_reassignment

> Cannot redeclare stateful field `%name%` in the constructor. The field was originally declared here: `%original_location%`

To create stateful class fields in the constructor, the rune assignment must be the _first_ assignment to the class field.
Assignments thereafter must not use the rune.

```ts
constructor() {
this.count = $state(0);
this.count = $state(1); // invalid, assigning to the same property with `$state` again
}

constructor() {
this.count = $state(0);
this.count = $state.raw(1); // invalid, assigning to the same property with a different rune
}

constructor() {
this.count = 0;
this.count = $state(1); // invalid, this property was created as a regular property, not state
}

constructor() {
this.count = $state(0);
this.count = 1; // valid, this is setting the state that has already been declared
}
```

## declaration_duplicate

> `%name%` has already been declared
Expand Down Expand Up @@ -218,7 +247,7 @@ It's possible to export a snippet from a `<script module>` block, but only if it

## state_invalid_placement

> `%rune%(...)` can only be used as a variable declaration initializer or a class field
> `%rune%(...)` can only be used as a variable declaration initializer, a class field declaration, or the first assignment to a class field at the top level of the constructor.

## store_invalid_scoped_subscription

Expand Down
15 changes: 13 additions & 2 deletions packages/svelte/src/compiler/errors.js
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,17 @@ export function constant_binding(node, thing) {
e(node, 'constant_binding', `Cannot bind to ${thing}\nhttps://svelte.dev/e/constant_binding`);
}

/**
* Cannot redeclare stateful field `%name%` in the constructor. The field was originally declared here: `%original_location%`
* @param {null | number | NodeLike} node
* @param {string} name
* @param {string} original_location
* @returns {never}
*/
export function constructor_state_reassignment(node, name, original_location) {
e(node, 'constructor_state_reassignment', `Cannot redeclare stateful field \`${name}\` in the constructor. The field was originally declared here: \`${original_location}\`\nhttps://svelte.dev/e/constructor_state_reassignment`);
}

/**
* `%name%` has already been declared
* @param {null | number | NodeLike} node
Expand Down Expand Up @@ -471,13 +482,13 @@ export function state_invalid_export(node) {
}

/**
* `%rune%(...)` can only be used as a variable declaration initializer or a class field
* `%rune%(...)` can only be used as a variable declaration initializer, a class field declaration, or the first assignment to a class field at the top level of the constructor.
* @param {null | number | NodeLike} node
* @param {string} rune
* @returns {never}
*/
export function state_invalid_placement(node, rune) {
e(node, 'state_invalid_placement', `\`${rune}(...)\` can only be used as a variable declaration initializer or a class field\nhttps://svelte.dev/e/state_invalid_placement`);
e(node, 'state_invalid_placement', `\`${rune}(...)\` can only be used as a variable declaration initializer, a class field declaration, or the first assignment to a class field at the top level of the constructor.\nhttps://svelte.dev/e/state_invalid_placement`);
}

/**
Expand Down
10 changes: 5 additions & 5 deletions packages/svelte/src/compiler/phases/2-analyze/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,6 @@ import { Attribute } from './visitors/Attribute.js';
import { AwaitBlock } from './visitors/AwaitBlock.js';
import { BindDirective } from './visitors/BindDirective.js';
import { CallExpression } from './visitors/CallExpression.js';
import { ClassBody } from './visitors/ClassBody.js';
import { ClassDeclaration } from './visitors/ClassDeclaration.js';
import { ClassDirective } from './visitors/ClassDirective.js';
import { Component } from './visitors/Component.js';
Expand All @@ -46,6 +45,7 @@ import { LetDirective } from './visitors/LetDirective.js';
import { MemberExpression } from './visitors/MemberExpression.js';
import { NewExpression } from './visitors/NewExpression.js';
import { OnDirective } from './visitors/OnDirective.js';
import { PropertyDefinition } from './visitors/PropertyDefinition.js';
import { RegularElement } from './visitors/RegularElement.js';
import { RenderTag } from './visitors/RenderTag.js';
import { SlotElement } from './visitors/SlotElement.js';
Expand Down Expand Up @@ -135,7 +135,6 @@ const visitors = {
AwaitBlock,
BindDirective,
CallExpression,
ClassBody,
ClassDeclaration,
ClassDirective,
Component,
Expand All @@ -159,6 +158,7 @@ const visitors = {
MemberExpression,
NewExpression,
OnDirective,
PropertyDefinition,
RegularElement,
RenderTag,
SlotElement,
Expand Down Expand Up @@ -259,7 +259,7 @@ export function analyze_module(ast, options) {
scope,
scopes,
analysis: /** @type {ComponentAnalysis} */ (analysis),
derived_state: [],
class_state: null,
// 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
ast_type: /** @type {any} */ (null),
Expand Down Expand Up @@ -618,7 +618,7 @@ export function analyze_component(root, source, options) {
has_props_rune: false,
component_slots: new Set(),
expression: null,
derived_state: [],
class_state: null,
function_depth: scope.function_depth,
reactive_statement: null
};
Expand Down Expand Up @@ -685,7 +685,7 @@ export function analyze_component(root, source, options) {
reactive_statement: null,
component_slots: new Set(),
expression: null,
derived_state: [],
class_state: null,
function_depth: scope.function_depth
};

Expand Down
5 changes: 4 additions & 1 deletion packages/svelte/src/compiler/phases/2-analyze/types.d.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import type { Scope } from '../scope.js';
import type { ComponentAnalysis, ReactiveStatement } from '../types.js';
import type { AST, ExpressionMetadata, ValidatedCompileOptions } from '#compiler';
import type { ClassAnalysis } from './visitors/shared/class-analysis.js';

export interface AnalysisState {
scope: Scope;
Expand All @@ -18,7 +19,9 @@ export interface AnalysisState {
component_slots: Set<string>;
/** Information about the current expression/directive/block value */
expression: ExpressionMetadata | null;
derived_state: { name: string; private: boolean }[];

/** Used to analyze class state. */
class_state: ClassAnalysis | null;
function_depth: number;

// legacy stuff
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,5 +23,6 @@ export function AssignmentExpression(node, context) {
}
}

context.state.class_state?.register?.(node, context);
context.next();
}
Original file line number Diff line number Diff line change
Expand Up @@ -116,9 +116,14 @@ export function CallExpression(node, context) {
case '$derived':
case '$derived.by':
if (
(parent.type !== 'VariableDeclarator' ||
get_parent(context.path, -3).type === 'ConstTag') &&
!(parent.type === 'PropertyDefinition' && !parent.static && !parent.computed)
!(
call_expression_is_variable_declaration(parent, context) ||
call_expression_is_class_property_definition(parent) ||
context.state.class_state?.is_class_property_assignment_at_constructor_root(
parent,
context.path.slice(0, -1)
)
)
) {
e.state_invalid_placement(node, rune);
}
Expand Down Expand Up @@ -270,3 +275,20 @@ function get_function_label(nodes) {
return parent.id.name;
}
}

/**
*
* @param {AST.SvelteNode} parent
* @param {Context} context
*/
function call_expression_is_variable_declaration(parent, context) {
return parent.type === 'VariableDeclarator' && get_parent(context.path, -3).type !== 'ConstTag';
}

/**
*
* @param {AST.SvelteNode} parent
*/
function call_expression_is_class_property_definition(parent) {
return parent.type === 'PropertyDefinition' && !parent.static && !parent.computed;
}

This file was deleted.

Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
/** @import { ClassDeclaration } from 'estree' */
/** @import { Context } from '../types' */
import * as w from '../../../warnings.js';
import { ClassAnalysis } from './shared/class-analysis.js';
import { validate_identifier_name } from './shared/utils.js';

/**
Expand All @@ -21,5 +22,8 @@ export function ClassDeclaration(node, context) {
w.perf_avoid_nested_class(node);
}

context.next();
context.next({
...context.state,
class_state: context.state.analysis.runes ? new ClassAnalysis() : null
});
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
/** @import { PropertyDefinition } from 'estree' */
/** @import { Context } from '../types' */

/**
*
* @param {PropertyDefinition} node
* @param {Context} context
*/
export function PropertyDefinition(node, context) {
context.state.class_state?.register?.(node, context);
context.next();
}
Loading