From d103adff9c0fc7802e0bf6000bcc75c77f4b2e3f Mon Sep 17 00:00:00 2001 From: Elliott Johnson Date: Mon, 19 May 2025 09:48:58 -0600 Subject: [PATCH] feat: State declarations in class constructors (#15820) * feat: State declarations in class constructors * feat: Analysis phase * misc * feat: client * improvements * feat: It is now at least backwards compatible. though the new stuff may still be wrong * feat: It works I think? * final cleanup?? * tests * test for better types * changeset * rename functions (the function doesn't test call-expression-ness) * small readability tweak * failing test * fix * disallow computed state fields * tweak message to better accommodate the case in which state is declared after a regular property * failing test * wildly confusing to have so many things called 'class analysis' - rename the transform-phrase ones to transformers * missed a spot * and another * store analysis for use during transformation * move code to where it is used * do the analysis upfront, it's way simpler * skip failing test for now * simplify * get rid of the class * on second thoughts * reduce indirection * make analysis available at transform time * WIP * WIP * WIP * fix * remove unused stuff * revert snapshot tests * unused * note to self * fix * unused * unused * remove some unused stuff * unused * lint, tidy up * reuse helper * tweak * simplify/DRY * unused * tweak * unused * more * tweak * tweak * fix proxying logic * tweak * tweak * adjust message to accommodate more cases * unskip and fix test * fix * move * revert unneeded drive-by change * fix * fix * update docs --------- Co-authored-by: Rich Harris --- .changeset/mean-squids-scream.md | 5 + documentation/docs/02-runes/02-$state.md | 8 +- .../98-reference/.generated/compile-errors.md | 34 ++- .../svelte/messages/compile-errors/script.md | 30 ++- packages/svelte/src/compiler/errors.js | 23 +- .../src/compiler/phases/2-analyze/index.js | 12 +- .../src/compiler/phases/2-analyze/types.d.ts | 7 +- .../visitors/AssignmentExpression.js | 2 +- .../2-analyze/visitors/BindDirective.js | 2 +- .../2-analyze/visitors/CallExpression.js | 51 +++- .../phases/2-analyze/visitors/ClassBody.js | 105 +++++++-- .../2-analyze/visitors/PropertyDefinition.js | 21 ++ .../2-analyze/visitors/UpdateExpression.js | 2 +- .../phases/2-analyze/visitors/shared/utils.js | 50 +++- .../3-transform/client/transform-client.js | 6 +- .../phases/3-transform/client/types.d.ts | 10 +- .../client/visitors/AssignmentExpression.js | 51 ++-- .../client/visitors/CallExpression.js | 35 ++- .../3-transform/client/visitors/ClassBody.js | 222 ++++++------------ .../client/visitors/MemberExpression.js | 6 +- .../client/visitors/UpdateExpression.js | 2 +- .../3-transform/server/transform-server.js | 6 +- .../phases/3-transform/server/types.d.ts | 2 - .../server/visitors/AssignmentExpression.js | 25 ++ .../server/visitors/CallExpression.js | 9 + .../3-transform/server/visitors/ClassBody.js | 133 ++++------- .../server/visitors/MemberExpression.js | 23 -- .../compiler/phases/3-transform/types.d.ts | 4 +- packages/svelte/src/compiler/phases/nodes.js | 13 + .../svelte/src/compiler/phases/types.d.ts | 15 +- packages/svelte/src/compiler/types/index.d.ts | 14 ++ packages/svelte/src/utils.js | 26 +- .../class-state-field-static/_config.js | 2 +- .../samples/runes-no-rune-each/_config.js | 3 +- .../runes-wrong-derived-placement/_config.js | 2 +- .../runes-wrong-state-placement/_config.js | 3 +- .../_config.js | 0 .../main.svelte | 0 .../_config.js | 13 + .../main.svelte | 13 + .../_config.js | 13 + .../main.svelte | 12 + .../_config.js | 3 + .../main.svelte | 9 + .../_config.js | 45 ++++ .../main.svelte | 37 +++ .../_config.js | 20 ++ .../main.svelte | 12 + .../_config.js | 20 ++ .../main.svelte | 22 ++ .../class-state-constructor/_config.js | 20 ++ .../class-state-constructor/main.svelte | 18 ++ .../class-state-constructor-1/errors.json | 14 ++ .../class-state-constructor-1/input.svelte.js | 7 + .../class-state-constructor-10/errors.json | 14 ++ .../input.svelte.js | 9 + .../class-state-constructor-2/errors.json | 14 ++ .../class-state-constructor-2/input.svelte.js | 7 + .../class-state-constructor-3/errors.json | 14 ++ .../class-state-constructor-3/input.svelte.js | 7 + .../class-state-constructor-4/errors.json | 14 ++ .../class-state-constructor-4/input.svelte.js | 7 + .../class-state-constructor-5/errors.json | 14 ++ .../class-state-constructor-5/input.svelte.js | 7 + .../class-state-constructor-6/errors.json | 14 ++ .../class-state-constructor-6/input.svelte.js | 6 + .../class-state-constructor-7/errors.json | 14 ++ .../class-state-constructor-7/input.svelte.js | 7 + .../class-state-constructor-8/errors.json | 14 ++ .../class-state-constructor-8/input.svelte.js | 6 + .../class-state-constructor-9/errors.json | 14 ++ .../class-state-constructor-9/input.svelte.js | 7 + .../const-tag-invalid-rune-usage/errors.json | 2 +- 73 files changed, 1070 insertions(+), 363 deletions(-) create mode 100644 .changeset/mean-squids-scream.md create mode 100644 packages/svelte/src/compiler/phases/2-analyze/visitors/PropertyDefinition.js delete mode 100644 packages/svelte/src/compiler/phases/3-transform/server/visitors/MemberExpression.js rename packages/svelte/tests/runtime-runes/samples/{class-state-constructor-closure-private => class-state-constructor-closure-private-1}/_config.js (100%) rename packages/svelte/tests/runtime-runes/samples/{class-state-constructor-closure-private => class-state-constructor-closure-private-1}/main.svelte (100%) create mode 100644 packages/svelte/tests/runtime-runes/samples/class-state-constructor-closure-private-2/_config.js create mode 100644 packages/svelte/tests/runtime-runes/samples/class-state-constructor-closure-private-2/main.svelte create mode 100644 packages/svelte/tests/runtime-runes/samples/class-state-constructor-closure-private-3/_config.js create mode 100644 packages/svelte/tests/runtime-runes/samples/class-state-constructor-closure-private-3/main.svelte create mode 100644 packages/svelte/tests/runtime-runes/samples/class-state-constructor-conflicting-get-name/_config.js create mode 100644 packages/svelte/tests/runtime-runes/samples/class-state-constructor-conflicting-get-name/main.svelte create mode 100644 packages/svelte/tests/runtime-runes/samples/class-state-constructor-derived-unowned/_config.js create mode 100644 packages/svelte/tests/runtime-runes/samples/class-state-constructor-derived-unowned/main.svelte create mode 100644 packages/svelte/tests/runtime-runes/samples/class-state-constructor-predeclared-field/_config.js create mode 100644 packages/svelte/tests/runtime-runes/samples/class-state-constructor-predeclared-field/main.svelte create mode 100644 packages/svelte/tests/runtime-runes/samples/class-state-constructor-subclass/_config.js create mode 100644 packages/svelte/tests/runtime-runes/samples/class-state-constructor-subclass/main.svelte create mode 100644 packages/svelte/tests/runtime-runes/samples/class-state-constructor/_config.js create mode 100644 packages/svelte/tests/runtime-runes/samples/class-state-constructor/main.svelte create mode 100644 packages/svelte/tests/validator/samples/class-state-constructor-1/errors.json create mode 100644 packages/svelte/tests/validator/samples/class-state-constructor-1/input.svelte.js create mode 100644 packages/svelte/tests/validator/samples/class-state-constructor-10/errors.json create mode 100644 packages/svelte/tests/validator/samples/class-state-constructor-10/input.svelte.js create mode 100644 packages/svelte/tests/validator/samples/class-state-constructor-2/errors.json create mode 100644 packages/svelte/tests/validator/samples/class-state-constructor-2/input.svelte.js create mode 100644 packages/svelte/tests/validator/samples/class-state-constructor-3/errors.json create mode 100644 packages/svelte/tests/validator/samples/class-state-constructor-3/input.svelte.js create mode 100644 packages/svelte/tests/validator/samples/class-state-constructor-4/errors.json create mode 100644 packages/svelte/tests/validator/samples/class-state-constructor-4/input.svelte.js create mode 100644 packages/svelte/tests/validator/samples/class-state-constructor-5/errors.json create mode 100644 packages/svelte/tests/validator/samples/class-state-constructor-5/input.svelte.js create mode 100644 packages/svelte/tests/validator/samples/class-state-constructor-6/errors.json create mode 100644 packages/svelte/tests/validator/samples/class-state-constructor-6/input.svelte.js create mode 100644 packages/svelte/tests/validator/samples/class-state-constructor-7/errors.json create mode 100644 packages/svelte/tests/validator/samples/class-state-constructor-7/input.svelte.js create mode 100644 packages/svelte/tests/validator/samples/class-state-constructor-8/errors.json create mode 100644 packages/svelte/tests/validator/samples/class-state-constructor-8/input.svelte.js create mode 100644 packages/svelte/tests/validator/samples/class-state-constructor-9/errors.json create mode 100644 packages/svelte/tests/validator/samples/class-state-constructor-9/input.svelte.js diff --git a/.changeset/mean-squids-scream.md b/.changeset/mean-squids-scream.md new file mode 100644 index 0000000000..2157ea85a6 --- /dev/null +++ b/.changeset/mean-squids-scream.md @@ -0,0 +1,5 @@ +--- +'svelte': minor +--- + +feat: allow state fields to be declared inside class constructors diff --git a/documentation/docs/02-runes/02-$state.md b/documentation/docs/02-runes/02-$state.md index aac006d2f5..f7314e2950 100644 --- a/documentation/docs/02-runes/02-$state.md +++ b/documentation/docs/02-runes/02-$state.md @@ -67,16 +67,15 @@ todos[0].done = !todos[0].done; ### Classes -You can also use `$state` in class fields (whether public or private): +You can also use `$state` in class fields (whether public or private), or as the first assignment to a property immediately inside the `constructor`: ```js // @errors: 7006 2554 class Todo { done = $state(false); - text = $state(); constructor(text) { - this.text = text; + this.text = $state(text); } reset() { @@ -110,10 +109,9 @@ You can either use an inline function... // @errors: 7006 2554 class Todo { done = $state(false); - text = $state(); constructor(text) { - this.text = text; + this.text = $state(text); } +++reset = () => {+++ diff --git a/documentation/docs/98-reference/.generated/compile-errors.md b/documentation/docs/98-reference/.generated/compile-errors.md index e8669ead53..db848a0299 100644 --- a/documentation/docs/98-reference/.generated/compile-errors.md +++ b/documentation/docs/98-reference/.generated/compile-errors.md @@ -846,6 +846,38 @@ Cannot reassign or bind to snippet parameter This snippet is shadowing the prop `%prop%` with the same name ``` +### state_field_duplicate + +``` +`%name%` has already been declared on this class +``` + +An assignment to a class field that uses a `$state` or `$derived` rune is considered a _state field declaration_. The declaration can happen in the class body... + +```js +class Counter { + count = $state(0); +} +``` + +...or inside the constructor... + +```js +class Counter { + constructor() { + this.count = $state(0); + } +} +``` + +...but it can only happen once. + +### state_field_invalid_assignment + +``` +Cannot assign to a state field before its declaration +``` + ### state_invalid_export ``` @@ -855,7 +887,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 diff --git a/packages/svelte/messages/compile-errors/script.md b/packages/svelte/messages/compile-errors/script.md index aabcbeae48..e11975aef2 100644 --- a/packages/svelte/messages/compile-errors/script.md +++ b/packages/svelte/messages/compile-errors/script.md @@ -212,13 +212,41 @@ It's possible to export a snippet from a ` + + diff --git a/packages/svelte/tests/runtime-runes/samples/class-state-constructor-closure-private-3/_config.js b/packages/svelte/tests/runtime-runes/samples/class-state-constructor-closure-private-3/_config.js new file mode 100644 index 0000000000..dd847ce2f2 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/class-state-constructor-closure-private-3/_config.js @@ -0,0 +1,13 @@ +import { flushSync } from 'svelte'; +import { test } from '../../test'; + +export default test({ + html: ``, + ssrHtml: ``, + + async test({ assert, target }) { + flushSync(); + + assert.htmlEqual(target.innerHTML, ``); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/class-state-constructor-closure-private-3/main.svelte b/packages/svelte/tests/runtime-runes/samples/class-state-constructor-closure-private-3/main.svelte new file mode 100644 index 0000000000..47b8c901eb --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/class-state-constructor-closure-private-3/main.svelte @@ -0,0 +1,12 @@ + + + diff --git a/packages/svelte/tests/runtime-runes/samples/class-state-constructor-conflicting-get-name/_config.js b/packages/svelte/tests/runtime-runes/samples/class-state-constructor-conflicting-get-name/_config.js new file mode 100644 index 0000000000..f47bee71df --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/class-state-constructor-conflicting-get-name/_config.js @@ -0,0 +1,3 @@ +import { test } from '../../test'; + +export default test({}); diff --git a/packages/svelte/tests/runtime-runes/samples/class-state-constructor-conflicting-get-name/main.svelte b/packages/svelte/tests/runtime-runes/samples/class-state-constructor-conflicting-get-name/main.svelte new file mode 100644 index 0000000000..e2c4f302b3 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/class-state-constructor-conflicting-get-name/main.svelte @@ -0,0 +1,9 @@ + \ No newline at end of file diff --git a/packages/svelte/tests/runtime-runes/samples/class-state-constructor-derived-unowned/_config.js b/packages/svelte/tests/runtime-runes/samples/class-state-constructor-derived-unowned/_config.js new file mode 100644 index 0000000000..4cf1aea213 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/class-state-constructor-derived-unowned/_config.js @@ -0,0 +1,45 @@ +import { flushSync } from 'svelte'; +import { test } from '../../test'; + +export default test({ + // The component context class instance gets shared between tests, strangely, causing hydration to fail? + mode: ['client', 'server'], + + async test({ assert, target, logs }) { + const btn = target.querySelector('button'); + + flushSync(() => { + btn?.click(); + }); + + assert.deepEqual(logs, [0, 'class trigger false', 'local trigger false', 1]); + + flushSync(() => { + btn?.click(); + }); + + assert.deepEqual(logs, [0, 'class trigger false', 'local trigger false', 1, 2]); + + flushSync(() => { + btn?.click(); + }); + + assert.deepEqual(logs, [0, 'class trigger false', 'local trigger false', 1, 2, 3]); + + flushSync(() => { + btn?.click(); + }); + + assert.deepEqual(logs, [ + 0, + 'class trigger false', + 'local trigger false', + 1, + 2, + 3, + 4, + 'class trigger true', + 'local trigger true' + ]); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/class-state-constructor-derived-unowned/main.svelte b/packages/svelte/tests/runtime-runes/samples/class-state-constructor-derived-unowned/main.svelte new file mode 100644 index 0000000000..03687d01bb --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/class-state-constructor-derived-unowned/main.svelte @@ -0,0 +1,37 @@ + + + + + diff --git a/packages/svelte/tests/runtime-runes/samples/class-state-constructor-predeclared-field/_config.js b/packages/svelte/tests/runtime-runes/samples/class-state-constructor-predeclared-field/_config.js new file mode 100644 index 0000000000..02cf36d900 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/class-state-constructor-predeclared-field/_config.js @@ -0,0 +1,20 @@ +import { flushSync } from 'svelte'; +import { test } from '../../test'; + +export default test({ + html: ``, + + test({ assert, target }) { + const btn = target.querySelector('button'); + + flushSync(() => { + btn?.click(); + }); + assert.htmlEqual(target.innerHTML, ``); + + flushSync(() => { + btn?.click(); + }); + assert.htmlEqual(target.innerHTML, ``); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/class-state-constructor-predeclared-field/main.svelte b/packages/svelte/tests/runtime-runes/samples/class-state-constructor-predeclared-field/main.svelte new file mode 100644 index 0000000000..5dbbb10afd --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/class-state-constructor-predeclared-field/main.svelte @@ -0,0 +1,12 @@ + + + diff --git a/packages/svelte/tests/runtime-runes/samples/class-state-constructor-subclass/_config.js b/packages/svelte/tests/runtime-runes/samples/class-state-constructor-subclass/_config.js new file mode 100644 index 0000000000..32cca6c693 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/class-state-constructor-subclass/_config.js @@ -0,0 +1,20 @@ +import { flushSync } from 'svelte'; +import { test } from '../../test'; + +export default test({ + html: ``, + + test({ assert, target }) { + const btn = target.querySelector('button'); + + flushSync(() => { + btn?.click(); + }); + assert.htmlEqual(target.innerHTML, ``); + + flushSync(() => { + btn?.click(); + }); + assert.htmlEqual(target.innerHTML, ``); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/class-state-constructor-subclass/main.svelte b/packages/svelte/tests/runtime-runes/samples/class-state-constructor-subclass/main.svelte new file mode 100644 index 0000000000..d8feb554cd --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/class-state-constructor-subclass/main.svelte @@ -0,0 +1,22 @@ + + + diff --git a/packages/svelte/tests/runtime-runes/samples/class-state-constructor/_config.js b/packages/svelte/tests/runtime-runes/samples/class-state-constructor/_config.js new file mode 100644 index 0000000000..f35dc57228 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/class-state-constructor/_config.js @@ -0,0 +1,20 @@ +import { flushSync } from 'svelte'; +import { test } from '../../test'; + +export default test({ + html: ``, + + test({ assert, target }) { + const btn = target.querySelector('button'); + + flushSync(() => { + btn?.click(); + }); + assert.htmlEqual(target.innerHTML, ``); + + flushSync(() => { + btn?.click(); + }); + assert.htmlEqual(target.innerHTML, ``); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/class-state-constructor/main.svelte b/packages/svelte/tests/runtime-runes/samples/class-state-constructor/main.svelte new file mode 100644 index 0000000000..aa8ba1658b --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/class-state-constructor/main.svelte @@ -0,0 +1,18 @@ + + + diff --git a/packages/svelte/tests/validator/samples/class-state-constructor-1/errors.json b/packages/svelte/tests/validator/samples/class-state-constructor-1/errors.json new file mode 100644 index 0000000000..82765c51c1 --- /dev/null +++ b/packages/svelte/tests/validator/samples/class-state-constructor-1/errors.json @@ -0,0 +1,14 @@ +[ + { + "code": "state_field_duplicate", + "message": "`count` has already been declared on this class", + "start": { + "line": 5, + "column": 2 + }, + "end": { + "line": 5, + "column": 24 + } + } +] diff --git a/packages/svelte/tests/validator/samples/class-state-constructor-1/input.svelte.js b/packages/svelte/tests/validator/samples/class-state-constructor-1/input.svelte.js new file mode 100644 index 0000000000..05cd4d9d9d --- /dev/null +++ b/packages/svelte/tests/validator/samples/class-state-constructor-1/input.svelte.js @@ -0,0 +1,7 @@ +export class Counter { + count = $state(0); + + constructor() { + this.count = $state(0); + } +} diff --git a/packages/svelte/tests/validator/samples/class-state-constructor-10/errors.json b/packages/svelte/tests/validator/samples/class-state-constructor-10/errors.json new file mode 100644 index 0000000000..c4cb0991d0 --- /dev/null +++ b/packages/svelte/tests/validator/samples/class-state-constructor-10/errors.json @@ -0,0 +1,14 @@ +[ + { + "code": "state_field_invalid_assignment", + "message": "Cannot assign to a state field before its declaration", + "start": { + "line": 4, + "column": 3 + }, + "end": { + "line": 4, + "column": 18 + } + } +] diff --git a/packages/svelte/tests/validator/samples/class-state-constructor-10/input.svelte.js b/packages/svelte/tests/validator/samples/class-state-constructor-10/input.svelte.js new file mode 100644 index 0000000000..e5ad562727 --- /dev/null +++ b/packages/svelte/tests/validator/samples/class-state-constructor-10/input.svelte.js @@ -0,0 +1,9 @@ +export class Counter { + constructor() { + if (true) { + this.count = -1; + } + + this.count = $state(0); + } +} diff --git a/packages/svelte/tests/validator/samples/class-state-constructor-2/errors.json b/packages/svelte/tests/validator/samples/class-state-constructor-2/errors.json new file mode 100644 index 0000000000..82765c51c1 --- /dev/null +++ b/packages/svelte/tests/validator/samples/class-state-constructor-2/errors.json @@ -0,0 +1,14 @@ +[ + { + "code": "state_field_duplicate", + "message": "`count` has already been declared on this class", + "start": { + "line": 5, + "column": 2 + }, + "end": { + "line": 5, + "column": 24 + } + } +] diff --git a/packages/svelte/tests/validator/samples/class-state-constructor-2/input.svelte.js b/packages/svelte/tests/validator/samples/class-state-constructor-2/input.svelte.js new file mode 100644 index 0000000000..e37be4b3e6 --- /dev/null +++ b/packages/svelte/tests/validator/samples/class-state-constructor-2/input.svelte.js @@ -0,0 +1,7 @@ +export class Counter { + constructor() { + this.count = $state(0); + this.count = 1; + this.count = $state(0); + } +} diff --git a/packages/svelte/tests/validator/samples/class-state-constructor-3/errors.json b/packages/svelte/tests/validator/samples/class-state-constructor-3/errors.json new file mode 100644 index 0000000000..175c41f98c --- /dev/null +++ b/packages/svelte/tests/validator/samples/class-state-constructor-3/errors.json @@ -0,0 +1,14 @@ +[ + { + "code": "state_field_duplicate", + "message": "`count` has already been declared on this class", + "start": { + "line": 5, + "column": 2 + }, + "end": { + "line": 5, + "column": 28 + } + } +] diff --git a/packages/svelte/tests/validator/samples/class-state-constructor-3/input.svelte.js b/packages/svelte/tests/validator/samples/class-state-constructor-3/input.svelte.js new file mode 100644 index 0000000000..f9196ff3cd --- /dev/null +++ b/packages/svelte/tests/validator/samples/class-state-constructor-3/input.svelte.js @@ -0,0 +1,7 @@ +export class Counter { + constructor() { + this.count = $state(0); + this.count = 1; + this.count = $state.raw(0); + } +} diff --git a/packages/svelte/tests/validator/samples/class-state-constructor-4/errors.json b/packages/svelte/tests/validator/samples/class-state-constructor-4/errors.json new file mode 100644 index 0000000000..9f959874c8 --- /dev/null +++ b/packages/svelte/tests/validator/samples/class-state-constructor-4/errors.json @@ -0,0 +1,14 @@ +[ + { + "code": "state_invalid_placement", + "message": "`$state(...)` 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.", + "start": { + "line": 4, + "column": 16 + }, + "end": { + "line": 4, + "column": 25 + } + } +] diff --git a/packages/svelte/tests/validator/samples/class-state-constructor-4/input.svelte.js b/packages/svelte/tests/validator/samples/class-state-constructor-4/input.svelte.js new file mode 100644 index 0000000000..bf1aada1b5 --- /dev/null +++ b/packages/svelte/tests/validator/samples/class-state-constructor-4/input.svelte.js @@ -0,0 +1,7 @@ +export class Counter { + constructor() { + if (true) { + this.count = $state(0); + } + } +} diff --git a/packages/svelte/tests/validator/samples/class-state-constructor-5/errors.json b/packages/svelte/tests/validator/samples/class-state-constructor-5/errors.json new file mode 100644 index 0000000000..af2f30dade --- /dev/null +++ b/packages/svelte/tests/validator/samples/class-state-constructor-5/errors.json @@ -0,0 +1,14 @@ +[ + { + "code": "state_field_duplicate", + "message": "`count` has already been declared on this class", + "start": { + "line": 5, + "column": 2 + }, + "end": { + "line": 5, + "column": 27 + } + } +] diff --git a/packages/svelte/tests/validator/samples/class-state-constructor-5/input.svelte.js b/packages/svelte/tests/validator/samples/class-state-constructor-5/input.svelte.js new file mode 100644 index 0000000000..bc3d19a14f --- /dev/null +++ b/packages/svelte/tests/validator/samples/class-state-constructor-5/input.svelte.js @@ -0,0 +1,7 @@ +export class Counter { + // prettier-ignore + 'count' = $state(0); + constructor() { + this['count'] = $state(0); + } +} diff --git a/packages/svelte/tests/validator/samples/class-state-constructor-6/errors.json b/packages/svelte/tests/validator/samples/class-state-constructor-6/errors.json new file mode 100644 index 0000000000..ae7a47f31b --- /dev/null +++ b/packages/svelte/tests/validator/samples/class-state-constructor-6/errors.json @@ -0,0 +1,14 @@ +[ + { + "code": "state_field_duplicate", + "message": "`count` has already been declared on this class", + "start": { + "line": 4, + "column": 2 + }, + "end": { + "line": 4, + "column": 27 + } + } +] diff --git a/packages/svelte/tests/validator/samples/class-state-constructor-6/input.svelte.js b/packages/svelte/tests/validator/samples/class-state-constructor-6/input.svelte.js new file mode 100644 index 0000000000..2ebe52e685 --- /dev/null +++ b/packages/svelte/tests/validator/samples/class-state-constructor-6/input.svelte.js @@ -0,0 +1,6 @@ +export class Counter { + count = $state(0); + constructor() { + this['count'] = $state(0); + } +} diff --git a/packages/svelte/tests/validator/samples/class-state-constructor-7/errors.json b/packages/svelte/tests/validator/samples/class-state-constructor-7/errors.json new file mode 100644 index 0000000000..64e56f8d5c --- /dev/null +++ b/packages/svelte/tests/validator/samples/class-state-constructor-7/errors.json @@ -0,0 +1,14 @@ +[ + { + "code": "state_invalid_placement", + "message": "`$state(...)` 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.", + "start": { + "line": 5, + "column": 16 + }, + "end": { + "line": 5, + "column": 25 + } + } +] diff --git a/packages/svelte/tests/validator/samples/class-state-constructor-7/input.svelte.js b/packages/svelte/tests/validator/samples/class-state-constructor-7/input.svelte.js new file mode 100644 index 0000000000..50c8559837 --- /dev/null +++ b/packages/svelte/tests/validator/samples/class-state-constructor-7/input.svelte.js @@ -0,0 +1,7 @@ +const count = 'count'; + +export class Counter { + constructor() { + this[count] = $state(0); + } +} diff --git a/packages/svelte/tests/validator/samples/class-state-constructor-8/errors.json b/packages/svelte/tests/validator/samples/class-state-constructor-8/errors.json new file mode 100644 index 0000000000..2e0bd10ff8 --- /dev/null +++ b/packages/svelte/tests/validator/samples/class-state-constructor-8/errors.json @@ -0,0 +1,14 @@ +[ + { + "code": "state_field_invalid_assignment", + "message": "Cannot assign to a state field before its declaration", + "start": { + "line": 3, + "column": 2 + }, + "end": { + "line": 3, + "column": 17 + } + } +] diff --git a/packages/svelte/tests/validator/samples/class-state-constructor-8/input.svelte.js b/packages/svelte/tests/validator/samples/class-state-constructor-8/input.svelte.js new file mode 100644 index 0000000000..0a76c6fec9 --- /dev/null +++ b/packages/svelte/tests/validator/samples/class-state-constructor-8/input.svelte.js @@ -0,0 +1,6 @@ +export class Counter { + constructor() { + this.count = -1; + this.count = $state(0); + } +} diff --git a/packages/svelte/tests/validator/samples/class-state-constructor-9/errors.json b/packages/svelte/tests/validator/samples/class-state-constructor-9/errors.json new file mode 100644 index 0000000000..b7dd4c8ed4 --- /dev/null +++ b/packages/svelte/tests/validator/samples/class-state-constructor-9/errors.json @@ -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 + } + } +] diff --git a/packages/svelte/tests/validator/samples/class-state-constructor-9/input.svelte.js b/packages/svelte/tests/validator/samples/class-state-constructor-9/input.svelte.js new file mode 100644 index 0000000000..a8469e13af --- /dev/null +++ b/packages/svelte/tests/validator/samples/class-state-constructor-9/input.svelte.js @@ -0,0 +1,7 @@ +export class Counter { + count = -1; + + constructor() { + this.count = $state(0); + } +} diff --git a/packages/svelte/tests/validator/samples/const-tag-invalid-rune-usage/errors.json b/packages/svelte/tests/validator/samples/const-tag-invalid-rune-usage/errors.json index 32594e4268..e1906b181a 100644 --- a/packages/svelte/tests/validator/samples/const-tag-invalid-rune-usage/errors.json +++ b/packages/svelte/tests/validator/samples/const-tag-invalid-rune-usage/errors.json @@ -1,7 +1,7 @@ [ { "code": "state_invalid_placement", - "message": "`$derived(...)` can only be used as a variable declaration initializer or a class field", + "message": "`$derived(...)` 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.", "start": { "line": 2, "column": 15