Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
5 changes: 5 additions & 0 deletions .changeset/gentle-toys-chew.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"svelte": patch
---

fix: allow to access private fields after `this` reassignment
12 changes: 12 additions & 0 deletions packages/svelte/src/compiler/phases/2-analyze/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -1503,6 +1503,18 @@ const common_visitors = {
return;
}
}
},
ThisExpression(node, { next, path }) {
const parent = path.at(-1);
if (parent?.type === 'MemberExpression' && parent.object === node) {
return;
}
const class_declaration = path.find((path_element) => path_element.type === 'ClassDeclaration');
if (class_declaration && class_declaration.type === 'ClassDeclaration') {
class_declaration.metadata = {
needs_private_getters: true
};
}
}
};

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,20 @@ export const global_visitors = {
return serialize_get_binding(node, state);
}
},
MemberExpression(node, { state, next }) {
MemberExpression(node, { state, next, path }) {
const class_declaration = path.find((path_element) => path_element.type === 'ClassDeclaration');
const needs_private_getters =
class_declaration?.type === 'ClassDeclaration' &&
!!class_declaration.metadata?.needs_private_getters;

if (node.object.type === 'ThisExpression') {
// rewrite `this.#foo` as `this.#foo.v` inside a constructor
if (node.property.type === 'PrivateIdentifier') {
const field = state.private_state.get(node.property.name);
if (field) {
if (field && needs_private_getters && state.in_constructor) {
return b.member(b.member(b.this, field.id), b.id('v'));
}
if (field && !needs_private_getters) {
return state.in_constructor ? b.member(node, b.id('v')) : b.call('$.get', node);
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,12 @@ import { regex_invalid_identifier_chars } from '../../../patterns.js';

/** @type {import('../types.js').ComponentVisitors} */
export const javascript_visitors_runes = {
ClassBody(node, { state, visit }) {
ClassBody(node, { state, visit, path }) {
const parent = path.at(-1);

const needs_private_getters =
parent?.type === 'ClassDeclaration' && !!parent.metadata?.needs_private_getters;

/** @type {Map<string, import('../types.js').StateField>} */
const public_state = new Map();

Expand Down Expand Up @@ -64,6 +69,19 @@ export const javascript_visitors_runes = {
}
}

if (needs_private_getters) {
// each `#foo = $state()` needs a backing `#_foo` field is the class needs private getters
for (const [name, field] of private_state) {
let deconflicted = name;
while (private_ids.includes(deconflicted)) {
deconflicted = '_' + deconflicted;
}

private_ids.push(deconflicted);
field.id = b.private_id(deconflicted);
}
}

// each `foo = $state()` needs a backing `#foo` field
for (const [name, field] of public_state) {
let deconflicted = name;
Expand Down Expand Up @@ -121,7 +139,7 @@ export const javascript_visitors_runes = {
value = b.call('$.source');
}

if (is_private) {
if (is_private && !needs_private_getters) {
body.push(b.prop_def(field.id, value));
} else {
// #foo;
Expand Down
6 changes: 6 additions & 0 deletions packages/svelte/src/compiler/phases/types.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -103,4 +103,10 @@ declare module 'estree' {
scope: Scope;
};
}

interface ClassDeclaration {
metadata?: {
needs_private_getters: boolean;
};
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { test } from '../../test';

export default test({
compileOptions: {
dev: true
},
async test({ assert, logs }) {
assert.deepEqual(logs, ['init', 1]);
}
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
<script>
class Counter {
#count = $state();

constructor(){
const instance = this;
instance.#count = 1;
}

get count(){
return this.#count;
}
}
const counter = new Counter();

$inspect(counter.count)
</script>