• 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                    {
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                    } as CompilerOptions);
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                CommandNames.ProvideInlayHints
282            ];
283
284            it("should not throw when commands are executed with invalid arguments", () => {
285                let i = 0;
286                for (const name of allCommandNames) {
287                    const req: protocol.Request = {
288                        command: name,
289                        seq: i,
290                        type: "request"
291                    };
292                    i++;
293                    session.onMessage(JSON.stringify(req));
294                    req.seq = i;
295                    i++;
296                    req.arguments = {};
297                    session.onMessage(JSON.stringify(req));
298                    req.seq = i;
299                    i++;
300                    req.arguments = null; // eslint-disable-line no-null/no-null
301                    session.onMessage(JSON.stringify(req));
302                    req.seq = i;
303                    i++;
304                    req.arguments = "";
305                    session.onMessage(JSON.stringify(req));
306                    req.seq = i;
307                    i++;
308                    req.arguments = 0;
309                    session.onMessage(JSON.stringify(req));
310                    req.seq = i;
311                    i++;
312                    req.arguments = [];
313                    session.onMessage(JSON.stringify(req));
314                }
315                session.onMessage("GARBAGE NON_JSON DATA");
316            });
317            it("should output the response for a correctly handled message", () => {
318                const req: protocol.ConfigureRequest = {
319                    command: CommandNames.Configure,
320                    seq: 0,
321                    type: "request",
322                    arguments: {
323                        hostInfo: "unit test",
324                        formatOptions: {
325                            newLineCharacter: "`n"
326                        }
327                    }
328                };
329
330                session.onMessage(JSON.stringify(req));
331
332                expect(lastSent).to.deep.equal({
333                    command: CommandNames.Configure,
334                    type: "response",
335                    success: true,
336                    request_seq: 0,
337                    seq: 0,
338                    body: undefined,
339                    performanceData: undefined,
340                } as protocol.ConfigureResponse);
341            });
342        });
343
344        describe("send", () => {
345            it("is an overrideable handle which sends protocol messages over the wire", () => {
346                const msg: protocol.Request = { seq: 0, type: "request", command: "" };
347                const strmsg = JSON.stringify(msg);
348                const len = 1 + Utils.byteLength(strmsg, "utf8");
349                const resultMsg = `Content-Length: ${len}\r\n\r\n${strmsg}\n`;
350
351                session.send = Session.prototype.send;
352                assert(session.send);
353                expect(session.send(msg)).to.not.exist; // eslint-disable-line @typescript-eslint/no-unused-expressions
354                expect(lastWrittenToHost).to.equal(resultMsg);
355            });
356        });
357
358        describe("addProtocolHandler", () => {
359            it("can add protocol handlers", () => {
360                const respBody = {
361                    item: false
362                };
363                const command = "newhandle";
364                const result: HandlerResponse = {
365                    response: respBody,
366                    responseRequired: true
367                };
368
369                session.addProtocolHandler(command, () => result);
370
371                expect(session.executeCommand({
372                    command,
373                    seq: 0,
374                    type: "request"
375                })).to.deep.equal(result);
376            });
377            it("throws when a duplicate handler is passed", () => {
378                const respBody = {
379                    item: false
380                };
381                const resp: HandlerResponse = {
382                    response: respBody,
383                    responseRequired: true
384                };
385                const command = "newhandle";
386
387                session.addProtocolHandler(command, () => resp);
388
389                expect(() => session.addProtocolHandler(command, () => resp))
390                    .to.throw(`Protocol handler already exists for command "${command}"`);
391            });
392        });
393
394        describe("event", () => {
395            it("can format event responses and send them", () => {
396                const evt = "notify-test";
397                const info = {
398                    test: true
399                };
400
401                session.event(info, evt);
402
403                expect(lastSent).to.deep.equal({
404                    type: "event",
405                    seq: 0,
406                    event: evt,
407                    body: info
408                });
409            });
410        });
411
412        describe("output", () => {
413            it("can format command responses and send them", () => {
414                const body = {
415                    block: {
416                        key: "value"
417                    }
418                };
419                const command = "test";
420
421                session.output(body, command, /*reqSeq*/ 0);
422
423                expect(lastSent).to.deep.equal({
424                    seq: 0,
425                    request_seq: 0,
426                    type: "response",
427                    command,
428                    body,
429                    success: true,
430                    performanceData: undefined,
431                });
432            });
433        });
434    });
435
436    describe("unittests:: tsserver:: Session:: exceptions", () => {
437
438        // 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
439        let oldPrepare: AnyFunction;
440        let oldStackTraceLimit: number;
441        before(() => {
442            oldStackTraceLimit = (Error as any).stackTraceLimit;
443            oldPrepare = (Error as any).prepareStackTrace;
444            delete (Error as any).prepareStackTrace;
445            (Error as any).stackTraceLimit = 10;
446        });
447
448        after(() => {
449            (Error as any).prepareStackTrace = oldPrepare;
450            (Error as any).stackTraceLimit = oldStackTraceLimit;
451        });
452
453        const command = "testhandler";
454        class TestSession extends Session {
455            lastSent: protocol.Message | undefined;
456            private exceptionRaisingHandler(_request: protocol.Request): { response?: any, responseRequired: boolean } {
457                f1();
458                return Debug.fail(); // unreachable, throw to make compiler happy
459                function f1() {
460                    throw new Error("myMessage");
461                }
462            }
463
464            constructor() {
465                super({
466                    host: mockHost,
467                    cancellationToken: nullCancellationToken,
468                    useSingleInferredProject: false,
469                    useInferredProjectPerProjectRoot: false,
470                    typingsInstaller: undefined!, // TODO: GH#18217
471                    byteLength: Utils.byteLength,
472                    hrtime: process.hrtime,
473                    logger: projectSystem.nullLogger(),
474                    canUseEvents: true
475                });
476                this.addProtocolHandler(command, this.exceptionRaisingHandler);
477            }
478            send(msg: protocol.Message) {
479                this.lastSent = msg;
480            }
481        }
482
483        it("raised in a protocol handler generate an event", () => {
484
485            const session = new TestSession();
486
487            const request = {
488                command,
489                seq: 0,
490                type: "request"
491            };
492
493            session.onMessage(JSON.stringify(request));
494            const lastSent = session.lastSent as protocol.Response;
495
496            expect(lastSent).to.contain({
497                seq: 0,
498                type: "response",
499                command,
500                success: false
501            });
502
503            expect(lastSent.message).has.string("myMessage").and.has.string("f1");
504        });
505    });
506
507    describe("unittests:: tsserver:: Session:: how Session is extendable via subclassing", () => {
508        class TestSession extends Session {
509            lastSent: protocol.Message | undefined;
510            customHandler = "testhandler";
511            constructor() {
512                super({
513                    host: mockHost,
514                    cancellationToken: nullCancellationToken,
515                    useSingleInferredProject: false,
516                    useInferredProjectPerProjectRoot: false,
517                    typingsInstaller: undefined!, // TODO: GH#18217
518                    byteLength: Utils.byteLength,
519                    hrtime: process.hrtime,
520                    logger: projectSystem.createHasErrorMessageLogger(),
521                    canUseEvents: true
522                });
523                this.addProtocolHandler(this.customHandler, () => {
524                    return { response: undefined, responseRequired: true };
525                });
526            }
527            send(msg: protocol.Message) {
528                this.lastSent = msg;
529            }
530        }
531
532        it("can override methods such as send", () => {
533            const session = new TestSession();
534            const body = {
535                block: {
536                    key: "value"
537                }
538            };
539            const command = "test";
540
541            session.output(body, command, /*reqSeq*/ 0);
542
543            expect(session.lastSent).to.deep.equal({
544                seq: 0,
545                request_seq: 0,
546                type: "response",
547                command,
548                body,
549                success: true,
550                performanceData: undefined,
551            });
552        });
553        it("can add and respond to new protocol handlers", () => {
554            const session = new TestSession();
555
556            expect(session.executeCommand({
557                seq: 0,
558                type: "request",
559                command: session.customHandler
560            })).to.deep.equal({
561                response: undefined,
562                responseRequired: true
563            });
564        });
565        it("has access to the project service", () => {
566            new class extends TestSession {
567                constructor() {
568                    super();
569                    assert(this.projectService);
570                    expect(this.projectService).to.be.instanceOf(ProjectService);
571                }
572            }();
573        });
574    });
575
576    describe("unittests:: tsserver:: Session:: an example of using the Session API to create an in-process server", () => {
577        class InProcSession extends Session {
578            private queue: protocol.Request[] = [];
579            constructor(private client: InProcClient) {
580                super({
581                    host: mockHost,
582                    cancellationToken: nullCancellationToken,
583                    useSingleInferredProject: false,
584                    useInferredProjectPerProjectRoot: false,
585                    typingsInstaller: undefined!, // TODO: GH#18217
586                    byteLength: Utils.byteLength,
587                    hrtime: process.hrtime,
588                    logger: projectSystem.createHasErrorMessageLogger(),
589                    canUseEvents: true
590                });
591                this.addProtocolHandler("echo", (req: protocol.Request) => ({
592                    response: req.arguments,
593                    responseRequired: true
594                }));
595            }
596
597            send(msg: protocol.Message) {
598                this.client.handle(msg);
599            }
600
601            enqueue(msg: protocol.Request) {
602                this.queue.unshift(msg);
603            }
604
605            handleRequest(msg: protocol.Request) {
606                let response: protocol.Response;
607                try {
608                    response = this.executeCommand(msg).response as protocol.Response;
609                }
610                catch (e) {
611                    this.output(undefined, msg.command, msg.seq, e.toString());
612                    return;
613                }
614                if (response) {
615                    this.output(response, msg.command, msg.seq);
616                }
617            }
618
619            consumeQueue() {
620                while (this.queue.length > 0) {
621                    const elem = this.queue.pop()!;
622                    this.handleRequest(elem);
623                }
624            }
625        }
626
627        class InProcClient {
628            private server: InProcSession | undefined;
629            private seq = 0;
630            private callbacks: ((resp: protocol.Response) => void)[] = [];
631            private eventHandlers = new Map<string, (args: any) => void>();
632
633            handle(msg: protocol.Message): void {
634                if (msg.type === "response") {
635                    const response = msg as protocol.Response;
636                    const handler = this.callbacks[response.request_seq];
637                    if (handler) {
638                        handler(response);
639                        delete this.callbacks[response.request_seq];
640                    }
641                }
642                else if (msg.type === "event") {
643                    const event = msg as protocol.Event;
644                    this.emit(event.event, event.body);
645                }
646            }
647
648            emit(name: string, args: any): void {
649                const handler = this.eventHandlers.get(name);
650                if (handler) {
651                    handler(args);
652                }
653            }
654
655            on(name: string, handler: (args: any) => void): void {
656                this.eventHandlers.set(name, handler);
657            }
658
659            connect(session: InProcSession): void {
660                this.server = session;
661            }
662
663            execute(command: string, args: any, callback: (resp: protocol.Response) => void): void {
664                if (!this.server) {
665                    return;
666                }
667                this.seq++;
668                this.server.enqueue({
669                    seq: this.seq,
670                    type: "request",
671                    command,
672                    arguments: args
673                });
674                this.callbacks[this.seq] = callback;
675            }
676        }
677
678        it("can be constructed and respond to commands", (done) => {
679            const cli = new InProcClient();
680            const session = new InProcSession(cli);
681            const toEcho = {
682                data: true
683            };
684            const toEvent = {
685                data: false
686            };
687            let responses = 0;
688
689            // Connect the client
690            cli.connect(session);
691
692            // Add an event handler
693            cli.on("testevent", (eventinfo) => {
694                expect(eventinfo).to.equal(toEvent);
695                responses++;
696                expect(responses).to.equal(1);
697            });
698
699            // Trigger said event from the server
700            session.event(toEvent, "testevent");
701
702            // Queue an echo command
703            cli.execute("echo", toEcho, (resp) => {
704                assert(resp.success, resp.message);
705                responses++;
706                expect(responses).to.equal(2);
707                expect(resp.body).to.deep.equal(toEcho);
708            });
709
710            // Queue a configure command
711            cli.execute("configure", {
712                hostInfo: "unit test",
713                formatOptions: {
714                    newLineCharacter: "`n"
715                }
716            }, (resp) => {
717                assert(resp.success, resp.message);
718                responses++;
719                expect(responses).to.equal(3);
720                done();
721            });
722
723            // Consume the queue and trigger the callbacks
724            session.consumeQueue();
725        });
726    });
727
728    describe("unittests:: tsserver:: Session:: helpers", () => {
729        it(getLocationInNewDocument.name, () => {
730            const text = `// blank line\nconst x = 0;`;
731            const renameLocationInOldText = text.indexOf("0");
732            const fileName = "/a.ts";
733            const edits: FileTextChanges = {
734                fileName,
735                textChanges: [
736                    {
737                        span: { start: 0, length: 0 },
738                        newText: "const newLocal = 0;\n\n",
739                    },
740                    {
741                        span: { start: renameLocationInOldText, length: 1 },
742                        newText: "newLocal",
743                    },
744                ],
745            };
746            const renameLocationInNewText = renameLocationInOldText + edits.textChanges[0].newText.length;
747            const res = getLocationInNewDocument(text, fileName, renameLocationInNewText, [edits]);
748            assert.deepEqual(res, { line: 4, offset: 11 });
749        });
750    });
751}
752