Skip to content

Commit

Permalink
(fix) if conditions control flow (#846)
Browse files Browse the repository at this point in the history
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
  • Loading branch information
dummdidumm authored Mar 10, 2021
1 parent 8f10ccb commit 52849e6
Show file tree
Hide file tree
Showing 41 changed files with 2,100 additions and 188 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,10 @@ export interface DocumentSnapshot extends ts.IScriptSnapshot {
* in order to prevent memory leaks.
*/
destroyFragment(): void;
/**
* Convenience function for getText(0, getLength())
*/
getFullText(): string;
}

/**
Expand Down Expand Up @@ -221,6 +225,10 @@ export class SvelteDocumentSnapshot implements DocumentSnapshot {
return this.text.length;
}

getFullText() {
return this.text;
}

getChangeRange() {
return undefined;
}
Expand Down Expand Up @@ -301,6 +309,10 @@ export class JSOrTSDocumentSnapshot
return this.text.length;
}

getFullText() {
return this.text;
}

getChangeRange() {
return undefined;
}
Expand Down
38 changes: 17 additions & 21 deletions packages/language-server/src/plugins/typescript/TypeScriptPlugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ import {
getTextInRange
} from '../../lib/documents';
import { LSConfigManager, LSTypescriptConfig } from '../../ls-config';
import { pathToUrl } from '../../utils';
import { isNotNullOrUndefined, pathToUrl } from '../../utils';
import {
AppCompletionItem,
AppCompletionList,
Expand All @@ -49,7 +49,6 @@ import {
SemanticTokensProvider,
UpdateTsOrJsFile
} from '../interfaces';
import { SnapshotFragment } from './DocumentSnapshot';
import { CodeActionsProviderImpl } from './features/CodeActionsProvider';
import {
CompletionEntryWithIdentifer,
Expand All @@ -67,6 +66,7 @@ import { SelectionRangeProviderImpl } from './features/SelectionRangeProvider';
import { SignatureHelpProviderImpl } from './features/SignatureHelpProvider';
import { SnapshotManager } from './SnapshotManager';
import { SemanticTokensProviderImpl } from './features/SemanticTokensProvider';
import { isNoTextSpanInGeneratedCode, SnapshotFragmentMap } from './features/utils';

export class TypeScriptPlugin
implements
Expand Down Expand Up @@ -263,35 +263,35 @@ export class TypeScriptPlugin
}

const { lang, tsDoc } = this.getLSAndTSDoc(document);
const fragment = await tsDoc.getFragment();
const mainFragment = await tsDoc.getFragment();

const defs = lang.getDefinitionAndBoundSpan(
tsDoc.filePath,
fragment.offsetAt(fragment.getGeneratedPosition(position))
mainFragment.offsetAt(mainFragment.getGeneratedPosition(position))
);

if (!defs || !defs.definitions) {
return [];
}

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

return await Promise.all(
const result = await Promise.all(
defs.definitions.map(async (def) => {
let defDoc = docs.get(def.fileName);
if (!defDoc) {
defDoc = await this.getSnapshot(def.fileName).getFragment();
docs.set(def.fileName, defDoc);
const { fragment, snapshot } = await docs.retrieve(def.fileName);

if (isNoTextSpanInGeneratedCode(snapshot.getFullText(), def.textSpan)) {
return LocationLink.create(
pathToUrl(def.fileName),
convertToLocationRange(fragment, def.textSpan),
convertToLocationRange(fragment, def.textSpan),
convertToLocationRange(mainFragment, defs.textSpan)
);
}

return LocationLink.create(
pathToUrl(def.fileName),
convertToLocationRange(defDoc, def.textSpan),
convertToLocationRange(defDoc, def.textSpan),
convertToLocationRange(fragment, defs.textSpan)
);
})
);
return result.filter(isNotNullOrUndefined);
}

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

private getSnapshot(filePath: string, document?: Document) {
return this.lsAndTsDocResolver.getSnapshot(filePath, document);
}

/**
*
* @internal
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,14 +9,15 @@ import {
WorkspaceEdit
} from 'vscode-languageserver';
import { Document, mapRangeToOriginal, isRangeInTag, isInTag } from '../../../lib/documents';
import { pathToUrl, flatten } from '../../../utils';
import { pathToUrl, flatten, isNotNullOrUndefined } from '../../../utils';
import { CodeActionsProvider } from '../../interfaces';
import { SnapshotFragment, SvelteSnapshotFragment } from '../DocumentSnapshot';
import { LSAndTSDocResolver } from '../LSAndTSDocResolver';
import { convertRange } from '../utils';

import ts from 'typescript';
import { CompletionsProviderImpl } from './CompletionProvider';
import { isNoTextSpanInGeneratedCode, SnapshotFragmentMap } from './utils';

interface RefactorArgs {
type: 'refactor';
Expand Down Expand Up @@ -134,53 +135,65 @@ export class CodeActionsProviderImpl implements CodeActionsProvider {
userPreferences
);

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

return await Promise.all(
codeFixes.map(async (fix) => {
const documentChanges = await Promise.all(
fix.changes.map(async (change) => {
const doc =
docs.get(change.fileName) ??
(await this.getAndCacheCodeActionDoc(change, docs));
const { snapshot, fragment } = await docs.retrieve(change.fileName);
return TextDocumentEdit.create(
VersionedTextDocumentIdentifier.create(pathToUrl(change.fileName), 0),
change.textChanges.map((edit) => {
if (
fix.fixName === 'import' &&
doc instanceof SvelteSnapshotFragment
) {
return this.completionProvider.codeActionChangeToTextEdit(
document,
doc,
edit,
true,
isInTag(range.start, document.scriptInfo) ||
isInTag(range.start, document.moduleScriptInfo)
);
}

let originalRange = mapRangeToOriginal(
doc,
convertRange(doc, edit.span)
);
if (fix.fixName === 'unusedIdentifier') {
originalRange = this.checkRemoveImportCodeActionRange(
edit,
doc,
originalRange
change.textChanges
.map((edit) => {
if (
fix.fixName === 'import' &&
fragment instanceof SvelteSnapshotFragment
) {
return this.completionProvider.codeActionChangeToTextEdit(
document,
fragment,
edit,
true,
isInTag(range.start, document.scriptInfo) ||
isInTag(range.start, document.moduleScriptInfo)
);
}

if (
!isNoTextSpanInGeneratedCode(
snapshot.getFullText(),
edit.span
)
) {
return undefined;
}

let originalRange = mapRangeToOriginal(
fragment,
convertRange(fragment, edit.span)
);
}

if (fix.fixName === 'fixMissingFunctionDeclaration') {
originalRange = this.checkEndOfFileCodeInsert(
originalRange,
range,
document
);
}

return TextEdit.replace(originalRange, edit.newText);
})
if (fix.fixName === 'unusedIdentifier') {
originalRange = this.checkRemoveImportCodeActionRange(
edit,
fragment,
originalRange
);
}

if (fix.fixName === 'fixMissingFunctionDeclaration') {
originalRange = this.checkEndOfFileCodeInsert(
originalRange,
range,
document
);
}

return TextEdit.replace(originalRange, edit.newText);
})
.filter(isNotNullOrUndefined)
);
})
);
Expand All @@ -195,15 +208,6 @@ export class CodeActionsProviderImpl implements CodeActionsProvider {
);
}

private async getAndCacheCodeActionDoc(
change: ts.FileTextChanges,
cache: Map<string, SnapshotFragment>
) {
const doc = await this.getSnapshot(change.fileName).getFragment();
cache.set(change.fileName, doc);
return doc;
}

private async getApplicableRefactors(document: Document, range: Range): Promise<CodeAction[]> {
if (
!isRangeInTag(range, document.scriptInfo) &&
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { DiagnosticsProvider } from '../../interfaces';
import { LSAndTSDocResolver } from '../LSAndTSDocResolver';
import { convertRange, mapSeverity } from '../utils';
import { SvelteDocumentSnapshot } from '../DocumentSnapshot';
import { isInGeneratedCode } from './utils';

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

return diagnostics
.filter(isNotGenerated(tsDoc.getText(0, tsDoc.getLength())))
.map<Diagnostic>((diagnostic) => ({
range: convertRange(tsDoc, diagnostic),
severity: mapSeverity(diagnostic.category),
Expand Down Expand Up @@ -208,3 +210,16 @@ function swapRangeStartEndIfNecessary(diag: Diagnostic): Diagnostic {
}
return diag;
}

/**
* Checks if diagnostic is not within a section that should be completely ignored
* because it's purely generated.
*/
function isNotGenerated(text: string) {
return (diagnostic: ts.Diagnostic) => {
if (diagnostic.start === undefined || diagnostic.length === undefined) {
return true;
}
return !isInGeneratedCode(text, diagnostic.start, diagnostic.start + diagnostic.length);
};
}
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import ts from 'typescript';
import { Location, Position, ReferenceContext } from 'vscode-languageserver';
import { Document } from '../../../lib/documents';
import { pathToUrl } from '../../../utils';
import { FindReferencesProvider } from '../../interfaces';
import { SnapshotFragment } from '../DocumentSnapshot';
import { LSAndTSDocResolver } from '../LSAndTSDocResolver';
import { convertToLocationRange } from '../utils';
import { isNoTextSpanInGeneratedCode, SnapshotFragmentMap } from './utils';

export class FindReferencesProviderImpl implements FindReferencesProvider {
constructor(private readonly lsAndTsDocResolver: LSAndTSDocResolver) {}
Expand All @@ -25,17 +26,15 @@ export class FindReferencesProviderImpl implements FindReferencesProvider {
return null;
}

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

return await Promise.all(
references
.filter((ref) => context.includeDeclaration || !ref.isDefinition)
.filter(notInGeneratedCode(tsDoc.getFullText()))
.map(async (ref) => {
let defDoc = docs.get(ref.fileName);
if (!defDoc) {
defDoc = await this.getSnapshot(ref.fileName).getFragment();
docs.set(ref.fileName, defDoc);
}
const defDoc = await docs.retrieveFragment(ref.fileName);

return Location.create(
pathToUrl(ref.fileName),
Expand All @@ -48,8 +47,10 @@ export class FindReferencesProviderImpl implements FindReferencesProvider {
private getLSAndTSDoc(document: Document) {
return this.lsAndTsDocResolver.getLSAndTSDoc(document);
}
}

private getSnapshot(filePath: string, document?: Document) {
return this.lsAndTsDocResolver.getSnapshot(filePath, document);
}
function notInGeneratedCode(text: string) {
return (ref: ts.ReferenceEntry) => {
return isNoTextSpanInGeneratedCode(text, ref.textSpan);
};
}
Loading

0 comments on commit 52849e6

Please sign in to comment.