Skip to content

Commit ef1d791

Browse files
authored
(feat) infer slot/events type from props (#952)
This changes the generation for accessing the events/slots of a component. Instead of using `instanceOf`, the component instance is created using the constructor and passing the props. This enables TypeScript to infer generic relationships between props and slots/events. #945 Note that this change does not cover all cases: If someone forwards a slot or an event whose type is generic and needs to be infered, that type will remain unknown, because reinstantiating the context in the `return { props: .. }` step is really hard.
1 parent 3150991 commit ef1d791

File tree

38 files changed

+449
-75
lines changed

38 files changed

+449
-75
lines changed

packages/language-server/src/plugins/typescript/features/utils.ts

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -61,10 +61,9 @@ export async function getComponentAtPosition(
6161
export function isInGeneratedCode(text: string, start: number, end: number) {
6262
const lineStart = text.lastIndexOf('\n', start);
6363
const lineEnd = text.indexOf('\n', end);
64-
return (
65-
text.substring(lineStart, start).includes('/*Ωignore_startΩ*/') &&
66-
text.substring(end, lineEnd).includes('/*Ωignore_endΩ*/')
67-
);
64+
const lastStart = text.substring(lineStart, start).lastIndexOf('/*Ωignore_startΩ*/');
65+
const lastEnd = text.substring(lineStart, start).lastIndexOf('/*Ωignore_endΩ*/');
66+
return lastStart > lastEnd && text.substring(end, lineEnd).includes('/*Ωignore_endΩ*/');
6867
}
6968

7069
/**

packages/language-server/test/plugins/typescript/features/RenameProvider.test.ts

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,8 @@ describe('RenameProvider', () => {
3636
const renameDoc5 = await openDoc('rename5.svelte');
3737
const renameDoc6 = await openDoc('rename6.svelte');
3838
const renameDocIgnoreGenerated = await openDoc('rename-ignore-generated.svelte');
39+
const renameDocSlotEventsImporter = await openDoc('rename-slot-events-importer.svelte');
40+
const renameDocPropWithSlotEvents = await openDoc('rename-prop-with-slot-events.svelte');
3941
return {
4042
provider,
4143
renameDoc1,
@@ -45,6 +47,8 @@ describe('RenameProvider', () => {
4547
renameDoc5,
4648
renameDoc6,
4749
renameDocIgnoreGenerated,
50+
renameDocSlotEventsImporter,
51+
renameDocPropWithSlotEvents,
4852
docManager
4953
};
5054

@@ -526,4 +530,61 @@ describe('RenameProvider', () => {
526530
}
527531
});
528532
});
533+
534+
it('rename prop correctly when events/slots present', async () => {
535+
const { provider, renameDocPropWithSlotEvents } = await setup();
536+
const result = await provider.rename(
537+
renameDocPropWithSlotEvents,
538+
Position.create(3, 15),
539+
'newName'
540+
);
541+
542+
assert.deepStrictEqual(result, {
543+
changes: {
544+
[getUri('rename-prop-with-slot-events.svelte')]: [
545+
{
546+
newText: 'newName',
547+
range: {
548+
end: {
549+
character: 17,
550+
line: 3
551+
},
552+
start: {
553+
character: 13,
554+
line: 3
555+
}
556+
}
557+
},
558+
{
559+
newText: 'newName',
560+
range: {
561+
end: {
562+
character: 17,
563+
line: 8
564+
},
565+
start: {
566+
character: 13,
567+
line: 8
568+
}
569+
}
570+
}
571+
],
572+
[getUri('rename-slot-events-importer.svelte')]: [
573+
{
574+
newText: 'newName',
575+
range: {
576+
end: {
577+
character: 7,
578+
line: 4
579+
},
580+
start: {
581+
character: 3,
582+
line: 4
583+
}
584+
}
585+
}
586+
]
587+
}
588+
});
589+
});
529590
});
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
<script>
2+
import { createEventDispatcher } from 'svelte';
3+
4+
export let prop;
5+
const dispatch = createEventDispatcher();
6+
</script>
7+
8+
<button on:click={() => dispatch('foo')}>click</button>
9+
<slot aSlot={prop} />
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
<script>
2+
import A from './rename-prop-with-slot-events.svelte';
3+
</script>
4+
5+
<A prop={1} on:click={e => e} let:aSlot>{aSlot}</A>

packages/svelte2tsx/src/htmlxtojsx/nodes/await.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ export function handleAwait(
1919
let ifCondition = ifScope.getFullCondition();
2020
ifCondition = ifCondition ? surroundWithIgnoreComments(`if(${ifCondition}) {`) : '';
2121
templateScopeManager.awaitEnter(awaitBlock);
22-
const constRedeclares = ifScope.getConstsToRedeclare();
22+
const constRedeclares = ifScope.getConstDeclaration();
2323
str.overwrite(
2424
awaitBlock.start,
2525
awaitBlock.expression.start,

packages/svelte2tsx/src/htmlxtojsx/nodes/each.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ export function handleEach(
1313
): void {
1414
// {#each items as item,i (key)} ->
1515
// {__sveltets_each(items, (item,i) => (key) && (possible if expression &&) <>
16-
const constRedeclares = ifScope.getConstsToRedeclare();
16+
const constRedeclares = ifScope.getConstDeclaration();
1717
const prefix = constRedeclares ? `{() => {${constRedeclares}() => ` : '';
1818
str.overwrite(eachBlock.start, eachBlock.expression.start, `${prefix}{__sveltets_each(`);
1919
str.overwrite(eachBlock.expression.end, eachBlock.context.start, ', (');

packages/svelte2tsx/src/htmlxtojsx/nodes/event-handler.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import MagicString from 'magic-string';
2-
import { getTypeForComponent, isQuote } from '../utils/node-utils';
2+
import { getInstanceType, isQuote } from '../utils/node-utils';
33
import { BaseDirective, BaseNode } from '../../interfaces';
44

55
/**
@@ -36,7 +36,7 @@ export function handleEventHandler(
3636
if (attr.expression) {
3737
const on = 'on';
3838
//for handler assignment, we change it to call to our __sveltets_ensureFunction
39-
str.appendRight(attr.start, `{__sveltets_instanceOf(${getTypeForComponent(parent)}).$`);
39+
str.appendRight(attr.start, `{${getInstanceType(parent, str.original)}.$`);
4040
const eventNameIndex = htmlx.indexOf(':', attr.start) + 1;
4141
str.overwrite(htmlx.indexOf(on, attr.start) + on.length, eventNameIndex, "('");
4242
const eventEnd = htmlx.lastIndexOf('=', attr.expression.start);

packages/svelte2tsx/src/htmlxtojsx/nodes/if-scope.ts

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -323,13 +323,21 @@ export class IfScope {
323323
* in the conditions which would be overwritten by the scope
324324
* (because they declare a variable with the same name, therefore shadowing the outer variable).
325325
*/
326-
getConstsToRedeclare(): string {
327-
const replacements = this.getNamesToRedeclare()
328-
.map((identifier) => `${this.replacementPrefix + identifier}=${identifier}`)
329-
.join(',');
326+
getConstDeclaration(): string {
327+
const replacements = this.getConstsToRedeclare().join(',');
330328
return replacements ? surroundWithIgnoreComments(`const ${replacements};`) : '';
331329
}
332330

331+
/**
332+
* Like `getConstsRedaclarationString`, but only returns a list of redaclaration-string without
333+
* merging the result with `const` and a ignore comments surround.
334+
*/
335+
getConstsToRedeclare(): string[] {
336+
return this.getNamesToRedeclare().map(
337+
(identifier) => `${this.replacementPrefix + identifier}=${identifier}`
338+
);
339+
}
340+
333341
/**
334342
* Returns true if given identifier is referenced in this IfScope or a parent scope.
335343
*/
@@ -353,7 +361,7 @@ export class IfScope {
353361
/**
354362
* Contains a list of identifiers which would be overwritten by the child template scope.
355363
*/
356-
private getNamesToRedeclare() {
364+
getNamesToRedeclare() {
357365
return [...this.scope.value.inits.keys()].filter((init) => {
358366
let parent = this.scope.value.parent;
359367
while (parent && parent !== this.ownScope) {

packages/svelte2tsx/src/htmlxtojsx/nodes/slot.ts

Lines changed: 64 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,27 @@
11
import MagicString from 'magic-string';
2-
import { beforeStart } from '../utils/node-utils';
3-
import { getSingleSlotDef } from '../../svelte2tsx/nodes/slot';
2+
import {
3+
beforeStart,
4+
getInstanceType,
5+
getInstanceTypeForDefaultSlot,
6+
PropsShadowedByLet
7+
} from '../utils/node-utils';
48
import { IfScope } from './if-scope';
59
import { TemplateScope } from '../nodes/template-scope';
610
import { BaseNode } from '../../interfaces';
11+
import { surroundWithIgnoreComments } from '../../utils/ignore';
12+
13+
const shadowedPropsSymbol = Symbol('shadowedProps');
14+
15+
interface ComponentNode extends BaseNode {
16+
// Not pretty, but it works, and because it's a symbol, estree-walker will ignore it
17+
[shadowedPropsSymbol]?: PropsShadowedByLet[];
18+
}
719

820
export function handleSlot(
921
htmlx: string,
1022
str: MagicString,
1123
slotEl: BaseNode,
12-
component: BaseNode,
24+
component: ComponentNode,
1325
slotName: string,
1426
ifScope: IfScope,
1527
templateScope: TemplateScope
@@ -57,16 +69,63 @@ export function handleSlot(
5769
return;
5870
}
5971

60-
const constRedeclares = ifScope.getConstsToRedeclare();
72+
const { singleSlotDef, constRedeclares } = getSingleSlotDefAndConstsRedeclaration(
73+
component,
74+
slotName,
75+
str.original,
76+
ifScope,
77+
slotElIsComponent
78+
);
6179
const prefix = constRedeclares ? `() => {${constRedeclares}` : '';
6280
str.appendLeft(slotDefInsertionPoint, `{${prefix}() => { let {`);
6381
str.appendRight(
6482
slotDefInsertionPoint,
65-
`} = ${getSingleSlotDef(component, slotName)}` + `;${ifScope.addPossibleIfCondition()}<>`
83+
`} = ${singleSlotDef}` + `;${ifScope.addPossibleIfCondition()}<>`
6684
);
6785

6886
const closeSlotDefInsertionPoint = slotElIsComponent
6987
? htmlx.lastIndexOf('<', slotEl.end - 1)
7088
: slotEl.end;
7189
str.appendLeft(closeSlotDefInsertionPoint, `</>}}${constRedeclares ? '}' : ''}`);
7290
}
91+
92+
function getSingleSlotDefAndConstsRedeclaration(
93+
componentNode: ComponentNode,
94+
slotName: string,
95+
originalStr: string,
96+
ifScope: IfScope,
97+
findAndRedeclareShadowedProps: boolean
98+
) {
99+
if (findAndRedeclareShadowedProps) {
100+
const replacement = 'Ψ';
101+
const { str, shadowedProps } = getInstanceTypeForDefaultSlot(
102+
componentNode,
103+
originalStr,
104+
replacement
105+
);
106+
componentNode[shadowedPropsSymbol] = shadowedProps;
107+
return {
108+
singleSlotDef: `${str}.$$slot_def['${slotName}']`,
109+
constRedeclares: getConstsToRedeclare(ifScope, shadowedProps)
110+
};
111+
} else {
112+
const str = getInstanceType(
113+
componentNode,
114+
originalStr,
115+
componentNode[shadowedPropsSymbol] || []
116+
);
117+
return {
118+
singleSlotDef: `${str}.$$slot_def['${slotName}']`,
119+
constRedeclares: ifScope.getConstDeclaration()
120+
};
121+
}
122+
}
123+
124+
function getConstsToRedeclare(ifScope: IfScope, shadowedProps: PropsShadowedByLet[]) {
125+
const ifScopeRedeclarations = ifScope.getConstsToRedeclare();
126+
const letRedeclarations = shadowedProps.map(
127+
({ value, replacement }) => `${replacement}=${value}`
128+
);
129+
const replacements = [...ifScopeRedeclarations, ...letRedeclarations].join(',');
130+
return replacements ? surroundWithIgnoreComments(`const ${replacements};`) : '';
131+
}

0 commit comments

Comments
 (0)