Skip to content

Commit 69336d0

Browse files
authored
(fix) move error if $$Props is an alias of another type (#1892)
also memoize some computations which don't need to be done everytime to share the result between branches #1523 and part of #1830
1 parent f45f2a8 commit 69336d0

File tree

18 files changed

+338
-26
lines changed

18 files changed

+338
-26
lines changed

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

Lines changed: 104 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import ts from 'typescript';
2-
import { CancellationToken, Diagnostic, DiagnosticSeverity } from 'vscode-languageserver';
2+
import { CancellationToken, Diagnostic, DiagnosticSeverity, Range } from 'vscode-languageserver';
33
import {
44
Document,
55
getNodeIfIsInStartTag,
@@ -21,7 +21,7 @@ import {
2121
isStoreVariableIn$storeDeclaration,
2222
get$storeOffsetOf$storeDeclaration
2323
} from './utils';
24-
import { not, flatten, passMap, regexIndexOf, swapRangeStartEndIfNecessary } from '../../../utils';
24+
import { not, flatten, passMap, swapRangeStartEndIfNecessary, memoize } from '../../../utils';
2525
import { LSConfigManager } from '../../../ls-config';
2626
import { isAttributeName, isEventHandler } from '../svelte-ast-utils';
2727

@@ -128,7 +128,7 @@ export class DiagnosticsProviderImpl implements DiagnosticsProvider {
128128
code: diagnostic.code,
129129
tags: getDiagnosticTag(diagnostic)
130130
}))
131-
.map(mapRange(tsDoc, document))
131+
.map(mapRange(tsDoc, document, lang))
132132
.filter(hasNoNegativeLines)
133133
.filter(isNoFalsePositive(document, tsDoc))
134134
.map(enhanceIfNecessary)
@@ -142,34 +142,25 @@ export class DiagnosticsProviderImpl implements DiagnosticsProvider {
142142

143143
function mapRange(
144144
snapshot: SvelteDocumentSnapshot,
145-
document: Document
145+
document: Document,
146+
lang: ts.LanguageService
146147
): (value: Diagnostic) => Diagnostic {
148+
const get$$PropsDefWithCache = memoize(() => get$$PropsDef(lang, snapshot));
149+
const get$$PropsAliasInfoWithCache = memoize(() =>
150+
get$$PropsAliasForInfo(get$$PropsDefWithCache, lang, document)
151+
);
152+
147153
return (diagnostic) => {
148154
let range = mapRangeToOriginal(snapshot, diagnostic.range);
149155

150156
if (range.start.line < 0) {
151-
const is$$PropsError =
152-
isAfterSvelte2TsxPropsReturn(
153-
snapshot.getFullText(),
154-
snapshot.offsetAt(diagnostic.range.start)
155-
) && diagnostic.message.includes('$$Props');
156-
157-
if (is$$PropsError) {
158-
const propsStart = regexIndexOf(
159-
document.getText(),
160-
/(interface|type)\s+\$\$Props[\s{=]/
161-
);
162-
163-
if (propsStart) {
164-
const start = document.positionAt(
165-
propsStart + document.getText().substring(propsStart).indexOf('$$Props')
166-
);
167-
range = {
168-
start,
169-
end: { ...start, character: start.character + '$$Props'.length }
170-
};
171-
}
172-
}
157+
range =
158+
movePropsErrorRangeBackIfNecessary(
159+
diagnostic,
160+
snapshot,
161+
get$$PropsDefWithCache,
162+
get$$PropsAliasInfoWithCache
163+
) ?? range;
173164
}
174165

175166
if (
@@ -416,3 +407,90 @@ function dedupDiagnostics() {
416407
}
417408
};
418409
}
410+
411+
function get$$PropsAliasForInfo(
412+
get$$PropsDefWithCache: () => ReturnType<typeof get$$PropsDef>,
413+
lang: ts.LanguageService,
414+
document: Document
415+
) {
416+
if (!/type\s+\$\$Props[\s\n]+=/.test(document.getText())) {
417+
return;
418+
}
419+
420+
const propsDef = get$$PropsDefWithCache();
421+
if (!propsDef || !ts.isTypeAliasDeclaration(propsDef)) {
422+
return;
423+
}
424+
425+
const type = lang.getProgram()?.getTypeChecker()?.getTypeAtLocation(propsDef.name);
426+
if (!type) {
427+
return;
428+
}
429+
430+
const rootSymbolName = (type.aliasSymbol ?? type.symbol).name;
431+
432+
return [rootSymbolName, propsDef] as const;
433+
}
434+
435+
function get$$PropsDef(lang: ts.LanguageService, snapshot: SvelteDocumentSnapshot) {
436+
const program = lang.getProgram();
437+
const sourceFile = program?.getSourceFile(snapshot.filePath);
438+
if (!program || !sourceFile) {
439+
return undefined;
440+
}
441+
442+
const renderFunction = sourceFile.statements.find(
443+
(statement): statement is ts.FunctionDeclaration =>
444+
ts.isFunctionDeclaration(statement) && statement.name?.getText() === 'render'
445+
);
446+
return renderFunction?.body?.statements.find(
447+
(node): node is ts.TypeAliasDeclaration | ts.InterfaceDeclaration =>
448+
(ts.isTypeAliasDeclaration(node) || ts.isInterfaceDeclaration(node)) &&
449+
node.name.getText() === '$$Props'
450+
);
451+
}
452+
453+
function movePropsErrorRangeBackIfNecessary(
454+
diagnostic: Diagnostic,
455+
snapshot: SvelteDocumentSnapshot,
456+
get$$PropsDefWithCache: () => ReturnType<typeof get$$PropsDef>,
457+
get$$PropsAliasForWithCache: () => ReturnType<typeof get$$PropsAliasForInfo>
458+
): Range | undefined {
459+
const possibly$$PropsError = isAfterSvelte2TsxPropsReturn(
460+
snapshot.getFullText(),
461+
snapshot.offsetAt(diagnostic.range.start)
462+
);
463+
if (!possibly$$PropsError) {
464+
return;
465+
}
466+
467+
if (diagnostic.message.includes('$$Props')) {
468+
const propsDef = get$$PropsDefWithCache();
469+
const generatedPropsStart = propsDef?.name.getStart();
470+
const propsStart =
471+
generatedPropsStart != null &&
472+
snapshot.getOriginalPosition(snapshot.positionAt(generatedPropsStart));
473+
474+
if (propsStart) {
475+
return {
476+
start: propsStart,
477+
end: { ...propsStart, character: propsStart.character + '$$Props'.length }
478+
};
479+
}
480+
481+
return;
482+
}
483+
484+
const aliasForInfo = get$$PropsAliasForWithCache();
485+
if (!aliasForInfo) {
486+
return;
487+
}
488+
489+
const [aliasFor, propsDef] = aliasForInfo;
490+
if (diagnostic.message.includes(aliasFor)) {
491+
return mapRangeToOriginal(snapshot, {
492+
start: snapshot.positionAt(propsDef.name.getStart()),
493+
end: snapshot.positionAt(propsDef.name.getEnd())
494+
});
495+
}
496+
}

packages/language-server/src/utils.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -321,3 +321,16 @@ export function createGetCanonicalFileName(
321321
function identity<T>(x: T) {
322322
return x;
323323
}
324+
325+
export function memoize<T>(callback: () => T): () => T {
326+
let value: T;
327+
let callbackInner: typeof callback | undefined = callback;
328+
329+
return () => {
330+
if (callbackInner) {
331+
value = callback();
332+
callbackInner = undefined;
333+
}
334+
return value;
335+
};
336+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
[
2+
{
3+
"range": {
4+
"start": {
5+
"line": 6,
6+
"character": 6
7+
},
8+
"end": {
9+
"line": 6,
10+
"character": 13
11+
}
12+
},
13+
"severity": 1,
14+
"source": "ts",
15+
"message": "Argument of type 'ItemData' is not assignable to parameter of type '{ x: any; y: string; }'.\n Types of property 'y' are incompatible.\n Type 'number' is not assignable to type 'string'.",
16+
"code": 2345,
17+
"tags": []
18+
}
19+
]
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
<script lang="ts">
2+
interface ItemData {
3+
x: number;
4+
y: number;
5+
}
6+
7+
type $$Props =
8+
ItemData;
9+
10+
export let x;
11+
export let y:string;
12+
</script>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
[
2+
{
3+
"range": {
4+
"start": {
5+
"line": 6,
6+
"character": 6
7+
},
8+
"end": {
9+
"line": 6,
10+
"character": 13
11+
}
12+
},
13+
"severity": 1,
14+
"source": "ts",
15+
"message": "Argument of type 'ItemData' is not assignable to parameter of type '{ x: any; y: string; }'.\n Types of property 'y' are incompatible.\n Type 'number' is not assignable to type 'string'.",
16+
"code": 2345,
17+
"tags": []
18+
}
19+
]
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
<script lang="ts">
2+
type ItemData = {
3+
x: number;
4+
y: number;
5+
}
6+
7+
type $$Props =
8+
ItemData;
9+
10+
export let x;
11+
export let y:string;
12+
</script>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
[
2+
{
3+
"range": {
4+
"start": {
5+
"line": 8,
6+
"character": 6
7+
},
8+
"end": {
9+
"line": 8,
10+
"character": 13
11+
}
12+
},
13+
"severity": 1,
14+
"source": "ts",
15+
"message": "Argument of type 'ItemData' is not assignable to parameter of type '{ x: any; y: string; }'.\n Types of property 'y' are incompatible.\n Type 'number' is not assignable to type 'string'.",
16+
"code": 2345,
17+
"tags": []
18+
}
19+
]
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
<script lang="ts">
2+
interface ItemData {
3+
x: number;
4+
y: number;
5+
}
6+
7+
type C = ItemData;
8+
9+
type $$Props = C;
10+
11+
export let x;
12+
export let y:string;
13+
</script>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
[
2+
{
3+
"range": {
4+
"start": {
5+
"line": 8,
6+
"character": 6
7+
},
8+
"end": {
9+
"line": 8,
10+
"character": 13
11+
}
12+
},
13+
"severity": 1,
14+
"source": "ts",
15+
"message": "Argument of type 'ItemData' is not assignable to parameter of type '{ x: any; y: string; }'.\n Types of property 'y' are incompatible.\n Type 'number' is not assignable to type 'string'.",
16+
"code": 2345,
17+
"tags": []
18+
}
19+
]
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
<script lang="ts">
2+
type ItemData = {
3+
x: number;
4+
y: number;
5+
}
6+
7+
type C = ItemData;
8+
9+
type $$Props = C;
10+
11+
export let x;
12+
export let y:string;
13+
</script>

0 commit comments

Comments
 (0)