diff --git a/.changeset/odd-phones-taste.md b/.changeset/odd-phones-taste.md new file mode 100644 index 000000000000..ec9534b74157 --- /dev/null +++ b/.changeset/odd-phones-taste.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: throw on duplicate class field declarations diff --git a/documentation/docs/98-reference/.generated/compile-errors.md b/documentation/docs/98-reference/.generated/compile-errors.md index 20f57770d122..957a9f67c7b0 100644 --- a/documentation/docs/98-reference/.generated/compile-errors.md +++ b/documentation/docs/98-reference/.generated/compile-errors.md @@ -364,6 +364,12 @@ The $ name is reserved, and cannot be used for variables and imports The $ prefix is reserved, and cannot be used for variables and imports ``` +### duplicate_class_field + +``` +`%name%` has already been declared +``` + ### each_item_invalid_assignment ``` diff --git a/packages/svelte/messages/compile-errors/script.md b/packages/svelte/messages/compile-errors/script.md index 2b0c5eafdf86..5c1080acedfe 100644 --- a/packages/svelte/messages/compile-errors/script.md +++ b/packages/svelte/messages/compile-errors/script.md @@ -30,6 +30,10 @@ > The $ prefix is reserved, and cannot be used for variables and imports +## duplicate_class_field + +> `%name%` has already been declared + ## each_item_invalid_assignment > Cannot reassign or bind to each block argument in runes mode. Use the array and index variables instead (e.g. `array[i] = value` instead of `entry = value`, or `bind:value={array[i]}` instead of `bind:value={entry}`) diff --git a/packages/svelte/src/compiler/errors.js b/packages/svelte/src/compiler/errors.js index 599d3e8248b0..e763a6e0733a 100644 --- a/packages/svelte/src/compiler/errors.js +++ b/packages/svelte/src/compiler/errors.js @@ -152,6 +152,16 @@ export function dollar_prefix_invalid(node) { e(node, 'dollar_prefix_invalid', `The $ prefix is reserved, and cannot be used for variables and imports\nhttps://svelte.dev/e/dollar_prefix_invalid`); } +/** + * `%name%` has already been declared + * @param {null | number | NodeLike} node + * @param {string} name + * @returns {never} + */ +export function duplicate_class_field(node, name) { + e(node, 'duplicate_class_field', `\`${name}\` has already been declared\nhttps://svelte.dev/e/duplicate_class_field`); +} + /** * Cannot reassign or bind to each block argument in runes mode. Use the array and index variables instead (e.g. `array[i] = value` instead of `entry = value`, or `bind:value={array[i]}` instead of `bind:value={entry}`) * @param {null | number | NodeLike} node diff --git a/packages/svelte/src/compiler/phases/2-analyze/visitors/ClassBody.js b/packages/svelte/src/compiler/phases/2-analyze/visitors/ClassBody.js index ffc39ac00de1..2bfc1dbce36c 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/visitors/ClassBody.js +++ b/packages/svelte/src/compiler/phases/2-analyze/visitors/ClassBody.js @@ -33,6 +33,9 @@ export function ClassBody(node, context) { /** @type {Map} */ const state_fields = new Map(); + /** @type {Map>} */ + const fields = new Map(); + context.state.analysis.classes.set(node, state_fields); /** @type {MethodDefinition | null} */ @@ -54,6 +57,14 @@ export function ClassBody(node, context) { e.state_field_duplicate(node, name); } + const _key = (key.type === 'PrivateIdentifier' ? '#' : '') + name; + const field = fields.get(_key); + + // if there's already a method or assigned field, error + if (field && !(field.length === 1 && field[0] === 'prop')) { + e.duplicate_class_field(node, _key); + } + state_fields.set(name, { node, type: rune, @@ -67,10 +78,48 @@ export function ClassBody(node, context) { for (const child of node.body) { if (child.type === 'PropertyDefinition' && !child.computed && !child.static) { handle(child, child.key, child.value); + const key = (child.key.type === 'PrivateIdentifier' ? '#' : '') + get_name(child.key); + const field = fields.get(key); + if (!field) { + fields.set(key, [child.value ? 'assigned_prop' : 'prop']); + continue; + } + e.duplicate_class_field(child, key); } - if (child.type === 'MethodDefinition' && child.kind === 'constructor') { - constructor = child; + if (child.type === 'MethodDefinition') { + if (child.kind === 'constructor') { + constructor = child; + } else if (!child.computed) { + const key = (child.key.type === 'PrivateIdentifier' ? '#' : '') + get_name(child.key); + const field = fields.get(key); + if (!field) { + fields.set(key, [child.kind]); + continue; + } + if ( + field.includes(child.kind) || + field.includes('prop') || + field.includes('assigned_prop') + ) { + e.duplicate_class_field(child, key); + } + if (child.kind === 'get') { + if (field.length === 1 && field[0] === 'set') { + field.push('get'); + continue; + } + } else if (child.kind === 'set') { + if (field.length === 1 && field[0] === 'get') { + field.push('set'); + continue; + } + } else { + field.push(child.kind); + continue; + } + e.duplicate_class_field(child, key); + } } } 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 index b7dd4c8ed457..94b5f191c2f7 100644 --- a/packages/svelte/tests/validator/samples/class-state-constructor-9/errors.json +++ b/packages/svelte/tests/validator/samples/class-state-constructor-9/errors.json @@ -1,14 +1,14 @@ [ { - "code": "state_field_invalid_assignment", - "message": "Cannot assign to a state field before its declaration", + "code": "duplicate_class_field", + "message": "`count` has already been declared", "start": { - "line": 2, - "column": 1 + "line": 5, + "column": 2 }, "end": { - "line": 2, - "column": 12 + "line": 5, + "column": 24 } } ]