Revision f24f74eb8908ceb955b013547908dc83b0231772 authored by Babak K. Shandiz on 16 August 2022, 16:27:29 UTC, committed by GitHub on 16 August 2022, 16:27:29 UTC
* ⚗️ Add test to verify code fix works when implementing a mapped type with indirect keyof Signed-off-by: Babak K. Shandiz <babak.k.shandiz@gmail.com> * 🔨 Add property as implementation for symbols that has no declaration Signed-off-by: Babak K. Shandiz <babak.k.shandiz@gmail.com> diff --git a/src/services/codefixes/helpers.ts b/src/services/codefixes/helpers.ts index 94b64e57..a4c11fd5 100644 --- a/src/services/codefixes/helpers.ts +++ b/src/services/codefixes/helpers.ts @@ -60,21 +60,19 @@ namespace ts.codefix { isAmbient = false, ): void { const declarations = symbol.getDeclarations(); - if (!(declarations && declarations.length)) { - return undefined; - } + const declaration = declarations ? declarations[0] : undefined; const checker = context.program.getTypeChecker(); const scriptTarget = getEmitScriptTarget(context.program.getCompilerOptions()); - const declaration = declarations[0]; + const kind = declaration?.kind ?? SyntaxKind.PropertySignature; const name = getSynthesizedDeepClone(getNameOfDeclaration(declaration), /*includeTrivia*/ false) as PropertyName; - const visibilityModifier = createVisibilityModifier(getEffectiveModifierFlags(declaration)); + const visibilityModifier = createVisibilityModifier(declaration ? getEffectiveModifierFlags(declaration) : ModifierFlags.None); const modifiers = visibilityModifier ? factory.createNodeArray([visibilityModifier]) : undefined; const type = checker.getWidenedType(checker.getTypeOfSymbolAtLocation(symbol, enclosingDeclaration)); const optional = !!(symbol.flags & SymbolFlags.Optional); const ambient = !!(enclosingDeclaration.flags & NodeFlags.Ambient) || isAmbient; const quotePreference = getQuotePreference(sourceFile, preferences); - switch (declaration.kind) { + switch (kind) { case SyntaxKind.PropertySignature: case SyntaxKind.PropertyDeclaration: const flags = quotePreference === QuotePreference.Single ? NodeBuilderFlags.UseSingleQuotesForStringLiteralType : undefined; @@ -88,13 +86,16 @@ namespace ts.codefix { } addClassElement(factory.createPropertyDeclaration( modifiers, - name, + declaration ? name : symbol.getName(), optional && (preserveOptional & PreserveOptionalFlags.Property) ? factory.createToken(SyntaxKind.QuestionToken) : undefined, typeNode, /*initializer*/ undefined)); break; case SyntaxKind.GetAccessor: case SyntaxKind.SetAccessor: { + if (!declarations) { + break; + } let typeNode = checker.typeToTypeNode(type, enclosingDeclaration, /*flags*/ undefined, getNoopSymbolTrackerWithResolver(context)); const allAccessors = getAllAccessorDeclarations(declarations, declaration as AccessorDeclaration); const orderedAccessors = allAccessors.secondAccessor @@ -138,6 +139,10 @@ namespace ts.codefix { // If there is more than one overload but no implementation signature // (eg: an abstract method or interface declaration), there is a 1-1 // correspondence of declarations and signatures. + if (!declarations) { + break; + } + const signatures = checker.getSignaturesOfType(type, SignatureKind.Call); if (!some(signatures)) { break; * 🔨 Improve code readability Signed-off-by: Babak K. Shandiz <babak.k.shandiz@gmail.com> diff --git a/src/services/codefixes/helpers.ts b/src/services/codefixes/helpers.ts index 2f5c8703ab..aea0206a8a 100644 --- a/src/services/codefixes/helpers.ts +++ b/src/services/codefixes/helpers.ts @@ -60,7 +60,7 @@ namespace ts.codefix { isAmbient = false, ): void { const declarations = symbol.getDeclarations(); - const declaration = declarations ? declarations[0] : undefined; + const declaration = declarations?.[0]; const checker = context.program.getTypeChecker(); const scriptTarget = getEmitScriptTarget(context.program.getCompilerOptions()); const kind = declaration?.kind ?? SyntaxKind.PropertySignature; @@ -93,9 +93,7 @@ namespace ts.codefix { break; case SyntaxKind.GetAccessor: case SyntaxKind.SetAccessor: { - if (!declarations) { - break; - } + Debug.assertIsDefined(declarations); let typeNode = checker.typeToTypeNode(type, enclosingDeclaration, /*flags*/ undefined, getNoopSymbolTrackerWithResolver(context)); const allAccessors = getAllAccessorDeclarations(declarations, declaration as AccessorDeclaration); const orderedAccessors = allAccessors.secondAccessor @@ -139,10 +137,7 @@ namespace ts.codefix { // If there is more than one overload but no implementation signature // (eg: an abstract method or interface declaration), there is a 1-1 // correspondence of declarations and signatures. - if (!declarations) { - break; - } - + Debug.assertIsDefined(declarations); const signatures = type.isUnion() ? flatMap(type.types, t => t.getCallSignatures()) : type.getCallSignatures(); if (!some(signatures)) { break; * 📜 Add comment regarding mapped type children's missing declaration Signed-off-by: Babak K. Shandiz <babak.k.shandiz@gmail.com> Signed-off-by: Babak K. Shandiz <babak.k.shandiz@gmail.com> Co-authored-by: Andrew Branch <andrew@wheream.io>
1 parent 90fa1c7
harnessLanguageService.ts
namespace Harness.LanguageService {
export function makeDefaultProxy(info: ts.server.PluginCreateInfo): ts.LanguageService {
const proxy = Object.create(/*prototype*/ null); // eslint-disable-line no-null/no-null
const langSvc: any = info.languageService;
for (const k of Object.keys(langSvc)) {
// eslint-disable-next-line only-arrow-functions
proxy[k] = function () {
return langSvc[k].apply(langSvc, arguments);
};
}
return proxy;
}
export class ScriptInfo {
public version = 1;
public editRanges: { length: number; textChangeRange: ts.TextChangeRange; }[] = [];
private lineMap: number[] | undefined;
constructor(public fileName: string, public content: string, public isRootFile: boolean) {
this.setContent(content);
}
private setContent(content: string): void {
this.content = content;
this.lineMap = undefined;
}
public getLineMap(): number[] {
return this.lineMap || (this.lineMap = ts.computeLineStarts(this.content));
}
public updateContent(content: string): void {
this.editRanges = [];
this.setContent(content);
this.version++;
}
public editContent(start: number, end: number, newText: string): void {
// Apply edits
const prefix = this.content.substring(0, start);
const middle = newText;
const suffix = this.content.substring(end);
this.setContent(prefix + middle + suffix);
// Store edit range + new length of script
this.editRanges.push({
length: this.content.length,
textChangeRange: ts.createTextChangeRange(
ts.createTextSpanFromBounds(start, end), newText.length)
});
// Update version #
this.version++;
}
public getTextChangeRangeBetweenVersions(startVersion: number, endVersion: number): ts.TextChangeRange {
if (startVersion === endVersion) {
// No edits!
return ts.unchangedTextChangeRange;
}
const initialEditRangeIndex = this.editRanges.length - (this.version - startVersion);
const lastEditRangeIndex = this.editRanges.length - (this.version - endVersion);
const entries = this.editRanges.slice(initialEditRangeIndex, lastEditRangeIndex);
return ts.collapseTextChangeRangesAcrossMultipleVersions(entries.map(e => e.textChangeRange));
}
}
class ScriptSnapshot implements ts.IScriptSnapshot {
public textSnapshot: string;
public version: number;
constructor(public scriptInfo: ScriptInfo) {
this.textSnapshot = scriptInfo.content;
this.version = scriptInfo.version;
}
public getText(start: number, end: number): string {
return this.textSnapshot.substring(start, end);
}
public getLength(): number {
return this.textSnapshot.length;
}
public getChangeRange(oldScript: ts.IScriptSnapshot): ts.TextChangeRange {
const oldShim = oldScript as ScriptSnapshot;
return this.scriptInfo.getTextChangeRangeBetweenVersions(oldShim.version, this.version);
}
}
class ScriptSnapshotProxy implements ts.ScriptSnapshotShim {
constructor(private readonly scriptSnapshot: ts.IScriptSnapshot) {
}
public getText(start: number, end: number): string {
return this.scriptSnapshot.getText(start, end);
}
public getLength(): number {
return this.scriptSnapshot.getLength();
}
public getChangeRange(oldScript: ts.ScriptSnapshotShim): string | undefined {
const range = this.scriptSnapshot.getChangeRange((oldScript as ScriptSnapshotProxy).scriptSnapshot);
return range && JSON.stringify(range);
}
}
class DefaultHostCancellationToken implements ts.HostCancellationToken {
public static readonly instance = new DefaultHostCancellationToken();
public isCancellationRequested() {
return false;
}
}
export interface LanguageServiceAdapter {
getHost(): LanguageServiceAdapterHost;
getLanguageService(): ts.LanguageService;
getClassifier(): ts.Classifier;
getPreProcessedFileInfo(fileName: string, fileContents: string): ts.PreProcessedFileInfo;
}
export abstract class LanguageServiceAdapterHost {
public readonly sys = new fakes.System(new vfs.FileSystem(/*ignoreCase*/ true, { cwd: virtualFileSystemRoot }));
public typesRegistry: ts.ESMap<string, void> | undefined;
private scriptInfos: collections.SortedMap<string, ScriptInfo>;
constructor(protected cancellationToken = DefaultHostCancellationToken.instance,
protected settings = ts.getDefaultCompilerOptions()) {
this.scriptInfos = new collections.SortedMap({ comparer: this.vfs.stringComparer, sort: "insertion" });
}
public get vfs() {
return this.sys.vfs;
}
public getNewLine(): string {
return harnessNewLine;
}
public getFilenames(): string[] {
const fileNames: string[] = [];
this.scriptInfos.forEach(scriptInfo => {
if (scriptInfo.isRootFile) {
// only include root files here
// usually it means that we won't include lib.d.ts in the list of root files so it won't mess the computation of compilation root dir.
fileNames.push(scriptInfo.fileName);
}
});
return fileNames;
}
public realpath(path: string): string {
try {
return this.vfs.realpathSync(path);
}
catch {
return path;
}
}
public fileExists(path: string): boolean {
try {
return this.vfs.existsSync(path);
}
catch {
return false;
}
}
public readFile(path: string): string | undefined {
try {
return this.vfs.readFileSync(path).toString();
}
catch {
return undefined;
}
}
public directoryExists(path: string) {
return this.vfs.statSync(path).isDirectory();
}
public getScriptInfo(fileName: string): ScriptInfo | undefined {
return this.scriptInfos.get(vpath.resolve(this.vfs.cwd(), fileName));
}
public addScript(fileName: string, content: string, isRootFile: boolean): void {
this.vfs.mkdirpSync(vpath.dirname(fileName));
this.vfs.writeFileSync(fileName, content);
this.scriptInfos.set(vpath.resolve(this.vfs.cwd(), fileName), new ScriptInfo(fileName, content, isRootFile));
}
public renameFileOrDirectory(oldPath: string, newPath: string): void {
this.vfs.mkdirpSync(ts.getDirectoryPath(newPath));
this.vfs.renameSync(oldPath, newPath);
const updater = ts.getPathUpdater(oldPath, newPath, ts.createGetCanonicalFileName(this.useCaseSensitiveFileNames()), /*sourceMapper*/ undefined);
this.scriptInfos.forEach((scriptInfo, key) => {
const newFileName = updater(key);
if (newFileName !== undefined) {
this.scriptInfos.delete(key);
this.scriptInfos.set(newFileName, scriptInfo);
scriptInfo.fileName = newFileName;
}
});
}
public editScript(fileName: string, start: number, end: number, newText: string) {
const script = this.getScriptInfo(fileName);
if (script) {
script.editContent(start, end, newText);
this.vfs.mkdirpSync(vpath.dirname(fileName));
this.vfs.writeFileSync(fileName, script.content);
return;
}
throw new Error("No script with name '" + fileName + "'");
}
public openFile(_fileName: string, _content?: string, _scriptKindName?: string): void { /*overridden*/ }
/**
* @param line 0 based index
* @param col 0 based index
*/
public positionToLineAndCharacter(fileName: string, position: number): ts.LineAndCharacter {
const script: ScriptInfo = this.getScriptInfo(fileName)!;
assert.isOk(script);
return ts.computeLineAndCharacterOfPosition(script.getLineMap(), position);
}
public lineAndCharacterToPosition(fileName: string, lineAndCharacter: ts.LineAndCharacter): number {
const script: ScriptInfo = this.getScriptInfo(fileName)!;
assert.isOk(script);
return ts.computePositionOfLineAndCharacter(script.getLineMap(), lineAndCharacter.line, lineAndCharacter.character);
}
useCaseSensitiveFileNames() {
return !this.vfs.ignoreCase;
}
}
/// Native adapter
class NativeLanguageServiceHost extends LanguageServiceAdapterHost implements ts.LanguageServiceHost, LanguageServiceAdapterHost {
isKnownTypesPackageName(name: string): boolean {
return !!this.typesRegistry && this.typesRegistry.has(name);
}
getGlobalTypingsCacheLocation() {
return "/Library/Caches/typescript";
}
installPackage = ts.notImplemented;
getCompilationSettings() { return this.settings; }
getCancellationToken() { return this.cancellationToken; }
getDirectories(path: string): string[] {
return this.sys.getDirectories(path);
}
getCurrentDirectory(): string { return virtualFileSystemRoot; }
getDefaultLibFileName(): string { return Compiler.defaultLibFileName; }
getScriptFileNames(): string[] {
return this.getFilenames().filter(ts.isAnySupportedFileExtension);
}
getScriptSnapshot(fileName: string): ts.IScriptSnapshot | undefined {
const script = this.getScriptInfo(fileName);
return script ? new ScriptSnapshot(script) : undefined;
}
getScriptKind(): ts.ScriptKind { return ts.ScriptKind.Unknown; }
getScriptVersion(fileName: string): string {
const script = this.getScriptInfo(fileName);
return script ? script.version.toString() : undefined!; // TODO: GH#18217
}
directoryExists(dirName: string): boolean {
return this.sys.directoryExists(dirName);
}
fileExists(fileName: string): boolean {
return this.sys.fileExists(fileName);
}
readDirectory(path: string, extensions?: readonly string[], exclude?: readonly string[], include?: readonly string[], depth?: number): string[] {
return this.sys.readDirectory(path, extensions, exclude, include, depth);
}
readFile(path: string): string | undefined {
return this.sys.readFile(path);
}
realpath(path: string): string {
return this.sys.realpath(path);
}
getTypeRootsVersion() {
return 0;
}
log = ts.noop;
trace = ts.noop;
error = ts.noop;
}
export class NativeLanguageServiceAdapter implements LanguageServiceAdapter {
private host: NativeLanguageServiceHost;
constructor(cancellationToken?: ts.HostCancellationToken, options?: ts.CompilerOptions) {
this.host = new NativeLanguageServiceHost(cancellationToken, options);
}
getHost(): LanguageServiceAdapterHost { return this.host; }
getLanguageService(): ts.LanguageService { return ts.createLanguageService(this.host); }
getClassifier(): ts.Classifier { return ts.createClassifier(); }
getPreProcessedFileInfo(fileName: string, fileContents: string): ts.PreProcessedFileInfo { return ts.preProcessFile(fileContents, /* readImportFiles */ true, ts.hasJSFileExtension(fileName)); }
}
/// Shim adapter
class ShimLanguageServiceHost extends LanguageServiceAdapterHost implements ts.LanguageServiceShimHost, ts.CoreServicesShimHost {
private nativeHost: NativeLanguageServiceHost;
public getModuleResolutionsForFile: ((fileName: string) => string) | undefined;
public getTypeReferenceDirectiveResolutionsForFile: ((fileName: string) => string) | undefined;
constructor(preprocessToResolve: boolean, cancellationToken?: ts.HostCancellationToken, options?: ts.CompilerOptions) {
super(cancellationToken, options);
this.nativeHost = new NativeLanguageServiceHost(cancellationToken, options);
if (preprocessToResolve) {
const compilerOptions = this.nativeHost.getCompilationSettings();
const moduleResolutionHost: ts.ModuleResolutionHost = {
fileExists: fileName => this.getScriptInfo(fileName) !== undefined,
readFile: fileName => {
const scriptInfo = this.getScriptInfo(fileName);
return scriptInfo && scriptInfo.content;
},
useCaseSensitiveFileNames: this.useCaseSensitiveFileNames()
};
this.getModuleResolutionsForFile = (fileName) => {
const scriptInfo = this.getScriptInfo(fileName)!;
const preprocessInfo = ts.preProcessFile(scriptInfo.content, /*readImportFiles*/ true);
const imports: ts.MapLike<string> = {};
for (const module of preprocessInfo.importedFiles) {
const resolutionInfo = ts.resolveModuleName(module.fileName, fileName, compilerOptions, moduleResolutionHost);
if (resolutionInfo.resolvedModule) {
imports[module.fileName] = resolutionInfo.resolvedModule.resolvedFileName;
}
}
return JSON.stringify(imports);
};
this.getTypeReferenceDirectiveResolutionsForFile = (fileName) => {
const scriptInfo = this.getScriptInfo(fileName);
if (scriptInfo) {
const preprocessInfo = ts.preProcessFile(scriptInfo.content, /*readImportFiles*/ false);
const resolutions: ts.MapLike<ts.ResolvedTypeReferenceDirective> = {};
const settings = this.nativeHost.getCompilationSettings();
for (const typeReferenceDirective of preprocessInfo.typeReferenceDirectives) {
const resolutionInfo = ts.resolveTypeReferenceDirective(typeReferenceDirective.fileName, fileName, settings, moduleResolutionHost);
if (resolutionInfo.resolvedTypeReferenceDirective!.resolvedFileName) {
resolutions[typeReferenceDirective.fileName] = resolutionInfo.resolvedTypeReferenceDirective!;
}
}
return JSON.stringify(resolutions);
}
else {
return "[]";
}
};
}
}
getFilenames(): string[] { return this.nativeHost.getFilenames(); }
getScriptInfo(fileName: string): ScriptInfo | undefined { return this.nativeHost.getScriptInfo(fileName); }
addScript(fileName: string, content: string, isRootFile: boolean): void { this.nativeHost.addScript(fileName, content, isRootFile); }
editScript(fileName: string, start: number, end: number, newText: string): void { this.nativeHost.editScript(fileName, start, end, newText); }
positionToLineAndCharacter(fileName: string, position: number): ts.LineAndCharacter { return this.nativeHost.positionToLineAndCharacter(fileName, position); }
getCompilationSettings(): string { return JSON.stringify(this.nativeHost.getCompilationSettings()); }
getCancellationToken(): ts.HostCancellationToken { return this.nativeHost.getCancellationToken(); }
getCurrentDirectory(): string { return this.nativeHost.getCurrentDirectory(); }
getDirectories(path: string): string { return JSON.stringify(this.nativeHost.getDirectories(path)); }
getDefaultLibFileName(): string { return this.nativeHost.getDefaultLibFileName(); }
getScriptFileNames(): string { return JSON.stringify(this.nativeHost.getScriptFileNames()); }
getScriptSnapshot(fileName: string): ts.ScriptSnapshotShim {
const nativeScriptSnapshot = this.nativeHost.getScriptSnapshot(fileName)!; // TODO: GH#18217
return nativeScriptSnapshot && new ScriptSnapshotProxy(nativeScriptSnapshot);
}
getScriptKind(): ts.ScriptKind { return this.nativeHost.getScriptKind(); }
getScriptVersion(fileName: string): string { return this.nativeHost.getScriptVersion(fileName); }
getLocalizedDiagnosticMessages(): string { return JSON.stringify({}); }
readDirectory = ts.notImplemented;
readDirectoryNames = ts.notImplemented;
readFileNames = ts.notImplemented;
fileExists(fileName: string) { return this.getScriptInfo(fileName) !== undefined; }
readFile(fileName: string) {
const snapshot = this.nativeHost.getScriptSnapshot(fileName);
return snapshot && ts.getSnapshotText(snapshot);
}
log(s: string): void { this.nativeHost.log(s); }
trace(s: string): void { this.nativeHost.trace(s); }
error(s: string): void { this.nativeHost.error(s); }
directoryExists(): boolean {
// for tests pessimistically assume that directory always exists
return true;
}
}
class ClassifierShimProxy implements ts.Classifier {
constructor(private shim: ts.ClassifierShim) {
}
getEncodedLexicalClassifications(_text: string, _lexState: ts.EndOfLineState, _classifyKeywordsInGenerics?: boolean): ts.Classifications {
return ts.notImplemented();
}
getClassificationsForLine(text: string, lexState: ts.EndOfLineState, classifyKeywordsInGenerics?: boolean): ts.ClassificationResult {
const result = this.shim.getClassificationsForLine(text, lexState, classifyKeywordsInGenerics).split("\n");
const entries: ts.ClassificationInfo[] = [];
let i = 0;
let position = 0;
for (; i < result.length - 1; i += 2) {
const t = entries[i / 2] = {
length: parseInt(result[i]),
classification: parseInt(result[i + 1])
};
assert.isTrue(t.length > 0, "Result length should be greater than 0, got :" + t.length);
position += t.length;
}
const finalLexState = parseInt(result[result.length - 1]);
assert.equal(position, text.length, "Expected cumulative length of all entries to match the length of the source. expected: " + text.length + ", but got: " + position);
return {
finalLexState,
entries
};
}
}
function unwrapJSONCallResult(result: string): any {
const parsedResult = JSON.parse(result);
if (parsedResult.error) {
throw new Error("Language Service Shim Error: " + JSON.stringify(parsedResult.error));
}
else if (parsedResult.canceled) {
throw new ts.OperationCanceledException();
}
return parsedResult.result;
}
class LanguageServiceShimProxy implements ts.LanguageService {
constructor(private shim: ts.LanguageServiceShim) {
}
cleanupSemanticCache(): void {
this.shim.cleanupSemanticCache();
}
getSyntacticDiagnostics(fileName: string): ts.DiagnosticWithLocation[] {
return unwrapJSONCallResult(this.shim.getSyntacticDiagnostics(fileName));
}
getSemanticDiagnostics(fileName: string): ts.DiagnosticWithLocation[] {
return unwrapJSONCallResult(this.shim.getSemanticDiagnostics(fileName));
}
getSuggestionDiagnostics(fileName: string): ts.DiagnosticWithLocation[] {
return unwrapJSONCallResult(this.shim.getSuggestionDiagnostics(fileName));
}
getCompilerOptionsDiagnostics(): ts.Diagnostic[] {
return unwrapJSONCallResult(this.shim.getCompilerOptionsDiagnostics());
}
getSyntacticClassifications(fileName: string, span: ts.TextSpan): ts.ClassifiedSpan[] {
return unwrapJSONCallResult(this.shim.getSyntacticClassifications(fileName, span.start, span.length));
}
getSemanticClassifications(fileName: string, span: ts.TextSpan, format?: ts.SemanticClassificationFormat): ts.ClassifiedSpan[] {
return unwrapJSONCallResult(this.shim.getSemanticClassifications(fileName, span.start, span.length, format));
}
getEncodedSyntacticClassifications(fileName: string, span: ts.TextSpan): ts.Classifications {
return unwrapJSONCallResult(this.shim.getEncodedSyntacticClassifications(fileName, span.start, span.length));
}
getEncodedSemanticClassifications(fileName: string, span: ts.TextSpan, format?: ts.SemanticClassificationFormat): ts.Classifications {
const responseFormat = format || ts.SemanticClassificationFormat.Original;
return unwrapJSONCallResult(this.shim.getEncodedSemanticClassifications(fileName, span.start, span.length, responseFormat));
}
getCompletionsAtPosition(fileName: string, position: number, preferences: ts.UserPreferences | undefined, formattingSettings: ts.FormatCodeSettings | undefined): ts.CompletionInfo {
return unwrapJSONCallResult(this.shim.getCompletionsAtPosition(fileName, position, preferences, formattingSettings));
}
getCompletionEntryDetails(fileName: string, position: number, entryName: string, formatOptions: ts.FormatCodeOptions | undefined, source: string | undefined, preferences: ts.UserPreferences | undefined, data: ts.CompletionEntryData | undefined): ts.CompletionEntryDetails {
return unwrapJSONCallResult(this.shim.getCompletionEntryDetails(fileName, position, entryName, JSON.stringify(formatOptions), source, preferences, data));
}
getCompletionEntrySymbol(): ts.Symbol {
throw new Error("getCompletionEntrySymbol not implemented across the shim layer.");
}
getQuickInfoAtPosition(fileName: string, position: number): ts.QuickInfo {
return unwrapJSONCallResult(this.shim.getQuickInfoAtPosition(fileName, position));
}
getNameOrDottedNameSpan(fileName: string, startPos: number, endPos: number): ts.TextSpan {
return unwrapJSONCallResult(this.shim.getNameOrDottedNameSpan(fileName, startPos, endPos));
}
getBreakpointStatementAtPosition(fileName: string, position: number): ts.TextSpan {
return unwrapJSONCallResult(this.shim.getBreakpointStatementAtPosition(fileName, position));
}
getSignatureHelpItems(fileName: string, position: number, options: ts.SignatureHelpItemsOptions | undefined): ts.SignatureHelpItems {
return unwrapJSONCallResult(this.shim.getSignatureHelpItems(fileName, position, options));
}
getRenameInfo(fileName: string, position: number, preferences: ts.UserPreferences): ts.RenameInfo {
return unwrapJSONCallResult(this.shim.getRenameInfo(fileName, position, preferences));
}
getSmartSelectionRange(fileName: string, position: number): ts.SelectionRange {
return unwrapJSONCallResult(this.shim.getSmartSelectionRange(fileName, position));
}
findRenameLocations(fileName: string, position: number, findInStrings: boolean, findInComments: boolean, providePrefixAndSuffixTextForRename?: boolean): ts.RenameLocation[] {
return unwrapJSONCallResult(this.shim.findRenameLocations(fileName, position, findInStrings, findInComments, providePrefixAndSuffixTextForRename));
}
getDefinitionAtPosition(fileName: string, position: number): ts.DefinitionInfo[] {
return unwrapJSONCallResult(this.shim.getDefinitionAtPosition(fileName, position));
}
getDefinitionAndBoundSpan(fileName: string, position: number): ts.DefinitionInfoAndBoundSpan {
return unwrapJSONCallResult(this.shim.getDefinitionAndBoundSpan(fileName, position));
}
getTypeDefinitionAtPosition(fileName: string, position: number): ts.DefinitionInfo[] {
return unwrapJSONCallResult(this.shim.getTypeDefinitionAtPosition(fileName, position));
}
getImplementationAtPosition(fileName: string, position: number): ts.ImplementationLocation[] {
return unwrapJSONCallResult(this.shim.getImplementationAtPosition(fileName, position));
}
getReferencesAtPosition(fileName: string, position: number): ts.ReferenceEntry[] {
return unwrapJSONCallResult(this.shim.getReferencesAtPosition(fileName, position));
}
findReferences(fileName: string, position: number): ts.ReferencedSymbol[] {
return unwrapJSONCallResult(this.shim.findReferences(fileName, position));
}
getFileReferences(fileName: string): ts.ReferenceEntry[] {
return unwrapJSONCallResult(this.shim.getFileReferences(fileName));
}
getOccurrencesAtPosition(fileName: string, position: number): ts.ReferenceEntry[] {
return unwrapJSONCallResult(this.shim.getOccurrencesAtPosition(fileName, position));
}
getDocumentHighlights(fileName: string, position: number, filesToSearch: string[]): ts.DocumentHighlights[] {
return unwrapJSONCallResult(this.shim.getDocumentHighlights(fileName, position, JSON.stringify(filesToSearch)));
}
getNavigateToItems(searchValue: string): ts.NavigateToItem[] {
return unwrapJSONCallResult(this.shim.getNavigateToItems(searchValue));
}
getNavigationBarItems(fileName: string): ts.NavigationBarItem[] {
return unwrapJSONCallResult(this.shim.getNavigationBarItems(fileName));
}
getNavigationTree(fileName: string): ts.NavigationTree {
return unwrapJSONCallResult(this.shim.getNavigationTree(fileName));
}
getOutliningSpans(fileName: string): ts.OutliningSpan[] {
return unwrapJSONCallResult(this.shim.getOutliningSpans(fileName));
}
getTodoComments(fileName: string, descriptors: ts.TodoCommentDescriptor[]): ts.TodoComment[] {
return unwrapJSONCallResult(this.shim.getTodoComments(fileName, JSON.stringify(descriptors)));
}
getBraceMatchingAtPosition(fileName: string, position: number): ts.TextSpan[] {
return unwrapJSONCallResult(this.shim.getBraceMatchingAtPosition(fileName, position));
}
getIndentationAtPosition(fileName: string, position: number, options: ts.EditorOptions): number {
return unwrapJSONCallResult(this.shim.getIndentationAtPosition(fileName, position, JSON.stringify(options)));
}
getFormattingEditsForRange(fileName: string, start: number, end: number, options: ts.FormatCodeOptions): ts.TextChange[] {
return unwrapJSONCallResult(this.shim.getFormattingEditsForRange(fileName, start, end, JSON.stringify(options)));
}
getFormattingEditsForDocument(fileName: string, options: ts.FormatCodeOptions): ts.TextChange[] {
return unwrapJSONCallResult(this.shim.getFormattingEditsForDocument(fileName, JSON.stringify(options)));
}
getFormattingEditsAfterKeystroke(fileName: string, position: number, key: string, options: ts.FormatCodeOptions): ts.TextChange[] {
return unwrapJSONCallResult(this.shim.getFormattingEditsAfterKeystroke(fileName, position, key, JSON.stringify(options)));
}
getDocCommentTemplateAtPosition(fileName: string, position: number, options?: ts.DocCommentTemplateOptions): ts.TextInsertion {
return unwrapJSONCallResult(this.shim.getDocCommentTemplateAtPosition(fileName, position, options));
}
isValidBraceCompletionAtPosition(fileName: string, position: number, openingBrace: number): boolean {
return unwrapJSONCallResult(this.shim.isValidBraceCompletionAtPosition(fileName, position, openingBrace));
}
getJsxClosingTagAtPosition(): never {
throw new Error("Not supported on the shim.");
}
getSpanOfEnclosingComment(fileName: string, position: number, onlyMultiLine: boolean): ts.TextSpan {
return unwrapJSONCallResult(this.shim.getSpanOfEnclosingComment(fileName, position, onlyMultiLine));
}
getCodeFixesAtPosition(): never {
throw new Error("Not supported on the shim.");
}
getCombinedCodeFix = ts.notImplemented;
applyCodeActionCommand = ts.notImplemented;
getCodeFixDiagnostics(): ts.Diagnostic[] {
throw new Error("Not supported on the shim.");
}
getEditsForRefactor(): ts.RefactorEditInfo {
throw new Error("Not supported on the shim.");
}
getApplicableRefactors(): ts.ApplicableRefactorInfo[] {
throw new Error("Not supported on the shim.");
}
organizeImports(_args: ts.OrganizeImportsArgs, _formatOptions: ts.FormatCodeSettings): readonly ts.FileTextChanges[] {
throw new Error("Not supported on the shim.");
}
getEditsForFileRename(): readonly ts.FileTextChanges[] {
throw new Error("Not supported on the shim.");
}
prepareCallHierarchy(fileName: string, position: number) {
return unwrapJSONCallResult(this.shim.prepareCallHierarchy(fileName, position));
}
provideCallHierarchyIncomingCalls(fileName: string, position: number) {
return unwrapJSONCallResult(this.shim.provideCallHierarchyIncomingCalls(fileName, position));
}
provideCallHierarchyOutgoingCalls(fileName: string, position: number) {
return unwrapJSONCallResult(this.shim.provideCallHierarchyOutgoingCalls(fileName, position));
}
provideInlayHints(fileName: string, span: ts.TextSpan, preference: ts.UserPreferences) {
return unwrapJSONCallResult(this.shim.provideInlayHints(fileName, span, preference));
}
getEmitOutput(fileName: string): ts.EmitOutput {
return unwrapJSONCallResult(this.shim.getEmitOutput(fileName));
}
getProgram(): ts.Program {
throw new Error("Program can not be marshaled across the shim layer.");
}
getCurrentProgram(): ts.Program | undefined {
throw new Error("Program can not be marshaled across the shim layer.");
}
getAutoImportProvider(): ts.Program | undefined {
throw new Error("Program can not be marshaled across the shim layer.");
}
updateIsDefinitionOfReferencedSymbols(_referencedSymbols: readonly ts.ReferencedSymbol[], _knownSymbolSpans: ts.Set<ts.DocumentSpan>): boolean {
return ts.notImplemented();
}
getNonBoundSourceFile(): ts.SourceFile {
throw new Error("SourceFile can not be marshaled across the shim layer.");
}
getSourceFile(): ts.SourceFile {
throw new Error("SourceFile can not be marshaled across the shim layer.");
}
getSourceMapper(): never {
return ts.notImplemented();
}
clearSourceMapperCache(): never {
return ts.notImplemented();
}
toggleLineComment(fileName: string, textRange: ts.TextRange): ts.TextChange[] {
return unwrapJSONCallResult(this.shim.toggleLineComment(fileName, textRange));
}
toggleMultilineComment(fileName: string, textRange: ts.TextRange): ts.TextChange[] {
return unwrapJSONCallResult(this.shim.toggleMultilineComment(fileName, textRange));
}
commentSelection(fileName: string, textRange: ts.TextRange): ts.TextChange[] {
return unwrapJSONCallResult(this.shim.commentSelection(fileName, textRange));
}
uncommentSelection(fileName: string, textRange: ts.TextRange): ts.TextChange[] {
return unwrapJSONCallResult(this.shim.uncommentSelection(fileName, textRange));
}
dispose(): void { this.shim.dispose({}); }
}
export class ShimLanguageServiceAdapter implements LanguageServiceAdapter {
private host: ShimLanguageServiceHost;
private factory: ts.TypeScriptServicesFactory;
constructor(preprocessToResolve: boolean, cancellationToken?: ts.HostCancellationToken, options?: ts.CompilerOptions) {
this.host = new ShimLanguageServiceHost(preprocessToResolve, cancellationToken, options);
this.factory = new ts.TypeScriptServicesFactory();
}
getHost() { return this.host; }
getLanguageService(): ts.LanguageService { return new LanguageServiceShimProxy(this.factory.createLanguageServiceShim(this.host)); }
getClassifier(): ts.Classifier { return new ClassifierShimProxy(this.factory.createClassifierShim(this.host)); }
getPreProcessedFileInfo(fileName: string, fileContents: string): ts.PreProcessedFileInfo {
const coreServicesShim = this.factory.createCoreServicesShim(this.host);
const shimResult: {
referencedFiles: ts.ShimsFileReference[];
typeReferenceDirectives: ts.ShimsFileReference[];
importedFiles: ts.ShimsFileReference[];
isLibFile: boolean;
} = unwrapJSONCallResult(coreServicesShim.getPreProcessedFileInfo(fileName, ts.ScriptSnapshot.fromString(fileContents)));
const convertResult: ts.PreProcessedFileInfo = {
referencedFiles: [],
importedFiles: [],
ambientExternalModules: [],
isLibFile: shimResult.isLibFile,
typeReferenceDirectives: [],
libReferenceDirectives: []
};
ts.forEach(shimResult.referencedFiles, refFile => {
convertResult.referencedFiles.push({
fileName: refFile.path,
pos: refFile.position,
end: refFile.position + refFile.length
});
});
ts.forEach(shimResult.importedFiles, importedFile => {
convertResult.importedFiles.push({
fileName: importedFile.path,
pos: importedFile.position,
end: importedFile.position + importedFile.length
});
});
ts.forEach(shimResult.typeReferenceDirectives, typeRefDirective => {
convertResult.importedFiles.push({
fileName: typeRefDirective.path,
pos: typeRefDirective.position,
end: typeRefDirective.position + typeRefDirective.length
});
});
return convertResult;
}
}
// Server adapter
class SessionClientHost extends NativeLanguageServiceHost implements ts.server.SessionClientHost {
private client!: ts.server.SessionClient;
constructor(cancellationToken: ts.HostCancellationToken | undefined, settings: ts.CompilerOptions | undefined) {
super(cancellationToken, settings);
}
onMessage = ts.noop;
writeMessage = ts.noop;
setClient(client: ts.server.SessionClient) {
this.client = client;
}
openFile(fileName: string, content?: string, scriptKindName?: "TS" | "JS" | "TSX" | "JSX"): void {
super.openFile(fileName, content, scriptKindName);
this.client.openFile(fileName, content, scriptKindName);
}
editScript(fileName: string, start: number, end: number, newText: string) {
const changeArgs = this.client.createChangeFileRequestArgs(fileName, start, end, newText);
super.editScript(fileName, start, end, newText);
this.client.changeFile(fileName, changeArgs);
}
}
class SessionServerHost implements ts.server.ServerHost, ts.server.Logger {
args: string[] = [];
newLine: string;
useCaseSensitiveFileNames = false;
constructor(private host: NativeLanguageServiceHost) {
this.newLine = this.host.getNewLine();
}
onMessage = ts.noop;
writeMessage = ts.noop; // overridden
write(message: string): void {
this.writeMessage(message);
}
readFile(fileName: string): string | undefined {
if (ts.stringContains(fileName, Compiler.defaultLibFileName)) {
fileName = Compiler.defaultLibFileName;
}
// System FS would follow symlinks, even though snapshots are stored by original file name
const snapshot = this.host.getScriptSnapshot(fileName) || this.host.getScriptSnapshot(this.realpath(fileName));
return snapshot && ts.getSnapshotText(snapshot);
}
realpath(path: string) {
return this.host.realpath(path);
}
writeFile = ts.noop;
resolvePath(path: string): string {
return path;
}
fileExists(path: string): boolean {
return this.host.fileExists(path);
}
directoryExists(): boolean {
// for tests assume that directory exists
return true;
}
getExecutingFilePath(): string {
return "";
}
exit = ts.noop;
createDirectory(_directoryName: string): void {
return ts.notImplemented();
}
getCurrentDirectory(): string {
return this.host.getCurrentDirectory();
}
getDirectories(path: string): string[] {
return this.host.getDirectories(path);
}
getEnvironmentVariable(name: string): string {
return ts.sys.getEnvironmentVariable(name);
}
readDirectory(path: string, extensions?: readonly string[], exclude?: readonly string[], include?: readonly string[], depth?: number): string[] {
return this.host.readDirectory(path, extensions, exclude, include, depth);
}
watchFile(): ts.FileWatcher {
return { close: ts.noop };
}
watchDirectory(): ts.FileWatcher {
return { close: ts.noop };
}
close = ts.noop;
info(message: string): void {
this.host.log(message);
}
msg(message: string): void {
this.host.log(message);
}
loggingEnabled() {
return true;
}
getLogFileName(): string | undefined {
return undefined;
}
hasLevel() {
return false;
}
startGroup() { throw ts.notImplemented(); }
endGroup() { throw ts.notImplemented(); }
perftrc(message: string): void {
return this.host.log(message);
}
setTimeout(callback: (...args: any[]) => void, ms: number, ...args: any[]): any {
// eslint-disable-next-line no-restricted-globals
return setTimeout(callback, ms, ...args);
}
clearTimeout(timeoutId: any): void {
// eslint-disable-next-line no-restricted-globals
clearTimeout(timeoutId);
}
setImmediate(callback: (...args: any[]) => void, _ms: number, ...args: any[]): any {
// eslint-disable-next-line no-restricted-globals
return setImmediate(callback, args);
}
clearImmediate(timeoutId: any): void {
// eslint-disable-next-line no-restricted-globals
clearImmediate(timeoutId);
}
createHash(s: string) {
return mockHash(s);
}
require(_initialDir: string, _moduleName: string): ts.RequireResult {
switch (_moduleName) {
// Adds to the Quick Info a fixed string and a string from the config file
// and replaces the first display part
case "quickinfo-augmeneter":
return {
module: () => ({
create(info: ts.server.PluginCreateInfo) {
const proxy = makeDefaultProxy(info);
const langSvc: any = info.languageService;
// eslint-disable-next-line only-arrow-functions
proxy.getQuickInfoAtPosition = function () {
const parts = langSvc.getQuickInfoAtPosition.apply(langSvc, arguments);
if (parts.displayParts.length > 0) {
parts.displayParts[0].text = "Proxied";
}
parts.displayParts.push({ text: info.config.message, kind: "punctuation" });
return parts;
};
return proxy;
}
}),
error: undefined
};
// Throws during initialization
case "create-thrower":
return {
module: () => ({
create() {
throw new Error("I am not a well-behaved plugin");
}
}),
error: undefined
};
// Adds another diagnostic
case "diagnostic-adder":
return {
module: () => ({
create(info: ts.server.PluginCreateInfo) {
const proxy = makeDefaultProxy(info);
proxy.getSemanticDiagnostics = filename => {
const prev = info.languageService.getSemanticDiagnostics(filename);
const sourceFile: ts.SourceFile = info.project.getSourceFile(ts.toPath(filename, /*basePath*/ undefined, ts.createGetCanonicalFileName(info.serverHost.useCaseSensitiveFileNames)))!;
prev.push({
category: ts.DiagnosticCategory.Warning,
file: sourceFile,
code: 9999,
length: 3,
messageText: `Plugin diagnostic`,
start: 0
});
return prev;
};
return proxy;
}
}),
error: undefined
};
// Accepts configurations
case "configurable-diagnostic-adder":
let customMessage = "default message";
return {
module: () => ({
create(info: ts.server.PluginCreateInfo) {
customMessage = info.config.message;
const proxy = makeDefaultProxy(info);
proxy.getSemanticDiagnostics = filename => {
const prev = info.languageService.getSemanticDiagnostics(filename);
const sourceFile: ts.SourceFile = info.project.getSourceFile(ts.toPath(filename, /*basePath*/ undefined, ts.createGetCanonicalFileName(info.serverHost.useCaseSensitiveFileNames)))!;
prev.push({
category: ts.DiagnosticCategory.Error,
file: sourceFile,
code: 9999,
length: 3,
messageText: customMessage,
start: 0
});
return prev;
};
return proxy;
},
onConfigurationChanged(config: any) {
customMessage = config.message;
}
}),
error: undefined
};
default:
return {
module: undefined,
error: new Error("Could not resolve module")
};
}
}
}
class FourslashSession extends ts.server.Session {
getText(fileName: string) {
return ts.getSnapshotText(this.projectService.getDefaultProjectForFile(ts.server.toNormalizedPath(fileName), /*ensureProject*/ true)!.getScriptSnapshot(fileName)!);
}
}
export class ServerLanguageServiceAdapter implements LanguageServiceAdapter {
private host: SessionClientHost;
private client: ts.server.SessionClient;
private server: FourslashSession;
constructor(cancellationToken?: ts.HostCancellationToken, options?: ts.CompilerOptions) {
// This is the main host that tests use to direct tests
const clientHost = new SessionClientHost(cancellationToken, options);
const client = new ts.server.SessionClient(clientHost);
// This host is just a proxy for the clientHost, it uses the client
// host to answer server queries about files on disk
const serverHost = new SessionServerHost(clientHost);
const opts: ts.server.SessionOptions = {
host: serverHost,
cancellationToken: ts.server.nullCancellationToken,
useSingleInferredProject: false,
useInferredProjectPerProjectRoot: false,
typingsInstaller: { ...ts.server.nullTypingsInstaller, globalTypingsCacheLocation: "/Library/Caches/typescript" },
byteLength: Utils.byteLength,
hrtime: process.hrtime,
logger: serverHost,
canUseEvents: true
};
this.server = new FourslashSession(opts);
// Fake the connection between the client and the server
serverHost.writeMessage = client.onMessage.bind(client);
clientHost.writeMessage = this.server.onMessage.bind(this.server);
// Wire the client to the host to get notifications when a file is open
// or edited.
clientHost.setClient(client);
// Set the properties
this.client = client;
this.host = clientHost;
}
getHost() { return this.host; }
getLanguageService(): ts.LanguageService { return this.client; }
getClassifier(): ts.Classifier { throw new Error("getClassifier is not available using the server interface."); }
getPreProcessedFileInfo(): ts.PreProcessedFileInfo { throw new Error("getPreProcessedFileInfo is not available using the server interface."); }
assertTextConsistent(fileName: string) {
const serverText = this.server.getText(fileName);
const clientText = this.host.readFile(fileName);
ts.Debug.assert(serverText === clientText, [
"Server and client text are inconsistent.",
"",
"\x1b[1mServer\x1b[0m\x1b[31m:",
serverText,
"",
"\x1b[1mClient\x1b[0m\x1b[31m:",
clientText,
"",
"This probably means something is wrong with the fourslash infrastructure, not with the test."
].join(ts.sys.newLine));
}
}
}
![swh spinner](/static/img/swh-spinner.gif)
Computing file changes ...