diff --git a/.gitignore b/.gitignore index 3147b8e8724..58a45545939 100644 --- a/.gitignore +++ b/.gitignore @@ -25,6 +25,7 @@ tests/baselines/reference/projectOutput/* tests/baselines/local/projectOutput/* tests/services/baselines/prototyping/local/* tests/services/browser/typescriptServices.js +scripts/configureNightly.js scripts/processDiagnosticMessages.d.ts scripts/processDiagnosticMessages.js scripts/importDefinitelyTypedTests.js diff --git a/Jakefile.js b/Jakefile.js index bd5074b43d6..ac40e30f77a 100644 --- a/Jakefile.js +++ b/Jakefile.js @@ -138,6 +138,7 @@ var harnessSources = harnessCoreSources.concat([ "services/documentRegistry.ts", "services/preProcessFile.ts", "services/patternMatcher.ts", + "session.ts", "versionCache.ts", "convertToBase64.ts", "transpile.ts" @@ -313,7 +314,7 @@ var processDiagnosticMessagesTs = path.join(scriptsDirectory, "processDiagnostic var diagnosticMessagesJson = path.join(compilerDirectory, "diagnosticMessages.json"); var diagnosticInfoMapTs = path.join(compilerDirectory, "diagnosticInformationMap.generated.ts"); -file(processDiagnosticMessagesTs) +file(processDiagnosticMessagesTs); // processDiagnosticMessages script compileFile(processDiagnosticMessagesJs, @@ -338,12 +339,50 @@ file(diagnosticInfoMapTs, [processDiagnosticMessagesJs, diagnosticMessagesJson], complete(); }); ex.run(); -}, {async: true}) +}, {async: true}); desc("Generates a diagnostic file in TypeScript based on an input JSON file"); -task("generate-diagnostics", [diagnosticInfoMapTs]) +task("generate-diagnostics", [diagnosticInfoMapTs]); +// Publish nightly +var configureNightlyJs = path.join(scriptsDirectory, "configureNightly.js"); +var configureNightlyTs = path.join(scriptsDirectory, "configureNightly.ts"); +var packageJson = "package.json"; +var programTs = path.join(compilerDirectory, "program.ts"); + +file(configureNightlyTs); + +compileFile(/*outfile*/configureNightlyJs, + /*sources*/ [configureNightlyTs], + /*prereqs*/ [configureNightlyTs], + /*prefixes*/ [], + /*useBuiltCompiler*/ false, + /*noOutFile*/ false, + /*generateDeclarations*/ false, + /*outDir*/ undefined, + /*preserveConstEnums*/ undefined, + /*keepComments*/ false, + /*noResolve*/ false, + /*stripInternal*/ false); + +task("setDebugMode", function() { + useDebugMode = true; +}); + +task("configure-nightly", [configureNightlyJs], function() { + var cmd = "node " + configureNightlyJs + " " + packageJson + " " + programTs; + console.log(cmd); + exec(cmd); +}, { async: true }); + +desc("Configure, build, test, and publish the nightly release."); +task("publish-nightly", ["configure-nightly", "LKG", "clean", "setDebugMode", "runtests"], function () { + var cmd = "npm publish --tag next"; + console.log(cmd); + exec(cmd); +}); + // Local target to build the compiler and services var tscFile = path.join(builtLocalDirectory, compilerFilename); compileFile(tscFile, compilerSources, [builtLocalDirectory, copyright].concat(compilerSources), [copyright], /*useBuiltCompiler:*/ false); @@ -440,11 +479,11 @@ file(specMd, [word2mdJs, specWord], function () { child_process.exec(cmd, function () { complete(); }); -}, {async: true}) +}, {async: true}); desc("Generates a Markdown version of the Language Specification"); -task("generate-spec", [specMd]) +task("generate-spec", [specMd]); // Makes a new LKG. This target does not build anything, but errors if not all the outputs are present in the built/local directory @@ -576,7 +615,7 @@ task("runtests", ["tests", builtLocalDirectory], function() { exec(cmd, deleteTemporaryProjectOutput); }, {async: true}); -desc("Generates code coverage data via instanbul") +desc("Generates code coverage data via instanbul"); task("generate-code-coverage", ["tests", builtLocalDirectory], function () { var cmd = 'istanbul cover node_modules/mocha/bin/_mocha -- -R min -t ' + testTimeout + ' ' + run; console.log(cmd); @@ -619,7 +658,7 @@ task("runtests-browser", ["tests", "browserify", builtLocalDirectory], function( function getDiffTool() { var program = process.env['DIFF'] if (!program) { - fail("Add the 'DIFF' environment variable to the path of the program you want to use.") + fail("Add the 'DIFF' environment variable to the path of the program you want to use."); } return program; } @@ -628,14 +667,14 @@ function getDiffTool() { desc("Diffs the compiler baselines using the diff tool specified by the 'DIFF' environment variable"); task('diff', function () { var cmd = '"' + getDiffTool() + '" ' + refBaseline + ' ' + localBaseline; - console.log(cmd) + console.log(cmd); exec(cmd); }, {async: true}); desc("Diffs the RWC baselines using the diff tool specified by the 'DIFF' environment variable"); task('diff-rwc', function () { var cmd = '"' + getDiffTool() + '" ' + refRwcBaseline + ' ' + localRwcBaseline; - console.log(cmd) + console.log(cmd); exec(cmd); }, {async: true}); diff --git a/package.json b/package.json index 70ada6fef15..c8b30f9de1c 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "typescript", "author": "Microsoft Corp.", "homepage": "http://typescriptlang.org/", - "version": "1.5.3", + "version": "1.6.0", "license": "Apache-2.0", "description": "TypeScript is a language for application scale JavaScript development", "keywords": [ diff --git a/scripts/configureNightly.ts b/scripts/configureNightly.ts new file mode 100644 index 00000000000..640f330b376 --- /dev/null +++ b/scripts/configureNightly.ts @@ -0,0 +1,73 @@ +/// + +/** + * A minimal description for a parsed package.json object. + */ +interface PackageJson { + name: string; + version: string; + keywords: string[]; +} + +function main(): void { + const sys = ts.sys; + if (sys.args.length < 2) { + sys.write("Usage:" + sys.newLine) + sys.write("\tnode configureNightly.js " + sys.newLine); + return; + } + + // Acquire the version from the package.json file and modify it appropriately. + const packageJsonFilePath = ts.normalizePath(sys.args[0]); + const packageJsonContents = sys.readFile(packageJsonFilePath); + const packageJsonValue: PackageJson = JSON.parse(packageJsonContents); + + const nightlyVersion = getNightlyVersionString(packageJsonValue.version); + + // Modify the package.json structure + packageJsonValue.version = nightlyVersion; + + // Acquire and modify the source file that exposes the version string. + const tsFilePath = ts.normalizePath(sys.args[1]); + const tsFileContents = sys.readFile(tsFilePath); + const versionAssignmentRegExp = /export\s+const\s+version\s+=\s+".*";/; + const modifiedTsFileContents = tsFileContents.replace(versionAssignmentRegExp, `export const version = "${nightlyVersion}";`); + + // Ensure we are actually changing something - the user probably wants to know that the update failed. + if (tsFileContents === modifiedTsFileContents) { + let err = `\n '${tsFilePath}' was not updated while configuring for a nightly publish.\n `; + + if (tsFileContents.match(versionAssignmentRegExp)) { + err += `Ensure that you have not already run this script; otherwise, erase your changes using 'git checkout -- "${tsFilePath}"'.`; + } + else { + err += `The file seems to no longer have a string matching '${versionAssignmentRegExp}'.`; + } + + throw err + "\n"; + } + + // Finally write the changes to disk. + sys.writeFile(packageJsonFilePath, JSON.stringify(packageJsonValue, /*replacer:*/ undefined, /*space:*/ 4)) + sys.writeFile(tsFilePath, modifiedTsFileContents); +} + +function getNightlyVersionString(versionString: string): string { + // If the version string already contains "-nightly", + // then get the base string and update based on that. + const dashNightlyPos = versionString.indexOf("-dev"); + if (dashNightlyPos >= 0) { + versionString = versionString.slice(0, dashNightlyPos); + } + + // We're going to append a representation of the current time at the end of the current version. + // String.prototype.toISOString() returns a 24-character string formatted as 'YYYY-MM-DDTHH:mm:ss.sssZ', + // but we'd prefer to just remove separators and limit ourselves to YYYYMMDD. + // UTC time will always be implicit here. + const now = new Date(); + const timeStr = now.toISOString().replace(/:|T|\.|-/g, "").slice(0, 8); + + return `${versionString}-dev.${timeStr}`; +} + +main(); \ No newline at end of file diff --git a/src/compiler/checker.ts b/src/compiler/checker.ts index b32ce5f6af0..3363987322a 100644 --- a/src/compiler/checker.ts +++ b/src/compiler/checker.ts @@ -68,6 +68,7 @@ namespace ts { getPropertyOfType, getSignaturesOfType, getIndexTypeOfType, + getBaseTypes, getReturnTypeOfSignature, getSymbolsInScope, getSymbolAtLocation, diff --git a/src/compiler/program.ts b/src/compiler/program.ts index 5ce4846b43b..d79f617e1cf 100644 --- a/src/compiler/program.ts +++ b/src/compiler/program.ts @@ -8,7 +8,7 @@ namespace ts { /* @internal */ export let ioWriteTime = 0; /** The version of the TypeScript compiler release */ - export const version = "1.5.3"; + export const version = "1.6.0"; export function findConfigFile(searchPath: string): string { let fileName = "tsconfig.json"; @@ -341,7 +341,7 @@ namespace ts { }); } - function getDeclarationDiagnosticsForFile(sourceFile: SourceFile, cancellationToken: CancellationToken): Diagnostic[] { + function getDeclarationDiagnosticsForFile(sourceFile: SourceFile, cancellationToken: CancellationToken): Diagnostic[] { return runWithCancellationToken(() => { if (!isDeclarationFile(sourceFile)) { let resolver = getDiagnosticsProducingTypeChecker().getEmitResolver(sourceFile, cancellationToken); @@ -350,7 +350,7 @@ namespace ts { return ts.getDeclarationDiagnostics(getEmitHost(writeFile), resolver, sourceFile); } }); - } + } function getOptionsDiagnostics(): Diagnostic[] { let allDiagnostics: Diagnostic[] = []; diff --git a/src/compiler/types.ts b/src/compiler/types.ts index 0a3fd4bcb51..4bc244a48c2 100644 --- a/src/compiler/types.ts +++ b/src/compiler/types.ts @@ -1404,6 +1404,7 @@ namespace ts { getPropertyOfType(type: Type, propertyName: string): Symbol; getSignaturesOfType(type: Type, kind: SignatureKind): Signature[]; getIndexTypeOfType(type: Type, kind: IndexKind): Type; + getBaseTypes(type: InterfaceType): ObjectType[]; getReturnTypeOfSignature(signature: Signature): Type; getSymbolsInScope(location: Node, meaning: SymbolFlags): Symbol[]; @@ -1808,7 +1809,9 @@ namespace ts { typeParameters: TypeParameter[]; // Type parameters (undefined if non-generic) outerTypeParameters: TypeParameter[]; // Outer type parameters (undefined if none) localTypeParameters: TypeParameter[]; // Local type parameters (undefined if none) + /* @internal */ resolvedBaseConstructorType?: Type; // Resolved base constructor type of class + /* @internal */ resolvedBaseTypes: ObjectType[]; // Resolved base types } diff --git a/src/harness/harness.ts b/src/harness/harness.ts index 17aa0dd325c..b6c10071cb1 100644 --- a/src/harness/harness.ts +++ b/src/harness/harness.ts @@ -29,6 +29,7 @@ var Buffer: BufferConstructor = require('buffer').Buffer; // this will work in the browser via browserify var _chai: typeof chai = require('chai'); var assert: typeof _chai.assert = _chai.assert; +var expect: typeof _chai.expect = _chai.expect; declare var __dirname: string; // Node-specific var global = Function("return this").call(null); diff --git a/src/server/session.ts b/src/server/session.ts index d2ac429aa3c..9a5cee32264 100644 --- a/src/server/session.ts +++ b/src/server/session.ts @@ -109,13 +109,13 @@ namespace ts.server { } export class Session { - projectService: ProjectService; - pendingOperation = false; - fileHash: ts.Map = {}; - nextFileId = 1; - errorTimer: any; /*NodeJS.Timer | number*/ - immediateId: any; - changeSeq = 0; + protected projectService: ProjectService; + private pendingOperation = false; + private fileHash: ts.Map = {}; + private nextFileId = 1; + private errorTimer: any; /*NodeJS.Timer | number*/ + private immediateId: any; + private changeSeq = 0; constructor( private host: ServerHost, @@ -129,7 +129,7 @@ namespace ts.server { }); } - handleEvent(eventName: string, project: Project, fileName: string) { + private handleEvent(eventName: string, project: Project, fileName: string) { if (eventName == "context") { this.projectService.log("got context event, updating diagnostics for" + fileName, "Info"); this.updateErrorCheck([{ fileName, project }], this.changeSeq, @@ -137,7 +137,7 @@ namespace ts.server { } } - logError(err: Error, cmd: string) { + public logError(err: Error, cmd: string) { var typedErr = err; var msg = "Exception on executing command " + cmd; if (typedErr.message) { @@ -149,11 +149,11 @@ namespace ts.server { this.projectService.log(msg); } - sendLineToClient(line: string) { + private sendLineToClient(line: string) { this.host.write(line + this.host.newLine); } - send(msg: protocol.Message) { + public send(msg: protocol.Message) { var json = JSON.stringify(msg); if (this.logger.isVerbose()) { this.logger.info(msg.type + ": " + json); @@ -162,7 +162,7 @@ namespace ts.server { '\r\n\r\n' + json); } - event(info: any, eventName: string) { + public event(info: any, eventName: string) { var ev: protocol.Event = { seq: 0, type: "event", @@ -172,7 +172,7 @@ namespace ts.server { this.send(ev); } - response(info: any, cmdName: string, reqSeq = 0, errorMsg?: string) { + private response(info: any, cmdName: string, reqSeq = 0, errorMsg?: string) { var res: protocol.Response = { seq: 0, type: "response", @@ -189,11 +189,11 @@ namespace ts.server { this.send(res); } - output(body: any, commandName: string, requestSequence = 0, errorMessage?: string) { + public output(body: any, commandName: string, requestSequence = 0, errorMessage?: string) { this.response(body, commandName, requestSequence, errorMessage); } - semanticCheck(file: string, project: Project) { + private semanticCheck(file: string, project: Project) { try { var diags = project.compilerService.languageService.getSemanticDiagnostics(file); @@ -207,7 +207,7 @@ namespace ts.server { } } - syntacticCheck(file: string, project: Project) { + private syntacticCheck(file: string, project: Project) { try { var diags = project.compilerService.languageService.getSyntacticDiagnostics(file); if (diags) { @@ -220,12 +220,12 @@ namespace ts.server { } } - errorCheck(file: string, project: Project) { + private errorCheck(file: string, project: Project) { this.syntacticCheck(file, project); this.semanticCheck(file, project); } - updateProjectStructure(seq: number, matchSeq: (seq: number) => boolean, ms = 1500) { + private updateProjectStructure(seq: number, matchSeq: (seq: number) => boolean, ms = 1500) { setTimeout(() => { if (matchSeq(seq)) { this.projectService.updateProjectStructure(); @@ -233,7 +233,7 @@ namespace ts.server { }, ms); } - updateErrorCheck(checkList: PendingErrorCheck[], seq: number, + private updateErrorCheck(checkList: PendingErrorCheck[], seq: number, matchSeq: (seq: number) => boolean, ms = 1500, followMs = 200) { if (followMs > ms) { followMs = ms; @@ -269,7 +269,7 @@ namespace ts.server { } } - getDefinition(line: number, offset: number, fileName: string): protocol.FileSpan[] { + private getDefinition(line: number, offset: number, fileName: string): protocol.FileSpan[] { var file = ts.normalizePath(fileName); var project = this.projectService.getProjectForFile(file); if (!project) { @@ -291,7 +291,7 @@ namespace ts.server { })); } - getTypeDefinition(line: number, offset: number, fileName: string): protocol.FileSpan[] { + private getTypeDefinition(line: number, offset: number, fileName: string): protocol.FileSpan[] { var file = ts.normalizePath(fileName); var project = this.projectService.getProjectForFile(file); if (!project) { @@ -313,7 +313,7 @@ namespace ts.server { })); } - getOccurrences(line: number, offset: number, fileName: string): protocol.OccurrencesResponseItem[]{ + private getOccurrences(line: number, offset: number, fileName: string): protocol.OccurrencesResponseItem[]{ fileName = ts.normalizePath(fileName); let project = this.projectService.getProjectForFile(fileName); @@ -343,7 +343,7 @@ namespace ts.server { }); } - getProjectInfo(fileName: string, needFileNameList: boolean): protocol.ProjectInfo { + private getProjectInfo(fileName: string, needFileNameList: boolean): protocol.ProjectInfo { fileName = ts.normalizePath(fileName) let project = this.projectService.getProjectForFile(fileName) @@ -358,7 +358,7 @@ namespace ts.server { return projectInfo; } - getRenameLocations(line: number, offset: number, fileName: string,findInComments: boolean, findInStrings: boolean): protocol.RenameResponseBody { + private getRenameLocations(line: number, offset: number, fileName: string,findInComments: boolean, findInStrings: boolean): protocol.RenameResponseBody { var file = ts.normalizePath(fileName); var project = this.projectService.getProjectForFile(file); if (!project) { @@ -426,7 +426,7 @@ namespace ts.server { return { info: renameInfo, locs: bakedRenameLocs }; } - getReferences(line: number, offset: number, fileName: string): protocol.ReferencesResponseBody { + private getReferences(line: number, offset: number, fileName: string): protocol.ReferencesResponseBody { // TODO: get all projects for this file; report refs for all projects deleting duplicates // can avoid duplicates by eliminating same ref file from subsequent projects var file = ts.normalizePath(fileName); @@ -473,12 +473,12 @@ namespace ts.server { }; } - openClientFile(fileName: string) { + private openClientFile(fileName: string) { var file = ts.normalizePath(fileName); this.projectService.openClientFile(file); } - getQuickInfo(line: number, offset: number, fileName: string): protocol.QuickInfoResponseBody { + private getQuickInfo(line: number, offset: number, fileName: string): protocol.QuickInfoResponseBody { var file = ts.normalizePath(fileName); var project = this.projectService.getProjectForFile(file); if (!project) { @@ -504,7 +504,7 @@ namespace ts.server { }; } - getFormattingEditsForRange(line: number, offset: number, endLine: number, endOffset: number, fileName: string): protocol.CodeEdit[] { + private getFormattingEditsForRange(line: number, offset: number, endLine: number, endOffset: number, fileName: string): protocol.CodeEdit[] { var file = ts.normalizePath(fileName); var project = this.projectService.getProjectForFile(file); if (!project) { @@ -531,7 +531,7 @@ namespace ts.server { }); } - getFormattingEditsAfterKeystroke(line: number, offset: number, key: string, fileName: string): protocol.CodeEdit[] { + private getFormattingEditsAfterKeystroke(line: number, offset: number, key: string, fileName: string): protocol.CodeEdit[] { var file = ts.normalizePath(fileName); var project = this.projectService.getProjectForFile(file); @@ -607,7 +607,7 @@ namespace ts.server { }); } - getCompletions(line: number, offset: number, prefix: string, fileName: string): protocol.CompletionEntry[] { + private getCompletions(line: number, offset: number, prefix: string, fileName: string): protocol.CompletionEntry[] { if (!prefix) { prefix = ""; } @@ -633,7 +633,7 @@ namespace ts.server { }, []).sort((a, b) => a.name.localeCompare(b.name)); } - getCompletionEntryDetails(line: number, offset: number, + private getCompletionEntryDetails(line: number, offset: number, entryNames: string[], fileName: string): protocol.CompletionEntryDetails[] { var file = ts.normalizePath(fileName); var project = this.projectService.getProjectForFile(file); @@ -653,7 +653,7 @@ namespace ts.server { }, []); } - getSignatureHelpItems(line: number, offset: number, fileName: string): protocol.SignatureHelpItems { + private getSignatureHelpItems(line: number, offset: number, fileName: string): protocol.SignatureHelpItems { var file = ts.normalizePath(fileName); var project = this.projectService.getProjectForFile(file); if (!project) { @@ -682,7 +682,7 @@ namespace ts.server { return result; } - getDiagnostics(delay: number, fileNames: string[]) { + private getDiagnostics(delay: number, fileNames: string[]) { var checkList = fileNames.reduce((accum: PendingErrorCheck[], fileName: string) => { fileName = ts.normalizePath(fileName); var project = this.projectService.getProjectForFile(fileName); @@ -697,7 +697,7 @@ namespace ts.server { } } - change(line: number, offset: number, endLine: number, endOffset: number, insertString: string, fileName: string) { + private change(line: number, offset: number, endLine: number, endOffset: number, insertString: string, fileName: string) { var file = ts.normalizePath(fileName); var project = this.projectService.getProjectForFile(file); if (project) { @@ -712,7 +712,7 @@ namespace ts.server { } } - reload(fileName: string, tempFileName: string, reqSeq = 0) { + private reload(fileName: string, tempFileName: string, reqSeq = 0) { var file = ts.normalizePath(fileName); var tmpfile = ts.normalizePath(tempFileName); var project = this.projectService.getProjectForFile(file); @@ -725,7 +725,7 @@ namespace ts.server { } } - saveToTmp(fileName: string, tempFileName: string) { + private saveToTmp(fileName: string, tempFileName: string) { var file = ts.normalizePath(fileName); var tmpfile = ts.normalizePath(tempFileName); @@ -735,12 +735,12 @@ namespace ts.server { } } - closeClientFile(fileName: string) { + private closeClientFile(fileName: string) { var file = ts.normalizePath(fileName); this.projectService.closeClientFile(file); } - decorateNavigationBarItem(project: Project, fileName: string, items: ts.NavigationBarItem[]): protocol.NavigationBarItem[] { + private decorateNavigationBarItem(project: Project, fileName: string, items: ts.NavigationBarItem[]): protocol.NavigationBarItem[] { if (!items) { return undefined; } @@ -759,7 +759,7 @@ namespace ts.server { })); } - getNavigationBarItems(fileName: string): protocol.NavigationBarItem[] { + private getNavigationBarItems(fileName: string): protocol.NavigationBarItem[] { var file = ts.normalizePath(fileName); var project = this.projectService.getProjectForFile(file); if (!project) { @@ -775,7 +775,7 @@ namespace ts.server { return this.decorateNavigationBarItem(project, fileName, items); } - getNavigateToItems(searchValue: string, fileName: string, maxResultCount?: number): protocol.NavtoItem[] { + private getNavigateToItems(searchValue: string, fileName: string, maxResultCount?: number): protocol.NavtoItem[] { var file = ts.normalizePath(fileName); var project = this.projectService.getProjectForFile(file); if (!project) { @@ -814,7 +814,7 @@ namespace ts.server { }); } - getBraceMatching(line: number, offset: number, fileName: string): protocol.TextSpan[] { + private getBraceMatching(line: number, offset: number, fileName: string): protocol.TextSpan[] { var file = ts.normalizePath(fileName); var project = this.projectService.getProjectForFile(file); @@ -836,7 +836,7 @@ namespace ts.server { })); } - exit() { + public exit() { } private handlers : Map<(request: protocol.Request) => {response?: any, responseRequired?: boolean}> = { @@ -942,14 +942,14 @@ namespace ts.server { return {response: this.getProjectInfo(file, needFileNameList), responseRequired: true}; }, }; - addProtocolHandler(command: string, handler: (request: protocol.Request) => {response?: any, responseRequired: boolean}) { + public addProtocolHandler(command: string, handler: (request: protocol.Request) => {response?: any, responseRequired: boolean}) { if (this.handlers[command]) { throw new Error(`Protocol handler already exists for command "${command}"`); } this.handlers[command] = handler; } - executeCommand(request: protocol.Request) : {response?: any, responseRequired?: boolean} { + public executeCommand(request: protocol.Request) : {response?: any, responseRequired?: boolean} { var handler = this.handlers[request.command]; if (handler) { return handler(request); @@ -960,7 +960,7 @@ namespace ts.server { } } - onMessage(message: string) { + public onMessage(message: string) { if (this.logger.isVerbose()) { this.logger.info("request: " + message); var start = this.hrtime(); diff --git a/src/services/services.ts b/src/services/services.ts index 2a0516b2d34..7769e9b2fc0 100644 --- a/src/services/services.ts +++ b/src/services/services.ts @@ -48,6 +48,7 @@ namespace ts { getConstructSignatures(): Signature[]; getStringIndexType(): Type; getNumberIndexType(): Type; + getBaseTypes(): ObjectType[] } export interface Signature { @@ -682,6 +683,11 @@ namespace ts { getNumberIndexType(): Type { return this.checker.getIndexTypeOfType(this, IndexKind.Number); } + getBaseTypes(): ObjectType[] { + return this.flags & (TypeFlags.Class | TypeFlags.Interface) + ? this.checker.getBaseTypes(this) + : undefined; + } } class SignatureObject implements Signature { diff --git a/tests/cases/unittests/session.ts b/tests/cases/unittests/session.ts new file mode 100644 index 00000000000..c1dfd508942 --- /dev/null +++ b/tests/cases/unittests/session.ts @@ -0,0 +1,447 @@ +/// + +namespace ts.server { + let lastWrittenToHost: string; + const mockHost: ServerHost = { + args: [], + newLine: "\n", + useCaseSensitiveFileNames: true, + write(s): void { lastWrittenToHost = s; }, + readFile(): string { return void 0; }, + writeFile(): void {}, + resolvePath(): string { return void 0; }, + fileExists: () => false, + directoryExists: () => false, + createDirectory(): void {}, + getExecutingFilePath(): string { return void 0; }, + getCurrentDirectory(): string { return void 0; }, + readDirectory(): string[] { return []; }, + exit(): void {} + }; + const mockLogger: Logger = { + close(): void {}, + isVerbose(): boolean { return false; }, + loggingEnabled(): boolean { return false; }, + perftrc(s: string): void {}, + info(s: string): void {}, + startGroup(): void {}, + endGroup(): void {}, + msg(s: string, type?: string): void {}, + }; + + describe("the Session class", () => { + let session: Session; + let lastSent: protocol.Message; + + beforeEach(() => { + session = new Session(mockHost, Buffer.byteLength, process.hrtime, mockLogger); + session.send = (msg: protocol.Message) => { + lastSent = msg; + }; + }); + + describe("executeCommand", () => { + it("should throw when commands are executed with invalid arguments", () => { + const req: protocol.FileRequest = { + command: CommandNames.Open, + seq: 0, + type: "command", + arguments: { + file: undefined + } + }; + + expect(() => session.executeCommand(req)).to.throw(); + }); + it("should output an error response when a command does not exist", () => { + const req: protocol.Request = { + command: "foobar", + seq: 0, + type: "command" + }; + + session.executeCommand(req); + + expect(lastSent).to.deep.equal({ + command: CommandNames.Unknown, + type: "response", + seq: 0, + message: "Unrecognized JSON command: foobar", + request_seq: 0, + success: false + }); + }); + it("should return a tuple containing the response and if a response is required on success", () => { + const req: protocol.ConfigureRequest = { + command: CommandNames.Configure, + seq: 0, + type: "command", + arguments: { + hostInfo: "unit test", + formatOptions: { + newLineCharacter: "`n" + } + } + }; + + expect(session.executeCommand(req)).to.deep.equal({ + responseRequired: false + }); + expect(lastSent).to.deep.equal({ + command: CommandNames.Configure, + type: "response", + success: true, + request_seq: 0, + seq: 0, + body: undefined + }); + }); + }); + + describe("onMessage", () => { + it("should not throw when commands are executed with invalid arguments", () => { + let i = 0; + for (name in CommandNames) { + if (!Object.prototype.hasOwnProperty.call(CommandNames, name)) { + continue; + } + const req: protocol.Request = { + command: name, + seq: i++, + type: "command" + }; + session.onMessage(JSON.stringify(req)); + req.seq = i++; + req.arguments = {}; + session.onMessage(JSON.stringify(req)); + req.seq = i++; + req.arguments = null; + session.onMessage(JSON.stringify(req)); + req.seq = i++; + req.arguments = ""; + session.onMessage(JSON.stringify(req)); + req.seq = i++; + req.arguments = 0; + session.onMessage(JSON.stringify(req)); + req.seq = i++; + req.arguments = []; + session.onMessage(JSON.stringify(req)); + } + session.onMessage("GARBAGE NON_JSON DATA"); + }); + it("should output the response for a correctly handled message", () => { + const req: protocol.ConfigureRequest = { + command: CommandNames.Configure, + seq: 0, + type: "command", + arguments: { + hostInfo: "unit test", + formatOptions: { + newLineCharacter: "`n" + } + } + }; + + session.onMessage(JSON.stringify(req)); + + expect(lastSent).to.deep.equal({ + command: CommandNames.Configure, + type: "response", + success: true, + request_seq: 0, + seq: 0, + body: undefined + }); + }); + }); + + describe("send", () => { + it("is an overrideable handle which sends protocol messages over the wire", () => { + const msg = {seq: 0, type: "none"}; + const strmsg = JSON.stringify(msg); + const len = 1 + Buffer.byteLength(strmsg, "utf8"); + const resultMsg = `Content-Length: ${len}\r\n\r\n${strmsg}\n`; + + session.send = Session.prototype.send; + assert(session.send); + expect(session.send(msg)).to.not.exist; + expect(lastWrittenToHost).to.equal(resultMsg); + }); + }); + + describe("addProtocolHandler", () => { + it("can add protocol handlers", () => { + const respBody = { + item: false + }; + const command = "newhandle"; + const result = { + response: respBody, + responseRequired: true + }; + + session.addProtocolHandler(command, (req) => result); + + expect(session.executeCommand({ + command, + seq: 0, + type: "command" + })).to.deep.equal(result); + }); + it("throws when a duplicate handler is passed", () => { + const respBody = { + item: false + }; + const resp = { + response: respBody, + responseRequired: true + }; + const command = "newhandle"; + + session.addProtocolHandler(command, (req) => resp); + + expect(() => session.addProtocolHandler(command, (req) => resp)) + .to.throw(`Protocol handler already exists for command "${command}"`); + }); + }); + + describe("event", () => { + it("can format event responses and send them", () => { + const evt = "notify-test"; + const info = { + test: true + }; + + session.event(info, evt); + + expect(lastSent).to.deep.equal({ + type: "event", + seq: 0, + event: evt, + body: info + }); + }); + }); + + describe("output", () => { + it("can format command responses and send them", () => { + const body = { + block: { + key: "value" + } + }; + const command = "test"; + + session.output(body, command); + + expect(lastSent).to.deep.equal({ + seq: 0, + request_seq: 0, + type: "response", + command, + body: body, + success: true + }); + }); + }); + }); + + describe("how Session is extendable via subclassing", () => { + class TestSession extends Session { + lastSent: protocol.Message; + customHandler = "testhandler"; + constructor() { + super(mockHost, Buffer.byteLength, process.hrtime, mockLogger); + this.addProtocolHandler(this.customHandler, () => { + return {response: undefined, responseRequired: true}; + }); + } + send(msg: protocol.Message) { + this.lastSent = msg; + } + }; + + it("can override methods such as send", () => { + const session = new TestSession(); + const body = { + block: { + key: "value" + } + }; + const command = "test"; + + session.output(body, command); + + expect(session.lastSent).to.deep.equal({ + seq: 0, + request_seq: 0, + type: "response", + command, + body: body, + success: true + }); + }); + it("can add and respond to new protocol handlers", () => { + const session = new TestSession(); + + expect(session.executeCommand({ + seq: 0, + type: "command", + command: session.customHandler + })).to.deep.equal({ + response: undefined, + responseRequired: true + }); + }); + it("has access to the project service", () => { + class ServiceSession extends TestSession { + constructor() { + super(); + assert(this.projectService); + expect(this.projectService).to.be.instanceOf(ProjectService); + } + }; + new ServiceSession(); + }); + }); + + describe("an example of using the Session API to create an in-process server", () => { + class InProcSession extends Session { + private queue: protocol.Request[] = []; + constructor(private client: InProcClient) { + super(mockHost, Buffer.byteLength, process.hrtime, mockLogger); + this.addProtocolHandler("echo", (req: protocol.Request) => ({ + response: req.arguments, + responseRequired: true + })); + } + + send(msg: protocol.Message) { + this.client.handle(msg); + } + + enqueue(msg: protocol.Request) { + this.queue.unshift(msg); + } + + handleRequest(msg: protocol.Request) { + let response: protocol.Response; + try { + ({response} = this.executeCommand(msg)); + } + catch (e) { + this.output(undefined, msg.command, msg.seq, e.toString()); + return; + } + if (response) { + this.output(response, msg.command, msg.seq); + } + } + + consumeQueue() { + while (this.queue.length > 0) { + const elem = this.queue.pop(); + this.handleRequest(elem); + } + } + } + + class InProcClient { + private server: InProcSession; + private seq = 0; + private callbacks: ts.Map<(resp: protocol.Response) => void> = {}; + private eventHandlers: ts.Map<(args: any) => void> = {}; + + handle(msg: protocol.Message): void { + if (msg.type === "response") { + const response = msg; + if (this.callbacks[response.request_seq]) { + this.callbacks[response.request_seq](response); + delete this.callbacks[response.request_seq]; + } + } + else if (msg.type === "event") { + const event = msg; + this.emit(event.event, event.body); + } + } + + emit(name: string, args: any): void { + if (this.eventHandlers[name]) { + this.eventHandlers[name](args); + } + } + + on(name: string, handler: (args: any) => void): void { + this.eventHandlers[name] = handler; + } + + connect(session: InProcSession): void { + this.server = session; + } + + execute(command: string, args: any, callback: (resp: protocol.Response) => void): void { + if (!this.server) { + return; + } + this.seq++; + this.server.enqueue({ + seq: this.seq, + type: "command", + command, + arguments: args + }); + this.callbacks[this.seq] = callback; + } + }; + + it("can be constructed and respond to commands", (done) => { + const cli = new InProcClient(); + const session = new InProcSession(cli); + const toEcho = { + data: true + }; + const toEvent = { + data: false + }; + let responses = 0; + + // Connect the client + cli.connect(session); + + // Add an event handler + cli.on("testevent", (eventinfo) => { + expect(eventinfo).to.equal(toEvent); + responses++; + expect(responses).to.equal(1); + }); + + // Trigger said event from the server + session.event(toEvent, "testevent"); + + // Queue an echo command + cli.execute("echo", toEcho, (resp) => { + assert(resp.success, resp.message); + responses++; + expect(responses).to.equal(2); + expect(resp.body).to.deep.equal(toEcho); + }); + + // Queue a configure command + cli.execute("configure", { + hostInfo: "unit test", + formatOptions: { + newLineCharacter: "`n" + } + }, (resp) => { + assert(resp.success, resp.message); + responses++; + expect(responses).to.equal(3); + done(); + }); + + // Consume the queue and trigger the callbacks + session.consumeQueue(); + }); + }); +} \ No newline at end of file