diff --git a/docs/packages/TypeScript_ESTree.mdx b/docs/packages/TypeScript_ESTree.mdx index e8a27eb0f012..1ac35da35cd5 100644 --- a/docs/packages/TypeScript_ESTree.mdx +++ b/docs/packages/TypeScript_ESTree.mdx @@ -367,3 +367,7 @@ const { ast, services } = parseAndGenerateServices(code, { If you encounter a bug with the parser that you want to investigate, you can turn on the debug logging via setting the environment variable: `DEBUG=typescript-eslint:*`. I.e. in this repo you can run: `DEBUG=typescript-eslint:* yarn lint`. + +This will include TypeScript server logs. +To turn off these logs, include `-typescript-eslint:typescript-estree:tsserver:*` when setting the environment variable. +I.e. for this repo change to: `DEBUG='typescript-eslint:*,-typescript-eslint:typescript-estree:tsserver:*' yarn lint`. diff --git a/packages/typescript-estree/src/create-program/createProjectService.ts b/packages/typescript-estree/src/create-program/createProjectService.ts index 9f0f44dcec68..508505b0ef2d 100644 --- a/packages/typescript-estree/src/create-program/createProjectService.ts +++ b/packages/typescript-estree/src/create-program/createProjectService.ts @@ -1,6 +1,7 @@ /* eslint-disable @typescript-eslint/no-empty-function -- for TypeScript APIs*/ import os from 'node:os'; +import debug from 'debug'; import type * as ts from 'typescript/lib/tsserverlibrary'; import type { ProjectServiceOptions } from '../parser-options'; @@ -8,6 +9,20 @@ import { validateDefaultProjectForFilesGlob } from './validateDefaultProjectForF const DEFAULT_PROJECT_MATCHED_FILES_THRESHOLD = 8; +const log = debug('typescript-eslint:typescript-estree:createProjectService'); +const logTsserverErr = debug( + 'typescript-eslint:typescript-estree:tsserver:err', +); +const logTsserverInfo = debug( + 'typescript-eslint:typescript-estree:tsserver:info', +); +const logTsserverPerf = debug( + 'typescript-eslint:typescript-estree:tsserver:perf', +); +const logTsserverEvent = debug( + 'typescript-eslint:typescript-estree:tsserver:event', +); + const doNothing = (): void => {}; const createStubFileWatcher = (): ts.FileWatcher => ({ @@ -48,27 +63,60 @@ export function createProjectService( watchFile: createStubFileWatcher, }; + const logger: ts.server.Logger = { + close: doNothing, + endGroup: doNothing, + getLogFileName: (): undefined => undefined, + // The debug library doesn't use levels without creating a namespace for each. + // Log levels are not passed to the writer so we wouldn't be able to forward + // to a respective namespace. Supporting would require an additional flag for + // granular control. Defaulting to all levels for now. + hasLevel: (): boolean => true, + info(s) { + this.msg(s, tsserver.server.Msg.Info); + }, + loggingEnabled: (): boolean => + // if none of the debug namespaces are enabled, then don't enable logging in tsserver + logTsserverInfo.enabled || + logTsserverErr.enabled || + logTsserverPerf.enabled, + msg: (s, type) => { + switch (type) { + case tsserver.server.Msg.Err: + logTsserverErr(s); + break; + case tsserver.server.Msg.Perf: + logTsserverPerf(s); + break; + default: + logTsserverInfo(s); + } + }, + perftrc(s) { + this.msg(s, tsserver.server.Msg.Perf); + }, + startGroup: doNothing, + }; + + log('Creating project service with: %o', options); + const service = new tsserver.server.ProjectService({ host: system, cancellationToken: { isCancellationRequested: (): boolean => false }, useSingleInferredProject: false, useInferredProjectPerProjectRoot: false, - logger: { - close: doNothing, - endGroup: doNothing, - getLogFileName: (): undefined => undefined, - hasLevel: (): boolean => false, - info: doNothing, - loggingEnabled: (): boolean => false, - msg: doNothing, - perftrc: doNothing, - startGroup: doNothing, - }, + logger, + eventHandler: logTsserverEvent.enabled + ? (e): void => { + logTsserverEvent(e); + } + : undefined, session: undefined, jsDocParsingMode, }); if (options.defaultProject) { + log('Enabling default project: %s', options.defaultProject); let configRead; try { diff --git a/packages/typescript-estree/tests/lib/createProjectService.test.ts b/packages/typescript-estree/tests/lib/createProjectService.test.ts index 1159e8625f41..f380c960ff1a 100644 --- a/packages/typescript-estree/tests/lib/createProjectService.test.ts +++ b/packages/typescript-estree/tests/lib/createProjectService.test.ts @@ -1,3 +1,4 @@ +import debug from 'debug'; import * as ts from 'typescript'; import { createProjectService } from '../../src/create-program/createProjectService'; @@ -9,7 +10,21 @@ jest.mock('typescript/lib/tsserverlibrary', () => ({ ...jest.requireActual('typescript/lib/tsserverlibrary'), readConfigFile: mockReadConfigFile, server: { + ...jest.requireActual('typescript/lib/tsserverlibrary').server, ProjectService: class { + logger: ts.server.Logger; + eventHandler: ts.server.ProjectServiceEventHandler | undefined; + constructor( + ...args: ConstructorParameters + ) { + this.logger = args[0].logger; + this.eventHandler = args[0].eventHandler; + if (this.eventHandler) { + this.eventHandler({ + eventName: 'projectLoadingStart', + } as ts.server.ProjectLoadingStartEvent); + } + } setCompilerOptionsForInferredProjects = mockSetCompilerOptionsForInferredProjects; }, @@ -17,6 +32,10 @@ jest.mock('typescript/lib/tsserverlibrary', () => ({ })); describe('createProjectService', () => { + afterEach(() => { + jest.resetAllMocks(); + }); + it('sets allowDefaultProject when options.allowDefaultProject is defined', () => { const allowDefaultProject = ['./*.js']; const settings = createProjectService({ allowDefaultProject }, undefined); @@ -89,4 +108,119 @@ describe('createProjectService', () => { compilerOptions, ); }); + + it('uses the default projects error debugger for error messages when enabled', () => { + jest.spyOn(process.stderr, 'write').mockImplementation(); + const { service } = createProjectService(undefined, undefined); + debug.enable('typescript-eslint:typescript-estree:tsserver:err'); + const enabled = service.logger.loggingEnabled(); + service.logger.msg('foo', ts.server.Msg.Err); + debug.disable(); + + expect(enabled).toBe(true); + expect(process.stderr.write).toHaveBeenCalledWith( + expect.stringMatching( + /^.*typescript-eslint:typescript-estree:tsserver:err foo\n$/, + ), + ); + }); + + it('does not use the default projects error debugger for error messages when disabled', () => { + jest.spyOn(process.stderr, 'write').mockImplementation(); + const { service } = createProjectService(undefined, undefined); + const enabled = service.logger.loggingEnabled(); + service.logger.msg('foo', ts.server.Msg.Err); + + expect(enabled).toBe(false); + expect(process.stderr.write).toHaveBeenCalledTimes(0); + }); + + it('uses the default projects info debugger for info messages when enabled', () => { + jest.spyOn(process.stderr, 'write').mockImplementation(); + const { service } = createProjectService(undefined, undefined); + debug.enable('typescript-eslint:typescript-estree:tsserver:info'); + const enabled = service.logger.loggingEnabled(); + service.logger.info('foo'); + debug.disable(); + + expect(enabled).toBe(true); + expect(process.stderr.write).toHaveBeenCalledWith( + expect.stringMatching( + /^.*typescript-eslint:typescript-estree:tsserver:info foo\n$/, + ), + ); + }); + + it('does not use the default projects info debugger for info messages when disabled', () => { + jest.spyOn(process.stderr, 'write').mockImplementation(); + const { service } = createProjectService(undefined, undefined); + const enabled = service.logger.loggingEnabled(); + service.logger.info('foo'); + + expect(enabled).toBe(false); + expect(process.stderr.write).toHaveBeenCalledTimes(0); + }); + + it('uses the default projects perf debugger for perf messages when enabled', () => { + jest.spyOn(process.stderr, 'write').mockImplementation(); + const { service } = createProjectService(undefined, undefined); + debug.enable('typescript-eslint:typescript-estree:tsserver:perf'); + const enabled = service.logger.loggingEnabled(); + service.logger.perftrc('foo'); + debug.disable(); + + expect(enabled).toBe(true); + expect(process.stderr.write).toHaveBeenCalledWith( + expect.stringMatching( + /^.*typescript-eslint:typescript-estree:tsserver:perf foo\n$/, + ), + ); + }); + + it('does not use the default projects perf debugger for perf messages when disabled', () => { + jest.spyOn(process.stderr, 'write').mockImplementation(); + const { service } = createProjectService(undefined, undefined); + const enabled = service.logger.loggingEnabled(); + service.logger.perftrc('foo'); + + expect(enabled).toBe(false); + expect(process.stderr.write).toHaveBeenCalledTimes(0); + }); + + it('enables all log levels for the default projects logger', () => { + const { service } = createProjectService(undefined, undefined); + + expect(service.logger.hasLevel(ts.server.LogLevel.terse)).toBe(true); + expect(service.logger.hasLevel(ts.server.LogLevel.normal)).toBe(true); + expect(service.logger.hasLevel(ts.server.LogLevel.requestTime)).toBe(true); + expect(service.logger.hasLevel(ts.server.LogLevel.verbose)).toBe(true); + }); + + it('does not return a log filename with the default projects logger', () => { + const { service } = createProjectService(undefined, undefined); + + expect(service.logger.getLogFileName()).toBeUndefined(); + }); + + it('uses the default projects event debugger for event handling when enabled', () => { + jest.spyOn(process.stderr, 'write').mockImplementation(); + + debug.enable('typescript-eslint:typescript-estree:tsserver:event'); + createProjectService(undefined, undefined); + debug.disable(); + + expect(process.stderr.write).toHaveBeenCalledWith( + expect.stringMatching( + /^.*typescript-eslint:typescript-estree:tsserver:event { eventName: 'projectLoadingStart' }\n$/, + ), + ); + }); + + it('does not use the default projects event debugger for event handling when disabled', () => { + jest.spyOn(process.stderr, 'write').mockImplementation(); + + createProjectService(undefined, undefined); + + expect(process.stderr.write).toHaveBeenCalledTimes(0); + }); });