• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1namespace ts.server {
2    const _chai: typeof import("chai") = require("chai");
3    const expect: typeof _chai.expect = _chai.expect;
4    let lastWrittenToHost: string;
5    const noopFileWatcher: FileWatcher = { close: noop };
6    const mockHost: ServerHost = {
7        args: [],
8        newLine: "\n",
9        useCaseSensitiveFileNames: true,
10        write(s): void { lastWrittenToHost = s; },
11        readFile: returnUndefined,
12        writeFile: noop,
13        resolvePath(): string { return undefined!; }, // TODO: GH#18217
14        fileExists: () => false,
15        directoryExists: () => false,
16        getDirectories: () => [],
17        createDirectory: noop,
18        getExecutingFilePath(): string { return ""; },
19        getCurrentDirectory(): string { return ""; },
20        getEnvironmentVariable(): string { return ""; },
21        readDirectory() { return []; },
22        exit: noop,
23        setTimeout() { return 0; },
24        clearTimeout: noop,
25        setImmediate: () => 0,
26        clearImmediate: noop,
27        createHash: Harness.mockHash,
28        watchFile: () => noopFileWatcher,
29        watchDirectory: () => noopFileWatcher
30    };
31
32    class TestSession extends Session {
33        getProjectService() {
34            return this.projectService;
35        }
36    }
37
38    describe("unittests:: tsserver:: Session:: General functionality", () => {
39        let session: TestSession;
40        let lastSent: protocol.Message;
41
42        function createSession(): TestSession {
43            const opts: SessionOptions = {
44                host: mockHost,
45                cancellationToken: nullCancellationToken,
46                useSingleInferredProject: false,
47                useInferredProjectPerProjectRoot: false,
48                typingsInstaller: undefined!, // TODO: GH#18217
49                byteLength: Utils.byteLength,
50                hrtime: process.hrtime,
51                logger: projectSystem.nullLogger,
52                canUseEvents: true
53            };
54            return new TestSession(opts);
55        }
56
57        // Disable sourcemap support for the duration of the test, as sourcemapping the errors generated during this test is slow and not something we care to test
58        let oldPrepare: AnyFunction;
59        before(() => {
60            oldPrepare = (Error as any).prepareStackTrace;
61            delete (Error as any).prepareStackTrace;
62        });
63
64        after(() => {
65            (Error as any).prepareStackTrace = oldPrepare;
66        });
67
68        beforeEach(() => {
69            session = createSession();
70            session.send = (msg: protocol.Message) => {
71                lastSent = msg;
72            };
73        });
74
75        describe("executeCommand", () => {
76            it("should throw when commands are executed with invalid arguments", () => {
77                const req: protocol.FileRequest = {
78                    command: CommandNames.Open,
79                    seq: 0,
80                    type: "request",
81                    arguments: {
82                        file: undefined! // TODO: GH#18217
83                    }
84                };
85
86                expect(() => session.executeCommand(req)).to.throw();
87            });
88            it("should output an error response when a command does not exist", () => {
89                const req: protocol.Request = {
90                    command: "foobar",
91                    seq: 0,
92                    type: "request"
93                };
94
95                session.executeCommand(req);
96
97                const expected: protocol.Response = {
98                    command: CommandNames.Unknown,
99                    type: "response",
100                    seq: 0,
101                    message: "Unrecognized JSON command: foobar",
102                    request_seq: 0,
103                    success: false,
104                    performanceData: undefined,
105                };
106                expect(lastSent).to.deep.equal(expected);
107            });
108            it("should return a tuple containing the response and if a response is required on success", () => {
109                const req: protocol.ConfigureRequest = {
110                    command: CommandNames.Configure,
111                    seq: 0,
112                    type: "request",
113                    arguments: {
114                        hostInfo: "unit test",
115                        formatOptions: {
116                            newLineCharacter: "`n"
117                        }
118                    }
119                };
120
121                expect(session.executeCommand(req)).to.deep.equal({
122                    responseRequired: false
123                });
124                expect(lastSent).to.deep.equal({
125                    command: CommandNames.Configure,
126                    type: "response",
127                    success: true,
128                    request_seq: 0,
129                    seq: 0,
130                    body: undefined,
131                    performanceData: undefined,
132                });
133            });
134            it("should handle literal types in request", () => {
135                const configureRequest: protocol.ConfigureRequest = {
136                    command: CommandNames.Configure,
137                    seq: 0,
138                    type: "request",
139                    arguments: {
140                        formatOptions: {
141                            indentStyle: protocol.IndentStyle.Block,
142                        }
143                    }
144                };
145
146                session.onMessage(JSON.stringify(configureRequest));
147
148                assert.equal(session.getProjectService().getFormatCodeOptions("" as NormalizedPath).indentStyle, IndentStyle.Block);
149
150                const setOptionsRequest: protocol.SetCompilerOptionsForInferredProjectsRequest = {
151                    command: CommandNames.CompilerOptionsForInferredProjects,
152                    seq: 1,
153                    type: "request",
154                    arguments: {
155                        options: {
156                            module: protocol.ModuleKind.System,
157                            target: protocol.ScriptTarget.ES5,
158                            jsx: protocol.JsxEmit.React,
159                            newLine: protocol.NewLineKind.Lf,
160                            moduleResolution: protocol.ModuleResolutionKind.Node,
161                        }
162                    }
163                };
164                session.onMessage(JSON.stringify(setOptionsRequest));
165                assert.deepEqual(
166                    session.getProjectService().getCompilerOptionsForInferredProjects(),
167                    <CompilerOptions>{
168                        module: ModuleKind.System,
169                        target: ScriptTarget.ES5,
170                        jsx: JsxEmit.React,
171                        newLine: NewLineKind.LineFeed,
172                        moduleResolution: ModuleResolutionKind.NodeJs,
173                        allowNonTsExtensions: true // injected by tsserver
174                    });
175            });
176
177            it("Status request gives ts.version", () => {
178                const req: protocol.StatusRequest = {
179                    command: CommandNames.Status,
180                    seq: 0,
181                    type: "request"
182                };
183
184                const expected: protocol.StatusResponseBody = {
185                    version: ts.version, // eslint-disable-line @typescript-eslint/no-unnecessary-qualifier
186                };
187                assert.deepEqual(session.executeCommand(req).response, expected);
188            });
189        });
190
191        describe("onMessage", () => {
192            const allCommandNames: CommandNames[] = [
193                CommandNames.Brace,
194                CommandNames.BraceFull,
195                CommandNames.BraceCompletion,
196                CommandNames.Change,
197                CommandNames.Close,
198                CommandNames.Completions,
199                CommandNames.CompletionsFull,
200                CommandNames.CompletionDetails,
201                CommandNames.CompileOnSaveAffectedFileList,
202                CommandNames.Configure,
203                CommandNames.Definition,
204                CommandNames.DefinitionFull,
205                CommandNames.DefinitionAndBoundSpan,
206                CommandNames.DefinitionAndBoundSpanFull,
207                CommandNames.Implementation,
208                CommandNames.ImplementationFull,
209                CommandNames.Exit,
210                CommandNames.FileReferences,
211                CommandNames.FileReferencesFull,
212                CommandNames.Format,
213                CommandNames.Formatonkey,
214                CommandNames.FormatFull,
215                CommandNames.FormatonkeyFull,
216                CommandNames.FormatRangeFull,
217                CommandNames.Geterr,
218                CommandNames.GeterrForProject,
219                CommandNames.SemanticDiagnosticsSync,
220                CommandNames.SyntacticDiagnosticsSync,
221                CommandNames.SuggestionDiagnosticsSync,
222                CommandNames.NavBar,
223                CommandNames.NavBarFull,
224                CommandNames.Navto,
225                CommandNames.NavtoFull,
226                CommandNames.NavTree,
227                CommandNames.NavTreeFull,
228                CommandNames.Occurrences,
229                CommandNames.DocumentHighlights,
230                CommandNames.DocumentHighlightsFull,
231                CommandNames.JsxClosingTag,
232                CommandNames.Open,
233                CommandNames.Quickinfo,
234                CommandNames.QuickinfoFull,
235                CommandNames.References,
236                CommandNames.ReferencesFull,
237                CommandNames.Reload,
238                CommandNames.Rename,
239                CommandNames.RenameInfoFull,
240                CommandNames.RenameLocationsFull,
241                CommandNames.Saveto,
242                CommandNames.SignatureHelp,
243                CommandNames.SignatureHelpFull,
244                CommandNames.Status,
245                CommandNames.TypeDefinition,
246                CommandNames.ProjectInfo,
247                CommandNames.ReloadProjects,
248                CommandNames.Unknown,
249                CommandNames.OpenExternalProject,
250                CommandNames.CloseExternalProject,
251                CommandNames.SynchronizeProjectList,
252                CommandNames.ApplyChangedToOpenFiles,
253                CommandNames.EncodedSemanticClassificationsFull,
254                CommandNames.Cleanup,
255                CommandNames.OutliningSpans,
256                CommandNames.TodoComments,
257                CommandNames.Indentation,
258                CommandNames.DocCommentTemplate,
259                CommandNames.CompilerOptionsDiagnosticsFull,
260                CommandNames.NameOrDottedNameSpan,
261                CommandNames.BreakpointStatement,
262                CommandNames.CompilerOptionsForInferredProjects,
263                CommandNames.GetCodeFixes,
264                CommandNames.GetCodeFixesFull,
265                CommandNames.GetSupportedCodeFixes,
266                CommandNames.GetApplicableRefactors,
267                CommandNames.GetEditsForRefactor,
268                CommandNames.GetEditsForRefactorFull,
269                CommandNames.OrganizeImports,
270                CommandNames.OrganizeImportsFull,
271                CommandNames.GetEditsForFileRename,
272                CommandNames.GetEditsForFileRenameFull,
273                CommandNames.SelectionRange,
274                CommandNames.PrepareCallHierarchy,
275                CommandNames.ProvideCallHierarchyIncomingCalls,
276                CommandNames.ProvideCallHierarchyOutgoingCalls,
277                CommandNames.ToggleLineComment,
278                CommandNames.ToggleMultilineComment,
279                CommandNames.CommentSelection,
280                CommandNames.UncommentSelection,
281            ];
282
283            it("should not throw when commands are executed with invalid arguments", () => {
284                let i = 0;
285                for (const name of allCommandNames) {
286                    const req: protocol.Request = {
287                        command: name,
288                        seq: i,
289                        type: "request"
290                    };
291                    i++;
292                    session.onMessage(JSON.stringify(req));
293                    req.seq = i;
294                    i++;
295                    req.arguments = {};
296                    session.onMessage(JSON.stringify(req));
297                    req.seq = i;
298                    i++;
299                    req.arguments = null; // eslint-disable-line no-null/no-null
300                    session.onMessage(JSON.stringify(req));
301                    req.seq = i;
302                    i++;
303                    req.arguments = "";
304                    session.onMessage(JSON.stringify(req));
305                    req.seq = i;
306                    i++;
307                    req.arguments = 0;
308                    session.onMessage(JSON.stringify(req));
309                    req.seq = i;
310                    i++;
311                    req.arguments = [];
312                    session.onMessage(JSON.stringify(req));
313                }
314                session.onMessage("GARBAGE NON_JSON DATA");
315            });
316            it("should output the response for a correctly handled message", () => {
317                const req: protocol.ConfigureRequest = {
318                    command: CommandNames.Configure,
319                    seq: 0,
320                    type: "request",
321                    arguments: {
322                        hostInfo: "unit test",
323                        formatOptions: {
324                            newLineCharacter: "`n"
325                        }
326                    }
327                };
328
329                session.onMessage(JSON.stringify(req));
330
331                expect(lastSent).to.deep.equal(<protocol.ConfigureResponse>{
332                    command: CommandNames.Configure,
333                    type: "response",
334                    success: true,
335                    request_seq: 0,
336                    seq: 0,
337                    body: undefined,
338                    performanceData: undefined,
339                });
340            });
341        });
342
343        describe("send", () => {
344            it("is an overrideable handle which sends protocol messages over the wire", () => {
345                const msg: protocol.Request = { seq: 0, type: "request", command: "" };
346                const strmsg = JSON.stringify(msg);
347                const len = 1 + Utils.byteLength(strmsg, "utf8");
348                const resultMsg = `Content-Length: ${len}\r\n\r\n${strmsg}\n`;
349
350                session.send = Session.prototype.send;
351                assert(session.send);
352                expect(session.send(msg)).to.not.exist; // eslint-disable-line @typescript-eslint/no-unused-expressions
353                expect(lastWrittenToHost).to.equal(resultMsg);
354            });
355        });
356
357        describe("addProtocolHandler", () => {
358            it("can add protocol handlers", () => {
359                const respBody = {
360                    item: false
361                };
362                const command = "newhandle";
363                const result: HandlerResponse = {
364                    response: respBody,
365                    responseRequired: true
366                };
367
368                session.addProtocolHandler(command, () => result);
369
370                expect(session.executeCommand({
371                    command,
372                    seq: 0,
373                    type: "request"
374                })).to.deep.equal(result);
375            });
376            it("throws when a duplicate handler is passed", () => {
377                const respBody = {
378                    item: false
379                };
380                const resp: HandlerResponse = {
381                    response: respBody,
382                    responseRequired: true
383                };
384                const command = "newhandle";
385
386                session.addProtocolHandler(command, () => resp);
387
388                expect(() => session.addProtocolHandler(command, () => resp))
389                    .to.throw(`Protocol handler already exists for command "${command}"`);
390            });
391        });
392
393        describe("event", () => {
394            it("can format event responses and send them", () => {
395                const evt = "notify-test";
396                const info = {
397                    test: true
398                };
399
400                session.event(info, evt);
401
402                expect(lastSent).to.deep.equal({
403                    type: "event",
404                    seq: 0,
405                    event: evt,
406                    body: info
407                });
408            });
409        });
410
411        describe("output", () => {
412            it("can format command responses and send them", () => {
413                const body = {
414                    block: {
415                        key: "value"
416                    }
417                };
418                const command = "test";
419
420                session.output(body, command, /*reqSeq*/ 0);
421
422                expect(lastSent).to.deep.equal({
423                    seq: 0,
424                    request_seq: 0,
425                    type: "response",
426                    command,
427                    body,
428                    success: true,
429                    performanceData: undefined,
430                });
431            });
432        });
433    });
434
435    describe("unittests:: tsserver:: Session:: exceptions", () => {
436
437        // Disable sourcemap support for the duration of the test, as sourcemapping the errors generated during this test is slow and not something we care to test
438        let oldPrepare: AnyFunction;
439        let oldStackTraceLimit: number;
440        before(() => {
441            oldStackTraceLimit = (Error as any).stackTraceLimit;
442            oldPrepare = (Error as any).prepareStackTrace;
443            delete (Error as any).prepareStackTrace;
444            (Error as any).stackTraceLimit = 10;
445        });
446
447        after(() => {
448            (Error as any).prepareStackTrace = oldPrepare;
449            (Error as any).stackTraceLimit = oldStackTraceLimit;
450        });
451
452        const command = "testhandler";
453        class TestSession extends Session {
454            lastSent: protocol.Message | undefined;
455            private exceptionRaisingHandler(_request: protocol.Request): { response?: any, responseRequired: boolean } {
456                f1();
457                return Debug.fail(); // unreachable, throw to make compiler happy
458                function f1() {
459                    throw new Error("myMessage");
460                }
461            }
462
463            constructor() {
464                super({
465                    host: mockHost,
466                    cancellationToken: nullCancellationToken,
467                    useSingleInferredProject: false,
468                    useInferredProjectPerProjectRoot: false,
469                    typingsInstaller: undefined!, // TODO: GH#18217
470                    byteLength: Utils.byteLength,
471                    hrtime: process.hrtime,
472                    logger: projectSystem.nullLogger,
473                    canUseEvents: true
474                });
475                this.addProtocolHandler(command, this.exceptionRaisingHandler);
476            }
477            send(msg: protocol.Message) {
478                this.lastSent = msg;
479            }
480        }
481
482        it("raised in a protocol handler generate an event", () => {
483
484            const session = new TestSession();
485
486            const request = {
487                command,
488                seq: 0,
489                type: "request"
490            };
491
492            session.onMessage(JSON.stringify(request));
493            const lastSent = session.lastSent as protocol.Response;
494
495            expect(lastSent).to.contain({
496                seq: 0,
497                type: "response",
498                command,
499                success: false
500            });
501
502            expect(lastSent.message).has.string("myMessage").and.has.string("f1");
503        });
504    });
505
506    describe("unittests:: tsserver:: Session:: how Session is extendable via subclassing", () => {
507        class TestSession extends Session {
508            lastSent: protocol.Message | undefined;
509            customHandler = "testhandler";
510            constructor() {
511                super({
512                    host: mockHost,
513                    cancellationToken: nullCancellationToken,
514                    useSingleInferredProject: false,
515                    useInferredProjectPerProjectRoot: false,
516                    typingsInstaller: undefined!, // TODO: GH#18217
517                    byteLength: Utils.byteLength,
518                    hrtime: process.hrtime,
519                    logger: projectSystem.createHasErrorMessageLogger().logger,
520                    canUseEvents: true
521                });
522                this.addProtocolHandler(this.customHandler, () => {
523                    return { response: undefined, responseRequired: true };
524                });
525            }
526            send(msg: protocol.Message) {
527                this.lastSent = msg;
528            }
529        }
530
531        it("can override methods such as send", () => {
532            const session = new TestSession();
533            const body = {
534                block: {
535                    key: "value"
536                }
537            };
538            const command = "test";
539
540            session.output(body, command, /*reqSeq*/ 0);
541
542            expect(session.lastSent).to.deep.equal({
543                seq: 0,
544                request_seq: 0,
545                type: "response",
546                command,
547                body,
548                success: true,
549                performanceData: undefined,
550            });
551        });
552        it("can add and respond to new protocol handlers", () => {
553            const session = new TestSession();
554
555            expect(session.executeCommand({
556                seq: 0,
557                type: "request",
558                command: session.customHandler
559            })).to.deep.equal({
560                response: undefined,
561                responseRequired: true
562            });
563        });
564        it("has access to the project service", () => {
565            new class extends TestSession {
566                constructor() {
567                    super();
568                    assert(this.projectService);
569                    expect(this.projectService).to.be.instanceOf(ProjectService);
570                }
571            }();
572        });
573    });
574
575    describe("unittests:: tsserver:: Session:: an example of using the Session API to create an in-process server", () => {
576        class InProcSession extends Session {
577            private queue: protocol.Request[] = [];
578            constructor(private client: InProcClient) {
579                super({
580                    host: mockHost,
581                    cancellationToken: nullCancellationToken,
582                    useSingleInferredProject: false,
583                    useInferredProjectPerProjectRoot: false,
584                    typingsInstaller: undefined!, // TODO: GH#18217
585                    byteLength: Utils.byteLength,
586                    hrtime: process.hrtime,
587                    logger: projectSystem.createHasErrorMessageLogger().logger,
588                    canUseEvents: true
589                });
590                this.addProtocolHandler("echo", (req: protocol.Request) => ({
591                    response: req.arguments,
592                    responseRequired: true
593                }));
594            }
595
596            send(msg: protocol.Message) {
597                this.client.handle(msg);
598            }
599
600            enqueue(msg: protocol.Request) {
601                this.queue.unshift(msg);
602            }
603
604            handleRequest(msg: protocol.Request) {
605                let response: protocol.Response;
606                try {
607                    response = this.executeCommand(msg).response as protocol.Response;
608                }
609                catch (e) {
610                    this.output(undefined, msg.command, msg.seq, e.toString());
611                    return;
612                }
613                if (response) {
614                    this.output(response, msg.command, msg.seq);
615                }
616            }
617
618            consumeQueue() {
619                while (this.queue.length > 0) {
620                    const elem = this.queue.pop()!;
621                    this.handleRequest(elem);
622                }
623            }
624        }
625
626        class InProcClient {
627            private server: InProcSession | undefined;
628            private seq = 0;
629            private callbacks: ((resp: protocol.Response) => void)[] = [];
630            private eventHandlers = new Map<string, (args: any) => void>();
631
632            handle(msg: protocol.Message): void {
633                if (msg.type === "response") {
634                    const response = <protocol.Response>msg;
635                    const handler = this.callbacks[response.request_seq];
636                    if (handler) {
637                        handler(response);
638                        delete this.callbacks[response.request_seq];
639                    }
640                }
641                else if (msg.type === "event") {
642                    const event = <protocol.Event>msg;
643                    this.emit(event.event, event.body);
644                }
645            }
646
647            emit(name: string, args: any): void {
648                const handler = this.eventHandlers.get(name);
649                if (handler) {
650                    handler(args);
651                }
652            }
653
654            on(name: string, handler: (args: any) => void): void {
655                this.eventHandlers.set(name, handler);
656            }
657
658            connect(session: InProcSession): void {
659                this.server = session;
660            }
661
662            execute(command: string, args: any, callback: (resp: protocol.Response) => void): void {
663                if (!this.server) {
664                    return;
665                }
666                this.seq++;
667                this.server.enqueue({
668                    seq: this.seq,
669                    type: "request",
670                    command,
671                    arguments: args
672                });
673                this.callbacks[this.seq] = callback;
674            }
675        }
676
677        it("can be constructed and respond to commands", (done) => {
678            const cli = new InProcClient();
679            const session = new InProcSession(cli);
680            const toEcho = {
681                data: true
682            };
683            const toEvent = {
684                data: false
685            };
686            let responses = 0;
687
688            // Connect the client
689            cli.connect(session);
690
691            // Add an event handler
692            cli.on("testevent", (eventinfo) => {
693                expect(eventinfo).to.equal(toEvent);
694                responses++;
695                expect(responses).to.equal(1);
696            });
697
698            // Trigger said event from the server
699            session.event(toEvent, "testevent");
700
701            // Queue an echo command
702            cli.execute("echo", toEcho, (resp) => {
703                assert(resp.success, resp.message);
704                responses++;
705                expect(responses).to.equal(2);
706                expect(resp.body).to.deep.equal(toEcho);
707            });
708
709            // Queue a configure command
710            cli.execute("configure", {
711                hostInfo: "unit test",
712                formatOptions: {
713                    newLineCharacter: "`n"
714                }
715            }, (resp) => {
716                assert(resp.success, resp.message);
717                responses++;
718                expect(responses).to.equal(3);
719                done();
720            });
721
722            // Consume the queue and trigger the callbacks
723            session.consumeQueue();
724        });
725    });
726
727    describe("unittests:: tsserver:: Session:: helpers", () => {
728        it(getLocationInNewDocument.name, () => {
729            const text = `// blank line\nconst x = 0;`;
730            const renameLocationInOldText = text.indexOf("0");
731            const fileName = "/a.ts";
732            const edits: FileTextChanges = {
733                fileName,
734                textChanges: [
735                    {
736                        span: { start: 0, length: 0 },
737                        newText: "const newLocal = 0;\n\n",
738                    },
739                    {
740                        span: { start: renameLocationInOldText, length: 1 },
741                        newText: "newLocal",
742                    },
743                ],
744            };
745            const renameLocationInNewText = renameLocationInOldText + edits.textChanges[0].newText.length;
746            const res = getLocationInNewDocument(text, fileName, renameLocationInNewText, [edits]);
747            assert.deepEqual(res, { line: 4, offset: 11 });
748        });
749    });
750}
751