Skip to content

Commit fb8d6d7

Browse files
feat: Analysis phase
1 parent 134d435 commit fb8d6d7

File tree

16 files changed

+257
-122
lines changed

16 files changed

+257
-122
lines changed

documentation/docs/98-reference/.generated/compile-errors.md

Lines changed: 32 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -208,6 +208,37 @@ Cannot assign to %thing%
208208
Cannot bind to %thing%
209209
```
210210

211+
### constructor_state_reassignment
212+
213+
```
214+
Cannot redeclare stateful field `%name%` in the constructor. The field was originally declared here: `%original_location%`
215+
```
216+
217+
To create stateful class fields in the constructor, the rune assignment must be the _first_ assignment to the class field.
218+
Assignments thereafter must not use the rune.
219+
220+
```ts
221+
constructor() {
222+
this.count = $state(0);
223+
this.count = $state(1); // invalid, assigning to the same property with `$state` again
224+
}
225+
226+
constructor() {
227+
this.count = $state(0);
228+
this.count = $state.raw(1); // invalid, assigning to the same property with a different rune
229+
}
230+
231+
constructor() {
232+
this.count = 0;
233+
this.count = $state(1); // invalid, this property was created as a regular property, not state
234+
}
235+
236+
constructor() {
237+
this.count = $state(0);
238+
this.count = 1; // valid, this is setting the state that has already been declared
239+
}
240+
```
241+
211242
### css_empty_declaration
212243

213244
```
@@ -855,7 +886,7 @@ Cannot export state from a module if it is reassigned. Either export a function
855886
### state_invalid_placement
856887

857888
```
858-
`%rune%(...)` can only be used as a variable declaration initializer or a class field
889+
`%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.
859890
```
860891

861892
### store_invalid_scoped_subscription

packages/svelte/messages/compile-errors/script.md

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,35 @@
1010

1111
> Cannot bind to %thing%
1212
13+
## constructor_state_reassignment
14+
15+
> Cannot redeclare stateful field `%name%` in the constructor. The field was originally declared here: `%original_location%`
16+
17+
To create stateful class fields in the constructor, the rune assignment must be the _first_ assignment to the class field.
18+
Assignments thereafter must not use the rune.
19+
20+
```ts
21+
constructor() {
22+
this.count = $state(0);
23+
this.count = $state(1); // invalid, assigning to the same property with `$state` again
24+
}
25+
26+
constructor() {
27+
this.count = $state(0);
28+
this.count = $state.raw(1); // invalid, assigning to the same property with a different rune
29+
}
30+
31+
constructor() {
32+
this.count = 0;
33+
this.count = $state(1); // invalid, this property was created as a regular property, not state
34+
}
35+
36+
constructor() {
37+
this.count = $state(0);
38+
this.count = 1; // valid, this is setting the state that has already been declared
39+
}
40+
```
41+
1342
## declaration_duplicate
1443

1544
> `%name%` has already been declared
@@ -218,7 +247,7 @@ It's possible to export a snippet from a `<script module>` block, but only if it
218247
219248
## state_invalid_placement
220249

221-
> `%rune%(...)` can only be used as a variable declaration initializer or a class field
250+
> `%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.
222251
223252
## store_invalid_scoped_subscription
224253

packages/svelte/src/compiler/errors.js

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,17 @@ export function constant_binding(node, thing) {
104104
e(node, 'constant_binding', `Cannot bind to ${thing}\nhttps://svelte.dev/e/constant_binding`);
105105
}
106106

107+
/**
108+
* Cannot redeclare stateful field `%name%` in the constructor. The field was originally declared here: `%original_location%`
109+
* @param {null | number | NodeLike} node
110+
* @param {string} name
111+
* @param {string} original_location
112+
* @returns {never}
113+
*/
114+
export function constructor_state_reassignment(node, name, original_location) {
115+
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`);
116+
}
117+
107118
/**
108119
* `%name%` has already been declared
109120
* @param {null | number | NodeLike} node
@@ -471,13 +482,13 @@ export function state_invalid_export(node) {
471482
}
472483

473484
/**
474-
* `%rune%(...)` can only be used as a variable declaration initializer or a class field
485+
* `%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.
475486
* @param {null | number | NodeLike} node
476487
* @param {string} rune
477488
* @returns {never}
478489
*/
479490
export function state_invalid_placement(node, rune) {
480-
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`);
491+
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`);
481492
}
482493

483494
/**

packages/svelte/src/compiler/phases/2-analyze/index.js

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,6 @@ import { Attribute } from './visitors/Attribute.js';
2222
import { AwaitBlock } from './visitors/AwaitBlock.js';
2323
import { BindDirective } from './visitors/BindDirective.js';
2424
import { CallExpression } from './visitors/CallExpression.js';
25-
import { ClassBody } from './visitors/ClassBody.js';
2625
import { ClassDeclaration } from './visitors/ClassDeclaration.js';
2726
import { ClassDirective } from './visitors/ClassDirective.js';
2827
import { Component } from './visitors/Component.js';
@@ -46,6 +45,7 @@ import { LetDirective } from './visitors/LetDirective.js';
4645
import { MemberExpression } from './visitors/MemberExpression.js';
4746
import { NewExpression } from './visitors/NewExpression.js';
4847
import { OnDirective } from './visitors/OnDirective.js';
48+
import { PropertyDefinition } from './visitors/PropertyDefinition.js';
4949
import { RegularElement } from './visitors/RegularElement.js';
5050
import { RenderTag } from './visitors/RenderTag.js';
5151
import { SlotElement } from './visitors/SlotElement.js';
@@ -135,7 +135,6 @@ const visitors = {
135135
AwaitBlock,
136136
BindDirective,
137137
CallExpression,
138-
ClassBody,
139138
ClassDeclaration,
140139
ClassDirective,
141140
Component,
@@ -159,6 +158,7 @@ const visitors = {
159158
MemberExpression,
160159
NewExpression,
161160
OnDirective,
161+
PropertyDefinition,
162162
RegularElement,
163163
RenderTag,
164164
SlotElement,
@@ -259,7 +259,7 @@ export function analyze_module(ast, options) {
259259
scope,
260260
scopes,
261261
analysis: /** @type {ComponentAnalysis} */ (analysis),
262-
derived_state: [],
262+
class_state: null,
263263
// TODO the following are not needed for modules, but we have to pass them in order to avoid type error,
264264
// and reducing the type would result in a lot of tedious type casts elsewhere - find a good solution one day
265265
ast_type: /** @type {any} */ (null),
@@ -618,7 +618,7 @@ export function analyze_component(root, source, options) {
618618
has_props_rune: false,
619619
component_slots: new Set(),
620620
expression: null,
621-
derived_state: [],
621+
class_state: null,
622622
function_depth: scope.function_depth,
623623
reactive_statement: null
624624
};
@@ -685,7 +685,7 @@ export function analyze_component(root, source, options) {
685685
reactive_statement: null,
686686
component_slots: new Set(),
687687
expression: null,
688-
derived_state: [],
688+
class_state: null,
689689
function_depth: scope.function_depth
690690
};
691691

packages/svelte/src/compiler/phases/2-analyze/types.d.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import type { Scope } from '../scope.js';
22
import type { ComponentAnalysis, ReactiveStatement } from '../types.js';
33
import type { AST, ExpressionMetadata, ValidatedCompileOptions } from '#compiler';
4+
import type { ClassAnalysis } from './visitors/shared/class-analysis.js';
45

56
export interface AnalysisState {
67
scope: Scope;
@@ -18,7 +19,9 @@ export interface AnalysisState {
1819
component_slots: Set<string>;
1920
/** Information about the current expression/directive/block value */
2021
expression: ExpressionMetadata | null;
21-
derived_state: { name: string; private: boolean }[];
22+
23+
/** Used to analyze class state. */
24+
class_state: ClassAnalysis | null;
2225
function_depth: number;
2326

2427
// legacy stuff

packages/svelte/src/compiler/phases/2-analyze/visitors/AssignmentExpression.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,5 +23,6 @@ export function AssignmentExpression(node, context) {
2323
}
2424
}
2525

26+
context.state.class_state?.register?.(node, context);
2627
context.next();
2728
}

packages/svelte/src/compiler/phases/2-analyze/visitors/CallExpression.js

Lines changed: 4 additions & 76 deletions
Original file line numberDiff line numberDiff line change
@@ -119,7 +119,10 @@ export function CallExpression(node, context) {
119119
!(
120120
call_expression_is_variable_declaration(parent, context) ||
121121
call_expression_is_class_property_definition(parent) ||
122-
call_expression_is_valid_class_property_assignment_in_constructor(parent, context)
122+
context.state.class_state?.is_class_property_assignment_at_constructor_root(
123+
parent,
124+
context.path.slice(0, -1)
125+
)
123126
)
124127
) {
125128
e.state_invalid_placement(node, rune);
@@ -289,78 +292,3 @@ function call_expression_is_variable_declaration(parent, context) {
289292
function call_expression_is_class_property_definition(parent) {
290293
return parent.type === 'PropertyDefinition' && !parent.static && !parent.computed;
291294
}
292-
293-
/**
294-
*
295-
* @param {AST.SvelteNode} parent
296-
* @param {Context} context
297-
* @returns
298-
*/
299-
function call_expression_is_valid_class_property_assignment_in_constructor(parent, context) {
300-
return (
301-
expression_is_assignment_to_top_level_property_of_this(parent) &&
302-
current_node_is_in_constructor_root_or_control_flow_blocks(context)
303-
);
304-
}
305-
306-
/**
307-
* yes:
308-
* - `this.foo = bar`
309-
*
310-
* no:
311-
* - `this = bar`
312-
* - `this.foo.baz = bar`
313-
* - `anything_other_than_this = bar`
314-
*
315-
* @param {AST.SvelteNode} node
316-
*/
317-
function expression_is_assignment_to_top_level_property_of_this(node) {
318-
return (
319-
node.type === 'AssignmentExpression' &&
320-
node.operator === '=' &&
321-
node.left.type === 'MemberExpression' &&
322-
node.left.object.type === 'ThisExpression' &&
323-
node.left.property.type === 'Identifier'
324-
);
325-
}
326-
327-
/**
328-
* @param {AST.SvelteNode} node
329-
*/
330-
function node_is_constructor(node) {
331-
return (
332-
node.type === 'MethodDefinition' &&
333-
node.key.type === 'Identifier' &&
334-
node.key.name === 'constructor'
335-
);
336-
}
337-
338-
// if blocks are just IfStatements with BlockStatements or other IfStatements as consequents
339-
const allowed_parent_types = new Set([
340-
'IfStatement',
341-
'BlockStatement',
342-
'SwitchCase',
343-
'SwitchStatement'
344-
]);
345-
346-
/**
347-
* Succeeds if the node's only direct parents are `if` / `else if` / `else` blocks _and_
348-
* those blocks are the direct children of the constructor.
349-
*
350-
* @param {Context} context
351-
*/
352-
function current_node_is_in_constructor_root_or_control_flow_blocks(context) {
353-
let parent_index = -3; // this gets us from CallExpression -> AssignmentExpression -> ExpressionStatement -> Whatever is here
354-
while (true) {
355-
const grandparent = get_parent(context.path, parent_index - 1);
356-
const parent = get_parent(context.path, parent_index);
357-
if (grandparent && node_is_constructor(grandparent)) {
358-
// if this is the case then `parent` is the FunctionExpression
359-
return true;
360-
}
361-
if (!allowed_parent_types.has(parent.type)) {
362-
return false;
363-
}
364-
parent_index--;
365-
}
366-
}

packages/svelte/src/compiler/phases/2-analyze/visitors/ClassBody.js

Lines changed: 0 additions & 30 deletions
This file was deleted.

packages/svelte/src/compiler/phases/2-analyze/visitors/ClassDeclaration.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
/** @import { ClassDeclaration } from 'estree' */
22
/** @import { Context } from '../types' */
33
import * as w from '../../../warnings.js';
4+
import { ClassAnalysis } from './shared/class-analysis.js';
45
import { validate_identifier_name } from './shared/utils.js';
56

67
/**
@@ -21,5 +22,5 @@ export function ClassDeclaration(node, context) {
2122
w.perf_avoid_nested_class(node);
2223
}
2324

24-
context.next();
25+
context.next({ ...context.state, class_state: new ClassAnalysis() });
2526
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
/** @import { PropertyDefinition } from 'estree' */
2+
/** @import { Context } from '../types' */
3+
4+
/**
5+
*
6+
* @param {PropertyDefinition} node
7+
* @param {Context} context
8+
*/
9+
export function PropertyDefinition(node, context) {
10+
context.state.class_state?.register?.(node, context);
11+
context.next();
12+
}

0 commit comments

Comments
 (0)