Skip to content

Commit 52849e6

Browse files
authored
(fix) if conditions control flow (#846)
This adds two new scopes to svelte2tsx: IfScope, which keeps track of the if scope including nested ifs, and TemplateScope, which tracks all initialized variables at certain AST levels (await, each, let:slot).. These scopes are used to prepend a repeated if-condition check to all inner lambda functions that svelte2tsx needs to create for slot, each, await. This way, TypeScript's control flow can determine the type of checked variables inside these lambda functions and no longer loses that info. #619
1 parent 8f10ccb commit 52849e6

File tree

41 files changed

+2100
-188
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

41 files changed

+2100
-188
lines changed

packages/language-server/src/plugins/typescript/DocumentSnapshot.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,10 @@ export interface DocumentSnapshot extends ts.IScriptSnapshot {
5757
* in order to prevent memory leaks.
5858
*/
5959
destroyFragment(): void;
60+
/**
61+
* Convenience function for getText(0, getLength())
62+
*/
63+
getFullText(): string;
6064
}
6165

6266
/**
@@ -221,6 +225,10 @@ export class SvelteDocumentSnapshot implements DocumentSnapshot {
221225
return this.text.length;
222226
}
223227

228+
getFullText() {
229+
return this.text;
230+
}
231+
224232
getChangeRange() {
225233
return undefined;
226234
}
@@ -301,6 +309,10 @@ export class JSOrTSDocumentSnapshot
301309
return this.text.length;
302310
}
303311

312+
getFullText() {
313+
return this.text;
314+
}
315+
304316
getChangeRange() {
305317
return undefined;
306318
}

packages/language-server/src/plugins/typescript/TypeScriptPlugin.ts

Lines changed: 17 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ import {
2828
getTextInRange
2929
} from '../../lib/documents';
3030
import { LSConfigManager, LSTypescriptConfig } from '../../ls-config';
31-
import { pathToUrl } from '../../utils';
31+
import { isNotNullOrUndefined, pathToUrl } from '../../utils';
3232
import {
3333
AppCompletionItem,
3434
AppCompletionList,
@@ -49,7 +49,6 @@ import {
4949
SemanticTokensProvider,
5050
UpdateTsOrJsFile
5151
} from '../interfaces';
52-
import { SnapshotFragment } from './DocumentSnapshot';
5352
import { CodeActionsProviderImpl } from './features/CodeActionsProvider';
5453
import {
5554
CompletionEntryWithIdentifer,
@@ -67,6 +66,7 @@ import { SelectionRangeProviderImpl } from './features/SelectionRangeProvider';
6766
import { SignatureHelpProviderImpl } from './features/SignatureHelpProvider';
6867
import { SnapshotManager } from './SnapshotManager';
6968
import { SemanticTokensProviderImpl } from './features/SemanticTokensProvider';
69+
import { isNoTextSpanInGeneratedCode, SnapshotFragmentMap } from './features/utils';
7070

7171
export class TypeScriptPlugin
7272
implements
@@ -263,35 +263,35 @@ export class TypeScriptPlugin
263263
}
264264

265265
const { lang, tsDoc } = this.getLSAndTSDoc(document);
266-
const fragment = await tsDoc.getFragment();
266+
const mainFragment = await tsDoc.getFragment();
267267

268268
const defs = lang.getDefinitionAndBoundSpan(
269269
tsDoc.filePath,
270-
fragment.offsetAt(fragment.getGeneratedPosition(position))
270+
mainFragment.offsetAt(mainFragment.getGeneratedPosition(position))
271271
);
272272

273273
if (!defs || !defs.definitions) {
274274
return [];
275275
}
276276

277-
const docs = new Map<string, SnapshotFragment>([[tsDoc.filePath, fragment]]);
277+
const docs = new SnapshotFragmentMap(this.lsAndTsDocResolver);
278+
docs.set(tsDoc.filePath, { fragment: mainFragment, snapshot: tsDoc });
278279

279-
return await Promise.all(
280+
const result = await Promise.all(
280281
defs.definitions.map(async (def) => {
281-
let defDoc = docs.get(def.fileName);
282-
if (!defDoc) {
283-
defDoc = await this.getSnapshot(def.fileName).getFragment();
284-
docs.set(def.fileName, defDoc);
282+
const { fragment, snapshot } = await docs.retrieve(def.fileName);
283+
284+
if (isNoTextSpanInGeneratedCode(snapshot.getFullText(), def.textSpan)) {
285+
return LocationLink.create(
286+
pathToUrl(def.fileName),
287+
convertToLocationRange(fragment, def.textSpan),
288+
convertToLocationRange(fragment, def.textSpan),
289+
convertToLocationRange(mainFragment, defs.textSpan)
290+
);
285291
}
286-
287-
return LocationLink.create(
288-
pathToUrl(def.fileName),
289-
convertToLocationRange(defDoc, def.textSpan),
290-
convertToLocationRange(defDoc, def.textSpan),
291-
convertToLocationRange(fragment, defs.textSpan)
292-
);
293292
})
294293
);
294+
return result.filter(isNotNullOrUndefined);
295295
}
296296

297297
async prepareRename(document: Document, position: Position): Promise<Range | null> {
@@ -436,10 +436,6 @@ export class TypeScriptPlugin
436436
return this.lsAndTsDocResolver.getLSAndTSDoc(document);
437437
}
438438

439-
private getSnapshot(filePath: string, document?: Document) {
440-
return this.lsAndTsDocResolver.getSnapshot(filePath, document);
441-
}
442-
443439
/**
444440
*
445441
* @internal

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

Lines changed: 53 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -9,14 +9,15 @@ import {
99
WorkspaceEdit
1010
} from 'vscode-languageserver';
1111
import { Document, mapRangeToOriginal, isRangeInTag, isInTag } from '../../../lib/documents';
12-
import { pathToUrl, flatten } from '../../../utils';
12+
import { pathToUrl, flatten, isNotNullOrUndefined } from '../../../utils';
1313
import { CodeActionsProvider } from '../../interfaces';
1414
import { SnapshotFragment, SvelteSnapshotFragment } from '../DocumentSnapshot';
1515
import { LSAndTSDocResolver } from '../LSAndTSDocResolver';
1616
import { convertRange } from '../utils';
1717

1818
import ts from 'typescript';
1919
import { CompletionsProviderImpl } from './CompletionProvider';
20+
import { isNoTextSpanInGeneratedCode, SnapshotFragmentMap } from './utils';
2021

2122
interface RefactorArgs {
2223
type: 'refactor';
@@ -134,53 +135,65 @@ export class CodeActionsProviderImpl implements CodeActionsProvider {
134135
userPreferences
135136
);
136137

137-
const docs = new Map<string, SnapshotFragment>([[tsDoc.filePath, fragment]]);
138+
const docs = new SnapshotFragmentMap(this.lsAndTsDocResolver);
139+
docs.set(tsDoc.filePath, { fragment, snapshot: tsDoc });
140+
138141
return await Promise.all(
139142
codeFixes.map(async (fix) => {
140143
const documentChanges = await Promise.all(
141144
fix.changes.map(async (change) => {
142-
const doc =
143-
docs.get(change.fileName) ??
144-
(await this.getAndCacheCodeActionDoc(change, docs));
145+
const { snapshot, fragment } = await docs.retrieve(change.fileName);
145146
return TextDocumentEdit.create(
146147
VersionedTextDocumentIdentifier.create(pathToUrl(change.fileName), 0),
147-
change.textChanges.map((edit) => {
148-
if (
149-
fix.fixName === 'import' &&
150-
doc instanceof SvelteSnapshotFragment
151-
) {
152-
return this.completionProvider.codeActionChangeToTextEdit(
153-
document,
154-
doc,
155-
edit,
156-
true,
157-
isInTag(range.start, document.scriptInfo) ||
158-
isInTag(range.start, document.moduleScriptInfo)
159-
);
160-
}
161-
162-
let originalRange = mapRangeToOriginal(
163-
doc,
164-
convertRange(doc, edit.span)
165-
);
166-
if (fix.fixName === 'unusedIdentifier') {
167-
originalRange = this.checkRemoveImportCodeActionRange(
168-
edit,
169-
doc,
170-
originalRange
148+
change.textChanges
149+
.map((edit) => {
150+
if (
151+
fix.fixName === 'import' &&
152+
fragment instanceof SvelteSnapshotFragment
153+
) {
154+
return this.completionProvider.codeActionChangeToTextEdit(
155+
document,
156+
fragment,
157+
edit,
158+
true,
159+
isInTag(range.start, document.scriptInfo) ||
160+
isInTag(range.start, document.moduleScriptInfo)
161+
);
162+
}
163+
164+
if (
165+
!isNoTextSpanInGeneratedCode(
166+
snapshot.getFullText(),
167+
edit.span
168+
)
169+
) {
170+
return undefined;
171+
}
172+
173+
let originalRange = mapRangeToOriginal(
174+
fragment,
175+
convertRange(fragment, edit.span)
171176
);
172-
}
173177

174-
if (fix.fixName === 'fixMissingFunctionDeclaration') {
175-
originalRange = this.checkEndOfFileCodeInsert(
176-
originalRange,
177-
range,
178-
document
179-
);
180-
}
181-
182-
return TextEdit.replace(originalRange, edit.newText);
183-
})
178+
if (fix.fixName === 'unusedIdentifier') {
179+
originalRange = this.checkRemoveImportCodeActionRange(
180+
edit,
181+
fragment,
182+
originalRange
183+
);
184+
}
185+
186+
if (fix.fixName === 'fixMissingFunctionDeclaration') {
187+
originalRange = this.checkEndOfFileCodeInsert(
188+
originalRange,
189+
range,
190+
document
191+
);
192+
}
193+
194+
return TextEdit.replace(originalRange, edit.newText);
195+
})
196+
.filter(isNotNullOrUndefined)
184197
);
185198
})
186199
);
@@ -195,15 +208,6 @@ export class CodeActionsProviderImpl implements CodeActionsProvider {
195208
);
196209
}
197210

198-
private async getAndCacheCodeActionDoc(
199-
change: ts.FileTextChanges,
200-
cache: Map<string, SnapshotFragment>
201-
) {
202-
const doc = await this.getSnapshot(change.fileName).getFragment();
203-
cache.set(change.fileName, doc);
204-
return doc;
205-
}
206-
207211
private async getApplicableRefactors(document: Document, range: Range): Promise<CodeAction[]> {
208212
if (
209213
!isRangeInTag(range, document.scriptInfo) &&

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

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { DiagnosticsProvider } from '../../interfaces';
55
import { LSAndTSDocResolver } from '../LSAndTSDocResolver';
66
import { convertRange, mapSeverity } from '../utils';
77
import { SvelteDocumentSnapshot } from '../DocumentSnapshot';
8+
import { isInGeneratedCode } from './utils';
89

910
export class DiagnosticsProviderImpl implements DiagnosticsProvider {
1011
constructor(private readonly lsAndTsDocResolver: LSAndTSDocResolver) {}
@@ -40,6 +41,7 @@ export class DiagnosticsProviderImpl implements DiagnosticsProvider {
4041
const fragment = await tsDoc.getFragment();
4142

4243
return diagnostics
44+
.filter(isNotGenerated(tsDoc.getText(0, tsDoc.getLength())))
4345
.map<Diagnostic>((diagnostic) => ({
4446
range: convertRange(tsDoc, diagnostic),
4547
severity: mapSeverity(diagnostic.category),
@@ -208,3 +210,16 @@ function swapRangeStartEndIfNecessary(diag: Diagnostic): Diagnostic {
208210
}
209211
return diag;
210212
}
213+
214+
/**
215+
* Checks if diagnostic is not within a section that should be completely ignored
216+
* because it's purely generated.
217+
*/
218+
function isNotGenerated(text: string) {
219+
return (diagnostic: ts.Diagnostic) => {
220+
if (diagnostic.start === undefined || diagnostic.length === undefined) {
221+
return true;
222+
}
223+
return !isInGeneratedCode(text, diagnostic.start, diagnostic.start + diagnostic.length);
224+
};
225+
}

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

Lines changed: 11 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
1+
import ts from 'typescript';
12
import { Location, Position, ReferenceContext } from 'vscode-languageserver';
23
import { Document } from '../../../lib/documents';
34
import { pathToUrl } from '../../../utils';
45
import { FindReferencesProvider } from '../../interfaces';
5-
import { SnapshotFragment } from '../DocumentSnapshot';
66
import { LSAndTSDocResolver } from '../LSAndTSDocResolver';
77
import { convertToLocationRange } from '../utils';
8+
import { isNoTextSpanInGeneratedCode, SnapshotFragmentMap } from './utils';
89

910
export class FindReferencesProviderImpl implements FindReferencesProvider {
1011
constructor(private readonly lsAndTsDocResolver: LSAndTSDocResolver) {}
@@ -25,17 +26,15 @@ export class FindReferencesProviderImpl implements FindReferencesProvider {
2526
return null;
2627
}
2728

28-
const docs = new Map<string, SnapshotFragment>([[tsDoc.filePath, fragment]]);
29+
const docs = new SnapshotFragmentMap(this.lsAndTsDocResolver);
30+
docs.set(tsDoc.filePath, { fragment, snapshot: tsDoc });
2931

3032
return await Promise.all(
3133
references
3234
.filter((ref) => context.includeDeclaration || !ref.isDefinition)
35+
.filter(notInGeneratedCode(tsDoc.getFullText()))
3336
.map(async (ref) => {
34-
let defDoc = docs.get(ref.fileName);
35-
if (!defDoc) {
36-
defDoc = await this.getSnapshot(ref.fileName).getFragment();
37-
docs.set(ref.fileName, defDoc);
38-
}
37+
const defDoc = await docs.retrieveFragment(ref.fileName);
3938

4039
return Location.create(
4140
pathToUrl(ref.fileName),
@@ -48,8 +47,10 @@ export class FindReferencesProviderImpl implements FindReferencesProvider {
4847
private getLSAndTSDoc(document: Document) {
4948
return this.lsAndTsDocResolver.getLSAndTSDoc(document);
5049
}
50+
}
5151

52-
private getSnapshot(filePath: string, document?: Document) {
53-
return this.lsAndTsDocResolver.getSnapshot(filePath, document);
54-
}
52+
function notInGeneratedCode(text: string) {
53+
return (ref: ts.ReferenceEntry) => {
54+
return isNoTextSpanInGeneratedCode(text, ref.textSpan);
55+
};
5556
}

0 commit comments

Comments
 (0)