From a7ae427d02ac4423bd7e6090a30dd6ee3e209cc1 Mon Sep 17 00:00:00 2001 From: Wesley Wigham Date: Mon, 6 Jun 2016 12:38:54 -0700 Subject: [PATCH 1/7] Language service extensions and tests --- src/compiler/program.ts | 9 +- src/compiler/types.ts | 21 +- src/harness/external/chai.d.ts | 1 + src/harness/harnessLanguageService.ts | 3 + src/server/client.ts | 4 + src/server/editorServices.ts | 2 +- src/services/services.ts | 160 +++++- src/services/shims.ts | 10 + tests/cases/unittests/extensionAPI.ts | 763 +++++++++++++++++++++++++- 9 files changed, 950 insertions(+), 23 deletions(-) diff --git a/src/compiler/program.ts b/src/compiler/program.ts index 9d9204c3dfbfb..3aa9b498888f6 100644 --- a/src/compiler/program.ts +++ b/src/compiler/program.ts @@ -1251,6 +1251,7 @@ namespace ts { switch (ext.kind) { case ExtensionKind.SemanticLint: case ExtensionKind.SyntacticLint: + case ExtensionKind.LanguageService: if (typeof potentialExtension !== "function") { programDiagnostics.add(createCompilerDiagnostic( Diagnostics.Extension_0_exported_member_1_has_extension_kind_2_but_was_type_3_when_type_4_was_expected, @@ -1262,7 +1263,13 @@ namespace ts { )); return; } - (ext as (SemanticLintExtension | SyntacticLintExtension)).ctor = potentialExtension; + (ext as (SemanticLintExtension | SyntacticLintExtension | LanguageServiceExtension)).ctor = potentialExtension; + break; + default: + // Include a default case which just puts the extension unchecked onto the base extension + // This can allow language service extensions to query for custom extension kinds + (ext as any).__extension = potentialExtension; + break; } aggregate.push(ext as Extension); } diff --git a/src/compiler/types.ts b/src/compiler/types.ts index 0c2740ead64f7..2b6856cd798e6 100644 --- a/src/compiler/types.ts +++ b/src/compiler/types.ts @@ -2920,17 +2920,29 @@ namespace ts { new (typescript: typeof ts, checker: TypeChecker, args: any): LintWalker; } + export interface LanguageServiceHost {} // The members for these interfaces are provided in the services layer + export interface LanguageService {} + export interface LanguageServiceProvider {} + export interface DocumentRegistry {} + + export interface LanguageServiceProviderStatic { + new (typescript: typeof ts, host: LanguageServiceHost, service: LanguageService, registry: DocumentRegistry, args: any): LanguageServiceProvider; + } + export namespace ExtensionKind { export const SemanticLint: "semantic-lint" = "semantic-lint"; export type SemanticLint = "semantic-lint"; export const SyntacticLint: "syntactic-lint" = "syntactic-lint"; export type SyntacticLint = "syntactic-lint"; + export const LanguageService: "language-service" = "language-service"; + export type LanguageService = "language-service"; } - export type ExtensionKind = ExtensionKind.SemanticLint | ExtensionKind.SyntacticLint; + export type ExtensionKind = ExtensionKind.SemanticLint | ExtensionKind.SyntacticLint | ExtensionKind.LanguageService; export interface ExtensionCollectionMap { "syntactic-lint"?: SyntacticLintExtension[]; "semantic-lint"?: SemanticLintExtension[]; + "language-service"?: LanguageServiceExtension[]; [index: string]: Extension[] | undefined; } @@ -2950,7 +2962,12 @@ namespace ts { ctor: SemanticLintProviderStatic; } - export type Extension = SyntacticLintExtension | SemanticLintExtension; + // @kind(ExtensionKind.LanguageService) + export interface LanguageServiceExtension extends ExtensionBase { + ctor: LanguageServiceProviderStatic; + } + + export type Extension = SyntacticLintExtension | SemanticLintExtension | LanguageServiceExtension; export interface CompilerHost extends ModuleResolutionHost { getSourceFile(fileName: string, languageVersion: ScriptTarget, onError?: (message: string) => void): SourceFile; diff --git a/src/harness/external/chai.d.ts b/src/harness/external/chai.d.ts index 5e4e6e7d00010..ccd1de1bfb59f 100644 --- a/src/harness/external/chai.d.ts +++ b/src/harness/external/chai.d.ts @@ -175,5 +175,6 @@ declare module chai { function isOk(actual: any, message?: string): void; function isUndefined(value: any, message?: string): void; function isDefined(value: any, message?: string): void; + function deepEqual(actual: any, expected: any, message?: string): void; } } \ No newline at end of file diff --git a/src/harness/harnessLanguageService.ts b/src/harness/harnessLanguageService.ts index 8567a9109de45..cf0083505e35c 100644 --- a/src/harness/harnessLanguageService.ts +++ b/src/harness/harnessLanguageService.ts @@ -366,6 +366,9 @@ namespace Harness.LanguageService { getCompilerOptionsDiagnostics(): ts.Diagnostic[] { return unwrapJSONCallResult(this.shim.getCompilerOptionsDiagnostics()); } + getProgramDiagnostics(): ts.Diagnostic[] { + return unwrapJSONCallResult(this.shim.getProgramDiagnostics()); + } getSyntacticClassifications(fileName: string, span: ts.TextSpan): ts.ClassifiedSpan[] { return unwrapJSONCallResult(this.shim.getSyntacticClassifications(fileName, span.start, span.length)); } diff --git a/src/server/client.ts b/src/server/client.ts index 09cfa2ac739fc..b64048df34756 100644 --- a/src/server/client.ts +++ b/src/server/client.ts @@ -395,6 +395,10 @@ namespace ts.server { } getCompilerOptionsDiagnostics(): Diagnostic[] { + return this.getProgramDiagnostics(); + } + + getProgramDiagnostics(): Diagnostic[] { throw new Error("Not Implemented Yet."); } diff --git a/src/server/editorServices.ts b/src/server/editorServices.ts index 9c0662e5534a5..fc88af2b4ab45 100644 --- a/src/server/editorServices.ts +++ b/src/server/editorServices.ts @@ -1147,7 +1147,7 @@ namespace ts.server { info.setFormatOptions(this.getFormatCodeOptions()); this.filenameToScriptInfo[fileName] = info; if (!info.isOpen) { - info.fileWatcher = this.host.watchFile(fileName, _ => { this.watchedFileChanged(fileName); }); + info.fileWatcher = this.host.watchFile && this.host.watchFile(fileName, _ => { this.watchedFileChanged(fileName); }); } } } diff --git a/src/services/services.ts b/src/services/services.ts index fd72fd40eee7c..8c29d5d8097b3 100644 --- a/src/services/services.ts +++ b/src/services/services.ts @@ -1079,6 +1079,8 @@ namespace ts { resolveTypeReferenceDirectives?(typeDirectiveNames: string[], containingFile: string): ResolvedTypeReferenceDirective[]; directoryExists?(directoryName: string): boolean; getDirectories?(directoryName: string): string[]; + + loadExtension?(path: string): any; } // @@ -1091,10 +1093,13 @@ namespace ts { getSyntacticDiagnostics(fileName: string): Diagnostic[]; getSemanticDiagnostics(fileName: string): Diagnostic[]; - // TODO: Rename this to getProgramDiagnostics to better indicate that these are any - // diagnostics present for the program level, and not just 'options' diagnostics. + /** + * @deprecated Use getProgramDiagnostics instead. + */ getCompilerOptionsDiagnostics(): Diagnostic[]; + getProgramDiagnostics(): Diagnostic[]; + /** * @deprecated Use getEncodedSyntacticClassifications instead. */ @@ -1158,6 +1163,86 @@ namespace ts { dispose(): void; } + export interface LanguageServiceProvider { + // Overrides + + // A plugin can implement one of the override methods to replace the results that would + // be returned by the TypeScript language service. If a plugin returns a defined results + // (that is, is not undefined) then that result is used instead of invoking the + // corresponding TypeScript method. If multiple plugins are registered, they are + // consulted in the order they are returned from the host. The first defined result + // returned by a plugin is used and no other plugin overrides are consulted. + + getProgramDiagnostics?(): Diagnostic[]; + getSyntacticDiagnostics?(fileName: string): Diagnostic[]; + getSemanticDiagnostics?(fileName: string): Diagnostic[]; + getEncodedSyntacticClassifications?(fileName: string, span: TextSpan): Classifications; + getEncodedSemanticClassifications?(fileName: string, span: TextSpan): Classifications; + getCompletionsAtPosition?(fileName: string, position: number): CompletionInfo; + getCompletionEntryDetails?(fileName: string, position: number, entryName: string): CompletionEntryDetails; + getQuickInfoAtPosition?(fileName: string, position: number): QuickInfo; + getNameOrDottedNameSpan?(fileName: string, startPos: number, endPos: number): TextSpan; + getBreakpointStatementAtPosition?(fileName: string, position: number): TextSpan; + getSignatureHelpItems?(fileName: string, position: number): SignatureHelpItems; + getRenameInfo?(fileName: string, position: number): RenameInfo; + findRenameLocations?(fileName: string, position: number, findInStrings: boolean, findInComments: boolean): RenameLocation[]; + getDefinitionAtPosition?(fileName: string, position: number): DefinitionInfo[]; + getTypeDefinitionAtPosition?(fileName: string, position: number): DefinitionInfo[]; + getReferencesAtPosition?(fileName: string, position: number): ReferenceEntry[]; + findReferences?(fileName: string, position: number): ReferencedSymbol[]; + getDocumentHighlights?(fileName: string, position: number, filesToSearch: string[]): DocumentHighlights[]; + getNavigateToItems?(searchValue: string, maxResultCount: number): NavigateToItem[]; + getNavigationBarItems?(fileName: string): NavigationBarItem[]; + getOutliningSpans?(fileName: string): OutliningSpan[]; + getTodoComments?(fileName: string, descriptors: TodoCommentDescriptor[]): TodoComment[]; + getBraceMatchingAtPosition?(fileName: string, position: number): TextSpan[]; + getIndentationAtPosition?(fileName: string, position: number, options: EditorOptions): number; + getFormattingEditsForRange?(fileName: string, start: number, end: number, options: FormatCodeOptions): TextChange[]; + getFormattingEditsForDocument?(fileName: string, options: FormatCodeOptions): TextChange[]; + getFormattingEditsAfterKeystroke?(fileName: string, position: number, key: string, options: FormatCodeOptions): TextChange[]; + getDocCommentTemplateAtPosition?(fileName: string, position: number): TextInsertion; + + // Filters + + // A plugin can implement one of the filter methods to augment, extend or modify a result + // prior to the host receiving it. The TypeScript language service is invoked and the + // result is passed to the plugin as the value of the previous parameter. If more than one + // plugin is registered, the plugins are consulted in the order they are returned from the + // host. The value passed in as previous is the result returned by the prior plugin. If a + // plugin returns undefined, the result passed in as previous is used and the undefined + // result is ignored. All plugins are consulted before the result is returned to the host. + // If a plugin overrides behavior of the method, no filter methods are consulted. + + getProgramDiagnosticsFilter?(previous: Diagnostic[]): Diagnostic[]; + getSyntacticDiagnosticsFilter?(fileName: string, previous: Diagnostic[]): Diagnostic[]; + getSemanticDiagnosticsFilter?(fileName: string, previous: Diagnostic[]): Diagnostic[]; + getEncodedSyntacticClassificationsFilter?(fileName: string, span: TextSpan, previous: Classifications): Classifications; + getEncodedSemanticClassificationsFilter?(fileName: string, span: TextSpan, previous: Classifications): Classifications; + getCompletionsAtPositionFilter?(fileName: string, position: number, previous: CompletionInfo): CompletionInfo; + getCompletionEntryDetailsFilter?(fileName: string, position: number, entryName: string, previous: CompletionEntryDetails): CompletionEntryDetails; + getQuickInfoAtPositionFilter?(fileName: string, position: number, previous: QuickInfo): QuickInfo; + getNameOrDottedNameSpanFilter?(fileName: string, startPos: number, endPos: number, previous: TextSpan): TextSpan; + getBreakpointStatementAtPositionFilter?(fileName: string, position: number, previous: TextSpan): TextSpan; + getSignatureHelpItemsFilter?(fileName: string, position: number, previous: SignatureHelpItems): SignatureHelpItems; + getRenameInfoFilter?(fileName: string, position: number, previous: RenameInfo): RenameInfo; + findRenameLocationsFilter?(fileName: string, position: number, findInStrings: boolean, findInComments: boolean, previous: RenameLocation[]): RenameLocation[]; + getDefinitionAtPositionFilter?(fileName: string, position: number, previous: DefinitionInfo[]): DefinitionInfo[]; + getTypeDefinitionAtPositionFilter?(fileName: string, position: number, previous: DefinitionInfo[]): DefinitionInfo[]; + getReferencesAtPositionFilter?(fileName: string, position: number, previous: ReferenceEntry[]): ReferenceEntry[]; + findReferencesFilter?(fileName: string, position: number, previous: ReferencedSymbol[]): ReferencedSymbol[]; + getDocumentHighlightsFilter?(fileName: string, position: number, filesToSearch: string[], previous: DocumentHighlights[]): DocumentHighlights[]; + getNavigateToItemsFilter?(searchValue: string, maxResultCount: number, previous: NavigateToItem[]): NavigateToItem[]; + getNavigationBarItemsFilter?(fileName: string, previous: NavigationBarItem[]): NavigationBarItem[]; + getOutliningSpansFilter?(fileName: string, previous: OutliningSpan[]): OutliningSpan[]; + getTodoCommentsFilter?(fileName: string, descriptors: TodoCommentDescriptor[], previous: TodoComment[]): TodoComment[]; + getBraceMatchingAtPositionFilter?(fileName: string, position: number, previous: TextSpan[]): TextSpan[]; + getIndentationAtPositionFilter?(fileName: string, position: number, options: EditorOptions, previous: number): number; + getFormattingEditsForRangeFilter?(fileName: string, start: number, end: number, options: FormatCodeOptions, previous: TextChange[]): TextChange[]; + getFormattingEditsForDocumentFilter?(fileName: string, options: FormatCodeOptions, previous: TextChange[]): TextChange[]; + getFormattingEditsAfterKeystrokeFilter?(fileName: string, position: number, key: string, options: FormatCodeOptions, previous: TextChange[]): TextChange[]; + getDocCommentTemplateAtPositionFilter?(fileName: string, position: number, previous: TextInsertion): TextInsertion; + } + export interface Classifications { spans: number[]; endOfLineState: EndOfLineState; @@ -2914,6 +2999,69 @@ namespace ts { } export function createLanguageService(host: LanguageServiceHost, + documentRegistry: DocumentRegistry = createDocumentRegistry(host.useCaseSensitiveFileNames && host.useCaseSensitiveFileNames(), host.getCurrentDirectory())): LanguageService { + const baseService = createUnextendedLanguageService(host, documentRegistry); + const extensions = baseService.getProgram().getCompilerExtensions()["language-service"]; + const instantiatedExtensions = map(extensions, extension => new extension.ctor(ts, host, baseService, documentRegistry, extension.args)); + + function wrap(key: string): Function { + return (...args: any[]) => { + if (instantiatedExtensions && instantiatedExtensions.length) { + for (let i = 0; i < instantiatedExtensions.length; i++) { + const extension = instantiatedExtensions[i]; + if ((extension as any)[key]) { + return (extension as any)[key](...args); + } + } + let result: any = (baseService as any)[key](...args); + const filterKey = `${key}Filter`; + for (let i = 0; i < instantiatedExtensions.length; i++) { + const extension = instantiatedExtensions[i]; + if ((extension as any)[filterKey]) { + result = (extension as any)[filterKey](...args, result); + } + } + return result; + } + return (baseService as any)[key](...args); + }; + } + + function buildWrappedService(underlyingMembers: Map, wrappedMembers: string[]): LanguageService { + // Add wrapped members to map + forEach(wrappedMembers, member => { + underlyingMembers[member] = wrap(member); + }); + // Map getProgramDiagnostics to deprecated getCompilerOptionsDiagnostics + underlyingMembers["getCompilerOptionsDiagnostics"] = underlyingMembers["getProgramDiagnostics"]; + return underlyingMembers as LanguageService; + } + + return buildWrappedService({ + cleanupSemanticCache: () => baseService.cleanupSemanticCache(), + getSyntacticClassifications: (fileName: string, span: TextSpan) => baseService.getSyntacticClassifications(fileName, span), + getSemanticClassifications: (fileName: string, span: TextSpan) => baseService.getSemanticClassifications(fileName, span), + getOccurrencesAtPosition: (fileName: string, position: number) => baseService.getOccurrencesAtPosition(fileName, position), + isValidBraceCompletionAtPostion: (fileName: string, pos: number, openingBrace: number) => baseService.isValidBraceCompletionAtPostion(fileName, pos, openingBrace), + getEmitOutput: (fileName: string) => baseService.getEmitOutput(fileName), + getProgram: () => baseService.getProgram(), + getNonBoundSourceFile: (fileName: string) => baseService.getNonBoundSourceFile(fileName), + dispose: () => baseService.dispose(), + }, [ + "getSyntacticDiagnostics", "getSemanticDiagnostics", "getProgramDiagnostics", + "getEncodedSyntacticClassifications", "getEncodedSemanticClassifications", "getCompletionsAtPosition", + "getCompletionEntryDetails", "getQuickInfoAtPosition", "getNameOrDottedNameSpan", + "getBreakpointStatementAtPosition", "getSignatureHelpItems", "getRenameInfo", + "findRenameLocations", "getDefinitionAtPosition", "getTypeDefinitionAtPosition", + "getReferencesAtPosition", "findReferences", "getDocumentHighlights", + "getNavigateToItems", "getNavigationBarItems", "getOutliningSpans", + "getTodoComments", "getBraceMatchingAtPosition", "getIndentationAtPosition", + "getFormattingEditsForRange", "getFormattingEditsForDocument", "getFormattingEditsAfterKeystroke", + "getDocCommentTemplateAtPosition" + ]); + } + + export function createUnextendedLanguageService(host: LanguageServiceHost, documentRegistry: DocumentRegistry = createDocumentRegistry(host.useCaseSensitiveFileNames && host.useCaseSensitiveFileNames(), host.getCurrentDirectory())): LanguageService { const syntaxTreeCache: SyntaxTreeCache = new SyntaxTreeCache(host); @@ -3020,6 +3168,9 @@ namespace ts { }, getDirectories: path => { return host.getDirectories ? host.getDirectories(path) : []; + }, + loadExtension: path => { + return host.loadExtension ? host.loadExtension(path) : undefined; } }; if (host.trace) { @@ -3197,7 +3348,7 @@ namespace ts { return concatenate(semanticDiagnostics, declarationDiagnostics); } - function getCompilerOptionsDiagnostics() { + function getProgramDiagnostics() { synchronizeHostData(); return program.getOptionsDiagnostics(cancellationToken).concat( program.getGlobalDiagnostics(cancellationToken)); @@ -8108,7 +8259,8 @@ namespace ts { cleanupSemanticCache, getSyntacticDiagnostics, getSemanticDiagnostics, - getCompilerOptionsDiagnostics, + getCompilerOptionsDiagnostics: getProgramDiagnostics, + getProgramDiagnostics, getSyntacticClassifications, getSemanticClassifications, getEncodedSyntacticClassifications, diff --git a/src/services/shims.ts b/src/services/shims.ts index 73c516ca19d65..9ffa37b36c9b0 100644 --- a/src/services/shims.ts +++ b/src/services/shims.ts @@ -119,6 +119,7 @@ namespace ts { getSyntacticDiagnostics(fileName: string): string; getSemanticDiagnostics(fileName: string): string; getCompilerOptionsDiagnostics(): string; + getProgramDiagnostics(): string; getSyntacticClassifications(fileName: string, start: number, length: number): string; getSemanticClassifications(fileName: string, start: number, length: number): string; @@ -678,6 +679,15 @@ namespace ts { }); } + public getProgramDiagnostics(): string { + return this.forwardJSONCall( + "getProgramDiagnostics()", + () => { + const diagnostics = this.languageService.getProgramDiagnostics(); + return this.realizeDiagnostics(diagnostics); + }); + } + /// QUICKINFO /** diff --git a/tests/cases/unittests/extensionAPI.ts b/tests/cases/unittests/extensionAPI.ts index b18168a4c724d..2583e855e0a29 100644 --- a/tests/cases/unittests/extensionAPI.ts +++ b/tests/cases/unittests/extensionAPI.ts @@ -20,10 +20,10 @@ namespace ts { } for (let i = 0; i < expectedDiagnosticCodes.length; i++) { - assert.equal(expectedDiagnosticCodes[i], diagnostics[i] && diagnostics[i].code, `Could not find expeced diagnostic.`); + assert.equal(expectedDiagnosticCodes[i], diagnostics[i] && diagnostics[i].code, `Could not find expeced diagnostic: (expected) [${expectedDiagnosticCodes.toString()}] vs (actual) [${map(diagnostics, d => d.code).toString()}]. First diagnostic: ${prettyPrintDiagnostic(diagnostics[0])}`); } if (expectedDiagnosticCodes.length === 0 && diagnostics.length) { - throw new Error(`Unexpected diagnostic (${diagnostics.length - 1} more): ${prettyPrintDiagnostic(diagnostics[0])}`); + throw new Error(`Unexpected diagnostic (${map(diagnostics, d => d.code).toString()}): ${prettyPrintDiagnostic(diagnostics[0])}`); } assert.equal(diagnostics.length, expectedDiagnosticCodes.length, "Resuting diagnostics count does not match expected"); } @@ -43,10 +43,12 @@ namespace ts { let virtualFs: Map = {}; - const getCanonicalFileName = createGetCanonicalFileName(true); + const innerCanonicalName = createGetCanonicalFileName(true); + const getCanonicalFileName = (fileName: string) => toPath(fileName, "/", innerCanonicalName); function loadSetIntoFsAt(set: Map, prefix: string) { - forEachKey(set, key => void (virtualFs[getCanonicalFileName(combinePaths(prefix, key))] = set[key])); + // Load a fileset at the given location, but exclude the /lib/ dir from the added set + forEachKey(set, key => startsWith(key, "/lib/") ? void 0 : void (virtualFs[getCanonicalFileName(prefix + key)] = set[key])); } function loadSetIntoFs(set: Map) { @@ -95,7 +97,7 @@ namespace ts { (name: string) => { return this.loadExtension( this.getCanonicalFileName( - ts.resolveModuleName(name, fullPath, {module: ts.ModuleKind.CommonJS}, this, true).resolvedModule.resolvedFileName + resolveModuleName(name, fullPath, {module: ModuleKind.CommonJS}, this, true).resolvedModule.resolvedFileName ) ); } @@ -107,6 +109,47 @@ namespace ts { } }; + function makeMockLSHost(files: string[], options: CompilerOptions): LanguageServiceHost { + files = filter(files, file => !endsWith(file, ".json")); + return { + getCompilationSettings: () => options, + getScriptFileNames: () => files, + getScriptVersion(fileName) { + return "1"; + }, + getScriptSnapshot(fileName): IScriptSnapshot { + const fileContents = virtualFs[getCanonicalFileName(fileName)]; + if (!fileContents) return; + return ScriptSnapshot.fromString(fileContents); + }, + getCurrentDirectory() { + return ""; + }, + getDefaultLibFileName() { + return "/lib/lib.d.ts"; + }, + loadExtension(path) { + const fullPath = getCanonicalFileName(path); + const m = {exports: {}}; + ((module, exports, require) => { eval(virtualFs[fullPath]); })( + m, + m.exports, + (name: string) => { + return this.loadExtension( + getCanonicalFileName( + resolveModuleName(name, fullPath, {module: ModuleKind.CommonJS}, mockHost, true).resolvedModule.resolvedFileName + ) + ); + } + ); + return m.exports; + }, + trace(s) { + console.log(s); + } + }; + }; + const extensionAPI: Map = { "package.json": `{ "name": "typescript-plugin-api", @@ -130,22 +173,51 @@ export abstract class SemanticLintWalker implements tsi.LintWalker { constructor(protected ts: typeof tsi, protected checker: tsi.TypeChecker, protected args: any) {} abstract visit(node: tsi.Node, accept: tsi.LintAcceptMethod, error: tsi.LintErrorMethod): void; } + +export abstract class LanguageServiceProvider implements tsi.LanguageServiceProvider { + private static __tsCompilerExtensionKind: tsi.ExtensionKind.LanguageService = "language-service"; + constructor(protected ts: typeof tsi, protected host: tsi.LanguageServiceHost, protected service: tsi.LanguageService, protected registry: tsi.DocumentRegistry, args: any) {} +} ` }; // Compile extension API once (generating .d.ts and .js) - function compile(fileset: Map, options: ts.CompilerOptions): Diagnostic[] { - loadSetIntoFs(virtualLib); - loadSetIntoFs(fileset); + function languageServiceCompile(typescriptFiles: string[], options: CompilerOptions, additionalVerifiers?: (service: LanguageService) => void): Diagnostic[] { + options.allowJs = true; + options.noEmit = true; + const service = createLanguageService(makeMockLSHost(getKeys(virtualFs), options)); + + if (additionalVerifiers) { + additionalVerifiers(service); + } + + const diagnostics = concatenate(concatenate( + service.getProgramDiagnostics(), + flatten(map(typescriptFiles, fileName => service.getSyntacticDiagnostics(getCanonicalFileName(fileName))))), + flatten(map(typescriptFiles, fileName => service.getSemanticDiagnostics(getCanonicalFileName(fileName))))); - const program = createProgram(filter(getKeys(fileset), name => name != "package.json"), options, mockHost); + return sortAndDeduplicateDiagnostics(diagnostics); + } + + type VirtualCompilationFunction = (files: string[], options: CompilerOptions, additionalVerifiers?: () => void) => Diagnostic[]; + + function programCompile(typescriptFiles: string[], options: CompilerOptions): Diagnostic[] { + const program = createProgram(typescriptFiles, options, mockHost); program.emit(); return ts.getPreEmitDiagnostics(program); } - function buildMap(map: Map, out: Map, compilerOptions?: CompilerOptions, shouldError?: boolean): Diagnostic[] { - const diagnostics = compile(map, compilerOptions ? compilerOptions : {module: ModuleKind.CommonJS, declaration: true}); + function compile(fileset: Map, options: ts.CompilerOptions, compileFunc: VirtualCompilationFunction, additionalVerifiers?: () => void): Diagnostic[] { + loadSetIntoFs(virtualLib); + loadSetIntoFs(fileset); + + const typescriptFiles = filter(getKeys(fileset), name => endsWith(name, ".ts")); + return compileFunc(typescriptFiles, options, additionalVerifiers); + } + + function buildMap(compileFunc: VirtualCompilationFunction, map: Map, out: Map, compilerOptions?: CompilerOptions, shouldError?: boolean, additionalVerifiers?: () => void): Diagnostic[] { + const diagnostics = compile(map, compilerOptions ? compilerOptions : {module: ModuleKind.CommonJS, declaration: true}, compileFunc, additionalVerifiers); if (shouldError && diagnostics && diagnostics.length) { for (let i = 0; i < diagnostics.length; i++) { console.log(prettyPrintDiagnostic(diagnostics[i])); @@ -156,7 +228,7 @@ export abstract class SemanticLintWalker implements tsi.LintWalker { virtualFs = {}; return diagnostics; } - buildMap(extensionAPI, extensionAPI, {module: ModuleKind.CommonJS, declaration: true, baseUrl: ".", paths: {"typescript": ["/lib/typescript.d.ts"]}}, /*shouldError*/true); + buildMap(programCompile, extensionAPI, extensionAPI, {module: ModuleKind.CommonJS, declaration: true, baseUrl: ".", paths: {"typescript": ["/lib/typescript.d.ts"]}}, /*shouldError*/true); const extensions: Map> = { "test-syntactic-lint": { @@ -294,6 +366,417 @@ export class IsValueBar extends SemanticLintWalker { accept(); } } +` + }, + "test-language-service": { + "package.json": `{ + "name": "test-language-service", + "version": "1.0.0", + "description": "", + "main": "index.js", + "author": "" +}`, + "index.ts": ` +import {LanguageServiceProvider} from "typescript-plugin-api"; + +export default class extends LanguageServiceProvider { + constructor(ts, host, service, registry, args) { super(ts, host, service, registry, args); } + getProgramDiagnosticsFilter(previous) { + previous.push({ + file: undefined, + start: undefined, + length: undefined, + messageText: "Test language service plugin loaded!", + category: 2, + code: "test-plugin-loaded", + }); + return previous; + } +} +` + }, + "test-service-overrides": { + "package.json": `{ + "name": "test-service-overrides", + "version": "1.0.0", + "description": "", + "main": "index.js", + "author": "" +}`, + "index.ts": ` +import {LanguageServiceProvider} from "typescript-plugin-api"; +import * as ts from "typescript"; + +export default class extends LanguageServiceProvider { + constructor(ts, host, service, registry, args) { super(ts, host, service, registry, args); } + getProgramDiagnostics() { + return [{ + file: undefined, + start: undefined, + length: undefined, + messageText: "Program diagnostics replaced!", + category: 2, + code: "program-diagnostics-replaced", + }]; + } + getSyntacticDiagnostics(fileName) { + return [{ + file: undefined, + start: undefined, + length: undefined, + messageText: "Syntactic diagnostics replaced!", + category: 2, + code: "syntactic-diagnostics-replaced", + }]; + } + getSemanticDiagnostics(fileName) { + return [{ + file: undefined, + start: undefined, + length: undefined, + messageText: "Semantic diagnostics replaced!", + category: 2, + code: "semantic-diagnostics-replaced", + }]; + } + getEncodedSyntacticClassifications(fileName, span) { + return { + spans: [span.start, span.length, this.ts.endsWith(fileName, "atotc.ts") ? this.ts.ClassificationType.text : this.ts.ClassificationType.comment], + endOfLineState: this.ts.EndOfLineState.None + }; + } + getEncodedSemanticClassifications(fileName, span) { + return { + spans: [span.start, span.length, this.ts.endsWith(fileName, "atotc.ts") ? this.ts.ClassificationType.moduleName : this.ts.ClassificationType.comment], + endOfLineState: this.ts.EndOfLineState.None + }; + } + getCompletionsAtPosition(fileName, position) { + return { + isMemberCompletion: false, + isNewIdentifierLocation: false, + entries: [{name: fileName, kind: 0, kindModifiers: 0, sortText: fileName}] + }; + } + getCompletionEntryDetails(fileName, position, entryName) { + return { + name: fileName, + kind: position.toString(), + kindModifiers: entryName, + displayParts: [], + documentation: [], + }; + } + getQuickInfoAtPosition(fileName, position) { + return {}; + } + getNameOrDottedNameSpan(fileName, startPos, endPos) { + return {}; + } + getBreakpointStatementAtPosition(fileName, position) { + return {}; + } + getSignatureHelpItems(fileName, position) { + return {}; + } + getRenameInfo(fileName, position) { + return {}; + } + findRenameLocations(fileName, position, findInStrings, findInComments) { + return {}; + } + getDefinitionAtPosition(fileName, position) { + return {}; + } + getTypeDefinitionAtPosition(fileName, position) { + return {}; + } + getReferencesAtPosition(fileName, position) { + return {}; + } + findReferences(fileName, position) { + return {}; + } + getDocumentHighlights(fileName, position, filesToSearch) { + return {}; + } + getNavigateToItems(searchValue, maxResultCount) { + return {}; + } + getNavigationBarItems(fileName) { + return {}; + } + getOutliningSpans(fileName) { + return {}; + } + getTodoComments(fileName, descriptors) { + return {}; + } + getBraceMatchingAtPosition(fileName, position) { + return {}; + } + getIndentationAtPosition(fileName, position, options) { + return {}; + } + getFormattingEditsForRange(fileName, start, end, options) { + return {}; + } + getFormattingEditsForDocument(fileName, options) { + return {}; + } + getFormattingEditsAfterKeystroke(fileName, position, key, options) { + return {}; + } + getDocCommentTemplateAtPosition(fileName, position) { + return {}; + } +} +` + }, + "test-service-filters": { + "package.json": `{ + "name": "test-service-filters", + "version": "1.0.0", + "description": "", + "main": "index.js", + "author": "" +}`, + "index.ts": ` +import {LanguageServiceProvider} from "typescript-plugin-api"; + +import * as ts from "typescript"; + +export default class extends LanguageServiceProvider { + constructor(ts, host, service, registry, args) { super(ts, host, service, registry, args); } + getProgramDiagnosticsFilter(previous) { + return [{ + file: undefined, + start: undefined, + length: undefined, + messageText: "Program diagnostics replaced!", + category: 2, + code: "program-diagnostics-replaced", + }]; + } + getSyntacticDiagnosticsFilter(fileName, previous) { + return [{ + file: undefined, + start: undefined, + length: undefined, + messageText: "Syntactic diagnostics replaced!", + category: 2, + code: "syntactic-diagnostics-replaced", + }]; + } + getSemanticDiagnosticsFilter(fileName, previous) { + return [{ + file: undefined, + start: undefined, + length: undefined, + messageText: "Semantic diagnostics replaced!", + category: 2, + code: "semantic-diagnostics-replaced", + }]; + } + getEncodedSyntacticClassificationsFilter(fileName, span, previous) { + return { + spans: [span.start, span.length, this.ts.endsWith(fileName, "atotc.ts") ? this.ts.ClassificationType.text : this.ts.ClassificationType.comment], + endOfLineState: this.ts.EndOfLineState.None + }; + } + getEncodedSemanticClassificationsFilter(fileName, span, previous) { + return { + spans: [span.start, span.length, this.ts.endsWith(fileName, "atotc.ts") ? this.ts.ClassificationType.moduleName : this.ts.ClassificationType.comment], + endOfLineState: this.ts.EndOfLineState.None + }; + } + getCompletionsAtPositionFilter(fileName, position, previous) { + return { + isMemberCompletion: false, + isNewIdentifierLocation: false, + entries: [{name: fileName, kind: 0, kindModifiers: 0, sortText: fileName}] + }; + } + getCompletionEntryDetailsFilter(fileName, position, entryName, previous) { + return { + name: fileName, + kind: position.toString(), + kindModifiers: entryName, + displayParts: [], + documentation: [], + }; + } + getQuickInfoAtPositionFilter(fileName, position, previous) { + return {}; + } + getNameOrDottedNameSpanFilter(fileName, startPos, endPos, previous) { + return {}; + } + getBreakpointStatementAtPositionFilter(fileName, position, previous) { + return {}; + } + getSignatureHelpItemsFilter(fileName, position, previous) { + return {}; + } + getRenameInfoFilter(fileName, position, previous) { + return {}; + } + findRenameLocationsFilter(fileName, position, findInStrings, findInComments, previous) { + return {}; + } + getDefinitionAtPositionFilter(fileName, position, previous) { + return {}; + } + getTypeDefinitionAtPositionFilter(fileName, position, previous) { + return {}; + } + getReferencesAtPositionFilter(fileName, position, previous) { + return {}; + } + findReferencesFilter(fileName, position, previous) { + return {}; + } + getDocumentHighlightsFilter(fileName, position, filesToSearch, previous) { + return {}; + } + getNavigateToItemsFilter(searchValue, maxResultCount, previous) { + return {}; + } + getNavigationBarItemsFilter(fileName, previous) { + return {}; + } + getOutliningSpansFilter(fileName, previous) { + return {}; + } + getTodoCommentsFilter(fileName, descriptors, previous) { + return {}; + } + getBraceMatchingAtPositionFilter(fileName, position, previous) { + return {}; + } + getIndentationAtPositionFilter(fileName, position, options, previous) { + return {}; + } + getFormattingEditsForRangeFilter(fileName, start, end, options, previous) { + return {}; + } + getFormattingEditsForDocumentFilter(fileName, options, previous) { + return {}; + } + getFormattingEditsAfterKeystrokeFilter(fileName, position, key, options, previous) { + return {}; + } + getDocCommentTemplateAtPositionFilter(fileName, position, previous) { + return {}; + } +} +` + }, + "test-service-passthru": { + "package.json": `{ + "name": "test-service-passthru", + "version": "1.0.0", + "description": "", + "main": "index.js", + "author": "" +}`, + "index.ts": ` +import {LanguageServiceProvider} from "typescript-plugin-api"; + +export default class extends LanguageServiceProvider { + constructor(ts, host, service, registry, args) { super(ts, host, service, registry, args); } + getProgramDiagnosticsFilter(previous) { + return previous; + } + getSyntacticDiagnosticsFilter(fileName, previous) { + return previous; + } + getSemanticDiagnosticsFilter(fileName, previous) { + return previous; + } +} +` + }, + "test-service-chain": { + "package.json": `{ + "name": "test-service-chain", + "version": "1.0.0", + "description": "", + "main": "index.js", + "author": "" +}`, + "index.ts": ` +import {LanguageServiceProvider} from "typescript-plugin-api"; + +export class AddsDiagnostics extends LanguageServiceProvider { + constructor(ts, host, service, registry, args) { super(ts, host, service, registry, args); } + getProgramDiagnosticsFilter(previous) { + return previous.concat([{ + file: undefined, + start: undefined, + length: undefined, + messageText: "Program diagnostics amended!", + category: 2, + code: "program-diagnostics-amended", + }]); + } + getSyntacticDiagnosticsFilter(fileName, previous) { + return previous.concat([{ + file: undefined, + start: undefined, + length: undefined, + messageText: "Syntactic diagnostics amended!", + category: 2, + code: "syntactic-diagnostics-amended", + }]); + } + getSemanticDiagnosticsFilter(fileName, previous) { + return previous.concat([{ + file: undefined, + start: undefined, + length: undefined, + messageText: "Semantic diagnostics amended!", + category: 2, + code: "semantic-diagnostics-amended", + }]); + } +} + +// Since this is exported second, it should be second in the chain. Probably. +// This is honestly dependent on js host key ordering +export class MutatesAddedDiagnostics extends LanguageServiceProvider { + constructor(ts, host, service, registry, args) { super(ts, host, service, registry, args); } + getProgramDiagnosticsFilter(previous) { + return previous.map(prev => prev.code === "program-diagnostics-amended" ? { + file: undefined, + start: undefined, + length: undefined, + messageText: "Program diagnostics mutated!", + category: 2, + code: "program-diagnostics-mutated", + } : prev); + } + getSyntacticDiagnosticsFilter(fileName, previous) { + return previous.map(prev => prev.code === "syntactic-diagnostics-amended" ? { + file: undefined, + start: undefined, + length: undefined, + messageText: "Syntactic diagnostics mutated!", + category: 2, + code: "syntactic-diagnostics-mutated", + } : prev); + } + getSemanticDiagnosticsFilter(fileName, previous) { + return previous.map(prev => prev.code === "semantic-diagnostics-amended" ? { + file: undefined, + start: undefined, + length: undefined, + messageText: "Semantic diagnostics mutated!", + category: 2, + code: "semantic-diagnostics-mutated", + } : prev); + } +} ` } }; @@ -301,15 +784,15 @@ export class IsValueBar extends SemanticLintWalker { // Compile each extension once with the extension API in its node_modules folder (also generating .d.ts and .js) forEachKey(extensions, extName => { loadSetIntoFsAt(extensionAPI, "/node_modules/typescript-plugin-api"); - buildMap(extensions[extName], extensions[extName], {module: ModuleKind.CommonJS, declaration: true, experimentalDecorators: true, baseUrl: "/", paths: {"typescript": ["lib/typescript.d.ts"]}}, /*shouldError*/true); + buildMap(programCompile, extensions[extName], extensions[extName], {module: ModuleKind.CommonJS, declaration: true, experimentalDecorators: true, baseUrl: "/", paths: {"typescript": ["lib/typescript.d.ts"]}}, /*shouldError*/true); }); /** * Setup a new test, where all extensions specified in the options hash are available in a node_modules folder, alongside the extension API */ - function test(sources: Map, options: ExtensionTestOptions) { + function test(sources: Map, options: ExtensionTestOptions, compileFunc: VirtualCompilationFunction = programCompile, additionalVerifiers?: (...args: any[]) => void) { forEach(options.availableExtensions, ext => loadSetIntoFsAt(extensions[ext], `/node_modules/${ext}`)); - const diagnostics = buildMap(sources, sources, options.compilerOptions); + const diagnostics = buildMap(compileFunc, sources, sources, options.compilerOptions, /*shouldError*/false, additionalVerifiers); checkDiagnostics(diagnostics, options.expectedDiagnostics); } @@ -451,5 +934,255 @@ export class IsValueBar extends SemanticLintWalker { } }); }); + + it("can load language service rules and add program diagnostics", () => { + test({ + "main.ts": "console.log('Did you know? The empty string is falsey.')" + }, { + availableExtensions: ["test-language-service"], + expectedDiagnostics: ["test-plugin-loaded"], + compilerOptions: { + extensions: ["test-language-service"], + module: ModuleKind.CommonJS, + } + }, languageServiceCompile); + }); + + const atotcFile = getCanonicalFileName("atotc.ts"); + const atotcText = ` +It was the best of times, it was the worst of times, +it was the age of wisdom, it was the age of foolishness, +it was the epoch of belief, it was the epoch of incredulity, +it was the season of Light, it was the season of Darkness, +it was the spring of hope, it was the winter of despair, +we had everything before us, we had nothing before us, +we were all going direct to Heaven, we were all going direct +the other way--in short, the period was so far like the present +period, that some of its noisiest authorities insisted on its +being received, for good or for evil, in the superlative degree +of comparison only. +`; + const testDummyLS = (service: LanguageService) => { + assert.deepEqual(service.getEncodedSyntacticClassifications(atotcFile, {start: 0, length: 24}), + {spans: [0, 24, ClassificationType.text], endOfLineState: EndOfLineState.None}, + "Syntactic classifications did not match!"); + assert.deepEqual(service.getEncodedSemanticClassifications(atotcFile, {start: 24, length: 42}), + {spans: [24, 42, ClassificationType.moduleName], endOfLineState: EndOfLineState.None}, + "Semantic classifications did not match!"); + assert.deepEqual(service.getCompletionsAtPosition(atotcFile, 0), { + isMemberCompletion: false, + isNewIdentifierLocation: false, + entries: [{name: atotcFile, kind: 0, kindModifiers: 0, sortText: atotcFile}] + }, "Completions did not match!"); + assert.deepEqual(service.getCompletionEntryDetails(atotcFile, 0, "first"), { + name: atotcFile, + kind: (0).toString(), + kindModifiers: "first", + displayParts: [], + documentation: [], + }, "Completion details did not match!"); + assert.deepEqual(service.getQuickInfoAtPosition(atotcFile, 0), {}, "Quick info did not match!"); + assert.deepEqual(service.getNameOrDottedNameSpan(atotcFile, 0, 0), {}, "Name or dotted span info did not match!"); + assert.deepEqual(service.getBreakpointStatementAtPosition(atotcFile, 0), {}, "Breakpoint statement info did not match!"); + assert.deepEqual(service.getSignatureHelpItems(atotcFile, 0), {}, "Signature help items did not match!"); + assert.deepEqual(service.getRenameInfo(atotcFile, 0), {}, "Rename info did not match!"); + assert.deepEqual(service.findRenameLocations(atotcFile, 0, false, false), {}, "Rename locations did not match!"); + assert.deepEqual(service.getDefinitionAtPosition(atotcFile, 0), {}, "Definition info did not match!"); + assert.deepEqual(service.getTypeDefinitionAtPosition(atotcFile, 0), {}, "Type definition info did not match!"); + assert.deepEqual(service.getReferencesAtPosition(atotcFile, 0), {}, "References did not match!"); + assert.deepEqual(service.findReferences(atotcFile, 0), {}, "Find references did not match!"); + assert.deepEqual(service.getDocumentHighlights(atotcFile, 0, []), {}, "Document highlights did not match!"); + assert.deepEqual(service.getNavigateToItems(atotcFile), {}, "NavTo items did not match!"); + assert.deepEqual(service.getNavigationBarItems(atotcFile), {}, "NavBar items did not match!"); + assert.deepEqual(service.getOutliningSpans(atotcFile), {}, "Outlining spans did not match!"); + assert.deepEqual(service.getTodoComments(atotcFile, []), {}, "Todo comments did not match!"); + assert.deepEqual(service.getBraceMatchingAtPosition(atotcFile, 0), {}, "Brace positions did not match!"); + assert.deepEqual(service.getIndentationAtPosition(atotcFile, 0, {} as EditorOptions), {}, "Indentation positions did not match!"); + assert.deepEqual(service.getFormattingEditsForRange(atotcFile, 0, 1, {} as FormatCodeOptions), {}, "Range edits did not match!"); + assert.deepEqual(service.getFormattingEditsForDocument(atotcFile, {} as FormatCodeOptions), {}, "Document edits did not match!"); + assert.deepEqual(service.getFormattingEditsAfterKeystroke(atotcFile, 0, "q", {} as FormatCodeOptions), {}, "Keystroke edits did not match!"); + assert.deepEqual(service.getDocCommentTemplateAtPosition(atotcFile, 0), {}, "Doc comment template did not match!"); + }; + + it("can override all language service functionality", () => { + test({ + [atotcFile]: atotcText + }, { + availableExtensions: ["test-service-overrides"], + expectedDiagnostics: ["program-diagnostics-replaced", "semantic-diagnostics-replaced", "syntactic-diagnostics-replaced"], + compilerOptions: { + extensions: ["test-service-overrides"], + module: ModuleKind.CommonJS, + } + }, languageServiceCompile, testDummyLS); + }); + + it("can filter all language service functionality", () => { + test({ + [atotcFile]: atotcText + }, { + availableExtensions: ["test-service-filters"], + expectedDiagnostics: ["program-diagnostics-replaced", "semantic-diagnostics-replaced", "syntactic-diagnostics-replaced"], + compilerOptions: { + extensions: ["test-service-filters"], + module: ModuleKind.CommonJS, + } + }, languageServiceCompile, testDummyLS); + }); + + it("can filter without altering functionality", () => { + test({ + ["main.ts"]: "console.log('Hello, test.') -" + }, { + availableExtensions: ["test-service-passthru"], + expectedDiagnostics: [2362, 1109], + compilerOptions: { + extensions: ["test-service-passthru"], + module: ModuleKind.CommonJS, + } + }, languageServiceCompile); + }); + + it("can filter and mutate while chaining plugins", () => { + test({ + ["main.ts"]: "console.log('Hello, test.') -" + }, { + availableExtensions: ["test-service-chain"], + expectedDiagnostics: ["program-diagnostics-mutated", "semantic-diagnostics-mutated", "syntactic-diagnostics-mutated", 2362, 1109], + compilerOptions: { + extensions: ["test-service-chain"], + module: ModuleKind.CommonJS, + } + }, languageServiceCompile); + }); + + it("can run all lint plugins in the language service", () => { + test({ + "main.ts": `console.log("Hello, world!");`, + }, { + availableExtensions: ["test-syntactic-lint"], + expectedDiagnostics: [], + compilerOptions: { + extensions: ["test-syntactic-lint"], + module: ModuleKind.CommonJS, + } + }, languageServiceCompile); + + test({ + "main.ts": `interface Foo {a; b;}`, + }, { + availableExtensions: ["test-syntactic-lint"], + expectedDiagnostics: ["test-syntactic-lint"], + compilerOptions: { + extensions: ["test-syntactic-lint"], + module: ModuleKind.CommonJS, + } + }, languageServiceCompile); + + test({ + "main.ts": `console.log("Hello, world!");`, + }, { + availableExtensions: ["test-semantic-lint"], + expectedDiagnostics: [], + compilerOptions: { + extensions: ["test-semantic-lint"], + module: ModuleKind.CommonJS, + } + }, languageServiceCompile); + + test({ + "main.ts": `const s: "foo" = "foo";`, + }, { + availableExtensions: ["test-semantic-lint"], + expectedDiagnostics: ["test-semantic-lint", "test-semantic-lint"], + compilerOptions: { + extensions: ["test-semantic-lint"], + module: ModuleKind.CommonJS, + } + }, languageServiceCompile); + + test({ + "main.ts": `console.log("Hello, world!");`, + }, { + availableExtensions: ["test-syntactic-lint", "test-semantic-lint"], + expectedDiagnostics: [], + compilerOptions: { + extensions: ["test-syntactic-lint", "test-semantic-lint"], + module: ModuleKind.CommonJS, + } + }, languageServiceCompile); + + test({ + "main.ts": `const s: "foo" = "foo";`, + }, { + availableExtensions: ["test-syntactic-lint", "test-semantic-lint"], + expectedDiagnostics: ["test-semantic-lint", "test-semantic-lint"], + compilerOptions: { + extensions: ["test-syntactic-lint", "test-semantic-lint"], + module: ModuleKind.CommonJS, + } + }, languageServiceCompile); + + test({ + "main.ts": `interface Foo {a; b;}`, + }, { + availableExtensions: ["test-syntactic-lint", "test-semantic-lint"], + expectedDiagnostics: ["test-syntactic-lint"], + compilerOptions: { + extensions: ["test-syntactic-lint", "test-semantic-lint"], + module: ModuleKind.CommonJS, + } + }, languageServiceCompile); + + test({ + "main.ts": `interface Foo {a; b;} + const s: "foo" = "foo";`, + }, { + availableExtensions: ["test-syntactic-lint", "test-semantic-lint"], + expectedDiagnostics: ["test-syntactic-lint", "test-semantic-lint", "test-semantic-lint"], + compilerOptions: { + extensions: ["test-syntactic-lint", "test-semantic-lint"], + module: ModuleKind.CommonJS, + } + }, languageServiceCompile); + + test({ + "main.ts": `interface Foo {a; b;}`, + }, { + availableExtensions: ["test-extension-arguments"], + expectedDiagnostics: ["test-extension-arguments", "test-extension-arguments"], + compilerOptions: { + extensions: { + "test-extension-arguments": ["a", "b"] + }, + module: ModuleKind.CommonJS, + } + }, languageServiceCompile); + + test({ + "main.ts": `interface Foo {b;} + interface Bar {a;} + const f: "foo" = "foo"; + let b: "bar" = "bar";`, + }, { + availableExtensions: ["test-multi-extension"], + expectedDiagnostics: ["test-multi-extension[IsNamedFoo]", "test-multi-extension[IsNamedBar]", "test-multi-extension[IsValueFoo]", "test-multi-extension[IsValueFoo]", "test-multi-extension[IsValueBar]", "test-multi-extension[IsValueBar]"], + compilerOptions: { + extensions: ["test-multi-extension"], + module: ModuleKind.CommonJS, + } + }, languageServiceCompile); + + test({ + "main.ts": `console.log("Hello, world!");`, + }, { + availableExtensions: [], + expectedDiagnostics: [6151, 6151], + compilerOptions: { + extensions: ["test-syntactic-lint", "test-semantic-lint"], + module: ModuleKind.CommonJS, + } + }, languageServiceCompile); + }); }); } From a03e4650d583aa8a079c876437efc17b02041030 Mon Sep 17 00:00:00 2001 From: Wesley Wigham Date: Mon, 20 Jun 2016 18:31:12 -0700 Subject: [PATCH 2/7] Fix comments - make behavior align with comments --- src/services/services.ts | 29 ++++++++++++++++++----------- 1 file changed, 18 insertions(+), 11 deletions(-) diff --git a/src/services/services.ts b/src/services/services.ts index 8c29d5d8097b3..bd9ac34c271a6 100644 --- a/src/services/services.ts +++ b/src/services/services.ts @@ -1170,7 +1170,7 @@ namespace ts { // be returned by the TypeScript language service. If a plugin returns a defined results // (that is, is not undefined) then that result is used instead of invoking the // corresponding TypeScript method. If multiple plugins are registered, they are - // consulted in the order they are returned from the host. The first defined result + // consulted in the order they are returned from the program. The first defined result // returned by a plugin is used and no other plugin overrides are consulted. getProgramDiagnostics?(): Diagnostic[]; @@ -1208,7 +1208,7 @@ namespace ts { // prior to the host receiving it. The TypeScript language service is invoked and the // result is passed to the plugin as the value of the previous parameter. If more than one // plugin is registered, the plugins are consulted in the order they are returned from the - // host. The value passed in as previous is the result returned by the prior plugin. If a + // program. The value passed in as previous is the result returned by the prior plugin. If a // plugin returns undefined, the result passed in as previous is used and the undefined // result is ignored. All plugins are consulted before the result is returned to the host. // If a plugin overrides behavior of the method, no filter methods are consulted. @@ -3003,28 +3003,35 @@ namespace ts { const baseService = createUnextendedLanguageService(host, documentRegistry); const extensions = baseService.getProgram().getCompilerExtensions()["language-service"]; const instantiatedExtensions = map(extensions, extension => new extension.ctor(ts, host, baseService, documentRegistry, extension.args)); + const extensionCount = instantiatedExtensions && instantiatedExtensions.length; function wrap(key: string): Function { - return (...args: any[]) => { - if (instantiatedExtensions && instantiatedExtensions.length) { - for (let i = 0; i < instantiatedExtensions.length; i++) { + if (extensionCount) { + return (...args: any[]) => { + for (let i = 0; i < extensionCount; i++) { const extension = instantiatedExtensions[i]; if ((extension as any)[key]) { - return (extension as any)[key](...args); + const temp = (extension as any)[key](...args); + if (temp !== undefined) { + return temp; + } } } let result: any = (baseService as any)[key](...args); const filterKey = `${key}Filter`; - for (let i = 0; i < instantiatedExtensions.length; i++) { + for (let i = 0; i < extensionCount; i++) { const extension = instantiatedExtensions[i]; if ((extension as any)[filterKey]) { - result = (extension as any)[filterKey](...args, result); + const temp = (extension as any)[filterKey](...args, result); + if (temp !== undefined) { + result = temp; + } } } return result; - } - return (baseService as any)[key](...args); - }; + }; + } + return (baseService as any)[key]; } function buildWrappedService(underlyingMembers: Map, wrappedMembers: string[]): LanguageService { From 257e939fa68074d312fac409bf0c55e038889900 Mon Sep 17 00:00:00 2001 From: Wesley Wigham Date: Wed, 29 Jun 2016 10:41:14 -0700 Subject: [PATCH 3/7] Fix lints, identify lifetimeissue --- src/services/services.ts | 6 ++++-- tests/cases/unittests/extensionAPI.ts | 19 ++++++++++--------- 2 files changed, 14 insertions(+), 11 deletions(-) diff --git a/src/services/services.ts b/src/services/services.ts index 798ff5e30a19e..098a97e0748e6 100644 --- a/src/services/services.ts +++ b/src/services/services.ts @@ -3126,7 +3126,7 @@ namespace ts { } // Get a fresh cache of the host information - let hostCache = new HostCache(host, getCanonicalFileName); + const hostCache = new HostCache(host, getCanonicalFileName); // If the program is already up-to-date, we can reuse it if (programUpToDate()) { @@ -3211,7 +3211,9 @@ namespace ts { // hostCache is captured in the closure for 'getOrCreateSourceFile' but it should not be used past this point. // It needs to be cleared to allow all collected snapshots to be released - hostCache = undefined; + // TODO (weswig): hostCache needs to exist as long as its associated compilerHost exists, since it is used in fileExists. + // As such, we cannot release it here - it must be tied to the lifetime of the compilerHost. + // hostCache = undefined; program = newProgram; diff --git a/tests/cases/unittests/extensionAPI.ts b/tests/cases/unittests/extensionAPI.ts index 46e6feaa0cf2b..1e9858c0c14ff 100644 --- a/tests/cases/unittests/extensionAPI.ts +++ b/tests/cases/unittests/extensionAPI.ts @@ -111,7 +111,7 @@ namespace ts { function makeMockLSHost(files: string[], options: CompilerOptions): LanguageServiceHost { files = filter(files, file => !endsWith(file, ".json")); - return { + const host: LanguageServiceHost = { getCompilationSettings: () => options, getScriptFileNames: () => files, getScriptVersion(fileName) { @@ -130,14 +130,14 @@ namespace ts { }, loadExtension(path) { const fullPath = getCanonicalFileName(path); - const m = {exports: {}}; + const m = { exports: {} }; ((module, exports, require) => { eval(virtualFs[fullPath]); })( m, m.exports, (name: string) => { - return this.loadExtension( + return host.loadExtension( getCanonicalFileName( - resolveModuleName(name, fullPath, {module: ModuleKind.CommonJS}, mockHost, true).resolvedModule.resolvedFileName + resolveModuleName(name, fullPath, { module: ModuleKind.CommonJS }, mockHost, true).resolvedModule.resolvedFileName ) ); } @@ -148,6 +148,7 @@ namespace ts { console.log(s); } }; + return host; }; const extensionAPI: Map = { @@ -1117,16 +1118,16 @@ being received, for good or for evil, in the superlative degree of comparison only. `; const testDummyLS = (service: LanguageService) => { - assert.deepEqual(service.getEncodedSyntacticClassifications(atotcFile, {start: 0, length: 24}), - {spans: [0, 24, ClassificationType.text], endOfLineState: EndOfLineState.None}, + assert.deepEqual(service.getEncodedSyntacticClassifications(atotcFile, { start: 0, length: 24 }), + { spans: [0, 24, ClassificationType.text], endOfLineState: EndOfLineState.None }, "Syntactic classifications did not match!"); - assert.deepEqual(service.getEncodedSemanticClassifications(atotcFile, {start: 24, length: 42}), - {spans: [24, 42, ClassificationType.moduleName], endOfLineState: EndOfLineState.None}, + assert.deepEqual(service.getEncodedSemanticClassifications(atotcFile, { start: 24, length: 42 }), + { spans: [24, 42, ClassificationType.moduleName], endOfLineState: EndOfLineState.None }, "Semantic classifications did not match!"); assert.deepEqual(service.getCompletionsAtPosition(atotcFile, 0), { isMemberCompletion: false, isNewIdentifierLocation: false, - entries: [{name: atotcFile, kind: 0, kindModifiers: 0, sortText: atotcFile}] + entries: [{ name: atotcFile, kind: 0, kindModifiers: 0, sortText: atotcFile }] }, "Completions did not match!"); assert.deepEqual(service.getCompletionEntryDetails(atotcFile, 0, "first"), { name: atotcFile, From 9425485a1c62769b82a6c1a439b1b90dc76ca907 Mon Sep 17 00:00:00 2001 From: Wesley Wigham Date: Wed, 29 Jun 2016 10:58:15 -0700 Subject: [PATCH 4/7] remove excess deep equal overload --- src/harness/external/chai.d.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/harness/external/chai.d.ts b/src/harness/external/chai.d.ts index ccd1de1bfb59f..5e4e6e7d00010 100644 --- a/src/harness/external/chai.d.ts +++ b/src/harness/external/chai.d.ts @@ -175,6 +175,5 @@ declare module chai { function isOk(actual: any, message?: string): void; function isUndefined(value: any, message?: string): void; function isDefined(value: any, message?: string): void; - function deepEqual(actual: any, expected: any, message?: string): void; } } \ No newline at end of file From adccddf913acbd8d01ce58f2c5d099e6515e3553 Mon Sep 17 00:00:00 2001 From: Wesley Wigham Date: Wed, 29 Jun 2016 11:14:39 -0700 Subject: [PATCH 5/7] Remove whitespace/trailing comma changes --- src/compiler/extensions.ts | 1 - tests/cases/unittests/extensionAPI.ts | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/src/compiler/extensions.ts b/src/compiler/extensions.ts index 2a931cf7cb135..d0ec9731cb4f5 100644 --- a/src/compiler/extensions.ts +++ b/src/compiler/extensions.ts @@ -1,5 +1,4 @@ namespace ts { - export type LintErrorMethod = { (err: string): void; (err: string, span: Node): void; diff --git a/tests/cases/unittests/extensionAPI.ts b/tests/cases/unittests/extensionAPI.ts index 60e2c7411b634..c83bfee404d52 100644 --- a/tests/cases/unittests/extensionAPI.ts +++ b/tests/cases/unittests/extensionAPI.ts @@ -842,7 +842,7 @@ export class NoShortNames extends SyntacticLintWalker { } } } -` +`, }, "test-errors": { "package.json": `{ From af9c1e1993a365203f451896ba833155cfd83df3 Mon Sep 17 00:00:00 2001 From: Wesley Wigham Date: Wed, 29 Jun 2016 11:19:15 -0700 Subject: [PATCH 6/7] Fix lint --- tests/cases/unittests/extensionAPI.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/cases/unittests/extensionAPI.ts b/tests/cases/unittests/extensionAPI.ts index c83bfee404d52..aea114919b363 100644 --- a/tests/cases/unittests/extensionAPI.ts +++ b/tests/cases/unittests/extensionAPI.ts @@ -480,7 +480,7 @@ export default class extends LanguageServiceProvider { return { isMemberCompletion: false, isNewIdentifierLocation: false, - entries: [{name: fileName, kind: '', kindModifiers: '', sortText: fileName}] + entries: [{name: fileName, kind: "", kindModifiers: "", sortText: fileName}] }; } getCompletionEntryDetails(fileName, position, entryName) { @@ -619,7 +619,7 @@ export default class extends LanguageServiceProvider { return { isMemberCompletion: false, isNewIdentifierLocation: false, - entries: [{name: fileName, kind: '', kindModifiers: '', sortText: fileName}] + entries: [{name: fileName, kind: "", kindModifiers: "", sortText: fileName}] }; } getCompletionEntryDetailsFilter(fileName, position, entryName, previous) { @@ -1127,7 +1127,7 @@ of comparison only. assert.deepEqual(service.getCompletionsAtPosition(atotcFile, 0), { isMemberCompletion: false, isNewIdentifierLocation: false, - entries: [{ name: atotcFile as Path, kind: '', kindModifiers: '', sortText: atotcFile }] + entries: [{ name: atotcFile as Path, kind: "", kindModifiers: "", sortText: atotcFile }] }, "Completions did not match!"); assert.deepEqual(service.getCompletionEntryDetails(atotcFile, 0, "first"), { name: atotcFile, From 908d9e786a0a26f9f3fb6c2a794ba491ee030ebd Mon Sep 17 00:00:00 2001 From: Wesley Wigham Date: Wed, 29 Jun 2016 11:21:43 -0700 Subject: [PATCH 7/7] Add missing members to type --- src/compiler/extensions.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/compiler/extensions.ts b/src/compiler/extensions.ts index d0ec9731cb4f5..a4ea4905d32e6 100644 --- a/src/compiler/extensions.ts +++ b/src/compiler/extensions.ts @@ -37,7 +37,8 @@ namespace ts { export interface LanguageServiceProvider {} export interface DocumentRegistry {} - export interface LanguageServiceProviderStatic { + export interface LanguageServiceProviderStatic extends BaseProviderStatic { + readonly ["extension-kind"]: ExtensionKind.LanguageService; new (state: { ts: typeof ts, args: any, host: LanguageServiceHost, service: LanguageService, registry: DocumentRegistry }): LanguageServiceProvider; }