Skip to content

Commit fba28b2

Browse files
authored
fix: detect shadowed variables/types during type hoisting (#2590)
#2589
1 parent bf2e459 commit fba28b2

File tree

9 files changed

+158
-11
lines changed

9 files changed

+158
-11
lines changed

packages/svelte2tsx/src/svelte2tsx/nodes/Generics.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,10 @@ import { surroundWithIgnoreComments } from '../../utils/ignore';
55
import { throwError } from '../utils/error';
66

77
export class Generics {
8+
/** The whole `T extends boolean` */
89
private definitions: string[] = [];
910
private typeReferences: string[] = [];
11+
/** The `T` in `T extends boolean` */
1012
private references: string[] = [];
1113
genericsAttr: Node | undefined;
1214

@@ -93,6 +95,10 @@ export class Generics {
9395
return this.typeReferences;
9496
}
9597

98+
getReferences() {
99+
return this.references;
100+
}
101+
96102
toDefinitionString(addIgnore = false) {
97103
const surround = addIgnore ? surroundWithIgnoreComments : (str: string) => str;
98104
return this.definitions.length ? surround(`<${this.definitions.join(',')}>`) : '';

packages/svelte2tsx/src/svelte2tsx/nodes/HoistableInterfaces.ts

Lines changed: 58 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -167,11 +167,16 @@ export class HoistableInterfaces {
167167
}
168168
});
169169

170-
this.interface_map.set(interface_name, {
171-
type_deps: type_dependencies,
172-
value_deps: value_dependencies,
173-
node
174-
});
170+
if (this.import_type_set.has(interface_name)) {
171+
// shadowed; delete because we can't hoist
172+
this.import_type_set.delete(interface_name);
173+
} else {
174+
this.interface_map.set(interface_name, {
175+
type_deps: type_dependencies,
176+
value_deps: value_dependencies,
177+
node
178+
});
179+
}
175180
}
176181

177182
// Handle Type Alias Declarations
@@ -188,12 +193,46 @@ export class HoistableInterfaces {
188193
generics
189194
);
190195

191-
this.interface_map.set(alias_name, {
192-
type_deps: type_dependencies,
193-
value_deps: value_dependencies,
194-
node
196+
if (this.import_type_set.has(alias_name)) {
197+
// shadowed; delete because we can't hoist
198+
this.import_type_set.delete(alias_name);
199+
} else {
200+
this.interface_map.set(alias_name, {
201+
type_deps: type_dependencies,
202+
value_deps: value_dependencies,
203+
node
204+
});
205+
}
206+
}
207+
208+
// Handle top-level declarations: They could shadow module declarations; delete them from the set of allowed import values
209+
if (ts.isVariableStatement(node)) {
210+
node.declarationList.declarations.forEach((declaration) => {
211+
if (ts.isIdentifier(declaration.name)) {
212+
this.import_value_set.delete(declaration.name.text);
213+
}
195214
});
196215
}
216+
217+
if (ts.isFunctionDeclaration(node) && node.name) {
218+
this.import_value_set.delete(node.name.text);
219+
}
220+
221+
if (ts.isClassDeclaration(node) && node.name) {
222+
this.import_value_set.delete(node.name.text);
223+
}
224+
225+
if (ts.isEnumDeclaration(node)) {
226+
this.import_value_set.delete(node.name.text);
227+
}
228+
229+
if (ts.isTypeAliasDeclaration(node)) {
230+
this.import_type_set.delete(node.name.text);
231+
}
232+
233+
if (ts.isInterfaceDeclaration(node)) {
234+
this.import_type_set.delete(node.name.text);
235+
}
197236
}
198237

199238
analyze$propsRune(
@@ -280,9 +319,18 @@ export class HoistableInterfaces {
280319
/**
281320
* Moves all interfaces that can be hoisted to the top of the script, if the $props rune's type is hoistable.
282321
*/
283-
moveHoistableInterfaces(str: MagicString, astOffset: number, scriptStart: number) {
322+
moveHoistableInterfaces(
323+
str: MagicString,
324+
astOffset: number,
325+
scriptStart: number,
326+
generics: string[]
327+
) {
284328
if (!this.props_interface.name) return;
285329

330+
for (const generic of generics) {
331+
this.import_type_set.delete(generic);
332+
}
333+
286334
const hoistable = this.determineHoistableInterfaces();
287335
if (hoistable.has(this.props_interface.name)) {
288336
for (const [, node] of hoistable) {

packages/svelte2tsx/src/svelte2tsx/processInstanceScriptContent.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -318,7 +318,12 @@ export function processInstanceScriptContent(
318318
transformInterfacesToTypes(tsAst, str, astOffset, nodesToMove);
319319
}
320320

321-
exportedNames.hoistableInterfaces.moveHoistableInterfaces(str, astOffset, script.start);
321+
exportedNames.hoistableInterfaces.moveHoistableInterfaces(
322+
str,
323+
astOffset,
324+
script.start,
325+
generics.getReferences()
326+
);
322327

323328
return {
324329
exportedNames,
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
///<reference types="svelte" />
2+
;
3+
type SomeType<T extends boolean> = T;
4+
type T = unknown;
5+
;;function render<T extends boolean>() {
6+
;type $$ComponentProps = { someProp: SomeType<T>; };
7+
let { someProp }:/*Ωignore_startΩ*/$$ComponentProps/*Ωignore_endΩ*/ = $props();
8+
;
9+
async () => {
10+
11+
};
12+
return { props: {} as any as $$ComponentProps, exports: {}, bindings: __sveltets_$$bindings(''), slots: {}, events: {} }}
13+
class __sveltets_Render<T extends boolean> {
14+
props() {
15+
return render<T>().props;
16+
}
17+
events() {
18+
return render<T>().events;
19+
}
20+
slots() {
21+
return render<T>().slots;
22+
}
23+
bindings() { return __sveltets_$$bindings(''); }
24+
exports() { return {}; }
25+
}
26+
27+
interface $$IsomorphicComponent {
28+
new <T extends boolean>(options: import('svelte').ComponentConstructorOptions<ReturnType<__sveltets_Render<T>['props']>>): import('svelte').SvelteComponent<ReturnType<__sveltets_Render<T>['props']>, ReturnType<__sveltets_Render<T>['events']>, ReturnType<__sveltets_Render<T>['slots']>> & { $$bindings?: ReturnType<__sveltets_Render<T>['bindings']> } & ReturnType<__sveltets_Render<T>['exports']>;
29+
<T extends boolean>(internal: unknown, props: ReturnType<__sveltets_Render<T>['props']> & {}): ReturnType<__sveltets_Render<T>['exports']>;
30+
z_$$bindings?: ReturnType<__sveltets_Render<any>['bindings']>;
31+
}
32+
const Input__SvelteComponent_: $$IsomorphicComponent = null as any;
33+
/*Ωignore_startΩ*/type Input__SvelteComponent_<T extends boolean> = InstanceType<typeof Input__SvelteComponent_<T>>;
34+
/*Ωignore_endΩ*/export default Input__SvelteComponent_;
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
<script lang="ts" module>
2+
type SomeType<T extends boolean> = T;
3+
type T = unknown;
4+
</script>
5+
6+
<script lang="ts" generics="T extends boolean">
7+
let { someProp }: { someProp: SomeType<T>; } = $props();
8+
</script>
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
///<reference types="svelte" />
2+
;
3+
let a = '';
4+
;;function render() {
5+
6+
let a = true;;type $$ComponentProps = { someProp: typeof a };
7+
let { someProp }:/*Ωignore_startΩ*/$$ComponentProps/*Ωignore_endΩ*/ = $props();
8+
;
9+
async () => {
10+
11+
};
12+
return { props: {} as any as $$ComponentProps, exports: {}, bindings: __sveltets_$$bindings(''), slots: {}, events: {} }}
13+
const Input__SvelteComponent_ = __sveltets_2_fn_component(render());
14+
type Input__SvelteComponent_ = ReturnType<typeof Input__SvelteComponent_>;
15+
export default Input__SvelteComponent_;
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
<script lang="ts" module>
2+
let a = '';
3+
</script>
4+
5+
<script lang="ts">
6+
let a = true;
7+
let { someProp }: { someProp: typeof a } = $props();
8+
</script>
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
///<reference types="svelte" />
2+
;
3+
type Shadowed = string;
4+
;;function render() {
5+
6+
type Shadowed = boolean;;type $$ComponentProps = { someProp: Shadowed };
7+
let { someProp }:/*Ωignore_startΩ*/$$ComponentProps/*Ωignore_endΩ*/ = $props();
8+
;
9+
async () => {
10+
11+
};
12+
return { props: {} as any as $$ComponentProps, exports: {}, bindings: __sveltets_$$bindings(''), slots: {}, events: {} }}
13+
const Input__SvelteComponent_ = __sveltets_2_fn_component(render());
14+
type Input__SvelteComponent_ = ReturnType<typeof Input__SvelteComponent_>;
15+
export default Input__SvelteComponent_;
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
<script lang="ts" module>
2+
type Shadowed = string;
3+
</script>
4+
5+
<script lang="ts">
6+
type Shadowed = boolean;
7+
let { someProp }: { someProp: Shadowed } = $props();
8+
</script>

0 commit comments

Comments
 (0)